├── 2d.html ├── 3d.html ├── LICENSE ├── README.md ├── js ├── 2d.js ├── 3d.js └── math.js └── lib ├── dat.gui.min.js └── stats.min.js /2d.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2D Smoke Simulation in WebGL 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /3d.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3D Smoke Simulation in WebGL 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 aadebdeb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPGPU Smoke Simulation in WebGL 2 | 3 | ## 2D 4 | 5 | https://aadebdeb.github.io/WebGL_SmokeSimulation/2d 6 | 7 | ![image for 2D smoke simulation](https://user-images.githubusercontent.com/10070637/57664039-8ea27f80-7631-11e9-88fa-7fd1a7afe3ed.gif) 8 | 9 | ## 3D 10 | 11 | Be careful, this demo is too heavy! 12 | 13 | https://aadebdeb.github.io/WebGL_SmokeSimulation/3d 14 | 15 | ![image for 3D smoke simulation](https://user-images.githubusercontent.com/10070637/57664040-906c4300-7631-11e9-8488-818238fb4c06.gif) -------------------------------------------------------------------------------- /js/2d.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function createShader(gl, source, type) { 4 | const shader = gl.createShader(type); 5 | gl.shaderSource(shader, source); 6 | gl.compileShader(shader); 7 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 8 | throw new Error(gl.getShaderInfoLog(shader) + source); 9 | } 10 | return shader; 11 | } 12 | 13 | function createProgramFromSource(gl, vertexShaderSource, fragmentShaderSource) { 14 | const program = gl.createProgram(); 15 | gl.attachShader(program, createShader(gl, vertexShaderSource, gl.VERTEX_SHADER)); 16 | gl.attachShader(program, createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER)); 17 | gl.linkProgram(program); 18 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 19 | throw new Error(gl.getProgramInfoLog(program)); 20 | } 21 | return program; 22 | } 23 | 24 | function getUniformLocations(gl, program, keys) { 25 | const locations = {}; 26 | keys.forEach(key => { 27 | locations[key] = gl.getUniformLocation(program, key); 28 | }); 29 | return locations; 30 | } 31 | 32 | function createTexture(gl, width, height, internalFormat, format, type) { 33 | const texture = gl.createTexture(); 34 | gl.bindTexture(gl.TEXTURE_2D, texture); 35 | gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, null); 36 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 37 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 38 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 39 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 40 | gl.bindTexture(gl.TEXTURE_2D, null); 41 | return texture; 42 | } 43 | 44 | function createVelocityFramebuffer(gl, width, height) { 45 | const framebuffer = gl.createFramebuffer(); 46 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 47 | const velocityTexture = createTexture(gl, width, height, gl.RG32F, gl.RG, gl.FLOAT); 48 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, velocityTexture, 0); 49 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 50 | gl.bindTexture(gl.TEXTURE_2D, null); 51 | return { 52 | framebuffer: framebuffer, 53 | velocityTexture: velocityTexture 54 | }; 55 | } 56 | 57 | function createPressureFramebuffer(gl, width, height) { 58 | const framebuffer = gl.createFramebuffer(); 59 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 60 | const pressureTexture = createTexture(gl, width, height, gl.R32F, gl.RED, gl.FLOAT); 61 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, pressureTexture, 0); 62 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 63 | gl.bindTexture(gl.TEXTURE_2D, null); 64 | return { 65 | framebuffer: framebuffer, 66 | pressureTexture: pressureTexture 67 | }; 68 | } 69 | 70 | function createSmokeFramebuffer(gl, width, height) { 71 | const framebuffer = gl.createFramebuffer(); 72 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 73 | const smokeTexture = createTexture(gl, width, height, gl.RG32F, gl.RG, gl.FLOAT); 74 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, smokeTexture, 0); 75 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 76 | gl.bindTexture(gl.TEXTURE_2D, null); 77 | return { 78 | framebuffer: framebuffer, 79 | smokeTexture: smokeTexture 80 | }; 81 | } 82 | 83 | function setUniformTexture(gl, index, texture, location) { 84 | gl.activeTexture(gl.TEXTURE0 + index); 85 | gl.bindTexture(gl.TEXTURE_2D, texture); 86 | gl.uniform1i(location, index); 87 | } 88 | 89 | const FILL_VIEWPORT_VERTEX_SHADER_SOURCE = 90 | `#version 300 es 91 | 92 | const vec3[4] POSITIONS = vec3[]( 93 | vec3(-1.0, -1.0, 0.0), 94 | vec3(1.0, -1.0, 0.0), 95 | vec3(-1.0, 1.0, 0.0), 96 | vec3(1.0, 1.0, 0.0) 97 | ); 98 | 99 | const int[6] INDICES = int[]( 100 | 0, 1, 2, 101 | 3, 2, 1 102 | ); 103 | 104 | void main(void) { 105 | vec3 position = POSITIONS[INDICES[gl_VertexID]]; 106 | gl_Position = vec4(position, 1.0); 107 | } 108 | `; 109 | 110 | const INITIALIZE_VELOCITY_FRAGMENT_SHADER_SOURCE = 111 | `#version 300 es 112 | 113 | precision highp float; 114 | 115 | out vec2 o_velocity; 116 | 117 | void main(void) { 118 | o_velocity = vec2(0.0); 119 | } 120 | `; 121 | 122 | const INITIALIZE_SMOKE_FRAGMENT_SHADER_SOURCE = 123 | `#version 300 es 124 | 125 | precision highp float; 126 | 127 | out vec2 o_smoke; // x: density, y: temperature 128 | 129 | #define AMBIENT_TEMPERATURE 273.0 130 | 131 | void main(void) { 132 | float density = 0.0; 133 | float temperature = AMBIENT_TEMPERATURE; 134 | o_smoke = vec2(density, temperature); 135 | } 136 | `; 137 | 138 | const ADD_BUOYANCY_FORCE_FRAGMENT_SHADER_SOURCE = 139 | `#version 300 es 140 | 141 | precision highp float; 142 | 143 | out vec2 o_velocity; 144 | 145 | uniform sampler2D u_velocityTexture; 146 | uniform sampler2D u_smokeTexture; 147 | uniform float u_deltaTime; 148 | uniform float u_densityScale; 149 | uniform float u_temperatureScale; 150 | 151 | #define GRAVITY vec2(0.0, -9.8) 152 | #define AMBIENT_TEMPERATURE 273.0 153 | 154 | void main(void) { 155 | ivec2 coord = ivec2(gl_FragCoord.xy); 156 | vec2 velocity = texelFetch(u_velocityTexture, coord, 0).xy; 157 | vec2 smoke = texelFetch(u_smokeTexture, coord, 0).xy; 158 | vec2 buoyancy = (u_densityScale * smoke.x 159 | - u_temperatureScale * (smoke.y - AMBIENT_TEMPERATURE)) * GRAVITY; 160 | o_velocity = velocity + u_deltaTime * buoyancy; 161 | } 162 | `; 163 | 164 | const ADVECT_VELOCITY_FRAGMENT_SHADER_SOURCE = 165 | `#version 300 es 166 | 167 | precision highp float; 168 | 169 | out vec2 o_velocity; 170 | 171 | uniform sampler2D u_velocityTexture; 172 | uniform float u_deltaTime; 173 | uniform float u_gridSpacing; 174 | 175 | vec2 sampleVelocity(ivec2 coord, ivec2 textureSize) { 176 | vec2 velocity = texelFetch(u_velocityTexture, coord, 0).xy; 177 | if (coord.x < 0 || coord.x >= textureSize.x) { 178 | velocity.x = 0.0; 179 | } 180 | if (coord.y < 0 || coord.y >= textureSize.y) { 181 | velocity.y = 0.0; 182 | } 183 | return velocity; 184 | } 185 | 186 | void main(void) { 187 | ivec2 coord = ivec2(gl_FragCoord.xy); 188 | vec2 position = (vec2(coord) + 0.5) * u_gridSpacing; 189 | vec2 velocity = texelFetch(u_velocityTexture, coord, 0).xy; 190 | 191 | vec2 prevPos = position - u_deltaTime * velocity; 192 | vec2 prevCoord = prevPos / u_gridSpacing - 0.5; 193 | 194 | ivec2 i = ivec2(prevCoord); 195 | vec2 f = fract(prevCoord); 196 | 197 | ivec2 textureSize = textureSize(u_velocityTexture, 0); 198 | vec2 vel00 = sampleVelocity(i, textureSize); 199 | vec2 vel10 = sampleVelocity(i + ivec2(1, 0), textureSize); 200 | vec2 vel01 = sampleVelocity(i + ivec2(0, 1), textureSize); 201 | vec2 vel11 = sampleVelocity(i + ivec2(1, 1), textureSize); 202 | 203 | o_velocity = mix(mix(vel00, vel10, f.x), mix(vel01, vel11, f.x), f.y); 204 | } 205 | `; 206 | 207 | const COMPUTE_PRESSURE_FRAGMENT_SHADER_SOURCE = 208 | `#version 300 es 209 | 210 | precision highp float; 211 | 212 | out float o_pressure; 213 | 214 | uniform sampler2D u_velocityTexture; 215 | uniform sampler2D u_pressureTexture; 216 | uniform float u_deltaTime; 217 | uniform float u_gridSpacing; 218 | uniform float u_density; 219 | 220 | vec2 sampleVelocity(ivec2 coord, ivec2 textureSize) { 221 | vec2 velocity = texelFetch(u_velocityTexture, coord, 0).xy; 222 | if (coord.x < 0 || coord.x >= textureSize.x) { 223 | velocity.x = 0.0; 224 | } 225 | if (coord.y < 0 || coord.y >= textureSize.y) { 226 | velocity.y = 0.0; 227 | } 228 | return velocity; 229 | } 230 | 231 | float samplePressure(ivec2 coord, ivec2 textureSize) { 232 | if (coord.x < 0) { 233 | coord.x = 0; 234 | } 235 | if (coord.x >= textureSize.x) { 236 | coord.x = textureSize.x - 1; 237 | } 238 | if (coord.y < 0) { 239 | coord.y = 0; 240 | } 241 | if (coord.y >= textureSize.y) { 242 | coord.y = textureSize.y - 1; 243 | } 244 | return texelFetch(u_pressureTexture, coord, 0).x; 245 | } 246 | 247 | void main(void) { 248 | ivec2 coord = ivec2(gl_FragCoord.xy); 249 | 250 | ivec2 pressTexSize = textureSize(u_pressureTexture, 0); 251 | float pl = samplePressure(coord + ivec2(-1, 0), pressTexSize); 252 | float pr = samplePressure(coord + ivec2(1, 0), pressTexSize); 253 | float pd = samplePressure(coord + ivec2(0, -1), pressTexSize); 254 | float pu = samplePressure(coord + ivec2(0, 1), pressTexSize); 255 | 256 | ivec2 velTexSize = textureSize(u_velocityTexture, 0); 257 | vec2 vl = sampleVelocity(coord + ivec2(-1, 0), velTexSize); 258 | vec2 vr = sampleVelocity(coord + ivec2(1, 0), velTexSize); 259 | vec2 vd = sampleVelocity(coord + ivec2(0, -1), velTexSize); 260 | vec2 vu = sampleVelocity(coord + ivec2(0, 1), velTexSize); 261 | 262 | o_pressure = 0.25 * (pl + pr + pd + pu 263 | - 0.5 * (vr.x - vl.x + vu.y - vd.y) * u_gridSpacing * u_density / u_deltaTime); 264 | } 265 | `; 266 | 267 | const ADD_PRESSURE_FORCE_FRAGMENT_SHADER_SOURCE = 268 | `#version 300 es 269 | 270 | precision highp float; 271 | 272 | out vec2 o_velocity; 273 | 274 | uniform sampler2D u_velocityTexture; 275 | uniform sampler2D u_pressureTexture; 276 | uniform float u_deltaTime; 277 | uniform float u_gridSpacing; 278 | uniform float u_density; 279 | 280 | float samplePressure(ivec2 coord, ivec2 textureSize) { 281 | if (coord.x < 0) { 282 | coord.x = 0; 283 | } 284 | if (coord.x >= textureSize.x) { 285 | coord.x = textureSize.x - 1; 286 | } 287 | if (coord.y < 0) { 288 | coord.y = 0; 289 | } 290 | if (coord.y >= textureSize.y) { 291 | coord.y = textureSize.y - 1; 292 | } 293 | return texelFetch(u_pressureTexture, coord, 0).x; 294 | } 295 | 296 | void main(void) { 297 | ivec2 coord = ivec2(gl_FragCoord.xy); 298 | vec2 velocity = texelFetch(u_velocityTexture, coord, 0).xy; 299 | 300 | ivec2 pressTexSize = textureSize(u_pressureTexture, 0); 301 | float pl = samplePressure(coord + ivec2(-1, 0), pressTexSize); 302 | float pr = samplePressure(coord + ivec2(1, 0), pressTexSize); 303 | float pd = samplePressure(coord + ivec2(0, -1), pressTexSize); 304 | float pu = samplePressure(coord + ivec2(0, 1), pressTexSize); 305 | 306 | o_velocity = velocity - 0.5 * u_deltaTime * vec2(pr - pl, pu - pd) / (u_gridSpacing * u_density); 307 | } 308 | `; 309 | 310 | const DECAY_VELOCITY_FRAGMENT_SHADER_SOURCE = 311 | `#version 300 es 312 | 313 | precision highp float; 314 | 315 | out vec2 o_velocity; 316 | 317 | uniform sampler2D u_velocityTexture; 318 | uniform float u_deltaTime; 319 | uniform float u_velocityDecay; 320 | 321 | void main(void) { 322 | vec2 velocity = texelFetch(u_velocityTexture, ivec2(gl_FragCoord.xy), 0).xy; 323 | velocity *= exp(-u_velocityDecay * u_deltaTime); 324 | o_velocity = velocity; 325 | } 326 | `; 327 | 328 | const ADVECT_SMOKE_FRAGMENT_SHADER_SOURCE = 329 | `#version 300 es 330 | 331 | precision highp float; 332 | 333 | out vec2 o_smoke; 334 | 335 | uniform sampler2D u_velocityTexture; 336 | uniform sampler2D u_smokeTexture; 337 | uniform float u_deltaTime; 338 | uniform float u_gridSpacing; 339 | 340 | #define AMBIENT_TEMPERATURE 273.0 341 | 342 | vec2 sampleSmoke(ivec2 coord, ivec2 textureSize) { 343 | vec2 smoke = texelFetch(u_smokeTexture, coord, 0).xy; 344 | if (coord.x < 0 || coord.x >= textureSize.x || coord.y < 0 || coord.y >= textureSize.y) { 345 | smoke = vec2(0.0, AMBIENT_TEMPERATURE); 346 | } 347 | return smoke; 348 | } 349 | 350 | void main(void) { 351 | ivec2 coord = ivec2(gl_FragCoord.xy); 352 | vec2 position = (vec2(coord) + 0.5) * u_gridSpacing; 353 | vec2 velocity = texelFetch(u_velocityTexture, coord, 0).xy; 354 | 355 | vec2 prevPos = position - u_deltaTime * velocity; 356 | vec2 prevCoord = prevPos / u_gridSpacing - 0.5; 357 | 358 | ivec2 i = ivec2(prevCoord); 359 | vec2 f = fract(prevCoord); 360 | 361 | ivec2 textureSize = textureSize(u_smokeTexture, 0); 362 | vec2 smoke00 = sampleSmoke(i, textureSize); 363 | vec2 smoke10 = sampleSmoke(i + ivec2(1, 0), textureSize); 364 | vec2 smoke01 = sampleSmoke(i + ivec2(0, 1), textureSize); 365 | vec2 smoke11 = sampleSmoke(i + ivec2(1, 1), textureSize); 366 | 367 | o_smoke = mix(mix(smoke00, smoke10, f.x), mix(smoke01, smoke11, f.x), f.y); 368 | } 369 | `; 370 | 371 | const ADD_SMOKE_FRAGMENT_SHADER_SOURCE = 372 | `#version 300 es 373 | 374 | precision highp float; 375 | 376 | out vec2 o_smoke; 377 | 378 | uniform sampler2D u_smokeTexture; 379 | uniform float u_deltaTime; 380 | uniform float u_gridSpacing; 381 | uniform bool u_addHeat; 382 | uniform vec2 u_heatSourceCenter; 383 | uniform float u_heatSourceRadius; 384 | uniform float u_heatSourceIntensity; 385 | uniform float u_densityDecay; 386 | uniform float u_temperatureDecay; 387 | 388 | #define AMBIENT_TEMPERATURE 273.0 389 | 390 | void main(void) { 391 | vec2 smoke = texelFetch(u_smokeTexture, ivec2(gl_FragCoord), 0).xy; 392 | float density = smoke.x; 393 | float temperature = smoke.y; 394 | 395 | float nextTemperature = temperature; 396 | vec2 position = (floor(gl_FragCoord.xy) + 0.5) * u_gridSpacing; 397 | if (u_addHeat) { 398 | nextTemperature += smoothstep(u_heatSourceRadius, 0.0, length(position - u_heatSourceCenter)) 399 | * u_deltaTime * u_heatSourceIntensity; 400 | } 401 | nextTemperature += (1.0 - exp(-u_temperatureDecay * u_deltaTime)) * (AMBIENT_TEMPERATURE - nextTemperature); 402 | 403 | float nextDensity = density + u_deltaTime * max(0.0, (nextTemperature - (AMBIENT_TEMPERATURE + 100.0))) * 0.01; 404 | nextDensity *= exp(-u_densityDecay * u_deltaTime); 405 | 406 | o_smoke = vec2(nextDensity, nextTemperature); 407 | } 408 | 409 | `; 410 | 411 | const RENDER_VELOCITY_FRAGMENT_SHADER_SOURCE = 412 | `#version 300 es 413 | 414 | precision highp float; 415 | 416 | #define PI 3.14159265359 417 | 418 | uniform sampler2D u_velocityTexture; 419 | 420 | out vec4 o_color; 421 | 422 | vec3 hsv2rgb(float h, float s, float v) { 423 | h = mod(h, 360.0); 424 | if (s == 0.0) { 425 | return vec3(0.0, 0.0, 0.0); 426 | } 427 | float c = v * s; 428 | float i = h / 60.0; 429 | float x = c * (1.0 - abs(mod(i, 2.0) - 1.0)); 430 | return vec3(v - c) + (i < 1.0 ? vec3(c, x, 0.0) : 431 | i < 2.0 ? vec3(x, c, 0.0) : 432 | i < 3.0 ? vec3(0.0, c, x) : 433 | i < 4.0 ? vec3(0.0, x, c) : 434 | i < 5.0 ? vec3(x, 0.0, c) : 435 | vec3(c, 0.0, x)); 436 | } 437 | 438 | void main(void) { 439 | vec2 velocity = texelFetch(u_velocityTexture, ivec2(gl_FragCoord.xy), 0).xy; 440 | 441 | vec2 normVel = normalize(velocity); 442 | float radian = atan(velocity.y, velocity.x) + PI; 443 | 444 | float hue = 360.0 * radian / (2.0 * PI); 445 | float brightness = min(1.0, length(velocity) / 0.5); 446 | vec3 color = hsv2rgb(hue, 1.0, brightness); 447 | 448 | o_color = vec4(color, 1.0); 449 | } 450 | `; 451 | 452 | const RENDER_DENSITY_FRAGMENT_SHADER_SOURCE = 453 | `#version 300 es 454 | 455 | precision highp float; 456 | 457 | out vec4 o_color; 458 | 459 | uniform sampler2D u_smokeTexture; 460 | 461 | void main(void) { 462 | float density = texelFetch(u_smokeTexture, ivec2(gl_FragCoord.xy), 0).x; 463 | o_color = vec4(vec3(density), 1.0); 464 | } 465 | `; 466 | 467 | const RENDER_TEMPERATURE_FRAGMENT_SHADER_SOURCE = 468 | `#version 300 es 469 | 470 | precision highp float; 471 | 472 | out vec4 o_color; 473 | 474 | uniform sampler2D u_smokeTexture; 475 | 476 | #define AMBIENT_TEMPERATURE 273.0 477 | 478 | const vec4[6] TEMPERATURE_COLOR = vec4[]( 479 | vec4(0.0, 0.0, 0.0, 0.0), 480 | vec4(0.0, 0.0, 0.0, AMBIENT_TEMPERATURE), 481 | vec4(1.0, 0.0, 0.0, AMBIENT_TEMPERATURE + 100.0), 482 | vec4(1.0, 0.5, 0.0, AMBIENT_TEMPERATURE + 200.0), 483 | vec4(1.0, 1.0, 1.0, AMBIENT_TEMPERATURE + 300.0), 484 | vec4(0.5, 0.5, 1.0, AMBIENT_TEMPERATURE + 400.0) 485 | ); 486 | 487 | void main(void) { 488 | float temperature = texelFetch(u_smokeTexture, ivec2(gl_FragCoord.xy), 0).y; 489 | 490 | vec3 color = TEMPERATURE_COLOR[5].xyz; 491 | for (int i = 0; i < 5; i++) { 492 | if (temperature < TEMPERATURE_COLOR[i + 1].w) { 493 | color = mix(TEMPERATURE_COLOR[i].xyz, TEMPERATURE_COLOR[i + 1].xyz, 494 | 1.0 - (TEMPERATURE_COLOR[i + 1].w - temperature) / (TEMPERATURE_COLOR[i + 1]. w - TEMPERATURE_COLOR[i].w)); 495 | break; 496 | } 497 | } 498 | o_color = vec4(color, 1.0); 499 | } 500 | `; 501 | 502 | let mousePosition = new Vector2(0.0, 0.0); 503 | let mousePressing = false; 504 | window.addEventListener('mousemove', event => { 505 | mousePosition = new Vector2(event.clientX, window.innerHeight - event.clientY); 506 | }); 507 | window.addEventListener('mousedown', _ => { 508 | mousePressing = true; 509 | }); 510 | window.addEventListener('mouseup', _ => { 511 | mousePressing = false; 512 | }); 513 | 514 | const stats = new Stats(); 515 | document.body.appendChild(stats.dom); 516 | 517 | const parameters = { 518 | 'grid spacing': 0.001, 519 | 'air density': 2.354, 520 | 'density force': 0.01, 521 | 'temperature force': 0.0001, 522 | 'heat radius': 0.1, 523 | 'heat intensity': 1000.0, 524 | 'velocity decay': 0.1, 525 | 'density decay': 0.5, 526 | 'temperature decay': 1.0, 527 | 'time step': 0.005, 528 | 'time scale': 1.0, 529 | 'render': 'density', 530 | 'reset': _ => reset() 531 | }; 532 | 533 | const gui = new dat.GUI(); 534 | gui.add(parameters, 'density force', 0.0, 0.1).step(0.0001); 535 | gui.add(parameters, 'temperature force', 0.0, 0.0003).step(0.00001); 536 | gui.add(parameters, 'heat radius', 0.0, 0.3).step(0.001); 537 | gui.add(parameters, 'heat intensity', 0.0, 2000.0).step(1.0); 538 | gui.add(parameters, 'velocity decay', 0.0, 5.0).step(0.1); 539 | gui.add(parameters, 'density decay', 0.0, 5.0).step(0.1); 540 | gui.add(parameters, 'temperature decay', 0.0, 5.0).step(0.1); 541 | gui.add(parameters, 'time step', 0.0001, 0.01).step(0.0001); 542 | gui.add(parameters, 'time scale', 0.5, 2.0).step(0.001); 543 | gui.add(parameters, 'render', ['density', 'temperature', 'velocity']); 544 | gui.add(parameters, 'reset'); 545 | 546 | const canvas = document.getElementById('canvas'); 547 | const gl = canvas.getContext('webgl2'); 548 | gl.getExtension('EXT_color_buffer_float'); 549 | 550 | const initializeVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, INITIALIZE_VELOCITY_FRAGMENT_SHADER_SOURCE); 551 | const initializeSmokeProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, INITIALIZE_SMOKE_FRAGMENT_SHADER_SOURCE); 552 | const addBuoyancyForceProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADD_BUOYANCY_FORCE_FRAGMENT_SHADER_SOURCE); 553 | const advectVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADVECT_VELOCITY_FRAGMENT_SHADER_SOURCE); 554 | const computePressureProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, COMPUTE_PRESSURE_FRAGMENT_SHADER_SOURCE); 555 | const addPressureForceProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADD_PRESSURE_FORCE_FRAGMENT_SHADER_SOURCE); 556 | const decayVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, DECAY_VELOCITY_FRAGMENT_SHADER_SOURCE); 557 | const advectSmokeProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADVECT_SMOKE_FRAGMENT_SHADER_SOURCE); 558 | const addSmokeProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADD_SMOKE_FRAGMENT_SHADER_SOURCE); 559 | const renderVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, RENDER_VELOCITY_FRAGMENT_SHADER_SOURCE); 560 | const renderDensityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, RENDER_DENSITY_FRAGMENT_SHADER_SOURCE); 561 | const renderTemperatureProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, RENDER_TEMPERATURE_FRAGMENT_SHADER_SOURCE); 562 | 563 | const addBuoyancyForceUniforms = getUniformLocations(gl, addBuoyancyForceProgram, ['u_velocityTexture', 'u_smokeTexture', 'u_deltaTime', 'u_densityScale', 'u_temperatureScale']); 564 | const advectVelocityUniforms = getUniformLocations(gl, advectVelocityProgram, ['u_velocityTexture', 'u_deltaTime', 'u_gridSpacing']); 565 | const computePressureUniforms = getUniformLocations(gl, computePressureProgram, ['u_velocityTexture', 'u_pressureTexture', 'u_deltaTime', 'u_gridSpacing', 'u_density']); 566 | const addPressureForceUniforms = getUniformLocations(gl, addPressureForceProgram, ['u_velocityTexture', 'u_pressureTexture', 'u_deltaTime', 'u_gridSpacing', 'u_density']); 567 | const decayVelocityUniforms = getUniformLocations(gl, decayVelocityProgram, ['u_velocityTexture', 'u_deltaTime', 'u_velocityDecay']); 568 | const advectSmokeUniforms = getUniformLocations(gl, advectSmokeProgram, ['u_velocityTexture', 'u_smokeTexture', 'u_deltaTime', 'u_gridSpacing']); 569 | const addSmokeUniforms = getUniformLocations(gl, addSmokeProgram, 570 | ['u_smokeTexture', 'u_deltaTime', 'u_gridSpacing', 'u_addHeat', 'u_heatSourceCenter', 'u_heatSourceRadius', 'u_heatSourceIntensity', 'u_densityDecay', 'u_temperatureDecay']); 571 | const renderVelocityUniforms = getUniformLocations(gl, renderVelocityProgram, ['u_velocityTexture']); 572 | const renderDensityUniforms = getUniformLocations(gl, renderDensityProgram, ['u_smokeTexture']); 573 | const renderTemperatureUniforms = getUniformLocations(gl, renderTemperatureProgram, ['u_smokeTexture']); 574 | 575 | let requestId = null; 576 | const reset = function() { 577 | if (requestId !== null) { 578 | cancelAnimationFrame(requestId); 579 | requestId = null; 580 | } 581 | 582 | canvas.width = window.innerWidth; 583 | canvas.height = window.innerHeight; 584 | gl.viewport(0.0, 0.0, canvas.width, canvas.height); 585 | 586 | let velocityFbObjR = createVelocityFramebuffer(gl, canvas.width, canvas.height); 587 | let velocityFbObjW = createVelocityFramebuffer(gl, canvas.width, canvas.height); 588 | const swapVelocityFbObj = function() { 589 | const tmp = velocityFbObjR; 590 | velocityFbObjR = velocityFbObjW; 591 | velocityFbObjW = tmp; 592 | }; 593 | 594 | let pressureFbObjR = createPressureFramebuffer(gl, canvas.width, canvas.height); 595 | let pressureFbObjW = createPressureFramebuffer(gl, canvas.width, canvas.height); 596 | const swapPressureFbObj = function() { 597 | const tmp = pressureFbObjR; 598 | pressureFbObjR = pressureFbObjW; 599 | pressureFbObjW = tmp; 600 | }; 601 | 602 | 603 | let smokeFbObjR = createSmokeFramebuffer(gl, canvas.width, canvas.height); 604 | let smokeFbObjW = createSmokeFramebuffer(gl, canvas.width, canvas.height); 605 | const swapSmokeFbObj = function() { 606 | const tmp = smokeFbObjR; 607 | smokeFbObjR = smokeFbObjW; 608 | smokeFbObjW = tmp; 609 | }; 610 | 611 | const initializeVelocity = function() { 612 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 613 | gl.useProgram(initializeVelocityProgram); 614 | gl.drawArrays(gl.TRIANGLES, 0, 6); 615 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 616 | swapVelocityFbObj(); 617 | }; 618 | 619 | const initializeSmoke = function() { 620 | gl.bindFramebuffer(gl.FRAMEBUFFER, smokeFbObjW.framebuffer); 621 | gl.useProgram(initializeSmokeProgram); 622 | gl.drawArrays(gl.TRIANGLES, 0, 6); 623 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 624 | swapSmokeFbObj(); 625 | } 626 | 627 | const addBuoyancyForce = function(deltaTime) { 628 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 629 | gl.useProgram(addBuoyancyForceProgram); 630 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, addBuoyancyForceUniforms['u_velocityTexture']); 631 | setUniformTexture(gl, 1, smokeFbObjR.smokeTexture, addBuoyancyForceUniforms['u_smokeTexture']); 632 | gl.uniform1f(addBuoyancyForceUniforms['u_deltaTime'], deltaTime); 633 | gl.uniform1f(addBuoyancyForceUniforms['u_densityScale'], parameters['density force']); 634 | gl.uniform1f(addBuoyancyForceUniforms['u_temperatureScale'], parameters['temperature force']); 635 | gl.drawArrays(gl.TRIANGLES, 0, 6); 636 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 637 | swapVelocityFbObj(); 638 | }; 639 | 640 | const advectVelocity = function(deltaTime) { 641 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 642 | gl.useProgram(advectVelocityProgram); 643 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, advectVelocityUniforms['u_velocityTexture']); 644 | gl.uniform1f(advectVelocityUniforms['u_deltaTime'], deltaTime); 645 | gl.uniform1f(advectVelocityUniforms['u_gridSpacing'], parameters['grid spacing']); 646 | gl.drawArrays(gl.TRIANGLES, 0, 6); 647 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 648 | swapVelocityFbObj(); 649 | }; 650 | 651 | const computePressure = function(deltaTime) { 652 | gl.useProgram(computePressureProgram); 653 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, computePressureUniforms['u_velocityTexture']); 654 | gl.uniform1f(computePressureUniforms['u_deltaTime'], deltaTime); 655 | gl.uniform1f(computePressureUniforms['u_gridSpacing'], parameters['grid spacing']); 656 | gl.uniform1f(computePressureUniforms['u_density'], parameters['air density']); 657 | for (let i = 0; i < 10; i++) { 658 | gl.bindFramebuffer(gl.FRAMEBUFFER, pressureFbObjW.framebuffer); 659 | setUniformTexture(gl, 1, pressureFbObjR.pressureTexture, computePressureUniforms['u_pressureTexture']) 660 | gl.drawArrays(gl.TRIANGLES, 0, 6); 661 | swapPressureFbObj(); 662 | } 663 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 664 | }; 665 | 666 | const addPressureForce = function(deltaTime) { 667 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 668 | gl.useProgram(addPressureForceProgram); 669 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, addPressureForceUniforms['u_velocityTexture']); 670 | setUniformTexture(gl, 1, pressureFbObjR.pressureTexture, addPressureForceUniforms['u_pressureTexture']); 671 | gl.uniform1f(addPressureForceUniforms['u_deltaTime'], deltaTime); 672 | gl.uniform1f(addPressureForceUniforms['u_gridSpacing'], parameters['grid spacing']); 673 | gl.uniform1f(addPressureForceUniforms['u_density'], parameters['air density']); 674 | gl.drawArrays(gl.TRIANGLES, 0, 6); 675 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 676 | swapVelocityFbObj(); 677 | }; 678 | 679 | const decayVelocity = function(deltaTime) { 680 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 681 | gl.useProgram(decayVelocityProgram); 682 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, decayVelocityUniforms['u_velocityTexture']); 683 | gl.uniform1f(decayVelocityUniforms['u_deltaTime'], deltaTime); 684 | gl.uniform1f(decayVelocityUniforms['u_velocityDecay'], parameters['velocity decay']); 685 | gl.drawArrays(gl.TRIANGLES, 0, 6); 686 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 687 | swapVelocityFbObj(); 688 | } 689 | 690 | const updateVelocity = function(deltaTime) { 691 | addBuoyancyForce(deltaTime); 692 | advectVelocity(deltaTime); 693 | computePressure(deltaTime); 694 | addPressureForce(deltaTime); 695 | decayVelocity(deltaTime); 696 | }; 697 | 698 | const advectSmoke = function(deltaTime) { 699 | gl.bindFramebuffer(gl.FRAMEBUFFER, smokeFbObjW.framebuffer); 700 | gl.useProgram(advectSmokeProgram); 701 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, advectSmokeUniforms['u_velocityTexture']); 702 | setUniformTexture(gl, 1, smokeFbObjR.smokeTexture, advectSmokeUniforms['u_smokeTexture']); 703 | gl.uniform1f(advectSmokeUniforms['u_deltaTime'], deltaTime); 704 | gl.uniform1f(advectSmokeUniforms['u_gridSpacing'], parameters['grid spacing']); 705 | gl.drawArrays(gl.TRIANGLES, 0, 6); 706 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 707 | swapSmokeFbObj(); 708 | } 709 | 710 | const addSmoke = function(deltaTime) { 711 | gl.bindFramebuffer(gl.FRAMEBUFFER, smokeFbObjW.framebuffer); 712 | gl.useProgram(addSmokeProgram); 713 | setUniformTexture(gl, 0, smokeFbObjR.smokeTexture, addSmokeUniforms['u_smokeTexture']); 714 | gl.uniform1f(addSmokeUniforms['u_deltaTime'], deltaTime); 715 | gl.uniform1f(addSmokeUniforms['u_gridSpacing'], parameters['grid spacing']); 716 | gl.uniform1i(addSmokeUniforms['u_addHeat'], mousePressing); 717 | const heatSourceCenter = Vector2.mul(mousePosition, parameters['grid spacing']); 718 | gl.uniform2fv(addSmokeUniforms['u_heatSourceCenter'], heatSourceCenter.toArray()); 719 | gl.uniform1f(addSmokeUniforms['u_heatSourceRadius'], parameters['heat radius']); 720 | gl.uniform1f(addSmokeUniforms['u_heatSourceIntensity'], parameters['heat intensity']); 721 | gl.uniform1f(addSmokeUniforms['u_densityDecay'], parameters['density decay']); 722 | gl.uniform1f(addSmokeUniforms['u_temperatureDecay'], parameters['temperature decay']); 723 | gl.drawArrays(gl.TRIANGLES, 0, 6); 724 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 725 | swapSmokeFbObj(); 726 | } 727 | 728 | const updateSmoke = function(deltaTime) { 729 | advectSmoke(deltaTime); 730 | addSmoke(deltaTime); 731 | } 732 | 733 | const stepSimulation = function(deltaTime) { 734 | updateVelocity(deltaTime); 735 | updateSmoke(deltaTime); 736 | } 737 | 738 | const renderVelocity = function() { 739 | gl.useProgram(renderVelocityProgram); 740 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, renderVelocityUniforms['u_velocityTexture']) 741 | gl.drawArrays(gl.TRIANGLES, 0, 6); 742 | } 743 | 744 | const renderDensity = function() { 745 | gl.useProgram(renderDensityProgram); 746 | setUniformTexture(gl, 0, smokeFbObjR.smokeTexture, renderDensityUniforms['u_smokeTexture']); 747 | gl.drawArrays(gl.TRIANGLES, 0, 6); 748 | } 749 | 750 | const renderTemperature = function() { 751 | gl.useProgram(renderTemperatureProgram); 752 | setUniformTexture(gl, 0, smokeFbObjR.smokeTexture, renderTemperatureUniforms['u_smokeTexture']); 753 | gl.drawArrays(gl.TRIANGLES, 0, 6); 754 | } 755 | 756 | const render = function() { 757 | if (parameters['render'] === 'velocity') { 758 | renderVelocity(); 759 | } else if (parameters['render'] === 'temperature') { 760 | renderTemperature(); 761 | } else { 762 | renderDensity(); 763 | } 764 | }; 765 | 766 | initializeVelocity(); 767 | initializeSmoke(); 768 | let simulationSeconds = 0.0; 769 | let remaindedSimulationSeconds = 0.0; 770 | let previousRealSeconds = performance.now() * 0.001; 771 | const loop = function() { 772 | stats.update(); 773 | 774 | const currentRealSeconds = performance.now() * 0.001; 775 | const nextSimulationSeconds = simulationSeconds + remaindedSimulationSeconds + parameters['time scale'] * Math.min(0.02, currentRealSeconds - previousRealSeconds); 776 | previousRealSeconds = currentRealSeconds; 777 | const timeStep = parameters['time step']; 778 | while(nextSimulationSeconds - simulationSeconds > timeStep) { 779 | stepSimulation(timeStep); 780 | simulationSeconds += timeStep; 781 | } 782 | remaindedSimulationSeconds = nextSimulationSeconds - simulationSeconds; 783 | 784 | render(); 785 | requestId = requestAnimationFrame(loop); 786 | }; 787 | loop(); 788 | }; 789 | reset(); 790 | 791 | window.addEventListener('resize', _ => { 792 | reset(); 793 | }); 794 | 795 | }()); -------------------------------------------------------------------------------- /js/3d.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function createShader(gl, source, type) { 4 | const shader = gl.createShader(type); 5 | gl.shaderSource(shader, source); 6 | gl.compileShader(shader); 7 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 8 | throw new Error(gl.getShaderInfoLog(shader) + source); 9 | } 10 | return shader; 11 | } 12 | 13 | function createProgramFromSource(gl, vertexShaderSource, fragmentShaderSource) { 14 | const program = gl.createProgram(); 15 | gl.attachShader(program, createShader(gl, vertexShaderSource, gl.VERTEX_SHADER)); 16 | gl.attachShader(program, createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER)); 17 | gl.linkProgram(program); 18 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 19 | throw new Error(gl.getProgramInfoLog(program)); 20 | } 21 | return program; 22 | } 23 | 24 | function getUniformLocations(gl, program, keys) { 25 | const locations = {}; 26 | keys.forEach(key => { 27 | locations[key] = gl.getUniformLocation(program, key); 28 | }); 29 | return locations; 30 | } 31 | 32 | function createTexture(gl, width, height, internalFormat, format, type) { 33 | const texture = gl.createTexture(); 34 | gl.bindTexture(gl.TEXTURE_2D, texture); 35 | gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, null); 36 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 37 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 38 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 39 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 40 | gl.bindTexture(gl.TEXTURE_2D, null); 41 | return texture; 42 | } 43 | 44 | function createVelocityFramebuffer(gl, width, height) { 45 | const framebuffer = gl.createFramebuffer(); 46 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 47 | const velocityTexture = createTexture(gl, width, height, gl.RGBA32F, gl.RGBA, gl.FLOAT); 48 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, velocityTexture, 0); 49 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 50 | gl.bindTexture(gl.TEXTURE_2D, null); 51 | return { 52 | framebuffer: framebuffer, 53 | velocityTexture: velocityTexture 54 | }; 55 | } 56 | 57 | function createPressureFramebuffer(gl, width, height) { 58 | const framebuffer = gl.createFramebuffer(); 59 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 60 | const pressureTexture = createTexture(gl, width, height, gl.R32F, gl.RED, gl.FLOAT); 61 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, pressureTexture, 0); 62 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 63 | gl.bindTexture(gl.TEXTURE_2D, null); 64 | return { 65 | framebuffer: framebuffer, 66 | pressureTexture: pressureTexture 67 | }; 68 | } 69 | 70 | function createSmokeFramebuffer(gl, width, height) { 71 | const framebuffer = gl.createFramebuffer(); 72 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 73 | const smokeTexture = createTexture(gl, width, height, gl.RG32F, gl.RG, gl.FLOAT); 74 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, smokeTexture, 0); 75 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 76 | gl.bindTexture(gl.TEXTURE_2D, null); 77 | return { 78 | framebuffer: framebuffer, 79 | smokeTexture: smokeTexture 80 | }; 81 | } 82 | 83 | function setUniformTexture(gl, index, texture, location) { 84 | gl.activeTexture(gl.TEXTURE0 + index); 85 | gl.bindTexture(gl.TEXTURE_2D, texture); 86 | gl.uniform1i(location, index); 87 | } 88 | 89 | const FILL_VIEWPORT_VERTEX_SHADER_SOURCE = 90 | `#version 300 es 91 | 92 | const vec3[4] POSITIONS = vec3[]( 93 | vec3(-1.0, -1.0, 0.0), 94 | vec3(1.0, -1.0, 0.0), 95 | vec3(-1.0, 1.0, 0.0), 96 | vec3(1.0, 1.0, 0.0) 97 | ); 98 | 99 | const int[6] INDICES = int[]( 100 | 0, 1, 2, 101 | 3, 2, 1 102 | ); 103 | 104 | void main(void) { 105 | vec3 position = POSITIONS[INDICES[gl_VertexID]]; 106 | gl_Position = vec4(position, 1.0); 107 | } 108 | `; 109 | 110 | const INITIALIZE_VELOCITY_FRAGMENT_SHADER_SOURCE = 111 | `#version 300 es 112 | 113 | precision highp float; 114 | 115 | out vec3 o_velocity; 116 | 117 | void main(void) { 118 | o_velocity = vec3(0.0, 0.01, 0.0); 119 | } 120 | `; 121 | 122 | const INITIALIZE_SMOKE_FRAGMENT_SHADER_SOURCE = 123 | `#version 300 es 124 | 125 | precision highp float; 126 | 127 | out vec2 o_smoke; 128 | 129 | uniform int u_cellNum; 130 | uniform int u_cellTextureSize; 131 | uniform ivec3 u_resolution; 132 | uniform float u_gridSpacing; 133 | uniform vec3 u_simulationSpace; 134 | 135 | #define AMBIENT_TEMPERATURE 273.0 136 | 137 | int convertCoordToCellId(ivec2 coord) { 138 | return coord.x + coord.y * u_cellTextureSize; 139 | } 140 | 141 | ivec3 convertCellIdToCellIndex(int cellId) { 142 | int z = cellId / (u_resolution.x * u_resolution.y); 143 | int y = (cellId % (u_resolution.x * u_resolution.y)) / u_resolution.x; 144 | int x = cellId % u_resolution.x; 145 | return ivec3(x, y, z); 146 | } 147 | 148 | vec3 convertCellIdToPosition(int cellId) { 149 | ivec3 cellIndex = convertCellIdToCellIndex(cellId); 150 | return (vec3(cellIndex) + 0.5) * u_gridSpacing; 151 | } 152 | 153 | void main(void) { 154 | ivec2 coord = ivec2(gl_FragCoord.xy); 155 | int cellId = convertCoordToCellId(coord); 156 | if (cellId >= u_cellNum) { 157 | return; 158 | } 159 | vec3 position = convertCellIdToPosition(cellId); 160 | float density = 0.0; 161 | float temperature = AMBIENT_TEMPERATURE; 162 | o_smoke = vec2(density, temperature); 163 | } 164 | `; 165 | 166 | const ADD_BUOYANCY_FORCE_FRAGMENT_SHADER_SOURCE = 167 | `#version 300 es 168 | 169 | precision highp float; 170 | 171 | out vec3 o_velocity; 172 | 173 | uniform int u_cellNum; 174 | uniform int u_cellTextureSize; 175 | uniform sampler2D u_velocityTexture; 176 | uniform sampler2D u_smokeTexture; 177 | uniform float u_deltaTime; 178 | uniform float u_densityScale; 179 | uniform float u_temperatureScale; 180 | 181 | #define GRAVITY vec3(0.0, -9.8, 0.0) 182 | #define AMBIENT_TEMPERATURE 273.0 183 | 184 | void main(void) { 185 | ivec2 coord = ivec2(gl_FragCoord.xy); 186 | int cellId = coord.x + coord.y * u_cellTextureSize; 187 | if (cellId >= u_cellNum) { 188 | return; 189 | } 190 | vec3 velocity = texelFetch(u_velocityTexture, coord, 0).xyz; 191 | vec2 smoke = texelFetch(u_smokeTexture, coord, 0).xy; 192 | vec3 buoyancy = (u_densityScale * smoke.x 193 | - u_temperatureScale * (smoke.y - AMBIENT_TEMPERATURE)) * GRAVITY; 194 | o_velocity = velocity + u_deltaTime * buoyancy; 195 | } 196 | `; 197 | 198 | const ADVECT_VELOCITY_FRAGMENT_SHADER_SOURCE = 199 | `#version 300 es 200 | 201 | precision highp float; 202 | 203 | out vec3 o_velocity; 204 | 205 | uniform int u_cellNum; 206 | uniform int u_cellTextureSize; 207 | uniform ivec3 u_resolution; 208 | uniform sampler2D u_velocityTexture; 209 | uniform float u_deltaTime; 210 | uniform float u_gridSpacing; 211 | 212 | ivec2 convertCellIdToCoord(int cellId) { 213 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 214 | } 215 | 216 | int convertCoordToCellId(ivec2 coord) { 217 | return coord.x + coord.y * u_cellTextureSize; 218 | } 219 | 220 | int convertCellIndexToCellId(ivec3 cellIndex) { 221 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 222 | } 223 | 224 | ivec3 convertCellIdToCellIndex(int cellId) { 225 | int z = cellId / (u_resolution.x * u_resolution.y); 226 | int y = (cellId % (u_resolution.x * u_resolution.y)) / u_resolution.x; 227 | int x = cellId % u_resolution.x; 228 | return ivec3(x, y, z); 229 | } 230 | 231 | vec3 convertCellIdToPosition(int cellId) { 232 | ivec3 cellIndex = convertCellIdToCellIndex(cellId); 233 | return (vec3(cellIndex) + 0.5) * u_gridSpacing; 234 | } 235 | 236 | ivec3 convertPositionToCellIndex(vec3 position) { 237 | return ivec3(position / u_gridSpacing - 0.5); 238 | } 239 | 240 | int convertPositionToCellId(vec3 position) { 241 | ivec3 cellIndex = convertPositionToCellIndex(position); 242 | return convertCellIndexToCellId(cellIndex); 243 | } 244 | 245 | ivec2 convertPositionToCoord(vec3 position) { 246 | int cellId = convertPositionToCellId(position); 247 | return convertCellIdToCoord(cellId); 248 | } 249 | 250 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 251 | int cellId = convertCellIndexToCellId(cellIndex); 252 | return convertCellIdToCoord(cellId); 253 | } 254 | 255 | vec3 sampleVelocity(ivec3 cellIndex) { 256 | if (cellIndex.x < 0 || cellIndex.x >= u_resolution.x || 257 | cellIndex.y < 0 || cellIndex.y >= u_resolution.y || 258 | cellIndex.z < 0 || cellIndex.z >= u_resolution.z) { 259 | return vec3(0.0); 260 | } 261 | ivec2 coord = convertCellIndexToCoord(cellIndex); 262 | return texelFetch(u_velocityTexture, coord, 0).xyz; 263 | } 264 | 265 | void main(void) { 266 | ivec2 coord = ivec2(gl_FragCoord.xy); 267 | int cellId = convertCoordToCellId(coord); 268 | if (cellId >= u_cellNum) { 269 | return; 270 | } 271 | vec3 position = convertCellIdToPosition(cellId); 272 | vec3 velocity = texelFetch(u_velocityTexture, coord, 0).xyz; 273 | 274 | vec3 prevPos = position - u_deltaTime * velocity; 275 | vec3 prevCellIndex = prevPos / u_gridSpacing - 0.5; 276 | ivec3 i = ivec3(prevCellIndex); 277 | vec3 f = fract(prevCellIndex); 278 | 279 | vec3 vel000 = sampleVelocity(i); 280 | vec3 vel100 = sampleVelocity(i + ivec3(1, 0, 0)); 281 | vec3 vel010 = sampleVelocity(i + ivec3(0, 1, 0)); 282 | vec3 vel110 = sampleVelocity(i + ivec3(1, 1, 0)); 283 | vec3 vel001 = sampleVelocity(i + ivec3(0, 0, 1)); 284 | vec3 vel101 = sampleVelocity(i + ivec3(1, 0, 1)); 285 | vec3 vel011 = sampleVelocity(i + ivec3(0, 1, 1)); 286 | vec3 vel111 = sampleVelocity(i + ivec3(1, 1, 1)); 287 | 288 | o_velocity = mix( 289 | mix(mix(vel000, vel100, f.x), mix(vel010, vel110, f.x), f.y), 290 | mix(mix(vel001, vel101, f.x), mix(vel011, vel111, f.x), f.y), 291 | f.z 292 | ); 293 | } 294 | `; 295 | 296 | const COMPUTE_PRESSURE_FRAGMENT_SHADER_SOURCE = 297 | `#version 300 es 298 | 299 | precision highp float; 300 | 301 | out float o_pressure; 302 | 303 | uniform int u_cellNum; 304 | uniform int u_cellTextureSize; 305 | uniform ivec3 u_resolution; 306 | uniform sampler2D u_velocityTexture; 307 | uniform sampler2D u_pressureTexture; 308 | uniform float u_deltaTime; 309 | uniform float u_gridSpacing; 310 | uniform float u_density; 311 | 312 | ivec2 convertCellIdToCoord(int cellId) { 313 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 314 | } 315 | 316 | int convertCoordToCellId(ivec2 coord) { 317 | return coord.x + coord.y * u_cellTextureSize; 318 | } 319 | 320 | int convertCellIndexToCellId(ivec3 cellIndex) { 321 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 322 | } 323 | 324 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 325 | int cellId = convertCellIndexToCellId(cellIndex); 326 | return convertCellIdToCoord(cellId); 327 | } 328 | 329 | ivec3 convertCellIdToCellIndex(int cellId) { 330 | int z = cellId / (u_resolution.x * u_resolution.y); 331 | int y = (cellId % (u_resolution.x * u_resolution.y)) / u_resolution.x; 332 | int x = cellId % u_resolution.x; 333 | return ivec3(x, y, z); 334 | } 335 | 336 | void sampleVelocityAndPressure(ivec3 cellIndex, out vec3 velocity, out float pressure) { 337 | if (cellIndex.x < 0 || cellIndex.x >= u_resolution.x || 338 | cellIndex.y < 0 || cellIndex.y >= u_resolution.y || 339 | cellIndex.z < 0 || cellIndex.z >= u_resolution.z) { 340 | velocity = vec3(0.0); 341 | pressure = 0.0; 342 | return; 343 | } 344 | ivec2 coord = convertCellIndexToCoord(cellIndex); 345 | velocity = texelFetch(u_velocityTexture, coord, 0).xyz; 346 | pressure = texelFetch(u_pressureTexture, coord, 0).x; 347 | } 348 | 349 | void main(void) { 350 | ivec2 coord = ivec2(gl_FragCoord.xy); 351 | int cellId = convertCoordToCellId(coord); 352 | if (cellId >= u_cellNum) { 353 | return; 354 | } 355 | 356 | ivec3 cellIndex = convertCellIdToCellIndex(cellId); 357 | vec3 vl, vr, vd, vu, vn, vf; 358 | float pl, pr, pd, pu, pn, pf; 359 | sampleVelocityAndPressure(cellIndex + ivec3(-1, 0, 0), vl, pl); 360 | sampleVelocityAndPressure(cellIndex + ivec3(1, 0, 0), vr, pr); 361 | sampleVelocityAndPressure(cellIndex + ivec3(0, -1, 0), vd, pd); 362 | sampleVelocityAndPressure(cellIndex + ivec3(0, 1, 0), vu, pu); 363 | sampleVelocityAndPressure(cellIndex + ivec3(0, 0, -1), vn, pn); 364 | sampleVelocityAndPressure(cellIndex + ivec3(0, 0, 1), vf, pf); 365 | 366 | o_pressure = (pl + pr + pd + pu + pn + pf 367 | - 0.5 * (vr.x - vl.x + vu.y - vd.y + vf.z - vn.z) 368 | * u_gridSpacing * u_density / u_deltaTime) / 6.0; 369 | } 370 | `; 371 | 372 | const ADD_PRESSURE_FORCE_FRAGMENT_SHADER_SOURCE = 373 | `#version 300 es 374 | 375 | precision highp float; 376 | 377 | out vec3 o_velocity; 378 | 379 | uniform int u_cellNum; 380 | uniform int u_cellTextureSize; 381 | uniform ivec3 u_resolution; 382 | uniform sampler2D u_velocityTexture; 383 | uniform sampler2D u_pressureTexture; 384 | uniform float u_deltaTime; 385 | uniform float u_gridSpacing; 386 | uniform float u_density; 387 | 388 | ivec2 convertCellIdToCoord(int cellId) { 389 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 390 | } 391 | 392 | int convertCoordToCellId(ivec2 coord) { 393 | return coord.x + coord.y * u_cellTextureSize; 394 | } 395 | 396 | int convertCellIndexToCellId(ivec3 cellIndex) { 397 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 398 | } 399 | 400 | ivec3 convertCellIdToCellIndex(int cellId) { 401 | int z = cellId / (u_resolution.x * u_resolution.y); 402 | int y = (cellId % (u_resolution.x * u_resolution.y)) / u_resolution.x; 403 | int x = cellId % u_resolution.x; 404 | return ivec3(x, y, z); 405 | } 406 | 407 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 408 | int cellId = convertCellIndexToCellId(cellIndex); 409 | return convertCellIdToCoord(cellId); 410 | } 411 | 412 | float samplePressure(ivec3 cellIndex) { 413 | if (cellIndex.x < 0) { 414 | cellIndex.x = 0; 415 | } 416 | if (cellIndex.x >= u_resolution.x) { 417 | cellIndex.x = u_resolution.x - 1; 418 | } 419 | if (cellIndex.y < 0) { 420 | cellIndex.y = 0; 421 | } 422 | if (cellIndex.y >= u_resolution.y) { 423 | cellIndex.y = u_resolution.y - 1; 424 | } 425 | if (cellIndex.z < 0) { 426 | cellIndex.z = 0; 427 | } 428 | if (cellIndex.z >= u_resolution.z) { 429 | cellIndex.z = u_resolution.z - 1; 430 | } 431 | ivec2 coord = convertCellIndexToCoord(cellIndex); 432 | return texelFetch(u_pressureTexture, coord, 0).x; 433 | } 434 | 435 | void main(void) { 436 | ivec2 coord = ivec2(gl_FragCoord.xy); 437 | int cellId = convertCoordToCellId(coord); 438 | if (cellId >= u_cellNum) { 439 | return; 440 | } 441 | 442 | vec3 velocity = texelFetch(u_velocityTexture, coord, 0).xyz; 443 | 444 | ivec3 cellIndex = convertCellIdToCellIndex(cellId); 445 | float pl = samplePressure(cellIndex + ivec3(-1, 0, 0)); 446 | float pr = samplePressure(cellIndex + ivec3(1, 0, 0)); 447 | float pd = samplePressure(cellIndex + ivec3(0, -1, 0)); 448 | float pu = samplePressure(cellIndex + ivec3(0, 1, 0)); 449 | float pn = samplePressure(cellIndex + ivec3(0, 0, -1)); 450 | float pf = samplePressure(cellIndex + ivec3(0, 0, 1)); 451 | 452 | o_velocity = velocity - 0.5 * u_deltaTime * vec3(pr - pl, pu - pd, pf - pn) / (u_gridSpacing * u_density); 453 | } 454 | `; 455 | 456 | const DECAY_VELOCITY_FRAGMENT_SHADER_SOURCE = 457 | `#version 300 es 458 | 459 | precision highp float; 460 | 461 | out vec3 o_velocity; 462 | 463 | uniform sampler2D u_velocityTexture; 464 | uniform float u_deltaTime; 465 | uniform float u_velocityDecay; 466 | 467 | void main(void) { 468 | vec3 velocity = texelFetch(u_velocityTexture, ivec2(gl_FragCoord.xy), 0).xyz; 469 | velocity *= exp(-u_velocityDecay * u_deltaTime); 470 | o_velocity = velocity; 471 | } 472 | `; 473 | 474 | const ADVECT_SMOKE_FRAGMENT_SHADER_SOURCE = 475 | `#version 300 es 476 | 477 | precision highp float; 478 | 479 | out vec2 o_smoke; 480 | 481 | uniform int u_cellNum; 482 | uniform int u_cellTextureSize; 483 | uniform ivec3 u_resolution; 484 | uniform sampler2D u_velocityTexture; 485 | uniform sampler2D u_smokeTexture; 486 | uniform float u_deltaTime; 487 | uniform float u_gridSpacing; 488 | 489 | #define AMBIENT_TEMPERATURE 273.0 490 | 491 | ivec2 convertCellIdToCoord(int cellId) { 492 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 493 | } 494 | 495 | int convertCoordToCellId(ivec2 coord) { 496 | return coord.x + coord.y * u_cellTextureSize; 497 | } 498 | 499 | int convertCellIndexToCellId(ivec3 cellIndex) { 500 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 501 | } 502 | 503 | ivec3 convertCellIdToCellIndex(int cellId) { 504 | int z = cellId / (u_resolution.x * u_resolution.y); 505 | int y = (cellId % (u_resolution.x * u_resolution.y)) / u_resolution.x; 506 | int x = cellId % u_resolution.x; 507 | return ivec3(x, y, z); 508 | } 509 | 510 | vec3 convertCellIdToPosition(int cellId) { 511 | ivec3 cellIndex = convertCellIdToCellIndex(cellId); 512 | return (vec3(cellIndex) + 0.5) * u_gridSpacing; 513 | } 514 | 515 | ivec3 convertPositionToCellIndex(vec3 position) { 516 | return ivec3(position / u_gridSpacing - 0.5); 517 | } 518 | 519 | int convertPositionToCellId(vec3 position) { 520 | ivec3 cellIndex = convertPositionToCellIndex(position); 521 | return convertCellIndexToCellId(cellIndex); 522 | } 523 | 524 | ivec2 convertPositionToCoord(vec3 position) { 525 | int cellId = convertPositionToCellId(position); 526 | return convertCellIdToCoord(cellId); 527 | } 528 | 529 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 530 | int cellId = convertCellIndexToCellId(cellIndex); 531 | return convertCellIdToCoord(cellId); 532 | } 533 | 534 | vec2 sampleSmoke(ivec3 cellIndex) { 535 | if (cellIndex.x < 0 || cellIndex.x >= u_resolution.x || 536 | cellIndex.y < 0 || cellIndex.y >= u_resolution.y || 537 | cellIndex.z < 0 || cellIndex.z >= u_resolution.z) { 538 | return vec2(0.0, AMBIENT_TEMPERATURE); 539 | } 540 | ivec2 coord = convertCellIndexToCoord(cellIndex); 541 | return texelFetch(u_smokeTexture, coord, 0).xy; 542 | } 543 | 544 | void main(void) { 545 | ivec2 coord = ivec2(gl_FragCoord.xy); 546 | int cellId = convertCoordToCellId(coord); 547 | if (cellId >= u_cellNum) { 548 | return; 549 | } 550 | vec3 position = convertCellIdToPosition(cellId); 551 | vec3 velocity = texelFetch(u_velocityTexture, coord, 0).xyz; 552 | 553 | vec3 prevPos = position - u_deltaTime * velocity; 554 | vec3 prevCellIndex = prevPos / u_gridSpacing - 0.5; 555 | ivec3 i = ivec3(prevCellIndex); 556 | vec3 f = fract(prevCellIndex); 557 | 558 | vec2 smoke000 = sampleSmoke(i); 559 | vec2 smoke100 = sampleSmoke(i + ivec3(1, 0, 0)); 560 | vec2 smoke010 = sampleSmoke(i + ivec3(0, 1, 0)); 561 | vec2 smoke110 = sampleSmoke(i + ivec3(1, 1, 0)); 562 | vec2 smoke001 = sampleSmoke(i + ivec3(0, 0, 1)); 563 | vec2 smoke101 = sampleSmoke(i + ivec3(1, 0, 1)); 564 | vec2 smoke011 = sampleSmoke(i + ivec3(0, 1, 1)); 565 | vec2 smoke111 = sampleSmoke(i + ivec3(1, 1, 1)); 566 | 567 | o_smoke = mix( 568 | mix(mix(smoke000, smoke100, f.x), mix(smoke010, smoke110, f.x), f.y), 569 | mix(mix(smoke001, smoke101, f.x), mix(smoke011, smoke111, f.x), f.y), 570 | f.z 571 | ); 572 | } 573 | `; 574 | 575 | const ADD_SMOKE_FRAGMENT_SHADER_SOURCE = 576 | `#version 300 es 577 | 578 | precision highp float; 579 | 580 | out vec2 o_smoke; 581 | 582 | uniform int u_cellNum; 583 | uniform int u_cellTextureSize; 584 | uniform ivec3 u_resolution; 585 | uniform vec3 u_simulationSpace; 586 | uniform sampler2D u_smokeTexture; 587 | uniform float u_deltaTime; 588 | uniform float u_gridSpacing; 589 | uniform bool u_addHeat; 590 | uniform vec2 u_mousePosition; 591 | uniform float u_heatSourceRadius; 592 | uniform float u_heatSourceIntensity; 593 | uniform float u_densityDecay; 594 | uniform float u_temperatureDecay; 595 | 596 | #define AMBIENT_TEMPERATURE 273.0 597 | 598 | ivec2 convertCellIdToCoord(int cellId) { 599 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 600 | } 601 | 602 | int convertCoordToCellId(ivec2 coord) { 603 | return coord.x + coord.y * u_cellTextureSize; 604 | } 605 | 606 | ivec3 convertCellIdToCellIndex(int cellId) { 607 | int z = cellId / (u_resolution.x * u_resolution.y); 608 | int y = (cellId % (u_resolution.x * u_resolution.y)) / u_resolution.x; 609 | int x = cellId % u_resolution.x; 610 | return ivec3(x, y, z); 611 | } 612 | 613 | vec3 convertCellIdToPosition(int cellId) { 614 | ivec3 cellIndex = convertCellIdToCellIndex(cellId); 615 | return (vec3(cellIndex) + 0.5) * u_gridSpacing; 616 | } 617 | 618 | void main(void) { 619 | ivec2 coord = ivec2(gl_FragCoord.xy); 620 | int cellId = convertCoordToCellId(coord); 621 | if (cellId >= u_cellNum) { 622 | return; 623 | } 624 | 625 | vec2 smoke = texelFetch(u_smokeTexture, convertCellIdToCoord(cellId), 0).xy; 626 | float density = smoke.x; 627 | float temperature = smoke.y; 628 | 629 | float nextTemperature = temperature; 630 | vec3 position = convertCellIdToPosition(cellId); 631 | if (u_addHeat) { 632 | vec3 heatCenter = vec3(u_mousePosition.x * u_simulationSpace.x, u_simulationSpace.y * 0.25, u_mousePosition.y * u_simulationSpace.z); 633 | nextTemperature += smoothstep(u_heatSourceRadius, 0.0, length(position - heatCenter)) 634 | * u_deltaTime * u_heatSourceIntensity; 635 | } 636 | nextTemperature += (1.0 - exp(-u_temperatureDecay * u_deltaTime)) * (AMBIENT_TEMPERATURE - nextTemperature); 637 | 638 | float nextDensity = density + u_deltaTime * max(0.0, (nextTemperature - (AMBIENT_TEMPERATURE + 100.0))) * 0.01; 639 | nextDensity *= exp(-u_densityDecay * u_deltaTime); 640 | 641 | o_smoke = vec2(nextDensity, nextTemperature); 642 | } 643 | `; 644 | 645 | const RAYMARCH_VERTEX_SHADER_SOURCE = 646 | `#version 300 es 647 | 648 | const vec3[8] CUBE_POSITIONS = vec3[]( 649 | vec3(-1.0, -1.0, 1.0), 650 | vec3( 1.0, -1.0, 1.0), 651 | vec3( 1.0, -1.0, -1.0), 652 | vec3(-1.0, -1.0, -1.0), 653 | vec3(-1.0, 1.0, 1.0), 654 | vec3( 1.0, 1.0, 1.0), 655 | vec3( 1.0, 1.0, -1.0), 656 | vec3(-1.0, 1.0, -1.0) 657 | ); 658 | 659 | const vec3[6] CUBE_NORMALS = vec3[]( 660 | vec3(0.0, 0.0, 1.0), 661 | vec3(1.0, 0.0, 0.0), 662 | vec3(0.0, 0.0, -1.0), 663 | vec3(-1.0, 0.0, 0.0), 664 | vec3(0.0, 1.0, 0.0), 665 | vec3(0.0, -1.0, 0.0) 666 | ); 667 | 668 | const int[36] CUBE_INDICES = int[]( 669 | 0, 5, 4, 0, 1, 5, 670 | 1, 6, 5, 1, 2, 6, 671 | 2, 7, 6, 2, 3, 7, 672 | 3, 4, 7, 3, 0, 4, 673 | 4, 6, 7, 4, 5, 6, 674 | 3, 1, 0, 3, 2, 1 675 | ); 676 | 677 | out vec3 v_position; 678 | out vec3 v_normal; 679 | 680 | uniform mat4 u_mvpMatrix; 681 | uniform mat4 u_modelMatrix; 682 | uniform vec3 u_scale; 683 | 684 | void main(void) { 685 | vec3 position = u_scale * CUBE_POSITIONS[CUBE_INDICES[gl_VertexID]]; 686 | vec3 normal = CUBE_NORMALS[gl_VertexID / 6]; 687 | v_position = (u_modelMatrix * vec4(position, 1.0)).xyz; 688 | v_normal = (u_modelMatrix * vec4(normal, 0.0)).xyz; 689 | gl_Position = u_mvpMatrix * vec4(position, 1.0); 690 | } 691 | `; 692 | 693 | const RENDER_VELOCITY_FRAGMENT_SHADER_SOURCE = 694 | `#version 300 es 695 | 696 | precision highp float; 697 | 698 | in vec3 v_position; 699 | in vec3 v_normal; 700 | 701 | out vec4 o_color; 702 | 703 | uniform mat4 u_mvpMatrix; 704 | uniform mat4 u_modelMatrix; 705 | uniform mat4 u_invModelMatrix; 706 | uniform vec3 u_scale; 707 | uniform vec3 u_cameraPosition; 708 | uniform int u_cellTextureSize; 709 | uniform ivec3 u_resolution; 710 | uniform vec3 u_simulationSpace; 711 | uniform sampler2D u_velocityTexture; 712 | uniform float u_gridSpacing; 713 | 714 | struct Ray { 715 | vec3 origin; 716 | vec3 dir; 717 | }; 718 | 719 | Ray convertRayFromWorldToObject(Ray ray) { 720 | vec3 origin = (u_invModelMatrix * vec4(ray.origin, 1.0)).xyz; 721 | vec3 dir = normalize((u_invModelMatrix * vec4(ray.dir, 0.0)).xyz); 722 | return Ray(origin, dir); 723 | } 724 | 725 | void getRange(Ray ray, inout float tmin, inout float tmax) { 726 | for (int i = 0; i < 3; i++) { 727 | float t1 = (u_scale[i] - ray.origin[i]) / ray.dir[i]; 728 | float t2 = (-u_scale[i] - ray.origin[i]) / ray.dir[i]; 729 | tmin = max(tmin, min(t1, t2)); 730 | tmax = min(tmax, max(t1, t2)); 731 | } 732 | } 733 | 734 | int convertCellIndexToCellId(ivec3 cellIndex) { 735 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 736 | } 737 | 738 | ivec2 convertCellIdToCoord(int cellId) { 739 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 740 | } 741 | 742 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 743 | int cellId = convertCellIndexToCellId(cellIndex); 744 | return convertCellIdToCoord(cellId); 745 | } 746 | 747 | ivec3 convertPositionToCellIndex(vec3 position) { 748 | return ivec3(position / u_gridSpacing - 0.5); 749 | } 750 | 751 | vec3 convertToSimulationSpace(vec3 p) { 752 | p /= u_scale; 753 | p *= u_simulationSpace; 754 | p = (p + u_simulationSpace) * 0.5; 755 | return p; 756 | } 757 | 758 | vec3 sampleVelocity(ivec3 cellIndex) { 759 | if (cellIndex.x < 0 || cellIndex.x >= u_resolution.x || 760 | cellIndex.y < 0 || cellIndex.y >= u_resolution.y || 761 | cellIndex.z < 0 || cellIndex.z >= u_resolution.z) { 762 | return vec3(0.0); 763 | } 764 | ivec2 coord = convertCellIndexToCoord(cellIndex); 765 | return texelFetch(u_velocityTexture, coord, 0).xyz; 766 | } 767 | 768 | vec3 sampleVelocity(vec3 p) { 769 | vec3 cellIndex = convertToSimulationSpace(p) / u_gridSpacing; 770 | 771 | ivec3 i = ivec3(cellIndex) - (1 - int(step(0.5, cellIndex))); 772 | vec3 f = smoothstep(0.0, 1.0, fract(cellIndex + 0.5)); 773 | 774 | vec3 vel000 = sampleVelocity(i); 775 | vec3 vel100 = sampleVelocity(i + ivec3(1, 0, 0)); 776 | vec3 vel010 = sampleVelocity(i + ivec3(0, 1, 0)); 777 | vec3 vel110 = sampleVelocity(i + ivec3(1, 1, 0)); 778 | vec3 vel001 = sampleVelocity(i + ivec3(0, 0, 1)); 779 | vec3 vel101 = sampleVelocity(i + ivec3(1, 0, 1)); 780 | vec3 vel011 = sampleVelocity(i + ivec3(0, 1, 1)); 781 | vec3 vel111 = sampleVelocity(i + ivec3(1, 1, 1)); 782 | 783 | return mix( 784 | mix(mix(vel000, vel100, f.x), mix(vel010, vel110, f.x), f.y), 785 | mix(mix(vel001, vel101, f.x), mix(vel011, vel111, f.x), f.y), 786 | f.z 787 | ); 788 | } 789 | 790 | #define RAYMARCH_ITERATIONS 48 791 | 792 | vec4 raymarch(vec3 ro, vec3 rd, float tmin, float tmax) { 793 | float raymarchSize = (2.0 * length(u_scale)) / float(RAYMARCH_ITERATIONS); 794 | vec3 p = ro + (tmin + (raymarchSize - mod(tmin, raymarchSize))) * rd; 795 | vec3 color = vec3(0.0); 796 | float transmittance = 1.0; 797 | for (int ri = 0; ri < RAYMARCH_ITERATIONS; ri++) { 798 | vec3 velocity = sampleVelocity(p); 799 | float density = clamp(length(velocity) * 20.0, 0.0, 1.0); 800 | color += (clamp(velocity * 20.0, -1.0, 1.0) * 0.5 + 0.5) * transmittance * density; 801 | transmittance *= 1.0 - density; 802 | if (transmittance < 0.001) { 803 | break; 804 | } 805 | p += raymarchSize * rd; 806 | } 807 | return vec4(color, 1.0 - transmittance); 808 | } 809 | 810 | void main(void) { 811 | vec3 dir = normalize(v_position - u_cameraPosition); 812 | Ray ray = convertRayFromWorldToObject(Ray(u_cameraPosition, dir)); 813 | float tmin = 0.0; 814 | float tmax = 1e16; 815 | getRange(ray, tmin, tmax); 816 | vec4 c = raymarch(ray.origin, ray.dir, tmin, tmax); 817 | if (c.w > 0.0) { 818 | o_color = vec4(c.rgb, c.a); 819 | } else { 820 | discard; 821 | } 822 | } 823 | `; 824 | 825 | const RENDER_DENSITY_FRAGMENT_SHADER_SOURCE = 826 | `#version 300 es 827 | 828 | precision highp float; 829 | 830 | in vec3 v_position; 831 | in vec3 v_normal; 832 | 833 | out vec4 o_color; 834 | 835 | uniform mat4 u_mvpMatrix; 836 | uniform mat4 u_modelMatrix; 837 | uniform mat4 u_invModelMatrix; 838 | uniform vec3 u_scale; 839 | uniform vec3 u_cameraPosition; 840 | uniform int u_cellTextureSize; 841 | uniform ivec3 u_resolution; 842 | uniform vec3 u_simulationSpace; 843 | uniform sampler2D u_smokeTexture; 844 | uniform float u_gridSpacing; 845 | 846 | struct Ray { 847 | vec3 origin; 848 | vec3 dir; 849 | }; 850 | 851 | Ray convertRayFromWorldToObject(Ray ray) { 852 | vec3 origin = (u_invModelMatrix * vec4(ray.origin, 1.0)).xyz; 853 | vec3 dir = normalize((u_invModelMatrix * vec4(ray.dir, 0.0)).xyz); 854 | return Ray(origin, dir); 855 | } 856 | 857 | void getRange(Ray ray, inout float tmin, inout float tmax) { 858 | for (int i = 0; i < 3; i++) { 859 | float t1 = (u_scale[i] - ray.origin[i]) / ray.dir[i]; 860 | float t2 = (-u_scale[i] - ray.origin[i]) / ray.dir[i]; 861 | tmin = max(tmin, min(t1, t2)); 862 | tmax = min(tmax, max(t1, t2)); 863 | } 864 | } 865 | 866 | int convertCellIndexToCellId(ivec3 cellIndex) { 867 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 868 | } 869 | 870 | ivec2 convertCellIdToCoord(int cellId) { 871 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 872 | } 873 | 874 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 875 | int cellId = convertCellIndexToCellId(cellIndex); 876 | return convertCellIdToCoord(cellId); 877 | } 878 | 879 | ivec3 convertPositionToCellIndex(vec3 position) { 880 | return ivec3(position / u_gridSpacing - 0.5); 881 | } 882 | 883 | vec3 convertToSimulationSpace(vec3 p) { 884 | p /= u_scale; 885 | p *= u_simulationSpace; 886 | p = (p + u_simulationSpace) * 0.5; 887 | return p; 888 | } 889 | 890 | float sampleDensity(ivec3 cellIndex) { 891 | if (cellIndex.x < 0 || cellIndex.x >= u_resolution.x || 892 | cellIndex.y < 0 || cellIndex.y >= u_resolution.y || 893 | cellIndex.z < 0 || cellIndex.z >= u_resolution.z) { 894 | return 0.0; 895 | } 896 | ivec2 coord = convertCellIndexToCoord(cellIndex); 897 | return texelFetch(u_smokeTexture, coord, 0).x; 898 | } 899 | 900 | float sampleDensity(vec3 p) { 901 | vec3 cellIndex = convertToSimulationSpace(p) / u_gridSpacing; 902 | 903 | ivec3 i = ivec3(cellIndex) - (1 - int(step(0.5, cellIndex))); 904 | vec3 f = smoothstep(0.0, 1.0, fract(cellIndex + 0.5)); 905 | 906 | float dens000 = sampleDensity(i); 907 | float dens100 = sampleDensity(i + ivec3(1, 0, 0)); 908 | float dens010 = sampleDensity(i + ivec3(0, 1, 0)); 909 | float dens110 = sampleDensity(i + ivec3(1, 1, 0)); 910 | float dens001 = sampleDensity(i + ivec3(0, 0, 1)); 911 | float dens101 = sampleDensity(i + ivec3(1, 0, 1)); 912 | float dens011 = sampleDensity(i + ivec3(0, 1, 1)); 913 | float dens111 = sampleDensity(i + ivec3(1, 1, 1)); 914 | 915 | return mix( 916 | mix(mix(dens000, dens100, f.x), mix(dens010, dens110, f.x), f.y), 917 | mix(mix(dens001, dens101, f.x), mix(dens011, dens111, f.x), f.y), 918 | f.z 919 | ); 920 | } 921 | 922 | #define RAYMARCH_ITERATIONS 128 923 | #define SHADOW_ITERATIONS 4 924 | #define SHADOW_LENGTH u_simulationSpace.y * 1.0 925 | 926 | #define LIGHT_DIR normalize(vec3(0.0, 1.0, 0.0)) 927 | #define LIGHT_COLOR vec3(1.0, 1.0, 0.95) 928 | 929 | vec4 raymarch(vec3 ro, vec3 rd, float tmin, float tmax) { 930 | float raymarchSize = (length(2.0 * u_scale)) / float(RAYMARCH_ITERATIONS); 931 | float shadowSize = SHADOW_LENGTH / float(SHADOW_ITERATIONS); 932 | vec3 rayStep = rd * raymarchSize; 933 | vec3 shadowStep = LIGHT_DIR * shadowSize; 934 | vec3 p = ro + (tmin + (raymarchSize - mod(tmin, raymarchSize))) * rd; 935 | vec3 color = vec3(0.0); 936 | float transmittance = 1.0; 937 | for (int ri = 0; ri < RAYMARCH_ITERATIONS; ri++) { 938 | float density = clamp(sampleDensity(p), 0.0, 1.0); 939 | if (density > 0.001) { 940 | vec3 shadowPosition = p; 941 | float shadowDensity = 0.0; 942 | for(int si = 0; si < SHADOW_ITERATIONS; si++) { 943 | shadowPosition += shadowStep; 944 | shadowDensity = sampleDensity(shadowPosition); 945 | } 946 | float atten = exp(-shadowDensity * 1.0); 947 | vec3 attenLight = LIGHT_COLOR * atten; 948 | color += vec3(0.95, 0.98, 1.0) * attenLight * transmittance * density; 949 | 950 | transmittance *= 1.0 - density; 951 | } 952 | if (transmittance < 0.001) { 953 | break; 954 | } 955 | p += rayStep; 956 | } 957 | return vec4(color, 1.0 - transmittance); 958 | } 959 | 960 | void main(void) { 961 | vec3 dir = normalize(v_position - u_cameraPosition); 962 | Ray ray = convertRayFromWorldToObject(Ray(u_cameraPosition, dir)); 963 | float tmin = 0.0; 964 | float tmax = 1e16; 965 | getRange(ray, tmin, tmax); 966 | vec4 c = raymarch(ray.origin, ray.dir, tmin, tmax); 967 | if (c.w > 0.0) { 968 | o_color = vec4(pow(c.rgb, vec3(1.0 / 2.2)), c.w); 969 | } else { 970 | discard; 971 | } 972 | } 973 | `; 974 | 975 | const RENDER_TEMPERATURE_FRAGMENT_SHADER_SOURCE = 976 | `#version 300 es 977 | 978 | precision highp float; 979 | 980 | in vec3 v_position; 981 | in vec3 v_normal; 982 | 983 | out vec4 o_color; 984 | 985 | uniform mat4 u_mvpMatrix; 986 | uniform mat4 u_modelMatrix; 987 | uniform mat4 u_invModelMatrix; 988 | uniform vec3 u_scale; 989 | uniform vec3 u_cameraPosition; 990 | uniform int u_cellTextureSize; 991 | uniform ivec3 u_resolution; 992 | uniform vec3 u_simulationSpace; 993 | uniform sampler2D u_smokeTexture; 994 | uniform float u_gridSpacing; 995 | 996 | #define AMBIENT_TEMPERATURE 273.0 997 | 998 | struct Ray { 999 | vec3 origin; 1000 | vec3 dir; 1001 | }; 1002 | 1003 | Ray convertRayFromWorldToObject(Ray ray) { 1004 | vec3 origin = (u_invModelMatrix * vec4(ray.origin, 1.0)).xyz; 1005 | vec3 dir = normalize((u_invModelMatrix * vec4(ray.dir, 0.0)).xyz); 1006 | return Ray(origin, dir); 1007 | } 1008 | 1009 | void getRange(Ray ray, inout float tmin, inout float tmax) { 1010 | for (int i = 0; i < 3; i++) { 1011 | float t1 = (u_scale[i] - ray.origin[i]) / ray.dir[i]; 1012 | float t2 = (-u_scale[i] - ray.origin[i]) / ray.dir[i]; 1013 | tmin = max(tmin, min(t1, t2)); 1014 | tmax = min(tmax, max(t1, t2)); 1015 | } 1016 | } 1017 | 1018 | int convertCellIndexToCellId(ivec3 cellIndex) { 1019 | return cellIndex.x + cellIndex.y * u_resolution.x + cellIndex.z * (u_resolution.x * u_resolution.y); 1020 | } 1021 | 1022 | ivec2 convertCellIdToCoord(int cellId) { 1023 | return ivec2(cellId % u_cellTextureSize, cellId / u_cellTextureSize); 1024 | } 1025 | 1026 | ivec2 convertCellIndexToCoord(ivec3 cellIndex) { 1027 | int cellId = convertCellIndexToCellId(cellIndex); 1028 | return convertCellIdToCoord(cellId); 1029 | } 1030 | 1031 | ivec3 convertPositionToCellIndex(vec3 position) { 1032 | return ivec3(position / u_gridSpacing - 0.5); 1033 | } 1034 | 1035 | vec3 convertToSimulationSpace(vec3 p) { 1036 | p /= u_scale; 1037 | p *= u_simulationSpace; 1038 | p = (p + u_simulationSpace) * 0.5; 1039 | return p; 1040 | } 1041 | 1042 | float sampleTemperature(ivec3 cellIndex) { 1043 | if (cellIndex.x < 0 || cellIndex.x >= u_resolution.x || 1044 | cellIndex.y < 0 || cellIndex.y >= u_resolution.y || 1045 | cellIndex.z < 0 || cellIndex.z >= u_resolution.z) { 1046 | return AMBIENT_TEMPERATURE; 1047 | } 1048 | ivec2 coord = convertCellIndexToCoord(cellIndex); 1049 | return texelFetch(u_smokeTexture, coord, 0).y; 1050 | } 1051 | 1052 | float sampleTemperature(vec3 p) { 1053 | vec3 cellIndex = convertToSimulationSpace(p) / u_gridSpacing; 1054 | 1055 | ivec3 i = ivec3(cellIndex) - (1 - int(step(0.5, cellIndex))); 1056 | vec3 f = smoothstep(0.0, 1.0, fract(cellIndex + 0.5)); 1057 | 1058 | float temp000 = sampleTemperature(i); 1059 | float temp100 = sampleTemperature(i + ivec3(1, 0, 0)); 1060 | float temp010 = sampleTemperature(i + ivec3(0, 1, 0)); 1061 | float temp110 = sampleTemperature(i + ivec3(1, 1, 0)); 1062 | float temp001 = sampleTemperature(i + ivec3(0, 0, 1)); 1063 | float temp101 = sampleTemperature(i + ivec3(1, 0, 1)); 1064 | float temp011 = sampleTemperature(i + ivec3(0, 1, 1)); 1065 | float temp111 = sampleTemperature(i + ivec3(1, 1, 1)); 1066 | 1067 | return mix( 1068 | mix(mix(temp000, temp100, f.x), mix(temp010, temp110, f.x), f.y), 1069 | mix(mix(temp001, temp101, f.x), mix(temp011, temp111, f.x), f.y), 1070 | f.z 1071 | ); 1072 | } 1073 | 1074 | #define RAYMARCH_ITERATIONS 128 1075 | 1076 | const vec4[6] TEMPERATURE_COLOR = vec4[]( 1077 | vec4(0.0, 0.0, 0.0, 0.0), 1078 | vec4(0.0, 0.0, 0.0, AMBIENT_TEMPERATURE), 1079 | vec4(1.0, 0.0, 0.0, AMBIENT_TEMPERATURE + 100.0), 1080 | vec4(1.0, 0.5, 0.0, AMBIENT_TEMPERATURE + 200.0), 1081 | vec4(1.0, 1.0, 1.0, AMBIENT_TEMPERATURE + 300.0), 1082 | vec4(0.5, 0.5, 1.0, AMBIENT_TEMPERATURE + 400.0) 1083 | ); 1084 | 1085 | vec3 getTemperatureColor(float temperature) { 1086 | vec3 color = TEMPERATURE_COLOR[5].xyz; 1087 | for (int i = 0; i < 5; i++) { 1088 | if (temperature < TEMPERATURE_COLOR[i + 1].w) { 1089 | color = mix(TEMPERATURE_COLOR[i].xyz, TEMPERATURE_COLOR[i + 1].xyz, 1090 | 1.0 - (TEMPERATURE_COLOR[i + 1].w - temperature) / (TEMPERATURE_COLOR[i + 1]. w - TEMPERATURE_COLOR[i].w)); 1091 | break; 1092 | } 1093 | } 1094 | return color; 1095 | } 1096 | 1097 | vec4 raymarch(vec3 ro, vec3 rd, float tmin, float tmax) { 1098 | float raymarchSize = (2.0 * length(u_scale)) / float(RAYMARCH_ITERATIONS); 1099 | vec3 p = ro + (tmin + (raymarchSize - mod(tmin, raymarchSize))) * rd; 1100 | vec3 color = vec3(0.0); 1101 | float transmittance = 1.0; 1102 | for (int ri = 0; ri < RAYMARCH_ITERATIONS; ri++) { 1103 | float temperature = sampleTemperature(p); 1104 | vec3 c = getTemperatureColor(temperature); 1105 | float density = clamp((temperature - AMBIENT_TEMPERATURE) / 500.0, 0.0, 1.0); 1106 | color += c * transmittance * density; 1107 | transmittance *= 1.0 - density; 1108 | if (transmittance < 0.001) { 1109 | break; 1110 | } 1111 | p += raymarchSize * rd; 1112 | } 1113 | return vec4(color, 1.0 - transmittance); 1114 | } 1115 | 1116 | void main(void) { 1117 | vec3 dir = normalize(v_position - u_cameraPosition); 1118 | Ray ray = convertRayFromWorldToObject(Ray(u_cameraPosition, dir)); 1119 | float tmin = 0.0; 1120 | float tmax = 1e16; 1121 | getRange(ray, tmin, tmax); 1122 | vec4 c = raymarch(ray.origin, ray.dir, tmin, tmax); 1123 | if (c.w > 0.0) { 1124 | o_color = vec4(c.rgb, c.w); 1125 | } else { 1126 | discard; 1127 | } 1128 | } 1129 | `; 1130 | 1131 | let mousePosition = new Vector2(0.0, 0.0); 1132 | let mousePressing = false; 1133 | window.addEventListener('mousemove', event => { 1134 | mousePosition = new Vector2(event.clientX, window.innerHeight - event.clientY); 1135 | }); 1136 | window.addEventListener('mousedown', _ => { 1137 | mousePressing = true; 1138 | }); 1139 | window.addEventListener('mouseup', _ => { 1140 | mousePressing = false; 1141 | }); 1142 | 1143 | const stats = new Stats(); 1144 | document.body.appendChild(stats.dom); 1145 | 1146 | const parameters = { 1147 | 'air density': 2.354, 1148 | 'density force': 0.02, 1149 | 'temperature force': 0.0001, 1150 | 'heat radius': 0.05, 1151 | 'heat intensity': 1000.0, 1152 | 'velocity decay': 0.1, 1153 | 'density decay': 0.3, 1154 | 'temperature decay': 0.5, 1155 | 'time step': 0.005, 1156 | 'time scale': 0.5, 1157 | 'render': 'density', 1158 | 'reset': _ => reset() 1159 | }; 1160 | 1161 | const gui = new dat.GUI(); 1162 | gui.add(parameters, 'density force', 0.0, 0.1).step(0.0001); 1163 | gui.add(parameters, 'temperature force', 0.0, 0.0003).step(0.00001); 1164 | gui.add(parameters, 'heat radius', 0.0, 0.1).step(0.001); 1165 | gui.add(parameters, 'heat intensity', 0.0, 2000.0).step(1.0); 1166 | gui.add(parameters, 'velocity decay', 0.0, 2.0).step(0.1); 1167 | gui.add(parameters, 'density decay', 0.0, 2.0).step(0.1); 1168 | gui.add(parameters, 'temperature decay', 0.0, 2.0).step(0.1); 1169 | gui.add(parameters, 'time step', 0.0001, 0.01).step(0.0001); 1170 | gui.add(parameters, 'time scale', 0.5, 2.0).step(0.001); 1171 | gui.add(parameters, 'render', ['density', 'temperature', 'velocity']); 1172 | gui.add(parameters, 'reset'); 1173 | 1174 | const SIMULATION_RESOLUTION = new Vector3(50, 50, 50); 1175 | const GRID_SPACING = 0.005; 1176 | const SIMULATION_SPACE = Vector3.mul(SIMULATION_RESOLUTION, GRID_SPACING); 1177 | const CELL_NUM = SIMULATION_RESOLUTION.x * SIMULATION_RESOLUTION.y * SIMULATION_RESOLUTION.z; 1178 | 1179 | let cellTextureSize; 1180 | for (let i = 0; ; i++) { 1181 | cellTextureSize = 2 ** i; 1182 | if (cellTextureSize * cellTextureSize >= CELL_NUM) { 1183 | break; 1184 | } 1185 | } 1186 | 1187 | const canvas = document.getElementById('canvas'); 1188 | const resizeCanvas = function() { 1189 | canvas.width = window.innerWidth; 1190 | canvas.height = window.innerHeight; 1191 | }; 1192 | window.addEventListener('resize', _ => { 1193 | resizeCanvas(); 1194 | }); 1195 | resizeCanvas(); 1196 | 1197 | const gl = canvas.getContext('webgl2'); 1198 | gl.getExtension('EXT_color_buffer_float'); 1199 | gl.clearColor(0.7, 0.7, 0.7, 1.0); 1200 | 1201 | const initializeVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, INITIALIZE_VELOCITY_FRAGMENT_SHADER_SOURCE); 1202 | const initializeSmokeProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, INITIALIZE_SMOKE_FRAGMENT_SHADER_SOURCE); 1203 | const addBuoyancyForceProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADD_BUOYANCY_FORCE_FRAGMENT_SHADER_SOURCE); 1204 | const advectVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADVECT_VELOCITY_FRAGMENT_SHADER_SOURCE); 1205 | const computePressureProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, COMPUTE_PRESSURE_FRAGMENT_SHADER_SOURCE); 1206 | const addPressureForceProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADD_PRESSURE_FORCE_FRAGMENT_SHADER_SOURCE); 1207 | const decayVelocityProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, DECAY_VELOCITY_FRAGMENT_SHADER_SOURCE); 1208 | const advectSmokeProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADVECT_SMOKE_FRAGMENT_SHADER_SOURCE); 1209 | const addSmokeProgram = createProgramFromSource(gl, FILL_VIEWPORT_VERTEX_SHADER_SOURCE, ADD_SMOKE_FRAGMENT_SHADER_SOURCE); 1210 | const renderVelocityProgram = createProgramFromSource(gl, RAYMARCH_VERTEX_SHADER_SOURCE, RENDER_VELOCITY_FRAGMENT_SHADER_SOURCE); 1211 | const renderDensityProgram = createProgramFromSource(gl, RAYMARCH_VERTEX_SHADER_SOURCE, RENDER_DENSITY_FRAGMENT_SHADER_SOURCE); 1212 | const renderTemperatureProgram = createProgramFromSource(gl, RAYMARCH_VERTEX_SHADER_SOURCE, RENDER_TEMPERATURE_FRAGMENT_SHADER_SOURCE); 1213 | 1214 | const initializeSmokeUniforms = getUniformLocations(gl, initializeSmokeProgram, ['u_cellNum', 'u_cellTextureSize', 'u_resolution', 'u_gridSpacing', 'u_simulationSpace']); 1215 | const addBuoyancyForceUniforms = getUniformLocations(gl, addBuoyancyForceProgram, ['u_cellNum', 'u_cellTextureSize', 'u_velocityTexture', 'u_smokeTexture', 'u_deltaTime', 'u_densityScale', 'u_temperatureScale']); 1216 | const advectVelocityUniforms = getUniformLocations(gl, advectVelocityProgram, 1217 | ['u_cellNum', 'u_cellTextureSize', 'u_resolution', 'u_velocityTexture', 'u_deltaTime', 'u_gridSpacing']); 1218 | const computePressureUniforms = getUniformLocations(gl, computePressureProgram, ['u_cellNum', 'u_cellTextureSize', 'u_resolution', 'u_velocityTexture', 'u_pressureTexture', 'u_deltaTime', 'u_gridSpacing', 'u_density']); 1219 | const addPressureForceUniforms = getUniformLocations(gl, addPressureForceProgram, ['u_cellNum', 'u_cellTextureSize', 'u_resolution', 'u_velocityTexture', 'u_pressureTexture', 'u_deltaTime', 'u_gridSpacing', 'u_density']); 1220 | const decayVelocityUniforms = getUniformLocations(gl, decayVelocityProgram, ['u_velocityTexture', 'u_deltaTime', 'u_velocityDecay']); 1221 | const advectSmokeUniforms = getUniformLocations(gl, advectSmokeProgram, 1222 | ['u_cellNum', 'u_cellTextureSize', 'u_resolution', 'u_velocityTexture', 'u_smokeTexture', 'u_deltaTime', 'u_gridSpacing']); 1223 | const addSmokeUniforms = getUniformLocations(gl, addSmokeProgram, 1224 | ['u_cellNum', 'u_cellTextureSize', 'u_resolution', 'u_simulationSpace', 'u_smokeTexture', 'u_deltaTime', 'u_gridSpacing', 'u_addHeat', 'u_mousePosition', 'u_heatSourceRadius', 'u_heatSourceIntensity', 'u_densityDecay', 'u_temperatureDecay']); 1225 | const renderVelocityUniforms = getUniformLocations(gl, renderVelocityProgram, 1226 | ['u_mvpMatrix', 'u_modelMatrix', 'u_invModelMatrix', 'u_scale', 'u_cameraPosition', 1227 | 'u_cellTextureSize', 'u_resolution', 'u_simulationSpace', 'u_velocityTexture', 'u_gridSpacing']); 1228 | const renderDensityUniforms = getUniformLocations(gl, renderDensityProgram, 1229 | ['u_mvpMatrix', 'u_modelMatrix', 'u_invModelMatrix', 'u_scale', 'u_cameraPosition', 1230 | 'u_cellTextureSize', 'u_resolution', 'u_simulationSpace', 'u_smokeTexture', 'u_gridSpacing']); 1231 | const renderTemperatureUniforms = getUniformLocations(gl, renderTemperatureProgram, 1232 | ['u_mvpMatrix', 'u_modelMatrix', 'u_invModelMatrix', 'u_scale', 'u_cameraPosition', 1233 | 'u_cellTextureSize', 'u_resolution', 'u_simulationSpace', 'u_smokeTexture', 'u_gridSpacing']); 1234 | 1235 | let requestId = null; 1236 | const reset = function() { 1237 | if (requestId !== null) { 1238 | cancelAnimationFrame(requestId); 1239 | requestId = null; 1240 | } 1241 | 1242 | let velocityFbObjR = createVelocityFramebuffer(gl, cellTextureSize, cellTextureSize); 1243 | let velocityFbObjW = createVelocityFramebuffer(gl, cellTextureSize, cellTextureSize); 1244 | const swapVelocityFbObj = function() { 1245 | const tmp = velocityFbObjR; 1246 | velocityFbObjR = velocityFbObjW; 1247 | velocityFbObjW = tmp; 1248 | }; 1249 | 1250 | let pressureFbObjR = createPressureFramebuffer(gl, cellTextureSize, cellTextureSize); 1251 | let pressureFbObjW = createPressureFramebuffer(gl, cellTextureSize, cellTextureSize); 1252 | const swapPressureFbObj = function() { 1253 | const tmp = pressureFbObjR; 1254 | pressureFbObjR = pressureFbObjW; 1255 | pressureFbObjW = tmp; 1256 | }; 1257 | 1258 | 1259 | let smokeFbObjR = createSmokeFramebuffer(gl, cellTextureSize, cellTextureSize); 1260 | let smokeFbObjW = createSmokeFramebuffer(gl, cellTextureSize, cellTextureSize); 1261 | const swapSmokeFbObj = function() { 1262 | const tmp = smokeFbObjR; 1263 | smokeFbObjR = smokeFbObjW; 1264 | smokeFbObjW = tmp; 1265 | }; 1266 | 1267 | const initializeVelocity = function() { 1268 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 1269 | gl.useProgram(initializeVelocityProgram); 1270 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1271 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1272 | swapVelocityFbObj(); 1273 | }; 1274 | 1275 | const initializeSmoke = function() { 1276 | gl.bindFramebuffer(gl.FRAMEBUFFER, smokeFbObjW.framebuffer); 1277 | gl.useProgram(initializeSmokeProgram); 1278 | gl.uniform1i(initializeSmokeUniforms['u_cellNum'], CELL_NUM); 1279 | gl.uniform1i(initializeSmokeUniforms['u_cellTextureSize'], cellTextureSize); 1280 | gl.uniform3iv(initializeSmokeUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1281 | gl.uniform1f(initializeSmokeUniforms['u_gridSpacing'], GRID_SPACING); 1282 | gl.uniform3fv(initializeSmokeUniforms['u_simulationSpace'], SIMULATION_SPACE.toArray()); 1283 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1284 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1285 | swapSmokeFbObj(); 1286 | } 1287 | 1288 | const addBuoyancyForce = function(deltaTime) { 1289 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 1290 | gl.useProgram(addBuoyancyForceProgram); 1291 | gl.uniform1i(addBuoyancyForceUniforms['u_cellNum'], CELL_NUM); 1292 | gl.uniform1i(addBuoyancyForceUniforms['u_cellTextureSize'], cellTextureSize); 1293 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, addBuoyancyForceUniforms['u_velocityTexture']); 1294 | setUniformTexture(gl, 1, smokeFbObjR.smokeTexture, addBuoyancyForceUniforms['u_smokeTexture']); 1295 | gl.uniform1f(addBuoyancyForceUniforms['u_deltaTime'], deltaTime); 1296 | gl.uniform1f(addBuoyancyForceUniforms['u_densityScale'], parameters['density force']); 1297 | gl.uniform1f(addBuoyancyForceUniforms['u_temperatureScale'], parameters['temperature force']); 1298 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1299 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1300 | swapVelocityFbObj(); 1301 | }; 1302 | 1303 | const advectVelocity = function(deltaTime) { 1304 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 1305 | gl.useProgram(advectVelocityProgram); 1306 | gl.uniform1i(advectVelocityUniforms['u_cellNum'], CELL_NUM); 1307 | gl.uniform1i(advectVelocityUniforms['u_cellTextureSize'], cellTextureSize); 1308 | gl.uniform3iv(advectVelocityUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1309 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, advectVelocityUniforms['u_velocityTexture']); 1310 | gl.uniform1f(advectVelocityUniforms['u_deltaTime'], deltaTime); 1311 | gl.uniform1f(advectVelocityUniforms['u_gridSpacing'], GRID_SPACING); 1312 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1313 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1314 | swapVelocityFbObj(); 1315 | }; 1316 | 1317 | const computePressure = function(deltaTime) { 1318 | gl.useProgram(computePressureProgram); 1319 | gl.uniform1i(computePressureUniforms['u_cellNum'], CELL_NUM); 1320 | gl.uniform1i(computePressureUniforms['u_cellTextureSize'], cellTextureSize); 1321 | gl.uniform3iv(computePressureUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1322 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, computePressureUniforms['u_velocityTexture']); 1323 | gl.uniform1f(computePressureUniforms['u_deltaTime'], deltaTime); 1324 | gl.uniform1f(computePressureUniforms['u_gridSpacing'], GRID_SPACING); 1325 | gl.uniform1f(computePressureUniforms['u_density'], parameters['air density']); 1326 | for (let i = 0; i < 10; i++) { 1327 | gl.bindFramebuffer(gl.FRAMEBUFFER, pressureFbObjW.framebuffer); 1328 | setUniformTexture(gl, 1, pressureFbObjR.pressureTexture, computePressureUniforms['u_pressureTexture']) 1329 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1330 | swapPressureFbObj(); 1331 | } 1332 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1333 | }; 1334 | 1335 | const addPressureForce = function(deltaTime) { 1336 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 1337 | gl.useProgram(addPressureForceProgram); 1338 | gl.uniform1i(addPressureForceUniforms['u_cellNum'], CELL_NUM); 1339 | gl.uniform1i(addPressureForceUniforms['u_cellTextureSize'], cellTextureSize); 1340 | gl.uniform3iv(addPressureForceUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1341 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, addPressureForceUniforms['u_velocityTexture']); 1342 | setUniformTexture(gl, 1, pressureFbObjR.pressureTexture, addPressureForceUniforms['u_pressureTexture']); 1343 | gl.uniform1f(addPressureForceUniforms['u_deltaTime'], deltaTime); 1344 | gl.uniform1f(addPressureForceUniforms['u_gridSpacing'], GRID_SPACING); 1345 | gl.uniform1f(addPressureForceUniforms['u_density'], parameters['air density']); 1346 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1347 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1348 | swapVelocityFbObj(); 1349 | }; 1350 | 1351 | const decayVelocity = function(deltaTime) { 1352 | gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFbObjW.framebuffer); 1353 | gl.useProgram(decayVelocityProgram); 1354 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, decayVelocityUniforms['u_velocityTexture']); 1355 | gl.uniform1f(decayVelocityUniforms['u_deltaTime'], deltaTime); 1356 | gl.uniform1f(decayVelocityUniforms['u_velocityDecay'], parameters['velocity decay']); 1357 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1358 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1359 | swapVelocityFbObj(); 1360 | } 1361 | 1362 | const updateVelocity = function(deltaTime) { 1363 | addBuoyancyForce(deltaTime); 1364 | advectVelocity(deltaTime); 1365 | computePressure(deltaTime); 1366 | addPressureForce(deltaTime); 1367 | decayVelocity(deltaTime); 1368 | }; 1369 | 1370 | const advectSmoke = function(deltaTime) { 1371 | gl.bindFramebuffer(gl.FRAMEBUFFER, smokeFbObjW.framebuffer); 1372 | gl.useProgram(advectSmokeProgram); 1373 | gl.uniform1i(advectSmokeUniforms['u_cellNum'], CELL_NUM); 1374 | gl.uniform1i(advectSmokeUniforms['u_cellTextureSize'], cellTextureSize); 1375 | gl.uniform3iv(advectSmokeUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1376 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, advectSmokeUniforms['u_velocityTexture']); 1377 | setUniformTexture(gl, 1, smokeFbObjR.smokeTexture, advectSmokeUniforms['u_smokeTexture']); 1378 | gl.uniform1f(advectSmokeUniforms['u_deltaTime'], deltaTime); 1379 | gl.uniform1f(advectSmokeUniforms['u_gridSpacing'], GRID_SPACING); 1380 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1381 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1382 | swapSmokeFbObj(); 1383 | } 1384 | 1385 | const addSmoke = function(deltaTime) { 1386 | gl.bindFramebuffer(gl.FRAMEBUFFER, smokeFbObjW.framebuffer); 1387 | gl.useProgram(addSmokeProgram); 1388 | gl.uniform1i(addSmokeUniforms['u_cellNum'], CELL_NUM); 1389 | gl.uniform1i(addSmokeUniforms['u_cellTextureSize'], cellTextureSize); 1390 | gl.uniform3iv(addSmokeUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1391 | gl.uniform3fv(addSmokeUniforms['u_simulationSpace'], SIMULATION_SPACE.toArray()); 1392 | setUniformTexture(gl, 0, smokeFbObjR.smokeTexture, addSmokeUniforms['u_smokeTexture']); 1393 | gl.uniform1f(addSmokeUniforms['u_deltaTime'], deltaTime); 1394 | gl.uniform1f(addSmokeUniforms['u_gridSpacing'], GRID_SPACING); 1395 | gl.uniform1i(addSmokeUniforms['u_addHeat'], mousePressing); 1396 | const heatSourceCenter = new Vector2(mousePosition.x / canvas.width, mousePosition.y / canvas.height); 1397 | gl.uniform2fv(addSmokeUniforms['u_mousePosition'], heatSourceCenter.toArray()); 1398 | gl.uniform1f(addSmokeUniforms['u_heatSourceRadius'], parameters['heat radius']); 1399 | gl.uniform1f(addSmokeUniforms['u_heatSourceIntensity'], parameters['heat intensity']); 1400 | gl.uniform1f(addSmokeUniforms['u_densityDecay'], parameters['density decay']); 1401 | gl.uniform1f(addSmokeUniforms['u_temperatureDecay'], parameters['temperature decay']); 1402 | gl.drawArrays(gl.TRIANGLES, 0, 6); 1403 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1404 | swapSmokeFbObj(); 1405 | } 1406 | 1407 | const updateSmoke = function(deltaTime) { 1408 | advectSmoke(deltaTime); 1409 | addSmoke(deltaTime); 1410 | } 1411 | 1412 | const stepSimulation = function(deltaTime) { 1413 | gl.viewport(0.0, 0.0, cellTextureSize, cellTextureSize); 1414 | updateVelocity(deltaTime); 1415 | updateSmoke(deltaTime); 1416 | } 1417 | 1418 | 1419 | const RENDER_SCALE = Vector3.mul(SIMULATION_SPACE, 75.0 / SIMULATION_SPACE.y); 1420 | const CAMERA_POSITION = new Vector3(100.0, 100.0, 150.0); 1421 | const MODEL_MATRIX = Matrix4.identity; 1422 | const iNV_MODEL_MATRIX = Matrix4.identity; 1423 | const VIEW_MATRIX = Matrix4.inverse(Matrix4.lookAt( 1424 | CAMERA_POSITION, 1425 | Vector3.zero, 1426 | new Vector3(0.0, 1.0, 0.0) 1427 | )); 1428 | let mvpMatrix; 1429 | const setMvpMatrix = function() { 1430 | const projectionMatrix = Matrix4.perspective(canvas.width / canvas.height, 60.0, 0.01, 1000.0); 1431 | mvpMatrix = Matrix4.mul(VIEW_MATRIX, projectionMatrix); 1432 | }; 1433 | setMvpMatrix(); 1434 | window.addEventListener('resize', _ => { 1435 | setMvpMatrix(); 1436 | }); 1437 | 1438 | const renderVelocity = function() { 1439 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 1440 | gl.useProgram(renderVelocityProgram); 1441 | gl.uniformMatrix4fv(renderVelocityUniforms['u_mvpMatrix'], false, mvpMatrix.elements); 1442 | gl.uniformMatrix4fv(renderVelocityUniforms['u_modelMatrix'], false, MODEL_MATRIX.elements); 1443 | gl.uniformMatrix4fv(renderVelocityUniforms['u_invModelMatrix'], false, iNV_MODEL_MATRIX.elements); 1444 | gl.uniform3fv(renderVelocityUniforms['u_scale'], RENDER_SCALE.toArray()); 1445 | gl.uniform3fv(renderVelocityUniforms['u_cameraPosition'], CAMERA_POSITION.toArray()); 1446 | gl.uniform1i(renderVelocityUniforms['u_cellTextureSize'], cellTextureSize); 1447 | gl.uniform3iv(renderVelocityUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1448 | gl.uniform3fv(renderVelocityUniforms['u_simulationSpace'], SIMULATION_SPACE.toArray()); 1449 | setUniformTexture(gl, 0, velocityFbObjR.velocityTexture, renderVelocityUniforms['u_velocityTexture']); 1450 | gl.uniform1f(renderVelocityUniforms['u_gridSpacing'], GRID_SPACING); 1451 | gl.enable(gl.DEPTH_TEST); 1452 | gl.enable(gl.CULL_FACE); 1453 | gl.enable(gl.BLEND); 1454 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 1455 | gl.drawArrays(gl.TRIANGLES, 0, 36); 1456 | gl.disable(gl.DEPTH_TEST); 1457 | gl.disable(gl.CULL_FACE); 1458 | gl.disable(gl.BLEND); 1459 | } 1460 | 1461 | const renderDensity = function() { 1462 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 1463 | gl.useProgram(renderDensityProgram); 1464 | gl.uniformMatrix4fv(renderDensityUniforms['u_mvpMatrix'], false, mvpMatrix.elements); 1465 | gl.uniformMatrix4fv(renderDensityUniforms['u_modelMatrix'], false, MODEL_MATRIX.elements); 1466 | gl.uniformMatrix4fv(renderDensityUniforms['u_invModelMatrix'], false, iNV_MODEL_MATRIX.elements); 1467 | gl.uniform3fv(renderDensityUniforms['u_scale'], RENDER_SCALE.toArray()); 1468 | gl.uniform3fv(renderDensityUniforms['u_cameraPosition'], CAMERA_POSITION.toArray()); 1469 | gl.uniform1i(renderDensityUniforms['u_cellTextureSize'], cellTextureSize); 1470 | gl.uniform3iv(renderDensityUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1471 | gl.uniform3fv(renderDensityUniforms['u_simulationSpace'], SIMULATION_SPACE.toArray()); 1472 | setUniformTexture(gl, 0, smokeFbObjR.smokeTexture, renderDensityUniforms['u_smokeTexture']); 1473 | gl.uniform1f(renderDensityUniforms['u_gridSpacing'], GRID_SPACING); 1474 | gl.enable(gl.DEPTH_TEST); 1475 | gl.enable(gl.CULL_FACE); 1476 | gl.enable(gl.BLEND); 1477 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 1478 | gl.drawArrays(gl.TRIANGLES, 0, 36); 1479 | gl.disable(gl.DEPTH_TEST); 1480 | gl.disable(gl.CULL_FACE); 1481 | gl.disable(gl.BLEND); 1482 | } 1483 | 1484 | const renderTemperature = function() { 1485 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 1486 | gl.useProgram(renderTemperatureProgram); 1487 | gl.uniformMatrix4fv(renderTemperatureUniforms['u_mvpMatrix'], false, mvpMatrix.elements); 1488 | gl.uniformMatrix4fv(renderTemperatureUniforms['u_modelMatrix'], false, MODEL_MATRIX.elements); 1489 | gl.uniformMatrix4fv(renderTemperatureUniforms['u_invModelMatrix'], false, iNV_MODEL_MATRIX.elements); 1490 | gl.uniform3fv(renderTemperatureUniforms['u_scale'], RENDER_SCALE.toArray()); 1491 | gl.uniform3fv(renderTemperatureUniforms['u_cameraPosition'], CAMERA_POSITION.toArray()); 1492 | gl.uniform1i(renderTemperatureUniforms['u_cellTextureSize'], cellTextureSize); 1493 | gl.uniform3iv(renderTemperatureUniforms['u_resolution'], SIMULATION_RESOLUTION.toArray()); 1494 | gl.uniform3fv(renderTemperatureUniforms['u_simulationSpace'], SIMULATION_SPACE.toArray()); 1495 | setUniformTexture(gl, 0, smokeFbObjR.smokeTexture, renderTemperatureUniforms['u_smokeTexture']); 1496 | gl.uniform1f(renderTemperatureUniforms['u_gridSpacing'], GRID_SPACING); 1497 | gl.enable(gl.DEPTH_TEST); 1498 | gl.enable(gl.CULL_FACE); 1499 | gl.enable(gl.BLEND); 1500 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 1501 | gl.drawArrays(gl.TRIANGLES, 0, 36); 1502 | gl.disable(gl.DEPTH_TEST); 1503 | gl.disable(gl.CULL_FACE); 1504 | gl.disable(gl.BLEND); 1505 | } 1506 | 1507 | const render = function() { 1508 | gl.viewport(0.0, 0.0, canvas.width, canvas.height); 1509 | if (parameters['render'] === 'velocity') { 1510 | renderVelocity(); 1511 | } else if (parameters['render'] === 'temperature') { 1512 | renderTemperature(); 1513 | } else { 1514 | renderDensity(); 1515 | } 1516 | }; 1517 | 1518 | initializeVelocity(); 1519 | initializeSmoke(); 1520 | let simulationSeconds = 0.0; 1521 | let remaindedSimulationSeconds = 0.0; 1522 | let previousRealSeconds = performance.now() * 0.001; 1523 | const loop = function() { 1524 | stats.update(); 1525 | 1526 | const currentRealSeconds = performance.now() * 0.001; 1527 | const nextSimulationSeconds = simulationSeconds + remaindedSimulationSeconds + parameters['time scale'] * Math.min(0.02, currentRealSeconds - previousRealSeconds); 1528 | previousRealSeconds = currentRealSeconds; 1529 | const timeStep = parameters['time step']; 1530 | while(nextSimulationSeconds - simulationSeconds > timeStep) { 1531 | stepSimulation(timeStep); 1532 | simulationSeconds += timeStep; 1533 | } 1534 | remaindedSimulationSeconds = nextSimulationSeconds - simulationSeconds; 1535 | 1536 | render(); 1537 | requestId = requestAnimationFrame(loop); 1538 | }; 1539 | loop(); 1540 | }; 1541 | reset(); 1542 | 1543 | }()); -------------------------------------------------------------------------------- /js/math.js: -------------------------------------------------------------------------------- 1 | class Vector2 { 2 | constructor(x, y) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | 7 | static get zero() { 8 | return new Vector2(0.0, 0.0); 9 | } 10 | 11 | static add(v1, v2) { 12 | return new Vector2(v1.x + v2.x, v1.y + v2.y); 13 | } 14 | 15 | static sub(v1, v2) { 16 | return new Vector2(v1.x - v2.x, v1.y - v2.y); 17 | } 18 | 19 | static mul(v, s) { 20 | return new Vector2(v.x * s, v.y * s); 21 | } 22 | 23 | static div(v, s) { 24 | return new Vector2(v.x / s, v.y / s); 25 | } 26 | 27 | static norm(v) { 28 | const m = v.mag(); 29 | return new Vector2(v.x / m, v.y / m); 30 | } 31 | 32 | static dot(v1, v2) { 33 | return v1.x * v2.x + v1.y * v2.y; 34 | } 35 | 36 | static dist(v1, v2) { 37 | const v = Vector2.sub(v1, v2); 38 | return v.mag(); 39 | } 40 | 41 | static equals(v1, v2) { 42 | return v1.x === v2.x && v1.y === v2.y; 43 | } 44 | 45 | add(v) { 46 | this.x += v.x; 47 | this.y += v.y; 48 | return this; 49 | } 50 | 51 | sub(v) { 52 | this.x -= v.x; 53 | this.y -= v.y; 54 | return this; 55 | } 56 | 57 | mul(s) { 58 | this.x *= s; 59 | this.y *= s; 60 | return this; 61 | } 62 | 63 | div(s) { 64 | this.x /= s; 65 | this.y /= s; 66 | return this; 67 | } 68 | 69 | ceil() { 70 | this.x = Math.ceil(this.x); 71 | this.y = Math.ceil(this.y); 72 | return this; 73 | } 74 | 75 | mag() { 76 | return Math.sqrt(this.sqMag()); 77 | } 78 | 79 | sqMag() { 80 | return this.x * this.x + this.y * this.y; 81 | } 82 | 83 | norm() { 84 | return Vector2.norm(this); 85 | } 86 | 87 | dot(v) { 88 | return Vector3.dot(this, v); 89 | } 90 | 91 | toArray() { 92 | return [this.x, this.y]; 93 | } 94 | } 95 | 96 | class Vector3 { 97 | constructor(x, y, z) { 98 | this.x = x; 99 | this.y = y; 100 | this.z = z; 101 | } 102 | 103 | static get zero() { 104 | return new Vector3(0.0, 0.0, 0.0); 105 | } 106 | 107 | static add(v1, v2) { 108 | return new Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); 109 | } 110 | 111 | static sub(v1, v2) { 112 | return new Vector3(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); 113 | } 114 | 115 | static mul(v, s) { 116 | return new Vector3(v.x * s, v.y * s, v.z * s); 117 | } 118 | 119 | static div(v, s) { 120 | return new Vector3(v.x / s, v.y / s, v.z / s); 121 | } 122 | 123 | static norm(v) { 124 | const m = v.mag(); 125 | return new Vector3(v.x / m, v.y / m, v.z / m); 126 | } 127 | 128 | static dot(v1, v2) { 129 | return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; 130 | } 131 | 132 | static cross(v1, v2) { 133 | return new Vector3( 134 | v1.y * v2.z - v1.z * v2.y, 135 | v1.z * v2.x - v1.x * v2.z, 136 | v1.x * v2.y - v1.y * v2.x 137 | ); 138 | } 139 | 140 | static dist(v1, v2) { 141 | const v = Vector3.sub(v1, v2); 142 | return v.mag(); 143 | } 144 | 145 | add(v) { 146 | this.x += v.x; 147 | this.y += v.y; 148 | this.z += v.z; 149 | return this; 150 | } 151 | 152 | sub(v) { 153 | this.x -= v.x; 154 | this.y -= v.y; 155 | this.z -= v.z; 156 | return this; 157 | } 158 | 159 | mul(s) { 160 | this.x *= s; 161 | this.y *= s; 162 | this.z *= s; 163 | return this; 164 | } 165 | 166 | div(s) { 167 | this.x /= s; 168 | this.y /= s; 169 | this.z /= s; 170 | return this; 171 | } 172 | 173 | ceil() { 174 | this.x = Math.ceil(this.x); 175 | this.y = Math.ceil(this.y); 176 | this.z = Math.ceil(this.z); 177 | return this; 178 | } 179 | 180 | mag() { 181 | return Math.sqrt(this.sqMag()); 182 | } 183 | 184 | sqMag() { 185 | return this.x * this.x + this.y * this.y + this.z * this.z; 186 | } 187 | 188 | norm() { 189 | const m = this.mag(); 190 | this.div(m); 191 | return this; 192 | } 193 | 194 | dot(v) { 195 | return Vector3.dot(this, v); 196 | } 197 | 198 | cross(v) { 199 | return Vector3.cross(this, v); 200 | } 201 | 202 | toArray() { 203 | return [this.x, this.y, this.z]; 204 | } 205 | } 206 | 207 | class Matrix4 { 208 | constructor(elements) { 209 | this.elements = new Float32Array(elements); 210 | } 211 | 212 | get determinant() { 213 | const e = this.elements; 214 | 215 | return e[0] * (e[5] * e[10] * e[15] + e[9] * e[14] * e[7] + e[13] * e[6] * e[11] - e[13] * e[10] * e[7] - e[9] * e[6] * e[15] - e[5] * e[14] * e[11]) 216 | - e[4] * (e[1] * e[10] * e[15] + e[9] * e[14] * e[3] + e[13] * e[2] * e[11] - e[13] * e[10] * e[3] - e[9] * e[2] * e[15] - e[1] * e[14] * e[11]) 217 | + e[8] * (e[1] * e[6] * e[15] + e[5] * e[14] * e[3] + e[13] * e[2] * e[7] - e[13] * e[6] * e[3] - e[5] * e[2] * e[15] - e[1] * e[14] * e[7]) 218 | - e[8] * (e[1] * e[6] * e[11] + e[5] * e[10] * e[3] + e[9] * e[2] * e[7] - e[9] * e[6] * e[3] - e[5] * e[2] * e[11] - e[1] * e[10] * e[7]) 219 | } 220 | 221 | static get identity() { 222 | return new Matrix4([ 223 | 1.0, 0.0, 0.0, 0.0, 224 | 0.0, 1.0, 0.0, 0.0, 225 | 0.0, 0.0, 1.0, 0.0, 226 | 0.0, 0.0, 0.0, 1.0 227 | ]); 228 | } 229 | 230 | static translate(x, y, z) { 231 | return new Matrix4([ 232 | 1.0, 0.0, 0.0, 0.0, 233 | 0.0, 1.0, 0.0, 0.0, 234 | 0.0, 0.0, 1.0, 0.0, 235 | x, y, z, 1.0 236 | ]); 237 | } 238 | 239 | static scale(x, y, z) { 240 | return new Matrix4([ 241 | x, 0.0, 0.0, 0.0, 242 | 0.0, y, 0.0, 0.0, 243 | 0.0, 0.0, z, 0.0, 244 | 0.0, 0.0, 0.0, 1.0 245 | ]); 246 | } 247 | 248 | static rotateX(r) { 249 | const c = Math.cos(r); 250 | const s = Math.sin(r); 251 | return new Matrix4([ 252 | 1.0, 0.0, 0.0, 0.0, 253 | 0.0, c, s, 0.0, 254 | 0.0, -s, c, 0.0, 255 | 0.0, 0.0, 0.0, 1.0 256 | ]); 257 | } 258 | 259 | static rotateY(r) { 260 | const c = Math.cos(r); 261 | const s = Math.sin(r); 262 | return new Matrix4([ 263 | c, 0.0, -s, 0.0, 264 | 0.0, 1.0, 0.0, 0.0, 265 | s, 0.0, c, 0.0, 266 | 0.0, 0.0, 0.0, 1.0 267 | ]); 268 | } 269 | 270 | static rotateZ(r) { 271 | const c = Math.cos(r); 272 | const s = Math.sin(r); 273 | return new Matrix4([ 274 | c, s, 0.0, 0.0, 275 | -s, c, 0.0, 0.0, 276 | 0.0, 0.0, 1.0, 0.0, 277 | 0.0, 0.0, 0.0, 1.0 278 | ]); 279 | } 280 | 281 | static rotateXYZ(x, y, z) { 282 | const cx = Math.cos(x); 283 | const sx = Math.sin(x); 284 | const cy = Math.cos(y); 285 | const sy = Math.sin(y); 286 | const cz = Math.cos(z); 287 | const sz = Math.sin(z); 288 | return new Matrix4([ 289 | cy * cz, cy * sz, -sy, 0.0, 290 | sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, sx * cy, 0.0, 291 | cx * sy * cz + sx * sz, cx * sy * sz - sx * cz, cx * cy, 0.0, 292 | 0.0, 0.0, 0.0, 1.0 293 | ]); 294 | } 295 | 296 | static transpose(m) { 297 | const e = m.elements; 298 | return new Matrix4([ 299 | e[0], e[4], e[8], e[12], 300 | e[1], e[5], e[9], e[13], 301 | e[2], e[6], e[10], e[14], 302 | e[3], e[7], e[11], e[15] 303 | ]); 304 | } 305 | 306 | static inverse(m) { 307 | const d = m.determinant; 308 | if (Math.abs(d) <= 0.0) { 309 | throw new Error('not invertiable'); 310 | } 311 | const invD = 1.0 / d; 312 | const e = m.elements; 313 | 314 | return new Matrix4([ 315 | (e[5] * e[10] * e[15] + e[9] * e[14] * e[7] + e[13] * e[6] * e[11] 316 | - e[13] * e[10] * e[7] - e[9] * e[6] * e[15] - e[5] * e[14] * e[11]) * invD, 317 | -(e[1] * e[10] * e[15] + e[9] * e[14] * e[3] + e[13] * e[2] * e[11] 318 | - e[13] * e[10] * e[3] - e[9] * e[2] * e[15] - e[1] * e[14] * e[11]) * invD, 319 | (e[1] * e[6] * e[15] + e[5] * e[14] * e[3] + e[13] * e[2] * e[7] 320 | - e[13] * e[6] * e[3] - e[5] * e[2] * e[15] - e[1] * e[14] * e[7]) * invD, 321 | -(e[1] * e[6] * e[11] + e[5] * e[10] * e[3] + e[9] * e[2] * e[7] 322 | - e[9] * e[6] * e[3] - e[5] * e[2] * e[11] - e[1] * e[10] * e[7]) * invD, 323 | -(e[4] * e[10] * e[15] + e[8] * e[14] * e[7] + e[12] * e[6] * e[11] 324 | - e[12] * e[10] * e[7] - e[8] * e[6] * e[15] - e[4] * e[14] * e[11]) * invD, 325 | (e[0] * e[10] * e[15] + e[8] * e[14] * e[3] + e[12] * e[2] * e[11] 326 | - e[12] * e[10] * e[3] - e[8] * e[2] * e[15] - e[0] * e[14] * e[11]) * invD, 327 | -(e[0] * e[6] * e[15] + e[4] * e[14] * e[3] + e[12] * e[2] * e[7] 328 | - e[12] * e[6] * e[3] - e[4] * e[2] * e[15] - e[0] * e[14] * e[7]) * invD, 329 | (e[0] * e[6] * e[11] + e[4] * e[10] * e[3] + e[8] * e[2] * e[7] 330 | - e[8] * e[6] * e[3] - e[4] * e[2] * e[11] - e[0] * e[10] * e[7]) * invD, 331 | (e[4] * e[9] * e[15] + e[8] * e[13] * e[7] + e[12] * e[5] * e[11] 332 | - e[12] * e[9] * e[7] - e[8] * e[5] * e[15] - e[4] * e[13] * e[11]) * invD, 333 | -(e[0] * e[9] * e[15] + e[8] * e[13] * e[3] + e[12] * e[1] * e[11] 334 | - e[12] * e[9] * e[3] - e[8] * e[1] * e[15] - e[0] * e[13] * e[11]) * invD, 335 | (e[0] * e[5] * e[15] + e[4] * e[13] * e[3] + e[12] * e[1] * e[7] 336 | - e[12] * e[5] * e[3] - e[4] * e[1] * e[15] - e[0] * e[13] * e[7]) * invD, 337 | -(e[0] * e[5] * e[11] + e[4] * e[9] * e[3] + e[8] * e[1] * e[7] 338 | - e[8] * e[5] * e[3] - e[4] * e[1] * e[11] - e[0] * e[9] * e[7]) * invD, 339 | -(e[4] * e[9] * e[14] + e[8] * e[13] * e[6] + e[12] * e[5] * e[10] 340 | - e[12] * e[9] * e[6] - e[8] * e[5] * e[14] - e[4] * e[13] * e[10]) * invD, 341 | (e[0] * e[9] * e[14] + e[8] * e[13] * e[2] + e[12] * e[1] * e[10] 342 | - e[12] * e[9] * e[2] - e[8] * e[1] * e[14] - e[0] * e[13] * e[10]) * invD, 343 | -(e[0] * e[5] * e[14] + e[4] * e[13] * e[2] + e[12] * e[1] * e[6] 344 | - e[12] * e[5] * e[2] - e[4] * e[1] * e[14] - e[0] * e[13] * e[6]) * invD, 345 | (e[0] * e[5] * e[10] + e[4] * e[9] * e[2] + e[8] * e[1] * e[6] 346 | - e[8] * e[5] * e[2] - e[4] * e[1] * e[10] - e[0] * e[9] * e[6]) * invD 347 | ]); 348 | } 349 | 350 | static mul(m1, m2) { 351 | const e1 = m1.elements; 352 | const e2 = m2.elements; 353 | return new Matrix4([ 354 | e1[0] * e2[0] + e1[1] * e2[4] + e1[2] * e2[8] + e1[3] * e2[12], 355 | e1[0] * e2[1] + e1[1] * e2[5] + e1[2] * e2[9] + e1[3] * e2[13], 356 | e1[0] * e2[2] + e1[1] * e2[6] + e1[2] * e2[10] + e1[3] * e2[14], 357 | e1[0] * e2[3] + e1[1] * e2[7] + e1[2] * e2[11] + e1[3] * e2[15], 358 | 359 | e1[4] * e2[0] + e1[5] * e2[4] + e1[6] * e2[8] + e1[7] * e2[12], 360 | e1[4] * e2[1] + e1[5] * e2[5] + e1[6] * e2[9] + e1[7] * e2[13], 361 | e1[4] * e2[2] + e1[5] * e2[6] + e1[6] * e2[10] + e1[7] * e2[14], 362 | e1[4] * e2[3] + e1[5] * e2[7] + e1[6] * e2[11] + e1[7] * e2[15], 363 | 364 | e1[8] * e2[0] + e1[9] * e2[4] + e1[10] * e2[8] + e1[11] * e2[12], 365 | e1[8] * e2[1] + e1[9] * e2[5] + e1[10] * e2[9] + e1[11] * e2[13], 366 | e1[8] * e2[2] + e1[9] * e2[6] + e1[10] * e2[10] + e1[11] * e2[14], 367 | e1[8] * e2[3] + e1[9] * e2[7] + e1[10] * e2[11] + e1[11] * e2[15], 368 | 369 | e1[12] * e2[0] + e1[13] * e2[4] + e1[14] * e2[8] + e1[15] * e2[12], 370 | e1[12] * e2[1] + e1[13] * e2[5] + e1[14] * e2[9] + e1[15] * e2[13], 371 | e1[12] * e2[2] + e1[13] * e2[6] + e1[14] * e2[10] + e1[15] * e2[14], 372 | e1[12] * e2[3] + e1[13] * e2[7] + e1[14] * e2[11] + e1[15] * e2[15], 373 | ]); 374 | } 375 | 376 | static perspective(aspect, vfov, near, far) { 377 | const theta = vfov * Math.PI / 180.0; 378 | const t = near * Math.tan(theta * 0.5); 379 | const r = aspect * t; 380 | const fpn = far + near; 381 | const fmn = far - near; 382 | 383 | return new Matrix4([ 384 | near / r, 0.0, 0.0, 0.0, 385 | 0.0, near / t, 0.0, 0.0, 386 | 0.0, 0.0, -fpn / fmn, -1.0, 387 | 0.0, 0.0, -2.0 * far * near / fmn, 0.0 388 | ]); 389 | } 390 | 391 | static orthographic(width, height, near, far) { 392 | const invFmn = 1.0 / (far - near); 393 | return new Matrix4([ 394 | 2.0 / width, 0.0, 0.0, 0.0, 395 | 0.0, 2.0 / height, 0.0, 0.0, 396 | 0.0, 0.0, -2.0 * invFmn, 0.0, 397 | 0.0, 0.0, -(far + near) * invFmn, 1.0 398 | ]); 399 | } 400 | 401 | static lookAt(origin , target, up) { 402 | const front = Vector3.sub(target, origin).norm(); 403 | const z = Vector3.mul(front, -1); 404 | const x = Vector3.cross(up, z); 405 | const y = Vector3.cross(z, x); 406 | 407 | return new Matrix4([ 408 | x.x, x.y, x.z, 0.0, 409 | y.x, y.y, y.z, 0.0, 410 | z.x, z.y, z.z, 0.0, 411 | origin.x, origin.y, origin.z, 1.0 412 | ]); 413 | } 414 | } -------------------------------------------------------------------------------- /lib/dat.gui.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dat-gui JavaScript Controller Library 3 | * http://code.google.com/p/dat-gui 4 | * 5 | * Copyright 2011 Data Arts Team, Google Creative Lab 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | */ 13 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.dat={})}(this,function(e){"use strict";function t(e,t){var n=e.__state.conversionName.toString(),o=Math.round(e.r),i=Math.round(e.g),r=Math.round(e.b),s=e.a,a=Math.round(e.h),l=e.s.toFixed(1),d=e.v.toFixed(1);if(t||"THREE_CHAR_HEX"===n||"SIX_CHAR_HEX"===n){for(var c=e.hex.toString(16);c.length<6;)c="0"+c;return"#"+c}return"CSS_RGB"===n?"rgb("+o+","+i+","+r+")":"CSS_RGBA"===n?"rgba("+o+","+i+","+r+","+s+")":"HEX"===n?"0x"+e.hex.toString(16):"RGB_ARRAY"===n?"["+o+","+i+","+r+"]":"RGBA_ARRAY"===n?"["+o+","+i+","+r+","+s+"]":"RGB_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+"}":"RGBA_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+",a:"+s+"}":"HSV_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+"}":"HSVA_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+",a:"+s+"}":"unknown format"}function n(e,t,n){Object.defineProperty(e,t,{get:function(){return"RGB"===this.__state.space?this.__state[t]:(I.recalculateRGB(this,t,n),this.__state[t])},set:function(e){"RGB"!==this.__state.space&&(I.recalculateRGB(this,t,n),this.__state.space="RGB"),this.__state[t]=e}})}function o(e,t){Object.defineProperty(e,t,{get:function(){return"HSV"===this.__state.space?this.__state[t]:(I.recalculateHSV(this),this.__state[t])},set:function(e){"HSV"!==this.__state.space&&(I.recalculateHSV(this),this.__state.space="HSV"),this.__state[t]=e}})}function i(e){if("0"===e||S.isUndefined(e))return 0;var t=e.match(U);return S.isNull(t)?0:parseFloat(t[1])}function r(e){var t=e.toString();return t.indexOf(".")>-1?t.length-t.indexOf(".")-1:0}function s(e,t){var n=Math.pow(10,t);return Math.round(e*n)/n}function a(e,t,n,o,i){return o+(e-t)/(n-t)*(i-o)}function l(e,t,n,o){e.style.background="",S.each(ee,function(i){e.style.cssText+="background: "+i+"linear-gradient("+t+", "+n+" 0%, "+o+" 100%); "})}function d(e){e.style.background="",e.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);",e.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}function c(e,t,n){var o=document.createElement("li");return t&&o.appendChild(t),n?e.__ul.insertBefore(o,n):e.__ul.appendChild(o),e.onResize(),o}function u(e){X.unbind(window,"resize",e.__resizeHandler),e.saveToLocalStorageIfPossible&&X.unbind(window,"unload",e.saveToLocalStorageIfPossible)}function _(e,t){var n=e.__preset_select[e.__preset_select.selectedIndex];n.innerHTML=t?n.value+"*":n.value}function h(e,t,n){if(n.__li=t,n.__gui=e,S.extend(n,{options:function(t){if(arguments.length>1){var o=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:o,factoryArgs:[S.toArray(arguments)]})}if(S.isArray(t)||S.isObject(t)){var i=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:i,factoryArgs:[t]})}},name:function(e){return n.__li.firstElementChild.firstElementChild.innerHTML=e,n},listen:function(){return n.__gui.listen(n),n},remove:function(){return n.__gui.remove(n),n}}),n instanceof q){var o=new Q(n.object,n.property,{min:n.__min,max:n.__max,step:n.__step});S.each(["updateDisplay","onChange","onFinishChange","step","min","max"],function(e){var t=n[e],i=o[e];n[e]=o[e]=function(){var e=Array.prototype.slice.call(arguments);return i.apply(o,e),t.apply(n,e)}}),X.addClass(t,"has-slider"),n.domElement.insertBefore(o.domElement,n.domElement.firstElementChild)}else if(n instanceof Q){var i=function(t){if(S.isNumber(n.__min)&&S.isNumber(n.__max)){var o=n.__li.firstElementChild.firstElementChild.innerHTML,i=n.__gui.__listening.indexOf(n)>-1;n.remove();var r=f(e,n.object,n.property,{before:n.__li.nextElementSibling,factoryArgs:[n.__min,n.__max,n.__step]});return r.name(o),i&&r.listen(),r}return t};n.min=S.compose(i,n.min),n.max=S.compose(i,n.max)}else n instanceof K?(X.bind(t,"click",function(){X.fakeEvent(n.__checkbox,"click")}),X.bind(n.__checkbox,"click",function(e){e.stopPropagation()})):n instanceof Z?(X.bind(t,"click",function(){X.fakeEvent(n.__button,"click")}),X.bind(t,"mouseover",function(){X.addClass(n.__button,"hover")}),X.bind(t,"mouseout",function(){X.removeClass(n.__button,"hover")})):n instanceof $&&(X.addClass(t,"color"),n.updateDisplay=S.compose(function(e){return t.style.borderLeftColor=n.__color.toString(),e},n.updateDisplay),n.updateDisplay());n.setValue=S.compose(function(t){return e.getRoot().__preset_select&&n.isModified()&&_(e.getRoot(),!0),t},n.setValue)}function p(e,t){var n=e.getRoot(),o=n.__rememberedObjects.indexOf(t.object);if(-1!==o){var i=n.__rememberedObjectIndecesToControllers[o];if(void 0===i&&(i={},n.__rememberedObjectIndecesToControllers[o]=i),i[t.property]=t,n.load&&n.load.remembered){var r=n.load.remembered,s=void 0;if(r[e.preset])s=r[e.preset];else{if(!r[se])return;s=r[se]}if(s[o]&&void 0!==s[o][t.property]){var a=s[o][t.property];t.initialValue=a,t.setValue(a)}}}}function f(e,t,n,o){if(void 0===t[n])throw new Error('Object "'+t+'" has no property "'+n+'"');var i=void 0;if(o.color)i=new $(t,n);else{var r=[t,n].concat(o.factoryArgs);i=ne.apply(e,r)}o.before instanceof z&&(o.before=o.before.__li),p(e,i),X.addClass(i.domElement,"c");var s=document.createElement("span");X.addClass(s,"property-name"),s.innerHTML=i.property;var a=document.createElement("div");a.appendChild(s),a.appendChild(i.domElement);var l=c(e,a,o.before);return X.addClass(l,he.CLASS_CONTROLLER_ROW),i instanceof $?X.addClass(l,"color"):X.addClass(l,H(i.getValue())),h(e,l,i),e.__controllers.push(i),i}function m(e,t){return document.location.href+"."+t}function g(e,t,n){var o=document.createElement("option");o.innerHTML=t,o.value=t,e.__preset_select.appendChild(o),n&&(e.__preset_select.selectedIndex=e.__preset_select.length-1)}function b(e,t){t.style.display=e.useLocalStorage?"block":"none"}function v(e){var t=e.__save_row=document.createElement("li");X.addClass(e.domElement,"has-save"),e.__ul.insertBefore(t,e.__ul.firstChild),X.addClass(t,"save-row");var n=document.createElement("span");n.innerHTML=" ",X.addClass(n,"button gears");var o=document.createElement("span");o.innerHTML="Save",X.addClass(o,"button"),X.addClass(o,"save");var i=document.createElement("span");i.innerHTML="New",X.addClass(i,"button"),X.addClass(i,"save-as");var r=document.createElement("span");r.innerHTML="Revert",X.addClass(r,"button"),X.addClass(r,"revert");var s=e.__preset_select=document.createElement("select");if(e.load&&e.load.remembered?S.each(e.load.remembered,function(t,n){g(e,n,n===e.preset)}):g(e,se,!1),X.bind(s,"change",function(){for(var t=0;t=0;n--)t=[e[n].apply(this,t)];return t[0]}},each:function(e,t,n){if(e)if(A&&e.forEach&&e.forEach===A)e.forEach(t,n);else if(e.length===e.length+0){var o=void 0,i=void 0;for(o=0,i=e.length;o1?S.toArray(arguments):arguments[0];return S.each(O,function(t){if(t.litmus(e))return S.each(t.conversions,function(t,n){if(T=t.read(e),!1===L&&!1!==T)return L=T,T.conversionName=n,T.conversion=t,S.BREAK}),S.BREAK}),L},B=void 0,N={hsv_to_rgb:function(e,t,n){var o=Math.floor(e/60)%6,i=e/60-Math.floor(e/60),r=n*(1-t),s=n*(1-i*t),a=n*(1-(1-i)*t),l=[[n,a,r],[s,n,r],[r,n,a],[r,s,n],[a,r,n],[n,r,s]][o];return{r:255*l[0],g:255*l[1],b:255*l[2]}},rgb_to_hsv:function(e,t,n){var o=Math.min(e,t,n),i=Math.max(e,t,n),r=i-o,s=void 0,a=void 0;return 0===i?{h:NaN,s:0,v:0}:(a=r/i,s=e===i?(t-n)/r:t===i?2+(n-e)/r:4+(e-t)/r,(s/=6)<0&&(s+=1),{h:360*s,s:a,v:i/255})},rgb_to_hex:function(e,t,n){var o=this.hex_with_component(0,2,e);return o=this.hex_with_component(o,1,t),o=this.hex_with_component(o,0,n)},component_from_hex:function(e,t){return e>>8*t&255},hex_with_component:function(e,t,n){return n<<(B=8*t)|e&~(255<this.__max&&(n=this.__max),void 0!==this.__step&&n%this.__step!=0&&(n=Math.round(n/this.__step)*this.__step),j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"setValue",this).call(this,n)}},{key:"min",value:function(e){return this.__min=e,this}},{key:"max",value:function(e){return this.__max=e,this}},{key:"step",value:function(e){return this.__step=e,this.__impliedStep=e,this.__precision=r(e),this}}]),t}(),Q=function(e){function t(e,n,o){function i(){l.__onFinishChange&&l.__onFinishChange.call(l,l.getValue())}function r(e){var t=d-e.clientY;l.setValue(l.getValue()+t*l.__impliedStep),d=e.clientY}function s(){X.unbind(window,"mousemove",r),X.unbind(window,"mouseup",s),i()}F(this,t);var a=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,o));a.__truncationSuspended=!1;var l=a,d=void 0;return a.__input=document.createElement("input"),a.__input.setAttribute("type","text"),X.bind(a.__input,"change",function(){var e=parseFloat(l.__input.value);S.isNaN(e)||l.setValue(e)}),X.bind(a.__input,"blur",function(){i()}),X.bind(a.__input,"mousedown",function(e){X.bind(window,"mousemove",r),X.bind(window,"mouseup",s),d=e.clientY}),X.bind(a.__input,"keydown",function(e){13===e.keyCode&&(l.__truncationSuspended=!0,this.blur(),l.__truncationSuspended=!1,i())}),a.updateDisplay(),a.domElement.appendChild(a.__input),a}return D(t,W),P(t,[{key:"updateDisplay",value:function(){return this.__input.value=this.__truncationSuspended?this.getValue():s(this.getValue(),this.__precision),j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),q=function(e){function t(e,n,o,i,r){function s(e){e.preventDefault();var t=_.__background.getBoundingClientRect();return _.setValue(a(e.clientX,t.left,t.right,_.__min,_.__max)),!1}function l(){X.unbind(window,"mousemove",s),X.unbind(window,"mouseup",l),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}function d(e){var t=e.touches[0].clientX,n=_.__background.getBoundingClientRect();_.setValue(a(t,n.left,n.right,_.__min,_.__max))}function c(){X.unbind(window,"touchmove",d),X.unbind(window,"touchend",c),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}F(this,t);var u=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,{min:o,max:i,step:r})),_=u;return u.__background=document.createElement("div"),u.__foreground=document.createElement("div"),X.bind(u.__background,"mousedown",function(e){document.activeElement.blur(),X.bind(window,"mousemove",s),X.bind(window,"mouseup",l),s(e)}),X.bind(u.__background,"touchstart",function(e){1===e.touches.length&&(X.bind(window,"touchmove",d),X.bind(window,"touchend",c),d(e))}),X.addClass(u.__background,"slider"),X.addClass(u.__foreground,"slider-fg"),u.updateDisplay(),u.__background.appendChild(u.__foreground),u.domElement.appendChild(u.__background),u}return D(t,W),P(t,[{key:"updateDisplay",value:function(){var e=(this.getValue()-this.__min)/(this.__max-this.__min);return this.__foreground.style.width=100*e+"%",j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),Z=function(e){function t(e,n,o){F(this,t);var i=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n)),r=i;return i.__button=document.createElement("div"),i.__button.innerHTML=void 0===o?"Fire":o,X.bind(i.__button,"click",function(e){return e.preventDefault(),r.fire(),!1}),X.addClass(i.__button,"button"),i.domElement.appendChild(i.__button),i}return D(t,z),P(t,[{key:"fire",value:function(){this.__onChange&&this.__onChange.call(this),this.getValue().call(this.object),this.__onFinishChange&&this.__onFinishChange.call(this,this.getValue())}}]),t}(),$=function(e){function t(e,n){function o(e){u(e),X.bind(window,"mousemove",u),X.bind(window,"touchmove",u),X.bind(window,"mouseup",r),X.bind(window,"touchend",r)}function i(e){_(e),X.bind(window,"mousemove",_),X.bind(window,"touchmove",_),X.bind(window,"mouseup",s),X.bind(window,"touchend",s)}function r(){X.unbind(window,"mousemove",u),X.unbind(window,"touchmove",u),X.unbind(window,"mouseup",r),X.unbind(window,"touchend",r),c()}function s(){X.unbind(window,"mousemove",_),X.unbind(window,"touchmove",_),X.unbind(window,"mouseup",s),X.unbind(window,"touchend",s),c()}function a(){var e=R(this.value);!1!==e?(p.__color.__state=e,p.setValue(p.__color.toOriginal())):this.value=p.__color.toString()}function c(){p.__onFinishChange&&p.__onFinishChange.call(p,p.__color.toOriginal())}function u(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__saturation_field.getBoundingClientRect(),n=e.touches&&e.touches[0]||e,o=n.clientX,i=n.clientY,r=(o-t.left)/(t.right-t.left),s=1-(i-t.top)/(t.bottom-t.top);return s>1?s=1:s<0&&(s=0),r>1?r=1:r<0&&(r=0),p.__color.v=s,p.__color.s=r,p.setValue(p.__color.toOriginal()),!1}function _(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__hue_field.getBoundingClientRect(),n=1-((e.touches&&e.touches[0]||e).clientY-t.top)/(t.bottom-t.top);return n>1?n=1:n<0&&(n=0),p.__color.h=360*n,p.setValue(p.__color.toOriginal()),!1}F(this,t);var h=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n));h.__color=new I(h.getValue()),h.__temp=new I(0);var p=h;h.domElement=document.createElement("div"),X.makeSelectable(h.domElement,!1),h.__selector=document.createElement("div"),h.__selector.className="selector",h.__saturation_field=document.createElement("div"),h.__saturation_field.className="saturation-field",h.__field_knob=document.createElement("div"),h.__field_knob.className="field-knob",h.__field_knob_border="2px solid ",h.__hue_knob=document.createElement("div"),h.__hue_knob.className="hue-knob",h.__hue_field=document.createElement("div"),h.__hue_field.className="hue-field",h.__input=document.createElement("input"),h.__input.type="text",h.__input_textShadow="0 1px 1px ",X.bind(h.__input,"keydown",function(e){13===e.keyCode&&a.call(this)}),X.bind(h.__input,"blur",a),X.bind(h.__selector,"mousedown",function(){X.addClass(this,"drag").bind(window,"mouseup",function(){X.removeClass(p.__selector,"drag")})}),X.bind(h.__selector,"touchstart",function(){X.addClass(this,"drag").bind(window,"touchend",function(){X.removeClass(p.__selector,"drag")})});var f=document.createElement("div");return S.extend(h.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"}),S.extend(h.__field_knob.style,{position:"absolute",width:"12px",height:"12px",border:h.__field_knob_border+(h.__color.v<.5?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1}),S.extend(h.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1}),S.extend(h.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"}),S.extend(f.style,{width:"100%",height:"100%",background:"none"}),l(f,"top","rgba(0,0,0,0)","#000"),S.extend(h.__hue_field.style,{width:"15px",height:"100px",border:"1px solid #555",cursor:"ns-resize",position:"absolute",top:"3px",right:"3px"}),d(h.__hue_field),S.extend(h.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:h.__input_textShadow+"rgba(0,0,0,0.7)"}),X.bind(h.__saturation_field,"mousedown",o),X.bind(h.__saturation_field,"touchstart",o),X.bind(h.__field_knob,"mousedown",o),X.bind(h.__field_knob,"touchstart",o),X.bind(h.__hue_field,"mousedown",i),X.bind(h.__hue_field,"touchstart",i),h.__saturation_field.appendChild(f),h.__selector.appendChild(h.__field_knob),h.__selector.appendChild(h.__saturation_field),h.__selector.appendChild(h.__hue_field),h.__hue_field.appendChild(h.__hue_knob),h.domElement.appendChild(h.__input),h.domElement.appendChild(h.__selector),h.updateDisplay(),h}return D(t,z),P(t,[{key:"updateDisplay",value:function(){var e=R(this.getValue());if(!1!==e){var t=!1;S.each(I.COMPONENTS,function(n){if(!S.isUndefined(e[n])&&!S.isUndefined(this.__color.__state[n])&&e[n]!==this.__color.__state[n])return t=!0,{}},this),t&&S.extend(this.__color.__state,e)}S.extend(this.__temp.__state,this.__color.__state),this.__temp.a=1;var n=this.__color.v<.5||this.__color.s>.5?255:0,o=255-n;S.extend(this.__field_knob.style,{marginLeft:100*this.__color.s-7+"px",marginTop:100*(1-this.__color.v)-7+"px",backgroundColor:this.__temp.toHexString(),border:this.__field_knob_border+"rgb("+n+","+n+","+n+")"}),this.__hue_knob.style.marginTop=100*(1-this.__color.h/360)+"px",this.__temp.s=1,this.__temp.v=1,l(this.__saturation_field,"left","#fff",this.__temp.toHexString()),this.__input.value=this.__color.toString(),S.extend(this.__input.style,{backgroundColor:this.__color.toHexString(),color:"rgb("+n+","+n+","+n+")",textShadow:this.__input_textShadow+"rgba("+o+","+o+","+o+",.7)"})}}]),t}(),ee=["-moz-","-o-","-webkit-","-ms-",""],te={load:function(e,t){var n=t||document,o=n.createElement("link");o.type="text/css",o.rel="stylesheet",o.href=e,n.getElementsByTagName("head")[0].appendChild(o)},inject:function(e,t){var n=t||document,o=document.createElement("style");o.type="text/css",o.innerHTML=e;var i=n.getElementsByTagName("head")[0];try{i.appendChild(o)}catch(e){}}},ne=function(e,t){var n=e[t];return S.isArray(arguments[2])||S.isObject(arguments[2])?new Y(e,t,arguments[2]):S.isNumber(n)?S.isNumber(arguments[2])&&S.isNumber(arguments[3])?S.isNumber(arguments[4])?new q(e,t,arguments[2],arguments[3],arguments[4]):new q(e,t,arguments[2],arguments[3]):S.isNumber(arguments[4])?new Q(e,t,{min:arguments[2],max:arguments[3],step:arguments[4]}):new Q(e,t,{min:arguments[2],max:arguments[3]}):S.isString(n)?new J(e,t):S.isFunction(n)?new Z(e,t,""):S.isBoolean(n)?new K(e,t):null},oe=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){setTimeout(e,1e3/60)},ie=function(){function e(){F(this,e),this.backgroundElement=document.createElement("div"),S.extend(this.backgroundElement.style,{backgroundColor:"rgba(0,0,0,0.8)",top:0,left:0,display:"none",zIndex:"1000",opacity:0,WebkitTransition:"opacity 0.2s linear",transition:"opacity 0.2s linear"}),X.makeFullscreen(this.backgroundElement),this.backgroundElement.style.position="fixed",this.domElement=document.createElement("div"),S.extend(this.domElement.style,{position:"fixed",display:"none",zIndex:"1001",opacity:0,WebkitTransition:"-webkit-transform 0.2s ease-out, opacity 0.2s linear",transition:"transform 0.2s ease-out, opacity 0.2s linear"}),document.body.appendChild(this.backgroundElement),document.body.appendChild(this.domElement);var t=this;X.bind(this.backgroundElement,"click",function(){t.hide()})}return P(e,[{key:"show",value:function(){var e=this;this.backgroundElement.style.display="block",this.domElement.style.display="block",this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)",this.layout(),S.defer(function(){e.backgroundElement.style.opacity=1,e.domElement.style.opacity=1,e.domElement.style.webkitTransform="scale(1)"})}},{key:"hide",value:function(){var e=this,t=function t(){e.domElement.style.display="none",e.backgroundElement.style.display="none",X.unbind(e.domElement,"webkitTransitionEnd",t),X.unbind(e.domElement,"transitionend",t),X.unbind(e.domElement,"oTransitionEnd",t)};X.bind(this.domElement,"webkitTransitionEnd",t),X.bind(this.domElement,"transitionend",t),X.bind(this.domElement,"oTransitionEnd",t),this.backgroundElement.style.opacity=0,this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)"}},{key:"layout",value:function(){this.domElement.style.left=window.innerWidth/2-X.getWidth(this.domElement)/2+"px",this.domElement.style.top=window.innerHeight/2-X.getHeight(this.domElement)/2+"px"}}]),e}(),re=function(e){if(e&&"undefined"!=typeof window){var t=document.createElement("style");return t.setAttribute("type","text/css"),t.innerHTML=e,document.head.appendChild(t),e}}(".dg ul{list-style:none;margin:0;padding:0;width:100%;clear:both}.dg.ac{position:fixed;top:0;left:0;right:0;height:0;z-index:0}.dg:not(.ac) .main{overflow:hidden}.dg.main{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear}.dg.main.taller-than-window{overflow-y:auto}.dg.main.taller-than-window .close-button{opacity:1;margin-top:-1px;border-top:1px solid #2c2c2c}.dg.main ul.closed .close-button{opacity:1 !important}.dg.main:hover .close-button,.dg.main .close-button.drag{opacity:1}.dg.main .close-button{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear;border:0;line-height:19px;height:20px;cursor:pointer;text-align:center;background-color:#000}.dg.main .close-button.close-top{position:relative}.dg.main .close-button.close-bottom{position:absolute}.dg.main .close-button:hover{background-color:#111}.dg.a{float:right;margin-right:15px;overflow-y:visible}.dg.a.has-save>ul.close-top{margin-top:0}.dg.a.has-save>ul.close-bottom{margin-top:27px}.dg.a.has-save>ul.closed{margin-top:0}.dg.a .save-row{top:0;z-index:1002}.dg.a .save-row.close-top{position:relative}.dg.a .save-row.close-bottom{position:fixed}.dg li{-webkit-transition:height .1s ease-out;-o-transition:height .1s ease-out;-moz-transition:height .1s ease-out;transition:height .1s ease-out;-webkit-transition:overflow .1s linear;-o-transition:overflow .1s linear;-moz-transition:overflow .1s linear;transition:overflow .1s linear}.dg li:not(.folder){cursor:auto;height:27px;line-height:27px;padding:0 4px 0 5px}.dg li.folder{padding:0;border-left:4px solid rgba(0,0,0,0)}.dg li.title{cursor:pointer;margin-left:-4px}.dg .closed li:not(.title),.dg .closed ul li,.dg .closed ul li>*{height:0;overflow:hidden;border:0}.dg .cr{clear:both;padding-left:3px;height:27px;overflow:hidden}.dg .property-name{cursor:default;float:left;clear:left;width:40%;overflow:hidden;text-overflow:ellipsis}.dg .c{float:left;width:60%;position:relative}.dg .c input[type=text]{border:0;margin-top:4px;padding:3px;width:100%;float:right}.dg .has-slider input[type=text]{width:30%;margin-left:0}.dg .slider{float:left;width:66%;margin-left:-5px;margin-right:0;height:19px;margin-top:4px}.dg .slider-fg{height:100%}.dg .c input[type=checkbox]{margin-top:7px}.dg .c select{margin-top:5px}.dg .cr.function,.dg .cr.function .property-name,.dg .cr.function *,.dg .cr.boolean,.dg .cr.boolean *{cursor:pointer}.dg .cr.color{overflow:visible}.dg .selector{display:none;position:absolute;margin-left:-9px;margin-top:23px;z-index:10}.dg .c:hover .selector,.dg .selector.drag{display:block}.dg li.save-row{padding:0}.dg li.save-row .button{display:inline-block;padding:0px 6px}.dg.dialogue{background-color:#222;width:460px;padding:15px;font-size:13px;line-height:15px}#dg-new-constructor{padding:10px;color:#222;font-family:Monaco, monospace;font-size:10px;border:0;resize:none;box-shadow:inset 1px 1px 1px #888;word-wrap:break-word;margin:12px 0;display:block;width:440px;overflow-y:scroll;height:100px;position:relative}#dg-local-explain{display:none;font-size:11px;line-height:17px;border-radius:3px;background-color:#333;padding:8px;margin-top:10px}#dg-local-explain code{font-size:10px}#dat-gui-save-locally{display:none}.dg{color:#eee;font:11px 'Lucida Grande', sans-serif;text-shadow:0 -1px 0 #111}.dg.main::-webkit-scrollbar{width:5px;background:#1a1a1a}.dg.main::-webkit-scrollbar-corner{height:0;display:none}.dg.main::-webkit-scrollbar-thumb{border-radius:5px;background:#676767}.dg li:not(.folder){background:#1a1a1a;border-bottom:1px solid #2c2c2c}.dg li.save-row{line-height:25px;background:#dad5cb;border:0}.dg li.save-row select{margin-left:5px;width:108px}.dg li.save-row .button{margin-left:5px;margin-top:1px;border-radius:2px;font-size:9px;line-height:7px;padding:4px 4px 5px 4px;background:#c5bdad;color:#fff;text-shadow:0 1px 0 #b0a58f;box-shadow:0 -1px 0 #b0a58f;cursor:pointer}.dg li.save-row .button.gears{background:#c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;height:7px;width:8px}.dg li.save-row .button:hover{background-color:#bab19e;box-shadow:0 -1px 0 #b0a58f}.dg li.folder{border-bottom:0}.dg li.title{padding-left:16px;background:#000 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.2)}.dg .closed li.title{background-image:url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==)}.dg .cr.boolean{border-left:3px solid #806787}.dg .cr.color{border-left:3px solid}.dg .cr.function{border-left:3px solid #e61d5f}.dg .cr.number{border-left:3px solid #2FA1D6}.dg .cr.number input[type=text]{color:#2FA1D6}.dg .cr.string{border-left:3px solid #1ed36f}.dg .cr.string input[type=text]{color:#1ed36f}.dg .cr.function:hover,.dg .cr.boolean:hover{background:#111}.dg .c input[type=text]{background:#303030;outline:none}.dg .c input[type=text]:hover{background:#3c3c3c}.dg .c input[type=text]:focus{background:#494949;color:#fff}.dg .c .slider{background:#303030;cursor:ew-resize}.dg .c .slider-fg{background:#2FA1D6;max-width:100%}.dg .c .slider:hover{background:#3c3c3c}.dg .c .slider:hover .slider-fg{background:#44abda}\n");te.inject(re);var se="Default",ae=function(){try{return!!window.localStorage}catch(e){return!1}}(),le=void 0,de=!0,ce=void 0,ue=!1,_e=[],he=function e(t){var n=this,o=t||{};this.domElement=document.createElement("div"),this.__ul=document.createElement("ul"),this.domElement.appendChild(this.__ul),X.addClass(this.domElement,"dg"),this.__folders={},this.__controllers=[],this.__rememberedObjects=[],this.__rememberedObjectIndecesToControllers=[],this.__listening=[],o=S.defaults(o,{closeOnTop:!1,autoPlace:!0,width:e.DEFAULT_WIDTH}),o=S.defaults(o,{resizable:o.autoPlace,hideable:o.autoPlace}),S.isUndefined(o.load)?o.load={preset:se}:o.preset&&(o.load.preset=o.preset),S.isUndefined(o.parent)&&o.hideable&&_e.push(this),o.resizable=S.isUndefined(o.parent)&&o.resizable,o.autoPlace&&S.isUndefined(o.scrollable)&&(o.scrollable=!0);var i=ae&&"true"===localStorage.getItem(m(this,"isLocal")),r=void 0,s=void 0;if(Object.defineProperties(this,{parent:{get:function(){return o.parent}},scrollable:{get:function(){return o.scrollable}},autoPlace:{get:function(){return o.autoPlace}},closeOnTop:{get:function(){return o.closeOnTop}},preset:{get:function(){return n.parent?n.getRoot().preset:o.load.preset},set:function(e){n.parent?n.getRoot().preset=e:o.load.preset=e,E(this),n.revert()}},width:{get:function(){return o.width},set:function(e){o.width=e,w(n,e)}},name:{get:function(){return o.name},set:function(e){o.name=e,s&&(s.innerHTML=o.name)}},closed:{get:function(){return o.closed},set:function(t){o.closed=t,o.closed?X.addClass(n.__ul,e.CLASS_CLOSED):X.removeClass(n.__ul,e.CLASS_CLOSED),this.onResize(),n.__closeButton&&(n.__closeButton.innerHTML=t?e.TEXT_OPEN:e.TEXT_CLOSED)}},load:{get:function(){return o.load}},useLocalStorage:{get:function(){return i},set:function(e){ae&&(i=e,e?X.bind(window,"unload",r):X.unbind(window,"unload",r),localStorage.setItem(m(n,"isLocal"),e))}}}),S.isUndefined(o.parent)){if(this.closed=o.closed||!1,X.addClass(this.domElement,e.CLASS_MAIN),X.makeSelectable(this.domElement,!1),ae&&i){n.useLocalStorage=!0;var a=localStorage.getItem(m(this,"gui"));a&&(o.load=JSON.parse(a))}this.__closeButton=document.createElement("div"),this.__closeButton.innerHTML=e.TEXT_CLOSED,X.addClass(this.__closeButton,e.CLASS_CLOSE_BUTTON),o.closeOnTop?(X.addClass(this.__closeButton,e.CLASS_CLOSE_TOP),this.domElement.insertBefore(this.__closeButton,this.domElement.childNodes[0])):(X.addClass(this.__closeButton,e.CLASS_CLOSE_BOTTOM),this.domElement.appendChild(this.__closeButton)),X.bind(this.__closeButton,"click",function(){n.closed=!n.closed})}else{void 0===o.closed&&(o.closed=!0);var l=document.createTextNode(o.name);X.addClass(l,"controller-name"),s=c(n,l);X.addClass(this.__ul,e.CLASS_CLOSED),X.addClass(s,"title"),X.bind(s,"click",function(e){return e.preventDefault(),n.closed=!n.closed,!1}),o.closed||(this.closed=!1)}o.autoPlace&&(S.isUndefined(o.parent)&&(de&&(ce=document.createElement("div"),X.addClass(ce,"dg"),X.addClass(ce,e.CLASS_AUTO_PLACE_CONTAINER),document.body.appendChild(ce),de=!1),ce.appendChild(this.domElement),X.addClass(this.domElement,e.CLASS_AUTO_PLACE)),this.parent||w(n,o.width)),this.__resizeHandler=function(){n.onResizeDebounced()},X.bind(window,"resize",this.__resizeHandler),X.bind(this.__ul,"webkitTransitionEnd",this.__resizeHandler),X.bind(this.__ul,"transitionend",this.__resizeHandler),X.bind(this.__ul,"oTransitionEnd",this.__resizeHandler),this.onResize(),o.resizable&&y(this),r=function(){ae&&"true"===localStorage.getItem(m(n,"isLocal"))&&localStorage.setItem(m(n,"gui"),JSON.stringify(n.getSaveObject()))},this.saveToLocalStorageIfPossible=r,o.parent||function(){var e=n.getRoot();e.width+=1,S.defer(function(){e.width-=1})}()};he.toggleHide=function(){ue=!ue,S.each(_e,function(e){e.domElement.style.display=ue?"none":""})},he.CLASS_AUTO_PLACE="a",he.CLASS_AUTO_PLACE_CONTAINER="ac",he.CLASS_MAIN="main",he.CLASS_CONTROLLER_ROW="cr",he.CLASS_TOO_TALL="taller-than-window",he.CLASS_CLOSED="closed",he.CLASS_CLOSE_BUTTON="close-button",he.CLASS_CLOSE_TOP="close-top",he.CLASS_CLOSE_BOTTOM="close-bottom",he.CLASS_DRAG="drag",he.DEFAULT_WIDTH=245,he.TEXT_CLOSED="Close Controls",he.TEXT_OPEN="Open Controls",he._keydownHandler=function(e){"text"===document.activeElement.type||72!==e.which&&72!==e.keyCode||he.toggleHide()},X.bind(window,"keydown",he._keydownHandler,!1),S.extend(he.prototype,{add:function(e,t){return f(this,e,t,{factoryArgs:Array.prototype.slice.call(arguments,2)})},addColor:function(e,t){return f(this,e,t,{color:!0})},remove:function(e){this.__ul.removeChild(e.__li),this.__controllers.splice(this.__controllers.indexOf(e),1);var t=this;S.defer(function(){t.onResize()})},destroy:function(){if(this.parent)throw new Error("Only the root GUI should be removed with .destroy(). For subfolders, use gui.removeFolder(folder) instead.");this.autoPlace&&ce.removeChild(this.domElement);var e=this;S.each(this.__folders,function(t){e.removeFolder(t)}),X.unbind(window,"keydown",he._keydownHandler,!1),u(this)},addFolder:function(e){if(void 0!==this.__folders[e])throw new Error('You already have a folder in this GUI by the name "'+e+'"');var t={name:e,parent:this};t.autoPlace=this.autoPlace,this.load&&this.load.folders&&this.load.folders[e]&&(t.closed=this.load.folders[e].closed,t.load=this.load.folders[e]);var n=new he(t);this.__folders[e]=n;var o=c(this,n.domElement);return X.addClass(o,"folder"),n},removeFolder:function(e){this.__ul.removeChild(e.domElement.parentElement),delete this.__folders[e.name],this.load&&this.load.folders&&this.load.folders[e.name]&&delete this.load.folders[e.name],u(e);var t=this;S.each(e.__folders,function(t){e.removeFolder(t)}),S.defer(function(){t.onResize()})},open:function(){this.closed=!1},close:function(){this.closed=!0},onResize:function(){var e=this.getRoot();if(e.scrollable){var t=X.getOffset(e.__ul).top,n=0;S.each(e.__ul.childNodes,function(t){e.autoPlace&&t===e.__save_row||(n+=X.getHeight(t))}),window.innerHeight-t-20GUI\'s constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n\n
\n\n
\n\n'),this.parent)throw new Error("You can only call remember on a top level GUI.");var e=this;S.each(Array.prototype.slice.call(arguments),function(t){0===e.__rememberedObjects.length&&v(e),-1===e.__rememberedObjects.indexOf(t)&&e.__rememberedObjects.push(t)}),this.autoPlace&&w(this,this.width)},getRoot:function(){for(var e=this;e.parent;)e=e.parent;return e},getSaveObject:function(){var e=this.load;return e.closed=this.closed,this.__rememberedObjects.length>0&&(e.preset=this.preset,e.remembered||(e.remembered={}),e.remembered[this.preset]=x(this)),e.folders={},S.each(this.__folders,function(t,n){e.folders[n]=t.getSaveObject()}),e},save:function(){this.load.remembered||(this.load.remembered={}),this.load.remembered[this.preset]=x(this),_(this,!1),this.saveToLocalStorageIfPossible()},saveAs:function(e){this.load.remembered||(this.load.remembered={},this.load.remembered[se]=x(this,!0)),this.load.remembered[e]=x(this),this.preset=e,g(this,e,!0),this.saveToLocalStorageIfPossible()},revert:function(e){S.each(this.__controllers,function(t){this.getRoot().load.remembered?p(e||this.getRoot(),t):t.setValue(t.initialValue),t.__onFinishChange&&t.__onFinishChange.call(t,t.getValue())},this),S.each(this.__folders,function(e){e.revert(e)}),e||_(this.getRoot(),!1)},listen:function(e){var t=0===this.__listening.length;this.__listening.push(e),t&&C(this.__listening)},updateDisplay:function(){S.each(this.__controllers,function(e){e.updateDisplay()}),S.each(this.__folders,function(e){e.updateDisplay()})}});var pe={Color:I,math:N,interpret:R},fe={Controller:z,BooleanController:K,OptionController:Y,StringController:J,NumberController:W,NumberControllerBox:Q,NumberControllerSlider:q,FunctionController:Z,ColorController:$},me={dom:X},ge={GUI:he},be=he,ve={color:pe,controllers:fe,dom:me,gui:ge,GUI:be};e.color=pe,e.controllers=fe,e.dom=me,e.gui=ge,e.GUI=be,e.default=ve,Object.defineProperty(e,"__esModule",{value:!0})}); -------------------------------------------------------------------------------- /lib/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | (function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof define&&define.amd?define(e):f.Stats=e()})(this,function(){var f=function(){function e(a){c.appendChild(a.dom);return a}function u(a){for(var d=0;dg+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/ 4 | 1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v); 5 | b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f}); --------------------------------------------------------------------------------