├── shaders ├── mark.frag ├── spheredepth.frag ├── grid.frag ├── point.frag ├── boxwireframe.frag ├── copy.frag ├── background.frag ├── background.vert ├── fullscreen.vert ├── particle.frag ├── sphere.frag ├── grid.vert ├── point.vert ├── boxwireframe.vert ├── box.vert ├── box.frag ├── spheredepth.vert ├── particle.vert ├── mark.vert ├── normalizegrid.frag ├── sphereao.vert ├── enforceboundaries.frag ├── sphere.vert ├── transfertogrid.vert ├── subtract.frag ├── addforce.frag ├── jacobi.frag ├── sphereao.frag ├── extendvelocity.frag ├── divergence.frag ├── fxaa.frag ├── transfertogrid.frag ├── transfertoparticles.frag ├── advect.frag ├── common.frag └── composite.frag ├── README.md ├── LICENSE ├── slider.js ├── flip.css ├── simulatorrenderer.js ├── camera.js ├── index.html ├── utilities.js ├── fluidparticles.js ├── renderer.js ├── simulator.js └── boxeditor.js /shaders/mark.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | void main () { 4 | gl_FragColor = vec4(1.0); 5 | } 6 | -------------------------------------------------------------------------------- /shaders/spheredepth.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | void main () { 4 | gl_FragColor = vec4(1.0); 5 | } 6 | -------------------------------------------------------------------------------- /shaders/grid.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | void main () { 4 | gl_FragColor = vec4(0.8, 0.8, 0.8, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /shaders/point.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | void main () { 4 | gl_FragColor = vec4(vec3(0.6), 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /shaders/boxwireframe.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | void main () { 4 | gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /shaders/copy.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform sampler2D u_texture; 4 | 5 | varying vec2 v_coordinates; 6 | 7 | void main () { 8 | gl_FragColor = texture2D(u_texture, v_coordinates); 9 | } 10 | -------------------------------------------------------------------------------- /shaders/background.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_position; 4 | 5 | void main () { 6 | vec3 backgroundColor = vec3(1.0) - length(v_position) * 0.1; 7 | gl_FragColor = vec4(backgroundColor, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /shaders/background.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec2 a_position; 4 | 5 | varying vec2 v_position; 6 | 7 | void main () { 8 | v_position = a_position; 9 | gl_Position = vec4(a_position, 0.0, 1.0); 10 | } 11 | -------------------------------------------------------------------------------- /shaders/fullscreen.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec2 a_position; 4 | 5 | varying vec2 v_coordinates; 6 | 7 | void main () { 8 | v_coordinates = a_position * 0.5 + 0.5; 9 | 10 | gl_Position = vec4(a_position, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /shaders/particle.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec3 v_velocity; 4 | 5 | void main () { 6 | gl_FragColor = vec4(v_velocity * 0.5 + 0.5, 1.0); 7 | 8 | gl_FragColor = vec4(mix(vec3(0.0, 0.2, 0.9), vec3(1.0, 0.3, 0.2), length(v_velocity) * 0.1), 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /shaders/sphere.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec3 v_viewSpacePosition; 4 | varying vec3 v_viewSpaceNormal; 5 | varying float v_speed; 6 | 7 | void main () { 8 | gl_FragColor = vec4(v_viewSpaceNormal.x, v_viewSpaceNormal.y, v_speed, v_viewSpacePosition.z); 9 | } 10 | -------------------------------------------------------------------------------- /shaders/grid.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_vertexPosition; 4 | 5 | uniform vec3 u_translation; 6 | 7 | uniform mat4 u_viewMatrix; 8 | uniform mat4 u_projectionMatrix; 9 | 10 | void main () { 11 | gl_Position = u_projectionMatrix * u_viewMatrix * vec4(u_translation + a_vertexPosition, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /shaders/point.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_position; 4 | 5 | uniform vec3 u_position; 6 | 7 | uniform mat3 u_rotation; 8 | 9 | uniform mat4 u_viewMatrix; 10 | uniform mat4 u_projectionMatrix; 11 | 12 | void main () { 13 | gl_Position = u_projectionMatrix * u_viewMatrix * vec4(u_position + u_rotation * a_position * 0.2, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /shaders/boxwireframe.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_cubeVertexPosition; 4 | 5 | uniform vec3 u_translation; 6 | uniform vec3 u_scale; 7 | 8 | uniform mat4 u_viewMatrix; 9 | uniform mat4 u_projectionMatrix; 10 | 11 | void main () { 12 | gl_Position = u_projectionMatrix * u_viewMatrix * vec4(a_cubeVertexPosition * u_scale + u_translation, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ####Fluid Particles 2 | 3 | Real-time particle-based 3D fluid simulation and rendering in WebGL. 4 | 5 | ![](http://david.li/images/fluidgithub.png) 6 | 7 | [http://david.li/fluid](http://david.li/fluid) ([video](http://www.youtube.com/watch?v=DhNt_A3k4B4)) 8 | 9 | Fluid simulation is a GPU implementation of the PIC/FLIP method (with various additions). Particle rendering uses spherical ambient occlusion volumes. 10 | -------------------------------------------------------------------------------- /shaders/box.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_cubeVertexPosition; 4 | 5 | uniform vec3 u_translation; 6 | uniform vec3 u_scale; 7 | 8 | uniform mat4 u_viewMatrix; 9 | uniform mat4 u_projectionMatrix; 10 | 11 | varying vec3 v_cubePosition; 12 | 13 | void main () { 14 | v_cubePosition = a_cubeVertexPosition; 15 | 16 | gl_Position = u_projectionMatrix * u_viewMatrix * vec4(a_cubeVertexPosition * u_scale + u_translation, 1.0); 17 | } 18 | -------------------------------------------------------------------------------- /shaders/box.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec3 v_cubePosition; 4 | 5 | uniform vec3 u_highlightSide; 6 | uniform vec3 u_highlightColor; 7 | 8 | void main () { 9 | float epsilon = 0.001; 10 | vec3 normalizedCubePosition = v_cubePosition * 2.0 - 1.0; 11 | 12 | if (abs(normalizedCubePosition.x - u_highlightSide.x) < epsilon || abs(normalizedCubePosition.y - u_highlightSide.y) < epsilon || abs(normalizedCubePosition.z - u_highlightSide.z) < epsilon ) { 13 | gl_FragColor = vec4(u_highlightColor, 1.0); 14 | } else { 15 | gl_FragColor = vec4(vec3(0.97), 1.0); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shaders/spheredepth.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_vertexPosition; 4 | attribute vec3 a_vertexNormal; 5 | 6 | attribute vec2 a_textureCoordinates; 7 | 8 | uniform mat4 u_projectionViewMatrix; 9 | 10 | uniform sampler2D u_positionsTexture; 11 | uniform sampler2D u_velocitiesTexture; 12 | 13 | uniform float u_sphereRadius; 14 | 15 | void main () { 16 | vec3 spherePosition = texture2D(u_positionsTexture, a_textureCoordinates).rgb; 17 | 18 | vec3 position = a_vertexPosition * u_sphereRadius + spherePosition; 19 | 20 | gl_Position = u_projectionViewMatrix * vec4(position, 1.0); 21 | } 22 | -------------------------------------------------------------------------------- /shaders/particle.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec2 a_textureCoordinates; //the texture coordinates that this particle's info is stored at 4 | 5 | uniform sampler2D u_positionTexture; 6 | uniform sampler2D u_velocityTexture; 7 | 8 | uniform vec2 u_resolution; 9 | 10 | varying vec3 v_velocity; 11 | 12 | uniform mat4 u_projectionMatrix; 13 | uniform mat4 u_viewMatrix; 14 | 15 | void main () { 16 | vec3 position = texture2D(u_positionTexture, a_textureCoordinates).rgb; 17 | vec3 velocity = texture2D(u_velocityTexture, a_textureCoordinates).rgb; 18 | v_velocity = velocity; 19 | 20 | gl_PointSize = 3.0; 21 | 22 | gl_Position = u_projectionMatrix * u_viewMatrix * vec4(position, 1.0); 23 | } 24 | -------------------------------------------------------------------------------- /shaders/mark.vert: -------------------------------------------------------------------------------- 1 | //marks pixels with 1.0 if there's a particle there 2 | 3 | precision highp float; 4 | 5 | attribute vec2 a_textureCoordinates; 6 | 7 | uniform sampler2D u_positionTexture; 8 | 9 | uniform vec3 u_gridResolution; 10 | uniform vec3 u_gridSize; 11 | 12 | void main () { 13 | gl_PointSize = 1.0; 14 | 15 | vec3 position = texture2D(u_positionTexture, a_textureCoordinates).rgb; 16 | position = (position / u_gridSize) * u_gridResolution; 17 | vec3 cellIndex = floor(position); 18 | 19 | vec2 textureCoordinates = vec2( 20 | cellIndex.z * u_gridResolution.x + cellIndex.x + 0.5, 21 | cellIndex.y + 0.5) / vec2(u_gridResolution.x * u_gridResolution.z, u_gridResolution.y); 22 | 23 | gl_Position = vec4(textureCoordinates * 2.0 - 1.0, 0.0, 1.0); 24 | } 25 | -------------------------------------------------------------------------------- /shaders/normalizegrid.frag: -------------------------------------------------------------------------------- 1 | //this does the divide in the weighted sum 2 | 3 | precision highp float; 4 | 5 | varying vec2 v_coordinates; 6 | 7 | uniform sampler2D u_accumulatedVelocityTexture; 8 | uniform sampler2D u_weightTexture; 9 | 10 | void main () { 11 | vec3 accumulatedVelocity = texture2D(u_accumulatedVelocityTexture, v_coordinates).rgb; 12 | vec3 weight = texture2D(u_weightTexture, v_coordinates).rgb; 13 | 14 | float xVelocity = 0.0; 15 | if (weight.x > 0.0) { 16 | xVelocity = accumulatedVelocity.x / weight.x; 17 | } 18 | 19 | float yVelocity = 0.0; 20 | if (weight.y > 0.0) { 21 | yVelocity = accumulatedVelocity.y / weight.y; 22 | } 23 | 24 | float zVelocity = 0.0; 25 | if (weight.z > 0.0) { 26 | zVelocity = accumulatedVelocity.z / weight.z; 27 | } 28 | 29 | gl_FragColor = vec4(xVelocity, yVelocity, zVelocity, 0.0); 30 | } 31 | -------------------------------------------------------------------------------- /shaders/sphereao.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_vertexPosition; 4 | 5 | attribute vec2 a_textureCoordinates; 6 | 7 | uniform mat4 u_projectionMatrix; 8 | uniform mat4 u_viewMatrix; 9 | 10 | uniform sampler2D u_positionsTexture; 11 | uniform sampler2D u_velocitiesTexture; 12 | 13 | uniform float u_sphereRadius; 14 | 15 | varying vec3 v_viewSpaceSpherePosition; 16 | varying float v_sphereRadius; 17 | varying float v_extrudedSphereRadius; 18 | 19 | void main () { 20 | vec3 spherePosition = texture2D(u_positionsTexture, a_textureCoordinates).rgb; 21 | v_viewSpaceSpherePosition = vec3(u_viewMatrix * vec4(spherePosition, 1.0)); 22 | 23 | v_sphereRadius = u_sphereRadius; 24 | v_extrudedSphereRadius = v_sphereRadius * 5.0; 25 | 26 | vec3 position = a_vertexPosition * v_extrudedSphereRadius + spherePosition; 27 | 28 | gl_Position = u_projectionMatrix * u_viewMatrix * vec4(position, 1.0); 29 | } 30 | -------------------------------------------------------------------------------- /shaders/enforceboundaries.frag: -------------------------------------------------------------------------------- 1 | //sets the velocities at the boundary cells 2 | 3 | precision highp float; 4 | 5 | varying vec2 v_coordinates; 6 | 7 | uniform sampler2D u_velocityTexture; 8 | 9 | uniform vec3 u_gridResolution; 10 | 11 | void main () { 12 | vec3 velocity = texture2D(u_velocityTexture, v_coordinates).rgb; 13 | vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0)); 14 | 15 | if (cellIndex.x < 0.5) { 16 | velocity.x = 0.0; 17 | } 18 | 19 | if (cellIndex.x > u_gridResolution.x - 0.5) { 20 | velocity.x = 0.0; 21 | } 22 | 23 | if (cellIndex.y < 0.5) { 24 | velocity.y = 0.0; 25 | } 26 | 27 | if (cellIndex.y > u_gridResolution.y - 0.5) { 28 | velocity.y = min(velocity.y, 0.0); 29 | } 30 | 31 | if (cellIndex.z < 0.5) { 32 | velocity.z = 0.0; 33 | } 34 | 35 | if (cellIndex.z > u_gridResolution.z - 0.5) { 36 | velocity.z = 0.0; 37 | } 38 | 39 | gl_FragColor = vec4(velocity, 0.0); 40 | } 41 | -------------------------------------------------------------------------------- /shaders/sphere.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 a_vertexPosition; 4 | attribute vec3 a_vertexNormal; 5 | 6 | attribute vec2 a_textureCoordinates; 7 | 8 | uniform mat4 u_projectionMatrix; 9 | uniform mat4 u_viewMatrix; 10 | 11 | uniform sampler2D u_positionsTexture; 12 | uniform sampler2D u_velocitiesTexture; 13 | 14 | uniform float u_sphereRadius; 15 | 16 | varying vec3 v_viewSpacePosition; 17 | varying vec3 v_viewSpaceNormal; 18 | varying float v_speed; 19 | 20 | void main () { 21 | vec3 spherePosition = texture2D(u_positionsTexture, a_textureCoordinates).rgb; 22 | 23 | vec3 position = a_vertexPosition * u_sphereRadius + spherePosition; 24 | 25 | v_viewSpacePosition = vec3(u_viewMatrix * vec4(position, 1.0)); 26 | v_viewSpaceNormal = vec3(u_viewMatrix * vec4(a_vertexNormal, 0.0)); //this assumes we're not doing any weird stuff in the view matrix 27 | 28 | gl_Position = u_projectionMatrix * vec4(v_viewSpacePosition, 1.0); 29 | 30 | vec3 velocity = texture2D(u_velocitiesTexture, a_textureCoordinates).rgb; 31 | v_speed = length(velocity); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Li (http://david.li) 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 | -------------------------------------------------------------------------------- /shaders/transfertogrid.vert: -------------------------------------------------------------------------------- 1 | //transfers particle velocities to the grid by splatting them using additive blending 2 | 3 | precision highp float; 4 | 5 | attribute vec2 a_textureCoordinates; 6 | 7 | uniform sampler2D u_positionTexture; 8 | uniform sampler2D u_velocityTexture; 9 | 10 | uniform vec3 u_gridSize; 11 | uniform vec3 u_gridResolution; 12 | 13 | varying vec3 v_position; 14 | varying vec3 v_velocity; 15 | 16 | uniform float u_zOffset; //the offset for the z layer we're splatting into 17 | varying float v_zIndex; //the z layer we're splatting into 18 | 19 | void main () { 20 | gl_PointSize = 5.0; //TODO: i can probably compute this more accurately 21 | 22 | vec3 position = texture2D(u_positionTexture, a_textureCoordinates).rgb; 23 | position = (position / u_gridSize) * u_gridResolution; 24 | 25 | vec3 velocity = texture2D(u_velocityTexture, a_textureCoordinates).rgb; 26 | v_velocity = velocity; 27 | v_position = position; 28 | 29 | vec3 cellIndex = vec3(floor(position.xyz)); 30 | v_zIndex = cellIndex.z + u_zOffset; //offset into the right layer 31 | 32 | vec2 textureCoordinates = vec2( 33 | v_zIndex * (u_gridResolution.x + 1.0) + cellIndex.x + 0.5, 34 | cellIndex.y + 0.5) / vec2((u_gridResolution.x + 1.0) * (u_gridResolution.z + 1.0), u_gridResolution.y + 1.0); 35 | 36 | gl_Position = vec4(textureCoordinates * 2.0 - 1.0, 0.0, 1.0); 37 | } 38 | -------------------------------------------------------------------------------- /shaders/subtract.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform vec3 u_gridResolution; 6 | 7 | uniform sampler2D u_pressureTexture; 8 | uniform sampler2D u_velocityTexture; 9 | uniform sampler2D u_markerTexture; 10 | 11 | void main () { 12 | vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0)); 13 | 14 | float left = texture3DNearest(u_pressureTexture, (cellIndex + vec3(-1.0, 0.0, 0.0) + 0.5) / u_gridResolution, u_gridResolution).r; 15 | float right = texture3DNearest(u_pressureTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).r; 16 | 17 | float bottom = texture3DNearest(u_pressureTexture, (cellIndex + vec3(0.0, -1.0, 0.0) + 0.5) / u_gridResolution, u_gridResolution).r; 18 | float top = texture3DNearest(u_pressureTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).r; 19 | 20 | float back = texture3DNearest(u_pressureTexture, (cellIndex + vec3(0.0, 0.0, -1.0) + 0.5) / u_gridResolution, u_gridResolution).r; 21 | float front = texture3DNearest(u_pressureTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).r; 22 | 23 | 24 | //compute gradient of pressure 25 | vec3 gradient = vec3(right - left, top - bottom, front - back) / 1.0; 26 | 27 | vec3 currentVelocity = texture2D(u_velocityTexture, v_coordinates).rgb; 28 | 29 | vec3 newVelocity = currentVelocity - gradient; 30 | 31 | gl_FragColor = vec4(newVelocity, 0.0); 32 | } 33 | -------------------------------------------------------------------------------- /shaders/addforce.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform sampler2D u_velocityTexture; 6 | 7 | uniform vec3 u_mouseVelocity; 8 | 9 | uniform vec3 u_gridResolution; 10 | uniform vec3 u_gridSize; 11 | 12 | uniform vec3 u_mouseRayOrigin; 13 | uniform vec3 u_mouseRayDirection; 14 | 15 | uniform float u_timeStep; 16 | 17 | float kernel (vec3 position, float radius) { 18 | vec3 worldPosition = (position / u_gridResolution) * u_gridSize; 19 | 20 | float distanceToMouseRay = length(cross(u_mouseRayDirection, worldPosition - u_mouseRayOrigin)); 21 | 22 | float normalizedDistance = max(0.0, distanceToMouseRay / radius); 23 | return smoothstep(1.0, 0.9, normalizedDistance); 24 | } 25 | 26 | void main () { 27 | vec3 velocity = texture2D(u_velocityTexture, v_coordinates).rgb; 28 | 29 | vec3 newVelocity = velocity + vec3(0.0, -40.0 * u_timeStep, 0.0); //add gravity 30 | 31 | vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0)); 32 | vec3 xPosition = vec3(cellIndex.x, cellIndex.y + 0.5, cellIndex.z + 0.5); 33 | vec3 yPosition = vec3(cellIndex.x + 0.5, cellIndex.y, cellIndex.z + 0.5); 34 | vec3 zPosition = vec3(cellIndex.x + 0.5, cellIndex.y + 0.5, cellIndex.z); 35 | 36 | float mouseRadius = 5.0; 37 | vec3 kernelValues = vec3(kernel(xPosition, mouseRadius), kernel(yPosition, mouseRadius), kernel(zPosition, mouseRadius)); 38 | 39 | newVelocity += u_mouseVelocity * kernelValues * 3.0 * smoothstep(0.0, 1.0 / 200.0, u_timeStep); 40 | 41 | gl_FragColor = vec4(newVelocity * 1.0, 0.0); 42 | } 43 | -------------------------------------------------------------------------------- /shaders/jacobi.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform vec3 u_gridResolution; 6 | 7 | uniform sampler2D u_pressureTexture; 8 | uniform sampler2D u_divergenceTexture; 9 | uniform sampler2D u_markerTexture; 10 | 11 | void main () { 12 | vec3 centerCoords = get3DFragCoord(u_gridResolution) / u_gridResolution; 13 | 14 | //pressure = 0 in air cells 15 | float fluidCell = texture3DNearest(u_markerTexture, centerCoords, u_gridResolution).x; 16 | if (fluidCell == 0.0) discard; //if this is an air cell 17 | 18 | vec3 delta = 1.0 / u_gridResolution; 19 | 20 | float divergenceCenter = texture3DNearest(u_divergenceTexture, centerCoords, u_gridResolution).r; 21 | 22 | float left = texture3DNearest(u_pressureTexture, centerCoords + vec3(-delta.x, 0.0, 0.0), u_gridResolution).r; 23 | float right = texture3DNearest(u_pressureTexture, centerCoords + vec3(delta.x, 0.0, 0.0), u_gridResolution).r; 24 | float bottom = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, -delta.y, 0.0), u_gridResolution).r; 25 | float top = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, delta.y, 0.0), u_gridResolution).r; 26 | float back = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, 0.0, -delta.z), u_gridResolution).r; 27 | float front = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, 0.0, delta.z), u_gridResolution).r; 28 | 29 | float newPressure = (left + right + bottom + top + back + front - divergenceCenter) / 6.0; 30 | 31 | 32 | gl_FragColor = vec4(newPressure, 0.0, 0.0, 0.0); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /shaders/sphereao.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform sampler2D u_renderingTexture; 4 | 5 | varying vec3 v_viewSpaceSpherePosition; 6 | varying float v_sphereRadius; 7 | varying float v_extrudedSphereRadius; 8 | 9 | uniform vec2 u_resolution; 10 | uniform float u_fov; 11 | 12 | const float PI = 3.14159265; 13 | 14 | void main () { 15 | vec2 coordinates = gl_FragCoord.xy / u_resolution; 16 | vec4 data = texture2D(u_renderingTexture, coordinates); 17 | 18 | //reconstruct position 19 | 20 | vec3 viewSpaceNormal = vec3(data.x, data.y, sqrt(1.0 - data.x * data.x - data.y * data.y)); 21 | 22 | float tanHalfFOV = tan(u_fov / 2.0); 23 | float viewSpaceZ = data.a; 24 | vec3 viewRay = vec3( 25 | (coordinates.x * 2.0 - 1.0) * tanHalfFOV * u_resolution.x / u_resolution.y, 26 | (coordinates.y * 2.0 - 1.0) * tanHalfFOV, 27 | -1.0); 28 | 29 | vec3 viewSpacePosition = viewRay * -viewSpaceZ; 30 | 31 | 32 | vec3 di = v_viewSpaceSpherePosition - viewSpacePosition; 33 | float l = length(di); 34 | 35 | float nl = dot(viewSpaceNormal, di / l); 36 | float h = l / v_sphereRadius; 37 | float h2 = h * h; 38 | float k2 = 1.0 - h2 * nl * nl; 39 | 40 | float result = max(0.0, nl) / h2; 41 | 42 | if (k2 > 0.0 && l > v_sphereRadius) { 43 | result = nl * acos(-nl * sqrt((h2 - 1.0) / (1.0 - nl * nl))) - sqrt(k2 * (h2 - 1.0)); 44 | result = result / h2 + atan(sqrt(k2 / (h2 - 1.0))); 45 | result /= PI; 46 | 47 | //result = pow( clamp(0.5*(nl*h+1.0)/h2,0.0,1.0), 1.5 ); //cheap approximation 48 | } 49 | 50 | gl_FragColor = vec4(result, 0.0, 0.0, 1.0); 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /shaders/extendvelocity.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform vec2 u_gridResolution; 6 | 7 | uniform sampler2D u_velocityTexture; 8 | uniform sampler2D u_weightTexture; 9 | 10 | void main () { 11 | vec2 velocity = texture2D(u_velocityTexture, v_coordinates).rg; 12 | 13 | vec2 delta = 1.0 / (u_gridResolution + 1.0); 14 | 15 | bool airX = texture2D(u_weightTexture, v_coordinates).x == 0.0; 16 | bool airY = texture2D(u_weightTexture, v_coordinates).y == 0.0; 17 | 18 | float closestXDistance = 100000.0; 19 | float closestYDistance = 100000.0; 20 | 21 | if (airX || airY) { 22 | const int SEARCH_WIDTH = 1; 23 | for (int y = -SEARCH_WIDTH; y <= SEARCH_WIDTH; ++y) { 24 | for (int x = -SEARCH_WIDTH; x <= SEARCH_WIDTH; ++x) { 25 | if (x != 0 && y != 0) { 26 | vec2 coordinates = v_coordinates + vec2(float(x), float(y)) * delta; 27 | float dist = float(x) * float(x) + float(y) * float(y); 28 | 29 | if (texture2D(u_weightTexture, coordinates).x > 0.0 && dist < closestXDistance && airX) { 30 | closestXDistance = dist; 31 | velocity.x = texture2D(u_velocityTexture, coordinates).r; 32 | } 33 | 34 | if (texture2D(u_weightTexture, coordinates).y > 0.0 && dist < closestYDistance && airY) { 35 | closestYDistance = dist; 36 | velocity.y = texture2D(u_velocityTexture, coordinates).g; 37 | } 38 | 39 | } 40 | } 41 | } 42 | 43 | } 44 | 45 | gl_FragColor = vec4(velocity, 0.0, 0.0); 46 | } 47 | -------------------------------------------------------------------------------- /shaders/divergence.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform sampler2D u_velocityTexture; 6 | uniform sampler2D u_markerTexture; 7 | uniform sampler2D u_weightTexture; 8 | 9 | uniform vec3 u_gridResolution; 10 | 11 | uniform float u_maxDensity; 12 | 13 | void main () { 14 | vec3 cellIndex = floor(get3DFragCoord(u_gridResolution)); 15 | 16 | //divergence = 0 in air cells 17 | float fluidCell = texture3DNearest(u_markerTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).x; 18 | if (fluidCell == 0.0) discard; 19 | 20 | 21 | float leftX = texture3DNearest(u_velocityTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x; 22 | float rightX = texture3DNearest(u_velocityTexture, (cellIndex + vec3(1.0, 0.0, 0.0) + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x; 23 | 24 | float bottomY = texture3DNearest(u_velocityTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y; 25 | float topY = texture3DNearest(u_velocityTexture, (cellIndex + vec3(0.0, 1.0, 0.0) + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y; 26 | 27 | float backZ = texture3DNearest(u_velocityTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z; 28 | float frontZ = texture3DNearest(u_velocityTexture, (cellIndex + vec3(0.0, 0.0, 1.0) + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z; 29 | 30 | float divergence = ((rightX - leftX) + (topY - bottomY) + (frontZ - backZ)) / 1.0; 31 | 32 | float density = texture3DNearest(u_weightTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).a; 33 | divergence -= max((density - u_maxDensity) * 1.0, 0.0); //volume conservation 34 | 35 | gl_FragColor = vec4(divergence, 0.0, 0.0, 0.0); 36 | } 37 | -------------------------------------------------------------------------------- /slider.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Slider = (function () { 4 | 5 | //changeCallback is called with the new value 6 | var Slider = function (element, initial, min, max, changeCallback) { 7 | this.value = initial; 8 | 9 | this.min = min; 10 | this.max = max; 11 | 12 | this.div = element; 13 | 14 | this.innerDiv = document.createElement('div'); 15 | this.innerDiv.style.position = 'absolute'; 16 | this.innerDiv.style.height = this.div.offsetHeight + 'px'; 17 | 18 | this.div.appendChild(this.innerDiv); 19 | 20 | this.changeCallback = changeCallback; 21 | 22 | this.mousePressed = false; 23 | 24 | this.redraw(); 25 | 26 | this.div.addEventListener('mousedown', (function (event) { 27 | this.mousePressed = true; 28 | this.onChange(event); 29 | }).bind(this)); 30 | 31 | document.addEventListener('mouseup', (function (event) { 32 | this.mousePressed = false; 33 | }).bind(this)); 34 | 35 | document.addEventListener('mousemove', (function (event) { 36 | if (this.mousePressed) { 37 | this.onChange(event); 38 | } 39 | }).bind(this)); 40 | 41 | }; 42 | 43 | Slider.prototype.redraw = function () { 44 | var fraction = (this.value - this.min) / (this.max - this.min); 45 | this.innerDiv.style.width = fraction * this.div.offsetWidth + 'px'; 46 | this.innerDiv.style.height = this.div.offsetHeight + 'px'; 47 | } 48 | 49 | Slider.prototype.onChange = function (event) { 50 | var mouseX = Utilities.getMousePosition(event, this.div).x; 51 | this.value = Utilities.clamp((mouseX / this.div.offsetWidth) * (this.max - this.min) + this.min, this.min, this.max); 52 | 53 | this.redraw(); 54 | 55 | this.changeCallback(this.value); 56 | } 57 | 58 | return Slider; 59 | }()); 60 | -------------------------------------------------------------------------------- /shaders/fxaa.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform sampler2D u_input; 6 | 7 | uniform vec2 u_resolution; 8 | 9 | const float FXAA_SPAN_MAX = 8.0; 10 | const float FXAA_REDUCE_MUL = 1.0 / 8.0; 11 | const float FXAA_REDUCE_MIN = 1.0 / 128.0; 12 | 13 | void main () { 14 | vec2 delta = 1.0 / u_resolution; 15 | 16 | vec3 rgbNW = texture2D(u_input, v_coordinates + vec2(-1.0, -1.0) * delta).rgb; 17 | vec3 rgbNE = texture2D(u_input, v_coordinates + vec2(1.0, -1.0) * delta).rgb; 18 | vec3 rgbSW = texture2D(u_input, v_coordinates + vec2(-1.0, 1.0) * delta).rgb; 19 | vec3 rgbSE = texture2D(u_input, v_coordinates + vec2(1.0, 1.0) * delta).rgb; 20 | vec3 rgbM = texture2D(u_input, v_coordinates).rgb; 21 | 22 | vec3 luma = vec3(0.299, 0.587, 0.114); 23 | float lumaNW = dot(rgbNW, luma); 24 | float lumaNE = dot(rgbNE, luma); 25 | float lumaSW = dot(rgbSW, luma); 26 | float lumaSE = dot(rgbSE, luma); 27 | float lumaM = dot(rgbM, luma); 28 | 29 | float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); 30 | float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); 31 | 32 | vec2 dir = vec2( 33 | -((lumaNW + lumaNE) - (lumaSW + lumaSE)), 34 | ((lumaNW + lumaSW) - (lumaNE + lumaSE))); 35 | 36 | float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); 37 | float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); 38 | dir = min(vec2(FXAA_SPAN_MAX), max(vec2(-FXAA_SPAN_MAX), dir * rcpDirMin)) * delta.xy; 39 | 40 | vec3 rgbA = 0.5 * (texture2D(u_input, v_coordinates.xy + dir * (1.0 / 3.0 - 0.5)).xyz + texture2D(u_input, v_coordinates.xy + dir * (2.0 / 3.0 - 0.5)).xyz); 41 | vec3 rgbB = rgbA * 0.5 + 0.25 * (texture2D(u_input, v_coordinates.xy + dir * -0.5).xyz + texture2D(u_input, v_coordinates.xy + dir * 0.5).xyz); 42 | float lumaB = dot(rgbB, luma); 43 | if (lumaB < lumaMin || lumaB > lumaMax) { 44 | gl_FragColor = vec4(rgbA, 1.0); 45 | } else { 46 | gl_FragColor = vec4(rgbB, 1.0); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /shaders/transfertogrid.frag: -------------------------------------------------------------------------------- 1 | //two modes: 2 | //in one we accumulate (xWeight, yWeight, zWeight, centerWeight) 3 | //in the other we accumulate (xWeight * velocity.x, yWeight * velocity.y, zWeight * velocity.z, 0) 4 | 5 | //needs a division as a second step 6 | 7 | varying vec3 v_position; //already in the grid coordinate system 8 | varying vec3 v_velocity; 9 | 10 | uniform vec3 u_gridResolution; 11 | 12 | varying float v_zIndex; 13 | 14 | uniform int u_accumulate; //when this is 0, we accumulate (xWeight, yWeight, 0, centerWeight), when 1 we accumulate (xWeight * velocity.x, yWeight * velocity.y, 0, 0) 15 | 16 | float h (float r) { 17 | if (r >= 0.0 && r <= 1.0) { 18 | return 1.0 - r; 19 | } else if (r >= -1.0 && r <= 0.0) { 20 | return 1.0 + r; 21 | } else { 22 | return 0.0; 23 | } 24 | } 25 | 26 | float k (vec3 v) { 27 | return h(v.x) * h(v.y) * h(v.z); 28 | } 29 | 30 | void main () { 31 | vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0)); 32 | 33 | if (cellIndex.z == v_zIndex) { //make sure we're in the right slice to prevent bleeding 34 | //staggered grid position and therefor weight is different for x, y, z and scalar values 35 | vec3 xPosition = vec3(cellIndex.x, cellIndex.y + 0.5, cellIndex.z + 0.5); 36 | float xWeight = k(v_position - xPosition); 37 | 38 | vec3 yPosition = vec3(cellIndex.x + 0.5, cellIndex.y, cellIndex.z + 0.5); 39 | float yWeight = k(v_position - yPosition); 40 | 41 | vec3 zPosition = vec3(cellIndex.x + 0.5, cellIndex.y + 0.5, cellIndex.z); 42 | float zWeight = k(v_position - zPosition); 43 | 44 | vec3 scalarPosition = vec3(cellIndex.x + 0.5, cellIndex.y + 0.5, cellIndex.z + 0.5); 45 | float scalarWeight = k(v_position - scalarPosition); 46 | 47 | if (u_accumulate == 0) { 48 | gl_FragColor = vec4(xWeight, yWeight, zWeight, scalarWeight); 49 | } else if (u_accumulate == 1) { 50 | gl_FragColor = vec4(xWeight * v_velocity.x, yWeight * v_velocity.y, zWeight * v_velocity.z, 0.0); 51 | } 52 | 53 | } else { 54 | gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /shaders/transfertoparticles.frag: -------------------------------------------------------------------------------- 1 | //transfers velocities back to the particles 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform sampler2D u_particlePositionTexture; 6 | uniform sampler2D u_particleVelocityTexture; 7 | 8 | uniform sampler2D u_gridVelocityTexture; 9 | uniform sampler2D u_originalGridVelocityTexture; //the grid velocities before the update 10 | 11 | uniform vec3 u_gridResolution; 12 | uniform vec3 u_gridSize; 13 | 14 | uniform float u_flipness; //0 is full PIC, 1 is full FLIP 15 | 16 | float sampleXVelocity (sampler2D texture, vec3 position) { 17 | vec3 cellIndex = vec3(position.x, position.y - 0.5, position.z - 0.5); 18 | return texture3D(texture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x; 19 | } 20 | 21 | float sampleYVelocity (sampler2D texture, vec3 position) { 22 | vec3 cellIndex = vec3(position.x - 0.5, position.y, position.z - 0.5); 23 | return texture3D(texture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y; 24 | } 25 | 26 | float sampleZVelocity (sampler2D texture, vec3 position) { 27 | vec3 cellIndex = vec3(position.x - 0.5, position.y - 0.5, position.z); 28 | return texture3D(texture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z; 29 | } 30 | 31 | vec3 sampleVelocity (sampler2D texture, vec3 position) { 32 | return vec3(sampleXVelocity(texture, position), sampleYVelocity(texture, position), sampleZVelocity(texture, position)); 33 | } 34 | 35 | void main () { 36 | vec3 particlePosition = texture2D(u_particlePositionTexture, v_coordinates).rgb; 37 | particlePosition = (particlePosition / u_gridSize) * u_gridResolution; 38 | 39 | vec3 particleVelocity = texture2D(u_particleVelocityTexture, v_coordinates).rgb; 40 | 41 | vec3 currentVelocity = sampleVelocity(u_gridVelocityTexture, particlePosition); 42 | vec3 originalVelocity = sampleVelocity(u_originalGridVelocityTexture, particlePosition); 43 | 44 | vec3 velocityChange = currentVelocity - originalVelocity; 45 | 46 | vec3 flipVelocity = particleVelocity + velocityChange; 47 | vec3 picVelocity = currentVelocity; 48 | 49 | gl_FragColor = vec4(mix(picVelocity, flipVelocity, u_flipness), 0.0); 50 | } 51 | -------------------------------------------------------------------------------- /shaders/advect.frag: -------------------------------------------------------------------------------- 1 | //advects particle positions with second order runge kutta 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform sampler2D u_positionsTexture; 6 | uniform sampler2D u_randomsTexture; 7 | 8 | uniform sampler2D u_velocityGrid; 9 | 10 | uniform vec3 u_gridResolution; 11 | uniform vec3 u_gridSize; 12 | 13 | uniform float u_timeStep; 14 | 15 | uniform float u_frameNumber; 16 | 17 | uniform vec2 u_particlesResolution; 18 | 19 | float sampleXVelocity (vec3 position) { 20 | vec3 cellIndex = vec3(position.x, position.y - 0.5, position.z - 0.5); 21 | return texture3D(u_velocityGrid, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x; 22 | } 23 | 24 | float sampleYVelocity (vec3 position) { 25 | vec3 cellIndex = vec3(position.x - 0.5, position.y, position.z - 0.5); 26 | return texture3D(u_velocityGrid, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y; 27 | } 28 | 29 | float sampleZVelocity (vec3 position) { 30 | vec3 cellIndex = vec3(position.x - 0.5, position.y - 0.5, position.z); 31 | return texture3D(u_velocityGrid, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z; 32 | } 33 | 34 | vec3 sampleVelocity (vec3 position) { 35 | vec3 gridPosition = (position / u_gridSize) * u_gridResolution; 36 | return vec3(sampleXVelocity(gridPosition), sampleYVelocity(gridPosition), sampleZVelocity(gridPosition)); 37 | } 38 | 39 | void main () { 40 | vec3 position = texture2D(u_positionsTexture, v_coordinates).rgb; 41 | vec3 randomDirection = texture2D(u_randomsTexture, fract(v_coordinates + u_frameNumber / u_particlesResolution)).rgb; 42 | 43 | vec3 velocity = sampleVelocity(position); 44 | 45 | vec3 halfwayPosition = position + velocity * u_timeStep * 0.5; 46 | vec3 halfwayVelocity = sampleVelocity(halfwayPosition); 47 | 48 | vec3 step = halfwayVelocity * u_timeStep; 49 | 50 | step += 0.05 * randomDirection * length(velocity) * u_timeStep; 51 | 52 | //step = clamp(step, -vec3(1.0), vec3(1.0)); //enforce CFL condition 53 | 54 | vec3 newPosition = position + step; 55 | 56 | newPosition = clamp(newPosition, vec3(0.01), u_gridSize - 0.01); 57 | 58 | gl_FragColor = vec4(newPosition, 0.0); 59 | } 60 | -------------------------------------------------------------------------------- /shaders/common.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | vec3 get3DFragCoord (vec3 resolution) { 4 | return vec3( 5 | mod(gl_FragCoord.x, resolution.x), 6 | gl_FragCoord.y, 7 | floor(gl_FragCoord.x / resolution.x) + 0.5); 8 | } 9 | 10 | vec4 texture3D(sampler2D texture, vec3 coordinates, vec3 resolution) { 11 | vec3 fullCoordinates = coordinates * resolution; //in [(0, 0, 0), (resolution.x, resolution.y, resolutionz)] 12 | 13 | fullCoordinates = clamp(fullCoordinates, vec3(0.5), vec3(resolution - 0.5)); 14 | 15 | //belowZIndex and aboveZIndex don't have the 0.5 offset 16 | float belowZIndex = floor(fullCoordinates.z - 0.5); 17 | float aboveZIndex = belowZIndex + 1.0; 18 | 19 | //we interpolate the z 20 | float fraction = fract(fullCoordinates.z - 0.5); 21 | 22 | vec2 belowCoordinates = vec2( 23 | belowZIndex * resolution.x + fullCoordinates.x, 24 | fullCoordinates.y) / vec2(resolution.x * resolution.z, resolution.y); 25 | 26 | vec2 aboveCoordinates = vec2( 27 | aboveZIndex * resolution.x + fullCoordinates.x, 28 | fullCoordinates.y) / vec2(resolution.x * resolution.z, resolution.y); 29 | 30 | return mix(texture2D(texture, belowCoordinates), texture2D(texture, aboveCoordinates), fraction); 31 | } 32 | 33 | vec4 texture3DNearest(sampler2D texture, vec3 coordinates, vec3 resolution) { //clamps the z coordinate 34 | vec3 fullCoordinates = coordinates * resolution; //in [(0, 0, 0), (resolution.x, resolution.y, resolutionz)] 35 | 36 | fullCoordinates = clamp(fullCoordinates, vec3(0.5), vec3(resolution - 0.5)); 37 | 38 | float zIndex = floor(fullCoordinates.z); 39 | 40 | vec2 textureCoordinates = vec2( 41 | zIndex * resolution.x + fullCoordinates.x, 42 | fullCoordinates.y) / vec2(resolution.x * resolution.z, resolution.y); 43 | 44 | return texture2D(texture, textureCoordinates); 45 | } 46 | 47 | /* 48 | vec4 texture3D(sampler2D tex, vec3 texCoord, vec3 resolution) { 49 | float size = resolution.z; 50 | float sliceSize = 1.0 / size; // space of 1 slice 51 | float slicePixelSize = sliceSize / size; // space of 1 pixel 52 | float sliceInnerSize = slicePixelSize * (size - 1.0); // space of size pixels 53 | float zSlice0 = min(floor(texCoord.z * size), size - 1.0); 54 | float zSlice1 = min(zSlice0 + 1.0, size - 1.0); 55 | float xOffset = slicePixelSize * 0.5 + texCoord.x * sliceInnerSize; 56 | float s0 = xOffset + (zSlice0 * sliceSize); 57 | float s1 = xOffset + (zSlice1 * sliceSize); 58 | vec4 slice0Color = texture2D(tex, vec2(s0, texCoord.y)); 59 | vec4 slice1Color = texture2D(tex, vec2(s1, texCoord.y)); 60 | float zOffset = mod(texCoord.z * size, 1.0); 61 | return mix(slice0Color, slice1Color, zOffset); 62 | } 63 | */ 64 | 65 | -------------------------------------------------------------------------------- /shaders/composite.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 v_coordinates; 4 | 5 | uniform sampler2D u_renderingTexture; 6 | uniform sampler2D u_occlusionTexture; 7 | 8 | uniform vec2 u_resolution; 9 | uniform float u_fov; 10 | 11 | uniform mat4 u_inverseViewMatrix; 12 | 13 | uniform sampler2D u_shadowDepthTexture; 14 | uniform vec2 u_shadowResolution; 15 | uniform mat4 u_lightProjectionViewMatrix; 16 | 17 | float linearstep (float left, float right, float x) { 18 | return clamp((x - left) / (right - left), 0.0, 1.0); 19 | } 20 | 21 | vec3 hsvToRGB(vec3 c) { 22 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 23 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 24 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 25 | } 26 | 27 | void main () { 28 | vec4 data = texture2D(u_renderingTexture, v_coordinates); 29 | float occlusion = texture2D(u_occlusionTexture, v_coordinates).r; 30 | 31 | vec3 viewSpaceNormal = vec3(data.x, data.y, sqrt(1.0 - data.x * data.x - data.y * data.y)); 32 | 33 | float viewSpaceZ = data.a; 34 | vec3 viewRay = vec3( 35 | (v_coordinates.x * 2.0 - 1.0) * tan(u_fov / 2.0) * u_resolution.x / u_resolution.y, 36 | (v_coordinates.y * 2.0 - 1.0) * tan(u_fov / 2.0), 37 | -1.0); 38 | 39 | vec3 viewSpacePosition = viewRay * -viewSpaceZ; 40 | vec3 worldSpacePosition = vec3(u_inverseViewMatrix * vec4(viewSpacePosition, 1.0)); 41 | 42 | float speed = data.b; 43 | vec3 color = hsvToRGB(vec3(max(0.6 - speed * 0.0025, 0.52), 0.75, 1.0)); 44 | 45 | 46 | vec4 lightSpacePosition = u_lightProjectionViewMatrix * vec4(worldSpacePosition, 1.0); 47 | lightSpacePosition /= lightSpacePosition.w; 48 | lightSpacePosition *= 0.5; 49 | lightSpacePosition += 0.5; 50 | vec2 lightSpaceCoordinates = lightSpacePosition.xy; 51 | 52 | float shadow = 1.0; 53 | const int PCF_WIDTH = 2; 54 | const float PCF_NORMALIZATION = float(PCF_WIDTH * 2 + 1) * float(PCF_WIDTH * 2 + 1); 55 | 56 | for (int xOffset = -PCF_WIDTH; xOffset <= PCF_WIDTH; ++xOffset) { 57 | for (int yOffset = -PCF_WIDTH; yOffset <= PCF_WIDTH; ++yOffset) { 58 | float shadowSample = texture2D(u_shadowDepthTexture, lightSpaceCoordinates + 5.0 * vec2(float(xOffset), float(yOffset)) / u_shadowResolution).r; 59 | if (lightSpacePosition.z > shadowSample + 0.001) shadow -= 1.0 / PCF_NORMALIZATION; 60 | } 61 | } 62 | 63 | 64 | float ambient = 1.0 - occlusion * 0.7; 65 | float direct = 1.0 - (1.0 - shadow) * 0.8; 66 | 67 | color *= ambient * direct; 68 | 69 | if (speed >= 0.0) { 70 | gl_FragColor = vec4(color, 1.0); 71 | } else { 72 | vec3 backgroundColor = vec3(1.0) - length(v_coordinates * 2.0 - 1.0) * 0.1; 73 | gl_FragColor = vec4(backgroundColor, 1.0); 74 | } 75 | 76 | //gl_FragColor = vec4(texture2D(u_shadowDepthTexture, v_coordinates).rrr, 1.0); 77 | } 78 | -------------------------------------------------------------------------------- /flip.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Asap', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | overflow: hidden; 9 | 10 | user-select: none; 11 | -moz-user-select: none; 12 | -webkit-user-select: none; 13 | -ms-user-select: none; 14 | } 15 | 16 | #start-button { 17 | font-size: 16px; 18 | color: white; 19 | font-weight: bold; 20 | 21 | margin-bottom: 15px; 22 | 23 | width: 120px; 24 | height: 40px; 25 | line-height: 40px; 26 | 27 | text-align: center; 28 | 29 | pointer-events: auto; 30 | } 31 | 32 | .start-button-active { 33 | cursor: default; 34 | background: rgb(70, 190, 60); 35 | } 36 | 37 | .start-button-active:hover { 38 | background: rgb(60, 180, 50); 39 | } 40 | 41 | .start-button-active:active { 42 | background: rgb(50, 170, 40); 43 | } 44 | 45 | .start-button-inactive { 46 | background: #999999; 47 | cursor: default; 48 | } 49 | 50 | #preset-button { 51 | margin-bottom: 10px; 52 | 53 | background: rgba(80, 160, 230, 1.0); 54 | 55 | font-size: 12px; 56 | font-weight: bold; 57 | color: white; 58 | 59 | width: 100px; 60 | height: 30px; 61 | line-height: 30px; 62 | 63 | text-align: center; 64 | 65 | cursor: default; 66 | 67 | pointer-events: auto; 68 | } 69 | 70 | #preset-button:hover { 71 | background: rgba(70, 150, 220, 1.0); 72 | } 73 | 74 | #preset-button:active { 75 | background: rgba(60, 140, 210, 1.0); 76 | } 77 | 78 | #ui { 79 | position: absolute; 80 | 81 | pointer-events: none; 82 | 83 | top: 30px; 84 | left: 30px; 85 | } 86 | 87 | #particle-count { 88 | color: #777777; 89 | 90 | font-size: 12px; 91 | 92 | margin-top: 4px; 93 | } 94 | 95 | .slider-label { 96 | color: #555555; 97 | font-size: 12px; 98 | font-weight: bold; 99 | 100 | margin-top: 5px; 101 | } 102 | 103 | .slider { 104 | margin-top: 4px; 105 | 106 | width: 200px; 107 | height: 22px; 108 | 109 | user-select: none; 110 | -moz-user-select: none; 111 | -webkit-user-select: none; 112 | -ms-user-select: none; 113 | 114 | background: rgba(0, 0, 0, 0.1); 115 | 116 | pointer-events: auto; 117 | } 118 | 119 | .slider:hover { 120 | background: rgba(0, 0, 0, 0.12); 121 | } 122 | 123 | .slider div { 124 | background: #666666; 125 | } 126 | 127 | .slider:hover div { 128 | background: #555555; 129 | } 130 | 131 | .instructions { 132 | position: absolute; 133 | 134 | bottom: 30px; 135 | left: 30px; 136 | 137 | font-size: 12px; 138 | line-height: 16px; 139 | 140 | color: #555555; 141 | 142 | margin-top: 20px; 143 | } 144 | 145 | .instructions span { 146 | font-weight: bold; 147 | 148 | color: #444444; 149 | } 150 | 151 | #footer { 152 | font-size: 12px; 153 | 154 | position: absolute; 155 | right: 20px; 156 | bottom: 20px; 157 | 158 | color: #555555; 159 | } 160 | 161 | #footer a { 162 | text-decoration: underline; 163 | color: #555555; 164 | } 165 | 166 | #container { 167 | color: #333333; 168 | font-size: 14px; 169 | 170 | margin-top: 40px; 171 | margin-left: 40px; 172 | } 173 | 174 | #video { 175 | color: #333333; 176 | font-size: 14px; 177 | 178 | margin-top: 20px; 179 | } 180 | 181 | #linkback { 182 | color: #333333; 183 | 184 | margin-top: 20px; 185 | } 186 | 187 | #linkback a { 188 | color: #333333; 189 | } 190 | -------------------------------------------------------------------------------- /simulatorrenderer.js: -------------------------------------------------------------------------------- 1 | var SimulatorRenderer = (function () { 2 | function SimulatorRenderer (canvas, wgl, projectionMatrix, camera, gridDimensions, onLoaded) { 3 | this.canvas = canvas; 4 | this.wgl = wgl; 5 | this.projectionMatrix = projectionMatrix; 6 | this.camera = camera; 7 | 8 | 9 | wgl.getExtension('OES_texture_float'); 10 | wgl.getExtension('OES_texture_float_linear'); 11 | 12 | var rendererLoaded = false, 13 | simulatorLoaded = false; 14 | 15 | this.renderer = new Renderer(this.canvas, this.wgl, gridDimensions, (function () { 16 | rendererLoaded = true; 17 | if (rendererLoaded && simulatorLoaded) { 18 | start.call(this); 19 | } 20 | }).bind(this)); 21 | 22 | this.simulator = new Simulator(this.wgl, (function () { 23 | simulatorLoaded = true; 24 | if (rendererLoaded && simulatorLoaded) { 25 | start.call(this); 26 | } 27 | }).bind(this)); 28 | 29 | 30 | function start () { 31 | ///////////////////////////////////////////// 32 | // interaction stuff 33 | 34 | //mouse position is in [-1, 1] 35 | this.mouseX = 0; 36 | this.mouseY = 0; 37 | 38 | //the mouse plane is a plane centered at the camera orbit point and orthogonal to the view direction 39 | this.lastMousePlaneX = 0; 40 | this.lastMousePlaneY = 0; 41 | 42 | setTimeout(onLoaded, 1); 43 | } 44 | } 45 | 46 | SimulatorRenderer.prototype.onMouseMove = function (event) { 47 | var position = Utilities.getMousePosition(event, this.canvas); 48 | var normalizedX = position.x / this.canvas.width; 49 | var normalizedY = position.y / this.canvas.height; 50 | 51 | this.mouseX = normalizedX * 2.0 - 1.0; 52 | this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0; 53 | 54 | this.camera.onMouseMove(event); 55 | }; 56 | 57 | SimulatorRenderer.prototype.onMouseDown = function (event) { 58 | this.camera.onMouseDown(event); 59 | }; 60 | 61 | SimulatorRenderer.prototype.onMouseUp = function (event) { 62 | this.camera.onMouseUp(event); 63 | }; 64 | 65 | SimulatorRenderer.prototype.reset = function (particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, particleDensity, sphereRadius) { 66 | this.simulator.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, particleDensity); 67 | this.renderer.reset(particlesWidth, particlesHeight, sphereRadius); 68 | } 69 | 70 | SimulatorRenderer.prototype.update = function (timeStep) { 71 | var fov = 2.0 * Math.atan(1.0 / this.projectionMatrix[5]); 72 | 73 | var viewSpaceMouseRay = [ 74 | this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height), 75 | this.mouseY * Math.tan(fov / 2.0), 76 | -1.0]; 77 | 78 | var mousePlaneX = viewSpaceMouseRay[0] * this.camera.distance; 79 | var mousePlaneY = viewSpaceMouseRay[1] * this.camera.distance; 80 | 81 | var mouseVelocityX = mousePlaneX - this.lastMousePlaneX; 82 | var mouseVelocityY = mousePlaneY - this.lastMousePlaneY; 83 | 84 | if (this.camera.isMouseDown()) { 85 | mouseVelocityX = 0.0; 86 | mouseVelocityY = 0.0; 87 | } 88 | 89 | this.lastMousePlaneX = mousePlaneX; 90 | this.lastMousePlaneY = mousePlaneY; 91 | 92 | var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()); 93 | var worldSpaceMouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix); 94 | Utilities.normalizeVector(worldSpaceMouseRay, worldSpaceMouseRay); 95 | 96 | 97 | var cameraViewMatrix = this.camera.getViewMatrix(); 98 | var cameraRight = [cameraViewMatrix[0], cameraViewMatrix[4], cameraViewMatrix[8]]; 99 | var cameraUp = [cameraViewMatrix[1], cameraViewMatrix[5], cameraViewMatrix[9]]; 100 | 101 | var mouseVelocity = []; 102 | for (var i = 0; i < 3; ++i) { 103 | mouseVelocity[i] = mouseVelocityX * cameraRight[i] + mouseVelocityY * cameraUp[i]; 104 | } 105 | 106 | this.simulator.simulate(timeStep, mouseVelocity, this.camera.getPosition(), worldSpaceMouseRay); 107 | this.renderer.draw(this.simulator, this.projectionMatrix, this.camera.getViewMatrix()); 108 | } 109 | 110 | SimulatorRenderer.prototype.onResize = function (event) { 111 | this.renderer.onResize(event); 112 | } 113 | 114 | return SimulatorRenderer; 115 | }()); 116 | -------------------------------------------------------------------------------- /camera.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Camera = (function () { 4 | var SENSITIVITY = 0.005; 5 | 6 | var MIN_DISTANCE = 25.0; 7 | var MAX_DISTANCE = 60.0; 8 | 9 | function Camera (element, orbitPoint) { 10 | this.element = element; 11 | this.distance = 40.0; 12 | this.orbitPoint = orbitPoint; 13 | 14 | this.azimuth = 0.0, 15 | this.elevation = 0.25 16 | 17 | this.minElevation = -Math.PI / 4; 18 | this.maxElevation = Math.PI / 4; 19 | 20 | this.currentMouseX = 0, 21 | this.currentMouseY = 0; 22 | 23 | this.lastMouseX = 0, 24 | this.lastMouseY = 0; 25 | 26 | this.mouseDown = false; 27 | 28 | this.viewMatrix = new Float32Array(16); 29 | 30 | 31 | this.recomputeViewMatrix(); 32 | 33 | 34 | element.addEventListener('wheel', (function (event) { 35 | var scrollDelta = event.deltaY; 36 | this.distance += ((scrollDelta > 0) ? 1 : -1) * 2.0; 37 | 38 | if (this.distance < MIN_DISTANCE) this.distance = MIN_DISTANCE; 39 | if (this.distance > MAX_DISTANCE) this.distance = MAX_DISTANCE; 40 | 41 | this.recomputeViewMatrix(); 42 | }).bind(this)); 43 | }; 44 | 45 | Camera.prototype.recomputeViewMatrix = function () { 46 | var xRotationMatrix = new Float32Array(16), 47 | yRotationMatrix = new Float32Array(16), 48 | distanceTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)), 49 | orbitTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)); 50 | 51 | Utilities.makeIdentityMatrix(this.viewMatrix); 52 | 53 | Utilities.makeXRotationMatrix(xRotationMatrix, this.elevation); 54 | Utilities.makeYRotationMatrix(yRotationMatrix, this.azimuth); 55 | distanceTranslationMatrix[14] = -this.distance; 56 | orbitTranslationMatrix[12] = -this.orbitPoint[0]; 57 | orbitTranslationMatrix[13] = -this.orbitPoint[1]; 58 | orbitTranslationMatrix[14] = -this.orbitPoint[2]; 59 | 60 | Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, orbitTranslationMatrix); 61 | Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, yRotationMatrix); 62 | Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, xRotationMatrix); 63 | Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, distanceTranslationMatrix); 64 | }; 65 | 66 | Camera.prototype.getPosition = function () { 67 | var position = [ 68 | this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.sin(-this.azimuth) + this.orbitPoint[0], 69 | this.distance * Math.cos(Math.PI / 2 - this.elevation) + this.orbitPoint[1], 70 | this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.cos(-this.azimuth) + this.orbitPoint[2] 71 | ]; 72 | 73 | return position; 74 | }; 75 | 76 | Camera.prototype.isMouseDown = function () { 77 | return this.mouseDown; 78 | }; 79 | 80 | Camera.prototype.getViewMatrix = function () { 81 | return this.viewMatrix; 82 | }; 83 | 84 | Camera.prototype.setBounds = function (minElevation, maxElevation) { 85 | this.minElevation = minElevation; 86 | this.maxElevation = maxElevation; 87 | 88 | if (this.elevation > this.maxElevation) this.elevation = this.maxElevation; 89 | if (this.elevation < this.minElevation) this.elevation = this.minElevation; 90 | 91 | this.recomputeViewMatrix(); 92 | }; 93 | 94 | Camera.prototype.onMouseDown = function (event) { 95 | event.preventDefault(); 96 | 97 | var x = Utilities.getMousePosition(event, this.element).x; 98 | var y = Utilities.getMousePosition(event, this.element).y; 99 | 100 | this.mouseDown = true; 101 | this.lastMouseX = x; 102 | this.lastMouseY = y; 103 | }; 104 | 105 | Camera.prototype.onMouseUp = function (event) { 106 | event.preventDefault(); 107 | 108 | this.mouseDown = false; 109 | }; 110 | 111 | Camera.prototype.onMouseMove = function (event) { 112 | event.preventDefault(); 113 | 114 | var x = Utilities.getMousePosition(event, this.element).x; 115 | var y = Utilities.getMousePosition(event, this.element).y; 116 | 117 | if (this.mouseDown) { 118 | this.currentMouseX = x; 119 | this.currentMouseY = y; 120 | 121 | var deltaAzimuth = (this.currentMouseX - this.lastMouseX) * SENSITIVITY; 122 | var deltaElevation = (this.currentMouseY - this.lastMouseY) * SENSITIVITY; 123 | 124 | this.azimuth += deltaAzimuth; 125 | this.elevation += deltaElevation; 126 | 127 | if (this.elevation > this.maxElevation) this.elevation = this.maxElevation; 128 | if (this.elevation < this.minElevation) this.elevation = this.minElevation; 129 | 130 | this.recomputeViewMatrix(); 131 | 132 | this.lastMouseX = this.currentMouseX; 133 | this.lastMouseY = this.currentMouseY; 134 | } 135 | }; 136 | 137 | return Camera; 138 | }()); 139 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fluid Particles 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 64 | 65 | 74 | 75 | 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /utilities.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Utilities = { 4 | clamp: function (x, min, max) { 5 | return Math.max(min, Math.min(max, x)); 6 | }, 7 | 8 | getMousePosition: function (event, element) { 9 | var boundingRect = element.getBoundingClientRect(); 10 | return { 11 | x: event.clientX - boundingRect.left, 12 | y: event.clientY - boundingRect.top 13 | }; 14 | }, 15 | 16 | addVectors: function (out, a, b) { 17 | out[0] = a[0] + b[0]; 18 | out[1] = a[1] + b[1]; 19 | out[2] = a[2] + b[2]; 20 | return out; 21 | }, 22 | 23 | subtractVectors: function (out, a, b) { 24 | out[0] = a[0] - b[0]; 25 | out[1] = a[1] - b[1]; 26 | out[2] = a[2] - b[2]; 27 | return out; 28 | }, 29 | 30 | magnitudeOfVector: function (v) { 31 | return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); 32 | }, 33 | 34 | dotVectors: function (a, b) { 35 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; 36 | }, 37 | 38 | multiplyVectorByScalar: function (out, v, k) { 39 | out[0] = v[0] * k; 40 | out[1] = v[1] * k; 41 | out[2] = v[2] * k; 42 | return out; 43 | }, 44 | 45 | 46 | normalizeVector: function (out, v) { 47 | var inverseMagnitude = 1.0 / Utilities.magnitudeOfVector(v); 48 | out[0] = v[0] * inverseMagnitude; 49 | out[1] = v[1] * inverseMagnitude; 50 | out[2] = v[2] * inverseMagnitude; 51 | return out; 52 | }, 53 | 54 | makePerspectiveMatrix: function (out, fovy, aspect, near, far) { 55 | var f = 1.0 / Math.tan(fovy / 2), 56 | nf = 1 / (near - far); 57 | 58 | out[0] = f / aspect; 59 | out[1] = 0; 60 | out[2] = 0; 61 | out[3] = 0; 62 | out[4] = 0; 63 | out[5] = f; 64 | out[6] = 0; 65 | out[7] = 0; 66 | out[8] = 0; 67 | out[9] = 0; 68 | out[10] = (far + near) * nf; 69 | out[11] = -1; 70 | out[12] = 0; 71 | out[13] = 0; 72 | out[14] = (2 * far * near) * nf; 73 | out[15] = 0; 74 | return out; 75 | }, 76 | 77 | makeIdentityMatrix: function (matrix) { 78 | matrix[0] = 1.0; 79 | matrix[1] = 0.0; 80 | matrix[2] = 0.0; 81 | matrix[3] = 0.0; 82 | matrix[4] = 0.0; 83 | matrix[5] = 1.0; 84 | matrix[6] = 0.0; 85 | matrix[7] = 0.0; 86 | matrix[8] = 0.0; 87 | matrix[9] = 0.0; 88 | matrix[10] = 1.0; 89 | matrix[11] = 0.0; 90 | matrix[12] = 0.0; 91 | matrix[13] = 0.0; 92 | matrix[14] = 0.0; 93 | matrix[15] = 1.0; 94 | return matrix; 95 | }, 96 | 97 | premultiplyMatrix: function (out, matrixA, matrixB) { //out = matrixB * matrixA 98 | var b0 = matrixB[0], b4 = matrixB[4], b8 = matrixB[8], b12 = matrixB[12], 99 | b1 = matrixB[1], b5 = matrixB[5], b9 = matrixB[9], b13 = matrixB[13], 100 | b2 = matrixB[2], b6 = matrixB[6], b10 = matrixB[10], b14 = matrixB[14], 101 | b3 = matrixB[3], b7 = matrixB[7], b11 = matrixB[11], b15 = matrixB[15], 102 | 103 | aX = matrixA[0], aY = matrixA[1], aZ = matrixA[2], aW = matrixA[3]; 104 | out[0] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW; 105 | out[1] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW; 106 | out[2] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW; 107 | out[3] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW; 108 | 109 | aX = matrixA[4], aY = matrixA[5], aZ = matrixA[6], aW = matrixA[7]; 110 | out[4] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW; 111 | out[5] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW; 112 | out[6] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW; 113 | out[7] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW; 114 | 115 | aX = matrixA[8], aY = matrixA[9], aZ = matrixA[10], aW = matrixA[11]; 116 | out[8] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW; 117 | out[9] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW; 118 | out[10] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW; 119 | out[11] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW; 120 | 121 | aX = matrixA[12], aY = matrixA[13], aZ = matrixA[14], aW = matrixA[15]; 122 | out[12] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW; 123 | out[13] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW; 124 | out[14] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW; 125 | out[15] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW; 126 | 127 | return out; 128 | }, 129 | 130 | makeXRotationMatrix: function (matrix, angle) { 131 | matrix[0] = 1.0; 132 | matrix[1] = 0.0; 133 | matrix[2] = 0.0; 134 | matrix[3] = 0.0; 135 | matrix[4] = 0.0; 136 | matrix[5] = Math.cos(angle); 137 | matrix[6] = Math.sin(angle); 138 | matrix[7] = 0.0; 139 | matrix[8] = 0.0; 140 | matrix[9] = -Math.sin(angle); 141 | matrix[10] = Math.cos(angle); 142 | matrix[11] = 0.0; 143 | matrix[12] = 0.0; 144 | matrix[13] = 0.0; 145 | matrix[14] = 0.0; 146 | matrix[15] = 1.0; 147 | return matrix; 148 | }, 149 | 150 | makeYRotationMatrix: function (matrix, angle) { 151 | matrix[0] = Math.cos(angle); 152 | matrix[1] = 0.0 153 | matrix[2] = -Math.sin(angle); 154 | matrix[3] = 0.0 155 | matrix[4] = 0.0 156 | matrix[5] = 1.0 157 | matrix[6] = 0.0; 158 | matrix[7] = 0.0; 159 | matrix[8] = Math.sin(angle); 160 | matrix[9] = 0.0 161 | matrix[10] = Math.cos(angle); 162 | matrix[11] = 0.0; 163 | matrix[12] = 0.0; 164 | matrix[13] = 0.0; 165 | matrix[14] = 0.0; 166 | matrix[15] = 1.0; 167 | return matrix; 168 | }, 169 | 170 | 171 | transformDirectionByMatrix: function (out, v, m) { 172 | var x = v[0], y = v[1], z = v[2]; 173 | out[0] = m[0] * x + m[4] * y + m[8] * z; 174 | out[1] = m[1] * x + m[5] * y + m[9] * z; 175 | out[2] = m[2] * x + m[6] * y + m[10] * z; 176 | out[3] = m[3] * x + m[7] * y + m[11] * z; 177 | return out; 178 | }, 179 | 180 | invertMatrix: function (out, m) { 181 | var m0 = m[0], m4 = m[4], m8 = m[8], m12 = m[12], 182 | m1 = m[1], m5 = m[5], m9 = m[9], m13 = m[13], 183 | m2 = m[2], m6 = m[6], m10 = m[10], m14 = m[14], 184 | m3 = m[3], m7 = m[7], m11 = m[11], m15 = m[15], 185 | 186 | temp0 = m10 * m15, 187 | temp1 = m14 * m11, 188 | temp2 = m6 * m15, 189 | temp3 = m14 * m7, 190 | temp4 = m6 * m11, 191 | temp5 = m10 * m7, 192 | temp6 = m2 * m15, 193 | temp7 = m14 * m3, 194 | temp8 = m2 * m11, 195 | temp9 = m10 * m3, 196 | temp10 = m2 * m7, 197 | temp11 = m6 * m3, 198 | temp12 = m8 * m13, 199 | temp13 = m12 * m9, 200 | temp14 = m4 * m13, 201 | temp15 = m12 * m5, 202 | temp16 = m4 * m9, 203 | temp17 = m8 * m5, 204 | temp18 = m0 * m13, 205 | temp19 = m12 * m1, 206 | temp20 = m0 * m9, 207 | temp21 = m8 * m1, 208 | temp22 = m0 * m5, 209 | temp23 = m4 * m1, 210 | 211 | t0 = (temp0 * m5 + temp3 * m9 + temp4 * m13) - (temp1 * m5 + temp2 * m9 + temp5 * m13), 212 | t1 = (temp1 * m1 + temp6 * m9 + temp9 * m13) - (temp0 * m1 + temp7 * m9 + temp8 * m13), 213 | t2 = (temp2 * m1 + temp7 * m5 + temp10 * m13) - (temp3 * m1 + temp6 * m5 + temp11 * m13), 214 | t3 = (temp5 * m1 + temp8 * m5 + temp11 * m9) - (temp4 * m1 + temp9 * m5 + temp10 * m9), 215 | 216 | d = 1.0 / (m0 * t0 + m4 * t1 + m8 * t2 + m12 * t3); 217 | 218 | out[0] = d * t0; 219 | out[1] = d * t1; 220 | out[2] = d * t2; 221 | out[3] = d * t3; 222 | out[4] = d * ((temp1 * m4 + temp2 * m8 + temp5 * m12) - (temp0 * m4 + temp3 * m8 + temp4 * m12)); 223 | out[5] = d * ((temp0 * m0 + temp7 * m8 + temp8 * m12) - (temp1 * m0 + temp6 * m8 + temp9 * m12)); 224 | out[6] = d * ((temp3 * m0 + temp6 * m4 + temp11 * m12) - (temp2 * m0 + temp7 * m4 + temp10 * m12)); 225 | out[7] = d * ((temp4 * m0 + temp9 * m4 + temp10 * m8) - (temp5 * m0 + temp8 * m4 + temp11 * m8)); 226 | out[8] = d * ((temp12 * m7 + temp15 * m11 + temp16 * m15) - (temp13 * m7 + temp14 * m11 + temp17 * m15)); 227 | out[9] = d * ((temp13 * m3 + temp18 * m11 + temp21 * m15) - (temp12 * m3 + temp19 * m11 + temp20 * m15)); 228 | out[10] = d * ((temp14 * m3 + temp19 * m7 + temp22 * m15) - (temp15 * m3 + temp18 * m7 + temp23 * m15)); 229 | out[11] = d * ((temp17 * m3 + temp20 * m7 + temp23 * m11) - (temp16 * m3 + temp21 * m7 + temp22 * m11)); 230 | out[12] = d * ((temp14 * m10 + temp17 * m14 + temp13 * m6) - (temp16 * m14 + temp12 * m6 + temp15 * m10)); 231 | out[13] = d * ((temp20 * m14 + temp12 * m2 + temp19 * m10) - (temp18 * m10 + temp21 * m14 + temp13 * m2)); 232 | out[14] = d * ((temp18 * m6 + temp23 * m14 + temp15 * m2) - (temp22 * m14 + temp14 * m2 + temp19 * m6)); 233 | out[15] = d * ((temp22 * m10 + temp16 * m2 + temp21 * m6) - (temp20 * m6 + temp23 * m10 + temp17 * m2)); 234 | 235 | return out; 236 | }, 237 | 238 | makeLookAtMatrix: function (matrix, eye, target, up) { //up is assumed to be normalized 239 | var forwardX = eye[0] - target[0], 240 | forwardY = eye[1] - target[1], 241 | forwardZ = eye[2] - target[2]; 242 | var forwardMagnitude = Math.sqrt(forwardX * forwardX + forwardY * forwardY + forwardZ * forwardZ); 243 | forwardX /= forwardMagnitude; 244 | forwardY /= forwardMagnitude; 245 | forwardZ /= forwardMagnitude; 246 | 247 | var rightX = up[2] * forwardY - up[1] * forwardZ; 248 | var rightY = up[0] * forwardZ - up[2] * forwardX; 249 | var rightZ = up[1] * forwardX - up[0] * forwardY; 250 | 251 | var rightMagnitude = Math.sqrt(rightX * rightX + rightY * rightY + rightZ * rightZ); 252 | rightX /= rightMagnitude; 253 | rightY /= rightMagnitude; 254 | rightZ /= rightMagnitude; 255 | 256 | var newUpX = forwardY * rightZ - forwardZ * rightY; 257 | var newUpY = forwardZ * rightX - forwardX * rightZ; 258 | var newUpZ = forwardX * rightY - forwardY * rightX; 259 | 260 | var newUpMagnitude = Math.sqrt(newUpX * newUpX + newUpY * newUpY + newUpZ * newUpZ); 261 | newUpX /= newUpMagnitude; 262 | newUpY /= newUpMagnitude; 263 | newUpZ /= newUpMagnitude; 264 | 265 | matrix[0] = rightX; 266 | matrix[1] = newUpX; 267 | matrix[2] = forwardX; 268 | matrix[3] = 0; 269 | matrix[4] = rightY; 270 | matrix[5] = newUpY; 271 | matrix[6] = forwardY; 272 | matrix[7] = 0; 273 | matrix[8] = rightZ; 274 | matrix[9] = newUpZ; 275 | matrix[10] = forwardZ; 276 | matrix[11] = 0; 277 | matrix[12] = -(rightX * eye[0] + rightY * eye[1] + rightZ * eye[2]); 278 | matrix[13] = -(newUpX * eye[0] + newUpY * eye[1] + newUpZ * eye[2]); 279 | matrix[14] = -(forwardX * eye[0] + forwardY * eye[1] + forwardZ * eye[2]); 280 | matrix[15] = 1; 281 | }, 282 | 283 | makeOrthographicMatrix: function (matrix, left, right, bottom, top, near, far) { 284 | matrix[0] = 2 / (right - left); 285 | matrix[1] = 0; 286 | matrix[2] = 0; 287 | matrix[3] = 0; 288 | matrix[4] = 0; 289 | matrix[5] = 2 / (top - bottom); 290 | matrix[6] = 0; 291 | matrix[7] = 0; 292 | matrix[8] = 0; 293 | matrix[9] = 0; 294 | matrix[10] = -2 / (far - near); 295 | matrix[11] = 0; 296 | matrix[12] = -(right + left) / (right - left); 297 | matrix[13] = -(top + bottom) / (top - bottom); 298 | matrix[14] = -(far + near) / (far - near); 299 | matrix[15] = 1; 300 | 301 | return matrix; 302 | }, 303 | } 304 | 305 | -------------------------------------------------------------------------------- /fluidparticles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var FluidParticles = (function () { 4 | var FOV = Math.PI / 3; 5 | 6 | var State = { 7 | EDITING: 0, 8 | SIMULATING: 1 9 | }; 10 | 11 | var GRID_WIDTH = 40, 12 | GRID_HEIGHT = 20, 13 | GRID_DEPTH = 20; 14 | 15 | var PARTICLES_PER_CELL = 10; 16 | 17 | function FluidParticles () { 18 | 19 | var canvas = this.canvas = document.getElementById('canvas'); 20 | var wgl = this.wgl = new WrappedGL(canvas); 21 | 22 | window.wgl = wgl; 23 | 24 | this.projectionMatrix = Utilities.makePerspectiveMatrix(new Float32Array(16), FOV, this.canvas.width / this.canvas.height, 0.1, 100.0); 25 | this.camera = new Camera(this.canvas, [GRID_WIDTH / 2, GRID_HEIGHT / 3, GRID_DEPTH / 2]); 26 | 27 | var boxEditorLoaded = false, 28 | simulatorRendererLoaded = false; 29 | 30 | this.boxEditor = new BoxEditor.BoxEditor(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], (function () { 31 | boxEditorLoaded = true; 32 | if (boxEditorLoaded && simulatorRendererLoaded) { 33 | start.call(this); 34 | } 35 | }).bind(this), 36 | (function () { 37 | this.redrawUI(); 38 | }).bind(this)); 39 | 40 | this.simulatorRenderer = new SimulatorRenderer(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], (function () { 41 | simulatorRendererLoaded = true; 42 | if (boxEditorLoaded && simulatorRendererLoaded) { 43 | start.call(this); 44 | } 45 | }).bind(this)); 46 | 47 | function start(programs) { 48 | this.state = State.EDITING; 49 | 50 | this.startButton = document.getElementById('start-button'); 51 | 52 | this.startButton.addEventListener('click', (function () { 53 | if (this.state === State.EDITING) { 54 | if (this.boxEditor.boxes.length > 0) { 55 | this.startSimulation(); 56 | } 57 | this.redrawUI(); 58 | } else if (this.state === State.SIMULATING) { 59 | this.stopSimulation(); 60 | this.redrawUI(); 61 | } 62 | }).bind(this)); 63 | 64 | this.currentPresetIndex = 0; 65 | this.editedSinceLastPreset = false; //whether the user has edited the last set preset 66 | var PRESETS = [ 67 | //dam break 68 | [ 69 | new BoxEditor.AABB([0, 0, 0], [15, 20, 20]) 70 | ], 71 | 72 | //block drop 73 | [ 74 | new BoxEditor.AABB([0, 0, 0], [40, 7, 20]), 75 | new BoxEditor.AABB([12, 12, 5], [28, 20, 15]) 76 | ], 77 | 78 | //double splash 79 | [ 80 | new BoxEditor.AABB([0, 0, 0], [10, 20, 15]), 81 | new BoxEditor.AABB([30, 0, 5], [40, 20, 20]) 82 | ], 83 | 84 | ]; 85 | 86 | this.presetButton = document.getElementById('preset-button'); 87 | this.presetButton.addEventListener('click', (function () { 88 | this.editedSinceLastPreset = false; 89 | 90 | this.boxEditor.boxes.length = 0; 91 | 92 | var preset = PRESETS[this.currentPresetIndex]; 93 | for (var i = 0; i < preset.length; ++i) { 94 | this.boxEditor.boxes.push(preset[i].clone()); 95 | } 96 | 97 | this.currentPresetIndex = (this.currentPresetIndex + 1) % PRESETS.length; 98 | 99 | this.redrawUI(); 100 | 101 | }).bind(this)); 102 | 103 | 104 | 105 | //////////////////////////////////////////////////////// 106 | // parameters/sliders 107 | 108 | //using gridCellDensity ensures a linear relationship to particle count 109 | this.gridCellDensity = 0.5; //simulation grid cell density per world space unit volume 110 | 111 | this.timeStep = 1.0 / 60.0; 112 | 113 | this.densitySlider = new Slider(document.getElementById('density-slider'), this.gridCellDensity, 0.2, 3.0, (function (value) { 114 | this.gridCellDensity = value; 115 | 116 | this.redrawUI(); 117 | }).bind(this)); 118 | 119 | this.flipnessSlider = new Slider(document.getElementById('fluidity-slider'), this.simulatorRenderer.simulator.flipness, 0.5, 0.99, (function (value) { 120 | this.simulatorRenderer.simulator.flipness = value; 121 | }).bind(this)); 122 | 123 | this.speedSlider = new Slider(document.getElementById('speed-slider'), this.timeStep, 0.0, 1.0 / 60.0, (function (value) { 124 | this.timeStep = value; 125 | }).bind(this)); 126 | 127 | 128 | this.redrawUI(); 129 | 130 | 131 | this.presetButton.click(); 132 | 133 | /////////////////////////////////////////////////////// 134 | // interaction state stuff 135 | 136 | canvas.addEventListener('mousemove', this.onMouseMove.bind(this)); 137 | canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); 138 | document.addEventListener('mouseup', this.onMouseUp.bind(this)); 139 | 140 | document.addEventListener('keydown', this.onKeyDown.bind(this)); 141 | document.addEventListener('keyup', this.onKeyUp.bind(this)); 142 | 143 | window.addEventListener('resize', this.onResize.bind(this)); 144 | this.onResize(); 145 | 146 | 147 | //////////////////////////////////////////////////// 148 | // start the update loop 149 | 150 | var lastTime = 0; 151 | var update = (function (currentTime) { 152 | var deltaTime = currentTime - lastTime || 0; 153 | lastTime = currentTime; 154 | 155 | this.update(deltaTime); 156 | 157 | requestAnimationFrame(update); 158 | }).bind(this); 159 | update(); 160 | 161 | 162 | } 163 | } 164 | 165 | FluidParticles.prototype.onResize = function (event) { 166 | this.canvas.width = window.innerWidth; 167 | this.canvas.height = window.innerHeight; 168 | Utilities.makePerspectiveMatrix(this.projectionMatrix, FOV, this.canvas.width / this.canvas.height, 0.1, 100.0); 169 | 170 | this.simulatorRenderer.onResize(event); 171 | } 172 | 173 | FluidParticles.prototype.onMouseMove = function (event) { 174 | event.preventDefault(); 175 | 176 | if (this.state === State.EDITING) { 177 | this.boxEditor.onMouseMove(event); 178 | 179 | if (this.boxEditor.interactionState !== null) { 180 | this.editedSinceLastPreset = true; 181 | } 182 | } else if (this.state === State.SIMULATING) { 183 | this.simulatorRenderer.onMouseMove(event); 184 | } 185 | }; 186 | 187 | FluidParticles.prototype.onMouseDown = function (event) { 188 | event.preventDefault(); 189 | 190 | if (this.state === State.EDITING) { 191 | this.boxEditor.onMouseDown(event); 192 | } else if (this.state === State.SIMULATING) { 193 | this.simulatorRenderer.onMouseDown(event); 194 | } 195 | }; 196 | 197 | FluidParticles.prototype.onMouseUp = function (event) { 198 | event.preventDefault(); 199 | 200 | if (this.state === State.EDITING) { 201 | this.boxEditor.onMouseUp(event); 202 | } else if (this.state === State.SIMULATING) { 203 | this.simulatorRenderer.onMouseUp(event); 204 | } 205 | }; 206 | 207 | FluidParticles.prototype.onKeyDown = function (event) { 208 | if (this.state === State.EDITING) { 209 | this.boxEditor.onKeyDown(event); 210 | } 211 | }; 212 | 213 | FluidParticles.prototype.onKeyUp = function (event) { 214 | if (this.state === State.EDITING) { 215 | this.boxEditor.onKeyUp(event); 216 | } 217 | }; 218 | 219 | //the UI elements are all created in the constructor, this just updates the DOM elements 220 | //should be called every time state changes 221 | FluidParticles.prototype.redrawUI = function () { 222 | 223 | var simulatingElements = document.querySelectorAll('.simulating-ui'); 224 | var editingElements = document.querySelectorAll('.editing-ui'); 225 | 226 | 227 | if (this.state === State.SIMULATING) { 228 | for (var i = 0; i < simulatingElements.length; ++i) { 229 | simulatingElements[i].style.display = 'block'; 230 | } 231 | 232 | for (var i = 0; i < editingElements.length; ++i) { 233 | editingElements[i].style.display = 'none'; 234 | } 235 | 236 | 237 | this.startButton.textContent = 'Edit'; 238 | this.startButton.className = 'start-button-active'; 239 | } else if (this.state === State.EDITING) { 240 | for (var i = 0; i < simulatingElements.length; ++i) { 241 | simulatingElements[i].style.display = 'none'; 242 | } 243 | 244 | for (var i = 0; i < editingElements.length; ++i) { 245 | editingElements[i].style.display = 'block'; 246 | } 247 | 248 | document.getElementById('particle-count').innerHTML = this.getParticleCount().toFixed(0) + ' particles'; 249 | 250 | if (this.boxEditor.boxes.length >= 2 || 251 | this.boxEditor.boxes.length === 1 && (this.boxEditor.interactionState === null || this.boxEditor.interactionState.mode !== BoxEditor.InteractionMode.EXTRUDING && this.boxEditor.interactionState.mode !== BoxEditor.InteractionMode.DRAWING)) { 252 | this.startButton.className = 'start-button-active'; 253 | } else { 254 | this.startButton.className = 'start-button-inactive'; 255 | } 256 | 257 | this.startButton.textContent = 'Start'; 258 | 259 | if (this.editedSinceLastPreset) { 260 | this.presetButton.innerHTML = 'Use Preset'; 261 | } else { 262 | this.presetButton.innerHTML = 'Next Preset'; 263 | } 264 | } 265 | 266 | this.flipnessSlider.redraw(); 267 | this.densitySlider.redraw(); 268 | this.speedSlider.redraw(); 269 | } 270 | 271 | 272 | //compute the number of particles for the current boxes and grid density 273 | FluidParticles.prototype.getParticleCount = function () { 274 | var boxEditor = this.boxEditor; 275 | 276 | var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity; 277 | 278 | //assuming x:y:z ratio of 2:1:1 279 | var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0)); 280 | var gridResolutionZ = gridResolutionY * 1; 281 | var gridResolutionX = gridResolutionY * 2; 282 | 283 | var totalGridCells = gridResolutionX * gridResolutionY * gridResolutionZ; 284 | 285 | 286 | var totalVolume = 0; 287 | var cumulativeVolume = []; //at index i, contains the total volume up to and including box i (so index 0 has volume of first box, last index has total volume) 288 | 289 | for (var i = 0; i < boxEditor.boxes.length; ++i) { 290 | var box = boxEditor.boxes[i]; 291 | var volume = box.computeVolume(); 292 | 293 | totalVolume += volume; 294 | cumulativeVolume[i] = totalVolume; 295 | } 296 | 297 | var fractionFilled = totalVolume / (GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH); 298 | 299 | var desiredParticleCount = fractionFilled * totalGridCells * PARTICLES_PER_CELL; //theoretical number of particles 300 | 301 | return desiredParticleCount; 302 | } 303 | 304 | //begin simulation using boxes from box editor 305 | //EDITING -> SIMULATING 306 | FluidParticles.prototype.startSimulation = function () { 307 | this.state = State.SIMULATING; 308 | 309 | var desiredParticleCount = this.getParticleCount(); //theoretical number of particles 310 | var particlesWidth = 512; //we fix particlesWidth 311 | var particlesHeight = Math.ceil(desiredParticleCount / particlesWidth); //then we calculate the particlesHeight that produces the closest particle count 312 | 313 | var particleCount = particlesWidth * particlesHeight; 314 | var particlePositions = []; 315 | 316 | var boxEditor = this.boxEditor; 317 | 318 | var totalVolume = 0; 319 | for (var i = 0; i < boxEditor.boxes.length; ++i) { 320 | totalVolume += boxEditor.boxes[i].computeVolume(); 321 | } 322 | 323 | var particlesCreatedSoFar = 0; 324 | for (var i = 0; i < boxEditor.boxes.length; ++i) { 325 | var box = boxEditor.boxes[i]; 326 | 327 | var particlesInBox = 0; 328 | if (i < boxEditor.boxes.length - 1) { 329 | particlesInBox = Math.floor(particleCount * box.computeVolume() / totalVolume); 330 | } else { //for the last box we just use up all the remaining particles 331 | particlesInBox = particleCount - particlesCreatedSoFar; 332 | } 333 | 334 | for (var j = 0; j < particlesInBox; ++j) { 335 | var position = box.randomPoint(); 336 | particlePositions.push(position); 337 | } 338 | 339 | particlesCreatedSoFar += particlesInBox; 340 | } 341 | 342 | var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity; 343 | 344 | //assuming x:y:z ratio of 2:1:1 345 | var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0)); 346 | var gridResolutionZ = gridResolutionY * 1; 347 | var gridResolutionX = gridResolutionY * 2; 348 | 349 | 350 | var gridSize = [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH]; 351 | var gridResolution = [gridResolutionX, gridResolutionY, gridResolutionZ]; 352 | 353 | var sphereRadius = 7.0 / gridResolutionX; 354 | this.simulatorRenderer.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, PARTICLES_PER_CELL, sphereRadius); 355 | 356 | this.camera.setBounds(0, Math.PI / 2); 357 | } 358 | 359 | //go back to box editing 360 | //SIMULATING -> EDITING 361 | FluidParticles.prototype.stopSimulation = function () { 362 | this.state = State.EDITING; 363 | 364 | this.camera.setBounds(-Math.PI / 4, Math.PI / 4); 365 | } 366 | 367 | FluidParticles.prototype.update = function () { 368 | if (this.state === State.EDITING) { 369 | this.boxEditor.draw(); 370 | } else if (this.state === State.SIMULATING) { 371 | this.simulatorRenderer.update(this.timeStep); 372 | } 373 | } 374 | 375 | return FluidParticles; 376 | }()); 377 | 378 | -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Renderer = (function () { 4 | 5 | var SHADOW_MAP_WIDTH = 256; 6 | var SHADOW_MAP_HEIGHT = 256; 7 | 8 | 9 | /* 10 | we render in a deferred way to a special RGBA texture format 11 | the format is (normal.x, normal.y, speed, depth) 12 | the normal is normalized (thus z can be reconstructed with sqrt(1.0 - x * x - y * y) 13 | the depth simply the z in view space 14 | */ 15 | 16 | //returns {vertices, normals, indices} 17 | function generateSphereGeometry (iterations) { 18 | 19 | var vertices = [], 20 | normals = []; 21 | 22 | var compareVectors = function (a, b) { 23 | var EPSILON = 0.001; 24 | return Math.abs(a[0] - b[0]) < EPSILON && Math.abs(a[1] - b[1]) < EPSILON && Math.abs(a[2] - b[2]) < EPSILON; 25 | }; 26 | 27 | var addVertex = function (v) { 28 | Utilities.normalizeVector(v, v); 29 | vertices.push(v); 30 | normals.push(v); 31 | }; 32 | 33 | var getMiddlePoint = function (vertexA, vertexB) { 34 | var middle = [ 35 | (vertexA[0] + vertexB[0]) / 2.0, 36 | (vertexA[1] + vertexB[1]) / 2.0, 37 | (vertexA[2] + vertexB[2]) / 2.0]; 38 | 39 | Utilities.normalizeVector(middle, middle); 40 | 41 | for (var i = 0; i < vertices.length; ++i) { 42 | if (compareVectors(vertices[i], middle)) { 43 | return i; 44 | } 45 | } 46 | 47 | addVertex(middle); 48 | return (vertices.length - 1); 49 | }; 50 | 51 | 52 | var t = (1.0 + Math.sqrt(5.0)) / 2.0; 53 | 54 | addVertex([-1, t, 0]); 55 | addVertex([1, t, 0]); 56 | addVertex([-1, -t, 0]); 57 | addVertex([1, -t, 0]); 58 | 59 | addVertex([0, -1, t]); 60 | addVertex([0, 1, t]); 61 | addVertex([0, -1, -t]); 62 | addVertex([0, 1, -t]); 63 | 64 | addVertex([t, 0, -1]); 65 | addVertex([t, 0, 1]); 66 | addVertex([-t, 0, -1]); 67 | addVertex([-t, 0, 1]); 68 | 69 | 70 | var faces = []; 71 | faces.push([0, 11, 5]); 72 | faces.push([0, 5, 1]); 73 | faces.push([0, 1, 7]); 74 | faces.push([0, 7, 10]); 75 | faces.push([0, 10, 11]); 76 | 77 | faces.push([1, 5, 9]); 78 | faces.push([5, 11, 4]); 79 | faces.push([11, 10, 2]); 80 | faces.push([10, 7, 6]); 81 | faces.push([7, 1, 8]); 82 | 83 | faces.push([3, 9, 4]); 84 | faces.push([3, 4, 2]); 85 | faces.push([3, 2, 6]); 86 | faces.push([3, 6, 8]); 87 | faces.push([3, 8, 9]); 88 | 89 | faces.push([4, 9, 5]); 90 | faces.push([2, 4, 11]); 91 | faces.push([6, 2, 10]); 92 | faces.push([8, 6, 7]); 93 | faces.push([9, 8, 1]); 94 | 95 | 96 | for (var i = 0; i < iterations; ++i) { 97 | var faces2 = []; 98 | 99 | for (var i = 0; i < faces.length; ++i) { 100 | var face = faces[i]; 101 | //replace triangle with 4 triangles 102 | var a = getMiddlePoint(vertices[face[0]], vertices[face[1]]); 103 | var b = getMiddlePoint(vertices[face[1]], vertices[face[2]]); 104 | var c = getMiddlePoint(vertices[face[2]], vertices[face[0]]); 105 | 106 | faces2.push([face[0], a, c]); 107 | faces2.push([face[1], b, a]); 108 | faces2.push([face[2], c, b]); 109 | faces2.push([a, b, c]); 110 | } 111 | 112 | faces = faces2; 113 | } 114 | 115 | 116 | var packedVertices = [], 117 | packedNormals = [], 118 | indices = []; 119 | 120 | for (var i = 0; i < vertices.length; ++i) { 121 | packedVertices.push(vertices[i][0]); 122 | packedVertices.push(vertices[i][1]); 123 | packedVertices.push(vertices[i][2]); 124 | 125 | packedNormals.push(normals[i][0]); 126 | packedNormals.push(normals[i][1]); 127 | packedNormals.push(normals[i][2]); 128 | } 129 | 130 | for (var i = 0; i < faces.length; ++i) { 131 | var face = faces[i]; 132 | indices.push(face[0]); 133 | indices.push(face[1]); 134 | indices.push(face[2]); 135 | } 136 | 137 | return { 138 | vertices: packedVertices, 139 | normals: packedNormals, 140 | indices: indices 141 | } 142 | } 143 | 144 | 145 | //you need to call reset() before drawing 146 | function Renderer (canvas, wgl, gridDimensions, onLoaded) { 147 | 148 | this.canvas = canvas; 149 | this.wgl = wgl; 150 | 151 | this.particlesWidth = 0; 152 | this.particlesHeight = 0; 153 | 154 | this.sphereRadius = 0.0; 155 | 156 | this.wgl.getExtension('ANGLE_instanced_arrays'); 157 | this.depthExt = this.wgl.getExtension('WEBGL_depth_texture'); 158 | 159 | 160 | this.quadVertexBuffer = wgl.createBuffer(); 161 | wgl.bufferData(this.quadVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), wgl.STATIC_DRAW); 162 | 163 | 164 | /////////////////////////////////////////////////////// 165 | // create stuff for rendering 166 | 167 | var sphereGeometry = this.sphereGeometry = generateSphereGeometry(3); 168 | 169 | this.sphereVertexBuffer = wgl.createBuffer(); 170 | wgl.bufferData(this.sphereVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array(sphereGeometry.vertices), wgl.STATIC_DRAW); 171 | 172 | this.sphereNormalBuffer = wgl.createBuffer(); 173 | wgl.bufferData(this.sphereNormalBuffer, wgl.ARRAY_BUFFER, new Float32Array(sphereGeometry.normals), wgl.STATIC_DRAW); 174 | 175 | this.sphereIndexBuffer = wgl.createBuffer(); 176 | wgl.bufferData(this.sphereIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(sphereGeometry.indices), wgl.STATIC_DRAW); 177 | 178 | this.depthFramebuffer = wgl.createFramebuffer(); 179 | this.depthColorTexture = wgl.buildTexture(wgl.RGBA, wgl.UNSIGNED_BYTE, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 180 | this.depthTexture = wgl.buildTexture(wgl.DEPTH_COMPONENT, wgl.UNSIGNED_SHORT, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 181 | 182 | 183 | //we light directly from above 184 | this.lightViewMatrix = new Float32Array(16); 185 | var midpoint = [gridDimensions[0] / 2, gridDimensions[1] / 2, gridDimensions[2] / 2]; 186 | Utilities.makeLookAtMatrix(this.lightViewMatrix, midpoint, [midpoint[0], midpoint[1] - 1.0, midpoint[2]], [0.0, 0.0, 1.0]); 187 | this.lightProjectionMatrix = Utilities.makeOrthographicMatrix(new Float32Array(16), -gridDimensions[0] / 2, gridDimensions[0] / 2, -gridDimensions[2] / 2, gridDimensions[2] / 2, -gridDimensions[1] / 2, gridDimensions[1] / 2); 188 | this.lightProjectionViewMatrix = new Float32Array(16); 189 | Utilities.premultiplyMatrix(this.lightProjectionViewMatrix, this.lightViewMatrix, this.lightProjectionMatrix); 190 | 191 | 192 | this.particleVertexBuffer = wgl.createBuffer(); 193 | 194 | this.renderingFramebuffer = wgl.createFramebuffer(); 195 | this.renderingRenderbuffer = wgl.createRenderbuffer(); 196 | this.renderingTexture = wgl.createTexture(); 197 | this.occlusionTexture = wgl.createTexture(); 198 | this.compositingTexture = wgl.createTexture(); 199 | 200 | 201 | this.onResize(); 202 | 203 | wgl.createProgramsFromFiles({ 204 | sphereProgram: { 205 | vertexShader: 'shaders/sphere.vert', 206 | fragmentShader: 'shaders/sphere.frag' 207 | }, 208 | sphereDepthProgram: { 209 | vertexShader: 'shaders/spheredepth.vert', 210 | fragmentShader: 'shaders/spheredepth.frag' 211 | }, 212 | sphereAOProgram: { 213 | vertexShader: 'shaders/sphereao.vert', 214 | fragmentShader: 'shaders/sphereao.frag' 215 | }, 216 | compositeProgram: { 217 | vertexShader: 'shaders/fullscreen.vert', 218 | fragmentShader: 'shaders/composite.frag', 219 | attributeLocations: { 'a_position': 0} 220 | }, 221 | fxaaProgram: { 222 | vertexShader: 'shaders/fullscreen.vert', 223 | fragmentShader: 'shaders/fxaa.frag', 224 | attributeLocations: { 'a_position': 0} 225 | }, 226 | }, (function (programs) { 227 | for (var programName in programs) { 228 | this[programName] = programs[programName]; 229 | } 230 | 231 | onLoaded(); 232 | }).bind(this)); 233 | } 234 | 235 | Renderer.prototype.onResize = function (event) { 236 | wgl.renderbufferStorage(this.renderingRenderbuffer, wgl.RENDERBUFFER, wgl.DEPTH_COMPONENT16, this.canvas.width, this.canvas.height); 237 | wgl.rebuildTexture(this.renderingTexture, wgl.RGBA, wgl.FLOAT, this.canvas.width, this.canvas.height, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); //contains (normal.x, normal.y, speed, depth) 238 | 239 | wgl.rebuildTexture(this.occlusionTexture, wgl.RGBA, wgl.UNSIGNED_BYTE, this.canvas.width, this.canvas.height, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 240 | 241 | wgl.rebuildTexture(this.compositingTexture, wgl.RGBA, wgl.UNSIGNED_BYTE, this.canvas.width, this.canvas.height, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 242 | } 243 | 244 | 245 | Renderer.prototype.reset = function (particlesWidth, particlesHeight, sphereRadius) { 246 | this.particlesWidth = particlesWidth; 247 | this.particlesHeight = particlesHeight; 248 | 249 | this.sphereRadius = sphereRadius; 250 | 251 | /////////////////////////////////////////////////////////// 252 | // create particle data 253 | 254 | var particleCount = this.particlesWidth * this.particlesHeight; 255 | 256 | //fill particle vertex buffer containing the relevant texture coordinates 257 | var particleTextureCoordinates = new Float32Array(this.particlesWidth * this.particlesHeight * 2); 258 | for (var y = 0; y < this.particlesHeight; ++y) { 259 | for (var x = 0; x < this.particlesWidth; ++x) { 260 | particleTextureCoordinates[(y * this.particlesWidth + x) * 2] = (x + 0.5) / this.particlesWidth; 261 | particleTextureCoordinates[(y * this.particlesWidth + x) * 2 + 1] = (y + 0.5) / this.particlesHeight; 262 | } 263 | } 264 | 265 | wgl.bufferData(this.particleVertexBuffer, wgl.ARRAY_BUFFER, particleTextureCoordinates, wgl.STATIC_DRAW); 266 | } 267 | 268 | //you need to call reset() with the correct parameters before drawing anything 269 | //projectionMatrix and viewMatrix are both expected to be Float32Array(16) 270 | Renderer.prototype.draw = function (simulator, projectionMatrix, viewMatrix) { 271 | var wgl = this.wgl; 272 | 273 | ///////////////////////////////////////////// 274 | // draw particles 275 | 276 | 277 | var projectionViewMatrix = Utilities.premultiplyMatrix(new Float32Array(16), viewMatrix, projectionMatrix); 278 | 279 | 280 | /////////////////////////////////////////////// 281 | //draw rendering data (normal, speed, depth) 282 | 283 | wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.renderingTexture, 0); 284 | wgl.framebufferRenderbuffer(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.DEPTH_ATTACHMENT, wgl.RENDERBUFFER, this.renderingRenderbuffer); 285 | 286 | wgl.clear( 287 | wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(-99999.0, -99999.0, -99999.0, -99999.0), 288 | wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT); 289 | 290 | 291 | var sphereDrawState = wgl.createDrawState() 292 | .bindFramebuffer(this.renderingFramebuffer) 293 | .viewport(0, 0, this.canvas.width, this.canvas.height) 294 | 295 | .enable(wgl.DEPTH_TEST) 296 | .enable(wgl.CULL_FACE) 297 | 298 | .useProgram(this.sphereProgram) 299 | 300 | .vertexAttribPointer(this.sphereVertexBuffer, this.sphereProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 301 | .vertexAttribPointer(this.sphereNormalBuffer, this.sphereProgram.getAttribLocation('a_vertexNormal'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 302 | 303 | .vertexAttribPointer(this.particleVertexBuffer, this.sphereProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0) 304 | .vertexAttribDivisorANGLE(this.sphereProgram.getAttribLocation('a_textureCoordinates'), 1) 305 | 306 | .bindIndexBuffer(this.sphereIndexBuffer) 307 | 308 | .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix) 309 | .uniformMatrix4fv('u_viewMatrix', false, viewMatrix) 310 | 311 | .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture) 312 | .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture) 313 | 314 | .uniform1f('u_sphereRadius', this.sphereRadius) 315 | 316 | 317 | wgl.drawElementsInstancedANGLE(sphereDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight); 318 | 319 | 320 | 321 | /////////////////////////////////////////////////// 322 | // draw occlusion 323 | 324 | wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.occlusionTexture, 0); 325 | 326 | wgl.clear( 327 | wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(0.0, 0.0, 0.0, 0.0), 328 | wgl.COLOR_BUFFER_BIT); 329 | 330 | var fov = 2.0 * Math.atan(1.0 / projectionMatrix[5]); 331 | 332 | var occlusionDrawState = wgl.createDrawState() 333 | .bindFramebuffer(this.renderingFramebuffer) 334 | .viewport(0, 0, this.canvas.width, this.canvas.height) 335 | 336 | .enable(wgl.DEPTH_TEST) 337 | .depthMask(false) 338 | 339 | .enable(wgl.CULL_FACE) 340 | 341 | .enable(wgl.BLEND) 342 | .blendEquation(wgl.FUNC_ADD) 343 | .blendFuncSeparate(wgl.ONE, wgl.ONE, wgl.ONE, wgl.ONE) 344 | 345 | .useProgram(this.sphereAOProgram) 346 | 347 | .vertexAttribPointer(this.sphereVertexBuffer, this.sphereAOProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 348 | .vertexAttribPointer(this.particleVertexBuffer, this.sphereAOProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0) 349 | .vertexAttribDivisorANGLE(this.sphereAOProgram.getAttribLocation('a_textureCoordinates'), 1) 350 | 351 | 352 | .bindIndexBuffer(this.sphereIndexBuffer) 353 | 354 | .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix) 355 | .uniformMatrix4fv('u_viewMatrix', false, viewMatrix) 356 | 357 | .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture) 358 | .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture) 359 | 360 | .uniformTexture('u_renderingTexture', 2, wgl.TEXTURE_2D, this.renderingTexture) 361 | .uniform2f('u_resolution', this.canvas.width, this.canvas.height) 362 | .uniform1f('u_fov', fov) 363 | 364 | 365 | .uniform1f('u_sphereRadius', this.sphereRadius) 366 | 367 | 368 | wgl.drawElementsInstancedANGLE(occlusionDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight); 369 | 370 | 371 | //////////////////////////////////////////////// 372 | // draw depth map 373 | 374 | wgl.framebufferTexture2D(this.depthFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.depthColorTexture, 0); 375 | wgl.framebufferTexture2D(this.depthFramebuffer, wgl.FRAMEBUFFER, wgl.DEPTH_ATTACHMENT, wgl.TEXTURE_2D, this.depthTexture, 0); 376 | 377 | wgl.clear( 378 | wgl.createClearState().bindFramebuffer(this.depthFramebuffer).clearColor(0, 0, 0, 0), 379 | wgl.DEPTH_BUFFER_BIT); 380 | 381 | 382 | var depthDrawState = wgl.createDrawState() 383 | .bindFramebuffer(this.depthFramebuffer) 384 | .viewport(0, 0, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT) 385 | 386 | .enable(wgl.DEPTH_TEST) 387 | .depthMask(true) 388 | 389 | //so no occlusion past end of shadow map (with clamp to edge) 390 | .enable(wgl.SCISSOR_TEST) 391 | .scissor(1, 1, SHADOW_MAP_WIDTH - 2, SHADOW_MAP_HEIGHT - 2) 392 | 393 | .colorMask(false, false, false, false) 394 | 395 | .enable(wgl.CULL_FACE) 396 | 397 | .useProgram(this.sphereDepthProgram) 398 | 399 | .vertexAttribPointer(this.sphereVertexBuffer, this.sphereDepthProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 400 | .vertexAttribPointer(this.particleVertexBuffer, this.sphereDepthProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0) 401 | .vertexAttribDivisorANGLE(this.sphereDepthProgram.getAttribLocation('a_textureCoordinates'), 1) 402 | 403 | .bindIndexBuffer(this.sphereIndexBuffer) 404 | 405 | .uniformMatrix4fv('u_projectionViewMatrix', false, this.lightProjectionViewMatrix) 406 | 407 | .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture) 408 | .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture) 409 | 410 | .uniform1f('u_sphereRadius', this.sphereRadius) 411 | 412 | 413 | wgl.drawElementsInstancedANGLE(depthDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight); 414 | 415 | 416 | /////////////////////////////////////////// 417 | // composite 418 | 419 | 420 | var inverseViewMatrix = Utilities.invertMatrix(new Float32Array(16), viewMatrix); 421 | 422 | wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.compositingTexture, 0); 423 | 424 | wgl.clear( 425 | wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(0, 0, 0, 0), 426 | wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT); 427 | 428 | var compositeDrawState = wgl.createDrawState() 429 | .bindFramebuffer(this.renderingFramebuffer) 430 | .viewport(0, 0, this.canvas.width, this.canvas.height) 431 | 432 | .useProgram(this.compositeProgram) 433 | 434 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 435 | 436 | .uniformTexture('u_renderingTexture', 0, wgl.TEXTURE_2D, this.renderingTexture) 437 | .uniformTexture('u_occlusionTexture', 1, wgl.TEXTURE_2D, this.occlusionTexture) 438 | .uniform2f('u_resolution', this.canvas.width, this.canvas.height) 439 | .uniform1f('u_fov', fov) 440 | 441 | .uniformMatrix4fv('u_inverseViewMatrix', false, inverseViewMatrix) 442 | 443 | .uniformTexture('u_shadowDepthTexture', 2, wgl.TEXTURE_2D, this.depthTexture) 444 | .uniform2f('u_shadowResolution', SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT) 445 | .uniformMatrix4fv('u_lightProjectionViewMatrix', false, this.lightProjectionViewMatrix); 446 | 447 | wgl.drawArrays(compositeDrawState, wgl.TRIANGLE_STRIP, 0, 4); 448 | 449 | 450 | ////////////////////////////////////// 451 | // FXAA 452 | 453 | var inverseViewMatrix = Utilities.invertMatrix(new Float32Array(16), viewMatrix); 454 | 455 | wgl.clear( 456 | wgl.createClearState().bindFramebuffer(null).clearColor(0, 0, 0, 0), 457 | wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT); 458 | 459 | var fxaaDrawState = wgl.createDrawState() 460 | .bindFramebuffer(null) 461 | .viewport(0, 0, this.canvas.width, this.canvas.height) 462 | 463 | .useProgram(this.fxaaProgram) 464 | 465 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 466 | 467 | .uniformTexture('u_input', 0, wgl.TEXTURE_2D, this.compositingTexture) 468 | .uniform2f('u_resolution', this.canvas.width, this.canvas.height); 469 | 470 | wgl.drawArrays(fxaaDrawState, wgl.TRIANGLE_STRIP, 0, 4); 471 | } 472 | 473 | return Renderer; 474 | }()); 475 | -------------------------------------------------------------------------------- /simulator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Simulator = (function () { 4 | 5 | //simulation grid dimensions and resolution 6 | //all particles are in the world position space ([0, 0, 0], [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH]) 7 | 8 | //when doing most grid operations, we transform positions from world position space into the grid position space ([0, 0, 0], [GRID_RESOLUTION_X, GRID_RESOLUTION_Y, GRID_RESOLUTION_Z]) 9 | 10 | 11 | //in grid space, cell boundaries are simply at integer values 12 | 13 | //we emulate 3D textures with tiled 2d textures 14 | //so the z slices of a 3d texture are laid out along the x axis 15 | //the 2d dimensions of a 3d texture are therefore [width * depth, height] 16 | 17 | 18 | /* 19 | we use a staggered MAC grid 20 | this means the velocity grid width = grid width + 1 and velocity grid height = grid height + 1 and velocity grid depth = grid depth + 1 21 | a scalar for cell [i, j, k] is positionally located at [i + 0.5, j + 0.5, k + 0.5] 22 | x velocity for cell [i, j, k] is positionally located at [i, j + 0.5, k + 0.5] 23 | y velocity for cell [i, j, k] is positionally located at [i + 0.5, j, k + 0.5] 24 | z velocity for cell [i, j, k] is positionally located at [i + 0.5, j + 0.5, k] 25 | */ 26 | 27 | //the boundaries are the boundaries of the grid 28 | //a grid cell can either be fluid, air (these are tracked by markTexture) or is a wall (implicit by position) 29 | 30 | function Simulator (wgl, onLoaded) { 31 | this.wgl = wgl; 32 | 33 | this.particlesWidth = 0; 34 | this.particlesHeight = 0; 35 | 36 | this.gridWidth = 0; 37 | this.gridHeight = 0; 38 | this.gridDepth = 0; 39 | 40 | this.gridResolutionX = 0; 41 | this.gridResolutionY = 0; 42 | this.gridResolutionZ = 0; 43 | 44 | this.particleDensity = 0; 45 | 46 | this.velocityTextureWidth = 0; 47 | this.velocityTextureHeight = 0; 48 | 49 | this.scalarTextureWidth = 0; 50 | this.scalarTextureHeight = 0; 51 | 52 | 53 | this.halfFloatExt = this.wgl.getExtension('OES_texture_half_float'); 54 | this.wgl.getExtension('OES_texture_half_float_linear'); 55 | 56 | this.simulationNumberType = this.halfFloatExt.HALF_FLOAT_OES; 57 | 58 | 59 | /////////////////////////////////////////////////////// 60 | // simulation parameters 61 | 62 | this.flipness = 0.99; //0 is full PIC, 1 is full FLIP 63 | 64 | 65 | this.frameNumber = 0; //used for motion randomness 66 | 67 | 68 | ///////////////////////////////////////////////// 69 | // simulation objects (most are filled in by reset) 70 | 71 | this.quadVertexBuffer = wgl.createBuffer(); 72 | wgl.bufferData(this.quadVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), wgl.STATIC_DRAW); 73 | 74 | this.simulationFramebuffer = wgl.createFramebuffer(); 75 | this.particleVertexBuffer = wgl.createBuffer(); 76 | 77 | 78 | this.particlePositionTexture = wgl.createTexture(); 79 | this.particlePositionTextureTemp = wgl.createTexture(); 80 | 81 | 82 | this.particleVelocityTexture = wgl.createTexture(); 83 | this.particleVelocityTextureTemp = wgl.createTexture(); 84 | 85 | this.particleRandomTexture = wgl.createTexture(); //contains a random normalized direction for each particle 86 | 87 | 88 | 89 | //////////////////////////////////////////////////// 90 | // create simulation textures 91 | 92 | this.velocityTexture = wgl.createTexture(); 93 | this.tempVelocityTexture = wgl.createTexture(); 94 | this.originalVelocityTexture = wgl.createTexture(); 95 | this.weightTexture = wgl.createTexture(); 96 | 97 | this.markerTexture = wgl.createTexture(); //marks fluid/air, 1 if fluid, 0 if air 98 | this.divergenceTexture = wgl.createTexture(); 99 | this.pressureTexture = wgl.createTexture(); 100 | this.tempSimulationTexture = wgl.createTexture(); 101 | 102 | 103 | 104 | ///////////////////////////// 105 | // load programs 106 | 107 | 108 | wgl.createProgramsFromFiles({ 109 | transferToGridProgram: { 110 | vertexShader: 'shaders/transfertogrid.vert', 111 | fragmentShader: ['shaders/common.frag', 'shaders/transfertogrid.frag'], 112 | attributeLocations: { 'a_textureCoordinates': 0} 113 | }, 114 | normalizeGridProgram: { 115 | vertexShader: 'shaders/fullscreen.vert', 116 | fragmentShader: 'shaders/normalizegrid.frag', 117 | attributeLocations: { 'a_position': 0} 118 | }, 119 | markProgram: { 120 | vertexShader: 'shaders/mark.vert', 121 | fragmentShader: 'shaders/mark.frag', 122 | attributeLocations: { 'a_textureCoordinates': 0} 123 | }, 124 | addForceProgram: { 125 | vertexShader: 'shaders/fullscreen.vert', 126 | fragmentShader: ['shaders/common.frag', 'shaders/addforce.frag'], 127 | attributeLocations: { 'a_position': 0} 128 | }, 129 | enforceBoundariesProgram: { 130 | vertexShader: 'shaders/fullscreen.vert', 131 | fragmentShader: ['shaders/common.frag', 'shaders/enforceboundaries.frag'], 132 | attributeLocations: { 'a_textureCoordinates': 0 } 133 | }, 134 | extendVelocityProgram: { 135 | vertexShader: 'shaders/fullscreen.vert', 136 | fragmentShader: 'shaders/extendvelocity.frag', 137 | attributeLocations: { 'a_textureCoordinates': 0 } 138 | }, 139 | transferToParticlesProgram: { 140 | vertexShader: 'shaders/fullscreen.vert', 141 | fragmentShader: ['shaders/common.frag', 'shaders/transfertoparticles.frag'], 142 | attributeLocations: { 'a_position': 0} 143 | }, 144 | divergenceProgram: { 145 | vertexShader: 'shaders/fullscreen.vert', 146 | fragmentShader: ['shaders/common.frag', 'shaders/divergence.frag'], 147 | attributeLocations: { 'a_position': 0} 148 | }, 149 | jacobiProgram: { 150 | vertexShader: 'shaders/fullscreen.vert', 151 | fragmentShader: ['shaders/common.frag', 'shaders/jacobi.frag'], 152 | attributeLocations: { 'a_position': 0} 153 | }, 154 | subtractProgram: { 155 | vertexShader: 'shaders/fullscreen.vert', 156 | fragmentShader: ['shaders/common.frag', 'shaders/subtract.frag'], 157 | attributeLocations: { 'a_position': 0} 158 | }, 159 | advectProgram: { 160 | vertexShader: 'shaders/fullscreen.vert', 161 | fragmentShader: ['shaders/common.frag', 'shaders/advect.frag'], 162 | attributeLocations: { 'a_position': 0} 163 | }, 164 | copyProgram: { 165 | vertexShader: 'shaders/fullscreen.vert', 166 | fragmentShader: 'shaders/copy.frag', 167 | attributeLocations: { 'a_position': 0} 168 | } 169 | }, (function (programs) { 170 | for (var programName in programs) { 171 | this[programName] = programs[programName]; 172 | } 173 | 174 | onLoaded(); 175 | }).bind(this)); 176 | } 177 | 178 | 179 | //expects an array of [x, y, z] particle positions 180 | //gridSize and gridResolution are both [x, y, z] 181 | 182 | //particleDensity is particles per simulation grid cell 183 | Simulator.prototype.reset = function (particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, particleDensity) { 184 | 185 | this.particlesWidth = particlesWidth; 186 | this.particlesHeight = particlesHeight; 187 | 188 | this.gridWidth = gridSize[0]; 189 | this.gridHeight = gridSize[1]; 190 | this.gridDepth = gridSize[2]; 191 | 192 | this.gridResolutionX = gridResolution[0]; 193 | this.gridResolutionY = gridResolution[1]; 194 | this.gridResolutionZ = gridResolution[2]; 195 | 196 | this.particleDensity = particleDensity; 197 | 198 | this.velocityTextureWidth = (this.gridResolutionX + 1) * (this.gridResolutionZ + 1); 199 | this.velocityTextureHeight = (this.gridResolutionY + 1); 200 | 201 | this.scalarTextureWidth = this.gridResolutionX * this.gridResolutionZ; 202 | this.scalarTextureHeight = this.gridResolutionY; 203 | 204 | 205 | 206 | /////////////////////////////////////////////////////////// 207 | // create particle data 208 | 209 | var particleCount = this.particlesWidth * this.particlesHeight; 210 | 211 | //fill particle vertex buffer containing the relevant texture coordinates 212 | var particleTextureCoordinates = new Float32Array(this.particlesWidth * this.particlesHeight * 2); 213 | for (var y = 0; y < this.particlesHeight; ++y) { 214 | for (var x = 0; x < this.particlesWidth; ++x) { 215 | particleTextureCoordinates[(y * this.particlesWidth + x) * 2] = (x + 0.5) / this.particlesWidth; 216 | particleTextureCoordinates[(y * this.particlesWidth + x) * 2 + 1] = (y + 0.5) / this.particlesHeight; 217 | } 218 | } 219 | 220 | wgl.bufferData(this.particleVertexBuffer, wgl.ARRAY_BUFFER, particleTextureCoordinates, wgl.STATIC_DRAW); 221 | 222 | //generate initial particle positions amd create particle position texture for them 223 | var particlePositionsData = new Float32Array(this.particlesWidth * this.particlesHeight * 4); 224 | var particleRandoms = new Float32Array(this.particlesWidth * this.particlesHeight * 4); 225 | for (var i = 0; i < this.particlesWidth * this.particlesHeight; ++i) { 226 | particlePositionsData[i * 4] = particlePositions[i][0]; 227 | particlePositionsData[i * 4 + 1] = particlePositions[i][1]; 228 | particlePositionsData[i * 4 + 2] = particlePositions[i][2]; 229 | particlePositionsData[i * 4 + 3] = 0.0; 230 | 231 | var theta = Math.random() * 2.0 * Math.PI; 232 | var u = Math.random() * 2.0 - 1.0; 233 | particleRandoms[i * 4] = Math.sqrt(1.0 - u * u) * Math.cos(theta); 234 | particleRandoms[i * 4 + 1] = Math.sqrt(1.0 - u * u) * Math.sin(theta); 235 | particleRandoms[i * 4 + 2] = u; 236 | particleRandoms[i * 4 + 3] = 0.0; 237 | } 238 | 239 | wgl.rebuildTexture(this.particlePositionTexture, wgl.RGBA, wgl.FLOAT, this.particlesWidth, this.particlesHeight, particlePositionsData, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.NEAREST, wgl.NEAREST); 240 | wgl.rebuildTexture(this.particlePositionTextureTemp, wgl.RGBA, wgl.FLOAT, this.particlesWidth, this.particlesHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.NEAREST, wgl.NEAREST); 241 | 242 | 243 | wgl.rebuildTexture(this.particleVelocityTexture, wgl.RGBA, this.simulationNumberType, this.particlesWidth, this.particlesHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.NEAREST, wgl.NEAREST); 244 | wgl.rebuildTexture(this.particleVelocityTextureTemp, wgl.RGBA, this.simulationNumberType, this.particlesWidth, this.particlesHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.NEAREST, wgl.NEAREST); 245 | 246 | wgl.rebuildTexture(this.particleRandomTexture, wgl.RGBA, wgl.FLOAT, this.particlesWidth, this.particlesHeight, particleRandoms, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.NEAREST, wgl.NEAREST); //contains a random normalized direction for each particle 247 | 248 | 249 | 250 | //////////////////////////////////////////////////// 251 | // create simulation textures 252 | 253 | wgl.rebuildTexture(this.velocityTexture, wgl.RGBA, this.simulationNumberType, this.velocityTextureWidth, this.velocityTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 254 | wgl.rebuildTexture(this.tempVelocityTexture, wgl.RGBA, this.simulationNumberType, this.velocityTextureWidth, this.velocityTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 255 | wgl.rebuildTexture(this.originalVelocityTexture, wgl.RGBA, this.simulationNumberType, this.velocityTextureWidth, this.velocityTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 256 | wgl.rebuildTexture(this.weightTexture, wgl.RGBA, this.simulationNumberType, this.velocityTextureWidth, this.velocityTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 257 | 258 | wgl.rebuildTexture(this.markerTexture, wgl.RGBA, wgl.UNSIGNED_BYTE, this.scalarTextureWidth, this.scalarTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); //marks fluid/air, 1 if fluid, 0 if air 259 | wgl.rebuildTexture(this.divergenceTexture, wgl.RGBA, this.simulationNumberType, this.scalarTextureWidth, this.scalarTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 260 | wgl.rebuildTexture(this.pressureTexture, wgl.RGBA, this.simulationNumberType, this.scalarTextureWidth, this.scalarTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 261 | wgl.rebuildTexture(this.tempSimulationTexture, wgl.RGBA, this.simulationNumberType, this.scalarTextureWidth, this.scalarTextureHeight, null, wgl.CLAMP_TO_EDGE, wgl.CLAMP_TO_EDGE, wgl.LINEAR, wgl.LINEAR); 262 | 263 | 264 | } 265 | 266 | function swap (object, a, b) { 267 | var temp = object[a]; 268 | object[a] = object[b]; 269 | object[b] = temp; 270 | } 271 | 272 | //you need to call reset() with correct parameters before simulating 273 | //mouseVelocity, mouseRayOrigin, mouseRayDirection are all expected to be arrays of 3 values 274 | Simulator.prototype.simulate = function (timeStep, mouseVelocity, mouseRayOrigin, mouseRayDirection) { 275 | if (timeStep === 0.0) return; 276 | 277 | this.frameNumber += 1; 278 | 279 | var wgl = this.wgl; 280 | 281 | /* 282 | the simulation process 283 | transfer particle velocities to velocity grid 284 | save this velocity grid 285 | 286 | solve velocity grid for non divergence 287 | 288 | update particle velocities with new velocity grid 289 | advect particles through the grid velocity field 290 | */ 291 | 292 | 293 | ////////////////////////////////////////////////////// 294 | //transfer particle velocities to grid 295 | 296 | //we transfer particle velocities to the grid in two steps 297 | //in the first step, we accumulate weight * velocity into tempVelocityTexture and then weight into weightTexture 298 | //in the second step: velocityTexture = tempVelocityTexture / weightTexture 299 | 300 | //we accumulate into velocityWeightTexture and then divide into velocityTexture 301 | 302 | var transferToGridDrawState = wgl.createDrawState() 303 | .bindFramebuffer(this.simulationFramebuffer) 304 | .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) 305 | 306 | .vertexAttribPointer(this.particleVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 307 | 308 | .useProgram(this.transferToGridProgram) 309 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 310 | .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth) 311 | .uniformTexture('u_positionTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture) 312 | .uniformTexture('u_velocityTexture', 1, wgl.TEXTURE_2D, this.particleVelocityTexture) 313 | 314 | .enable(wgl.BLEND) 315 | .blendEquation(wgl.FUNC_ADD) 316 | .blendFuncSeparate(wgl.ONE, wgl.ONE, wgl.ONE, wgl.ONE); 317 | 318 | 319 | //accumulate weight 320 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.weightTexture, 0); 321 | 322 | wgl.clear( 323 | wgl.createClearState().bindFramebuffer(this.simulationFramebuffer).clearColor(0, 0, 0, 0), 324 | wgl.COLOR_BUFFER_BIT); 325 | 326 | transferToGridDrawState.uniform1i('u_accumulate', 0) 327 | 328 | //each particle gets splatted layer by layer from z - (SPLAT_SIZE - 1) / 2 to z + (SPLAT_SIZE - 1) / 2 329 | var SPLAT_DEPTH = 5; 330 | 331 | for (var z = -(SPLAT_DEPTH - 1) / 2; z <= (SPLAT_DEPTH - 1) / 2; ++z) { 332 | transferToGridDrawState.uniform1f('u_zOffset', z); 333 | wgl.drawArrays(transferToGridDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight); 334 | } 335 | 336 | //accumulate (weight * velocity) 337 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); 338 | wgl.clear( 339 | wgl.createClearState().bindFramebuffer(this.simulationFramebuffer), 340 | wgl.COLOR_BUFFER_BIT); 341 | 342 | transferToGridDrawState.uniform1i('u_accumulate', 1) 343 | 344 | for (var z = -(SPLAT_DEPTH - 1) / 2; z <= (SPLAT_DEPTH - 1) / 2; ++z) { 345 | transferToGridDrawState.uniform1f('u_zOffset', z); 346 | wgl.drawArrays(transferToGridDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight); 347 | } 348 | 349 | 350 | //in the second step, we divide sum(weight * velocity) by sum(weight) (the two accumulated quantities from before) 351 | 352 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.velocityTexture, 0); 353 | 354 | var normalizeDrawState = wgl.createDrawState() 355 | .bindFramebuffer(this.simulationFramebuffer) 356 | .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) 357 | 358 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 359 | 360 | .useProgram(this.normalizeGridProgram) 361 | .uniformTexture('u_weightTexture', 0, wgl.TEXTURE_2D, this.weightTexture) 362 | .uniformTexture('u_accumulatedVelocityTexture', 1, wgl.TEXTURE_2D, this.tempVelocityTexture) 363 | 364 | wgl.drawArrays(normalizeDrawState, wgl.TRIANGLE_STRIP, 0, 4); 365 | 366 | 367 | ////////////////////////////////////////////////////// 368 | // mark cells with fluid 369 | 370 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.markerTexture, 0); 371 | wgl.clear( 372 | wgl.createClearState().bindFramebuffer(this.simulationFramebuffer), 373 | wgl.COLOR_BUFFER_BIT); 374 | 375 | var markDrawState = wgl.createDrawState() 376 | .bindFramebuffer(this.simulationFramebuffer) 377 | .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) 378 | 379 | .vertexAttribPointer(this.particleVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 380 | 381 | .useProgram(this.markProgram) 382 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 383 | .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth) 384 | .uniformTexture('u_positionTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture); 385 | 386 | wgl.drawArrays(markDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight); 387 | 388 | //////////////////////////////////////////////////// 389 | // save our original velocity grid 390 | 391 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.originalVelocityTexture, 0); 392 | 393 | var copyDrawState = wgl.createDrawState() 394 | .bindFramebuffer(this.simulationFramebuffer) 395 | .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) 396 | 397 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 398 | 399 | .useProgram(this.copyProgram) 400 | .uniformTexture('u_texture', 0, wgl.TEXTURE_2D, this.velocityTexture) 401 | 402 | wgl.drawArrays(copyDrawState, wgl.TRIANGLE_STRIP, 0, 4); 403 | 404 | 405 | ///////////////////////////////////////////////////// 406 | // add forces to velocity grid 407 | 408 | 409 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); 410 | 411 | var addForceDrawState = wgl.createDrawState() 412 | .bindFramebuffer(this.simulationFramebuffer) 413 | .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) 414 | 415 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 416 | 417 | .useProgram(this.addForceProgram) 418 | .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture) 419 | 420 | .uniform1f('u_timeStep', timeStep) 421 | 422 | .uniform3f('u_mouseVelocity', mouseVelocity[0], mouseVelocity[1], mouseVelocity[2]) 423 | 424 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 425 | .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth) 426 | 427 | .uniform3f('u_mouseRayOrigin', mouseRayOrigin[0], mouseRayOrigin[1], mouseRayOrigin[2]) 428 | .uniform3f('u_mouseRayDirection', mouseRayDirection[0], mouseRayDirection[1], mouseRayDirection[2]) 429 | 430 | 431 | wgl.drawArrays(addForceDrawState, wgl.TRIANGLE_STRIP, 0, 4); 432 | 433 | swap(this, 'velocityTexture', 'tempVelocityTexture'); 434 | 435 | 436 | ///////////////////////////////////////////////////// 437 | // enforce boundary velocity conditions 438 | 439 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); 440 | 441 | var enforceBoundariesDrawState = wgl.createDrawState() 442 | .bindFramebuffer(this.simulationFramebuffer) 443 | .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) 444 | 445 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 446 | 447 | .useProgram(this.enforceBoundariesProgram) 448 | .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture) 449 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ); 450 | 451 | wgl.drawArrays(enforceBoundariesDrawState, wgl.TRIANGLE_STRIP, 0, 4); 452 | 453 | swap(this, 'velocityTexture', 'tempVelocityTexture'); 454 | 455 | 456 | ///////////////////////////////////////////////////// 457 | // update velocityTexture for non divergence 458 | 459 | 460 | //compute divergence for pressure projection 461 | 462 | var divergenceDrawState = wgl.createDrawState() 463 | 464 | .bindFramebuffer(this.simulationFramebuffer) 465 | .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) 466 | 467 | .useProgram(this.divergenceProgram) 468 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 469 | .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture) 470 | .uniformTexture('u_markerTexture', 1, wgl.TEXTURE_2D, this.markerTexture) 471 | .uniformTexture('u_weightTexture', 2, wgl.TEXTURE_2D, this.weightTexture) 472 | 473 | .uniform1f('u_maxDensity', this.particleDensity) 474 | 475 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) 476 | 477 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.divergenceTexture, 0); 478 | wgl.clear( 479 | wgl.createClearState().bindFramebuffer(this.simulationFramebuffer), 480 | wgl.COLOR_BUFFER_BIT); 481 | 482 | wgl.drawArrays(divergenceDrawState, wgl.TRIANGLE_STRIP, 0, 4); 483 | 484 | 485 | //compute pressure via jacobi iteration 486 | 487 | var jacobiDrawState = wgl.createDrawState() 488 | .bindFramebuffer(this.simulationFramebuffer) 489 | .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) 490 | 491 | .useProgram(this.jacobiProgram) 492 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 493 | .uniformTexture('u_divergenceTexture', 1, wgl.TEXTURE_2D, this.divergenceTexture) 494 | .uniformTexture('u_markerTexture', 2, wgl.TEXTURE_2D, this.markerTexture) 495 | 496 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) 497 | 498 | 499 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.pressureTexture, 0); 500 | wgl.clear( 501 | wgl.createClearState().bindFramebuffer(this.simulationFramebuffer), 502 | wgl.COLOR_BUFFER_BIT); 503 | 504 | var PRESSURE_JACOBI_ITERATIONS = 50; 505 | for (var i = 0; i < PRESSURE_JACOBI_ITERATIONS; ++i) { 506 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempSimulationTexture, 0); 507 | jacobiDrawState.uniformTexture('u_pressureTexture', 0, wgl.TEXTURE_2D, this.pressureTexture); 508 | 509 | wgl.drawArrays(jacobiDrawState, wgl.TRIANGLE_STRIP, 0, 4); 510 | 511 | swap(this, 'pressureTexture', 'tempSimulationTexture'); 512 | } 513 | 514 | 515 | //subtract pressure gradient from velocity 516 | 517 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); 518 | 519 | var subtractDrawState = wgl.createDrawState() 520 | .bindFramebuffer(this.simulationFramebuffer) 521 | .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) 522 | 523 | .useProgram(this.subtractProgram) 524 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 525 | .uniformTexture('u_pressureTexture', 0, wgl.TEXTURE_2D, this.pressureTexture) 526 | .uniformTexture('u_velocityTexture', 1, wgl.TEXTURE_2D, this.velocityTexture) 527 | .uniformTexture('u_markerTexture', 2, wgl.TEXTURE_2D, this.markerTexture) 528 | 529 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) 530 | 531 | wgl.drawArrays(subtractDrawState, wgl.TRIANGLE_STRIP, 0, 4); 532 | 533 | swap(this, 'velocityTexture', 'tempVelocityTexture'); 534 | 535 | ///////////////////////////////////////////////////////////// 536 | // transfer velocities back to particles 537 | 538 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.particleVelocityTextureTemp, 0); 539 | 540 | var transferToParticlesDrawState = wgl.createDrawState() 541 | .bindFramebuffer(this.simulationFramebuffer) 542 | .viewport(0, 0, this.particlesWidth, this.particlesHeight) 543 | 544 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 545 | 546 | .useProgram(this.transferToParticlesProgram) 547 | .uniformTexture('u_particlePositionTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture) 548 | .uniformTexture('u_particleVelocityTexture', 1, wgl.TEXTURE_2D, this.particleVelocityTexture) 549 | .uniformTexture('u_gridVelocityTexture', 2, wgl.TEXTURE_2D, this.velocityTexture) 550 | .uniformTexture('u_originalGridVelocityTexture', 3, wgl.TEXTURE_2D, this.originalVelocityTexture) 551 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 552 | .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth) 553 | 554 | .uniform1f('u_flipness', this.flipness) 555 | 556 | wgl.drawArrays(transferToParticlesDrawState, wgl.TRIANGLE_STRIP, 0, 4); 557 | 558 | swap(this, 'particleVelocityTextureTemp', 'particleVelocityTexture'); 559 | 560 | /////////////////////////////////////////////// 561 | // advect particle positions with velocity grid using RK2 562 | 563 | 564 | wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.particlePositionTextureTemp, 0); 565 | wgl.clear( 566 | wgl.createClearState().bindFramebuffer(this.simulationFramebuffer), 567 | wgl.COLOR_BUFFER_BIT); 568 | 569 | var advectDrawState = wgl.createDrawState() 570 | .bindFramebuffer(this.simulationFramebuffer) 571 | .viewport(0, 0, this.particlesWidth, this.particlesHeight) 572 | 573 | .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) 574 | 575 | .useProgram(this.advectProgram) 576 | .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture) 577 | .uniformTexture('u_randomsTexture', 1, wgl.TEXTURE_2D, this.particleRandomTexture) 578 | .uniformTexture('u_velocityGrid', 2, wgl.TEXTURE_2D, this.velocityTexture) 579 | .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) 580 | .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth) 581 | .uniform1f('u_timeStep', timeStep) 582 | .uniform1f('u_frameNumber', this.frameNumber) 583 | .uniform2f('u_particlesResolution', this.particlesWidth, this.particlesHeight); 584 | 585 | wgl.drawArrays(advectDrawState, wgl.TRIANGLE_STRIP, 0, 4); 586 | 587 | swap(this, 'particlePositionTextureTemp', 'particlePositionTexture'); 588 | } 589 | 590 | return Simulator; 591 | }()); 592 | -------------------------------------------------------------------------------- /boxeditor.js: -------------------------------------------------------------------------------- 1 | var BoxEditor = (function () { 2 | 3 | //min and max are both number[3] 4 | function AABB (min, max) { 5 | this.min = [min[0], min[1], min[2]]; 6 | this.max = [max[0], max[1], max[2]]; 7 | } 8 | 9 | AABB.prototype.computeVolume = function () { 10 | var volume = 1; 11 | for (var i = 0; i < 3; ++i) { 12 | volume *= (this.max[i] - this.min[i]); 13 | } 14 | return volume; 15 | } 16 | 17 | AABB.prototype.computeSurfaceArea = function () { 18 | var width = this.max[0] - this.min[0]; 19 | var height = this.max[1] - this.min[1]; 20 | var depth = this.max[2] - this.min[2]; 21 | 22 | return 2 * (width * height + width * depth + height * depth); 23 | } 24 | 25 | //returns new AABB with the same min and max (but not the same array references) 26 | AABB.prototype.clone = function () { 27 | return new AABB( 28 | [this.min[0], this.min[1], this.min[2]], 29 | [this.max[0], this.max[1], this.max[2]] 30 | ); 31 | } 32 | 33 | AABB.prototype.randomPoint = function () { //random point in this AABB 34 | var point = []; 35 | for (var i = 0; i < 3; ++i) { 36 | point[i] = this.min[i] + Math.random() * (this.max[i] - this.min[i]); 37 | } 38 | return point; 39 | } 40 | 41 | var InteractionMode = { 42 | RESIZING: 0, 43 | TRANSLATING: 1, 44 | 45 | DRAWING: 2, //whilst we're drawing a rectangle on a plane 46 | EXTRUDING: 3 //whilst we're extruding that rectangle into a box 47 | }; 48 | 49 | var STEP = 1.0; 50 | 51 | 52 | function exclusiveAABBOverlap (a, b) { 53 | return a.min[0] < b.max[0] && a.max[0] > b.min[0] && 54 | a.min[1] < b.max[1] && a.max[1] > b.min[1] && 55 | a.min[2] < b.max[2] && a.max[2] > b.min[2]; 56 | } 57 | 58 | function inclusiveAABBOverlap (a, b) { 59 | return a.min[0] <= b.max[0] && a.max[0] >= b.min[0] && 60 | a.min[1] <= b.max[1] && a.max[1] >= b.min[1] && 61 | a.min[2] <= b.max[2] && a.max[2] >= b.min[2]; 62 | } 63 | 64 | 65 | /* 66 | if there is an intersection then this returns: 67 | { 68 | aabb: aabb, 69 | t: distance to intersection, 70 | 71 | point: point of intersection, 72 | 73 | //axis and side together define the plane of intersection (+x, -x, etc) 74 | axis: 0, 1 or 2 depending on x, y or z, 75 | side: -1 or 1 depending on which side the intersection happened on 76 | } 77 | 78 | 79 | otherwise it returns null 80 | */ 81 | 82 | function rayAABBIntersection (rayOrigin, rayDirection, aabb) { 83 | //we see it as a series of clippings in t of the line in the AABB planes along each axis 84 | //the part we are left with after clipping if successful is the region of the line within the AABB and thus we can extract the intersection 85 | 86 | //the part of the line we have clipped so far 87 | var lowT = -Infinity; 88 | var highT = Infinity; 89 | 90 | var intersectionAxis = 0; 91 | 92 | for (var i = 0; i < 3; ++i) { 93 | var t1 = (aabb.min[i] - rayOrigin[i]) / rayDirection[i]; 94 | var t2 = (aabb.max[i] - rayOrigin[i]) / rayDirection[i]; 95 | //so between t1 and t2 we are within the aabb planes in this dimension 96 | 97 | //ensure t1 < t2 (swap if necessary) 98 | if (t1 > t2) { 99 | var temp = t1; 100 | t1 = t2; 101 | t2 = temp; 102 | } 103 | 104 | //t1 and t2 now hold the lower and upper intersection t's respectively 105 | 106 | //the part of the line we just clipped for does not overlap the part previously clipped and thus there is no intersection 107 | if (t2 < lowT || t1 > highT) return null; 108 | 109 | //further clip the line between the planes in this axis 110 | if (t1 > lowT) { 111 | lowT = t1; 112 | 113 | intersectionAxis = i; //if we needed to futher clip in this axis then this is the closest intersection axis 114 | } 115 | 116 | if (t2 < highT) highT = t2; 117 | } 118 | 119 | if (lowT > highT) return null; 120 | 121 | //if we've reached this far then there is an intersection 122 | 123 | var intersection = []; 124 | for (var i = 0; i < 3; ++i) { 125 | intersection[i] = rayOrigin[i] + rayDirection[i] * lowT; 126 | } 127 | 128 | 129 | return { 130 | aabb: aabb, 131 | t: lowT, 132 | axis: intersectionAxis, 133 | side: rayDirection[intersectionAxis] > 0 ? -1 : 1, 134 | point: intersection 135 | }; 136 | } 137 | 138 | //finds the closest points between the line1 and line2 139 | //returns [closest point on line1, closest point on line2] 140 | function closestPointsOnLines (line1Origin, line1Direction, line2Origin, line2Direction) { 141 | var w0 = Utilities.subtractVectors([], line1Origin, line2Origin); 142 | 143 | var a = Utilities.dotVectors(line1Direction, line1Direction); 144 | var b = Utilities.dotVectors(line1Direction, line2Direction); 145 | var c = Utilities.dotVectors(line2Direction, line2Direction); 146 | var d = Utilities.dotVectors(line1Direction, w0); 147 | var e = Utilities.dotVectors(line2Direction, w0); 148 | 149 | 150 | var t1 = (b * e - c * d) / (a * c - b * b); 151 | var t2 = (a * e - b * d) / (a * c - b * b); 152 | 153 | return [ 154 | Utilities.addVectors([], line1Origin, Utilities.multiplyVectorByScalar([], line1Direction, t1)), 155 | Utilities.addVectors([], line2Origin, Utilities.multiplyVectorByScalar([], line2Direction, t2)) 156 | ]; 157 | } 158 | 159 | //this defines the bounds of our editing space 160 | //the grid starts at (0, 0, 0) 161 | //gridSize is [width, height, depth] 162 | //onChange is a callback that gets called anytime a box gets edited 163 | function BoxEditor (canvas, wgl, projectionMatrix, camera, gridSize, onLoaded, onChange) { 164 | this.canvas = canvas; 165 | 166 | this.wgl = wgl; 167 | 168 | this.gridWidth = gridSize[0]; 169 | this.gridHeight = gridSize[1]; 170 | this.gridDepth = gridSize[2]; 171 | this.gridDimensions = [this.gridWidth, this.gridHeight, this.gridDepth]; 172 | 173 | this.projectionMatrix = projectionMatrix; 174 | this.camera = camera; 175 | 176 | this.onChange = onChange; 177 | 178 | //the cube geometry is a 1x1 cube with the origin at the bottom left corner 179 | 180 | this.cubeVertexBuffer = wgl.createBuffer(); 181 | wgl.bufferData(this.cubeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([ 182 | // Front face 183 | 0.0, 0.0, 1.0, 184 | 1.0, 0.0, 1.0, 185 | 1.0, 1.0, 1.0, 186 | 0.0, 1.0, 1.0, 187 | 188 | // Back face 189 | 0.0, 0.0, 0.0, 190 | 0.0, 1.0, 0.0, 191 | 1.0, 1.0, 0.0, 192 | 1.0, 0.0, 0.0, 193 | 194 | // Top face 195 | 0.0, 1.0, 0.0, 196 | 0.0, 1.0, 1.0, 197 | 1.0, 1.0, 1.0, 198 | 1.0, 1.0, 0.0, 199 | 200 | // Bottom face 201 | 0.0, 0.0, 0.0, 202 | 1.0, 0.0, 0.0, 203 | 1.0, 0.0, 1.0, 204 | 0.0, 0.0, 1.0, 205 | 206 | // Right face 207 | 1.0, 0.0, 0.0, 208 | 1.0, 1.0, 0.0, 209 | 1.0, 1.0, 1.0, 210 | 1.0, 0.0, 1.0, 211 | 212 | // Left face 213 | 0.0, 0.0, 0.0, 214 | 0.0, 0.0, 1.0, 215 | 0.0, 1.0, 1.0, 216 | 0.0, 1.0, 0.0 217 | ]), wgl.STATIC_DRAW); 218 | 219 | 220 | 221 | this.cubeIndexBuffer = wgl.createBuffer(); 222 | wgl.bufferData(this.cubeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ 223 | 0, 1, 2, 0, 2, 3, // front 224 | 4, 5, 6, 4, 6, 7, // back 225 | 8, 9, 10, 8, 10, 11, // top 226 | 12, 13, 14, 12, 14, 15, // bottom 227 | 16, 17, 18, 16, 18, 19, // right 228 | 20, 21, 22, 20, 22, 23 // left 229 | ]), wgl.STATIC_DRAW); 230 | 231 | 232 | this.cubeWireframeVertexBuffer = wgl.createBuffer(); 233 | wgl.bufferData(this.cubeWireframeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([ 234 | 0.0, 0.0, 0.0, 235 | 1.0, 0.0, 0.0, 236 | 1.0, 1.0, 0.0, 237 | 0.0, 1.0, 0.0, 238 | 239 | 0.0, 0.0, 1.0, 240 | 1.0, 0.0, 1.0, 241 | 1.0, 1.0, 1.0, 242 | 0.0, 1.0, 1.0]), wgl.STATIC_DRAW); 243 | 244 | this.cubeWireframeIndexBuffer = wgl.createBuffer(); 245 | wgl.bufferData(this.cubeWireframeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ 246 | 0, 1, 1, 2, 2, 3, 3, 0, 247 | 4, 5, 5, 6, 6, 7, 7, 4, 248 | 0, 4, 1, 5, 2, 6, 3, 7 249 | ]), wgl.STATIC_DRAW); 250 | 251 | 252 | //there's one grid vertex buffer for the planes normal to each axis 253 | this.gridVertexBuffers = []; 254 | 255 | for (var axis = 0; axis < 3; ++axis) { 256 | this.gridVertexBuffers[axis] = wgl.createBuffer(); 257 | 258 | var vertexData = []; 259 | 260 | 261 | var points; //the points that make up this grid plane 262 | 263 | if (axis === 0) { 264 | 265 | points = [ 266 | [0, 0, 0], 267 | [0, this.gridHeight, 0], 268 | [0, this.gridHeight, this.gridDepth], 269 | [0, 0, this.gridDepth] 270 | ]; 271 | 272 | } else if (axis === 1) { 273 | points = [ 274 | [0, 0, 0], 275 | [this.gridWidth, 0, 0], 276 | [this.gridWidth, 0, this.gridDepth], 277 | [0, 0, this.gridDepth] 278 | ]; 279 | } else if (axis === 2) { 280 | 281 | points = [ 282 | [0, 0, 0], 283 | [this.gridWidth, 0, 0], 284 | [this.gridWidth, this.gridHeight, 0], 285 | [0, this.gridHeight, 0] 286 | ]; 287 | } 288 | 289 | 290 | for (var i = 0; i < 4; ++i) { 291 | vertexData.push(points[i][0]); 292 | vertexData.push(points[i][1]); 293 | vertexData.push(points[i][2]); 294 | 295 | vertexData.push(points[(i + 1) % 4][0]); 296 | vertexData.push(points[(i + 1) % 4][1]); 297 | vertexData.push(points[(i + 1) % 4][2]); 298 | } 299 | 300 | 301 | wgl.bufferData(this.gridVertexBuffers[axis], wgl.ARRAY_BUFFER, new Float32Array(vertexData), wgl.STATIC_DRAW); 302 | } 303 | 304 | this.pointVertexBuffer = wgl.createBuffer(); 305 | wgl.bufferData(this.pointVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0]), wgl.STATIC_DRAW); 306 | 307 | 308 | this.quadVertexBuffer = wgl.createBuffer(); 309 | wgl.bufferData(this.quadVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), wgl.STATIC_DRAW); 310 | 311 | 312 | ///////////////////////////////////////////////// 313 | // box state 314 | 315 | this.boxes = []; 316 | 317 | 318 | //////////////////////////////////////////////// 319 | // interaction stuff 320 | 321 | //mouse x and y are in [-1, 1] (clip space) 322 | this.mouseX = 999; 323 | this.mouseY = 999; 324 | 325 | this.keyPressed = []; //an array of booleans that maps a key code to whether or not it's pressed 326 | for (var i = 0; i < 256; ++i) { 327 | this.keyPressed[i] = false; 328 | } 329 | 330 | /* 331 | interactions: 332 | click on a plane and hold down to begin drawing 333 | when mouse is released we enter extrusion mode for new box 334 | click again to create box 335 | 336 | click and drag on side of boxes to resize 337 | 338 | click and drag on side of boxes whilst holding shift to move 339 | 340 | 341 | //while we're not interacting, this is null 342 | //while we are interacting this contains an object 343 | /* 344 | 345 | { 346 | mode: the interaction mode, 347 | 348 | during resizing or translating or extrusion: 349 | box: box we're currently manipulating, 350 | axis: axis of plane we're manipulating: 0, 1 or 2 351 | side: side of plane we're manipulating: -1 or 1 352 | point: the point at which the interaction started 353 | 354 | 355 | during translation we also have: 356 | startMax: the starting max along the interaction axis 357 | startMin: the starting min along the interaction axis 358 | 359 | 360 | during drawing 361 | box: box we're currently drawing 362 | point: the point at which we started drawing 363 | axis: the axis of the plane which we're drawing on 364 | side: the side of the plane which we're drawin on 365 | 366 | } 367 | */ 368 | this.interactionState = null; 369 | 370 | 371 | /////////////////////////////////// 372 | // load programs 373 | 374 | 375 | wgl.createProgramsFromFiles({ 376 | backgroundProgram: { 377 | vertexShader: 'shaders/background.vert', 378 | fragmentShader: 'shaders/background.frag' 379 | }, 380 | boxProgram: { 381 | vertexShader: 'shaders/box.vert', 382 | fragmentShader: 'shaders/box.frag' 383 | }, 384 | boxWireframeProgram: { 385 | vertexShader: 'shaders/boxwireframe.vert', 386 | fragmentShader: 'shaders/boxwireframe.frag' 387 | }, 388 | gridProgram: { 389 | vertexShader: 'shaders/grid.vert', 390 | fragmentShader: 'shaders/grid.frag' 391 | }, 392 | pointProgram: { 393 | vertexShader: 'shaders/point.vert', 394 | fragmentShader: 'shaders/point.frag' 395 | } 396 | }, (function (programs) { 397 | for (var programName in programs) { 398 | this[programName] = programs[programName]; 399 | } 400 | 401 | onLoaded(); 402 | }).bind(this)); 403 | } 404 | 405 | function quantize (x, step) { 406 | return Math.round(x / step) * step; 407 | } 408 | 409 | function quantizeVector (v, step) { 410 | for (var i = 0; i < v.length; ++i) { 411 | v[i] = quantize(v[i], step); 412 | } 413 | 414 | return v; 415 | } 416 | 417 | BoxEditor.prototype.onKeyDown = function (event) { 418 | this.keyPressed[event.keyCode] = true; 419 | } 420 | 421 | BoxEditor.prototype.onKeyUp = function (event) { 422 | this.keyPressed[event.keyCode] = false; 423 | } 424 | 425 | BoxEditor.prototype.onMouseMove = function (event) { 426 | event.preventDefault(); 427 | 428 | var position = Utilities.getMousePosition(event, this.canvas); 429 | var normalizedX = position.x / this.canvas.width; 430 | var normalizedY = position.y / this.canvas.height; 431 | 432 | this.mouseX = normalizedX * 2.0 - 1.0; 433 | this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0; 434 | 435 | 436 | 437 | if (this.interactionState !== null) { 438 | this.onChange(); 439 | 440 | if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) { 441 | var mouseRay = this.getMouseRay(); 442 | 443 | //so when we are dragging to make a box bigger or smaller, what we do is we extend a line out from the intersection point normal to the plane 444 | 445 | var dragLineOrigin = this.interactionState.point; 446 | var dragLineDirection = [0, 0, 0]; 447 | dragLineDirection[this.interactionState.axis] = 1.0; 448 | 449 | //then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged' 450 | var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction); 451 | var newCoordinate = closestPoints[0][this.interactionState.axis]; //the new coordinate for this box plane 452 | newCoordinate = quantize(newCoordinate, STEP); 453 | 454 | var box = this.interactionState.box, 455 | side = this.interactionState.side, 456 | axis = this.interactionState.axis; 457 | 458 | //resize the box, clamping it to itself and the overall grid 459 | if (side === -1) { 460 | box.min[axis] = Math.max(Math.min(newCoordinate, box.max[axis]), 0); 461 | } else if (side === 1) { 462 | box.max[axis] = Math.min(Math.max(newCoordinate, box.min[axis]), this.gridDimensions[axis]); 463 | } 464 | 465 | //collision detection 466 | for (var i = 0; i < this.boxes.length; ++i) { 467 | var otherBox = this.boxes[i]; 468 | if (box !== otherBox) { //don't collide with self 469 | if (exclusiveAABBOverlap(box, otherBox)) { 470 | 471 | //resolve collision 472 | if (side === -1) { 473 | box.min[axis] = otherBox.max[axis]; 474 | } else if (side === 1) { 475 | box.max[axis] = otherBox.min[axis]; 476 | } 477 | } 478 | } 479 | } 480 | 481 | } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { 482 | 483 | var mouseRay = this.getMouseRay(); 484 | 485 | //so when we are translating a box, what we do is we extend a line out from the intersection point normal to the plane 486 | 487 | var dragLineOrigin = this.interactionState.point; 488 | var dragLineDirection = [0, 0, 0]; 489 | dragLineDirection[this.interactionState.axis] = 1.0; 490 | 491 | //then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged' 492 | var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction); 493 | var newCoordinate = closestPoints[0][this.interactionState.axis]; //the new coordinate for this box plane 494 | newCoordinate = quantize(newCoordinate, STEP); 495 | 496 | var box = this.interactionState.box, 497 | side = this.interactionState.side, 498 | axis = this.interactionState.axis; 499 | 500 | 501 | var length = this.interactionState.startMax - this.interactionState.startMin; //the length of the box along the translation axis 502 | 503 | if (side === -1) { 504 | box.min[axis] = newCoordinate; 505 | box.max[axis] = newCoordinate + length; 506 | } else if (side === 1) { 507 | box.max[axis] = newCoordinate; 508 | box.min[axis] = newCoordinate - length; 509 | } 510 | 511 | //clamp to boundaries 512 | if (box.min[axis] < 0) { 513 | box.min[axis] = 0; 514 | box.max[axis] = length; 515 | } 516 | 517 | if (box.max[axis] > this.gridDimensions[axis]) { 518 | box.max[axis] = this.gridDimensions[axis]; 519 | box.min[axis] = this.gridDimensions[axis] - length; 520 | } 521 | 522 | 523 | var translationDirection = 0; //is either -1 or 1 depending on which way we're pushing our box 524 | //how we resolve collisions depends on our translation direction 525 | if (side === -1) { 526 | translationDirection = newCoordinate < this.interactionState.startMin ? -1 : 1; 527 | } else if (side === 1) { 528 | translationDirection = newCoordinate < this.interactionState.startMax ? -1 : 1; 529 | } 530 | 531 | 532 | var sweptBox = box.clone(); //we sweep out translating AABB for collision detection to prevent ghosting through boxes 533 | //reset swept box to original box location before translation 534 | sweptBox.min[axis] = this.interactionState.startMin; 535 | sweptBox.max[axis] = this.interactionState.startMax; 536 | 537 | //sweep out the correct plane to where it has been translated to 538 | if (translationDirection === 1) { 539 | sweptBox.max[axis] = box.max[axis]; 540 | } else if (translationDirection === -1) { 541 | sweptBox.min[axis] = box.min[axis]; 542 | } 543 | 544 | //collision detection 545 | for (var i = 0; i < this.boxes.length; ++i) { 546 | var otherBox = this.boxes[i]; 547 | if (box !== otherBox) { //don't collide with self 548 | if (exclusiveAABBOverlap(sweptBox, otherBox)) { 549 | 550 | //resolve collision 551 | if (translationDirection === -1) { 552 | box.min[axis] = otherBox.max[axis]; 553 | box.max[axis] = otherBox.max[axis] + length; 554 | } else if (translationDirection === 1) { 555 | box.max[axis] = otherBox.min[axis]; 556 | box.min[axis] = otherBox.min[axis] - length; 557 | } 558 | } 559 | } 560 | } 561 | 562 | } else if (this.interactionState.mode === InteractionMode.DRAWING) { 563 | 564 | var mouseRay = this.getMouseRay(); 565 | 566 | //get the mouse ray intersection with the drawing plane 567 | 568 | var axis = this.interactionState.axis, 569 | side = this.interactionState.side, 570 | startPoint = this.interactionState.point; 571 | 572 | var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; 573 | var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis]; 574 | 575 | if (t > 0) { //if the mouse ray misses the drawing plane then the box just stays the same size as it was before 576 | 577 | var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)); 578 | quantizeVector(intersection, STEP); 579 | 580 | for (var i = 0; i < 3; ++i) { 581 | intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]); 582 | intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]); 583 | } 584 | 585 | var min = [Math.min(startPoint[0], intersection[0]), Math.min(startPoint[1], intersection[1]), Math.min(startPoint[2], intersection[2])]; 586 | var max = [Math.max(startPoint[0], intersection[0]), Math.max(startPoint[1], intersection[1]), Math.max(startPoint[2], intersection[2])]; 587 | 588 | 589 | var box = this.interactionState.box; 590 | 591 | var sweptBox = new AABB(min, max); //we sweep the box a bit into the grid to make sure it collides along the plane axis 592 | if (this.interactionState.side === -1) { 593 | sweptBox.max[this.interactionState.axis] = STEP * 0.1; 594 | } else { 595 | sweptBox.min[this.interactionState.axis] = this.gridDimensions[this.interactionState.axis] - STEP * 0.1; 596 | 597 | } 598 | 599 | //collision detection 600 | for (var i = 0; i < this.boxes.length; ++i) { 601 | var otherBox = this.boxes[i]; 602 | 603 | if (box !== otherBox) { //don't collide with self 604 | if (exclusiveAABBOverlap(sweptBox, otherBox)) { 605 | 606 | //we resolve along the axis with the smaller overlap and where the start point doesn't already overlap the other box in that axis 607 | var smallestOverlap = 99999999; 608 | var smallestOverlapAxis = -1; 609 | 610 | for (var axis = 0; axis < 3; ++axis) { 611 | if (axis !== this.interactionState.axis) { //only resolve collisions in the drawing plane 612 | var overlap = Math.min(max[axis], otherBox.max[axis]) - Math.max(min[axis], otherBox.min[axis]); 613 | 614 | if (overlap > 0 && overlap < smallestOverlap && (startPoint[axis] < otherBox.min[axis] || startPoint[axis] > otherBox.max[axis])) { 615 | smallestOverlap = overlap; 616 | smallestOverlapAxis = axis; 617 | } 618 | } 619 | } 620 | 621 | if (intersection[smallestOverlapAxis] > startPoint[smallestOverlapAxis]) { //if we're resizing in the positive direction 622 | max[smallestOverlapAxis] = otherBox.min[smallestOverlapAxis]; 623 | } else { //if we're resizing in the negative direction 624 | min[smallestOverlapAxis] = otherBox.max[smallestOverlapAxis]; 625 | } 626 | } 627 | } 628 | } 629 | 630 | this.interactionState.box.min = min; 631 | this.interactionState.box.max = max; 632 | 633 | } 634 | } 635 | } 636 | 637 | this.camera.onMouseMove(event); 638 | } 639 | 640 | //returns the closest box intersection data (same as rayAABBIntersection) for the given ray 641 | //if there is no intersection it returns null 642 | BoxEditor.prototype.getBoxIntersection = function (rayOrigin, rayDirection) { 643 | //find the closest box that this collides with 644 | 645 | var bestIntersectionSoFar = { 646 | aabb: null, 647 | t: Infinity 648 | } 649 | 650 | for (var i = 0; i < this.boxes.length; ++i) { 651 | var box = this.boxes[i]; 652 | 653 | var intersection = rayAABBIntersection(rayOrigin, rayDirection, box); 654 | 655 | if (intersection !== null) { //if there is an intersection 656 | if (intersection.t < bestIntersectionSoFar.t) { //if this is closer than the best we've seen so far 657 | bestIntersectionSoFar = intersection; 658 | } 659 | } 660 | } 661 | 662 | if (bestIntersectionSoFar.aabb === null) { //if we didn't intersect any boxes 663 | return null; 664 | } else { 665 | return bestIntersectionSoFar; 666 | } 667 | } 668 | 669 | //tests for intersection with one of the bounding planes 670 | /* 671 | if there is an intersection returns 672 | {axis, side, point} 673 | otherwise, returns null 674 | */ 675 | BoxEditor.prototype.getBoundingPlaneIntersection = function (rayOrigin, rayDirection) { 676 | //we try to intersect with the two planes on each axis in turn (as long as they are facing towards the camera) 677 | //we assume we could only ever intersect with one of the planes so we break out as soon as we've found something 678 | 679 | for (var axis = 0; axis < 3; ++axis) { 680 | 681 | //now let's try intersecting with each side in turn 682 | for (var side = -1; side <= 1; side += 2) { //goes between -1 and 1 (hackish! 683 | 684 | //first let's make sure the plane is front facing to the ray 685 | var frontFacing = side === -1 ? rayDirection[axis] < 0 : rayDirection[axis] > 0; 686 | if (frontFacing) { 687 | var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; //the coordinate of the plane along this axis 688 | 689 | var t = (planeCoordinate - rayOrigin[axis]) / rayDirection[axis]; 690 | 691 | 692 | if (t > 0) { 693 | var intersection = Utilities.addVectors([], rayOrigin, Utilities.multiplyVectorByScalar([], rayDirection, t)); 694 | 695 | //if we're still within the bounds of the grid 696 | if (intersection[0] >= 0.0 && intersection[0] <= this.gridDimensions[0] && 697 | intersection[1] >= 0.0 && intersection[1] <= this.gridDimensions[1] && 698 | intersection[2] >= 0.0 && intersection[2] <= this.gridDimensions[2]) { 699 | 700 | return { 701 | axis: axis, 702 | side: side, 703 | point: intersection 704 | } 705 | } 706 | } 707 | } 708 | } 709 | } 710 | 711 | return null; //no intersection found 712 | } 713 | 714 | 715 | BoxEditor.prototype.onMouseDown = function (event) { 716 | event.preventDefault(); 717 | 718 | this.onMouseMove(event); 719 | 720 | if (!this.keyPressed[32]) { //if space isn't held down 721 | 722 | //we've finished extruding a box 723 | if (this.interactionState !== null && this.interactionState.mode === InteractionMode.EXTRUDING) { 724 | //delete zero volume boxes 725 | if (this.interactionState.box.computeVolume() === 0) { 726 | this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1); 727 | } 728 | this.interactionState = null; 729 | 730 | this.onChange(); 731 | 732 | return; 733 | } else { 734 | 735 | var mouseRay = this.getMouseRay(); 736 | 737 | //find the closest box that this collides with 738 | 739 | var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction); 740 | 741 | 742 | //if we've intersected at least one box then let's start manipulating that box 743 | if (boxIntersection !== null) { 744 | var intersection = boxIntersection; 745 | 746 | if (this.keyPressed[16]) { //if we're holding shift we start to translate 747 | this.interactionState = { 748 | mode: InteractionMode.TRANSLATING, 749 | box: intersection.aabb, 750 | axis: intersection.axis, 751 | side: intersection.side, 752 | point: intersection.point, 753 | 754 | startMax: intersection.aabb.max[intersection.axis], 755 | startMin: intersection.aabb.min[intersection.axis] 756 | }; 757 | } else { //otherwise we start resizing 758 | 759 | this.interactionState = { 760 | mode: InteractionMode.RESIZING, 761 | box: intersection.aabb, 762 | axis: intersection.axis, 763 | side: intersection.side, 764 | point: intersection.point 765 | }; 766 | } 767 | } 768 | 769 | 770 | //if we've not intersected any box then let's see if we should start the box creation process 771 | if (boxIntersection === null) { 772 | var mouseRay = this.getMouseRay(); 773 | 774 | var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction); 775 | 776 | if (planeIntersection !== null) { //if we've hit one of the planes 777 | //go into drawing mode 778 | 779 | var point = planeIntersection.point; 780 | point[0] = quantize(point[0], STEP); 781 | point[1] = quantize(point[1], STEP); 782 | point[2] = quantize(point[2], STEP); 783 | 784 | var newBox = new AABB(point, point); 785 | this.boxes.push(newBox); 786 | 787 | this.interactionState = { 788 | mode: InteractionMode.DRAWING, 789 | box: newBox, 790 | axis: planeIntersection.axis, 791 | side: planeIntersection.side, 792 | point: planeIntersection.point 793 | }; 794 | } 795 | 796 | this.onChange(); 797 | } 798 | 799 | } 800 | 801 | } 802 | 803 | if (this.interactionState === null) { 804 | this.camera.onMouseDown(event); 805 | } 806 | 807 | } 808 | 809 | BoxEditor.prototype.onMouseUp = function (event) { 810 | event.preventDefault(); 811 | 812 | if (this.interactionState !== null) { 813 | if (this.interactionState.mode === InteractionMode.RESIZING) { //the end of a resize 814 | //if we've resized to zero volume then we delete the box 815 | if (this.interactionState.box.computeVolume() === 0) { 816 | this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1); 817 | } 818 | 819 | this.interactionState = null; 820 | 821 | } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { //the end of a translate 822 | this.interactionState = null; 823 | } else if (this.interactionState.mode === InteractionMode.DRAWING) { //the end of a draw 824 | //TODO: DRY this 825 | 826 | if (this.interactionState.box.computeSurfaceArea() > 0) { //make sure we have something to extrude 827 | 828 | var mouseRay = this.getMouseRay(); 829 | 830 | var axis = this.interactionState.axis, 831 | side = this.interactionState.side, 832 | startPoint = this.interactionState.point; 833 | 834 | var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; 835 | var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis]; 836 | 837 | var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)); 838 | quantizeVector(intersection, STEP); 839 | 840 | //clamp extrusion point to grid and to box 841 | for (var i = 0; i < 3; ++i) { 842 | intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]); 843 | intersection[i] = Utilities.clamp(intersection[i], this.interactionState.box.min[i], this.interactionState.box.max[i]); 844 | } 845 | 846 | 847 | //go into extrusion mode 848 | this.interactionState = { 849 | mode: InteractionMode.EXTRUDING, 850 | box: this.interactionState.box, 851 | axis: this.interactionState.axis, 852 | side: this.interactionState.side * -1, 853 | point: intersection 854 | }; 855 | 856 | } else { //otherwise delete the box we were editing and go straight back into regular mode 857 | this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1); 858 | this.interactionState = null; 859 | } 860 | } 861 | 862 | this.onChange(); 863 | } 864 | 865 | 866 | if (this.interactionState === null) { 867 | this.camera.onMouseUp(event); 868 | } 869 | } 870 | 871 | 872 | //returns an object 873 | /* 874 | { 875 | origin: [x, y, z], 876 | direction: [x, y, z] //normalized 877 | } 878 | */ 879 | BoxEditor.prototype.getMouseRay = function () { 880 | var fov = 2.0 * Math.atan(1.0 / this.projectionMatrix[5]); 881 | 882 | var viewSpaceMouseRay = [ 883 | this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height), 884 | this.mouseY * Math.tan(fov / 2.0), 885 | -1.0]; 886 | 887 | var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()); 888 | var mouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix); 889 | Utilities.normalizeVector(mouseRay, mouseRay); 890 | 891 | 892 | var rayOrigin = this.camera.getPosition(); 893 | 894 | return { 895 | origin: rayOrigin, 896 | direction: mouseRay 897 | }; 898 | } 899 | 900 | BoxEditor.prototype.draw = function () { 901 | var wgl = this.wgl; 902 | 903 | wgl.clear( 904 | wgl.createClearState().bindFramebuffer(null).clearColor(0.9, 0.9, 0.9, 1.0), 905 | wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT); 906 | 907 | ///////////////////////////////////////////// 908 | //draw background 909 | 910 | var backgroundDrawState = wgl.createDrawState() 911 | .bindFramebuffer(null) 912 | .viewport(0, 0, this.canvas.width, this.canvas.height) 913 | 914 | .useProgram(this.backgroundProgram) 915 | 916 | .vertexAttribPointer(this.quadVertexBuffer, this.backgroundProgram.getAttribLocation('a_position'), 2, wgl.FLOAT, wgl.FALSE, 0, 0); 917 | 918 | wgl.drawArrays(backgroundDrawState, wgl.TRIANGLE_STRIP, 0, 4); 919 | 920 | 921 | ///////////////////////////////////////////// 922 | //draw grid 923 | 924 | for (var axis = 0; axis < 3; ++axis) { 925 | for (var side = 0; side <= 1; ++side) { 926 | var cameraPosition = this.camera.getPosition(); 927 | 928 | var planePosition = [this.gridWidth / 2, this.gridHeight / 2, this.gridDepth / 2]; 929 | planePosition[axis] = side === 0 ? 0 : this.gridDimensions[axis]; 930 | 931 | var cameraDirection = Utilities.subtractVectors([], planePosition, cameraPosition); 932 | 933 | var gridDrawState = wgl.createDrawState() 934 | .bindFramebuffer(null) 935 | .viewport(0, 0, this.canvas.width, this.canvas.height) 936 | 937 | .useProgram(this.gridProgram) 938 | 939 | .vertexAttribPointer(this.gridVertexBuffers[axis], this.gridProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 940 | 941 | .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) 942 | .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()); 943 | 944 | var translation = [0, 0, 0]; 945 | translation[axis] = side * this.gridDimensions[axis]; 946 | 947 | gridDrawState.uniform3f('u_translation', translation[0], translation[1], translation[2]); 948 | 949 | 950 | if (side === 0 && cameraDirection[axis] <= 0 || side === 1 && cameraDirection[axis] >= 0) { 951 | wgl.drawArrays(gridDrawState, wgl.LINES, 0, 8); 952 | } 953 | } 954 | } 955 | 956 | 957 | /////////////////////////////////////////////// 958 | //draw boxes and point 959 | 960 | var boxDrawState = wgl.createDrawState() 961 | .bindFramebuffer(null) 962 | .viewport(0, 0, this.canvas.width, this.canvas.height) 963 | 964 | .enable(wgl.DEPTH_TEST) 965 | .enable(wgl.CULL_FACE) 966 | 967 | .useProgram(this.boxProgram) 968 | 969 | .vertexAttribPointer(this.cubeVertexBuffer, this.boxProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 970 | 971 | .bindIndexBuffer(this.cubeIndexBuffer) 972 | 973 | .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) 974 | .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) 975 | 976 | .enable(wgl.POLYGON_OFFSET_FILL) 977 | .polygonOffset(1, 1); 978 | 979 | 980 | var boxToHighlight = null, 981 | sideToHighlight = null, 982 | highlightColor = null; 983 | 984 | if (this.interactionState !== null) { 985 | if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) { 986 | boxToHighlight = this.interactionState.box; 987 | sideToHighlight = [1.5, 1.5, 1.5]; 988 | sideToHighlight[this.interactionState.axis] = this.interactionState.side; 989 | 990 | highlightColor = [0.75, 0.75, 0.75]; 991 | } 992 | } else if (!this.keyPressed[32] && !this.camera.isMouseDown()) { //if we're not interacting with anything and we're not in camera mode 993 | var mouseRay = this.getMouseRay(); 994 | 995 | var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction); 996 | 997 | //if we're over a box, let's highlight the side we're hovering over 998 | 999 | if (boxIntersection !== null) { 1000 | boxToHighlight = boxIntersection.aabb; 1001 | sideToHighlight = [1.5, 1.5, 1.5]; 1002 | sideToHighlight[boxIntersection.axis] = boxIntersection.side; 1003 | 1004 | highlightColor = [0.9, 0.9, 0.9]; 1005 | } 1006 | 1007 | 1008 | //if we're not over a box but hovering over a bounding plane, let's draw a indicator point 1009 | if (boxIntersection === null && !this.keyPressed[32]) { 1010 | var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction); 1011 | 1012 | if (planeIntersection !== null) { 1013 | var pointPosition = planeIntersection.point; 1014 | quantizeVector(pointPosition, STEP); 1015 | 1016 | var rotation = [ 1017 | new Float32Array([0, 0, 1, 0, 1, 0, 1, 0, 0]), 1018 | new Float32Array([1, 0, 0, 0, 0, 1, 0, 1, 0]), 1019 | new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) 1020 | ][planeIntersection.axis]; 1021 | 1022 | var pointDrawState = wgl.createDrawState() 1023 | .bindFramebuffer(null) 1024 | .viewport(0, 0, this.canvas.width, this.canvas.height) 1025 | 1026 | .enable(wgl.DEPTH_TEST) 1027 | 1028 | .useProgram(this.pointProgram) 1029 | 1030 | .vertexAttribPointer(this.pointVertexBuffer, this.pointProgram.getAttribLocation('a_position'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 1031 | 1032 | .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) 1033 | .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) 1034 | 1035 | .uniform3f('u_position', pointPosition[0], pointPosition[1], pointPosition[2]) 1036 | 1037 | .uniformMatrix3fv('u_rotation', false, rotation); 1038 | 1039 | wgl.drawArrays(pointDrawState, wgl.TRIANGLE_STRIP, 0, 4); 1040 | } 1041 | } 1042 | } 1043 | 1044 | for (var i = 0; i < this.boxes.length; ++i) { 1045 | var box = this.boxes[i]; 1046 | 1047 | boxDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2]) 1048 | .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]); 1049 | 1050 | if (box === boxToHighlight) { 1051 | boxDrawState.uniform3f('u_highlightSide', sideToHighlight[0], sideToHighlight[1], sideToHighlight[2]); 1052 | boxDrawState.uniform3f('u_highlightColor', highlightColor[0], highlightColor[1], highlightColor[2]); 1053 | } else { 1054 | boxDrawState.uniform3f('u_highlightSide', 1.5, 1.5, 1.5); 1055 | } 1056 | 1057 | wgl.drawElements(boxDrawState, wgl.TRIANGLES, 36, wgl.UNSIGNED_SHORT); 1058 | } 1059 | 1060 | 1061 | 1062 | var boxWireframeDrawState = wgl.createDrawState() 1063 | .bindFramebuffer(null) 1064 | .viewport(0, 0, this.canvas.width, this.canvas.height) 1065 | 1066 | .enable(wgl.DEPTH_TEST) 1067 | 1068 | .useProgram(this.boxWireframeProgram) 1069 | 1070 | .vertexAttribPointer(this.cubeWireframeVertexBuffer, this.boxWireframeProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) 1071 | 1072 | .bindIndexBuffer(this.cubeWireframeIndexBuffer) 1073 | 1074 | .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) 1075 | .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) 1076 | 1077 | 1078 | for (var i = 0; i < this.boxes.length; ++i) { 1079 | var box = this.boxes[i]; 1080 | 1081 | boxWireframeDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2]) 1082 | .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]); 1083 | 1084 | wgl.drawElements(boxWireframeDrawState, wgl.LINES, 24, wgl.UNSIGNED_SHORT); 1085 | } 1086 | 1087 | 1088 | } 1089 | 1090 | return { 1091 | BoxEditor: BoxEditor, 1092 | AABB: AABB, 1093 | InteractionMode: InteractionMode 1094 | }; 1095 | }()); 1096 | --------------------------------------------------------------------------------