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