├── .gitignore ├── README.md ├── dist ├── index.html └── main.min.js ├── js ├── main.js ├── modules │ ├── Advection.js │ ├── Common.js │ ├── Controls.js │ ├── Divergence.js │ ├── ExternalForce.js │ ├── Mouse.js │ ├── Output.js │ ├── Poisson.js │ ├── Pressure.js │ ├── ShaderPass.js │ ├── Simulation.js │ ├── Viscous.js │ ├── WebGL.js │ └── glsl │ │ └── sim │ │ ├── advection.frag │ │ ├── color.frag │ │ ├── divergence.frag │ │ ├── externalForce.frag │ │ ├── face.vert │ │ ├── line.vert │ │ ├── mouse.vert │ │ ├── poisson.frag │ │ ├── pressure.frag │ │ └── viscous.frag └── utils │ ├── EventBus.js │ ├── math.js │ └── utils.js ├── package.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FLUID-THREE 2 | 3 | This source code and the demo are for a explanation in my blog post. 4 | 5 | 1. Clone this repository. 6 | 2. Install Node.js (v14.18.2) and yarn(v1.22.10) 7 | 3. Run following commands 8 | ``` 9 | yarn install 10 | yarn start 11 | ``` 12 | 13 | ## Demo 14 | https://mnmxmx.github.io/fluid-three/dist/ 15 | 16 | ## License 17 | MIT Licence 18 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fluid-three 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import EventBus from "./utils/EventBus"; 2 | window.EventBus = EventBus; 3 | import WebGL from "./modules/WebGL"; 4 | 5 | 6 | if(!window.isDev) window.isDev = false; 7 | 8 | const webglMng = new WebGL({ 9 | $wrapper: document.body 10 | }); 11 | -------------------------------------------------------------------------------- /js/modules/Advection.js: -------------------------------------------------------------------------------- 1 | import face_vert from "./glsl/sim/face.vert"; 2 | import line_vert from "./glsl/sim/line.vert"; 3 | 4 | import advection_frag from "./glsl/sim/advection.frag"; 5 | import ShaderPass from "./ShaderPass"; 6 | 7 | import * as THREE from "three"; 8 | 9 | 10 | export default class Advection extends ShaderPass{ 11 | constructor(simProps){ 12 | super({ 13 | material: { 14 | vertexShader: face_vert, 15 | fragmentShader: advection_frag, 16 | uniforms: { 17 | boundarySpace: { 18 | value: simProps.cellScale 19 | }, 20 | px: { 21 | value: simProps.cellScale 22 | }, 23 | fboSize: { 24 | value: simProps.fboSize 25 | }, 26 | velocity: { 27 | value: simProps.src.texture 28 | }, 29 | dt: { 30 | value: simProps.dt 31 | }, 32 | isBFECC: { 33 | value: true 34 | } 35 | }, 36 | }, 37 | output: simProps.dst 38 | }); 39 | 40 | this.init(); 41 | } 42 | 43 | init(){ 44 | super.init(); 45 | this.createBoundary(); 46 | } 47 | 48 | createBoundary(){ 49 | const boundaryG = new THREE.BufferGeometry(); 50 | const vertices_boundary = new Float32Array([ 51 | // left 52 | -1, -1, 0, 53 | -1, 1, 0, 54 | 55 | // top 56 | -1, 1, 0, 57 | 1, 1, 0, 58 | 59 | // right 60 | 1, 1, 0, 61 | 1, -1, 0, 62 | 63 | // bottom 64 | 1, -1, 0, 65 | -1, -1, 0 66 | ]); 67 | boundaryG.setAttribute( 'position', new THREE.BufferAttribute( vertices_boundary, 3 ) ); 68 | 69 | const boundaryM = new THREE.RawShaderMaterial({ 70 | vertexShader: line_vert, 71 | fragmentShader: advection_frag, 72 | uniforms: this.uniforms 73 | }); 74 | 75 | this.line = new THREE.LineSegments(boundaryG, boundaryM); 76 | this.scene.add(this.line); 77 | } 78 | 79 | update({ dt, isBounce, BFECC }){ 80 | 81 | this.uniforms.dt.value = dt; 82 | this.line.visible = isBounce; 83 | this.uniforms.isBFECC.value = BFECC; 84 | 85 | super.update(); 86 | } 87 | } -------------------------------------------------------------------------------- /js/modules/Common.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | class Common{ 4 | constructor(){ 5 | this.width = null; 6 | this.height = null; 7 | this.aspect = this.width / this.height; 8 | this.isMobile = false; 9 | this.breakpoint = 768; 10 | 11 | this.fboWidth = null; 12 | this.fboHeight = null; 13 | 14 | this.resizeFunc = this.resize.bind(this); 15 | 16 | this.time = 0; 17 | this.delta = 0; 18 | 19 | } 20 | 21 | init(){ 22 | this.pixelRatio = window.devicePixelRatio; 23 | 24 | this.resize(); 25 | 26 | this.renderer = new THREE.WebGLRenderer( { 27 | antialias: true, 28 | alpha: true, 29 | }); 30 | 31 | this.renderer.autoClear = false; 32 | 33 | this.renderer.setSize( this.width, this.height ); 34 | 35 | this.renderer.setClearColor( 0x000000 ); 36 | 37 | this.renderer.setPixelRatio(this.pixelRatio); 38 | 39 | this.clock = new THREE.Clock(); 40 | this.clock.start(); 41 | } 42 | 43 | resize(){ 44 | this.width = window.innerWidth; // document.body.clientWidth; 45 | this.height = window.innerHeight; 46 | this.aspect = this.width / this.height; 47 | 48 | if(this.renderer) this.renderer.setSize(this.width, this.height); 49 | } 50 | 51 | update(){ 52 | this.delta = this.clock.getDelta(); // Math.min(0.01, this.clock.getDelta()); 53 | this.time += this.delta; 54 | } 55 | } 56 | 57 | export default new Common(); -------------------------------------------------------------------------------- /js/modules/Controls.js: -------------------------------------------------------------------------------- 1 | import * as dat from "dat.gui"; 2 | 3 | export default class Controls{ 4 | constructor(params){ 5 | this.params = params; 6 | this.init(); 7 | } 8 | 9 | init(){ 10 | this.gui = new dat.GUI({width: 300}); 11 | this.gui.add(this.params, "mouse_force",20, 200); 12 | this.gui.add(this.params, "cursor_size", 10, 200); 13 | this.gui.add(this.params, "isViscous"); 14 | this.gui.add(this.params, "viscous", 0, 500); 15 | this.gui.add(this.params, "iterations_viscous", 1, 32); 16 | this.gui.add(this.params, "iterations_poisson", 1, 32); 17 | this.gui.add(this.params, "dt", 1/200, 1/30); 18 | this.gui.add(this.params, 'BFECC'); 19 | this.gui.close(); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /js/modules/Divergence.js: -------------------------------------------------------------------------------- 1 | import face_vert from "./glsl/sim/face.vert"; 2 | import divergence_frag from "./glsl/sim/divergence.frag"; 3 | 4 | import ShaderPass from "./ShaderPass"; 5 | 6 | export default class Divergence extends ShaderPass{ 7 | constructor(simProps){ 8 | super({ 9 | material: { 10 | vertexShader: face_vert, 11 | fragmentShader: divergence_frag, 12 | uniforms: { 13 | boundarySpace: { 14 | value: simProps.boundarySpace 15 | }, 16 | velocity: { 17 | value: simProps.src.texture 18 | }, 19 | px: { 20 | value: simProps.cellScale 21 | }, 22 | dt: { 23 | value: simProps.dt 24 | } 25 | } 26 | }, 27 | output: simProps.dst 28 | }) 29 | 30 | this.init(); 31 | } 32 | 33 | update({ vel }){ 34 | this.uniforms.velocity.value = vel.texture; 35 | super.update(); 36 | } 37 | } -------------------------------------------------------------------------------- /js/modules/ExternalForce.js: -------------------------------------------------------------------------------- 1 | import mouse_vert from "./glsl/sim/mouse.vert"; 2 | import externalForce_frag from "./glsl/sim/externalForce.frag"; 3 | import ShaderPass from "./ShaderPass"; 4 | import Mouse from "./Mouse"; 5 | 6 | import * as THREE from "three"; 7 | 8 | export default class ExternalForce extends ShaderPass{ 9 | constructor(simProps){ 10 | super({ 11 | output: simProps.dst 12 | }); 13 | 14 | this.init(simProps); 15 | } 16 | 17 | init(simProps){ 18 | super.init(); 19 | const mouseG = new THREE.PlaneBufferGeometry( 20 | 1, 1 21 | ); 22 | 23 | const mouseM = new THREE.RawShaderMaterial({ 24 | vertexShader: mouse_vert, 25 | fragmentShader: externalForce_frag, 26 | blending: THREE.AdditiveBlending, 27 | uniforms: { 28 | px: { 29 | value: simProps.cellScale 30 | }, 31 | force: { 32 | value: new THREE.Vector2(0.0, 0.0) 33 | }, 34 | center: { 35 | value: new THREE.Vector2(0.0, 0.0) 36 | }, 37 | scale: { 38 | value: new THREE.Vector2(simProps.cursor_size, simProps.cursor_size) 39 | } 40 | }, 41 | }) 42 | 43 | this.mouse = new THREE.Mesh(mouseG, mouseM); 44 | this.scene.add(this.mouse); 45 | } 46 | 47 | update(props){ 48 | const forceX = Mouse.diff.x / 2 * props.mouse_force; 49 | const forceY = Mouse.diff.y / 2 * props.mouse_force; 50 | 51 | const cursorSizeX = props.cursor_size * props.cellScale.x; 52 | const cursorSizeY = props.cursor_size * props.cellScale.y; 53 | 54 | const centerX = Math.min(Math.max(Mouse.coords.x, -1 + cursorSizeX + props.cellScale.x * 2), 1 - cursorSizeX - props.cellScale.x * 2); 55 | const centerY = Math.min(Math.max(Mouse.coords.y, -1 + cursorSizeY + props.cellScale.y * 2), 1 - cursorSizeY - props.cellScale.y * 2); 56 | 57 | const uniforms = this.mouse.material.uniforms; 58 | 59 | uniforms.force.value.set(forceX, forceY); 60 | uniforms.center.value.set(centerX, centerY); 61 | uniforms.scale.value.set(props.cursor_size, props.cursor_size); 62 | 63 | super.update(); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /js/modules/Mouse.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Common from "./Common"; 3 | 4 | class Mouse{ 5 | constructor(){ 6 | this.mouseMoved = false; 7 | this.coords = new THREE.Vector2(); 8 | this.coords_old = new THREE.Vector2(); 9 | this.diff = new THREE.Vector2(); 10 | this.timer = null; 11 | this.count = 0; 12 | } 13 | 14 | init(){ 15 | document.body.addEventListener( 'mousemove', this.onDocumentMouseMove.bind(this), false ); 16 | document.body.addEventListener( 'touchstart', this.onDocumentTouchStart.bind(this), false ); 17 | document.body.addEventListener( 'touchmove', this.onDocumentTouchMove.bind(this), false ); 18 | } 19 | 20 | setCoords( x, y ) { 21 | if(this.timer) clearTimeout(this.timer); 22 | this.coords.set( ( x / Common.width ) * 2 - 1, - ( y / Common.height ) * 2 + 1 ); 23 | this.mouseMoved = true; 24 | this.timer = setTimeout(() => { 25 | this.mouseMoved = false; 26 | }, 100); 27 | 28 | } 29 | onDocumentMouseMove( event ) { 30 | this.setCoords( event.clientX, event.clientY ); 31 | } 32 | onDocumentTouchStart( event ) { 33 | if ( event.touches.length === 1 ) { 34 | // event.preventDefault(); 35 | this.setCoords( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 36 | } 37 | } 38 | onDocumentTouchMove( event ) { 39 | if ( event.touches.length === 1 ) { 40 | // event.preventDefault(); 41 | 42 | this.setCoords( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 43 | } 44 | } 45 | 46 | update(){ 47 | this.diff.subVectors(this.coords, this.coords_old); 48 | this.coords_old.copy(this.coords); 49 | 50 | if(this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0); 51 | } 52 | } 53 | 54 | export default new Mouse(); 55 | -------------------------------------------------------------------------------- /js/modules/Output.js: -------------------------------------------------------------------------------- 1 | import Common from "./Common"; 2 | import * as THREE from "three"; 3 | 4 | import Simulation from "./Simulation"; 5 | import face_vert from "./glsl/sim/face.vert"; 6 | import color_frag from "./glsl/sim/color.frag"; 7 | 8 | 9 | export default class Output{ 10 | constructor(){ 11 | this.init(); 12 | } 13 | 14 | init(){ 15 | this.simulation = new Simulation(); 16 | 17 | this.scene = new THREE.Scene(); 18 | this.camera = new THREE.Camera(); 19 | 20 | this.output = new THREE.Mesh( 21 | new THREE.PlaneBufferGeometry(2, 2), 22 | new THREE.RawShaderMaterial({ 23 | vertexShader: face_vert, 24 | fragmentShader: color_frag, 25 | uniforms: { 26 | velocity: { 27 | value: this.simulation.fbos.vel_0.texture 28 | }, 29 | boundarySpace: { 30 | value: new THREE.Vector2() 31 | } 32 | }, 33 | }) 34 | ); 35 | 36 | this.scene.add(this.output); 37 | } 38 | addScene(mesh){ 39 | this.scene.add(mesh); 40 | } 41 | 42 | resize(){ 43 | this.simulation.resize(); 44 | } 45 | 46 | render(){ 47 | Common.renderer.setRenderTarget(null); 48 | Common.renderer.render(this.scene, this.camera); 49 | } 50 | 51 | update(){ 52 | this.simulation.update(); 53 | this.render(); 54 | } 55 | } -------------------------------------------------------------------------------- /js/modules/Poisson.js: -------------------------------------------------------------------------------- 1 | import face_vert from "./glsl/sim/face.vert"; 2 | import poisson_frag from "./glsl/sim/poisson.frag"; 3 | 4 | import ShaderPass from "./ShaderPass"; 5 | 6 | export default class Divergence extends ShaderPass{ 7 | constructor(simProps){ 8 | super({ 9 | material: { 10 | vertexShader: face_vert, 11 | fragmentShader: poisson_frag, 12 | uniforms: { 13 | boundarySpace: { 14 | value: simProps.boundarySpace 15 | }, 16 | pressure: { 17 | value: simProps.dst_.texture 18 | }, 19 | divergence: { 20 | value: simProps.src.texture 21 | }, 22 | px: { 23 | value: simProps.cellScale 24 | } 25 | }, 26 | }, 27 | output: simProps.dst, 28 | 29 | output0: simProps.dst_, 30 | output1: simProps.dst 31 | }); 32 | 33 | this.init(); 34 | } 35 | 36 | update({iterations}){ 37 | let p_in, p_out; 38 | 39 | for(var i = 0; i < iterations; i++) { 40 | if(i % 2 == 0){ 41 | p_in = this.props.output0; 42 | p_out = this.props.output1; 43 | } else { 44 | p_in = this.props.output1; 45 | p_out = this.props.output0; 46 | } 47 | 48 | this.uniforms.pressure.value = p_in.texture; 49 | this.props.output = p_out; 50 | super.update(); 51 | } 52 | 53 | return p_out; 54 | } 55 | } -------------------------------------------------------------------------------- /js/modules/Pressure.js: -------------------------------------------------------------------------------- 1 | import face_vert from "./glsl/sim/face.vert"; 2 | import pressure_frag from "./glsl/sim/pressure.frag"; 3 | import ShaderPass from "./ShaderPass"; 4 | 5 | export default class Divergence extends ShaderPass{ 6 | constructor(simProps){ 7 | super({ 8 | material: { 9 | vertexShader: face_vert, 10 | fragmentShader: pressure_frag, 11 | uniforms: { 12 | boundarySpace: { 13 | value: simProps.boundarySpace 14 | }, 15 | pressure: { 16 | value: simProps.src_p.texture 17 | }, 18 | velocity: { 19 | value: simProps.src_v.texture 20 | }, 21 | px: { 22 | value: simProps.cellScale 23 | }, 24 | dt: { 25 | value: simProps.dt 26 | } 27 | } 28 | }, 29 | output: simProps.dst 30 | }); 31 | 32 | this.init(); 33 | } 34 | 35 | update({vel, pressure}){ 36 | this.uniforms.velocity.value = vel.texture; 37 | this.uniforms.pressure.value = pressure.texture; 38 | super.update(); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /js/modules/ShaderPass.js: -------------------------------------------------------------------------------- 1 | import Common from "./Common"; 2 | import * as THREE from "three"; 3 | 4 | 5 | export default class ShaderPass{ 6 | constructor(props){ 7 | this.props = props; 8 | this.uniforms = this.props.material?.uniforms; 9 | } 10 | 11 | init(){ 12 | this.scene = new THREE.Scene(); 13 | this.camera = new THREE.Camera(); 14 | 15 | if(this.uniforms){ 16 | this.material = new THREE.RawShaderMaterial(this.props.material); 17 | this.geometry = new THREE.PlaneBufferGeometry(2.0, 2.0); 18 | this.plane = new THREE.Mesh(this.geometry, this.material); 19 | this.scene.add(this.plane); 20 | } 21 | 22 | } 23 | 24 | update(){ 25 | Common.renderer.setRenderTarget(this.props.output); 26 | Common.renderer.render(this.scene, this.camera); 27 | Common.renderer.setRenderTarget(null); 28 | } 29 | } -------------------------------------------------------------------------------- /js/modules/Simulation.js: -------------------------------------------------------------------------------- 1 | import Mouse from "./Mouse"; 2 | import Common from "./Common"; 3 | import * as THREE from "three"; 4 | import Controls from "./Controls"; 5 | 6 | import Advection from "./Advection"; 7 | import ExternalForce from "./ExternalForce"; 8 | import Viscous from "./Viscous"; 9 | import Divergence from "./Divergence"; 10 | import Poisson from "./Poisson"; 11 | import Pressure from "./Pressure"; 12 | 13 | export default class Simulation{ 14 | constructor(props){ 15 | this.props = props; 16 | 17 | this.fbos = { 18 | vel_0: null, 19 | vel_1: null, 20 | 21 | // for calc next velocity with viscous 22 | vel_viscous0: null, 23 | vel_viscous1: null, 24 | 25 | // for calc pressure 26 | div: null, 27 | 28 | // for calc poisson equation 29 | pressure_0: null, 30 | pressure_1: null, 31 | }; 32 | 33 | this.options = { 34 | iterations_poisson: 32, 35 | iterations_viscous: 32, 36 | mouse_force: 20, 37 | resolution: 0.5, 38 | cursor_size: 100, 39 | viscous: 30, 40 | isBounce: false, 41 | dt: 0.014, 42 | isViscous: false, 43 | BFECC: true 44 | }; 45 | 46 | const controls = new Controls(this.options); 47 | 48 | this.fboSize = new THREE.Vector2(); 49 | this.cellScale = new THREE.Vector2(); 50 | this.boundarySpace = new THREE.Vector2(); 51 | 52 | this.init(); 53 | } 54 | 55 | 56 | init(){ 57 | this.calcSize(); 58 | this.createAllFBO(); 59 | this.createShaderPass(); 60 | } 61 | 62 | createAllFBO(){ 63 | const type = ( /(iPad|iPhone|iPod)/g.test( navigator.userAgent ) ) ? THREE.HalfFloatType : THREE.FloatType; 64 | 65 | for(let key in this.fbos){ 66 | this.fbos[key] = new THREE.WebGLRenderTarget( 67 | this.fboSize.x, 68 | this.fboSize.y, 69 | { 70 | type: type 71 | } 72 | ) 73 | } 74 | } 75 | 76 | createShaderPass(){ 77 | this.advection = new Advection({ 78 | cellScale: this.cellScale, 79 | fboSize: this.fboSize, 80 | dt: this.options.dt, 81 | src: this.fbos.vel_0, 82 | dst: this.fbos.vel_1 83 | }); 84 | 85 | this.externalForce = new ExternalForce({ 86 | cellScale: this.cellScale, 87 | cursor_size: this.options.cursor_size, 88 | dst: this.fbos.vel_1, 89 | }); 90 | 91 | this.viscous = new Viscous({ 92 | cellScale: this.cellScale, 93 | boundarySpace: this.boundarySpace, 94 | viscous: this.options.viscous, 95 | src: this.fbos.vel_1, 96 | dst: this.fbos.vel_viscous1, 97 | dst_: this.fbos.vel_viscous0, 98 | dt: this.options.dt, 99 | }); 100 | 101 | this.divergence = new Divergence({ 102 | cellScale: this.cellScale, 103 | boundarySpace: this.boundarySpace, 104 | src: this.fbos.vel_viscous0, 105 | dst: this.fbos.div, 106 | dt: this.options.dt, 107 | }); 108 | 109 | this.poisson = new Poisson({ 110 | cellScale: this.cellScale, 111 | boundarySpace: this.boundarySpace, 112 | src: this.fbos.div, 113 | dst: this.fbos.pressure_1, 114 | dst_: this.fbos.pressure_0, 115 | }); 116 | 117 | this.pressure = new Pressure({ 118 | cellScale: this.cellScale, 119 | boundarySpace: this.boundarySpace, 120 | src_p: this.fbos.pressure_0, 121 | src_v: this.fbos.vel_viscous0, 122 | dst: this.fbos.vel_0, 123 | dt: this.options.dt, 124 | }); 125 | } 126 | 127 | calcSize(){ 128 | const width = Math.round(this.options.resolution * Common.width); 129 | const height = Math.round(this.options.resolution * Common.height); 130 | 131 | const px_x = 1.0 / width; 132 | const px_y = 1.0 / height; 133 | 134 | this.cellScale.set(px_x, px_y); 135 | this.fboSize.set(width, height); 136 | } 137 | 138 | resize(){ 139 | this.calcSize(); 140 | 141 | for(let key in this.fbos){ 142 | this.fbos[key].setSize(this.fboSize.x, this.fboSize.y); 143 | } 144 | } 145 | 146 | 147 | update(){ 148 | 149 | if(this.options.isBounce){ 150 | this.boundarySpace.set(0, 0); 151 | } else { 152 | this.boundarySpace.copy(this.cellScale); 153 | } 154 | 155 | this.advection.update(this.options); 156 | 157 | this.externalForce.update({ 158 | cursor_size: this.options.cursor_size, 159 | mouse_force: this.options.mouse_force, 160 | cellScale: this.cellScale 161 | }); 162 | 163 | let vel = this.fbos.vel_1; 164 | 165 | if(this.options.isViscous){ 166 | vel = this.viscous.update({ 167 | viscous: this.options.viscous, 168 | iterations: this.options.iterations_viscous, 169 | dt: this.options.dt 170 | }); 171 | } 172 | 173 | this.divergence.update({vel}); 174 | 175 | const pressure = this.poisson.update({ 176 | iterations: this.options.iterations_poisson, 177 | }); 178 | 179 | this.pressure.update({ vel , pressure}); 180 | } 181 | } -------------------------------------------------------------------------------- /js/modules/Viscous.js: -------------------------------------------------------------------------------- 1 | import face_vert from "./glsl/sim/face.vert"; 2 | import viscous_frag from "./glsl/sim/viscous.frag"; 3 | 4 | import ShaderPass from "./ShaderPass"; 5 | import * as THREE from "three"; 6 | 7 | export default class Viscous extends ShaderPass{ 8 | constructor(simProps){ 9 | super({ 10 | material: { 11 | vertexShader: face_vert, 12 | fragmentShader: viscous_frag, 13 | uniforms: { 14 | boundarySpace: { 15 | value: simProps.boundarySpace 16 | }, 17 | velocity: { 18 | value: simProps.src.texture 19 | }, 20 | velocity_new: { 21 | value: simProps.dst_.texture 22 | }, 23 | v: { 24 | value: simProps.viscous, 25 | }, 26 | px: { 27 | value: simProps.cellScale 28 | }, 29 | dt: { 30 | value: simProps.dt 31 | } 32 | } 33 | }, 34 | 35 | output: simProps.dst, 36 | 37 | output0: simProps.dst_, 38 | output1: simProps.dst 39 | }) 40 | 41 | this.init(); 42 | } 43 | 44 | update({ viscous, iterations, dt }){ 45 | let fbo_in, fbo_out; 46 | this.uniforms.v.value = viscous; 47 | for(var i = 0; i < iterations; i++){ 48 | if(i % 2 == 0){ 49 | fbo_in = this.props.output0; 50 | fbo_out = this.props.output1; 51 | } else { 52 | fbo_in = this.props.output1; 53 | fbo_out = this.props.output0; 54 | } 55 | 56 | this.uniforms.velocity_new.value = fbo_in.texture; 57 | this.props.output = fbo_out; 58 | this.uniforms.dt.value = dt; 59 | 60 | super.update(); 61 | } 62 | 63 | return fbo_out; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /js/modules/WebGL.js: -------------------------------------------------------------------------------- 1 | // import * as THREE from "three"; 2 | import Common from "./Common"; 3 | import Output from "./Output"; 4 | import Mouse from "./Mouse"; 5 | 6 | export default class Webgl{ 7 | constructor(props){ 8 | this.props = props; 9 | 10 | Common.init(); 11 | Mouse.init(); 12 | 13 | this.init(); 14 | this.loop(); 15 | 16 | window.addEventListener("resize", this.resize.bind(this)); 17 | } 18 | 19 | init(){ 20 | this.props.$wrapper.prepend(Common.renderer.domElement); 21 | this.output = new Output(); 22 | } 23 | 24 | resize(){ 25 | Common.resize(); 26 | this.output.resize(); 27 | } 28 | 29 | render(){ 30 | Mouse.update(); 31 | Common.update(); 32 | this.output.update(); 33 | } 34 | 35 | loop(){ 36 | this.render(); 37 | requestAnimationFrame(this.loop.bind(this)); 38 | } 39 | } -------------------------------------------------------------------------------- /js/modules/glsl/sim/advection.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform sampler2D velocity; 3 | uniform float dt; 4 | uniform bool isBFECC; 5 | // uniform float uvScale; 6 | uniform vec2 fboSize; 7 | uniform vec2 px; 8 | varying vec2 uv; 9 | 10 | void main(){ 11 | vec2 ratio = max(fboSize.x, fboSize.y) / fboSize; 12 | 13 | if(isBFECC == false){ 14 | vec2 vel = texture2D(velocity, uv).xy; 15 | vec2 uv2 = uv - vel * dt * ratio; 16 | vec2 newVel = texture2D(velocity, uv2).xy; 17 | gl_FragColor = vec4(newVel, 0.0, 0.0); 18 | } else { 19 | vec2 spot_new = uv; 20 | vec2 vel_old = texture2D(velocity, uv).xy; 21 | // back trace 22 | vec2 spot_old = spot_new - vel_old * dt * ratio; 23 | vec2 vel_new1 = texture2D(velocity, spot_old).xy; 24 | 25 | // forward trace 26 | vec2 spot_new2 = spot_old + vel_new1 * dt * ratio; 27 | 28 | vec2 error = spot_new2 - spot_new; 29 | 30 | vec2 spot_new3 = spot_new - error / 2.0; 31 | vec2 vel_2 = texture2D(velocity, spot_new3).xy; 32 | 33 | // back trace 2 34 | vec2 spot_old2 = spot_new3 - vel_2 * dt * ratio; 35 | // gl_FragColor = vec4(spot_old2, 0.0, 0.0); 36 | vec2 newVel2 = texture2D(velocity, spot_old2).xy; 37 | gl_FragColor = vec4(newVel2, 0.0, 0.0); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/color.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform sampler2D velocity; 3 | varying vec2 uv; 4 | 5 | void main(){ 6 | vec2 vel = texture2D(velocity, uv).xy; 7 | float len = length(vel); 8 | vel = vel * 0.5 + 0.5; 9 | 10 | vec3 color = vec3(vel.x, vel.y, 1.0); 11 | color = mix(vec3(1.0), color, len); 12 | 13 | gl_FragColor = vec4(color, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/divergence.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform sampler2D velocity; 3 | uniform float dt; 4 | uniform vec2 px; 5 | varying vec2 uv; 6 | 7 | void main(){ 8 | float x0 = texture2D(velocity, uv-vec2(px.x, 0)).x; 9 | float x1 = texture2D(velocity, uv+vec2(px.x, 0)).x; 10 | float y0 = texture2D(velocity, uv-vec2(0, px.y)).y; 11 | float y1 = texture2D(velocity, uv+vec2(0, px.y)).y; 12 | float divergence = (x1-x0 + y1-y0) / 2.0; 13 | 14 | gl_FragColor = vec4(divergence / dt); 15 | } 16 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/externalForce.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform vec2 force; 4 | uniform vec2 center; 5 | uniform vec2 scale; 6 | uniform vec2 px; 7 | varying vec2 vUv; 8 | 9 | void main(){ 10 | vec2 circle = (vUv - 0.5) * 2.0; 11 | float d = 1.0-min(length(circle), 1.0); 12 | d *= d; 13 | gl_FragColor = vec4(force * d, 0, 1); 14 | } 15 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/face.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform vec2 px; 3 | uniform vec2 boundarySpace; 4 | varying vec2 uv; 5 | 6 | precision highp float; 7 | 8 | void main(){ 9 | vec3 pos = position; 10 | vec2 scale = 1.0 - boundarySpace * 2.0; 11 | pos.xy = pos.xy * scale; 12 | uv = vec2(0.5)+(pos.xy)*0.5; 13 | gl_Position = vec4(pos, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/line.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | varying vec2 uv; 3 | uniform vec2 px; 4 | 5 | 6 | precision highp float; 7 | 8 | void main(){ 9 | vec3 pos = position; 10 | uv = 0.5 + pos.xy * 0.5; 11 | vec2 n = sign(pos.xy); 12 | pos.xy = abs(pos.xy) - px * 1.0; 13 | pos.xy *= n; 14 | gl_Position = vec4(pos, 1.0); 15 | } -------------------------------------------------------------------------------- /js/modules/glsl/sim/mouse.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 position; 4 | attribute vec2 uv; 5 | uniform vec2 center; 6 | uniform vec2 scale; 7 | uniform vec2 px; 8 | varying vec2 vUv; 9 | 10 | void main(){ 11 | vec2 pos = position.xy * scale * 2.0 * px + center; 12 | vUv = uv; 13 | gl_Position = vec4(pos, 0.0, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/poisson.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform sampler2D pressure; 3 | uniform sampler2D divergence; 4 | uniform vec2 px; 5 | varying vec2 uv; 6 | 7 | void main(){ 8 | // poisson equation 9 | float p0 = texture2D(pressure, uv+vec2(px.x * 2.0, 0)).r; 10 | float p1 = texture2D(pressure, uv-vec2(px.x * 2.0, 0)).r; 11 | float p2 = texture2D(pressure, uv+vec2(0, px.y * 2.0 )).r; 12 | float p3 = texture2D(pressure, uv-vec2(0, px.y * 2.0 )).r; 13 | float div = texture2D(divergence, uv).r; 14 | 15 | float newP = (p0 + p1 + p2 + p3) / 4.0 - div; 16 | gl_FragColor = vec4(newP); 17 | } 18 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/pressure.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform sampler2D pressure; 3 | uniform sampler2D velocity; 4 | uniform vec2 px; 5 | uniform float dt; 6 | varying vec2 uv; 7 | 8 | void main(){ 9 | float step = 1.0; 10 | 11 | float p0 = texture2D(pressure, uv+vec2(px.x * step, 0)).r; 12 | float p1 = texture2D(pressure, uv-vec2(px.x * step, 0)).r; 13 | float p2 = texture2D(pressure, uv+vec2(0, px.y * step)).r; 14 | float p3 = texture2D(pressure, uv-vec2(0, px.y * step)).r; 15 | 16 | vec2 v = texture2D(velocity, uv).xy; 17 | vec2 gradP = vec2(p0 - p1, p2 - p3) * 0.5; 18 | v = v - gradP * dt; 19 | gl_FragColor = vec4(v, 0.0, 1.0); 20 | } 21 | -------------------------------------------------------------------------------- /js/modules/glsl/sim/viscous.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform sampler2D velocity; 3 | uniform sampler2D velocity_new; 4 | uniform float v; 5 | uniform vec2 px; 6 | uniform float dt; 7 | 8 | varying vec2 uv; 9 | 10 | void main(){ 11 | // poisson equation 12 | vec2 old = texture2D(velocity, uv).xy; 13 | vec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0)).xy; 14 | vec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0)).xy; 15 | vec2 new2 = texture2D(velocity_new, uv + vec2(0, px.y * 2.0)).xy; 16 | vec2 new3 = texture2D(velocity_new, uv - vec2(0, px.y * 2.0)).xy; 17 | 18 | vec2 new = 4.0 * old + v * dt * (new0 + new1 + new2 + new3); 19 | new /= 4.0 * (1.0 + v * dt); 20 | 21 | gl_FragColor = vec4(new, 0.0, 0.0); 22 | } 23 | -------------------------------------------------------------------------------- /js/utils/EventBus.js: -------------------------------------------------------------------------------- 1 | class EventBus{ 2 | /** 3 | * Initialize a new event bus instance. 4 | */ 5 | constructor(){ 6 | this.bus = document.createElement('fakeelement'); 7 | } 8 | 9 | /** 10 | * Add an event listener. 11 | */ 12 | on(event, callback){ 13 | this.bus.addEventListener(event, callback); 14 | } 15 | 16 | /** 17 | * Remove an event listener. 18 | */ 19 | off(event, callback){ 20 | this.bus.removeEventListener(event, callback); 21 | } 22 | 23 | /** 24 | * Dispatch an event. 25 | */ 26 | emit(event, detail = {}){ 27 | this.bus.dispatchEvent(new CustomEvent(event, { detail })); 28 | } 29 | } 30 | 31 | export default new EventBus(); -------------------------------------------------------------------------------- /js/utils/math.js: -------------------------------------------------------------------------------- 1 | export const lerp = function(start, target, easing){ 2 | return start + (target - start) * easing; 3 | }; -------------------------------------------------------------------------------- /js/utils/utils.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluid-three", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "webpack-dev-server --port 3001 --hot --host 0.0.0.0", 8 | "build": "webpack" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.7.2", 12 | "@babel/preset-env": "^7.7.1", 13 | "babel-loader": "^8.0.6", 14 | "copy-webpack-plugin": "^5.0.5", 15 | "raw-loader": "^3.1.0", 16 | "webpack": "^4.41.2", 17 | "webpack-cli": "^3.3.10", 18 | "webpack-dev-server": "^3.9.0" 19 | }, 20 | "dependencies": { 21 | "dat.gui": "^0.7.6", 22 | "terser-webpack-plugin": "^2.3.0", 23 | "three": "^0.111.0", 24 | "three-obj-loader": "^1.1.3", 25 | "uglify-js": "^3.7.2", 26 | "uglifyjs-webpack-plugin": "^2.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const entry = {}; 5 | 6 | module.exports = { 7 | mode: "development", //'production', // 8 | entry: './js/main.js', 9 | output: { 10 | filename: 'main.min.js', 11 | path: __dirname + "/dist" 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | use: [ 19 | { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: [['@babel/preset-env', { modules: false }]] 23 | } 24 | } 25 | ] 26 | }, 27 | { 28 | test: /\.(vert|frag|obj)$/i, 29 | use: 'raw-loader', 30 | } 31 | ], 32 | }, 33 | // optimization: { 34 | // minimize: true, 35 | // minimizer: [ 36 | // new TerserPlugin({ 37 | // test: /\.js(\?.*)?$/i, 38 | // }) 39 | // ], 40 | // }, 41 | devServer: { 42 | contentBase: path.join(__dirname, 'dist'), 43 | compress: false, 44 | open: true, 45 | disableHostCheck: true 46 | } 47 | }; --------------------------------------------------------------------------------