├── .gitignore
├── screenshot.png
├── src
├── resources
│ ├── gradient.jpg
│ └── iconfont.ttf
├── index.html
├── RenderTarget.ts
├── passes
│ ├── AdvectionPass.ts
│ ├── DivergencePass.ts
│ ├── BoundaryPass.ts
│ ├── JacobiIterationsPass.ts
│ ├── GradientSubstractionPass.ts
│ ├── ColorInitPass.ts
│ ├── VelocityInitPass.ts
│ ├── TouchColorPass.ts
│ ├── TouchForcePass.ts
│ └── CompositionPass.ts
└── index.ts
├── tsconfig.json
├── tslint.json
├── webpack
├── common.config.js
├── prod.config.js
└── dev.config.js
├── .vscode
└── launch.json
├── .github
└── workflows
│ ├── nodejs.yml
│ └── ghpages.yml
├── README.md
├── LICENSE
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | *.DS_Store
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-fluid-sim/HEAD/screenshot.png
--------------------------------------------------------------------------------
/src/resources/gradient.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-fluid-sim/HEAD/src/resources/gradient.jpg
--------------------------------------------------------------------------------
/src/resources/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-fluid-sim/HEAD/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # three-fluid-sim
2 | 2D Fluid Simulation Three.js implementation.
3 |
4 | 
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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 2D Fluid Simulation
6 |
10 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------