├── 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 | 
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 |
--------------------------------------------------------------------------------