10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Loïc Magne
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 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow one concurrent deployment
19 | concurrency:
20 | group: "pages"
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | # Single deploy job since we're just deploying
25 | deploy:
26 | environment:
27 | name: github-pages
28 | url: ${{ steps.deployment.outputs.page_url }}
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v3
33 | - name: Setup Pages
34 | uses: actions/configure-pages@v3
35 | - name: Upload artifact
36 | uses: actions/upload-pages-artifact@v1
37 | with:
38 | # Upload entire repository
39 | path: '.'
40 | - name: Deploy to GitHub Pages
41 | id: deployment
42 | uses: actions/deploy-pages@v1
43 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | webgl2 fluid simulation
9 |
10 |
49 |
50 |
51 |
52 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/script.min.js:
--------------------------------------------------------------------------------
1 | "use strict";var e=document.querySelector("#fluidsim_canvas");const v=e.getContext("webgl2");v.getExtension("EXT_color_buffer_float");let t={i:1,t:.5,o:"dye",v:.1,u:.99,l:.99,p:128,_:1024,h:20},o,r,c,s,u;function i(e){return o<1?{width:e,height:Math.round(e/o)}:{width:Math.round(e*o),height:e}}function x(){var e=v.canvas.clientWidth,n=v.canvas.clientHeight;if(o=v.canvas.clientWidth/v.canvas.clientHeight,v.canvas.width!==e||v.canvas.height!==n)return v.canvas.width=e,v.canvas.height=n,e=i(t.p),n=i(t._),r=e.width,c=e.height,s=n.width,u=n.height,1}x();let a=v.createVertexArray();var n=new Float32Array([-1,-1,1,1,1,-1,-1,-1,-1,1,1,1]),l=(l=a,n=n,A=2,O=v.FLOAT,w=!1,C=M=0,v.bindVertexArray(l),l=v.createBuffer(),v.bindBuffer(v.ARRAY_BUFFER,l),v.bufferData(v.ARRAY_BUFFER,n,v.STATIC_DRAW),v.enableVertexAttribArray(0),v.vertexAttribPointer(0,A,O,w,M,C),`#version 300 es
2 | in vec2 a_position;
3 | out vec2 v_position;
4 |
5 | void main() {
6 | v_position = a_position * 0.5 + 0.5;
7 | gl_Position = vec4(a_position, 0, 1);
8 | }`);function R(e,n,i){n=e.createShader(n);return e.shaderSource(n,i),e.compileShader(n),e.getShaderParameter(n,e.COMPILE_STATUS)?n:(console.error(e.getShaderInfoLog(n)),e.deleteShader(n),null)}function p(e,n){var i,t,e=R(v,v.VERTEX_SHADER,e),n=R(v,v.FRAGMENT_SHADER,n),e=(i=v,e=e,n=n,t=i.createProgram(),i.attachShader(t,e),i.attachShader(t,n),i.linkProgram(t),i.getProgramParameter(t,i.LINK_STATUS)?t:(console.error(i.getProgramInfoLog(t)),i.deleteProgram(t),null)),n=function(n,i){var t={},o=n.getProgramParameter(i,n.ACTIVE_UNIFORMS);for(let e=0;e 0.0 ? u_alpha : 1.0;
51 | res = coef * texture(u_x, v_position + dir * u_res);
52 | }`),d=p(l,`#version 300 es
53 | precision highp float;
54 |
55 | in vec2 v_position;
56 | uniform sampler2D u_x;
57 | uniform float u_alpha;
58 | out vec4 res;
59 |
60 | void main() {
61 | res = u_alpha * texture(u_x, v_position);
62 | }`),b=p(l,`#version 300 es
63 | precision highp float;
64 |
65 | in vec2 v_position;
66 | uniform sampler2D u_x;
67 | uniform vec2 u_point;
68 | uniform vec3 u_value;
69 | uniform float u_radius;
70 | uniform float u_ratio;
71 | out vec4 res;
72 |
73 | void main() {
74 | vec4 init = texture(u_x, v_position);
75 | vec2 v = v_position - u_point;
76 | v.x *= u_ratio;
77 | vec3 force = exp(-dot(v,v)/u_radius) * u_value;
78 |
79 | res = vec4(init.xyz + force, 1.);
80 | }`);function g(e,n,i,t,o,r){const c=v.createTexture(),s=(v.activeTexture(v.TEXTURE0),v.bindTexture(v.TEXTURE_2D,c),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_MIN_FILTER,r),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_MAG_FILTER,r),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_WRAP_S,v.CLAMP_TO_EDGE),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_WRAP_T,v.CLAMP_TO_EDGE),v.texImage2D(v.TEXTURE_2D,0,i,e,n,0,t,o,null),v.createFramebuffer());return v.bindFramebuffer(v.FRAMEBUFFER,s),v.framebufferTexture2D(v.FRAMEBUFFER,v.COLOR_ATTACHMENT0,v.TEXTURE_2D,c,0),{F:c,D:s,bind:()=>{v.bindFramebuffer(v.FRAMEBUFFER,s),v.viewport(0,0,e,n)},N:e=>(v.activeTexture(v.TEXTURE0+e),v.bindTexture(v.TEXTURE_2D,c),e)}}function F(e,n,i,t,o,r){return{read:g(e,n,i,t,o,r),write:g(e,n,i,t,o,r),S:function(){var e=this.read;this.read=this.write,this.write=e}}}function U(e,n,i,t,o,r,c){n=g(n,i,t,o,r,c);return v.useProgram(d.m),v.uniform1i(d.g.u_x,e.N(0)),I(n,v.TRIANGLES,6),n}function D(e,n,i,t,o,r,c){var s=F(n,i,t,o,r,c);return s.read=U(e.read,n,i,t,o,r,c),s.write=U(e.write,n,i,t,o,r,c),s}let y=F(r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST),N=F(r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),z=g(r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),Y=g(r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST),S=F(s,u,v.RGBA32F,v.RGBA,v.FLOAT,v.NEAREST);const q={F:null,D:null,bind:()=>{v.bindFramebuffer(v.FRAMEBUFFER,null),v.viewport(0,0,v.canvas.width,v.canvas.height)}};function I(e,n,i,t=!1){v.bindVertexArray(a),e.bind(),t&&(v.clearColor(0,0,0,1),v.clear(v.COLOR_BUFFER_BIT)),v.drawArrays(n,0,i)}function E(e,n){v.useProgram(m.m),v.uniform2f(m.g.u_res,1/r,1/c),v.uniform1i(m.g.u_x,e.read.N(0)),v.uniform1f(m.g.u_alpha,n),I(e.write,v.TRIANGLES,6),e.S()}let V=0;requestAnimationFrame(function e(n){var i=(n-V)/1e3,n=(V=n,x()&&(y=D(y,r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST),N=D(N,r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),S=D(S,s,u,v.RGBA32F,v.RGBA,v.FLOAT,v.NEAREST),z=g(r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),Y=g(r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST)),T.forEach(e=>{v.useProgram(b.m),v.uniform1i(b.g.u_x,y.read.N(0)),v.uniform2fv(b.g.u_point,[e.x,e.y]),v.uniform3fv(b.g.u_value,[e.dx*o,e.dy,0].map(e=>e*t.h)),v.uniform1f(b.g.u_radius,t.v),v.uniform1f(b.g.u_ratio,o),I(y.write,v.TRIANGLES,6),y.S(),v.uniform1i(b.g.u_x,S.read.N(0)),v.uniform3fv(b.g.u_value,e.color.map(e=>.2*e)),I(S.write,v.TRIANGLES,6),S.S()}),i);E(y,-1),v.useProgram(_.m),v.uniform1i(_.g.u_v,0),v.uniform1i(_.g.u_x,y.read.N(0)),v.uniform1f(_.g.u_dt,n),v.uniform1f(_.g.u_dissipation,t.u),I(y.write,v.TRIANGLES,6),y.S(),E(S,0),v.useProgram(_.m),v.uniform1i(_.g.u_v,y.read.N(0)),v.uniform1i(_.g.u_x,S.read.N(1)),v.uniform1f(_.g.u_dt,n),v.uniform1f(_.g.u_dissipation,t.l),I(S.write,v.TRIANGLES,6),S.S(),E(y,-1),v.useProgram(h.m),n=1/(t.i*n),v.uniform1f(h.g.u_alpha,n),v.uniform1f(h.g.u_beta,4+n);for(let e=0;e<20;e++)v.uniform1i(h.g.u_x,y.read.N(0)),v.uniform1i(h.g.u_b,y.read.N(0)),I(y.write,v.TRIANGLES,6),y.S();E(y,-1),v.useProgram(L.m),v.uniform1i(L.g.u_x,y.read.N(0)),I(z,v.TRIANGLES,6),v.useProgram(d.m),v.uniform1i(d.g.u_x,N.read.N(0)),v.uniform1f(d.g.u_alpha,t.t),I(N.write,v.TRIANGLES,6),N.S();for(let e=0;e<50;e++)E(N,1),v.useProgram(h.m),v.uniform1i(h.g.u_b,z.N(0)),v.uniform1i(h.g.u_x,N.read.N(1)),v.uniform1f(h.g.u_alpha,-1),v.uniform1f(h.g.u_beta,4),I(N.write,v.TRIANGLES,6),N.S();E(y,-1),E(N,1),v.useProgram(f.m),v.uniform1i(f.g.u_p,N.read.N(0)),v.uniform1i(f.g.u_v,y.read.N(1)),I(y.write,v.TRIANGLES,6),y.S(),i=("velocity"==t.o?y:"pressure"==t.o?N:S).read,v.useProgram(d.m),v.uniform1i(d.g.u_x,i.N(0)),v.uniform1f(d.g.u_alpha,1),I(q,v.TRIANGLES,6),requestAnimationFrame(e)});const T=[];function X(e){return{id:e.pointerId,x:e.offsetX/v.canvas.clientWidth,y:1-e.offsetY/v.canvas.clientHeight,dx:0,dy:0,color:[Math.random(),Math.random(),Math.random()]}}e.addEventListener("pointerdown",e=>{T.push(X(e))}),e.addEventListener("pointerup",n=>{var e=T.findIndex(e=>e.id===n.pointerId);e<0||T.splice(e,1)}),e.addEventListener("pointermove",n=>{var e,i=T.findIndex(e=>e.id===n.pointerId);i<0||(e=X(n),T[i]=(i=T[i],(e=e).color=i.color,e.dx=e.x-i.x,e.dy=e.y-i.y,e))}),e.addEventListener("pointerout",n=>{var e=T.findIndex(e=>e.id===n.pointerId);e<0||T.splice(e,1)});var n=document.querySelector("#viscosity"),A=document.querySelector("#pressure"),O=document.querySelector("#radius"),w=document.querySelector("#velocity_dissipation"),M=document.querySelector("#density_dissipation"),C=document.querySelector("#display_radio");function P(e,n,i){return n*Math.pow(i/n,e)}const j=e=>P(e,1e-4,1e3),k=e=>P(e,1e-4,.01),B=e=>1-P(e,.001,.1),G=e=>1-P(e,.001,.1);t.i=j(n.value),t.t=A.value,t.v=k(O.value),t.u=B(w.value),t.l=G(M.value),n.addEventListener("input",e=>{t.i=j(e.target.value)}),A.addEventListener("input",e=>{t.t=e.target.value}),O.addEventListener("input",e=>{t.v=k(e.target.value)}),w.addEventListener("input",e=>{t.u=B(e.target.value)}),M.addEventListener("input",e=>{t.l=G(e.target.value)}),C.addEventListener("input",e=>{t.o=e.target.value});
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /** @type {HTMLCanvasElement} */
4 | const canvas = document.querySelector('#fluidsim_canvas');
5 | /** @type {WebGL2RenderingContext} */
6 | const gl = canvas.getContext('webgl2');
7 |
8 | gl.getExtension('EXT_color_buffer_float');
9 |
10 | let config = {
11 | NU: 1.,
12 | PRESSURE: 0.5,
13 | DISPLAY: 'dye',
14 | RADIUS: 0.1,
15 | VELOCITY_DISSIPATION: 0.99,
16 | DYE_DISSIPATION: 0.99,
17 | SIM_RESOLUTION: 128,
18 | DYE_RESOLUTION: 1024,
19 | SPLAT_FORCE: 20.
20 | }
21 |
22 | let aspect_ratio;
23 | let sim_width, sim_height;
24 | let dye_width, dye_height;
25 |
26 | function get_aspect_ratio() {
27 | return gl.canvas.clientWidth / gl.canvas.clientHeight;
28 | }
29 |
30 | function get_size(target_size) {
31 | if (aspect_ratio < 1) return { width: target_size, height: Math.round(target_size / aspect_ratio) };
32 | else return { width: Math.round(target_size * aspect_ratio), height: target_size };
33 | }
34 |
35 | function setup_sizes() {
36 | const w = gl.canvas.clientWidth;
37 | const h = gl.canvas.clientHeight;
38 |
39 | aspect_ratio = get_aspect_ratio();
40 |
41 | if (gl.canvas.width === w && gl.canvas.height === h) return false;
42 |
43 | gl.canvas.width = w;
44 | gl.canvas.height = h;
45 |
46 | const sim_size = get_size(config.SIM_RESOLUTION);
47 | const dye_size = get_size(config.DYE_RESOLUTION);
48 |
49 | sim_width = sim_size.width;
50 | sim_height = sim_size.height;
51 | dye_width = dye_size.width;
52 | dye_height = dye_size.height;
53 |
54 | return true;
55 | }
56 |
57 | setup_sizes();
58 |
59 | /* GEOMETRY SETUP */
60 |
61 | let full_vao = gl.createVertexArray();
62 |
63 | const POSITION_LOCATION = 0;
64 |
65 | function setup_geometry(vao, position_data, size, type, normalized, stride, offset) {
66 | gl.bindVertexArray(vao);
67 |
68 | const position_buffer = gl.createBuffer();
69 | gl.bindBuffer(gl.ARRAY_BUFFER, position_buffer);
70 | gl.bufferData(gl.ARRAY_BUFFER, position_data, gl.STATIC_DRAW);
71 | gl.enableVertexAttribArray(POSITION_LOCATION);
72 | gl.vertexAttribPointer(POSITION_LOCATION, size, type, normalized, stride, offset);
73 | }
74 |
75 | const full_pos = new Float32Array([-1, -1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1,]);
76 | setup_geometry(full_vao, full_pos, 2, gl.FLOAT, false, 0, 0);
77 |
78 | /* SHADERS SETUP */
79 |
80 | const utility_neightbors = `
81 | struct Neighbors {
82 | vec4 l;
83 | vec4 r;
84 | vec4 t;
85 | vec4 b;
86 | vec4 c;
87 | };
88 |
89 | Neighbors tex_neighbors(sampler2D tex, ivec2 pos) {
90 | vec4 b = texelFetch(tex, pos - ivec2(0, 1), 0);
91 | vec4 t = texelFetch(tex, pos + ivec2(0, 1), 0);
92 | vec4 l = texelFetch(tex, pos - ivec2(1, 0), 0);
93 | vec4 r = texelFetch(tex, pos + ivec2(1, 0), 0);
94 | vec4 c = texelFetch(tex, pos, 0);
95 | return Neighbors(l, r, t, b, c);
96 | }`
97 |
98 | const base_vs = `#version 300 es
99 | in vec2 a_position;
100 | out vec2 v_position;
101 |
102 | void main() {
103 | v_position = a_position * 0.5 + 0.5;
104 | gl_Position = vec4(a_position, 0, 1);
105 | }`;
106 |
107 | const advection_fs = `#version 300 es
108 | precision highp float;
109 |
110 | uniform sampler2D u_v;
111 | uniform sampler2D u_x;
112 | uniform float u_dt;
113 | uniform float u_dissipation;
114 | out vec4 res;
115 |
116 | vec4 bilerp(sampler2D tex, vec2 x_norm, vec2 size) {
117 | vec2 x = x_norm * size - 0.5;
118 | vec2 fx = fract(x);
119 | ivec2 ix = ivec2(floor(x));
120 |
121 | vec4 x00 = texelFetch(tex, ix + ivec2(0,0), 0);
122 | vec4 x01 = texelFetch(tex, ix + ivec2(0,1), 0);
123 | vec4 x10 = texelFetch(tex, ix + ivec2(1,0), 0);
124 | vec4 x11 = texelFetch(tex, ix + ivec2(1,1), 0);
125 |
126 | return mix(mix(x00, x10, fx.x), mix(x01, x11, fx.x), fx.y);
127 | }
128 |
129 | void main() {
130 | vec2 size_v = vec2(textureSize(u_v, 0));
131 | vec2 size_x = vec2(textureSize(u_x, 0));
132 | vec2 aspect_ratio = vec2(size_x.x / size_x.y, 1.0);
133 | vec2 normalized_pos = gl_FragCoord.xy / size_x;
134 | vec2 prev = normalized_pos - u_dt * bilerp(u_v, normalized_pos, size_v).xy / aspect_ratio;
135 | res = u_dissipation * bilerp(u_x, prev, size_x);
136 | }`
137 |
138 | const jacobi_fs = `#version 300 es
139 | precision highp float;
140 |
141 | uniform sampler2D u_x;
142 | uniform sampler2D u_b;
143 | uniform float u_alpha;
144 | uniform float u_beta;
145 | out vec4 res;
146 |
147 | ${utility_neightbors}
148 |
149 | void main() {
150 | ivec2 pos = ivec2(gl_FragCoord.xy);
151 | Neighbors n = tex_neighbors(u_x, pos);
152 | vec4 b = texelFetch(u_b, pos, 0);
153 | res = (n.b + n.t + n.l + n.r + u_alpha * b) / u_beta;
154 | }`
155 |
156 | const subtract_grad_fs = `#version 300 es
157 | precision highp float;
158 |
159 | uniform sampler2D u_v;
160 | uniform sampler2D u_p;
161 | out vec4 res;
162 |
163 | ${utility_neightbors}
164 |
165 | void main() {
166 | ivec2 pos = ivec2(gl_FragCoord.xy);
167 | Neighbors n = tex_neighbors(u_p, pos);
168 |
169 | vec4 grad = vec4(n.r.x - n.l.x, n.t.x - n.b.x, 0, 0) / 2.;
170 | vec4 init_v = texelFetch(u_v, pos, 0);
171 | res = init_v - grad;
172 | }`
173 |
174 | const div_fs = `#version 300 es
175 | precision highp float;
176 |
177 | uniform sampler2D u_x;
178 | out vec4 res;
179 |
180 | ${utility_neightbors}
181 |
182 | void main() {
183 | ivec2 pos = ivec2(gl_FragCoord.xy);
184 | Neighbors n = tex_neighbors(u_x, pos);
185 |
186 | float div = (n.r.x - n.l.x + n.t.y - n.b.y) / 2.;
187 |
188 | res = vec4(div, 0, 0, 1);
189 | }`
190 |
191 | const boundary_fs = `#version 300 es
192 | precision highp float;
193 |
194 | in vec2 v_position;
195 | uniform sampler2D u_x;
196 | uniform vec2 u_res;
197 | uniform float u_alpha;
198 | out vec4 res;
199 |
200 | void main() {
201 | vec2 dir = vec2(0, 0);
202 | dir += vec2(lessThan(v_position, u_res));
203 | dir -= vec2(greaterThan(v_position, vec2(1.0) - u_res));
204 | float coef = length(dir) > 0.0 ? u_alpha : 1.0;
205 | res = coef * texture(u_x, v_position + dir * u_res);
206 | }`;
207 |
208 | const display_fs = `#version 300 es
209 | precision highp float;
210 |
211 | in vec2 v_position;
212 | uniform sampler2D u_x;
213 | uniform float u_alpha;
214 | out vec4 res;
215 |
216 | void main() {
217 | res = u_alpha * texture(u_x, v_position);
218 | }`;
219 |
220 | const splat_fs = `#version 300 es
221 | precision highp float;
222 |
223 | in vec2 v_position;
224 | uniform sampler2D u_x;
225 | uniform vec2 u_point;
226 | uniform vec3 u_value;
227 | uniform float u_radius;
228 | uniform float u_ratio;
229 | out vec4 res;
230 |
231 | void main() {
232 | vec4 init = texture(u_x, v_position);
233 | vec2 v = v_position - u_point;
234 | v.x *= u_ratio;
235 | vec3 force = exp(-dot(v,v)/u_radius) * u_value;
236 |
237 | res = vec4(init.xyz + force, 1.);
238 | }`;
239 |
240 | function compile_shader(gl, type, source) {
241 | const shader = gl.createShader(type);
242 | gl.shaderSource(shader, source);
243 | gl.compileShader(shader);
244 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
245 | console.error(gl.getShaderInfoLog(shader));
246 | gl.deleteShader(shader);
247 | return null;
248 | }
249 | return shader;
250 | }
251 |
252 | function compile_program(gl, vs, fs) {
253 | const program = gl.createProgram();
254 | gl.attachShader(program, vs);
255 | gl.attachShader(program, fs);
256 | gl.linkProgram(program);
257 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
258 | console.error(gl.getProgramInfoLog(program));
259 | gl.deleteProgram(program);
260 | return null;
261 | }
262 | return program;
263 | }
264 |
265 | function compile_uniforms(gl, program) {
266 | const uniforms = {};
267 | const n = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
268 | for (let i = 0; i < n; ++i) {
269 | const info = gl.getActiveUniform(program, i);
270 | uniforms[info.name] = gl.getUniformLocation(program, info.name);
271 | }
272 | return uniforms;
273 | }
274 |
275 | function create_program(vs_source, fs_source) {
276 | const vs = compile_shader(gl, gl.VERTEX_SHADER, vs_source);
277 | const fs = compile_shader(gl, gl.FRAGMENT_SHADER, fs_source);
278 | const program = compile_program(gl, vs, fs);
279 | const uniforms = compile_uniforms(gl, program);
280 |
281 | gl.bindAttribLocation(program, POSITION_LOCATION, "a_position");
282 |
283 | return { program, uniforms };
284 | }
285 |
286 | const advection_program = create_program(base_vs, advection_fs);
287 | const jacobi_program = create_program(base_vs, jacobi_fs);
288 | const subtract_grad_program = create_program(base_vs, subtract_grad_fs);
289 | const div_program = create_program(base_vs, div_fs);
290 | const boundary_program = create_program(base_vs, boundary_fs);
291 | const display_program = create_program(base_vs, display_fs);
292 | const splat_program = create_program(base_vs, splat_fs);
293 |
294 | /* TARGET TEXTURES / FRAMEBUFFERS SETUP */
295 |
296 | function create_fbo(w, h, internal_format, format, type, filter) {
297 | const texture = gl.createTexture();
298 | gl.activeTexture(gl.TEXTURE0);
299 | gl.bindTexture(gl.TEXTURE_2D, texture);
300 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
301 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
302 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
303 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
304 | gl.texImage2D(gl.TEXTURE_2D, 0, internal_format, w, h, 0, format, type, null);
305 |
306 | const fb = gl.createFramebuffer();
307 | gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
308 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
309 |
310 | return {
311 | tex: texture,
312 | fb: fb,
313 | bind: () => {
314 | gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
315 | gl.viewport(0, 0, w, h);
316 | },
317 | bind_tex: (i) => {
318 | gl.activeTexture(gl.TEXTURE0+i);
319 | gl.bindTexture(gl.TEXTURE_2D, texture);
320 | return i;
321 | },
322 | };
323 | }
324 |
325 | function create_fbo_pair(w, h, internal_format, format, type, filter) {
326 | return {
327 | read: create_fbo(w, h, internal_format, format, type, filter),
328 | write: create_fbo(w, h, internal_format, format, type, filter),
329 | swap: function() {
330 | const temp = this.read;
331 | this.read = this.write;
332 | this.write = temp;
333 | },
334 | };
335 | }
336 |
337 | function resize_fbo(src, w, h, internal_format, format, type, filter) {
338 | const new_fbo = create_fbo(w, h, internal_format, format, type, filter);
339 | gl.useProgram(display_program.program);
340 | gl.uniform1i(display_program.uniforms.u_x, src.bind_tex(0));
341 | render(new_fbo, gl.TRIANGLES, 6);
342 | return new_fbo;
343 | }
344 |
345 | function resize_fbo_pair(src, w, h, internal_format, format, type, filter) {
346 | const new_fbo = create_fbo_pair(w, h, internal_format, format, type, filter);
347 | new_fbo.read = resize_fbo(src.read, w, h, internal_format, format, type, filter);
348 | new_fbo.write = resize_fbo(src.write, w, h, internal_format, format, type, filter);
349 | return new_fbo;
350 | }
351 |
352 | let velocity = create_fbo_pair(sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST);
353 | let pressure = create_fbo_pair(sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST);
354 | let tmp_1f = create_fbo(sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST);
355 | let tmp_2f = create_fbo(sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST);
356 | let dye = create_fbo_pair(dye_width, dye_height, gl.RGBA32F, gl.RGBA, gl.FLOAT, gl.NEAREST);
357 |
358 | const screen = {
359 | tex: null,
360 | fb: null,
361 | bind: () => {
362 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
363 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
364 | }
365 | }
366 |
367 | function setup_fbos() {
368 | velocity = resize_fbo_pair(velocity, sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST);
369 | pressure = resize_fbo_pair(pressure, sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST);
370 | dye = resize_fbo_pair(dye, dye_width, dye_height, gl.RGBA32F, gl.RGBA, gl.FLOAT, gl.NEAREST);
371 | tmp_1f = create_fbo(sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST);
372 | tmp_2f = create_fbo(sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST);
373 | }
374 |
375 | /* SIMULATION / RENDERING */
376 |
377 | function render(fbo, geometry, count, clear = false) {
378 | gl.bindVertexArray(full_vao);
379 | fbo.bind();
380 | if (clear) {
381 | gl.clearColor(0.0, 0.0, 0.0, 1.0);
382 | gl.clear(gl.COLOR_BUFFER_BIT);
383 | }
384 | gl.drawArrays(geometry, 0, count);
385 | }
386 |
387 | function render_screen(fbo) {
388 | gl.useProgram(display_program.program);
389 | gl.uniform1i(display_program.uniforms.u_x, fbo.bind_tex(0));
390 | gl.uniform1f(display_program.uniforms.u_alpha, 1.0);
391 | render(screen, gl.TRIANGLES, 6);
392 | }
393 |
394 | function set_boundary(fbo_pair, alpha) {
395 | gl.useProgram(boundary_program.program);
396 | gl.uniform2f(boundary_program.uniforms.u_res, 1. / sim_width, 1. / sim_height)
397 | gl.uniform1i(boundary_program.uniforms.u_x, fbo_pair.read.bind_tex(0));
398 | gl.uniform1f(boundary_program.uniforms.u_alpha, alpha);
399 | render(fbo_pair.write, gl.TRIANGLES, 6);
400 | fbo_pair.swap();
401 | }
402 |
403 | function step_sim(dt) {
404 |
405 | // Advect velocity
406 | set_boundary(velocity, -1.0);
407 | gl.useProgram(advection_program.program);
408 |
409 | gl.uniform1i(advection_program.uniforms.u_v, 0);
410 | gl.uniform1i(advection_program.uniforms.u_x, velocity.read.bind_tex(0));
411 | gl.uniform1f(advection_program.uniforms.u_dt, dt);
412 | gl.uniform1f(advection_program.uniforms.u_dissipation, config.VELOCITY_DISSIPATION);
413 |
414 | render(velocity.write, gl.TRIANGLES, 6);
415 | velocity.swap();
416 |
417 | // Advect dye
418 | set_boundary(dye, 0.);
419 | gl.useProgram(advection_program.program);
420 |
421 | gl.uniform1i(advection_program.uniforms.u_v, velocity.read.bind_tex(0));
422 | gl.uniform1i(advection_program.uniforms.u_x, dye.read.bind_tex(1));
423 | gl.uniform1f(advection_program.uniforms.u_dt, dt);
424 | gl.uniform1f(advection_program.uniforms.u_dissipation, config.DYE_DISSIPATION);
425 |
426 | render(dye.write, gl.TRIANGLES, 6);
427 | dye.swap();
428 |
429 | // Diffuse velocity
430 | set_boundary(velocity, -1.0);
431 | gl.useProgram(jacobi_program.program);
432 |
433 | const factor = 1./ (config.NU * dt);
434 | gl.uniform1f(jacobi_program.uniforms.u_alpha, factor);
435 | gl.uniform1f(jacobi_program.uniforms.u_beta, factor + 4.0);
436 | for (let i = 0; i < 20; i++) {
437 | gl.uniform1i(jacobi_program.uniforms.u_x, velocity.read.bind_tex(0));
438 | gl.uniform1i(jacobi_program.uniforms.u_b, velocity.read.bind_tex(0));
439 | render(velocity.write, gl.TRIANGLES, 6);
440 | velocity.swap();
441 | }
442 |
443 | // Project velocity
444 | // Compute divergence
445 | set_boundary(velocity, -1);
446 | gl.useProgram(div_program.program);
447 | gl.uniform1i(div_program.uniforms.u_x, velocity.read.bind_tex(0));
448 | render(tmp_1f, gl.TRIANGLES, 6);
449 |
450 | // Clear pressure
451 | gl.useProgram(display_program.program);
452 | gl.uniform1i(display_program.uniforms.u_x, pressure.read.bind_tex(0));
453 | gl.uniform1f(display_program.uniforms.u_alpha, config.PRESSURE);
454 | render(pressure.write, gl.TRIANGLES, 6);
455 | pressure.swap();
456 |
457 | // Solve for pressure
458 | for (let i = 0; i < 50; i++) {
459 | set_boundary(pressure, 1.);
460 |
461 | // Jacobi iteration
462 | gl.useProgram(jacobi_program.program);
463 | gl.uniform1i(jacobi_program.uniforms.u_b, tmp_1f.bind_tex(0));
464 | gl.uniform1i(jacobi_program.uniforms.u_x, pressure.read.bind_tex(1));
465 | gl.uniform1f(jacobi_program.uniforms.u_alpha, -1.0);
466 | gl.uniform1f(jacobi_program.uniforms.u_beta, 4.0);
467 | render(pressure.write, gl.TRIANGLES, 6);
468 | pressure.swap();
469 | }
470 |
471 | // Compute pressure gradient and subtract from velocity
472 | set_boundary(velocity, -1.);
473 | set_boundary(pressure, 1.);
474 | gl.useProgram(subtract_grad_program.program);
475 | gl.uniform1i(subtract_grad_program.uniforms.u_p, pressure.read.bind_tex(0));
476 | gl.uniform1i(subtract_grad_program.uniforms.u_v, velocity.read.bind_tex(1));
477 | render(velocity.write, gl.TRIANGLES, 6);
478 | velocity.swap();
479 | }
480 |
481 | let last_time = 0;
482 | function loop(t) {
483 | let dt = (t - last_time) / 1000.;
484 | last_time = t;
485 |
486 | if (setup_sizes()) setup_fbos();
487 | step_user();
488 | step_sim(dt);
489 | render_screen(
490 | config.DISPLAY == 'velocity' ? velocity.read :
491 | config.DISPLAY == 'pressure' ? pressure.read :
492 | dye.read
493 | );
494 |
495 | requestAnimationFrame(loop);
496 | }
497 |
498 | requestAnimationFrame(loop);
499 |
500 | /* USER INPUTS */
501 |
502 | const pointers = [];
503 |
504 | function create_pointer(pointer) {
505 | return {
506 | id: pointer.pointerId,
507 | x: pointer.offsetX / gl.canvas.clientWidth,
508 | y: 1. - pointer.offsetY / gl.canvas.clientHeight,
509 | dx: 0,
510 | dy: 0,
511 | color: [Math.random(), Math.random(), Math.random()]
512 | };
513 | }
514 |
515 | function update_pointer(old_ptr, new_ptr) {
516 | new_ptr.color = old_ptr.color;
517 | new_ptr.dx = new_ptr.x - old_ptr.x;
518 | new_ptr.dy = new_ptr.y - old_ptr.y;
519 | return new_ptr;
520 | }
521 |
522 | canvas.addEventListener('pointerdown', (e) => {
523 | pointers.push(create_pointer(e));
524 | });
525 |
526 | canvas.addEventListener('pointerup', (e) => {
527 | const pointer_idx = pointers.findIndex(p => p.id === e.pointerId);
528 | if (pointer_idx < 0) return;
529 |
530 | pointers.splice(pointer_idx, 1);
531 | });
532 |
533 | canvas.addEventListener('pointermove', (e) => {
534 | const pointer_idx = pointers.findIndex(p => p.id === e.pointerId);
535 | if (pointer_idx < 0) return;
536 |
537 | const new_pointer = create_pointer(e);
538 | pointers[pointer_idx] = update_pointer(pointers[pointer_idx], new_pointer);
539 | });
540 |
541 | canvas.addEventListener('pointerout', (e) => {
542 | const pointer_idx = pointers.findIndex(p => p.id === e.pointerId);
543 | if (pointer_idx < 0) return;
544 |
545 | pointers.splice(pointer_idx, 1);
546 | });
547 |
548 | function step_user() {
549 | pointers.forEach(p => {
550 | gl.useProgram(splat_program.program);
551 | gl.uniform1i(splat_program.uniforms.u_x, velocity.read.bind_tex(0));
552 | gl.uniform2fv(splat_program.uniforms.u_point, [p.x, p.y]);
553 | gl.uniform3fv(splat_program.uniforms.u_value, [p.dx * aspect_ratio, p.dy, 0].map(c => c * config.SPLAT_FORCE));
554 | gl.uniform1f(splat_program.uniforms.u_radius, config.RADIUS);
555 | gl.uniform1f(splat_program.uniforms.u_ratio, aspect_ratio);
556 | render(velocity.write, gl.TRIANGLES, 6);
557 | velocity.swap();
558 |
559 | gl.uniform1i(splat_program.uniforms.u_x, dye.read.bind_tex(0));
560 | gl.uniform3fv(splat_program.uniforms.u_value, p.color.map(c => c * 0.2));
561 | render(dye.write, gl.TRIANGLES, 6);
562 | dye.swap();
563 | });
564 | }
565 |
566 | /* UI */
567 |
568 | const viscosity_slider = document.querySelector('#viscosity');
569 | const pressure_slider = document.querySelector('#pressure');
570 | const radius_slider = document.querySelector('#radius');
571 | const velocity_dissipation_slider = document.querySelector('#velocity_dissipation');
572 | const density_dissipation_slider = document.querySelector('#density_dissipation');
573 | const display_radio = document.querySelector('#display_radio');
574 |
575 | // Transform a 0-1 slider value to an a-b log scale
576 | function log_scale(value, a, b) {
577 | return a * Math.pow(b/a, value);
578 | }
579 |
580 | const viscosity_transform = value => log_scale(value, 0.0001, 1000.);
581 | const radius_transform = value => log_scale(value, 0.0001, 0.01);
582 | const velocity_dissipation_transform = value => 1. - log_scale(value, 0.001, 0.1);
583 | const density_dissipation_transform = value => 1. - log_scale(value, 0.001, 0.1);
584 |
585 | config.NU = viscosity_transform(viscosity_slider.value);
586 | config.PRESSURE = pressure_slider.value;
587 | config.RADIUS = radius_transform(radius_slider.value);
588 | config.VELOCITY_DISSIPATION = velocity_dissipation_transform(velocity_dissipation_slider.value);
589 | config.DYE_DISSIPATION = density_dissipation_transform(density_dissipation_slider.value);
590 |
591 | viscosity_slider.addEventListener('input', e => {config.NU = viscosity_transform(e.target.value);});
592 | pressure_slider.addEventListener('input', e => {config.PRESSURE = e.target.value;});
593 | radius_slider.addEventListener('input', (e) => {config.RADIUS = radius_transform(e.target.value);});
594 | velocity_dissipation_slider.addEventListener('input', e => {config.VELOCITY_DISSIPATION = velocity_dissipation_transform(e.target.value);});
595 | density_dissipation_slider.addEventListener('input', e => {config.DYE_DISSIPATION = density_dissipation_transform(e.target.value);});
596 | display_radio.addEventListener('input', (e) => {config.DISPLAY = e.target.value;});
--------------------------------------------------------------------------------