├── .github └── workflows │ ├── ghpages.yml │ └── nodejs.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── package.json ├── screenshot.png ├── src ├── RenderTarget.ts ├── index.html ├── index.ts ├── passes │ ├── AdvectionPass.ts │ ├── BoundaryPass.ts │ ├── ColorInitPass.ts │ ├── CompositionPass.ts │ ├── DivergencePass.ts │ ├── GradientSubstractionPass.ts │ ├── JacobiIterationsPass.ts │ ├── TouchColorPass.ts │ ├── TouchForcePass.ts │ └── VelocityInitPass.ts └── resources │ ├── gradient.jpg │ └── iconfont.ttf ├── tsconfig.json ├── tslint.json ├── webpack ├── common.config.js ├── dev.config.js └── prod.config.js └── yarn.lock /.github/workflows/ghpages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages Deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | 19 | - name: yarn install and build 20 | run: | 21 | yarn install 22 | yarn build 23 | env: 24 | CI: true 25 | 26 | - name: Deploy 27 | if: success() 28 | uses: crazy-max/ghaction-github-pages@v1 29 | with: 30 | target_branch: gh-pages 31 | build_dir: dist 32 | env: 33 | GITHUB_PAT: ${{ secrets.GITHUB_PAT }} 34 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | 11 | - name: Use Node.js ${{ matrix.node-version }} 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: ${{ matrix.node-version }} 15 | 16 | - name: yarn install, build, and test 17 | run: | 18 | yarn install 19 | yarn build 20 | yarn test 21 | env: 22 | CI: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.DS_Store -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrés Valencia Téllez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-fluid-sim 2 | 2D Fluid Simulation Three.js implementation. 3 | 4 | ![screenshot](screenshot.png) 5 | 6 | ## References 7 | - [GPU Gems Chapter 38: Fast Fluid Dynamics Simulation on the GPU](http://developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch38.html) 8 | - [Jonas Wagner's fluidwebgl](https://github.com/jwagner/fluidwebgl) 9 | - [Jamie Wong's article](http://jamie-wong.com/2016/08/05/webgl-fluid-simulation/) 10 | - [Pavel Dobryakov's WebGL-Fluid-Simulation](https://github.com/PavelDoGreat/WebGL-Fluid-Simulation) 11 | 12 | ## License 13 | The code is available under the [MIT license](LICENSE) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-fluid-sim", 3 | "version": "1.0.0", 4 | "description": "2D Fluid Simulation three.js implementation.", 5 | "main": "index.js", 6 | "repository": "https://github.com/amsXYZ/three-fluid-sim.git", 7 | "author": "Andrés Valencia Téllez ", 8 | "license": "MIT", 9 | "scripts": { 10 | "test": "yarn run tslint && yarn run prettier", 11 | "fix": "yarn tslint:fix && yarn prettier:fix", 12 | "build": "webpack --config webpack/prod.config.js", 13 | "start": "webpack-dev-server --config webpack/dev.config.js", 14 | "start:local": "yarn start --host 0.0.0.0", 15 | "tslint": "tslint --project tsconfig.json", 16 | "tslint:fix": "tslint --fix --project tsconfig.json", 17 | "prettier": "prettier -l \"**/*.ts\" \"**/*.tsx\" \"**/*.json\"", 18 | "prettier:fix": "prettier --write -l \"**/*.ts\" \"**/*.tsx\" \"**/*.json\"" 19 | }, 20 | "dependencies": { 21 | "dat.gui": "0.7.6", 22 | "stats.js": "^0.17.0", 23 | "three": "^0.111.0" 24 | }, 25 | "devDependencies": { 26 | "copy-webpack-plugin": "^5.1.1", 27 | "html-webpack-plugin": "^3.2.0", 28 | "path": "^0.12.7", 29 | "prettier": "^1.19.1", 30 | "source-map-loader": "^0.2.4", 31 | "ts-loader": "^6.2.1", 32 | "tslint": "^5.20.1", 33 | "tslint-config-prettier": "^1.18.0", 34 | "tslint-plugin-prettier": "^2.0.1", 35 | "typescript": "^3.7.2", 36 | "webpack": "^4.41.2", 37 | "webpack-cli": "^3.3.10", 38 | "webpack-dev-server": "^3.9.0", 39 | "webpack-merge": "^4.2.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amsXYZ/three-fluid-sim/1a03b276289d82f243db3cf780205da7661201ea/screenshot.png -------------------------------------------------------------------------------- /src/RenderTarget.ts: -------------------------------------------------------------------------------- 1 | import { Texture, Vector2, WebGLRenderer, WebGLRenderTarget } from "three"; 2 | 3 | interface IBuffer { 4 | target: WebGLRenderTarget; 5 | needsResize: boolean; 6 | } 7 | 8 | export class RenderTarget { 9 | private index: number; 10 | private buffers: IBuffer[]; 11 | 12 | constructor( 13 | readonly resolution: Vector2, 14 | readonly nBuffers: number, 15 | readonly format: number, 16 | readonly type: number 17 | ) { 18 | this.index = 0; 19 | this.buffers = [ 20 | { 21 | target: new WebGLRenderTarget(resolution.x, resolution.y, { 22 | format, 23 | type, 24 | depthBuffer: false, 25 | stencilBuffer: false 26 | }), 27 | needsResize: false 28 | } 29 | ]; 30 | for (let i = 1; i < nBuffers; ++i) { 31 | this.buffers[i] = { 32 | target: this.buffers[0].target.clone(), 33 | needsResize: false 34 | }; 35 | } 36 | } 37 | 38 | public resize(resolution: Vector2): void { 39 | resolution.copy(resolution); 40 | for (let i = 0; i < this.nBuffers; ++i) { 41 | this.buffers[i].needsResize = true; 42 | } 43 | } 44 | 45 | public set(renderer: WebGLRenderer): Texture { 46 | const buffer = this.buffers[this.index++]; 47 | if (buffer.needsResize) { 48 | buffer.needsResize = false; 49 | buffer.target.setSize(this.resolution.x, this.resolution.y); 50 | } 51 | renderer.setRenderTarget(buffer.target); 52 | this.index %= this.nBuffers; 53 | return buffer.target.texture; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2D Fluid Simulation 6 | 10 | 38 | 39 | 40 |
41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HalfFloatType, 3 | OrthographicCamera, 4 | RGBFormat, 5 | Texture, 6 | TextureLoader, 7 | UnsignedByteType, 8 | Vector2, 9 | Vector4, 10 | WebGLRenderer 11 | } from "three"; 12 | import { AdvectionPass } from "./passes/AdvectionPass"; 13 | import { BoundaryPass } from "./passes/BoundaryPass"; 14 | import { ColorInitPass } from "./passes/ColorInitPass"; 15 | import { CompositionPass } from "./passes/CompositionPass"; 16 | import { DivergencePass } from "./passes/DivergencePass"; 17 | import { GradientSubstractionPass } from "./passes/GradientSubstractionPass"; 18 | import { JacobiIterationsPass } from "./passes/JacobiIterationsPass"; 19 | import { TouchColorPass } from "./passes/TouchColorPass"; 20 | import { TouchForcePass } from "./passes/TouchForcePass"; 21 | import { VelocityInitPass } from "./passes/VelocityInitPass"; 22 | import { RenderTarget } from "./RenderTarget"; 23 | 24 | // tslint:disable:no-var-requires 25 | const Stats = require("stats.js"); 26 | const dat = require("dat.gui"); 27 | // tslint:enable:no-var-requires 28 | 29 | const gradients: string[] = ["gradient.jpg"]; 30 | const gradientTextures: Texture[] = []; 31 | loadGradients(); 32 | 33 | // App configuration options. 34 | const configuration = { 35 | Simulate: true, 36 | Iterations: 32, 37 | Radius: 0.25, 38 | Scale: 0.5, 39 | ColorDecay: 0.01, 40 | Boundaries: true, 41 | AddColor: true, 42 | Visualize: "Color", 43 | Mode: "Spectral", 44 | Timestep: "1/60", 45 | Reset: () => { 46 | velocityAdvectionPass.update({ 47 | inputTexture: velocityInitTexture, 48 | velocity: velocityInitTexture 49 | }); 50 | colorAdvectionPass.update({ 51 | inputTexture: colorInitTexture, 52 | velocity: velocityInitTexture 53 | }); 54 | v = undefined; 55 | c = undefined; 56 | }, 57 | Github: () => { 58 | window.open("https://github.com/amsXYZ/three-fluid-sim"); 59 | }, 60 | Twitter: () => { 61 | window.open("https://twitter.com/_amsXYZ"); 62 | } 63 | }; 64 | 65 | // Html/Three.js initialization. 66 | const canvas = document.getElementById("canvas") as HTMLCanvasElement; 67 | const stats = new Stats(); 68 | canvas.parentElement.appendChild(stats.dom); 69 | const gui = new dat.GUI(); 70 | initGUI(); 71 | 72 | const renderer = new WebGLRenderer({ canvas }); 73 | renderer.autoClear = false; 74 | renderer.setSize(window.innerWidth, window.innerHeight); 75 | renderer.setPixelRatio(window.devicePixelRatio); 76 | const camera = new OrthographicCamera(0, 0, 0, 0, 0, 0); 77 | let dt = 1 / 60; 78 | 79 | // Check floating point texture support. 80 | if ( 81 | !( 82 | renderer.context.getExtension("OES_texture_half_float") && 83 | renderer.context.getExtension("OES_texture_half_float_linear") 84 | ) 85 | ) { 86 | alert("This demo is not supported on your device."); 87 | } 88 | 89 | const resolution = new Vector2( 90 | configuration.Scale * window.innerWidth, 91 | configuration.Scale * window.innerHeight 92 | ); 93 | const aspect = new Vector2(resolution.x / resolution.y, 1.0); 94 | 95 | // RenderTargets initialization. 96 | const velocityRT = new RenderTarget(resolution, 2, RGBFormat, HalfFloatType); 97 | const divergenceRT = new RenderTarget(resolution, 1, RGBFormat, HalfFloatType); 98 | const pressureRT = new RenderTarget(resolution, 2, RGBFormat, HalfFloatType); 99 | const colorRT = new RenderTarget(resolution, 2, RGBFormat, UnsignedByteType); 100 | 101 | // These variables are used to store the result the result of the different 102 | // render passes. Not needed but nice for convenience. 103 | let c: Texture; 104 | let v: Texture; 105 | let d: Texture; 106 | let p: Texture; 107 | 108 | // Render passes initialization. 109 | const velocityInitPass = new VelocityInitPass(renderer, resolution); 110 | const velocityInitTexture = velocityInitPass.render(); 111 | const colorInitPass = new ColorInitPass(renderer, resolution); 112 | const colorInitTexture = colorInitPass.render(); 113 | const velocityAdvectionPass = new AdvectionPass( 114 | velocityInitTexture, 115 | velocityInitTexture, 116 | 0 117 | ); 118 | const colorAdvectionPass = new AdvectionPass( 119 | velocityInitTexture, 120 | colorInitTexture, 121 | configuration.ColorDecay 122 | ); 123 | const touchForceAdditionPass = new TouchForcePass( 124 | resolution, 125 | configuration.Radius 126 | ); 127 | const touchColorAdditionPass = new TouchColorPass( 128 | resolution, 129 | configuration.Radius 130 | ); 131 | const velocityBoundary = new BoundaryPass(); 132 | const velocityDivergencePass = new DivergencePass(); 133 | const pressurePass = new JacobiIterationsPass(); 134 | const pressureSubstractionPass = new GradientSubstractionPass(); 135 | const compositionPass = new CompositionPass(); 136 | 137 | // Event listeners (resizing and mouse/touch input). 138 | window.addEventListener("resize", (event: UIEvent) => { 139 | renderer.setSize(window.innerWidth, window.innerHeight); 140 | renderer.setPixelRatio(window.devicePixelRatio); 141 | 142 | resolution.set( 143 | configuration.Scale * window.innerWidth, 144 | configuration.Scale * window.innerHeight 145 | ); 146 | velocityRT.resize(resolution); 147 | divergenceRT.resize(resolution); 148 | pressureRT.resize(resolution); 149 | colorRT.resize(resolution); 150 | 151 | aspect.set(resolution.x / resolution.y, 1.0); 152 | touchForceAdditionPass.update({ aspect }); 153 | touchColorAdditionPass.update({ aspect }); 154 | }); 155 | 156 | window.addEventListener("keyup", (event: KeyboardEvent) => { 157 | if (event.keyCode === 72) { 158 | stats.dom.hidden = !stats.dom.hidden; 159 | } 160 | }); 161 | 162 | interface ITouchInput { 163 | id: string | number; 164 | input: Vector4; 165 | } 166 | 167 | let inputTouches: ITouchInput[] = []; 168 | canvas.addEventListener("mousedown", (event: MouseEvent) => { 169 | if (event.button === 0) { 170 | const x = (event.clientX / canvas.clientWidth) * aspect.x; 171 | const y = 1.0 - (event.clientY + window.scrollY) / canvas.clientHeight; 172 | inputTouches.push({ 173 | id: "mouse", 174 | input: new Vector4(x, y, 0, 0) 175 | }); 176 | } 177 | }); 178 | canvas.addEventListener("mousemove", (event: MouseEvent) => { 179 | if (inputTouches.length > 0) { 180 | const x = (event.clientX / canvas.clientWidth) * aspect.x; 181 | const y = 1.0 - (event.clientY + window.scrollY) / canvas.clientHeight; 182 | inputTouches[0].input 183 | .setZ(x - inputTouches[0].input.x) 184 | .setW(y - inputTouches[0].input.y); 185 | inputTouches[0].input.setX(x).setY(y); 186 | } 187 | }); 188 | canvas.addEventListener("mouseup", (event: MouseEvent) => { 189 | if (event.button === 0) { 190 | inputTouches.pop(); 191 | } 192 | }); 193 | 194 | canvas.addEventListener("touchstart", (event: TouchEvent) => { 195 | for (const touch of event.changedTouches) { 196 | const x = (touch.clientX / canvas.clientWidth) * aspect.x; 197 | const y = 1.0 - (touch.clientY + window.scrollY) / canvas.clientHeight; 198 | inputTouches.push({ 199 | id: touch.identifier, 200 | input: new Vector4(x, y, 0, 0) 201 | }); 202 | } 203 | }); 204 | 205 | canvas.addEventListener("touchmove", (event: TouchEvent) => { 206 | event.preventDefault(); 207 | for (const touch of event.changedTouches) { 208 | const registeredTouch = inputTouches.find(value => { 209 | return value.id === touch.identifier; 210 | }); 211 | if (registeredTouch !== undefined) { 212 | const x = (touch.clientX / canvas.clientWidth) * aspect.x; 213 | const y = 1.0 - (touch.clientY + window.scrollY) / canvas.clientHeight; 214 | registeredTouch.input 215 | .setZ(x - registeredTouch.input.x) 216 | .setW(y - registeredTouch.input.y); 217 | registeredTouch.input.setX(x).setY(y); 218 | } 219 | } 220 | }); 221 | 222 | canvas.addEventListener("touchend", (event: TouchEvent) => { 223 | for (const touch of event.changedTouches) { 224 | const registeredTouch = inputTouches.find(value => { 225 | return value.id === touch.identifier; 226 | }); 227 | if (registeredTouch !== undefined) { 228 | inputTouches = inputTouches.filter(value => { 229 | return value.id !== registeredTouch.id; 230 | }); 231 | } 232 | } 233 | }); 234 | 235 | canvas.addEventListener("touchcancel", (event: TouchEvent) => { 236 | for (let i = 0; i < inputTouches.length; ++i) { 237 | for (let j = 0; j < event.touches.length; ++j) { 238 | if (inputTouches[i].id === event.touches.item(j).identifier) { 239 | break; 240 | } else if (j === event.touches.length - 1) { 241 | inputTouches.splice(i--, 1); 242 | } 243 | } 244 | } 245 | }); 246 | 247 | // Dat.GUI configuration. 248 | function loadGradients() { 249 | const textureLoader = new TextureLoader().setPath("./resources/"); 250 | for (let i = 0; i < gradients.length; ++i) { 251 | textureLoader.load(gradients[i], (texture: Texture) => { 252 | gradientTextures[i] = texture; 253 | }); 254 | } 255 | } 256 | 257 | // Dat.GUI configuration. 258 | function initGUI() { 259 | const sim = gui.addFolder("Simulation"); 260 | sim 261 | .add(configuration, "Scale", 0.1, 2.0, 0.1) 262 | .onFinishChange((value: number) => { 263 | resolution.set( 264 | configuration.Scale * window.innerWidth, 265 | configuration.Scale * window.innerHeight 266 | ); 267 | velocityRT.resize(resolution); 268 | divergenceRT.resize(resolution); 269 | pressureRT.resize(resolution); 270 | colorRT.resize(resolution); 271 | }); 272 | sim.add(configuration, "Iterations", 16, 128, 1); 273 | sim.add(configuration, "ColorDecay", 0.0, 0.1, 0.01); 274 | sim 275 | .add(configuration, "Timestep", ["1/15", "1/30", "1/60", "1/90", "1/120"]) 276 | .onChange((value: string) => { 277 | switch (value) { 278 | case "1/15": 279 | dt = 1 / 15; 280 | break; 281 | case "1/30": 282 | dt = 1 / 30; 283 | break; 284 | case "1/60": 285 | dt = 1 / 60; 286 | break; 287 | case "1/90": 288 | dt = 1 / 90; 289 | break; 290 | case "1/120": 291 | dt = 1 / 120; 292 | break; 293 | } 294 | }); 295 | sim.add(configuration, "Simulate"); 296 | sim.add(configuration, "Boundaries"); 297 | sim.add(configuration, "Reset"); 298 | 299 | const input = gui.addFolder("Input"); 300 | input.add(configuration, "Radius", 0.1, 1, 0.1); 301 | input.add(configuration, "AddColor"); 302 | 303 | gui.add(configuration, "Visualize", [ 304 | "Color", 305 | "Velocity", 306 | "Divergence", 307 | "Pressure" 308 | ]); 309 | gui.add(configuration, "Mode", [ 310 | "Normal", 311 | "Luminance", 312 | "Spectral", 313 | "Gradient" 314 | ]); 315 | 316 | const github = gui.add(configuration, "Github"); 317 | github.__li.className = "guiIconText"; 318 | github.__li.style.borderLeft = "3px solid #8C8C8C"; 319 | const githubIcon = document.createElement("span"); 320 | githubIcon.className = "guiIcon github"; 321 | github.domElement.parentElement.appendChild(githubIcon); 322 | 323 | const twitter = gui.add(configuration, "Twitter"); 324 | twitter.__li.className = "guiIconText"; 325 | twitter.__li.style.borderLeft = "3px solid #8C8C8C"; 326 | const twitterIcon = document.createElement("span"); 327 | twitterIcon.className = "guiIcon twitter"; 328 | twitter.domElement.parentElement.appendChild(twitterIcon); 329 | } 330 | 331 | // Render loop. 332 | function render() { 333 | if (configuration.Simulate) { 334 | // Advect the velocity vector field. 335 | velocityAdvectionPass.update({ timeDelta: dt }); 336 | v = velocityRT.set(renderer); 337 | renderer.render(velocityAdvectionPass.scene, camera); 338 | 339 | // Add external forces/colors according to input. 340 | if (inputTouches.length > 0) { 341 | touchForceAdditionPass.update({ 342 | touches: inputTouches, 343 | radius: configuration.Radius, 344 | velocity: v 345 | }); 346 | v = velocityRT.set(renderer); 347 | renderer.render(touchForceAdditionPass.scene, camera); 348 | 349 | if (configuration.AddColor) { 350 | touchColorAdditionPass.update({ 351 | touches: inputTouches, 352 | radius: configuration.Radius, 353 | color: c 354 | }); 355 | c = colorRT.set(renderer); 356 | renderer.render(touchColorAdditionPass.scene, camera); 357 | } 358 | } 359 | 360 | // Add velocity boundaries (simulation walls). 361 | if (configuration.Boundaries) { 362 | velocityBoundary.update({ velocity: v }); 363 | v = velocityRT.set(renderer); 364 | renderer.render(velocityBoundary.scene, camera); 365 | } 366 | 367 | // Compute the divergence of the advected velocity vector field. 368 | velocityDivergencePass.update({ 369 | timeDelta: dt, 370 | velocity: v 371 | }); 372 | d = divergenceRT.set(renderer); 373 | renderer.render(velocityDivergencePass.scene, camera); 374 | 375 | // Compute the pressure gradient of the advected velocity vector field (using 376 | // jacobi iterations). 377 | pressurePass.update({ divergence: d }); 378 | for (let i = 0; i < configuration.Iterations; ++i) { 379 | p = pressureRT.set(renderer); 380 | renderer.render(pressurePass.scene, camera); 381 | pressurePass.update({ previousIteration: p }); 382 | } 383 | 384 | // Substract the pressure gradient from to obtain a velocity vector field with 385 | // zero divergence. 386 | pressureSubstractionPass.update({ 387 | timeDelta: dt, 388 | velocity: v, 389 | pressure: p 390 | }); 391 | v = velocityRT.set(renderer); 392 | renderer.render(pressureSubstractionPass.scene, camera); 393 | 394 | // Advect the color buffer with the divergence-free velocity vector field. 395 | colorAdvectionPass.update({ 396 | timeDelta: dt, 397 | inputTexture: c, 398 | velocity: v, 399 | decay: configuration.ColorDecay 400 | }); 401 | c = colorRT.set(renderer); 402 | renderer.render(colorAdvectionPass.scene, camera); 403 | 404 | // Feed the input of the advection passes with the last advected results. 405 | velocityAdvectionPass.update({ 406 | inputTexture: v, 407 | velocity: v 408 | }); 409 | colorAdvectionPass.update({ 410 | inputTexture: c 411 | }); 412 | } 413 | 414 | // Render to the main framebuffer the desired visualization. 415 | renderer.setRenderTarget(null); 416 | let visualization; 417 | switch (configuration.Visualize) { 418 | case "Color": 419 | visualization = c; 420 | break; 421 | case "Velocity": 422 | visualization = v; 423 | break; 424 | case "Divergence": 425 | visualization = d; 426 | break; 427 | case "Pressure": 428 | visualization = p; 429 | break; 430 | } 431 | compositionPass.update({ 432 | colorBuffer: visualization, 433 | mode: configuration.Mode, 434 | gradient: gradientTextures[0] 435 | }); 436 | renderer.render(compositionPass.scene, camera); 437 | } 438 | function animate() { 439 | requestAnimationFrame(animate); 440 | stats.begin(); 441 | render(); 442 | stats.end(); 443 | } 444 | animate(); 445 | -------------------------------------------------------------------------------- /src/passes/AdvectionPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform 9 | } from "three"; 10 | 11 | export class AdvectionPass { 12 | public readonly scene: Scene; 13 | 14 | private material: RawShaderMaterial; 15 | private mesh: Mesh; 16 | 17 | constructor( 18 | readonly initialVelocity: Texture, 19 | readonly initialValue: Texture, 20 | readonly decay: number 21 | ) { 22 | this.scene = new Scene(); 23 | 24 | const geometry = new BufferGeometry(); 25 | geometry.setAttribute( 26 | "position", 27 | new BufferAttribute( 28 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 29 | 2 30 | ) 31 | ); 32 | this.material = new RawShaderMaterial({ 33 | uniforms: { 34 | timeDelta: new Uniform(0.0), 35 | inputTexture: new Uniform(initialValue), 36 | velocity: new Uniform(initialVelocity), 37 | decay: new Uniform(decay) 38 | }, 39 | vertexShader: ` 40 | attribute vec2 position; 41 | varying vec2 vUV; 42 | 43 | void main() { 44 | vUV = position * 0.5 + 0.5; 45 | gl_Position = vec4(position, 0.0, 1.0); 46 | }`, 47 | fragmentShader: ` 48 | precision highp float; 49 | precision highp int; 50 | varying vec2 vUV; 51 | uniform float timeDelta; 52 | uniform sampler2D inputTexture; 53 | uniform sampler2D velocity; 54 | uniform float decay; 55 | 56 | void main() { 57 | vec2 prevUV = fract(vUV - timeDelta * texture2D(velocity, vUV).xy); 58 | gl_FragColor = texture2D(inputTexture, prevUV) * (1.0 - decay); 59 | }`, 60 | depthTest: false, 61 | depthWrite: false 62 | }); 63 | this.mesh = new Mesh(geometry, this.material); 64 | this.mesh.frustumCulled = false; // Just here to silence a console error. 65 | 66 | this.scene.add(this.mesh); 67 | } 68 | 69 | public update(uniforms: any): void { 70 | if (uniforms.timeDelta !== undefined) { 71 | this.material.uniforms.timeDelta.value = uniforms.timeDelta; 72 | } 73 | if (uniforms.inputTexture !== undefined) { 74 | this.material.uniforms.inputTexture.value = uniforms.inputTexture; 75 | } 76 | if (uniforms.velocity !== undefined) { 77 | this.material.uniforms.velocity.value = uniforms.velocity; 78 | } 79 | if (uniforms.decay !== undefined) { 80 | this.material.uniforms.decay.value = uniforms.decay; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/passes/BoundaryPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform, 9 | Vector2 10 | } from "three"; 11 | 12 | export class BoundaryPass { 13 | public readonly scene: Scene; 14 | 15 | private material: RawShaderMaterial; 16 | private mesh: Mesh; 17 | 18 | constructor() { 19 | this.scene = new Scene(); 20 | 21 | const geometry = new BufferGeometry(); 22 | geometry.setAttribute( 23 | "position", 24 | new BufferAttribute( 25 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 26 | 2 27 | ) 28 | ); 29 | this.material = new RawShaderMaterial({ 30 | uniforms: { 31 | velocity: new Uniform(Texture.DEFAULT_IMAGE) 32 | }, 33 | vertexShader: ` 34 | attribute vec2 position; 35 | varying vec2 vUV; 36 | 37 | void main() { 38 | vUV = position * 0.5 + 0.5; 39 | gl_Position = vec4(position, 0.0, 1.0); 40 | }`, 41 | fragmentShader: ` 42 | precision highp float; 43 | precision highp int; 44 | varying vec2 vUV; 45 | uniform sampler2D velocity; 46 | 47 | void main() { 48 | vec2 texelSize = vec2(dFdx(vUV.x), dFdy(vUV.y)); 49 | 50 | float leftEdgeMask = ceil(texelSize.x - vUV.x); 51 | float bottomEdgeMask = ceil(texelSize.y - vUV.y); 52 | float rightEdgeMask = ceil(vUV.x - (1.0 - texelSize.x)); 53 | float topEdgeMask = ceil(vUV.y - (1.0 - texelSize.y)); 54 | float mask = clamp(leftEdgeMask + bottomEdgeMask + rightEdgeMask + topEdgeMask, 0.0, 1.0); 55 | float direction = mix(1.0, -1.0, mask); 56 | 57 | gl_FragColor = texture2D(velocity, vUV) * direction; 58 | }`, 59 | depthTest: false, 60 | depthWrite: false, 61 | extensions: { derivatives: true } 62 | }); 63 | this.mesh = new Mesh(geometry, this.material); 64 | this.mesh.frustumCulled = false; // Just here to silence a console error. 65 | this.scene.add(this.mesh); 66 | } 67 | 68 | public update(uniforms: any): void { 69 | if (uniforms.position !== undefined) { 70 | this.material.uniforms.position.value = uniforms.position; 71 | } 72 | if (uniforms.direction !== undefined) { 73 | this.material.uniforms.direction.value = uniforms.direction; 74 | } 75 | if (uniforms.radius !== undefined) { 76 | this.material.uniforms.radius.value = uniforms.radius; 77 | } 78 | if (uniforms.velocity !== undefined) { 79 | this.material.uniforms.velocity.value = uniforms.velocity; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/passes/ColorInitPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | OrthographicCamera, 6 | RawShaderMaterial, 7 | RGBFormat, 8 | Scene, 9 | Texture, 10 | Uniform, 11 | UnsignedByteType, 12 | Vector2, 13 | WebGLRenderer, 14 | WebGLRenderTarget 15 | } from "three"; 16 | 17 | export class ColorInitPass { 18 | public readonly scene: Scene; 19 | public readonly camera: OrthographicCamera; 20 | 21 | private material: RawShaderMaterial; 22 | private mesh: Mesh; 23 | 24 | private renderTarget: WebGLRenderTarget; 25 | 26 | constructor(readonly renderer: WebGLRenderer, readonly resolution: Vector2) { 27 | this.scene = new Scene(); 28 | this.camera = new OrthographicCamera(0, 0, 0, 0, 0, 0); 29 | 30 | this.renderTarget = new WebGLRenderTarget(resolution.x, resolution.y, { 31 | format: RGBFormat, 32 | type: UnsignedByteType, 33 | depthBuffer: false, 34 | stencilBuffer: false 35 | }); 36 | 37 | const geometry = new BufferGeometry(); 38 | geometry.setAttribute( 39 | "position", 40 | new BufferAttribute( 41 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 42 | 2 43 | ) 44 | ); 45 | this.material = new RawShaderMaterial({ 46 | uniforms: { 47 | scale: new Uniform( 48 | window.innerWidth > window.innerHeight 49 | ? new Vector2(window.innerWidth / window.innerHeight, 1.0) 50 | : new Vector2(1.0, window.innerHeight / window.innerWidth) 51 | ) 52 | }, 53 | vertexShader: ` 54 | attribute vec2 position; 55 | varying vec2 clipPos; 56 | 57 | void main() { 58 | clipPos = position; 59 | gl_Position = vec4(position, 0.0, 1.0); 60 | }`, 61 | fragmentShader: ` 62 | precision highp float; 63 | precision highp int; 64 | varying vec2 clipPos; 65 | 66 | void main() { 67 | vec3 color = vec3(clipPos * 0.5 + 0.5, 0.0); 68 | gl_FragColor = vec4(color, 1.0); 69 | }`, 70 | depthTest: false, 71 | depthWrite: false 72 | }); 73 | this.mesh = new Mesh(geometry, this.material); 74 | this.mesh.frustumCulled = false; // Just here to silence a console error. 75 | this.scene.add(this.mesh); 76 | } 77 | 78 | public update(uniforms: any): void { 79 | if (uniforms.width !== undefined && uniforms.height !== undefined) { 80 | this.renderTarget.setSize(uniforms.width, uniforms.height); 81 | 82 | const isWider = window.innerWidth > window.innerHeight; 83 | isWider 84 | ? this.material.uniforms.scale.value.set( 85 | window.innerWidth / window.innerHeight, 86 | 1.0 87 | ) 88 | : this.material.uniforms.scale.value.set( 89 | 1.0, 90 | window.innerHeight / window.innerWidth 91 | ); 92 | } 93 | } 94 | 95 | public render(): Texture { 96 | this.renderer.setRenderTarget(this.renderTarget); 97 | this.renderer.render(this.scene, this.camera); 98 | return this.renderTarget.texture; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/passes/CompositionPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform 9 | } from "three"; 10 | 11 | export class CompositionPass { 12 | public readonly scene: Scene; 13 | 14 | private material: RawShaderMaterial; 15 | private mesh: Mesh; 16 | 17 | constructor() { 18 | this.scene = new Scene(); 19 | 20 | const geometry = new BufferGeometry(); 21 | geometry.setAttribute( 22 | "position", 23 | new BufferAttribute( 24 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 25 | 2 26 | ) 27 | ); 28 | this.material = new RawShaderMaterial({ 29 | uniforms: { 30 | colorBuffer: new Uniform(Texture.DEFAULT_IMAGE), 31 | gradient: new Uniform(Texture.DEFAULT_IMAGE) 32 | }, 33 | defines: { 34 | MODE: 0 35 | }, 36 | vertexShader: ` 37 | attribute vec2 position; 38 | varying vec2 vUV; 39 | 40 | void main() { 41 | vUV = position * 0.5 + 0.5; 42 | gl_Position = vec4(position, 0.0, 1.0); 43 | }`, 44 | fragmentShader: ` 45 | precision highp float; 46 | precision highp int; 47 | 48 | varying vec2 vUV; 49 | uniform sampler2D colorBuffer; 50 | uniform sampler2D gradient; 51 | 52 | const vec3 W = vec3(0.2125, 0.7154, 0.0721); 53 | float luminance(in vec3 color) { 54 | return dot(color, W); 55 | } 56 | 57 | // Based on code by Spektre posted at http://stackoverflow.com/questions/3407942/rgb-values-of-visible-spectrum 58 | vec4 spectral(float l) // RGB <0,1> <- lambda l <400,700> [nm] 59 | { 60 | float r=0.0,g=0.0,b=0.0; 61 | if ((l>=400.0)&&(l<410.0)) { float t=(l-400.0)/(410.0-400.0); r= +(0.33*t)-(0.20*t*t); } 62 | else if ((l>=410.0)&&(l<475.0)) { float t=(l-410.0)/(475.0-410.0); r=0.14 -(0.13*t*t); } 63 | else if ((l>=545.0)&&(l<595.0)) { float t=(l-545.0)/(595.0-545.0); r= +(1.98*t)-( t*t); } 64 | else if ((l>=595.0)&&(l<650.0)) { float t=(l-595.0)/(650.0-595.0); r=0.98+(0.06*t)-(0.40*t*t); } 65 | else if ((l>=650.0)&&(l<700.0)) { float t=(l-650.0)/(700.0-650.0); r=0.65-(0.84*t)+(0.20*t*t); } 66 | if ((l>=415.0)&&(l<475.0)) { float t=(l-415.0)/(475.0-415.0); g= +(0.80*t*t); } 67 | else if ((l>=475.0)&&(l<590.0)) { float t=(l-475.0)/(590.0-475.0); g=0.8 +(0.76*t)-(0.80*t*t); } 68 | else if ((l>=585.0)&&(l<639.0)) { float t=(l-585.0)/(639.0-585.0); g=0.82-(0.80*t) ; } 69 | if ((l>=400.0)&&(l<475.0)) { float t=(l-400.0)/(475.0-400.0); b= +(2.20*t)-(1.50*t*t); } 70 | else if ((l>=475.0)&&(l<560.0)) { float t=(l-475.0)/(560.0-475.0); b=0.7 -( t)+(0.30*t*t); } 71 | 72 | return vec4(r, g, b, 1.0); 73 | } 74 | 75 | void main() { 76 | vec4 color = texture2D(colorBuffer, vUV); 77 | float lum = luminance(abs(color.rgb)); 78 | #if MODE == 0 79 | gl_FragColor = color; 80 | #elif MODE == 1 81 | gl_FragColor = vec4(lum); 82 | #elif MODE == 2 83 | gl_FragColor = spectral(mix(340.0, 700.0, lum)); 84 | #elif MODE == 3 85 | gl_FragColor = texture2D(gradient, vec2(lum, 0.0)); 86 | #endif 87 | }`, 88 | depthTest: false, 89 | depthWrite: false, 90 | transparent: true 91 | }); 92 | this.mesh = new Mesh(geometry, this.material); 93 | this.mesh.frustumCulled = false; // Just here to silence a console error. 94 | this.scene.add(this.mesh); 95 | } 96 | 97 | public update(uniforms: any): void { 98 | if (uniforms.colorBuffer !== undefined) { 99 | this.material.uniforms.colorBuffer.value = uniforms.colorBuffer; 100 | } 101 | if (uniforms.mode !== undefined) { 102 | let mode = 0; 103 | switch (uniforms.mode) { 104 | case "Luminance": 105 | mode = 1; 106 | break; 107 | case "Spectral": 108 | mode = 2; 109 | break; 110 | case "Gradient": 111 | mode = 3; 112 | break; 113 | case "Normal": 114 | default: 115 | } 116 | if (mode !== this.material.defines.MODE) { 117 | this.material.defines.MODE = mode; 118 | this.material.needsUpdate = true; 119 | } 120 | } 121 | if (uniforms.gradient !== undefined) { 122 | this.material.uniforms.gradient.value = uniforms.gradient; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/passes/DivergencePass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform 9 | } from "three"; 10 | 11 | export class DivergencePass { 12 | public readonly scene: Scene; 13 | 14 | private material: RawShaderMaterial; 15 | private mesh: Mesh; 16 | 17 | constructor() { 18 | this.scene = new Scene(); 19 | 20 | const geometry = new BufferGeometry(); 21 | geometry.setAttribute( 22 | "position", 23 | new BufferAttribute( 24 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 25 | 2 26 | ) 27 | ); 28 | this.material = new RawShaderMaterial({ 29 | uniforms: { 30 | timeDelta: new Uniform(0.0), 31 | velocity: new Uniform(Texture.DEFAULT_IMAGE) 32 | }, 33 | vertexShader: ` 34 | attribute vec2 position; 35 | varying vec2 vUV; 36 | 37 | void main() { 38 | vUV = position * 0.5 + 0.5; 39 | gl_Position = vec4(position, 0.0, 1.0); 40 | }`, 41 | fragmentShader: ` 42 | precision highp float; 43 | precision highp int; 44 | varying vec2 vUV; 45 | uniform float timeDelta; 46 | uniform sampler2D velocity; 47 | 48 | void main() { 49 | vec2 texelSize = vec2(dFdx(vUV.x), dFdy(vUV.y)); 50 | 51 | float x0 = texture2D(velocity, vUV - vec2(texelSize.x, 0)).x; 52 | float x1 = texture2D(velocity, vUV + vec2(texelSize.x, 0)).x; 53 | float y0 = texture2D(velocity, vUV - vec2(0, texelSize.y)).y; 54 | float y1 = texture2D(velocity, vUV + vec2(0, texelSize.y)).y; 55 | float divergence = ( x1 - x0 + y1 - y0) * 0.5; 56 | 57 | gl_FragColor = vec4(divergence); 58 | }`, 59 | depthTest: false, 60 | depthWrite: false, 61 | extensions: { derivatives: true } 62 | }); 63 | this.mesh = new Mesh(geometry, this.material); 64 | this.mesh.frustumCulled = false; // Just here to silence a console error. 65 | this.scene.add(this.mesh); 66 | } 67 | 68 | public update(uniforms: any): void { 69 | if (uniforms.timeDelta !== undefined) { 70 | this.material.uniforms.timeDelta.value = uniforms.timeDelta; 71 | } 72 | if (uniforms.density !== undefined) { 73 | this.material.uniforms.density.value = uniforms.density; 74 | } 75 | if (uniforms.velocity !== undefined) { 76 | this.material.uniforms.velocity.value = uniforms.velocity; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/passes/GradientSubstractionPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform 9 | } from "three"; 10 | 11 | export class GradientSubstractionPass { 12 | public readonly scene: Scene; 13 | 14 | private material: RawShaderMaterial; 15 | private mesh: Mesh; 16 | 17 | constructor() { 18 | this.scene = new Scene(); 19 | 20 | const geometry = new BufferGeometry(); 21 | geometry.setAttribute( 22 | "position", 23 | new BufferAttribute( 24 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 25 | 2 26 | ) 27 | ); 28 | this.material = new RawShaderMaterial({ 29 | uniforms: { 30 | timeDelta: new Uniform(0.0), 31 | velocity: new Uniform(Texture.DEFAULT_IMAGE), 32 | pressure: new Uniform(Texture.DEFAULT_IMAGE) 33 | }, 34 | vertexShader: ` 35 | attribute vec2 position; 36 | varying vec2 vUV; 37 | 38 | void main() { 39 | vUV = position * 0.5 + 0.5; 40 | gl_Position = vec4(position, 0.0, 1.0); 41 | }`, 42 | fragmentShader: ` 43 | precision highp float; 44 | precision highp int; 45 | varying vec2 vUV; 46 | uniform float timeDelta; 47 | uniform sampler2D velocity; 48 | uniform sampler2D pressure; 49 | 50 | void main() { 51 | vec2 texelSize = vec2(dFdx(vUV.x), dFdy(vUV.y)); 52 | 53 | float x0 = texture2D(pressure, vUV - vec2(texelSize.x, 0)).r; 54 | float x1 = texture2D(pressure, vUV + vec2(texelSize.x, 0)).r; 55 | float y0 = texture2D(pressure, vUV - vec2(0, texelSize.y)).r; 56 | float y1 = texture2D(pressure, vUV + vec2(0, texelSize.y)).r; 57 | 58 | vec2 v = texture2D(velocity, vUV).xy; 59 | v -= 0.5 * vec2(x1 - x0, y1 - y0); 60 | 61 | gl_FragColor = vec4(v, 0.0, 1.0); 62 | }`, 63 | depthTest: false, 64 | depthWrite: false, 65 | extensions: { derivatives: true } 66 | }); 67 | this.mesh = new Mesh(geometry, this.material); 68 | this.mesh.frustumCulled = false; // Just here to silence a console error. 69 | this.scene.add(this.mesh); 70 | } 71 | 72 | public update(uniforms: any): void { 73 | if (uniforms.timeDelta !== undefined) { 74 | this.material.uniforms.timeDelta.value = uniforms.timeDelta; 75 | } 76 | if (uniforms.density !== undefined) { 77 | this.material.uniforms.density.value = uniforms.density; 78 | } 79 | if (uniforms.velocity !== undefined) { 80 | this.material.uniforms.velocity.value = uniforms.velocity; 81 | } 82 | if (uniforms.pressure !== undefined) { 83 | this.material.uniforms.pressure.value = uniforms.pressure; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/passes/JacobiIterationsPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform 9 | } from "three"; 10 | 11 | export class JacobiIterationsPass { 12 | public readonly scene: Scene; 13 | 14 | private material: RawShaderMaterial; 15 | private mesh: Mesh; 16 | 17 | constructor() { 18 | this.scene = new Scene(); 19 | 20 | const geometry = new BufferGeometry(); 21 | geometry.setAttribute( 22 | "position", 23 | new BufferAttribute( 24 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 25 | 2 26 | ) 27 | ); 28 | this.material = new RawShaderMaterial({ 29 | uniforms: { 30 | alpha: new Uniform(-1.0), // TODO: Configure this parameters accordingly! 31 | beta: new Uniform(0.25), 32 | previousIteration: new Uniform(Texture.DEFAULT_IMAGE), 33 | divergence: new Uniform(Texture.DEFAULT_IMAGE) 34 | }, 35 | vertexShader: ` 36 | attribute vec2 position; 37 | varying vec2 vUV; 38 | 39 | void main() { 40 | vUV = position * 0.5 + 0.5; 41 | gl_Position = vec4(position, 0.0, 1.0); 42 | }`, 43 | fragmentShader: ` 44 | precision highp float; 45 | precision highp int; 46 | varying vec2 vUV; 47 | uniform float alpha; 48 | uniform float beta; 49 | uniform sampler2D previousIteration; 50 | uniform sampler2D divergence; 51 | 52 | void main() { 53 | vec2 texelSize = vec2(dFdx(vUV.x), dFdy(vUV.y)); 54 | 55 | vec4 x0 = texture2D(previousIteration, vUV - vec2(texelSize.x, 0)); 56 | vec4 x1 = texture2D(previousIteration, vUV + vec2(texelSize.x, 0)); 57 | vec4 y0 = texture2D(previousIteration, vUV - vec2(0, texelSize.y)); 58 | vec4 y1 = texture2D(previousIteration, vUV + vec2(0, texelSize.y)); 59 | vec4 d = texture2D(divergence, vUV); 60 | 61 | gl_FragColor = (x0 + x1 + y0 + y1 + alpha * d) * beta; 62 | }`, 63 | depthTest: false, 64 | depthWrite: false, 65 | extensions: { derivatives: true } 66 | }); 67 | this.mesh = new Mesh(geometry, this.material); 68 | this.mesh.frustumCulled = false; // Just here to silence a console error. 69 | this.scene.add(this.mesh); 70 | } 71 | 72 | public update(uniforms: any): void { 73 | if (uniforms.previousIteration !== undefined) { 74 | this.material.uniforms.previousIteration.value = 75 | uniforms.previousIteration; 76 | } 77 | if (uniforms.divergence !== undefined) { 78 | this.material.uniforms.divergence.value = uniforms.divergence; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/passes/TouchColorPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform, 9 | Vector2, 10 | Vector4 11 | } from "three"; 12 | 13 | const MAX_TOUCHES = 10; 14 | 15 | export class TouchColorPass { 16 | public readonly scene: Scene; 17 | 18 | private material: RawShaderMaterial; 19 | private mesh: Mesh; 20 | 21 | constructor(readonly resolution: Vector2, readonly radius: number) { 22 | this.scene = new Scene(); 23 | 24 | const geometry = new BufferGeometry(); 25 | geometry.setAttribute( 26 | "position", 27 | new BufferAttribute( 28 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 29 | 2 30 | ) 31 | ); 32 | this.material = new RawShaderMaterial({ 33 | uniforms: { 34 | aspect: new Uniform(new Vector2(resolution.x / resolution.y, 1.0)), 35 | input0: new Uniform(new Vector4()), 36 | input1: new Uniform(new Vector4()), 37 | input2: new Uniform(new Vector4()), 38 | input3: new Uniform(new Vector4()), 39 | input4: new Uniform(new Vector4()), 40 | input5: new Uniform(new Vector4()), 41 | input6: new Uniform(new Vector4()), 42 | input7: new Uniform(new Vector4()), 43 | input8: new Uniform(new Vector4()), 44 | input9: new Uniform(new Vector4()), 45 | radius: new Uniform(radius), 46 | color: new Uniform(Texture.DEFAULT_IMAGE) 47 | }, 48 | vertexShader: ` 49 | attribute vec2 position; 50 | varying vec2 vUV; 51 | varying vec2 vScaledUV; 52 | uniform vec2 aspect; 53 | 54 | void main() { 55 | vUV = position * 0.5 + 0.5; 56 | vScaledUV = position * aspect * 0.5 + aspect * 0.5; 57 | gl_Position = vec4(position, 0.0, 1.0); 58 | }`, 59 | fragmentShader: ` 60 | precision highp float; 61 | precision highp int; 62 | varying vec2 vUV; 63 | varying vec2 vScaledUV; 64 | uniform vec4 input0; 65 | uniform vec4 input1; 66 | uniform vec4 input2; 67 | uniform vec4 input3; 68 | uniform vec4 input4; 69 | uniform vec4 input5; 70 | uniform vec4 input6; 71 | uniform vec4 input7; 72 | uniform vec4 input8; 73 | uniform vec4 input9; 74 | uniform float radius; 75 | uniform sampler2D color; 76 | 77 | vec2 getColor(vec4 inputVec) { 78 | float d = distance(vScaledUV, inputVec.xy) / radius; 79 | float strength = 1.0 / max(d * d, 0.01); 80 | strength *= clamp(dot(normalize(vScaledUV - inputVec.xy), normalize(inputVec.zw)), 0.0, 1.0); 81 | return strength * abs(inputVec.zw) * radius; 82 | } 83 | 84 | void main() { 85 | vec4 touchColor = vec4(0.0); 86 | touchColor.xy += getColor(input0); 87 | touchColor.xy += getColor(input1); 88 | touchColor.xy += getColor(input2); 89 | touchColor.xy += getColor(input3); 90 | touchColor.xy += getColor(input4); 91 | touchColor.xy += getColor(input5); 92 | touchColor.xy += getColor(input6); 93 | touchColor.xy += getColor(input7); 94 | touchColor.xy += getColor(input8); 95 | touchColor.xy += getColor(input9); 96 | 97 | gl_FragColor = texture2D(color, vUV) + touchColor; 98 | }`, 99 | depthTest: false, 100 | depthWrite: false 101 | }); 102 | this.mesh = new Mesh(geometry, this.material); 103 | this.mesh.frustumCulled = false; // Just here to silence a console error. 104 | this.scene.add(this.mesh); 105 | } 106 | 107 | public update(uniforms: any): void { 108 | if (uniforms.aspect !== undefined) { 109 | this.material.uniforms.aspect.value = uniforms.aspect; 110 | } 111 | if (uniforms.touches !== undefined) { 112 | const touchMax = Math.min(MAX_TOUCHES, uniforms.touches.length); 113 | for (let i = 0; i < touchMax; ++i) { 114 | this.material.uniforms["input" + i].value = uniforms.touches[i].input; 115 | } 116 | for (let i = uniforms.touches.length; i < MAX_TOUCHES; ++i) { 117 | this.material.uniforms["input" + i].value.set(0, 0, 0, 0); 118 | } 119 | } 120 | if (uniforms.radius !== undefined) { 121 | this.material.uniforms.radius.value = uniforms.radius; 122 | } 123 | if (uniforms.color !== undefined) { 124 | this.material.uniforms.color.value = uniforms.color; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/passes/TouchForcePass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | RawShaderMaterial, 6 | Scene, 7 | Texture, 8 | Uniform, 9 | Vector2, 10 | Vector4 11 | } from "three"; 12 | 13 | const MAX_TOUCHES = 10; 14 | 15 | export class TouchForcePass { 16 | public readonly scene: Scene; 17 | 18 | private material: RawShaderMaterial; 19 | private mesh: Mesh; 20 | 21 | constructor(readonly resolution: Vector2, readonly radius: number) { 22 | this.scene = new Scene(); 23 | 24 | const geometry = new BufferGeometry(); 25 | geometry.setAttribute( 26 | "position", 27 | new BufferAttribute( 28 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 29 | 2 30 | ) 31 | ); 32 | this.material = new RawShaderMaterial({ 33 | uniforms: { 34 | aspect: new Uniform(new Vector2(resolution.x / resolution.y, 1.0)), 35 | input0: new Uniform(new Vector4()), 36 | input1: new Uniform(new Vector4()), 37 | input2: new Uniform(new Vector4()), 38 | input3: new Uniform(new Vector4()), 39 | input4: new Uniform(new Vector4()), 40 | input5: new Uniform(new Vector4()), 41 | input6: new Uniform(new Vector4()), 42 | input7: new Uniform(new Vector4()), 43 | input8: new Uniform(new Vector4()), 44 | input9: new Uniform(new Vector4()), 45 | radius: new Uniform(radius), 46 | velocity: new Uniform(Texture.DEFAULT_IMAGE) 47 | }, 48 | vertexShader: ` 49 | attribute vec2 position; 50 | varying vec2 vUV; 51 | varying vec2 vScaledUV; 52 | uniform vec2 aspect; 53 | 54 | void main() { 55 | vUV = position * 0.5 + 0.5; 56 | vScaledUV = position * aspect * 0.5 + aspect * 0.5; 57 | gl_Position = vec4(position, 0.0, 1.0); 58 | }`, 59 | fragmentShader: ` 60 | precision highp float; 61 | precision highp int; 62 | varying vec2 vUV; 63 | varying vec2 vScaledUV; 64 | uniform vec4 input0; 65 | uniform vec4 input1; 66 | uniform vec4 input2; 67 | uniform vec4 input3; 68 | uniform vec4 input4; 69 | uniform vec4 input5; 70 | uniform vec4 input6; 71 | uniform vec4 input7; 72 | uniform vec4 input8; 73 | uniform vec4 input9; 74 | uniform float radius; 75 | uniform sampler2D velocity; 76 | 77 | vec2 getForce(vec4 inputVec) { 78 | float d = distance(vScaledUV, inputVec.xy) / radius; 79 | float strength = 1.0 / max(d * d, 0.01); 80 | strength *= clamp(dot(normalize(vScaledUV - inputVec.xy), normalize(inputVec.zw)), 0.0, 1.0); 81 | return strength * inputVec.zw * radius; 82 | } 83 | 84 | void main() { 85 | vec4 touchForce = vec4(0.0); 86 | touchForce.xy += getForce(input0); 87 | touchForce.xy += getForce(input1); 88 | touchForce.xy += getForce(input2); 89 | touchForce.xy += getForce(input3); 90 | touchForce.xy += getForce(input4); 91 | touchForce.xy += getForce(input5); 92 | touchForce.xy += getForce(input6); 93 | touchForce.xy += getForce(input7); 94 | touchForce.xy += getForce(input8); 95 | touchForce.xy += getForce(input9); 96 | 97 | gl_FragColor = texture2D(velocity, vUV) + touchForce; 98 | }`, 99 | depthTest: false, 100 | depthWrite: false 101 | }); 102 | this.mesh = new Mesh(geometry, this.material); 103 | this.mesh.frustumCulled = false; // Just here to silence a console error. 104 | this.scene.add(this.mesh); 105 | } 106 | 107 | public update(uniforms: any): void { 108 | if (uniforms.aspect !== undefined) { 109 | this.material.uniforms.aspect.value = uniforms.aspect; 110 | } 111 | if (uniforms.touches !== undefined) { 112 | const touchMax = Math.min(MAX_TOUCHES, uniforms.touches.length); 113 | for (let i = 0; i < touchMax; ++i) { 114 | this.material.uniforms["input" + i].value = uniforms.touches[i].input; 115 | } 116 | for (let i = uniforms.touches.length; i < MAX_TOUCHES; ++i) { 117 | this.material.uniforms["input" + i].value.set(0, 0, 0, 0); 118 | } 119 | } 120 | if (uniforms.radius !== undefined) { 121 | this.material.uniforms.radius.value = uniforms.radius; 122 | } 123 | if (uniforms.velocity !== undefined) { 124 | this.material.uniforms.velocity.value = uniforms.velocity; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/passes/VelocityInitPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | HalfFloatType, 5 | Mesh, 6 | OrthographicCamera, 7 | RawShaderMaterial, 8 | RGBFormat, 9 | Scene, 10 | Texture, 11 | Uniform, 12 | Vector2, 13 | WebGLRenderer, 14 | WebGLRenderTarget 15 | } from "three"; 16 | 17 | export class VelocityInitPass { 18 | public readonly scene: Scene; 19 | public readonly camera: OrthographicCamera; 20 | 21 | private geometry: BufferGeometry; 22 | private material: RawShaderMaterial; 23 | private mesh: Mesh; 24 | 25 | private renderTarget: WebGLRenderTarget; 26 | 27 | constructor(readonly renderer: WebGLRenderer, readonly resolution: Vector2) { 28 | this.scene = new Scene(); 29 | this.camera = new OrthographicCamera(0, 0, 0, 0, 0, 0); 30 | 31 | this.renderTarget = new WebGLRenderTarget(resolution.x, resolution.y, { 32 | format: RGBFormat, 33 | type: HalfFloatType, 34 | depthBuffer: false, 35 | stencilBuffer: false 36 | }); 37 | 38 | this.geometry = new BufferGeometry(); 39 | this.geometry.setAttribute( 40 | "position", 41 | new BufferAttribute( 42 | new Float32Array([-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1]), 43 | 2 44 | ) 45 | ); 46 | this.material = new RawShaderMaterial({ 47 | uniforms: { 48 | scale: new Uniform( 49 | window.innerWidth > window.innerHeight 50 | ? new Vector2(window.innerWidth / window.innerHeight, 1.0) 51 | : new Vector2(1.0, window.innerHeight / window.innerWidth) 52 | ) 53 | }, 54 | vertexShader: ` 55 | attribute vec2 position; 56 | varying vec2 clipPos; 57 | 58 | void main() { 59 | clipPos = position; 60 | gl_Position = vec4(position, 0.0, 1.0); 61 | }`, 62 | fragmentShader: ` 63 | #define PI 3.1415926535897932384626433832795 64 | precision highp float; 65 | precision highp int; 66 | varying vec2 clipPos; 67 | 68 | void main() { 69 | vec2 v = vec2(sin(2.0 * PI * clipPos.y), sin(2.0 * PI * clipPos.x)); 70 | gl_FragColor = vec4(v, 0.0, 1.0); 71 | }`, 72 | depthTest: false, 73 | depthWrite: false 74 | }); 75 | this.mesh = new Mesh(this.geometry, this.material); 76 | this.mesh.frustumCulled = false; // Just here to silence a console error. 77 | this.scene.add(this.mesh); 78 | } 79 | 80 | public update(uniforms: any): void { 81 | if (uniforms.width !== undefined && uniforms.height !== undefined) { 82 | this.renderTarget.setSize(uniforms.width, uniforms.height); 83 | 84 | const isWider = window.innerWidth > window.innerHeight; 85 | isWider 86 | ? this.material.uniforms.scale.value.set( 87 | window.innerWidth / window.innerHeight, 88 | 1.0 89 | ) 90 | : this.material.uniforms.scale.value.set( 91 | 1.0, 92 | window.innerHeight / window.innerWidth 93 | ); 94 | } 95 | } 96 | 97 | public render(): Texture { 98 | this.renderer.setRenderTarget(this.renderTarget); 99 | this.renderer.render(this.scene, this.camera); 100 | return this.renderTarget.texture; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/resources/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amsXYZ/three-fluid-sim/1a03b276289d82f243db3cf780205da7661201ea/src/resources/gradient.jpg -------------------------------------------------------------------------------- /src/resources/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amsXYZ/three-fluid-sim/1a03b276289d82f243db3cf780205da7661201ea/src/resources/iconfont.ttf -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es2017", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-plugin-prettier", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "prettier": true, 9 | "object-literal-sort-keys": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack/common.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | target: "web", 5 | mode: "production", 6 | output: { 7 | filename: "[name].bundle.js", 8 | path: path.resolve(__dirname, "../dist") 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts(x?)$/, 14 | exclude: /node_modules/, 15 | loader: "ts-loader" 16 | } 17 | ] 18 | }, 19 | resolve: { 20 | extensions: [".ts", ".tsx", ".js", ".jsx"] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const path = require("path"); 3 | 4 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | 7 | const commonConfig = require("./common.config"); 8 | 9 | const config = merge(commonConfig, { 10 | mode: "development", 11 | module: { 12 | rules: [ 13 | { 14 | enforce: "pre", 15 | test: /\.js$/, 16 | loader: "source-map-loader" 17 | } 18 | ] 19 | }, 20 | devtool: "source-map", 21 | devServer: { 22 | contentBase: "./dist" 23 | }, 24 | plugins: [ 25 | new CopyWebpackPlugin([ 26 | { 27 | from: path.join(__dirname, "../src/resources"), 28 | to: "resources", 29 | toType: "dir" 30 | } 31 | ]), 32 | new HtmlWebpackPlugin({ 33 | template: "./src/index.html" 34 | }) 35 | ] 36 | }); 37 | 38 | module.exports = config; 39 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const path = require("path"); 3 | 4 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | 7 | const commonConfig = require("./common.config"); 8 | 9 | const config = merge(commonConfig, { 10 | plugins: [ 11 | new CopyWebpackPlugin([ 12 | { 13 | from: path.join(__dirname, "../src/resources"), 14 | to: "resources", 15 | toType: "dir" 16 | } 17 | ]), 18 | new HtmlWebpackPlugin({ 19 | template: "./src/index.html" 20 | }) 21 | ] 22 | }); 23 | 24 | module.exports = config; 25 | --------------------------------------------------------------------------------