diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9f0e7e1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 David Li (http://david.li)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README b/README
new file mode 100644
index 0000000..b5c0438
--- /dev/null
+++ b/README
@@ -0,0 +1,7 @@
+#Fluid Particles
+
+![](http://david.li/images/fluidgithub.png)
+
+[http://david.li/fluid](http://david.li/fluid) ([video](http://www.youtube.com/watch?v=DhNt_A3k4B4))
+
+Fluid simulation is a GPU implementation of the FLIP method (with various additions). Particle rendering uses spherical ambient occlusion volumes.
diff --git a/boxeditor.js b/boxeditor.js
new file mode 100644
index 0000000..98a41be
--- /dev/null
+++ b/boxeditor.js
@@ -0,0 +1,1095 @@
+var BoxEditor = (function () {
+
+ //min and max are both number[3]
+ function AABB (min, max) {
+ this.min = [min[0], min[1], min[2]];
+ this.max = [max[0], max[1], max[2]];
+ }
+
+ AABB.prototype.computeVolume = function () {
+ var volume = 1;
+ for (var i = 0; i < 3; ++i) {
+ volume *= (this.max[i] - this.min[i]);
+ }
+ return volume;
+ }
+
+ AABB.prototype.computeSurfaceArea = function () {
+ var width = this.max[0] - this.min[0];
+ var height = this.max[1] - this.min[1];
+ var depth = this.max[2] - this.min[2];
+
+ return 2 * (width * height + width * depth + height * depth);
+ }
+
+ //returns new AABB with the same min and max (but not the same array references)
+ AABB.prototype.clone = function () {
+ return new AABB(
+ [this.min[0], this.min[1], this.min[2]],
+ [this.max[0], this.max[1], this.max[2]]
+ );
+ }
+
+ AABB.prototype.randomPoint = function () { //random point in this AABB
+ var point = [];
+ for (var i = 0; i < 3; ++i) {
+ point[i] = this.min[i] + Math.random() * (this.max[i] - this.min[i]);
+ }
+ return point;
+ }
+
+ var InteractionMode = {
+ RESIZING: 0,
+ TRANSLATING: 1,
+
+ DRAWING: 2, //whilst we're drawing a rectangle on a plane
+ EXTRUDING: 3 //whilst we're extruding that rectangle into a box
+ };
+
+ var STEP = 1.0;
+
+
+ function exclusiveAABBOverlap (a, b) {
+ return a.min[0] < b.max[0] && a.max[0] > b.min[0] &&
+ a.min[1] < b.max[1] && a.max[1] > b.min[1] &&
+ a.min[2] < b.max[2] && a.max[2] > b.min[2];
+ }
+
+ function inclusiveAABBOverlap (a, b) {
+ return a.min[0] <= b.max[0] && a.max[0] >= b.min[0] &&
+ a.min[1] <= b.max[1] && a.max[1] >= b.min[1] &&
+ a.min[2] <= b.max[2] && a.max[2] >= b.min[2];
+ }
+
+
+ /*
+ if there is an intersection then this returns:
+ {
+ aabb: aabb,
+ t: distance to intersection,
+
+ point: point of intersection,
+
+ //axis and side together define the plane of intersection (+x, -x, etc)
+ axis: 0, 1 or 2 depending on x, y or z,
+ side: -1 or 1 depending on which side the intersection happened on
+ }
+
+
+ otherwise it returns null
+ */
+
+ function rayAABBIntersection (rayOrigin, rayDirection, aabb) {
+ //we see it as a series of clippings in t of the line in the AABB planes along each axis
+ //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
+
+ //the part of the line we have clipped so far
+ var lowT = -Infinity;
+ var highT = Infinity;
+
+ var intersectionAxis = 0;
+
+ for (var i = 0; i < 3; ++i) {
+ var t1 = (aabb.min[i] - rayOrigin[i]) / rayDirection[i];
+ var t2 = (aabb.max[i] - rayOrigin[i]) / rayDirection[i];
+ //so between t1 and t2 we are within the aabb planes in this dimension
+
+ //ensure t1 < t2 (swap if necessary)
+ if (t1 > t2) {
+ var temp = t1;
+ t1 = t2;
+ t2 = temp;
+ }
+
+ //t1 and t2 now hold the lower and upper intersection t's respectively
+
+ //the part of the line we just clipped for does not overlap the part previously clipped and thus there is no intersection
+ if (t2 < lowT || t1 > highT) return null;
+
+ //further clip the line between the planes in this axis
+ if (t1 > lowT) {
+ lowT = t1;
+
+ intersectionAxis = i; //if we needed to futher clip in this axis then this is the closest intersection axis
+ }
+
+ if (t2 < highT) highT = t2;
+ }
+
+ if (lowT > highT) return null;
+
+ //if we've reached this far then there is an intersection
+
+ var intersection = [];
+ for (var i = 0; i < 3; ++i) {
+ intersection[i] = rayOrigin[i] + rayDirection[i] * lowT;
+ }
+
+
+ return {
+ aabb: aabb,
+ t: lowT,
+ axis: intersectionAxis,
+ side: rayDirection[intersectionAxis] > 0 ? -1 : 1,
+ point: intersection
+ };
+ }
+
+ //finds the closest points between the line1 and line2
+ //returns [closest point on line1, closest point on line2]
+ function closestPointsOnLines (line1Origin, line1Direction, line2Origin, line2Direction) {
+ var w0 = Utilities.subtractVectors([], line1Origin, line2Origin);
+
+ var a = Utilities.dotVectors(line1Direction, line1Direction);
+ var b = Utilities.dotVectors(line1Direction, line2Direction);
+ var c = Utilities.dotVectors(line2Direction, line2Direction);
+ var d = Utilities.dotVectors(line1Direction, w0);
+ var e = Utilities.dotVectors(line2Direction, w0);
+
+
+ var t1 = (b * e - c * d) / (a * c - b * b);
+ var t2 = (a * e - b * d) / (a * c - b * b);
+
+ return [
+ Utilities.addVectors([], line1Origin, Utilities.multiplyVectorByScalar([], line1Direction, t1)),
+ Utilities.addVectors([], line2Origin, Utilities.multiplyVectorByScalar([], line2Direction, t2))
+ ];
+ }
+
+ //this defines the bounds of our editing space
+ //the grid starts at (0, 0, 0)
+ //gridSize is [width, height, depth]
+ //onChange is a callback that gets called anytime a box gets edited
+ function BoxEditor (canvas, wgl, projectionMatrix, camera, gridSize, onLoaded, onChange) {
+ this.canvas = canvas;
+
+ this.wgl = wgl;
+
+ this.gridWidth = gridSize[0];
+ this.gridHeight = gridSize[1];
+ this.gridDepth = gridSize[2];
+ this.gridDimensions = [this.gridWidth, this.gridHeight, this.gridDepth];
+
+ this.projectionMatrix = projectionMatrix;
+ this.camera = camera;
+
+ this.onChange = onChange;
+
+ //the cube geometry is a 1x1 cube with the origin at the bottom left corner
+
+ this.cubeVertexBuffer = wgl.createBuffer();
+ wgl.bufferData(this.cubeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([
+ // Front face
+ 0.0, 0.0, 1.0,
+ 1.0, 0.0, 1.0,
+ 1.0, 1.0, 1.0,
+ 0.0, 1.0, 1.0,
+
+ // Back face
+ 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0,
+ 1.0, 1.0, 0.0,
+ 1.0, 0.0, 0.0,
+
+ // Top face
+ 0.0, 1.0, 0.0,
+ 0.0, 1.0, 1.0,
+ 1.0, 1.0, 1.0,
+ 1.0, 1.0, 0.0,
+
+ // Bottom face
+ 0.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0,
+ 1.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0,
+
+ // Right face
+ 1.0, 0.0, 0.0,
+ 1.0, 1.0, 0.0,
+ 1.0, 1.0, 1.0,
+ 1.0, 0.0, 1.0,
+
+ // Left face
+ 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0,
+ 0.0, 1.0, 1.0,
+ 0.0, 1.0, 0.0
+ ]), wgl.STATIC_DRAW);
+
+
+
+ this.cubeIndexBuffer = wgl.createBuffer();
+ wgl.bufferData(this.cubeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([
+ 0, 1, 2, 0, 2, 3, // front
+ 4, 5, 6, 4, 6, 7, // back
+ 8, 9, 10, 8, 10, 11, // top
+ 12, 13, 14, 12, 14, 15, // bottom
+ 16, 17, 18, 16, 18, 19, // right
+ 20, 21, 22, 20, 22, 23 // left
+ ]), wgl.STATIC_DRAW);
+
+
+ this.cubeWireframeVertexBuffer = wgl.createBuffer();
+ wgl.bufferData(this.cubeWireframeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([
+ 0.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0,
+ 1.0, 1.0, 0.0,
+ 0.0, 1.0, 0.0,
+
+ 0.0, 0.0, 1.0,
+ 1.0, 0.0, 1.0,
+ 1.0, 1.0, 1.0,
+ 0.0, 1.0, 1.0]), wgl.STATIC_DRAW);
+
+ this.cubeWireframeIndexBuffer = wgl.createBuffer();
+ wgl.bufferData(this.cubeWireframeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([
+ 0, 1, 1, 2, 2, 3, 3, 0,
+ 4, 5, 5, 6, 6, 7, 7, 4,
+ 0, 4, 1, 5, 2, 6, 3, 7
+ ]), wgl.STATIC_DRAW);
+
+
+ //there's one grid vertex buffer for the planes normal to each axis
+ this.gridVertexBuffers = [];
+
+ for (var axis = 0; axis < 3; ++axis) {
+ this.gridVertexBuffers[axis] = wgl.createBuffer();
+
+ var vertexData = [];
+
+
+ var points; //the points that make up this grid plane
+
+ if (axis === 0) {
+
+ points = [
+ [0, 0, 0],
+ [0, this.gridHeight, 0],
+ [0, this.gridHeight, this.gridDepth],
+ [0, 0, this.gridDepth]
+ ];
+
+ } else if (axis === 1) {
+ points = [
+ [0, 0, 0],
+ [this.gridWidth, 0, 0],
+ [this.gridWidth, 0, this.gridDepth],
+ [0, 0, this.gridDepth]
+ ];
+ } else if (axis === 2) {
+
+ points = [
+ [0, 0, 0],
+ [this.gridWidth, 0, 0],
+ [this.gridWidth, this.gridHeight, 0],
+ [0, this.gridHeight, 0]
+ ];
+ }
+
+
+ for (var i = 0; i < 4; ++i) {
+ vertexData.push(points[i][0]);
+ vertexData.push(points[i][1]);
+ vertexData.push(points[i][2]);
+
+ vertexData.push(points[(i + 1) % 4][0]);
+ vertexData.push(points[(i + 1) % 4][1]);
+ vertexData.push(points[(i + 1) % 4][2]);
+ }
+
+
+ wgl.bufferData(this.gridVertexBuffers[axis], wgl.ARRAY_BUFFER, new Float32Array(vertexData), wgl.STATIC_DRAW);
+ }
+
+ this.pointVertexBuffer = wgl.createBuffer();
+ 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);
+
+
+ this.quadVertexBuffer = wgl.createBuffer();
+ 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);
+
+
+ /////////////////////////////////////////////////
+ // box state
+
+ this.boxes = [];
+
+
+ ////////////////////////////////////////////////
+ // interaction stuff
+
+ //mouse x and y are in [-1, 1] (clip space)
+ this.mouseX = 999;
+ this.mouseY = 999;
+
+ this.keyPressed = []; //an array of booleans that maps a key code to whether or not it's pressed
+ for (var i = 0; i < 256; ++i) {
+ this.keyPressed[i] = false;
+ }
+
+ /*
+ interactions:
+ click on a plane and hold down to begin drawing
+ when mouse is released we enter extrusion mode for new box
+ click again to create box
+
+ click and drag on side of boxes to resize
+
+ click and drag on side of boxes whilst holding shift to move
+
+
+ //while we're not interacting, this is null
+ //while we are interacting this contains an object
+ /*
+
+ {
+ mode: the interaction mode,
+
+ during resizing or translating or extrusion:
+ box: box we're currently manipulating,
+ axis: axis of plane we're manipulating: 0, 1 or 2
+ side: side of plane we're manipulating: -1 or 1
+ point: the point at which the interaction started
+
+
+ during translation we also have:
+ startMax: the starting max along the interaction axis
+ startMin: the starting min along the interaction axis
+
+
+ during drawing
+ box: box we're currently drawing
+ point: the point at which we started drawing
+ axis: the axis of the plane which we're drawing on
+ side: the side of the plane which we're drawin on
+
+ }
+ */
+ this.interactionState = null;
+
+
+ ///////////////////////////////////
+ // load programs
+
+
+ wgl.createProgramsFromFiles({
+ backgroundProgram: {
+ vertexShader: 'shaders/background.vert',
+ fragmentShader: 'shaders/background.frag'
+ },
+ boxProgram: {
+ vertexShader: 'shaders/box.vert',
+ fragmentShader: 'shaders/box.frag'
+ },
+ boxWireframeProgram: {
+ vertexShader: 'shaders/boxwireframe.vert',
+ fragmentShader: 'shaders/boxwireframe.frag'
+ },
+ gridProgram: {
+ vertexShader: 'shaders/grid.vert',
+ fragmentShader: 'shaders/grid.frag'
+ },
+ pointProgram: {
+ vertexShader: 'shaders/point.vert',
+ fragmentShader: 'shaders/point.frag'
+ }
+ }, (function (programs) {
+ for (var programName in programs) {
+ this[programName] = programs[programName];
+ }
+
+ onLoaded();
+ }).bind(this));
+ }
+
+ function quantize (x, step) {
+ return Math.round(x / step) * step;
+ }
+
+ function quantizeVector (v, step) {
+ for (var i = 0; i < v.length; ++i) {
+ v[i] = quantize(v[i], step);
+ }
+
+ return v;
+ }
+
+ BoxEditor.prototype.onKeyDown = function (event) {
+ this.keyPressed[event.keyCode] = true;
+ }
+
+ BoxEditor.prototype.onKeyUp = function (event) {
+ this.keyPressed[event.keyCode] = false;
+ }
+
+ BoxEditor.prototype.onMouseMove = function (event) {
+ event.preventDefault();
+
+ var position = Utilities.getMousePosition(event, this.canvas);
+ var normalizedX = position.x / this.canvas.width;
+ var normalizedY = position.y / this.canvas.height;
+
+ this.mouseX = normalizedX * 2.0 - 1.0;
+ this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0;
+
+
+
+ if (this.interactionState !== null) {
+ this.onChange();
+
+ if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) {
+ var mouseRay = this.getMouseRay();
+
+ //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
+
+ var dragLineOrigin = this.interactionState.point;
+ var dragLineDirection = [0, 0, 0];
+ dragLineDirection[this.interactionState.axis] = 1.0;
+
+ //then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged'
+ var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction);
+ var newCoordinate = closestPoints[0][this.interactionState.axis]; //the new coordinate for this box plane
+ newCoordinate = quantize(newCoordinate, STEP);
+
+ var box = this.interactionState.box,
+ side = this.interactionState.side,
+ axis = this.interactionState.axis;
+
+ //resize the box, clamping it to itself and the overall grid
+ if (side === -1) {
+ box.min[axis] = Math.max(Math.min(newCoordinate, box.max[axis]), 0);
+ } else if (side === 1) {
+ box.max[axis] = Math.min(Math.max(newCoordinate, box.min[axis]), this.gridDimensions[axis]);
+ }
+
+ //collision detection
+ for (var i = 0; i < this.boxes.length; ++i) {
+ var otherBox = this.boxes[i];
+ if (box !== otherBox) { //don't collide with self
+ if (exclusiveAABBOverlap(box, otherBox)) {
+
+ //resolve collision
+ if (side === -1) {
+ box.min[axis] = otherBox.max[axis];
+ } else if (side === 1) {
+ box.max[axis] = otherBox.min[axis];
+ }
+ }
+ }
+ }
+
+ } else if (this.interactionState.mode === InteractionMode.TRANSLATING) {
+
+ var mouseRay = this.getMouseRay();
+
+ //so when we are translating a box, what we do is we extend a line out from the intersection point normal to the plane
+
+ var dragLineOrigin = this.interactionState.point;
+ var dragLineDirection = [0, 0, 0];
+ dragLineDirection[this.interactionState.axis] = 1.0;
+
+ //then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged'
+ var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction);
+ var newCoordinate = closestPoints[0][this.interactionState.axis]; //the new coordinate for this box plane
+ newCoordinate = quantize(newCoordinate, STEP);
+
+ var box = this.interactionState.box,
+ side = this.interactionState.side,
+ axis = this.interactionState.axis;
+
+
+ var length = this.interactionState.startMax - this.interactionState.startMin; //the length of the box along the translation axis
+
+ if (side === -1) {
+ box.min[axis] = newCoordinate;
+ box.max[axis] = newCoordinate + length;
+ } else if (side === 1) {
+ box.max[axis] = newCoordinate;
+ box.min[axis] = newCoordinate - length;
+ }
+
+ //clamp to boundaries
+ if (box.min[axis] < 0) {
+ box.min[axis] = 0;
+ box.max[axis] = length;
+ }
+
+ if (box.max[axis] > this.gridDimensions[axis]) {
+ box.max[axis] = this.gridDimensions[axis];
+ box.min[axis] = this.gridDimensions[axis] - length;
+ }
+
+
+ var translationDirection = 0; //is either -1 or 1 depending on which way we're pushing our box
+ //how we resolve collisions depends on our translation direction
+ if (side === -1) {
+ translationDirection = newCoordinate < this.interactionState.startMin ? -1 : 1;
+ } else if (side === 1) {
+ translationDirection = newCoordinate < this.interactionState.startMax ? -1 : 1;
+ }
+
+
+ var sweptBox = box.clone(); //we sweep out translating AABB for collision detection to prevent ghosting through boxes
+ //reset swept box to original box location before translation
+ sweptBox.min[axis] = this.interactionState.startMin;
+ sweptBox.max[axis] = this.interactionState.startMax;
+
+ //sweep out the correct plane to where it has been translated to
+ if (translationDirection === 1) {
+ sweptBox.max[axis] = box.max[axis];
+ } else if (translationDirection === -1) {
+ sweptBox.min[axis] = box.min[axis];
+ }
+
+ //collision detection
+ for (var i = 0; i < this.boxes.length; ++i) {
+ var otherBox = this.boxes[i];
+ if (box !== otherBox) { //don't collide with self
+ if (exclusiveAABBOverlap(sweptBox, otherBox)) {
+
+ //resolve collision
+ if (translationDirection === -1) {
+ box.min[axis] = otherBox.max[axis];
+ box.max[axis] = otherBox.max[axis] + length;
+ } else if (translationDirection === 1) {
+ box.max[axis] = otherBox.min[axis];
+ box.min[axis] = otherBox.min[axis] - length;
+ }
+ }
+ }
+ }
+
+ } else if (this.interactionState.mode === InteractionMode.DRAWING) {
+
+ var mouseRay = this.getMouseRay();
+
+ //get the mouse ray intersection with the drawing plane
+
+ var axis = this.interactionState.axis,
+ side = this.interactionState.side,
+ startPoint = this.interactionState.point;
+
+ var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis];
+ var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis];
+
+ if (t > 0) { //if the mouse ray misses the drawing plane then the box just stays the same size as it was before
+
+ var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t));
+ quantizeVector(intersection, STEP);
+
+ for (var i = 0; i < 3; ++i) {
+ intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]);
+ intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]);
+ }
+
+ var min = [Math.min(startPoint[0], intersection[0]), Math.min(startPoint[1], intersection[1]), Math.min(startPoint[2], intersection[2])];
+ var max = [Math.max(startPoint[0], intersection[0]), Math.max(startPoint[1], intersection[1]), Math.max(startPoint[2], intersection[2])];
+
+
+ var box = this.interactionState.box;
+
+ var sweptBox = new AABB(min, max); //we sweep the box a bit into the grid to make sure it collides along the plane axis
+ if (this.interactionState.side === -1) {
+ sweptBox.max[this.interactionState.axis] = STEP * 0.1;
+ } else {
+ sweptBox.min[this.interactionState.axis] = this.gridDimensions[this.interactionState.axis] - STEP * 0.1;
+
+ }
+
+ //collision detection
+ for (var i = 0; i < this.boxes.length; ++i) {
+ var otherBox = this.boxes[i];
+
+ if (box !== otherBox) { //don't collide with self
+ if (exclusiveAABBOverlap(sweptBox, otherBox)) {
+
+ //we resolve along the axis with the smaller overlap and where the start point doesn't already overlap the other box in that axis
+ var smallestOverlap = 99999999;
+ var smallestOverlapAxis = -1;
+
+ for (var axis = 0; axis < 3; ++axis) {
+ if (axis !== this.interactionState.axis) { //only resolve collisions in the drawing plane
+ var overlap = Math.min(max[axis], otherBox.max[axis]) - Math.max(min[axis], otherBox.min[axis]);
+
+ if (overlap > 0 && overlap < smallestOverlap && (startPoint[axis] < otherBox.min[axis] || startPoint[axis] > otherBox.max[axis])) {
+ smallestOverlap = overlap;
+ smallestOverlapAxis = axis;
+ }
+ }
+ }
+
+ if (intersection[smallestOverlapAxis] > startPoint[smallestOverlapAxis]) { //if we're resizing in the positive direction
+ max[smallestOverlapAxis] = otherBox.min[smallestOverlapAxis];
+ } else { //if we're resizing in the negative direction
+ min[smallestOverlapAxis] = otherBox.max[smallestOverlapAxis];
+ }
+ }
+ }
+ }
+
+ this.interactionState.box.min = min;
+ this.interactionState.box.max = max;
+
+ }
+ }
+ }
+
+ this.camera.onMouseMove(event);
+ }
+
+ //returns the closest box intersection data (same as rayAABBIntersection) for the given ray
+ //if there is no intersection it returns null
+ BoxEditor.prototype.getBoxIntersection = function (rayOrigin, rayDirection) {
+ //find the closest box that this collides with
+
+ var bestIntersectionSoFar = {
+ aabb: null,
+ t: Infinity
+ }
+
+ for (var i = 0; i < this.boxes.length; ++i) {
+ var box = this.boxes[i];
+
+ var intersection = rayAABBIntersection(rayOrigin, rayDirection, box);
+
+ if (intersection !== null) { //if there is an intersection
+ if (intersection.t < bestIntersectionSoFar.t) { //if this is closer than the best we've seen so far
+ bestIntersectionSoFar = intersection;
+ }
+ }
+ }
+
+ if (bestIntersectionSoFar.aabb === null) { //if we didn't intersect any boxes
+ return null;
+ } else {
+ return bestIntersectionSoFar;
+ }
+ }
+
+ //tests for intersection with one of the bounding planes
+ /*
+ if there is an intersection returns
+ {axis, side, point}
+ otherwise, returns null
+ */
+ BoxEditor.prototype.getBoundingPlaneIntersection = function (rayOrigin, rayDirection) {
+ //we try to intersect with the two planes on each axis in turn (as long as they are facing towards the camera)
+ //we assume we could only ever intersect with one of the planes so we break out as soon as we've found something
+
+ for (var axis = 0; axis < 3; ++axis) {
+
+ //now let's try intersecting with each side in turn
+ for (var side = -1; side <= 1; side += 2) { //goes between -1 and 1 (hackish!
+
+ //first let's make sure the plane is front facing to the ray
+ var frontFacing = side === -1 ? rayDirection[axis] < 0 : rayDirection[axis] > 0;
+ if (frontFacing) {
+ var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; //the coordinate of the plane along this axis
+
+ var t = (planeCoordinate - rayOrigin[axis]) / rayDirection[axis];
+
+
+ if (t > 0) {
+ var intersection = Utilities.addVectors([], rayOrigin, Utilities.multiplyVectorByScalar([], rayDirection, t));
+
+ //if we're still within the bounds of the grid
+ if (intersection[0] >= 0.0 && intersection[0] <= this.gridDimensions[0] &&
+ intersection[1] >= 0.0 && intersection[1] <= this.gridDimensions[1] &&
+ intersection[2] >= 0.0 && intersection[2] <= this.gridDimensions[2]) {
+
+ return {
+ axis: axis,
+ side: side,
+ point: intersection
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null; //no intersection found
+ }
+
+
+ BoxEditor.prototype.onMouseDown = function (event) {
+ event.preventDefault();
+
+ this.onMouseMove(event);
+
+ if (!this.keyPressed[32]) { //if space isn't held down
+
+ //we've finished extruding a box
+ if (this.interactionState !== null && this.interactionState.mode === InteractionMode.EXTRUDING) {
+ //delete zero volume boxes
+ if (this.interactionState.box.computeVolume() === 0) {
+ this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1);
+ }
+ this.interactionState = null;
+
+ this.onChange();
+
+ return;
+ } else {
+
+ var mouseRay = this.getMouseRay();
+
+ //find the closest box that this collides with
+
+ var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction);
+
+
+ //if we've intersected at least one box then let's start manipulating that box
+ if (boxIntersection !== null) {
+ var intersection = boxIntersection;
+
+ if (this.keyPressed[16]) { //if we're holding shift we start to translate
+ this.interactionState = {
+ mode: InteractionMode.TRANSLATING,
+ box: intersection.aabb,
+ axis: intersection.axis,
+ side: intersection.side,
+ point: intersection.point,
+
+ startMax: intersection.aabb.max[intersection.axis],
+ startMin: intersection.aabb.min[intersection.axis]
+ };
+ } else { //otherwise we start resizing
+
+ this.interactionState = {
+ mode: InteractionMode.RESIZING,
+ box: intersection.aabb,
+ axis: intersection.axis,
+ side: intersection.side,
+ point: intersection.point
+ };
+ }
+ }
+
+
+ //if we've not intersected any box then let's see if we should start the box creation process
+ if (boxIntersection === null) {
+ var mouseRay = this.getMouseRay();
+
+ var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction);
+
+ if (planeIntersection !== null) { //if we've hit one of the planes
+ //go into drawing mode
+
+ var point = planeIntersection.point;
+ point[0] = quantize(point[0], STEP);
+ point[1] = quantize(point[1], STEP);
+ point[2] = quantize(point[2], STEP);
+
+ var newBox = new AABB(point, point);
+ this.boxes.push(newBox);
+
+ this.interactionState = {
+ mode: InteractionMode.DRAWING,
+ box: newBox,
+ axis: planeIntersection.axis,
+ side: planeIntersection.side,
+ point: planeIntersection.point
+ };
+ }
+
+ this.onChange();
+ }
+
+ }
+
+ }
+
+ if (this.interactionState === null) {
+ this.camera.onMouseDown(event);
+ }
+
+ }
+
+ BoxEditor.prototype.onMouseUp = function (event) {
+ event.preventDefault();
+
+ if (this.interactionState !== null) {
+ if (this.interactionState.mode === InteractionMode.RESIZING) { //the end of a resize
+ //if we've resized to zero volume then we delete the box
+ if (this.interactionState.box.computeVolume() === 0) {
+ this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1);
+ }
+
+ this.interactionState = null;
+
+ } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { //the end of a translate
+ this.interactionState = null;
+ } else if (this.interactionState.mode === InteractionMode.DRAWING) { //the end of a draw
+ //TODO: DRY this
+
+ if (this.interactionState.box.computeSurfaceArea() > 0) { //make sure we have something to extrude
+
+ var mouseRay = this.getMouseRay();
+
+ var axis = this.interactionState.axis,
+ side = this.interactionState.side,
+ startPoint = this.interactionState.point;
+
+ var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis];
+ var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis];
+
+ var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t));
+ quantizeVector(intersection, STEP);
+
+ //clamp extrusion point to grid and to box
+ for (var i = 0; i < 3; ++i) {
+ intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]);
+ intersection[i] = Utilities.clamp(intersection[i], this.interactionState.box.min[i], this.interactionState.box.max[i]);
+ }
+
+
+ //go into extrusion mode
+ this.interactionState = {
+ mode: InteractionMode.EXTRUDING,
+ box: this.interactionState.box,
+ axis: this.interactionState.axis,
+ side: this.interactionState.side * -1,
+ point: intersection
+ };
+
+ } else { //otherwise delete the box we were editing and go straight back into regular mode
+ this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1);
+ this.interactionState = null;
+ }
+ }
+
+ this.onChange();
+ }
+
+
+ if (this.interactionState === null) {
+ this.camera.onMouseUp(event);
+ }
+ }
+
+
+ //returns an object
+ /*
+ {
+ origin: [x, y, z],
+ direction: [x, y, z] //normalized
+ }
+ */
+ BoxEditor.prototype.getMouseRay = function () {
+ var fov = 2.0 * Math.atan(1.0 / this.projectionMatrix[5]);
+
+ var viewSpaceMouseRay = [
+ this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height),
+ this.mouseY * Math.tan(fov / 2.0),
+ -1.0];
+
+ var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix());
+ var mouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix);
+ Utilities.normalizeVector(mouseRay, mouseRay);
+
+
+ var rayOrigin = this.camera.getPosition();
+
+ return {
+ origin: rayOrigin,
+ direction: mouseRay
+ };
+ }
+
+ BoxEditor.prototype.draw = function () {
+ var wgl = this.wgl;
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(null).clearColor(0.9, 0.9, 0.9, 1.0),
+ wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT);
+
+ /////////////////////////////////////////////
+ //draw background
+
+ var backgroundDrawState = wgl.createDrawState()
+ .bindFramebuffer(null)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .useProgram(this.backgroundProgram)
+
+ .vertexAttribPointer(this.quadVertexBuffer, this.backgroundProgram.getAttribLocation('a_position'), 2, wgl.FLOAT, wgl.FALSE, 0, 0);
+
+ wgl.drawArrays(backgroundDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+
+ /////////////////////////////////////////////
+ //draw grid
+
+ for (var axis = 0; axis < 3; ++axis) {
+ for (var side = 0; side <= 1; ++side) {
+ var cameraPosition = this.camera.getPosition();
+
+ var planePosition = [this.gridWidth / 2, this.gridHeight / 2, this.gridDepth / 2];
+ planePosition[axis] = side === 0 ? 0 : this.gridDimensions[axis];
+
+ var cameraDirection = Utilities.subtractVectors([], planePosition, cameraPosition);
+
+ var gridDrawState = wgl.createDrawState()
+ .bindFramebuffer(null)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .useProgram(this.gridProgram)
+
+ .vertexAttribPointer(this.gridVertexBuffers[axis], this.gridProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix)
+ .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix());
+
+ var translation = [0, 0, 0];
+ translation[axis] = side * this.gridDimensions[axis];
+
+ gridDrawState.uniform3f('u_translation', translation[0], translation[1], translation[2]);
+
+
+ if (side === 0 && cameraDirection[axis] <= 0 || side === 1 && cameraDirection[axis] >= 0) {
+ wgl.drawArrays(gridDrawState, wgl.LINES, 0, 8);
+ }
+ }
+ }
+
+
+ ///////////////////////////////////////////////
+ //draw boxes and point
+
+ var boxDrawState = wgl.createDrawState()
+ .bindFramebuffer(null)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .enable(wgl.DEPTH_TEST)
+ .enable(wgl.CULL_FACE)
+
+ .useProgram(this.boxProgram)
+
+ .vertexAttribPointer(this.cubeVertexBuffer, this.boxProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .bindIndexBuffer(this.cubeIndexBuffer)
+
+ .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix)
+ .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix())
+
+ .enable(wgl.POLYGON_OFFSET_FILL)
+ .polygonOffset(1, 1);
+
+
+ var boxToHighlight = null,
+ sideToHighlight = null,
+ highlightColor = null;
+
+ if (this.interactionState !== null) {
+ if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) {
+ boxToHighlight = this.interactionState.box;
+ sideToHighlight = [1.5, 1.5, 1.5];
+ sideToHighlight[this.interactionState.axis] = this.interactionState.side;
+
+ highlightColor = [0.75, 0.75, 0.75];
+ }
+ } else if (!this.keyPressed[32] && !this.camera.isMouseDown()) { //if we're not interacting with anything and we're not in camera mode
+ var mouseRay = this.getMouseRay();
+
+ var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction);
+
+ //if we're over a box, let's highlight the side we're hovering over
+
+ if (boxIntersection !== null) {
+ boxToHighlight = boxIntersection.aabb;
+ sideToHighlight = [1.5, 1.5, 1.5];
+ sideToHighlight[boxIntersection.axis] = boxIntersection.side;
+
+ highlightColor = [0.9, 0.9, 0.9];
+ }
+
+
+ //if we're not over a box but hovering over a bounding plane, let's draw a indicator point
+ if (boxIntersection === null && !this.keyPressed[32]) {
+ var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction);
+
+ if (planeIntersection !== null) {
+ var pointPosition = planeIntersection.point;
+ quantizeVector(pointPosition, STEP);
+
+ var rotation = [
+ new Float32Array([0, 0, 1, 0, 1, 0, 1, 0, 0]),
+ new Float32Array([1, 0, 0, 0, 0, 1, 0, 1, 0]),
+ new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1])
+ ][planeIntersection.axis];
+
+ var pointDrawState = wgl.createDrawState()
+ .bindFramebuffer(null)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .enable(wgl.DEPTH_TEST)
+
+ .useProgram(this.pointProgram)
+
+ .vertexAttribPointer(this.pointVertexBuffer, this.pointProgram.getAttribLocation('a_position'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix)
+ .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix())
+
+ .uniform3f('u_position', pointPosition[0], pointPosition[1], pointPosition[2])
+
+ .uniformMatrix3fv('u_rotation', false, rotation);
+
+ wgl.drawArrays(pointDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+ }
+ }
+ }
+
+ for (var i = 0; i < this.boxes.length; ++i) {
+ var box = this.boxes[i];
+
+ boxDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2])
+ .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]);
+
+ if (box === boxToHighlight) {
+ boxDrawState.uniform3f('u_highlightSide', sideToHighlight[0], sideToHighlight[1], sideToHighlight[2]);
+ boxDrawState.uniform3f('u_highlightColor', highlightColor[0], highlightColor[1], highlightColor[2]);
+ } else {
+ boxDrawState.uniform3f('u_highlightSide', 1.5, 1.5, 1.5);
+ }
+
+ wgl.drawElements(boxDrawState, wgl.TRIANGLES, 36, wgl.UNSIGNED_SHORT);
+ }
+
+
+
+ var boxWireframeDrawState = wgl.createDrawState()
+ .bindFramebuffer(null)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .enable(wgl.DEPTH_TEST)
+
+ .useProgram(this.boxWireframeProgram)
+
+ .vertexAttribPointer(this.cubeWireframeVertexBuffer, this.boxWireframeProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .bindIndexBuffer(this.cubeWireframeIndexBuffer)
+
+ .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix)
+ .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix())
+
+
+ for (var i = 0; i < this.boxes.length; ++i) {
+ var box = this.boxes[i];
+
+ boxWireframeDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2])
+ .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]);
+
+ wgl.drawElements(boxWireframeDrawState, wgl.LINES, 24, wgl.UNSIGNED_SHORT);
+ }
+
+
+ }
+
+ return {
+ BoxEditor: BoxEditor,
+ AABB: AABB,
+ InteractionMode: InteractionMode
+ };
+}());
diff --git a/camera.js b/camera.js
new file mode 100644
index 0000000..50168c2
--- /dev/null
+++ b/camera.js
@@ -0,0 +1,138 @@
+'use strict'
+
+var Camera = (function () {
+ var SENSITIVITY = 0.005;
+
+ var MIN_DISTANCE = 25.0;
+ var MAX_DISTANCE = 60.0;
+
+ function Camera (element, orbitPoint) {
+ this.element = element;
+ this.distance = 40.0;
+ this.orbitPoint = orbitPoint;
+
+ this.azimuth = 0.0,
+ this.elevation = 0.25
+
+ this.minElevation = -Math.PI / 4;
+ this.maxElevation = Math.PI / 4;
+
+ this.currentMouseX = 0,
+ this.currentMouseY = 0;
+
+ this.lastMouseX = 0,
+ this.lastMouseY = 0;
+
+ this.mouseDown = false;
+
+ this.viewMatrix = new Float32Array(16);
+
+
+ this.recomputeViewMatrix();
+
+
+ element.addEventListener('wheel', (function (event) {
+ var scrollDelta = event.deltaY;
+ this.distance += ((scrollDelta > 0) ? 1 : -1) * 2.0;
+
+ if (this.distance < MIN_DISTANCE) this.distance = MIN_DISTANCE;
+ if (this.distance > MAX_DISTANCE) this.distance = MAX_DISTANCE;
+
+ this.recomputeViewMatrix();
+ }).bind(this));
+ };
+
+ Camera.prototype.recomputeViewMatrix = function () {
+ var xRotationMatrix = new Float32Array(16),
+ yRotationMatrix = new Float32Array(16),
+ distanceTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)),
+ orbitTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16));
+
+ Utilities.makeIdentityMatrix(this.viewMatrix);
+
+ Utilities.makeXRotationMatrix(xRotationMatrix, this.elevation);
+ Utilities.makeYRotationMatrix(yRotationMatrix, this.azimuth);
+ distanceTranslationMatrix[14] = -this.distance;
+ orbitTranslationMatrix[12] = -this.orbitPoint[0];
+ orbitTranslationMatrix[13] = -this.orbitPoint[1];
+ orbitTranslationMatrix[14] = -this.orbitPoint[2];
+
+ Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, orbitTranslationMatrix);
+ Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, yRotationMatrix);
+ Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, xRotationMatrix);
+ Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, distanceTranslationMatrix);
+ };
+
+ Camera.prototype.getPosition = function () {
+ var position = [
+ this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.sin(-this.azimuth) + this.orbitPoint[0],
+ this.distance * Math.cos(Math.PI / 2 - this.elevation) + this.orbitPoint[1],
+ this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.cos(-this.azimuth) + this.orbitPoint[2]
+ ];
+
+ return position;
+ };
+
+ Camera.prototype.isMouseDown = function () {
+ return this.mouseDown;
+ };
+
+ Camera.prototype.getViewMatrix = function () {
+ return this.viewMatrix;
+ };
+
+ Camera.prototype.setBounds = function (minElevation, maxElevation) {
+ this.minElevation = minElevation;
+ this.maxElevation = maxElevation;
+
+ if (this.elevation > this.maxElevation) this.elevation = this.maxElevation;
+ if (this.elevation < this.minElevation) this.elevation = this.minElevation;
+
+ this.recomputeViewMatrix();
+ };
+
+ Camera.prototype.onMouseDown = function (event) {
+ event.preventDefault();
+
+ var x = Utilities.getMousePosition(event, this.element).x;
+ var y = Utilities.getMousePosition(event, this.element).y;
+
+ this.mouseDown = true;
+ this.lastMouseX = x;
+ this.lastMouseY = y;
+ };
+
+ Camera.prototype.onMouseUp = function (event) {
+ event.preventDefault();
+
+ this.mouseDown = false;
+ };
+
+ Camera.prototype.onMouseMove = function (event) {
+ event.preventDefault();
+
+ var x = Utilities.getMousePosition(event, this.element).x;
+ var y = Utilities.getMousePosition(event, this.element).y;
+
+ if (this.mouseDown) {
+ this.currentMouseX = x;
+ this.currentMouseY = y;
+
+ var deltaAzimuth = (this.currentMouseX - this.lastMouseX) * SENSITIVITY;
+ var deltaElevation = (this.currentMouseY - this.lastMouseY) * SENSITIVITY;
+
+ this.azimuth += deltaAzimuth;
+ this.elevation += deltaElevation;
+
+ if (this.elevation > this.maxElevation) this.elevation = this.maxElevation;
+ if (this.elevation < this.minElevation) this.elevation = this.minElevation;
+
+ this.recomputeViewMatrix();
+
+ this.lastMouseX = this.currentMouseX;
+ this.lastMouseY = this.currentMouseY;
+ }
+ };
+
+ return Camera;
+}());
diff --git a/flip.css b/flip.css
new file mode 100644
index 0000000..cc96425
--- /dev/null
+++ b/flip.css
@@ -0,0 +1,189 @@
+* {
+ font-family: 'Asap', Helvetica, Arial, sans-serif;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+
+ user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+}
+
+#start-button {
+ font-size: 16px;
+ color: white;
+ font-weight: bold;
+
+ margin-bottom: 15px;
+
+ width: 120px;
+ height: 40px;
+ line-height: 40px;
+
+ text-align: center;
+
+ pointer-events: auto;
+}
+
+.start-button-active {
+ cursor: default;
+ background: rgb(70, 190, 60);
+}
+
+.start-button-active:hover {
+ background: rgb(60, 180, 50);
+}
+
+.start-button-active:active {
+ background: rgb(50, 170, 40);
+}
+
+.start-button-inactive {
+ background: #999999;
+ cursor: default;
+}
+
+#preset-button {
+ margin-bottom: 10px;
+
+ background: rgba(80, 160, 230, 1.0);
+
+ font-size: 12px;
+ font-weight: bold;
+ color: white;
+
+ width: 100px;
+ height: 30px;
+ line-height: 30px;
+
+ text-align: center;
+
+ cursor: default;
+
+ pointer-events: auto;
+}
+
+#preset-button:hover {
+ background: rgba(70, 150, 220, 1.0);
+}
+
+#preset-button:active {
+ background: rgba(60, 140, 210, 1.0);
+}
+
+#ui {
+ position: absolute;
+
+ pointer-events: none;
+
+ top: 30px;
+ left: 30px;
+}
+
+#particle-count {
+ color: #777777;
+
+ font-size: 12px;
+
+ margin-top: 4px;
+}
+
+.slider-label {
+ color: #555555;
+ font-size: 12px;
+ font-weight: bold;
+
+ margin-top: 5px;
+}
+
+.slider {
+ margin-top: 4px;
+
+ width: 200px;
+ height: 22px;
+
+ user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+
+ background: rgba(0, 0, 0, 0.1);
+
+ pointer-events: auto;
+}
+
+.slider:hover {
+ background: rgba(0, 0, 0, 0.12);
+}
+
+.slider div {
+ background: #666666;
+}
+
+.slider:hover div {
+ background: #555555;
+}
+
+.instructions {
+ position: absolute;
+
+ bottom: 30px;
+ left: 30px;
+
+ font-size: 12px;
+ line-height: 16px;
+
+ color: #555555;
+
+ margin-top: 20px;
+}
+
+.instructions span {
+ font-weight: bold;
+
+ color: #444444;
+}
+
+#footer {
+ font-size: 12px;
+
+ position: absolute;
+ right: 20px;
+ bottom: 20px;
+
+ color: #555555;
+}
+
+#footer a {
+ text-decoration: underline;
+ color: #555555;
+}
+
+#container {
+ color: #333333;
+ font-size: 14px;
+
+ margin-top: 40px;
+ margin-left: 40px;
+}
+
+#video {
+ color: #333333;
+ font-size: 14px;
+
+ margin-top: 20px;
+}
+
+#linkback {
+ color: #333333;
+
+ margin-top: 20px;
+}
+
+#linkback a {
+ color: #333333;
+}
diff --git a/fluidparticles.js b/fluidparticles.js
new file mode 100644
index 0000000..52c338a
--- /dev/null
+++ b/fluidparticles.js
@@ -0,0 +1,377 @@
+'use strict'
+
+var FluidParticles = (function () {
+ var FOV = Math.PI / 3;
+
+ var State = {
+ EDITING: 0,
+ SIMULATING: 1
+ };
+
+ var GRID_WIDTH = 40,
+ GRID_HEIGHT = 20,
+ GRID_DEPTH = 20;
+
+ var PARTICLES_PER_CELL = 10;
+
+ function FluidParticles () {
+
+ var canvas = this.canvas = document.getElementById('canvas');
+ var wgl = this.wgl = new WrappedGL(canvas);
+
+ window.wgl = wgl;
+
+ this.projectionMatrix = Utilities.makePerspectiveMatrix(new Float32Array(16), FOV, this.canvas.width / this.canvas.height, 0.1, 100.0);
+ this.camera = new Camera(this.canvas, [GRID_WIDTH / 2, GRID_HEIGHT / 3, GRID_DEPTH / 2]);
+
+ var boxEditorLoaded = false,
+ simulatorRendererLoaded = false;
+
+ this.boxEditor = new BoxEditor.BoxEditor(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], (function () {
+ boxEditorLoaded = true;
+ if (boxEditorLoaded && simulatorRendererLoaded) {
+ start.call(this);
+ }
+ }).bind(this),
+ (function () {
+ this.redrawUI();
+ }).bind(this));
+
+ this.simulatorRenderer = new SimulatorRenderer(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], (function () {
+ simulatorRendererLoaded = true;
+ if (boxEditorLoaded && simulatorRendererLoaded) {
+ start.call(this);
+ }
+ }).bind(this));
+
+ function start(programs) {
+ this.state = State.EDITING;
+
+ this.startButton = document.getElementById('start-button');
+
+ this.startButton.addEventListener('click', (function () {
+ if (this.state === State.EDITING) {
+ if (this.boxEditor.boxes.length > 0) {
+ this.startSimulation();
+ }
+ this.redrawUI();
+ } else if (this.state === State.SIMULATING) {
+ this.stopSimulation();
+ this.redrawUI();
+ }
+ }).bind(this));
+
+ this.currentPresetIndex = 0;
+ this.editedSinceLastPreset = false; //whether the user has edited the last set preset
+ var PRESETS = [
+ //dam break
+ [
+ new BoxEditor.AABB([0, 0, 0], [15, 20, 20])
+ ],
+
+ //block drop
+ [
+ new BoxEditor.AABB([0, 0, 0], [40, 7, 20]),
+ new BoxEditor.AABB([12, 12, 5], [28, 20, 15])
+ ],
+
+ //double splash
+ [
+ new BoxEditor.AABB([0, 0, 0], [10, 20, 15]),
+ new BoxEditor.AABB([30, 0, 5], [40, 20, 20])
+ ],
+
+ ];
+
+ this.presetButton = document.getElementById('preset-button');
+ this.presetButton.addEventListener('click', (function () {
+ this.editedSinceLastPreset = false;
+
+ this.boxEditor.boxes.length = 0;
+
+ var preset = PRESETS[this.currentPresetIndex];
+ for (var i = 0; i < preset.length; ++i) {
+ this.boxEditor.boxes.push(preset[i].clone());
+ }
+
+ this.currentPresetIndex = (this.currentPresetIndex + 1) % PRESETS.length;
+
+ this.redrawUI();
+
+ }).bind(this));
+
+
+
+ ////////////////////////////////////////////////////////
+ // parameters/sliders
+
+ //using gridCellDensity ensures a linear relationship to particle count
+ this.gridCellDensity = 0.5; //simulation grid cell density per world space unit volume
+
+ this.timeStep = 1.0 / 60.0;
+
+ this.densitySlider = new Slider(document.getElementById('density-slider'), this.gridCellDensity, 0.2, 3.0, (function (value) {
+ this.gridCellDensity = value;
+
+ this.redrawUI();
+ }).bind(this));
+
+ this.flipnessSlider = new Slider(document.getElementById('fluidity-slider'), this.simulatorRenderer.simulator.flipness, 0.5, 0.99, (function (value) {
+ this.simulatorRenderer.simulator.flipness = value;
+ }).bind(this));
+
+ this.speedSlider = new Slider(document.getElementById('speed-slider'), this.timeStep, 0.0, 1.0 / 60.0, (function (value) {
+ this.timeStep = value;
+ }).bind(this));
+
+
+ this.redrawUI();
+
+
+ this.presetButton.click();
+
+ ///////////////////////////////////////////////////////
+ // interaction state stuff
+
+ canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
+ canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
+ document.addEventListener('mouseup', this.onMouseUp.bind(this));
+
+ document.addEventListener('keydown', this.onKeyDown.bind(this));
+ document.addEventListener('keyup', this.onKeyUp.bind(this));
+
+ window.addEventListener('resize', this.onResize.bind(this));
+ this.onResize();
+
+
+ ////////////////////////////////////////////////////
+ // start the update loop
+
+ var lastTime = 0;
+ var update = (function (currentTime) {
+ var deltaTime = currentTime - lastTime || 0;
+ lastTime = currentTime;
+
+ this.update(deltaTime);
+
+ requestAnimationFrame(update);
+ }).bind(this);
+ update();
+
+
+ }
+ }
+
+ FluidParticles.prototype.onResize = function (event) {
+ this.canvas.width = window.innerWidth;
+ this.canvas.height = window.innerHeight;
+ Utilities.makePerspectiveMatrix(this.projectionMatrix, FOV, this.canvas.width / this.canvas.height, 0.1, 100.0);
+
+ this.simulatorRenderer.onResize(event);
+ }
+
+ FluidParticles.prototype.onMouseMove = function (event) {
+ event.preventDefault();
+
+ if (this.state === State.EDITING) {
+ this.boxEditor.onMouseMove(event);
+
+ if (this.boxEditor.interactionState !== null) {
+ this.editedSinceLastPreset = true;
+ }
+ } else if (this.state === State.SIMULATING) {
+ this.simulatorRenderer.onMouseMove(event);
+ }
+ };
+
+ FluidParticles.prototype.onMouseDown = function (event) {
+ event.preventDefault();
+
+ if (this.state === State.EDITING) {
+ this.boxEditor.onMouseDown(event);
+ } else if (this.state === State.SIMULATING) {
+ this.simulatorRenderer.onMouseDown(event);
+ }
+ };
+
+ FluidParticles.prototype.onMouseUp = function (event) {
+ event.preventDefault();
+
+ if (this.state === State.EDITING) {
+ this.boxEditor.onMouseUp(event);
+ } else if (this.state === State.SIMULATING) {
+ this.simulatorRenderer.onMouseUp(event);
+ }
+ };
+
+ FluidParticles.prototype.onKeyDown = function (event) {
+ if (this.state === State.EDITING) {
+ this.boxEditor.onKeyDown(event);
+ }
+ };
+
+ FluidParticles.prototype.onKeyUp = function (event) {
+ if (this.state === State.EDITING) {
+ this.boxEditor.onKeyUp(event);
+ }
+ };
+
+ //the UI elements are all created in the constructor, this just updates the DOM elements
+ //should be called every time state changes
+ FluidParticles.prototype.redrawUI = function () {
+
+ var simulatingElements = document.querySelectorAll('.simulating-ui');
+ var editingElements = document.querySelectorAll('.editing-ui');
+
+
+ if (this.state === State.SIMULATING) {
+ for (var i = 0; i < simulatingElements.length; ++i) {
+ simulatingElements[i].style.display = 'block';
+ }
+
+ for (var i = 0; i < editingElements.length; ++i) {
+ editingElements[i].style.display = 'none';
+ }
+
+
+ this.startButton.textContent = 'Edit';
+ this.startButton.className = 'start-button-active';
+ } else if (this.state === State.EDITING) {
+ for (var i = 0; i < simulatingElements.length; ++i) {
+ simulatingElements[i].style.display = 'none';
+ }
+
+ for (var i = 0; i < editingElements.length; ++i) {
+ editingElements[i].style.display = 'block';
+ }
+
+ document.getElementById('particle-count').innerHTML = this.getParticleCount().toFixed(0) + ' particles';
+
+ if (this.boxEditor.boxes.length >= 2 ||
+ this.boxEditor.boxes.length === 1 && (this.boxEditor.interactionState === null || this.boxEditor.interactionState.mode !== BoxEditor.InteractionMode.EXTRUDING && this.boxEditor.interactionState.mode !== BoxEditor.InteractionMode.DRAWING)) {
+ this.startButton.className = 'start-button-active';
+ } else {
+ this.startButton.className = 'start-button-inactive';
+ }
+
+ this.startButton.textContent = 'Start';
+
+ if (this.editedSinceLastPreset) {
+ this.presetButton.innerHTML = 'Use Preset';
+ } else {
+ this.presetButton.innerHTML = 'Next Preset';
+ }
+ }
+
+ this.flipnessSlider.redraw();
+ this.densitySlider.redraw();
+ this.speedSlider.redraw();
+ }
+
+
+ //compute the number of particles for the current boxes and grid density
+ FluidParticles.prototype.getParticleCount = function () {
+ var boxEditor = this.boxEditor;
+
+ var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity;
+
+ //assuming x:y:z ratio of 2:1:1
+ var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0));
+ var gridResolutionZ = gridResolutionY * 1;
+ var gridResolutionX = gridResolutionY * 2;
+
+ var totalGridCells = gridResolutionX * gridResolutionY * gridResolutionZ;
+
+
+ var totalVolume = 0;
+ 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)
+
+ for (var i = 0; i < boxEditor.boxes.length; ++i) {
+ var box = boxEditor.boxes[i];
+ var volume = box.computeVolume();
+
+ totalVolume += volume;
+ cumulativeVolume[i] = totalVolume;
+ }
+
+ var fractionFilled = totalVolume / (GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH);
+
+ var desiredParticleCount = fractionFilled * totalGridCells * PARTICLES_PER_CELL; //theoretical number of particles
+
+ return desiredParticleCount;
+ }
+
+ //begin simulation using boxes from box editor
+ //EDITING -> SIMULATING
+ FluidParticles.prototype.startSimulation = function () {
+ this.state = State.SIMULATING;
+
+ var desiredParticleCount = this.getParticleCount(); //theoretical number of particles
+ var particlesWidth = 512; //we fix particlesWidth
+ var particlesHeight = Math.ceil(desiredParticleCount / particlesWidth); //then we calculate the particlesHeight that produces the closest particle count
+
+ var particleCount = particlesWidth * particlesHeight;
+ var particlePositions = [];
+
+ var boxEditor = this.boxEditor;
+
+ var totalVolume = 0;
+ for (var i = 0; i < boxEditor.boxes.length; ++i) {
+ totalVolume += boxEditor.boxes[i].computeVolume();
+ }
+
+ var particlesCreatedSoFar = 0;
+ for (var i = 0; i < boxEditor.boxes.length; ++i) {
+ var box = boxEditor.boxes[i];
+
+ var particlesInBox = 0;
+ if (i < boxEditor.boxes.length - 1) {
+ particlesInBox = Math.floor(particleCount * box.computeVolume() / totalVolume);
+ } else { //for the last box we just use up all the remaining particles
+ particlesInBox = particleCount - particlesCreatedSoFar;
+ }
+
+ for (var j = 0; j < particlesInBox; ++j) {
+ var position = box.randomPoint();
+ particlePositions.push(position);
+ }
+
+ particlesCreatedSoFar += particlesInBox;
+ }
+
+ var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity;
+
+ //assuming x:y:z ratio of 2:1:1
+ var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0));
+ var gridResolutionZ = gridResolutionY * 1;
+ var gridResolutionX = gridResolutionY * 2;
+
+
+ var gridSize = [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH];
+ var gridResolution = [gridResolutionX, gridResolutionY, gridResolutionZ];
+
+ var sphereRadius = 7.0 / gridResolutionX;
+ this.simulatorRenderer.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, PARTICLES_PER_CELL, sphereRadius);
+
+ this.camera.setBounds(0, Math.PI / 2);
+ }
+
+ //go back to box editing
+ //SIMULATING -> EDITING
+ FluidParticles.prototype.stopSimulation = function () {
+ this.state = State.EDITING;
+
+ this.camera.setBounds(-Math.PI / 4, Math.PI / 4);
+ }
+
+ FluidParticles.prototype.update = function () {
+ if (this.state === State.EDITING) {
+ this.boxEditor.draw();
+ } else if (this.state === State.SIMULATING) {
+ this.simulatorRenderer.update(this.timeStep);
+ }
+ }
+
+ return FluidParticles;
+}());
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a4a4c36
--- /dev/null
+++ b/index.html
@@ -0,0 +1,122 @@
+
+
+
+
+ Fluid Particles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/renderer.js b/renderer.js
new file mode 100644
index 0000000..1b2463e
--- /dev/null
+++ b/renderer.js
@@ -0,0 +1,474 @@
+'use strict'
+
+var Renderer = (function () {
+
+ var SHADOW_MAP_WIDTH = 256;
+ var SHADOW_MAP_HEIGHT = 256;
+
+
+ /*
+ we render in a deferred way to a special RGBA texture format
+ the format is (normal.x, normal.y, speed, depth)
+ the normal is normalized (thus z can be reconstructed with sqrt(1.0 - x * x - y * y)
+ the depth simply the z in view space
+ */
+
+ //returns {vertices, normals, indices}
+ function generateSphereGeometry (iterations) {
+
+ var vertices = [],
+ normals = [];
+
+ var compareVectors = function (a, b) {
+ var EPSILON = 0.001;
+ return Math.abs(a[0] - b[0]) < EPSILON && Math.abs(a[1] - b[1]) < EPSILON && Math.abs(a[2] - b[2]) < EPSILON;
+ };
+
+ var addVertex = function (v) {
+ Utilities.normalizeVector(v, v);
+ vertices.push(v);
+ normals.push(v);
+ };
+
+ var getMiddlePoint = function (vertexA, vertexB) {
+ var middle = [
+ (vertexA[0] + vertexB[0]) / 2.0,
+ (vertexA[1] + vertexB[1]) / 2.0,
+ (vertexA[2] + vertexB[2]) / 2.0];
+
+ Utilities.normalizeVector(middle, middle);
+
+ for (var i = 0; i < vertices.length; ++i) {
+ if (compareVectors(vertices[i], middle)) {
+ return i;
+ }
+ }
+
+ addVertex(middle);
+ return (vertices.length - 1);
+ };
+
+
+ var t = (1.0 + Math.sqrt(5.0)) / 2.0;
+
+ addVertex([-1, t, 0]);
+ addVertex([1, t, 0]);
+ addVertex([-1, -t, 0]);
+ addVertex([1, -t, 0]);
+
+ addVertex([0, -1, t]);
+ addVertex([0, 1, t]);
+ addVertex([0, -1, -t]);
+ addVertex([0, 1, -t]);
+
+ addVertex([t, 0, -1]);
+ addVertex([t, 0, 1]);
+ addVertex([-t, 0, -1]);
+ addVertex([-t, 0, 1]);
+
+
+ var faces = [];
+ faces.push([0, 11, 5]);
+ faces.push([0, 5, 1]);
+ faces.push([0, 1, 7]);
+ faces.push([0, 7, 10]);
+ faces.push([0, 10, 11]);
+
+ faces.push([1, 5, 9]);
+ faces.push([5, 11, 4]);
+ faces.push([11, 10, 2]);
+ faces.push([10, 7, 6]);
+ faces.push([7, 1, 8]);
+
+ faces.push([3, 9, 4]);
+ faces.push([3, 4, 2]);
+ faces.push([3, 2, 6]);
+ faces.push([3, 6, 8]);
+ faces.push([3, 8, 9]);
+
+ faces.push([4, 9, 5]);
+ faces.push([2, 4, 11]);
+ faces.push([6, 2, 10]);
+ faces.push([8, 6, 7]);
+ faces.push([9, 8, 1]);
+
+
+ for (var i = 0; i < iterations; ++i) {
+ var faces2 = [];
+
+ for (var i = 0; i < faces.length; ++i) {
+ var face = faces[i];
+ //replace triangle with 4 triangles
+ var a = getMiddlePoint(vertices[face[0]], vertices[face[1]]);
+ var b = getMiddlePoint(vertices[face[1]], vertices[face[2]]);
+ var c = getMiddlePoint(vertices[face[2]], vertices[face[0]]);
+
+ faces2.push([face[0], a, c]);
+ faces2.push([face[1], b, a]);
+ faces2.push([face[2], c, b]);
+ faces2.push([a, b, c]);
+ }
+
+ faces = faces2;
+ }
+
+
+ var packedVertices = [],
+ packedNormals = [],
+ indices = [];
+
+ for (var i = 0; i < vertices.length; ++i) {
+ packedVertices.push(vertices[i][0]);
+ packedVertices.push(vertices[i][1]);
+ packedVertices.push(vertices[i][2]);
+
+ packedNormals.push(normals[i][0]);
+ packedNormals.push(normals[i][1]);
+ packedNormals.push(normals[i][2]);
+ }
+
+ for (var i = 0; i < faces.length; ++i) {
+ var face = faces[i];
+ indices.push(face[0]);
+ indices.push(face[1]);
+ indices.push(face[2]);
+ }
+
+ return {
+ vertices: packedVertices,
+ normals: packedNormals,
+ indices: indices
+ }
+ }
+
+
+ //you need to call reset() before drawing
+ function Renderer (canvas, wgl, gridDimensions, onLoaded) {
+
+ this.canvas = canvas;
+ this.wgl = wgl;
+
+ this.particlesWidth = 0;
+ this.particlesHeight = 0;
+
+ this.sphereRadius = 0.0;
+
+ this.wgl.getExtension('ANGLE_instanced_arrays');
+ this.depthExt = this.wgl.getExtension('WEBGL_depth_texture');
+
+
+ this.quadVertexBuffer = wgl.createBuffer();
+ 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);
+
+
+ ///////////////////////////////////////////////////////
+ // create stuff for rendering
+
+ var sphereGeometry = this.sphereGeometry = generateSphereGeometry(3);
+
+ this.sphereVertexBuffer = wgl.createBuffer();
+ wgl.bufferData(this.sphereVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array(sphereGeometry.vertices), wgl.STATIC_DRAW);
+
+ this.sphereNormalBuffer = wgl.createBuffer();
+ wgl.bufferData(this.sphereNormalBuffer, wgl.ARRAY_BUFFER, new Float32Array(sphereGeometry.normals), wgl.STATIC_DRAW);
+
+ this.sphereIndexBuffer = wgl.createBuffer();
+ wgl.bufferData(this.sphereIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(sphereGeometry.indices), wgl.STATIC_DRAW);
+
+ this.depthFramebuffer = wgl.createFramebuffer();
+ 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);
+ 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);
+
+
+ //we light directly from above
+ this.lightViewMatrix = new Float32Array(16);
+ var midpoint = [gridDimensions[0] / 2, gridDimensions[1] / 2, gridDimensions[2] / 2];
+ Utilities.makeLookAtMatrix(this.lightViewMatrix, midpoint, [midpoint[0], midpoint[1] - 1.0, midpoint[2]], [0.0, 0.0, 1.0]);
+ 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);
+ this.lightProjectionViewMatrix = new Float32Array(16);
+ Utilities.premultiplyMatrix(this.lightProjectionViewMatrix, this.lightViewMatrix, this.lightProjectionMatrix);
+
+
+ this.particleVertexBuffer = wgl.createBuffer();
+
+ this.renderingFramebuffer = wgl.createFramebuffer();
+ this.renderingRenderbuffer = wgl.createRenderbuffer();
+ this.renderingTexture = wgl.createTexture();
+ this.occlusionTexture = wgl.createTexture();
+ this.compositingTexture = wgl.createTexture();
+
+
+ this.onResize();
+
+ wgl.createProgramsFromFiles({
+ sphereProgram: {
+ vertexShader: 'shaders/sphere.vert',
+ fragmentShader: 'shaders/sphere.frag'
+ },
+ sphereDepthProgram: {
+ vertexShader: 'shaders/spheredepth.vert',
+ fragmentShader: 'shaders/spheredepth.frag'
+ },
+ sphereAOProgram: {
+ vertexShader: 'shaders/sphereao.vert',
+ fragmentShader: 'shaders/sphereao.frag'
+ },
+ compositeProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: 'shaders/composite.frag',
+ attributeLocations: { 'a_position': 0}
+ },
+ fxaaProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: 'shaders/fxaa.frag',
+ attributeLocations: { 'a_position': 0}
+ },
+ }, (function (programs) {
+ for (var programName in programs) {
+ this[programName] = programs[programName];
+ }
+
+ onLoaded();
+ }).bind(this));
+ }
+
+ Renderer.prototype.onResize = function (event) {
+ wgl.renderbufferStorage(this.renderingRenderbuffer, wgl.RENDERBUFFER, wgl.DEPTH_COMPONENT16, this.canvas.width, this.canvas.height);
+ 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)
+
+ 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);
+
+ 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);
+ }
+
+
+ Renderer.prototype.reset = function (particlesWidth, particlesHeight, sphereRadius) {
+ this.particlesWidth = particlesWidth;
+ this.particlesHeight = particlesHeight;
+
+ this.sphereRadius = sphereRadius;
+
+ ///////////////////////////////////////////////////////////
+ // create particle data
+
+ var particleCount = this.particlesWidth * this.particlesHeight;
+
+ //fill particle vertex buffer containing the relevant texture coordinates
+ var particleTextureCoordinates = new Float32Array(this.particlesWidth * this.particlesHeight * 2);
+ for (var y = 0; y < this.particlesHeight; ++y) {
+ for (var x = 0; x < this.particlesWidth; ++x) {
+ particleTextureCoordinates[(y * this.particlesWidth + x) * 2] = (x + 0.5) / this.particlesWidth;
+ particleTextureCoordinates[(y * this.particlesWidth + x) * 2 + 1] = (y + 0.5) / this.particlesHeight;
+ }
+ }
+
+ wgl.bufferData(this.particleVertexBuffer, wgl.ARRAY_BUFFER, particleTextureCoordinates, wgl.STATIC_DRAW);
+ }
+
+ //you need to call reset() with the correct parameters before drawing anything
+ //projectionMatrix and viewMatrix are both expected to be Float32Array(16)
+ Renderer.prototype.draw = function (simulator, projectionMatrix, viewMatrix) {
+ var wgl = this.wgl;
+
+ /////////////////////////////////////////////
+ // draw particles
+
+
+ var projectionViewMatrix = Utilities.premultiplyMatrix(new Float32Array(16), viewMatrix, projectionMatrix);
+
+
+ ///////////////////////////////////////////////
+ //draw rendering data (normal, speed, depth)
+
+ wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.renderingTexture, 0);
+ wgl.framebufferRenderbuffer(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.DEPTH_ATTACHMENT, wgl.RENDERBUFFER, this.renderingRenderbuffer);
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(-99999.0, -99999.0, -99999.0, -99999.0),
+ wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT);
+
+
+ var sphereDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.renderingFramebuffer)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .enable(wgl.DEPTH_TEST)
+ .enable(wgl.CULL_FACE)
+
+ .useProgram(this.sphereProgram)
+
+ .vertexAttribPointer(this.sphereVertexBuffer, this.sphereProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+ .vertexAttribPointer(this.sphereNormalBuffer, this.sphereProgram.getAttribLocation('a_vertexNormal'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .vertexAttribPointer(this.particleVertexBuffer, this.sphereProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+ .vertexAttribDivisorANGLE(this.sphereProgram.getAttribLocation('a_textureCoordinates'), 1)
+
+ .bindIndexBuffer(this.sphereIndexBuffer)
+
+ .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix)
+ .uniformMatrix4fv('u_viewMatrix', false, viewMatrix)
+
+ .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture)
+ .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture)
+
+ .uniform1f('u_sphereRadius', this.sphereRadius)
+
+
+ wgl.drawElementsInstancedANGLE(sphereDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight);
+
+
+
+ ///////////////////////////////////////////////////
+ // draw occlusion
+
+ wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.occlusionTexture, 0);
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(0.0, 0.0, 0.0, 0.0),
+ wgl.COLOR_BUFFER_BIT);
+
+ var fov = 2.0 * Math.atan(1.0 / projectionMatrix[5]);
+
+ var occlusionDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.renderingFramebuffer)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .enable(wgl.DEPTH_TEST)
+ .depthMask(false)
+
+ .enable(wgl.CULL_FACE)
+
+ .enable(wgl.BLEND)
+ .blendEquation(wgl.FUNC_ADD)
+ .blendFuncSeparate(wgl.ONE, wgl.ONE, wgl.ONE, wgl.ONE)
+
+ .useProgram(this.sphereAOProgram)
+
+ .vertexAttribPointer(this.sphereVertexBuffer, this.sphereAOProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+ .vertexAttribPointer(this.particleVertexBuffer, this.sphereAOProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+ .vertexAttribDivisorANGLE(this.sphereAOProgram.getAttribLocation('a_textureCoordinates'), 1)
+
+
+ .bindIndexBuffer(this.sphereIndexBuffer)
+
+ .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix)
+ .uniformMatrix4fv('u_viewMatrix', false, viewMatrix)
+
+ .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture)
+ .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture)
+
+ .uniformTexture('u_renderingTexture', 2, wgl.TEXTURE_2D, this.renderingTexture)
+ .uniform2f('u_resolution', this.canvas.width, this.canvas.height)
+ .uniform1f('u_fov', fov)
+
+
+ .uniform1f('u_sphereRadius', this.sphereRadius)
+
+
+ wgl.drawElementsInstancedANGLE(occlusionDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight);
+
+
+ ////////////////////////////////////////////////
+ // draw depth map
+
+ wgl.framebufferTexture2D(this.depthFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.depthColorTexture, 0);
+ wgl.framebufferTexture2D(this.depthFramebuffer, wgl.FRAMEBUFFER, wgl.DEPTH_ATTACHMENT, wgl.TEXTURE_2D, this.depthTexture, 0);
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.depthFramebuffer).clearColor(0, 0, 0, 0),
+ wgl.DEPTH_BUFFER_BIT);
+
+
+ var depthDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.depthFramebuffer)
+ .viewport(0, 0, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT)
+
+ .enable(wgl.DEPTH_TEST)
+ .depthMask(true)
+
+ //so no occlusion past end of shadow map (with clamp to edge)
+ .enable(wgl.SCISSOR_TEST)
+ .scissor(1, 1, SHADOW_MAP_WIDTH - 2, SHADOW_MAP_HEIGHT - 2)
+
+ .colorMask(false, false, false, false)
+
+ .enable(wgl.CULL_FACE)
+
+ .useProgram(this.sphereDepthProgram)
+
+ .vertexAttribPointer(this.sphereVertexBuffer, this.sphereDepthProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0)
+ .vertexAttribPointer(this.particleVertexBuffer, this.sphereDepthProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+ .vertexAttribDivisorANGLE(this.sphereDepthProgram.getAttribLocation('a_textureCoordinates'), 1)
+
+ .bindIndexBuffer(this.sphereIndexBuffer)
+
+ .uniformMatrix4fv('u_projectionViewMatrix', false, this.lightProjectionViewMatrix)
+
+ .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture)
+ .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture)
+
+ .uniform1f('u_sphereRadius', this.sphereRadius)
+
+
+ wgl.drawElementsInstancedANGLE(depthDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight);
+
+
+ ///////////////////////////////////////////
+ // composite
+
+
+ var inverseViewMatrix = Utilities.invertMatrix(new Float32Array(16), viewMatrix);
+
+ wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.compositingTexture, 0);
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(0, 0, 0, 0),
+ wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT);
+
+ var compositeDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.renderingFramebuffer)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .useProgram(this.compositeProgram)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .uniformTexture('u_renderingTexture', 0, wgl.TEXTURE_2D, this.renderingTexture)
+ .uniformTexture('u_occlusionTexture', 1, wgl.TEXTURE_2D, this.occlusionTexture)
+ .uniform2f('u_resolution', this.canvas.width, this.canvas.height)
+ .uniform1f('u_fov', fov)
+
+ .uniformMatrix4fv('u_inverseViewMatrix', false, inverseViewMatrix)
+
+ .uniformTexture('u_shadowDepthTexture', 2, wgl.TEXTURE_2D, this.depthTexture)
+ .uniform2f('u_shadowResolution', SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT)
+ .uniformMatrix4fv('u_lightProjectionViewMatrix', false, this.lightProjectionViewMatrix);
+
+ wgl.drawArrays(compositeDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+
+ //////////////////////////////////////
+ // FXAA
+
+ var inverseViewMatrix = Utilities.invertMatrix(new Float32Array(16), viewMatrix);
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(null).clearColor(0, 0, 0, 0),
+ wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT);
+
+ var fxaaDrawState = wgl.createDrawState()
+ .bindFramebuffer(null)
+ .viewport(0, 0, this.canvas.width, this.canvas.height)
+
+ .useProgram(this.fxaaProgram)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .uniformTexture('u_input', 0, wgl.TEXTURE_2D, this.compositingTexture)
+ .uniform2f('u_resolution', this.canvas.width, this.canvas.height);
+
+ wgl.drawArrays(fxaaDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+ }
+
+ return Renderer;
+}());
diff --git a/shaders/addforce.frag b/shaders/addforce.frag
new file mode 100644
index 0000000..03fb9eb
--- /dev/null
+++ b/shaders/addforce.frag
@@ -0,0 +1,42 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_velocityTexture;
+
+uniform vec3 u_mouseVelocity;
+
+uniform vec3 u_gridResolution;
+uniform vec3 u_gridSize;
+
+uniform vec3 u_mouseRayOrigin;
+uniform vec3 u_mouseRayDirection;
+
+uniform float u_timeStep;
+
+float kernel (vec3 position, float radius) {
+ vec3 worldPosition = (position / u_gridResolution) * u_gridSize;
+
+ float distanceToMouseRay = length(cross(u_mouseRayDirection, worldPosition - u_mouseRayOrigin));
+
+ float normalizedDistance = max(0.0, distanceToMouseRay / radius);
+ return smoothstep(1.0, 0.9, normalizedDistance);
+}
+
+void main () {
+ vec3 velocity = texture2D(u_velocityTexture, v_coordinates).rgb;
+
+ vec3 newVelocity = velocity + vec3(0.0, -40.0 * u_timeStep, 0.0); //add gravity
+
+ vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0));
+ vec3 xPosition = vec3(cellIndex.x, cellIndex.y + 0.5, cellIndex.z + 0.5);
+ vec3 yPosition = vec3(cellIndex.x + 0.5, cellIndex.y, cellIndex.z + 0.5);
+ vec3 zPosition = vec3(cellIndex.x + 0.5, cellIndex.y + 0.5, cellIndex.z);
+
+ float mouseRadius = 5.0;
+ vec3 kernelValues = vec3(kernel(xPosition, mouseRadius), kernel(yPosition, mouseRadius), kernel(zPosition, mouseRadius));
+
+ newVelocity += u_mouseVelocity * kernelValues * 3.0 * smoothstep(0.0, 1.0 / 200.0, u_timeStep);
+
+ gl_FragColor = vec4(newVelocity * 1.0, 0.0);
+}
diff --git a/shaders/advect.frag b/shaders/advect.frag
new file mode 100644
index 0000000..f8c8721
--- /dev/null
+++ b/shaders/advect.frag
@@ -0,0 +1,59 @@
+//advects particle positions with second order runge kutta
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_positionsTexture;
+uniform sampler2D u_randomsTexture;
+
+uniform sampler2D u_velocityGrid;
+
+uniform vec3 u_gridResolution;
+uniform vec3 u_gridSize;
+
+uniform float u_timeStep;
+
+uniform float u_frameNumber;
+
+uniform vec2 u_particlesResolution;
+
+float sampleXVelocity (vec3 position) {
+ vec3 cellIndex = vec3(position.x, position.y - 0.5, position.z - 0.5);
+ return texture3D(u_velocityGrid, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x;
+}
+
+float sampleYVelocity (vec3 position) {
+ vec3 cellIndex = vec3(position.x - 0.5, position.y, position.z - 0.5);
+ return texture3D(u_velocityGrid, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y;
+}
+
+float sampleZVelocity (vec3 position) {
+ vec3 cellIndex = vec3(position.x - 0.5, position.y - 0.5, position.z);
+ return texture3D(u_velocityGrid, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z;
+}
+
+vec3 sampleVelocity (vec3 position) {
+ vec3 gridPosition = (position / u_gridSize) * u_gridResolution;
+ return vec3(sampleXVelocity(gridPosition), sampleYVelocity(gridPosition), sampleZVelocity(gridPosition));
+}
+
+void main () {
+ vec3 position = texture2D(u_positionsTexture, v_coordinates).rgb;
+ vec3 randomDirection = texture2D(u_randomsTexture, fract(v_coordinates + u_frameNumber / u_particlesResolution)).rgb;
+
+ vec3 velocity = sampleVelocity(position);
+
+ vec3 halfwayPosition = position + velocity * u_timeStep * 0.5;
+ vec3 halfwayVelocity = sampleVelocity(halfwayPosition);
+
+ vec3 step = halfwayVelocity * u_timeStep;
+
+ step += 0.05 * randomDirection * length(velocity) * u_timeStep;
+
+ //step = clamp(step, -vec3(1.0), vec3(1.0)); //enforce CFL condition
+
+ vec3 newPosition = position + step;
+
+ newPosition = clamp(newPosition, vec3(0.01), u_gridSize - 0.01);
+
+ gl_FragColor = vec4(newPosition, 0.0);
+}
diff --git a/shaders/background.frag b/shaders/background.frag
new file mode 100644
index 0000000..3445ffd
--- /dev/null
+++ b/shaders/background.frag
@@ -0,0 +1,8 @@
+precision highp float;
+
+varying vec2 v_position;
+
+void main () {
+ vec3 backgroundColor = vec3(1.0) - length(v_position) * 0.1;
+ gl_FragColor = vec4(backgroundColor, 1.0);
+}
diff --git a/shaders/background.vert b/shaders/background.vert
new file mode 100644
index 0000000..46bfa3a
--- /dev/null
+++ b/shaders/background.vert
@@ -0,0 +1,10 @@
+precision highp float;
+
+attribute vec2 a_position;
+
+varying vec2 v_position;
+
+void main () {
+ v_position = a_position;
+ gl_Position = vec4(a_position, 0.0, 1.0);
+}
diff --git a/shaders/box.frag b/shaders/box.frag
new file mode 100644
index 0000000..0bff27d
--- /dev/null
+++ b/shaders/box.frag
@@ -0,0 +1,17 @@
+precision highp float;
+
+varying vec3 v_cubePosition;
+
+uniform vec3 u_highlightSide;
+uniform vec3 u_highlightColor;
+
+void main () {
+ float epsilon = 0.001;
+ vec3 normalizedCubePosition = v_cubePosition * 2.0 - 1.0;
+
+ if (abs(normalizedCubePosition.x - u_highlightSide.x) < epsilon || abs(normalizedCubePosition.y - u_highlightSide.y) < epsilon || abs(normalizedCubePosition.z - u_highlightSide.z) < epsilon ) {
+ gl_FragColor = vec4(u_highlightColor, 1.0);
+ } else {
+ gl_FragColor = vec4(vec3(0.97), 1.0);
+ }
+}
diff --git a/shaders/box.vert b/shaders/box.vert
new file mode 100644
index 0000000..4bec929
--- /dev/null
+++ b/shaders/box.vert
@@ -0,0 +1,17 @@
+precision highp float;
+
+attribute vec3 a_cubeVertexPosition;
+
+uniform vec3 u_translation;
+uniform vec3 u_scale;
+
+uniform mat4 u_viewMatrix;
+uniform mat4 u_projectionMatrix;
+
+varying vec3 v_cubePosition;
+
+void main () {
+ v_cubePosition = a_cubeVertexPosition;
+
+ gl_Position = u_projectionMatrix * u_viewMatrix * vec4(a_cubeVertexPosition * u_scale + u_translation, 1.0);
+}
diff --git a/shaders/boxwireframe.frag b/shaders/boxwireframe.frag
new file mode 100644
index 0000000..a77a87f
--- /dev/null
+++ b/shaders/boxwireframe.frag
@@ -0,0 +1,5 @@
+precision highp float;
+
+void main () {
+ gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
+}
diff --git a/shaders/boxwireframe.vert b/shaders/boxwireframe.vert
new file mode 100644
index 0000000..988fc5c
--- /dev/null
+++ b/shaders/boxwireframe.vert
@@ -0,0 +1,13 @@
+precision highp float;
+
+attribute vec3 a_cubeVertexPosition;
+
+uniform vec3 u_translation;
+uniform vec3 u_scale;
+
+uniform mat4 u_viewMatrix;
+uniform mat4 u_projectionMatrix;
+
+void main () {
+ gl_Position = u_projectionMatrix * u_viewMatrix * vec4(a_cubeVertexPosition * u_scale + u_translation, 1.0);
+}
diff --git a/shaders/common.frag b/shaders/common.frag
new file mode 100644
index 0000000..12831b3
--- /dev/null
+++ b/shaders/common.frag
@@ -0,0 +1,64 @@
+precision highp float;
+
+vec3 get3DFragCoord (vec3 resolution) {
+ return vec3(
+ mod(gl_FragCoord.x, resolution.x),
+ gl_FragCoord.y,
+ floor(gl_FragCoord.x / resolution.x) + 0.5);
+}
+
+vec4 texture3D(sampler2D texture, vec3 coordinates, vec3 resolution) {
+ vec3 fullCoordinates = coordinates * resolution; //in [(0, 0, 0), (resolution.x, resolution.y, resolutionz)]
+
+ fullCoordinates = clamp(fullCoordinates, vec3(0.5), vec3(resolution - 0.5));
+
+ //belowZIndex and aboveZIndex don't have the 0.5 offset
+ float belowZIndex = floor(fullCoordinates.z - 0.5);
+ float aboveZIndex = belowZIndex + 1.0;
+
+ //we interpolate the z
+ float fraction = fract(fullCoordinates.z - 0.5);
+
+ vec2 belowCoordinates = vec2(
+ belowZIndex * resolution.x + fullCoordinates.x,
+ fullCoordinates.y) / vec2(resolution.x * resolution.z, resolution.y);
+
+ vec2 aboveCoordinates = vec2(
+ aboveZIndex * resolution.x + fullCoordinates.x,
+ fullCoordinates.y) / vec2(resolution.x * resolution.z, resolution.y);
+
+ return mix(texture2D(texture, belowCoordinates), texture2D(texture, aboveCoordinates), fraction);
+}
+
+vec4 texture3DNearest(sampler2D texture, vec3 coordinates, vec3 resolution) { //clamps the z coordinate
+ vec3 fullCoordinates = coordinates * resolution; //in [(0, 0, 0), (resolution.x, resolution.y, resolutionz)]
+
+ fullCoordinates = clamp(fullCoordinates, vec3(0.5), vec3(resolution - 0.5));
+
+ float zIndex = floor(fullCoordinates.z);
+
+ vec2 textureCoordinates = vec2(
+ zIndex * resolution.x + fullCoordinates.x,
+ fullCoordinates.y) / vec2(resolution.x * resolution.z, resolution.y);
+
+ return texture2D(texture, textureCoordinates);
+}
+
+/*
+vec4 texture3D(sampler2D tex, vec3 texCoord, vec3 resolution) {
+ float size = resolution.z;
+ float sliceSize = 1.0 / size; // space of 1 slice
+ float slicePixelSize = sliceSize / size; // space of 1 pixel
+ float sliceInnerSize = slicePixelSize * (size - 1.0); // space of size pixels
+ float zSlice0 = min(floor(texCoord.z * size), size - 1.0);
+ float zSlice1 = min(zSlice0 + 1.0, size - 1.0);
+ float xOffset = slicePixelSize * 0.5 + texCoord.x * sliceInnerSize;
+ float s0 = xOffset + (zSlice0 * sliceSize);
+ float s1 = xOffset + (zSlice1 * sliceSize);
+ vec4 slice0Color = texture2D(tex, vec2(s0, texCoord.y));
+ vec4 slice1Color = texture2D(tex, vec2(s1, texCoord.y));
+ float zOffset = mod(texCoord.z * size, 1.0);
+ return mix(slice0Color, slice1Color, zOffset);
+}
+*/
+
diff --git a/shaders/composite.frag b/shaders/composite.frag
new file mode 100644
index 0000000..de630de
--- /dev/null
+++ b/shaders/composite.frag
@@ -0,0 +1,77 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_renderingTexture;
+uniform sampler2D u_occlusionTexture;
+
+uniform vec2 u_resolution;
+uniform float u_fov;
+
+uniform mat4 u_inverseViewMatrix;
+
+uniform sampler2D u_shadowDepthTexture;
+uniform vec2 u_shadowResolution;
+uniform mat4 u_lightProjectionViewMatrix;
+
+float linearstep (float left, float right, float x) {
+ return clamp((x - left) / (right - left), 0.0, 1.0);
+}
+
+vec3 hsvToRGB(vec3 c) {
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+void main () {
+ vec4 data = texture2D(u_renderingTexture, v_coordinates);
+ float occlusion = texture2D(u_occlusionTexture, v_coordinates).r;
+
+ vec3 viewSpaceNormal = vec3(data.x, data.y, sqrt(1.0 - data.x * data.x - data.y * data.y));
+
+ float viewSpaceZ = data.a;
+ vec3 viewRay = vec3(
+ (v_coordinates.x * 2.0 - 1.0) * tan(u_fov / 2.0) * u_resolution.x / u_resolution.y,
+ (v_coordinates.y * 2.0 - 1.0) * tan(u_fov / 2.0),
+ -1.0);
+
+ vec3 viewSpacePosition = viewRay * -viewSpaceZ;
+ vec3 worldSpacePosition = vec3(u_inverseViewMatrix * vec4(viewSpacePosition, 1.0));
+
+ float speed = data.b;
+ vec3 color = hsvToRGB(vec3(max(0.6 - speed * 0.0025, 0.52), 0.75, 1.0));
+
+
+ vec4 lightSpacePosition = u_lightProjectionViewMatrix * vec4(worldSpacePosition, 1.0);
+ lightSpacePosition /= lightSpacePosition.w;
+ lightSpacePosition *= 0.5;
+ lightSpacePosition += 0.5;
+ vec2 lightSpaceCoordinates = lightSpacePosition.xy;
+
+ float shadow = 1.0;
+ const int PCF_WIDTH = 2;
+ const float PCF_NORMALIZATION = float(PCF_WIDTH * 2 + 1) * float(PCF_WIDTH * 2 + 1);
+
+ for (int xOffset = -PCF_WIDTH; xOffset <= PCF_WIDTH; ++xOffset) {
+ for (int yOffset = -PCF_WIDTH; yOffset <= PCF_WIDTH; ++yOffset) {
+ float shadowSample = texture2D(u_shadowDepthTexture, lightSpaceCoordinates + 5.0 * vec2(float(xOffset), float(yOffset)) / u_shadowResolution).r;
+ if (lightSpacePosition.z > shadowSample + 0.001) shadow -= 1.0 / PCF_NORMALIZATION;
+ }
+ }
+
+
+ float ambient = 1.0 - occlusion * 0.7;
+ float direct = 1.0 - (1.0 - shadow) * 0.8;
+
+ color *= ambient * direct;
+
+ if (speed >= 0.0) {
+ gl_FragColor = vec4(color, 1.0);
+ } else {
+ vec3 backgroundColor = vec3(1.0) - length(v_coordinates * 2.0 - 1.0) * 0.1;
+ gl_FragColor = vec4(backgroundColor, 1.0);
+ }
+
+ //gl_FragColor = vec4(texture2D(u_shadowDepthTexture, v_coordinates).rrr, 1.0);
+}
diff --git a/shaders/copy.frag b/shaders/copy.frag
new file mode 100644
index 0000000..a574c21
--- /dev/null
+++ b/shaders/copy.frag
@@ -0,0 +1,9 @@
+precision highp float;
+
+uniform sampler2D u_texture;
+
+varying vec2 v_coordinates;
+
+void main () {
+ gl_FragColor = texture2D(u_texture, v_coordinates);
+}
diff --git a/shaders/divergence.frag b/shaders/divergence.frag
new file mode 100644
index 0000000..7c0498d
--- /dev/null
+++ b/shaders/divergence.frag
@@ -0,0 +1,36 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_velocityTexture;
+uniform sampler2D u_markerTexture;
+uniform sampler2D u_weightTexture;
+
+uniform vec3 u_gridResolution;
+
+uniform float u_maxDensity;
+
+void main () {
+ vec3 cellIndex = floor(get3DFragCoord(u_gridResolution));
+
+ //divergence = 0 in air cells
+ float fluidCell = texture3DNearest(u_markerTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).x;
+ if (fluidCell == 0.0) discard;
+
+
+ float leftX = texture3DNearest(u_velocityTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x;
+ float rightX = texture3DNearest(u_velocityTexture, (cellIndex + vec3(1.0, 0.0, 0.0) + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x;
+
+ float bottomY = texture3DNearest(u_velocityTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y;
+ float topY = texture3DNearest(u_velocityTexture, (cellIndex + vec3(0.0, 1.0, 0.0) + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y;
+
+ float backZ = texture3DNearest(u_velocityTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z;
+ float frontZ = texture3DNearest(u_velocityTexture, (cellIndex + vec3(0.0, 0.0, 1.0) + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z;
+
+ float divergence = ((rightX - leftX) + (topY - bottomY) + (frontZ - backZ)) / 1.0;
+
+ float density = texture3DNearest(u_weightTexture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).a;
+ divergence -= max((density - u_maxDensity) * 1.0, 0.0); //volume conservation
+
+ gl_FragColor = vec4(divergence, 0.0, 0.0, 0.0);
+}
diff --git a/shaders/enforceboundaries.frag b/shaders/enforceboundaries.frag
new file mode 100644
index 0000000..bdd5473
--- /dev/null
+++ b/shaders/enforceboundaries.frag
@@ -0,0 +1,40 @@
+//sets the velocities at the boundary cells
+
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_velocityTexture;
+
+uniform vec3 u_gridResolution;
+
+void main () {
+ vec3 velocity = texture2D(u_velocityTexture, v_coordinates).rgb;
+ vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0));
+
+ if (cellIndex.x < 0.5) {
+ velocity.x = 0.0;
+ }
+
+ if (cellIndex.x > u_gridResolution.x - 0.5) {
+ velocity.x = 0.0;
+ }
+
+ if (cellIndex.y < 0.5) {
+ velocity.y = 0.0;
+ }
+
+ if (cellIndex.y > u_gridResolution.y - 0.5) {
+ velocity.y = min(velocity.y, 0.0);
+ }
+
+ if (cellIndex.z < 0.5) {
+ velocity.z = 0.0;
+ }
+
+ if (cellIndex.z > u_gridResolution.z - 0.5) {
+ velocity.z = 0.0;
+ }
+
+ gl_FragColor = vec4(velocity, 0.0);
+}
diff --git a/shaders/extendvelocity.frag b/shaders/extendvelocity.frag
new file mode 100644
index 0000000..60827ba
--- /dev/null
+++ b/shaders/extendvelocity.frag
@@ -0,0 +1,46 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform vec2 u_gridResolution;
+
+uniform sampler2D u_velocityTexture;
+uniform sampler2D u_weightTexture;
+
+void main () {
+ vec2 velocity = texture2D(u_velocityTexture, v_coordinates).rg;
+
+ vec2 delta = 1.0 / (u_gridResolution + 1.0);
+
+ bool airX = texture2D(u_weightTexture, v_coordinates).x == 0.0;
+ bool airY = texture2D(u_weightTexture, v_coordinates).y == 0.0;
+
+ float closestXDistance = 100000.0;
+ float closestYDistance = 100000.0;
+
+ if (airX || airY) {
+ const int SEARCH_WIDTH = 1;
+ for (int y = -SEARCH_WIDTH; y <= SEARCH_WIDTH; ++y) {
+ for (int x = -SEARCH_WIDTH; x <= SEARCH_WIDTH; ++x) {
+ if (x != 0 && y != 0) {
+ vec2 coordinates = v_coordinates + vec2(float(x), float(y)) * delta;
+ float dist = float(x) * float(x) + float(y) * float(y);
+
+ if (texture2D(u_weightTexture, coordinates).x > 0.0 && dist < closestXDistance && airX) {
+ closestXDistance = dist;
+ velocity.x = texture2D(u_velocityTexture, coordinates).r;
+ }
+
+ if (texture2D(u_weightTexture, coordinates).y > 0.0 && dist < closestYDistance && airY) {
+ closestYDistance = dist;
+ velocity.y = texture2D(u_velocityTexture, coordinates).g;
+ }
+
+ }
+ }
+ }
+
+ }
+
+ gl_FragColor = vec4(velocity, 0.0, 0.0);
+}
diff --git a/shaders/fullscreen.vert b/shaders/fullscreen.vert
new file mode 100644
index 0000000..8182322
--- /dev/null
+++ b/shaders/fullscreen.vert
@@ -0,0 +1,11 @@
+precision highp float;
+
+attribute vec2 a_position;
+
+varying vec2 v_coordinates;
+
+void main () {
+ v_coordinates = a_position * 0.5 + 0.5;
+
+ gl_Position = vec4(a_position, 0.0, 1.0);
+}
diff --git a/shaders/fxaa.frag b/shaders/fxaa.frag
new file mode 100644
index 0000000..2d0fc0b
--- /dev/null
+++ b/shaders/fxaa.frag
@@ -0,0 +1,48 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_input;
+
+uniform vec2 u_resolution;
+
+const float FXAA_SPAN_MAX = 8.0;
+const float FXAA_REDUCE_MUL = 1.0 / 8.0;
+const float FXAA_REDUCE_MIN = 1.0 / 128.0;
+
+void main () {
+ vec2 delta = 1.0 / u_resolution;
+
+ vec3 rgbNW = texture2D(u_input, v_coordinates + vec2(-1.0, -1.0) * delta).rgb;
+ vec3 rgbNE = texture2D(u_input, v_coordinates + vec2(1.0, -1.0) * delta).rgb;
+ vec3 rgbSW = texture2D(u_input, v_coordinates + vec2(-1.0, 1.0) * delta).rgb;
+ vec3 rgbSE = texture2D(u_input, v_coordinates + vec2(1.0, 1.0) * delta).rgb;
+ vec3 rgbM = texture2D(u_input, v_coordinates).rgb;
+
+ vec3 luma = vec3(0.299, 0.587, 0.114);
+ float lumaNW = dot(rgbNW, luma);
+ float lumaNE = dot(rgbNE, luma);
+ float lumaSW = dot(rgbSW, luma);
+ float lumaSE = dot(rgbSE, luma);
+ float lumaM = dot(rgbM, luma);
+
+ float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
+ float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
+
+ vec2 dir = vec2(
+ -((lumaNW + lumaNE) - (lumaSW + lumaSE)),
+ ((lumaNW + lumaSW) - (lumaNE + lumaSE)));
+
+ float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN);
+ float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
+ dir = min(vec2(FXAA_SPAN_MAX), max(vec2(-FXAA_SPAN_MAX), dir * rcpDirMin)) * delta.xy;
+
+ 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);
+ 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);
+ float lumaB = dot(rgbB, luma);
+ if (lumaB < lumaMin || lumaB > lumaMax) {
+ gl_FragColor = vec4(rgbA, 1.0);
+ } else {
+ gl_FragColor = vec4(rgbB, 1.0);
+ }
+}
diff --git a/shaders/grid.frag b/shaders/grid.frag
new file mode 100644
index 0000000..0f43d97
--- /dev/null
+++ b/shaders/grid.frag
@@ -0,0 +1,5 @@
+precision highp float;
+
+void main () {
+ gl_FragColor = vec4(0.8, 0.8, 0.8, 1.0);
+}
diff --git a/shaders/grid.vert b/shaders/grid.vert
new file mode 100644
index 0000000..e3daca5
--- /dev/null
+++ b/shaders/grid.vert
@@ -0,0 +1,12 @@
+precision highp float;
+
+attribute vec3 a_vertexPosition;
+
+uniform vec3 u_translation;
+
+uniform mat4 u_viewMatrix;
+uniform mat4 u_projectionMatrix;
+
+void main () {
+ gl_Position = u_projectionMatrix * u_viewMatrix * vec4(u_translation + a_vertexPosition, 1.0);
+}
diff --git a/shaders/jacobi.frag b/shaders/jacobi.frag
new file mode 100644
index 0000000..e2d21ce
--- /dev/null
+++ b/shaders/jacobi.frag
@@ -0,0 +1,34 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform vec3 u_gridResolution;
+
+uniform sampler2D u_pressureTexture;
+uniform sampler2D u_divergenceTexture;
+uniform sampler2D u_markerTexture;
+
+void main () {
+ vec3 centerCoords = get3DFragCoord(u_gridResolution) / u_gridResolution;
+
+ //pressure = 0 in air cells
+ float fluidCell = texture3DNearest(u_markerTexture, centerCoords, u_gridResolution).x;
+ if (fluidCell == 0.0) discard; //if this is an air cell
+
+ vec3 delta = 1.0 / u_gridResolution;
+
+ float divergenceCenter = texture3DNearest(u_divergenceTexture, centerCoords, u_gridResolution).r;
+
+ float left = texture3DNearest(u_pressureTexture, centerCoords + vec3(-delta.x, 0.0, 0.0), u_gridResolution).r;
+ float right = texture3DNearest(u_pressureTexture, centerCoords + vec3(delta.x, 0.0, 0.0), u_gridResolution).r;
+ float bottom = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, -delta.y, 0.0), u_gridResolution).r;
+ float top = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, delta.y, 0.0), u_gridResolution).r;
+ float back = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, 0.0, -delta.z), u_gridResolution).r;
+ float front = texture3DNearest(u_pressureTexture, centerCoords + vec3(0.0, 0.0, delta.z), u_gridResolution).r;
+
+ float newPressure = (left + right + bottom + top + back + front - divergenceCenter) / 6.0;
+
+
+ gl_FragColor = vec4(newPressure, 0.0, 0.0, 0.0);
+
+}
diff --git a/shaders/mark.frag b/shaders/mark.frag
new file mode 100644
index 0000000..4d2f488
--- /dev/null
+++ b/shaders/mark.frag
@@ -0,0 +1,5 @@
+precision highp float;
+
+void main () {
+ gl_FragColor = vec4(1.0);
+}
diff --git a/shaders/mark.vert b/shaders/mark.vert
new file mode 100644
index 0000000..e5e436c
--- /dev/null
+++ b/shaders/mark.vert
@@ -0,0 +1,24 @@
+//marks pixels with 1.0 if there's a particle there
+
+precision highp float;
+
+attribute vec2 a_textureCoordinates;
+
+uniform sampler2D u_positionTexture;
+
+uniform vec3 u_gridResolution;
+uniform vec3 u_gridSize;
+
+void main () {
+ gl_PointSize = 1.0;
+
+ vec3 position = texture2D(u_positionTexture, a_textureCoordinates).rgb;
+ position = (position / u_gridSize) * u_gridResolution;
+ vec3 cellIndex = floor(position);
+
+ vec2 textureCoordinates = vec2(
+ cellIndex.z * u_gridResolution.x + cellIndex.x + 0.5,
+ cellIndex.y + 0.5) / vec2(u_gridResolution.x * u_gridResolution.z, u_gridResolution.y);
+
+ gl_Position = vec4(textureCoordinates * 2.0 - 1.0, 0.0, 1.0);
+}
diff --git a/shaders/normalizegrid.frag b/shaders/normalizegrid.frag
new file mode 100644
index 0000000..22a226c
--- /dev/null
+++ b/shaders/normalizegrid.frag
@@ -0,0 +1,30 @@
+//this does the divide in the weighted sum
+
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_accumulatedVelocityTexture;
+uniform sampler2D u_weightTexture;
+
+void main () {
+ vec3 accumulatedVelocity = texture2D(u_accumulatedVelocityTexture, v_coordinates).rgb;
+ vec3 weight = texture2D(u_weightTexture, v_coordinates).rgb;
+
+ float xVelocity = 0.0;
+ if (weight.x > 0.0) {
+ xVelocity = accumulatedVelocity.x / weight.x;
+ }
+
+ float yVelocity = 0.0;
+ if (weight.y > 0.0) {
+ yVelocity = accumulatedVelocity.y / weight.y;
+ }
+
+ float zVelocity = 0.0;
+ if (weight.z > 0.0) {
+ zVelocity = accumulatedVelocity.z / weight.z;
+ }
+
+ gl_FragColor = vec4(xVelocity, yVelocity, zVelocity, 0.0);
+}
diff --git a/shaders/particle.frag b/shaders/particle.frag
new file mode 100644
index 0000000..26933da
--- /dev/null
+++ b/shaders/particle.frag
@@ -0,0 +1,9 @@
+precision highp float;
+
+varying vec3 v_velocity;
+
+void main () {
+ gl_FragColor = vec4(v_velocity * 0.5 + 0.5, 1.0);
+
+ 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);
+}
diff --git a/shaders/particle.vert b/shaders/particle.vert
new file mode 100644
index 0000000..900a94b
--- /dev/null
+++ b/shaders/particle.vert
@@ -0,0 +1,23 @@
+precision highp float;
+
+attribute vec2 a_textureCoordinates; //the texture coordinates that this particle's info is stored at
+
+uniform sampler2D u_positionTexture;
+uniform sampler2D u_velocityTexture;
+
+uniform vec2 u_resolution;
+
+varying vec3 v_velocity;
+
+uniform mat4 u_projectionMatrix;
+uniform mat4 u_viewMatrix;
+
+void main () {
+ vec3 position = texture2D(u_positionTexture, a_textureCoordinates).rgb;
+ vec3 velocity = texture2D(u_velocityTexture, a_textureCoordinates).rgb;
+ v_velocity = velocity;
+
+ gl_PointSize = 3.0;
+
+ gl_Position = u_projectionMatrix * u_viewMatrix * vec4(position, 1.0);
+}
diff --git a/shaders/point.frag b/shaders/point.frag
new file mode 100644
index 0000000..1c91c6f
--- /dev/null
+++ b/shaders/point.frag
@@ -0,0 +1,5 @@
+precision highp float;
+
+void main () {
+ gl_FragColor = vec4(vec3(0.6), 1.0);
+}
diff --git a/shaders/point.vert b/shaders/point.vert
new file mode 100644
index 0000000..9fd814e
--- /dev/null
+++ b/shaders/point.vert
@@ -0,0 +1,14 @@
+precision highp float;
+
+attribute vec3 a_position;
+
+uniform vec3 u_position;
+
+uniform mat3 u_rotation;
+
+uniform mat4 u_viewMatrix;
+uniform mat4 u_projectionMatrix;
+
+void main () {
+ gl_Position = u_projectionMatrix * u_viewMatrix * vec4(u_position + u_rotation * a_position * 0.2, 1.0);
+}
diff --git a/shaders/sphere.frag b/shaders/sphere.frag
new file mode 100644
index 0000000..0795319
--- /dev/null
+++ b/shaders/sphere.frag
@@ -0,0 +1,9 @@
+precision highp float;
+
+varying vec3 v_viewSpacePosition;
+varying vec3 v_viewSpaceNormal;
+varying float v_speed;
+
+void main () {
+ gl_FragColor = vec4(v_viewSpaceNormal.x, v_viewSpaceNormal.y, v_speed, v_viewSpacePosition.z);
+}
diff --git a/shaders/sphere.vert b/shaders/sphere.vert
new file mode 100644
index 0000000..0d7fd06
--- /dev/null
+++ b/shaders/sphere.vert
@@ -0,0 +1,32 @@
+precision highp float;
+
+attribute vec3 a_vertexPosition;
+attribute vec3 a_vertexNormal;
+
+attribute vec2 a_textureCoordinates;
+
+uniform mat4 u_projectionMatrix;
+uniform mat4 u_viewMatrix;
+
+uniform sampler2D u_positionsTexture;
+uniform sampler2D u_velocitiesTexture;
+
+uniform float u_sphereRadius;
+
+varying vec3 v_viewSpacePosition;
+varying vec3 v_viewSpaceNormal;
+varying float v_speed;
+
+void main () {
+ vec3 spherePosition = texture2D(u_positionsTexture, a_textureCoordinates).rgb;
+
+ vec3 position = a_vertexPosition * u_sphereRadius + spherePosition;
+
+ v_viewSpacePosition = vec3(u_viewMatrix * vec4(position, 1.0));
+ v_viewSpaceNormal = vec3(u_viewMatrix * vec4(a_vertexNormal, 0.0)); //this assumes we're not doing any weird stuff in the view matrix
+
+ gl_Position = u_projectionMatrix * vec4(v_viewSpacePosition, 1.0);
+
+ vec3 velocity = texture2D(u_velocitiesTexture, a_textureCoordinates).rgb;
+ v_speed = length(velocity);
+}
diff --git a/shaders/sphereao.frag b/shaders/sphereao.frag
new file mode 100644
index 0000000..8b91d6e
--- /dev/null
+++ b/shaders/sphereao.frag
@@ -0,0 +1,53 @@
+precision highp float;
+
+uniform sampler2D u_renderingTexture;
+
+varying vec3 v_viewSpaceSpherePosition;
+varying float v_sphereRadius;
+varying float v_extrudedSphereRadius;
+
+uniform vec2 u_resolution;
+uniform float u_fov;
+
+const float PI = 3.14159265;
+
+void main () {
+ vec2 coordinates = gl_FragCoord.xy / u_resolution;
+ vec4 data = texture2D(u_renderingTexture, coordinates);
+
+ //reconstruct position
+
+ vec3 viewSpaceNormal = vec3(data.x, data.y, sqrt(1.0 - data.x * data.x - data.y * data.y));
+
+ float tanHalfFOV = tan(u_fov / 2.0);
+ float viewSpaceZ = data.a;
+ vec3 viewRay = vec3(
+ (coordinates.x * 2.0 - 1.0) * tanHalfFOV * u_resolution.x / u_resolution.y,
+ (coordinates.y * 2.0 - 1.0) * tanHalfFOV,
+ -1.0);
+
+ vec3 viewSpacePosition = viewRay * -viewSpaceZ;
+
+
+ vec3 di = v_viewSpaceSpherePosition - viewSpacePosition;
+ float l = length(di);
+
+ float nl = dot(viewSpaceNormal, di / l);
+ float h = l / v_sphereRadius;
+ float h2 = h * h;
+ float k2 = 1.0 - h2 * nl * nl;
+
+ float result = max(0.0, nl) / h2;
+
+ if (k2 > 0.0 && l > v_sphereRadius) {
+ result = nl * acos(-nl * sqrt((h2 - 1.0) / (1.0 - nl * nl))) - sqrt(k2 * (h2 - 1.0));
+ result = result / h2 + atan(sqrt(k2 / (h2 - 1.0)));
+ result /= PI;
+
+ //result = pow( clamp(0.5*(nl*h+1.0)/h2,0.0,1.0), 1.5 ); //cheap approximation
+ }
+
+ gl_FragColor = vec4(result, 0.0, 0.0, 1.0);
+
+
+}
diff --git a/shaders/sphereao.vert b/shaders/sphereao.vert
new file mode 100644
index 0000000..cdfb9f7
--- /dev/null
+++ b/shaders/sphereao.vert
@@ -0,0 +1,29 @@
+precision highp float;
+
+attribute vec3 a_vertexPosition;
+
+attribute vec2 a_textureCoordinates;
+
+uniform mat4 u_projectionMatrix;
+uniform mat4 u_viewMatrix;
+
+uniform sampler2D u_positionsTexture;
+uniform sampler2D u_velocitiesTexture;
+
+uniform float u_sphereRadius;
+
+varying vec3 v_viewSpaceSpherePosition;
+varying float v_sphereRadius;
+varying float v_extrudedSphereRadius;
+
+void main () {
+ vec3 spherePosition = texture2D(u_positionsTexture, a_textureCoordinates).rgb;
+ v_viewSpaceSpherePosition = vec3(u_viewMatrix * vec4(spherePosition, 1.0));
+
+ v_sphereRadius = u_sphereRadius;
+ v_extrudedSphereRadius = v_sphereRadius * 5.0;
+
+ vec3 position = a_vertexPosition * v_extrudedSphereRadius + spherePosition;
+
+ gl_Position = u_projectionMatrix * u_viewMatrix * vec4(position, 1.0);
+}
diff --git a/shaders/spheredepth.frag b/shaders/spheredepth.frag
new file mode 100644
index 0000000..4d2f488
--- /dev/null
+++ b/shaders/spheredepth.frag
@@ -0,0 +1,5 @@
+precision highp float;
+
+void main () {
+ gl_FragColor = vec4(1.0);
+}
diff --git a/shaders/spheredepth.vert b/shaders/spheredepth.vert
new file mode 100644
index 0000000..f8cd0bb
--- /dev/null
+++ b/shaders/spheredepth.vert
@@ -0,0 +1,21 @@
+precision highp float;
+
+attribute vec3 a_vertexPosition;
+attribute vec3 a_vertexNormal;
+
+attribute vec2 a_textureCoordinates;
+
+uniform mat4 u_projectionViewMatrix;
+
+uniform sampler2D u_positionsTexture;
+uniform sampler2D u_velocitiesTexture;
+
+uniform float u_sphereRadius;
+
+void main () {
+ vec3 spherePosition = texture2D(u_positionsTexture, a_textureCoordinates).rgb;
+
+ vec3 position = a_vertexPosition * u_sphereRadius + spherePosition;
+
+ gl_Position = u_projectionViewMatrix * vec4(position, 1.0);
+}
diff --git a/shaders/subtract.frag b/shaders/subtract.frag
new file mode 100644
index 0000000..cab5255
--- /dev/null
+++ b/shaders/subtract.frag
@@ -0,0 +1,32 @@
+precision highp float;
+
+varying vec2 v_coordinates;
+
+uniform vec3 u_gridResolution;
+
+uniform sampler2D u_pressureTexture;
+uniform sampler2D u_velocityTexture;
+uniform sampler2D u_markerTexture;
+
+void main () {
+ vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0));
+
+ float left = texture3DNearest(u_pressureTexture, (cellIndex + vec3(-1.0, 0.0, 0.0) + 0.5) / u_gridResolution, u_gridResolution).r;
+ float right = texture3DNearest(u_pressureTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).r;
+
+ float bottom = texture3DNearest(u_pressureTexture, (cellIndex + vec3(0.0, -1.0, 0.0) + 0.5) / u_gridResolution, u_gridResolution).r;
+ float top = texture3DNearest(u_pressureTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).r;
+
+ float back = texture3DNearest(u_pressureTexture, (cellIndex + vec3(0.0, 0.0, -1.0) + 0.5) / u_gridResolution, u_gridResolution).r;
+ float front = texture3DNearest(u_pressureTexture, (cellIndex + 0.5) / u_gridResolution, u_gridResolution).r;
+
+
+ //compute gradient of pressure
+ vec3 gradient = vec3(right - left, top - bottom, front - back) / 1.0;
+
+ vec3 currentVelocity = texture2D(u_velocityTexture, v_coordinates).rgb;
+
+ vec3 newVelocity = currentVelocity - gradient;
+
+ gl_FragColor = vec4(newVelocity, 0.0);
+}
diff --git a/shaders/transfertogrid.frag b/shaders/transfertogrid.frag
new file mode 100644
index 0000000..e18cea7
--- /dev/null
+++ b/shaders/transfertogrid.frag
@@ -0,0 +1,56 @@
+//two modes:
+//in one we accumulate (xWeight, yWeight, zWeight, centerWeight)
+//in the other we accumulate (xWeight * velocity.x, yWeight * velocity.y, zWeight * velocity.z, 0)
+
+//needs a division as a second step
+
+varying vec3 v_position; //already in the grid coordinate system
+varying vec3 v_velocity;
+
+uniform vec3 u_gridResolution;
+
+varying float v_zIndex;
+
+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)
+
+float h (float r) {
+ if (r >= 0.0 && r <= 1.0) {
+ return 1.0 - r;
+ } else if (r >= -1.0 && r <= 0.0) {
+ return 1.0 + r;
+ } else {
+ return 0.0;
+ }
+}
+
+float k (vec3 v) {
+ return h(v.x) * h(v.y) * h(v.z);
+}
+
+void main () {
+ vec3 cellIndex = floor(get3DFragCoord(u_gridResolution + 1.0));
+
+ if (cellIndex.z == v_zIndex) { //make sure we're in the right slice to prevent bleeding
+ //staggered grid position and therefor weight is different for x, y, z and scalar values
+ vec3 xPosition = vec3(cellIndex.x, cellIndex.y + 0.5, cellIndex.z + 0.5);
+ float xWeight = k(v_position - xPosition);
+
+ vec3 yPosition = vec3(cellIndex.x + 0.5, cellIndex.y, cellIndex.z + 0.5);
+ float yWeight = k(v_position - yPosition);
+
+ vec3 zPosition = vec3(cellIndex.x + 0.5, cellIndex.y + 0.5, cellIndex.z);
+ float zWeight = k(v_position - zPosition);
+
+ vec3 scalarPosition = vec3(cellIndex.x + 0.5, cellIndex.y + 0.5, cellIndex.z + 0.5);
+ float scalarWeight = k(v_position - scalarPosition);
+
+ if (u_accumulate == 0) {
+ gl_FragColor = vec4(xWeight, yWeight, zWeight, scalarWeight);
+ } else if (u_accumulate == 1) {
+ gl_FragColor = vec4(xWeight * v_velocity.x, yWeight * v_velocity.y, zWeight * v_velocity.z, 0.0);
+ }
+
+ } else {
+ gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
+ }
+}
diff --git a/shaders/transfertogrid.vert b/shaders/transfertogrid.vert
new file mode 100644
index 0000000..6095155
--- /dev/null
+++ b/shaders/transfertogrid.vert
@@ -0,0 +1,37 @@
+//transfers particle velocities to the grid by splatting them using additive blending
+
+precision highp float;
+
+attribute vec2 a_textureCoordinates;
+
+uniform sampler2D u_positionTexture;
+uniform sampler2D u_velocityTexture;
+
+uniform vec3 u_gridSize;
+uniform vec3 u_gridResolution;
+
+varying vec3 v_position;
+varying vec3 v_velocity;
+
+uniform float u_zOffset; //the offset for the z layer we're splatting into
+varying float v_zIndex; //the z layer we're splatting into
+
+void main () {
+ gl_PointSize = 5.0; //TODO: i can probably compute this more accurately
+
+ vec3 position = texture2D(u_positionTexture, a_textureCoordinates).rgb;
+ position = (position / u_gridSize) * u_gridResolution;
+
+ vec3 velocity = texture2D(u_velocityTexture, a_textureCoordinates).rgb;
+ v_velocity = velocity;
+ v_position = position;
+
+ vec3 cellIndex = vec3(floor(position.xyz));
+ v_zIndex = cellIndex.z + u_zOffset; //offset into the right layer
+
+ vec2 textureCoordinates = vec2(
+ v_zIndex * (u_gridResolution.x + 1.0) + cellIndex.x + 0.5,
+ cellIndex.y + 0.5) / vec2((u_gridResolution.x + 1.0) * (u_gridResolution.z + 1.0), u_gridResolution.y + 1.0);
+
+ gl_Position = vec4(textureCoordinates * 2.0 - 1.0, 0.0, 1.0);
+}
diff --git a/shaders/transfertoparticles.frag b/shaders/transfertoparticles.frag
new file mode 100644
index 0000000..a15ba70
--- /dev/null
+++ b/shaders/transfertoparticles.frag
@@ -0,0 +1,50 @@
+//transfers velocities back to the particles
+
+varying vec2 v_coordinates;
+
+uniform sampler2D u_particlePositionTexture;
+uniform sampler2D u_particleVelocityTexture;
+
+uniform sampler2D u_gridVelocityTexture;
+uniform sampler2D u_originalGridVelocityTexture; //the grid velocities before the update
+
+uniform vec3 u_gridResolution;
+uniform vec3 u_gridSize;
+
+uniform float u_flipness; //0 is full PIC, 1 is full FLIP
+
+float sampleXVelocity (sampler2D texture, vec3 position) {
+ vec3 cellIndex = vec3(position.x, position.y - 0.5, position.z - 0.5);
+ return texture3D(texture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).x;
+}
+
+float sampleYVelocity (sampler2D texture, vec3 position) {
+ vec3 cellIndex = vec3(position.x - 0.5, position.y, position.z - 0.5);
+ return texture3D(texture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).y;
+}
+
+float sampleZVelocity (sampler2D texture, vec3 position) {
+ vec3 cellIndex = vec3(position.x - 0.5, position.y - 0.5, position.z);
+ return texture3D(texture, (cellIndex + 0.5) / (u_gridResolution + 1.0), u_gridResolution + 1.0).z;
+}
+
+vec3 sampleVelocity (sampler2D texture, vec3 position) {
+ return vec3(sampleXVelocity(texture, position), sampleYVelocity(texture, position), sampleZVelocity(texture, position));
+}
+
+void main () {
+ vec3 particlePosition = texture2D(u_particlePositionTexture, v_coordinates).rgb;
+ particlePosition = (particlePosition / u_gridSize) * u_gridResolution;
+
+ vec3 particleVelocity = texture2D(u_particleVelocityTexture, v_coordinates).rgb;
+
+ vec3 currentVelocity = sampleVelocity(u_gridVelocityTexture, particlePosition);
+ vec3 originalVelocity = sampleVelocity(u_originalGridVelocityTexture, particlePosition);
+
+ vec3 velocityChange = currentVelocity - originalVelocity;
+
+ vec3 flipVelocity = particleVelocity + velocityChange;
+ vec3 picVelocity = currentVelocity;
+
+ gl_FragColor = vec4(mix(picVelocity, flipVelocity, u_flipness), 0.0);
+}
diff --git a/simulator.js b/simulator.js
new file mode 100644
index 0000000..f0475cd
--- /dev/null
+++ b/simulator.js
@@ -0,0 +1,591 @@
+'use strict'
+
+var Simulator = (function () {
+
+ //simulation grid dimensions and resolution
+ //all particles are in the world position space ([0, 0, 0], [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH])
+
+ //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])
+
+
+ //in grid space, cell boundaries are simply at integer values
+
+ //we emulate 3D textures with tiled 2d textures
+ //so the z slices of a 3d texture are laid out along the x axis
+ //the 2d dimensions of a 3d texture are therefore [width * depth, height]
+
+
+ /*
+ we use a staggered MAC grid
+ this means the velocity grid width = grid width + 1 and velocity grid height = grid height + 1 and velocity grid depth = grid depth + 1
+ a scalar for cell [i, j, k] is positionally located at [i + 0.5, j + 0.5, k + 0.5]
+ x velocity for cell [i, j, k] is positionally located at [i, j + 0.5, k + 0.5]
+ y velocity for cell [i, j, k] is positionally located at [i + 0.5, j, k + 0.5]
+ z velocity for cell [i, j, k] is positionally located at [i + 0.5, j + 0.5, k]
+ */
+
+ //the boundaries are the boundaries of the grid
+ //a grid cell can either be fluid, air (these are tracked by markTexture) or is a wall (implicit by position)
+
+ function Simulator (wgl, onLoaded) {
+ this.wgl = wgl;
+
+ this.particlesWidth = 0;
+ this.particlesHeight = 0;
+
+ this.gridWidth = 0;
+ this.gridHeight = 0;
+ this.gridDepth = 0;
+
+ this.gridResolutionX = 0;
+ this.gridResolutionY = 0;
+ this.gridResolutionZ = 0;
+
+ this.particleDensity = 0;
+
+ this.velocityTextureWidth = 0;
+ this.velocityTextureHeight = 0;
+
+ this.scalarTextureWidth = 0;
+ this.scalarTextureHeight = 0;
+
+
+ this.halfFloatExt = this.wgl.getExtension('OES_texture_half_float');
+ this.wgl.getExtension('OES_texture_half_float_linear');
+
+ this.simulationNumberType = this.halfFloatExt.HALF_FLOAT_OES;
+
+
+ ///////////////////////////////////////////////////////
+ // simulation parameters
+
+ this.flipness = 0.99; //0 is full PIC, 1 is full FLIP
+
+
+ this.frameNumber = 0; //used for motion randomness
+
+
+ /////////////////////////////////////////////////
+ // simulation objects (most are filled in by reset)
+
+ this.quadVertexBuffer = wgl.createBuffer();
+ 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);
+
+ this.simulationFramebuffer = wgl.createFramebuffer();
+ this.particleVertexBuffer = wgl.createBuffer();
+
+
+ this.particlePositionTexture = wgl.createTexture();
+ this.particlePositionTextureTemp = wgl.createTexture();
+
+
+ this.particleVelocityTexture = wgl.createTexture();
+ this.particleVelocityTextureTemp = wgl.createTexture();
+
+ this.particleRandomTexture = wgl.createTexture(); //contains a random normalized direction for each particle
+
+
+
+ ////////////////////////////////////////////////////
+ // create simulation textures
+
+ this.velocityTexture = wgl.createTexture();
+ this.tempVelocityTexture = wgl.createTexture();
+ this.originalVelocityTexture = wgl.createTexture();
+ this.weightTexture = wgl.createTexture();
+
+ this.markerTexture = wgl.createTexture(); //marks fluid/air, 1 if fluid, 0 if air
+ this.divergenceTexture = wgl.createTexture();
+ this.pressureTexture = wgl.createTexture();
+ this.tempSimulationTexture = wgl.createTexture();
+
+
+
+ /////////////////////////////
+ // load programs
+
+
+ wgl.createProgramsFromFiles({
+ transferToGridProgram: {
+ vertexShader: 'shaders/transfertogrid.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/transfertogrid.frag'],
+ attributeLocations: { 'a_textureCoordinates': 0}
+ },
+ normalizeGridProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: 'shaders/normalizegrid.frag',
+ attributeLocations: { 'a_position': 0}
+ },
+ markProgram: {
+ vertexShader: 'shaders/mark.vert',
+ fragmentShader: 'shaders/mark.frag',
+ attributeLocations: { 'a_textureCoordinates': 0}
+ },
+ addForceProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/addforce.frag'],
+ attributeLocations: { 'a_position': 0}
+ },
+ enforceBoundariesProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/enforceboundaries.frag'],
+ attributeLocations: { 'a_textureCoordinates': 0 }
+ },
+ extendVelocityProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: 'shaders/extendvelocity.frag',
+ attributeLocations: { 'a_textureCoordinates': 0 }
+ },
+ transferToParticlesProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/transfertoparticles.frag'],
+ attributeLocations: { 'a_position': 0}
+ },
+ divergenceProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/divergence.frag'],
+ attributeLocations: { 'a_position': 0}
+ },
+ jacobiProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/jacobi.frag'],
+ attributeLocations: { 'a_position': 0}
+ },
+ subtractProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/subtract.frag'],
+ attributeLocations: { 'a_position': 0}
+ },
+ advectProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: ['shaders/common.frag', 'shaders/advect.frag'],
+ attributeLocations: { 'a_position': 0}
+ },
+ copyProgram: {
+ vertexShader: 'shaders/fullscreen.vert',
+ fragmentShader: 'shaders/copy.frag',
+ attributeLocations: { 'a_position': 0}
+ }
+ }, (function (programs) {
+ for (var programName in programs) {
+ this[programName] = programs[programName];
+ }
+
+ onLoaded();
+ }).bind(this));
+ }
+
+
+ //expects an array of [x, y, z] particle positions
+ //gridSize and gridResolution are both [x, y, z]
+
+ //particleDensity is particles per simulation grid cell
+ Simulator.prototype.reset = function (particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, particleDensity) {
+
+ this.particlesWidth = particlesWidth;
+ this.particlesHeight = particlesHeight;
+
+ this.gridWidth = gridSize[0];
+ this.gridHeight = gridSize[1];
+ this.gridDepth = gridSize[2];
+
+ this.gridResolutionX = gridResolution[0];
+ this.gridResolutionY = gridResolution[1];
+ this.gridResolutionZ = gridResolution[2];
+
+ this.particleDensity = particleDensity;
+
+ this.velocityTextureWidth = (this.gridResolutionX + 1) * (this.gridResolutionZ + 1);
+ this.velocityTextureHeight = (this.gridResolutionY + 1);
+
+ this.scalarTextureWidth = this.gridResolutionX * this.gridResolutionZ;
+ this.scalarTextureHeight = this.gridResolutionY;
+
+
+
+ ///////////////////////////////////////////////////////////
+ // create particle data
+
+ var particleCount = this.particlesWidth * this.particlesHeight;
+
+ //fill particle vertex buffer containing the relevant texture coordinates
+ var particleTextureCoordinates = new Float32Array(this.particlesWidth * this.particlesHeight * 2);
+ for (var y = 0; y < this.particlesHeight; ++y) {
+ for (var x = 0; x < this.particlesWidth; ++x) {
+ particleTextureCoordinates[(y * this.particlesWidth + x) * 2] = (x + 0.5) / this.particlesWidth;
+ particleTextureCoordinates[(y * this.particlesWidth + x) * 2 + 1] = (y + 0.5) / this.particlesHeight;
+ }
+ }
+
+ wgl.bufferData(this.particleVertexBuffer, wgl.ARRAY_BUFFER, particleTextureCoordinates, wgl.STATIC_DRAW);
+
+ //generate initial particle positions amd create particle position texture for them
+ var particlePositionsData = new Float32Array(this.particlesWidth * this.particlesHeight * 4);
+ var particleRandoms = new Float32Array(this.particlesWidth * this.particlesHeight * 4);
+ for (var i = 0; i < this.particlesWidth * this.particlesHeight; ++i) {
+ particlePositionsData[i * 4] = particlePositions[i][0];
+ particlePositionsData[i * 4 + 1] = particlePositions[i][1];
+ particlePositionsData[i * 4 + 2] = particlePositions[i][2];
+ particlePositionsData[i * 4 + 3] = 0.0;
+
+ var theta = Math.random() * 2.0 * Math.PI;
+ var u = Math.random() * 2.0 - 1.0;
+ particleRandoms[i * 4] = Math.sqrt(1.0 - u * u) * Math.cos(theta);
+ particleRandoms[i * 4 + 1] = Math.sqrt(1.0 - u * u) * Math.sin(theta);
+ particleRandoms[i * 4 + 2] = u;
+ particleRandoms[i * 4 + 3] = 0.0;
+ }
+
+ 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);
+ 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);
+
+
+ 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);
+ 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);
+
+ 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
+
+
+
+ ////////////////////////////////////////////////////
+ // create simulation textures
+
+ 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);
+ 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);
+ 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);
+ 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);
+
+ 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
+ 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);
+ 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);
+ 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);
+
+
+ }
+
+ function swap (object, a, b) {
+ var temp = object[a];
+ object[a] = object[b];
+ object[b] = temp;
+ }
+
+ //you need to call reset() with correct parameters before simulating
+ //mouseVelocity, mouseRayOrigin, mouseRayDirection are all expected to be arrays of 3 values
+ Simulator.prototype.simulate = function (timeStep, mouseVelocity, mouseRayOrigin, mouseRayDirection) {
+ if (timeStep === 0.0) return;
+
+ this.frameNumber += 1;
+
+ var wgl = this.wgl;
+
+ /*
+ the simulation process
+ transfer particle velocities to velocity grid
+ save this velocity grid
+
+ solve velocity grid for non divergence
+
+ update particle velocities with new velocity grid
+ advect particles through the grid velocity field
+ */
+
+
+ //////////////////////////////////////////////////////
+ //transfer particle velocities to grid
+
+ //we transfer particle velocities to the grid in two steps
+ //in the first step, we accumulate weight * velocity into tempVelocityTexture and then weight into weightTexture
+ //in the second step: velocityTexture = tempVelocityTexture / weightTexture
+
+ //we accumulate into velocityWeightTexture and then divide into velocityTexture
+
+ var transferToGridDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight)
+
+ .vertexAttribPointer(this.particleVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.transferToGridProgram)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth)
+ .uniformTexture('u_positionTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture)
+ .uniformTexture('u_velocityTexture', 1, wgl.TEXTURE_2D, this.particleVelocityTexture)
+
+ .enable(wgl.BLEND)
+ .blendEquation(wgl.FUNC_ADD)
+ .blendFuncSeparate(wgl.ONE, wgl.ONE, wgl.ONE, wgl.ONE);
+
+
+ //accumulate weight
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.weightTexture, 0);
+
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.simulationFramebuffer).clearColor(0, 0, 0, 0),
+ wgl.COLOR_BUFFER_BIT);
+
+ transferToGridDrawState.uniform1i('u_accumulate', 0)
+
+ //each particle gets splatted layer by layer from z - (SPLAT_SIZE - 1) / 2 to z + (SPLAT_SIZE - 1) / 2
+ var SPLAT_DEPTH = 5;
+
+ for (var z = -(SPLAT_DEPTH - 1) / 2; z <= (SPLAT_DEPTH - 1) / 2; ++z) {
+ transferToGridDrawState.uniform1f('u_zOffset', z);
+ wgl.drawArrays(transferToGridDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight);
+ }
+
+ //accumulate (weight * velocity)
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0);
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.simulationFramebuffer),
+ wgl.COLOR_BUFFER_BIT);
+
+ transferToGridDrawState.uniform1i('u_accumulate', 1)
+
+ for (var z = -(SPLAT_DEPTH - 1) / 2; z <= (SPLAT_DEPTH - 1) / 2; ++z) {
+ transferToGridDrawState.uniform1f('u_zOffset', z);
+ wgl.drawArrays(transferToGridDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight);
+ }
+
+
+ //in the second step, we divide sum(weight * velocity) by sum(weight) (the two accumulated quantities from before)
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.velocityTexture, 0);
+
+ var normalizeDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.normalizeGridProgram)
+ .uniformTexture('u_weightTexture', 0, wgl.TEXTURE_2D, this.weightTexture)
+ .uniformTexture('u_accumulatedVelocityTexture', 1, wgl.TEXTURE_2D, this.tempVelocityTexture)
+
+ wgl.drawArrays(normalizeDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+
+ //////////////////////////////////////////////////////
+ // mark cells with fluid
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.markerTexture, 0);
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.simulationFramebuffer),
+ wgl.COLOR_BUFFER_BIT);
+
+ var markDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight)
+
+ .vertexAttribPointer(this.particleVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.markProgram)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth)
+ .uniformTexture('u_positionTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture);
+
+ wgl.drawArrays(markDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight);
+
+ ////////////////////////////////////////////////////
+ // save our original velocity grid
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.originalVelocityTexture, 0);
+
+ var copyDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.copyProgram)
+ .uniformTexture('u_texture', 0, wgl.TEXTURE_2D, this.velocityTexture)
+
+ wgl.drawArrays(copyDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+
+ /////////////////////////////////////////////////////
+ // add forces to velocity grid
+
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0);
+
+ var addForceDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.addForceProgram)
+ .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture)
+
+ .uniform1f('u_timeStep', timeStep)
+
+ .uniform3f('u_mouseVelocity', mouseVelocity[0], mouseVelocity[1], mouseVelocity[2])
+
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth)
+
+ .uniform3f('u_mouseRayOrigin', mouseRayOrigin[0], mouseRayOrigin[1], mouseRayOrigin[2])
+ .uniform3f('u_mouseRayDirection', mouseRayDirection[0], mouseRayDirection[1], mouseRayDirection[2])
+
+
+ wgl.drawArrays(addForceDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+ swap(this, 'velocityTexture', 'tempVelocityTexture');
+
+
+ /////////////////////////////////////////////////////
+ // enforce boundary velocity conditions
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0);
+
+ var enforceBoundariesDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.enforceBoundariesProgram)
+ .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ);
+
+ wgl.drawArrays(enforceBoundariesDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+ swap(this, 'velocityTexture', 'tempVelocityTexture');
+
+
+ /////////////////////////////////////////////////////
+ // update velocityTexture for non divergence
+
+
+ //compute divergence for pressure projection
+
+ var divergenceDrawState = wgl.createDrawState()
+
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight)
+
+ .useProgram(this.divergenceProgram)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture)
+ .uniformTexture('u_markerTexture', 1, wgl.TEXTURE_2D, this.markerTexture)
+ .uniformTexture('u_weightTexture', 2, wgl.TEXTURE_2D, this.weightTexture)
+
+ .uniform1f('u_maxDensity', this.particleDensity)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0)
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.divergenceTexture, 0);
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.simulationFramebuffer),
+ wgl.COLOR_BUFFER_BIT);
+
+ wgl.drawArrays(divergenceDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+
+ //compute pressure via jacobi iteration
+
+ var jacobiDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight)
+
+ .useProgram(this.jacobiProgram)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniformTexture('u_divergenceTexture', 1, wgl.TEXTURE_2D, this.divergenceTexture)
+ .uniformTexture('u_markerTexture', 2, wgl.TEXTURE_2D, this.markerTexture)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0)
+
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.pressureTexture, 0);
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.simulationFramebuffer),
+ wgl.COLOR_BUFFER_BIT);
+
+ var PRESSURE_JACOBI_ITERATIONS = 40;
+ for (var i = 0; i < PRESSURE_JACOBI_ITERATIONS; ++i) {
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempSimulationTexture, 0);
+ jacobiDrawState.uniformTexture('u_pressureTexture', 0, wgl.TEXTURE_2D, this.pressureTexture);
+
+ wgl.drawArrays(jacobiDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+ swap(this, 'pressureTexture', 'tempSimulationTexture');
+ }
+
+
+ //subtract pressure gradient from velocity
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0);
+
+ var subtractDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight)
+
+ .useProgram(this.subtractProgram)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniformTexture('u_pressureTexture', 0, wgl.TEXTURE_2D, this.pressureTexture)
+ .uniformTexture('u_velocityTexture', 1, wgl.TEXTURE_2D, this.velocityTexture)
+ .uniformTexture('u_markerTexture', 2, wgl.TEXTURE_2D, this.markerTexture)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0)
+
+ wgl.drawArrays(subtractDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+ swap(this, 'velocityTexture', 'tempVelocityTexture');
+
+ /////////////////////////////////////////////////////////////
+ // transfer velocities back to particles
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.particleVelocityTextureTemp, 0);
+
+ var transferToParticlesDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.particlesWidth, this.particlesHeight)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.transferToParticlesProgram)
+ .uniformTexture('u_particlePositionTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture)
+ .uniformTexture('u_particleVelocityTexture', 1, wgl.TEXTURE_2D, this.particleVelocityTexture)
+ .uniformTexture('u_gridVelocityTexture', 2, wgl.TEXTURE_2D, this.velocityTexture)
+ .uniformTexture('u_originalGridVelocityTexture', 3, wgl.TEXTURE_2D, this.originalVelocityTexture)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth)
+
+ .uniform1f('u_flipness', this.flipness)
+
+ wgl.drawArrays(transferToParticlesDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+ swap(this, 'particleVelocityTextureTemp', 'particleVelocityTexture');
+
+ ///////////////////////////////////////////////
+ // advect particle positions with velocity grid using RK2
+
+
+ wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.particlePositionTextureTemp, 0);
+ wgl.clear(
+ wgl.createClearState().bindFramebuffer(this.simulationFramebuffer),
+ wgl.COLOR_BUFFER_BIT);
+
+ var advectDrawState = wgl.createDrawState()
+ .bindFramebuffer(this.simulationFramebuffer)
+ .viewport(0, 0, this.particlesWidth, this.particlesHeight)
+
+ .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0)
+
+ .useProgram(this.advectProgram)
+ .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, this.particlePositionTexture)
+ .uniformTexture('u_randomsTexture', 1, wgl.TEXTURE_2D, this.particleRandomTexture)
+ .uniformTexture('u_velocityGrid', 2, wgl.TEXTURE_2D, this.velocityTexture)
+ .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ)
+ .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth)
+ .uniform1f('u_timeStep', timeStep)
+ .uniform1f('u_frameNumber', this.frameNumber)
+ .uniform2f('u_particlesResolution', this.particlesWidth, this.particlesHeight);
+
+ wgl.drawArrays(advectDrawState, wgl.TRIANGLE_STRIP, 0, 4);
+
+ swap(this, 'particlePositionTextureTemp', 'particlePositionTexture');
+ }
+
+ return Simulator;
+}());
diff --git a/simulatorrenderer.js b/simulatorrenderer.js
new file mode 100644
index 0000000..5c182b2
--- /dev/null
+++ b/simulatorrenderer.js
@@ -0,0 +1,115 @@
+var SimulatorRenderer = (function () {
+ function SimulatorRenderer (canvas, wgl, projectionMatrix, camera, gridDimensions, onLoaded) {
+ this.canvas = canvas;
+ this.wgl = wgl;
+ this.projectionMatrix = projectionMatrix;
+ this.camera = camera;
+
+
+ wgl.getExtension('OES_texture_float');
+ wgl.getExtension('OES_texture_float_linear');
+
+ var rendererLoaded = false,
+ simulatorLoaded = false;
+
+ this.renderer = new Renderer(this.canvas, this.wgl, gridDimensions, (function () {
+ rendererLoaded = true;
+ if (rendererLoaded && simulatorLoaded) {
+ start.call(this);
+ }
+ }).bind(this));
+
+ this.simulator = new Simulator(this.wgl, (function () {
+ simulatorLoaded = true;
+ if (rendererLoaded && simulatorLoaded) {
+ start.call(this);
+ }
+ }).bind(this));
+
+
+ function start () {
+ /////////////////////////////////////////////
+ // interaction stuff
+
+ //mouse position is in [-1, 1]
+ this.mouseX = 0;
+ this.mouseY = 0;
+
+ //the mouse plane is a plane centered at the camera orbit point and orthogonal to the view direction
+ this.lastMousePlaneX = 0;
+ this.lastMousePlaneY = 0;
+
+ setTimeout(onLoaded, 1);
+ }
+ }
+
+ SimulatorRenderer.prototype.onMouseMove = function (event) {
+ var position = Utilities.getMousePosition(event, this.canvas);
+ var normalizedX = position.x / this.canvas.width;
+ var normalizedY = position.y / this.canvas.height;
+
+ this.mouseX = normalizedX * 2.0 - 1.0;
+ this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0;
+
+ this.camera.onMouseMove(event);
+ };
+
+ SimulatorRenderer.prototype.onMouseDown = function (event) {
+ this.camera.onMouseDown(event);
+ };
+
+ SimulatorRenderer.prototype.onMouseUp = function (event) {
+ this.camera.onMouseUp(event);
+ };
+
+ SimulatorRenderer.prototype.reset = function (particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, particleDensity, sphereRadius) {
+ this.simulator.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, particleDensity);
+ this.renderer.reset(particlesWidth, particlesHeight, sphereRadius);
+ }
+
+ SimulatorRenderer.prototype.update = function (timeStep) {
+ var fov = 2.0 * Math.atan(1.0 / this.projectionMatrix[5]);
+
+ var viewSpaceMouseRay = [
+ this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height),
+ this.mouseY * Math.tan(fov / 2.0),
+ -1.0];
+
+ var mousePlaneX = viewSpaceMouseRay[0] * this.camera.distance;
+ var mousePlaneY = viewSpaceMouseRay[1] * this.camera.distance;
+
+ var mouseVelocityX = mousePlaneX - this.lastMousePlaneX;
+ var mouseVelocityY = mousePlaneY - this.lastMousePlaneY;
+
+ if (this.camera.isMouseDown()) {
+ mouseVelocityX = 0.0;
+ mouseVelocityY = 0.0;
+ }
+
+ this.lastMousePlaneX = mousePlaneX;
+ this.lastMousePlaneY = mousePlaneY;
+
+ var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix());
+ var worldSpaceMouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix);
+ Utilities.normalizeVector(worldSpaceMouseRay, worldSpaceMouseRay);
+
+
+ var cameraViewMatrix = this.camera.getViewMatrix();
+ var cameraRight = [cameraViewMatrix[0], cameraViewMatrix[4], cameraViewMatrix[8]];
+ var cameraUp = [cameraViewMatrix[1], cameraViewMatrix[5], cameraViewMatrix[9]];
+
+ var mouseVelocity = [];
+ for (var i = 0; i < 3; ++i) {
+ mouseVelocity[i] = mouseVelocityX * cameraRight[i] + mouseVelocityY * cameraUp[i];
+ }
+
+ this.simulator.simulate(timeStep, mouseVelocity, this.camera.getPosition(), worldSpaceMouseRay);
+ this.renderer.draw(this.simulator, this.projectionMatrix, this.camera.getViewMatrix());
+ }
+
+ SimulatorRenderer.prototype.onResize = function (event) {
+ this.renderer.onResize(event);
+ }
+
+ return SimulatorRenderer;
+}());
diff --git a/slider.js b/slider.js
new file mode 100644
index 0000000..fc72434
--- /dev/null
+++ b/slider.js
@@ -0,0 +1,59 @@
+'use strict'
+
+var Slider = (function () {
+
+ //changeCallback is called with the new value
+ var Slider = function (element, initial, min, max, changeCallback) {
+ this.value = initial;
+
+ this.min = min;
+ this.max = max;
+
+ this.div = element;
+
+ this.innerDiv = document.createElement('div');
+ this.innerDiv.style.position = 'absolute';
+ this.innerDiv.style.height = this.div.offsetHeight + 'px';
+
+ this.div.appendChild(this.innerDiv);
+
+ this.changeCallback = changeCallback;
+
+ this.mousePressed = false;
+
+ this.redraw();
+
+ this.div.addEventListener('mousedown', (function (event) {
+ this.mousePressed = true;
+ this.onChange(event);
+ }).bind(this));
+
+ document.addEventListener('mouseup', (function (event) {
+ this.mousePressed = false;
+ }).bind(this));
+
+ document.addEventListener('mousemove', (function (event) {
+ if (this.mousePressed) {
+ this.onChange(event);
+ }
+ }).bind(this));
+
+ };
+
+ Slider.prototype.redraw = function () {
+ var fraction = (this.value - this.min) / (this.max - this.min);
+ this.innerDiv.style.width = fraction * this.div.offsetWidth + 'px';
+ this.innerDiv.style.height = this.div.offsetHeight + 'px';
+ }
+
+ Slider.prototype.onChange = function (event) {
+ var mouseX = Utilities.getMousePosition(event, this.div).x;
+ this.value = Utilities.clamp((mouseX / this.div.offsetWidth) * (this.max - this.min) + this.min, this.min, this.max);
+
+ this.redraw();
+
+ this.changeCallback(this.value);
+ }
+
+ return Slider;
+}());
diff --git a/utilities.js b/utilities.js
new file mode 100644
index 0000000..4d9f2fc
--- /dev/null
+++ b/utilities.js
@@ -0,0 +1,304 @@
+'use strict'
+
+var Utilities = {
+ clamp: function (x, min, max) {
+ return Math.max(min, Math.min(max, x));
+ },
+
+ getMousePosition: function (event, element) {
+ var boundingRect = element.getBoundingClientRect();
+ return {
+ x: event.clientX - boundingRect.left,
+ y: event.clientY - boundingRect.top
+ };
+ },
+
+ addVectors: function (out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ out[2] = a[2] + b[2];
+ return out;
+ },
+
+ subtractVectors: function (out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ out[2] = a[2] - b[2];
+ return out;
+ },
+
+ magnitudeOfVector: function (v) {
+ return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
+ },
+
+ dotVectors: function (a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ },
+
+ multiplyVectorByScalar: function (out, v, k) {
+ out[0] = v[0] * k;
+ out[1] = v[1] * k;
+ out[2] = v[2] * k;
+ return out;
+ },
+
+
+ normalizeVector: function (out, v) {
+ var inverseMagnitude = 1.0 / Utilities.magnitudeOfVector(v);
+ out[0] = v[0] * inverseMagnitude;
+ out[1] = v[1] * inverseMagnitude;
+ out[2] = v[2] * inverseMagnitude;
+ return out;
+ },
+
+ makePerspectiveMatrix: function (out, fovy, aspect, near, far) {
+ var f = 1.0 / Math.tan(fovy / 2),
+ nf = 1 / (near - far);
+
+ out[0] = f / aspect;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = f;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = (far + near) * nf;
+ out[11] = -1;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = (2 * far * near) * nf;
+ out[15] = 0;
+ return out;
+ },
+
+ makeIdentityMatrix: function (matrix) {
+ matrix[0] = 1.0;
+ matrix[1] = 0.0;
+ matrix[2] = 0.0;
+ matrix[3] = 0.0;
+ matrix[4] = 0.0;
+ matrix[5] = 1.0;
+ matrix[6] = 0.0;
+ matrix[7] = 0.0;
+ matrix[8] = 0.0;
+ matrix[9] = 0.0;
+ matrix[10] = 1.0;
+ matrix[11] = 0.0;
+ matrix[12] = 0.0;
+ matrix[13] = 0.0;
+ matrix[14] = 0.0;
+ matrix[15] = 1.0;
+ return matrix;
+ },
+
+ premultiplyMatrix: function (out, matrixA, matrixB) { //out = matrixB * matrixA
+ var b0 = matrixB[0], b4 = matrixB[4], b8 = matrixB[8], b12 = matrixB[12],
+ b1 = matrixB[1], b5 = matrixB[5], b9 = matrixB[9], b13 = matrixB[13],
+ b2 = matrixB[2], b6 = matrixB[6], b10 = matrixB[10], b14 = matrixB[14],
+ b3 = matrixB[3], b7 = matrixB[7], b11 = matrixB[11], b15 = matrixB[15],
+
+ aX = matrixA[0], aY = matrixA[1], aZ = matrixA[2], aW = matrixA[3];
+ out[0] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW;
+ out[1] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW;
+ out[2] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW;
+ out[3] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW;
+
+ aX = matrixA[4], aY = matrixA[5], aZ = matrixA[6], aW = matrixA[7];
+ out[4] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW;
+ out[5] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW;
+ out[6] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW;
+ out[7] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW;
+
+ aX = matrixA[8], aY = matrixA[9], aZ = matrixA[10], aW = matrixA[11];
+ out[8] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW;
+ out[9] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW;
+ out[10] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW;
+ out[11] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW;
+
+ aX = matrixA[12], aY = matrixA[13], aZ = matrixA[14], aW = matrixA[15];
+ out[12] = b0 * aX + b4 * aY + b8 * aZ + b12 * aW;
+ out[13] = b1 * aX + b5 * aY + b9 * aZ + b13 * aW;
+ out[14] = b2 * aX + b6 * aY + b10 * aZ + b14 * aW;
+ out[15] = b3 * aX + b7 * aY + b11 * aZ + b15 * aW;
+
+ return out;
+ },
+
+ makeXRotationMatrix: function (matrix, angle) {
+ matrix[0] = 1.0;
+ matrix[1] = 0.0;
+ matrix[2] = 0.0;
+ matrix[3] = 0.0;
+ matrix[4] = 0.0;
+ matrix[5] = Math.cos(angle);
+ matrix[6] = Math.sin(angle);
+ matrix[7] = 0.0;
+ matrix[8] = 0.0;
+ matrix[9] = -Math.sin(angle);
+ matrix[10] = Math.cos(angle);
+ matrix[11] = 0.0;
+ matrix[12] = 0.0;
+ matrix[13] = 0.0;
+ matrix[14] = 0.0;
+ matrix[15] = 1.0;
+ return matrix;
+ },
+
+ makeYRotationMatrix: function (matrix, angle) {
+ matrix[0] = Math.cos(angle);
+ matrix[1] = 0.0
+ matrix[2] = -Math.sin(angle);
+ matrix[3] = 0.0
+ matrix[4] = 0.0
+ matrix[5] = 1.0
+ matrix[6] = 0.0;
+ matrix[7] = 0.0;
+ matrix[8] = Math.sin(angle);
+ matrix[9] = 0.0
+ matrix[10] = Math.cos(angle);
+ matrix[11] = 0.0;
+ matrix[12] = 0.0;
+ matrix[13] = 0.0;
+ matrix[14] = 0.0;
+ matrix[15] = 1.0;
+ return matrix;
+ },
+
+
+ transformDirectionByMatrix: function (out, v, m) {
+ var x = v[0], y = v[1], z = v[2];
+ out[0] = m[0] * x + m[4] * y + m[8] * z;
+ out[1] = m[1] * x + m[5] * y + m[9] * z;
+ out[2] = m[2] * x + m[6] * y + m[10] * z;
+ out[3] = m[3] * x + m[7] * y + m[11] * z;
+ return out;
+ },
+
+ invertMatrix: function (out, m) {
+ var m0 = m[0], m4 = m[4], m8 = m[8], m12 = m[12],
+ m1 = m[1], m5 = m[5], m9 = m[9], m13 = m[13],
+ m2 = m[2], m6 = m[6], m10 = m[10], m14 = m[14],
+ m3 = m[3], m7 = m[7], m11 = m[11], m15 = m[15],
+
+ temp0 = m10 * m15,
+ temp1 = m14 * m11,
+ temp2 = m6 * m15,
+ temp3 = m14 * m7,
+ temp4 = m6 * m11,
+ temp5 = m10 * m7,
+ temp6 = m2 * m15,
+ temp7 = m14 * m3,
+ temp8 = m2 * m11,
+ temp9 = m10 * m3,
+ temp10 = m2 * m7,
+ temp11 = m6 * m3,
+ temp12 = m8 * m13,
+ temp13 = m12 * m9,
+ temp14 = m4 * m13,
+ temp15 = m12 * m5,
+ temp16 = m4 * m9,
+ temp17 = m8 * m5,
+ temp18 = m0 * m13,
+ temp19 = m12 * m1,
+ temp20 = m0 * m9,
+ temp21 = m8 * m1,
+ temp22 = m0 * m5,
+ temp23 = m4 * m1,
+
+ t0 = (temp0 * m5 + temp3 * m9 + temp4 * m13) - (temp1 * m5 + temp2 * m9 + temp5 * m13),
+ t1 = (temp1 * m1 + temp6 * m9 + temp9 * m13) - (temp0 * m1 + temp7 * m9 + temp8 * m13),
+ t2 = (temp2 * m1 + temp7 * m5 + temp10 * m13) - (temp3 * m1 + temp6 * m5 + temp11 * m13),
+ t3 = (temp5 * m1 + temp8 * m5 + temp11 * m9) - (temp4 * m1 + temp9 * m5 + temp10 * m9),
+
+ d = 1.0 / (m0 * t0 + m4 * t1 + m8 * t2 + m12 * t3);
+
+ out[0] = d * t0;
+ out[1] = d * t1;
+ out[2] = d * t2;
+ out[3] = d * t3;
+ out[4] = d * ((temp1 * m4 + temp2 * m8 + temp5 * m12) - (temp0 * m4 + temp3 * m8 + temp4 * m12));
+ out[5] = d * ((temp0 * m0 + temp7 * m8 + temp8 * m12) - (temp1 * m0 + temp6 * m8 + temp9 * m12));
+ out[6] = d * ((temp3 * m0 + temp6 * m4 + temp11 * m12) - (temp2 * m0 + temp7 * m4 + temp10 * m12));
+ out[7] = d * ((temp4 * m0 + temp9 * m4 + temp10 * m8) - (temp5 * m0 + temp8 * m4 + temp11 * m8));
+ out[8] = d * ((temp12 * m7 + temp15 * m11 + temp16 * m15) - (temp13 * m7 + temp14 * m11 + temp17 * m15));
+ out[9] = d * ((temp13 * m3 + temp18 * m11 + temp21 * m15) - (temp12 * m3 + temp19 * m11 + temp20 * m15));
+ out[10] = d * ((temp14 * m3 + temp19 * m7 + temp22 * m15) - (temp15 * m3 + temp18 * m7 + temp23 * m15));
+ out[11] = d * ((temp17 * m3 + temp20 * m7 + temp23 * m11) - (temp16 * m3 + temp21 * m7 + temp22 * m11));
+ out[12] = d * ((temp14 * m10 + temp17 * m14 + temp13 * m6) - (temp16 * m14 + temp12 * m6 + temp15 * m10));
+ out[13] = d * ((temp20 * m14 + temp12 * m2 + temp19 * m10) - (temp18 * m10 + temp21 * m14 + temp13 * m2));
+ out[14] = d * ((temp18 * m6 + temp23 * m14 + temp15 * m2) - (temp22 * m14 + temp14 * m2 + temp19 * m6));
+ out[15] = d * ((temp22 * m10 + temp16 * m2 + temp21 * m6) - (temp20 * m6 + temp23 * m10 + temp17 * m2));
+
+ return out;
+ },
+
+ makeLookAtMatrix: function (matrix, eye, target, up) { //up is assumed to be normalized
+ var forwardX = eye[0] - target[0],
+ forwardY = eye[1] - target[1],
+ forwardZ = eye[2] - target[2];
+ var forwardMagnitude = Math.sqrt(forwardX * forwardX + forwardY * forwardY + forwardZ * forwardZ);
+ forwardX /= forwardMagnitude;
+ forwardY /= forwardMagnitude;
+ forwardZ /= forwardMagnitude;
+
+ var rightX = up[2] * forwardY - up[1] * forwardZ;
+ var rightY = up[0] * forwardZ - up[2] * forwardX;
+ var rightZ = up[1] * forwardX - up[0] * forwardY;
+
+ var rightMagnitude = Math.sqrt(rightX * rightX + rightY * rightY + rightZ * rightZ);
+ rightX /= rightMagnitude;
+ rightY /= rightMagnitude;
+ rightZ /= rightMagnitude;
+
+ var newUpX = forwardY * rightZ - forwardZ * rightY;
+ var newUpY = forwardZ * rightX - forwardX * rightZ;
+ var newUpZ = forwardX * rightY - forwardY * rightX;
+
+ var newUpMagnitude = Math.sqrt(newUpX * newUpX + newUpY * newUpY + newUpZ * newUpZ);
+ newUpX /= newUpMagnitude;
+ newUpY /= newUpMagnitude;
+ newUpZ /= newUpMagnitude;
+
+ matrix[0] = rightX;
+ matrix[1] = newUpX;
+ matrix[2] = forwardX;
+ matrix[3] = 0;
+ matrix[4] = rightY;
+ matrix[5] = newUpY;
+ matrix[6] = forwardY;
+ matrix[7] = 0;
+ matrix[8] = rightZ;
+ matrix[9] = newUpZ;
+ matrix[10] = forwardZ;
+ matrix[11] = 0;
+ matrix[12] = -(rightX * eye[0] + rightY * eye[1] + rightZ * eye[2]);
+ matrix[13] = -(newUpX * eye[0] + newUpY * eye[1] + newUpZ * eye[2]);
+ matrix[14] = -(forwardX * eye[0] + forwardY * eye[1] + forwardZ * eye[2]);
+ matrix[15] = 1;
+ },
+
+ makeOrthographicMatrix: function (matrix, left, right, bottom, top, near, far) {
+ matrix[0] = 2 / (right - left);
+ matrix[1] = 0;
+ matrix[2] = 0;
+ matrix[3] = 0;
+ matrix[4] = 0;
+ matrix[5] = 2 / (top - bottom);
+ matrix[6] = 0;
+ matrix[7] = 0;
+ matrix[8] = 0;
+ matrix[9] = 0;
+ matrix[10] = -2 / (far - near);
+ matrix[11] = 0;
+ matrix[12] = -(right + left) / (right - left);
+ matrix[13] = -(top + bottom) / (top - bottom);
+ matrix[14] = -(far + near) / (far - near);
+ matrix[15] = 1;
+
+ return matrix;
+ },
+}
+
diff --git a/wrappedgl.js b/wrappedgl.js
new file mode 100644
index 0000000..ce98a24
--- /dev/null
+++ b/wrappedgl.js
@@ -0,0 +1,1435 @@
+'use strict'
+
+var WrappedGL = (function () {
+
+ var CONSTANT_NAMES = [
+ 'ACTIVE_ATTRIBUTES',
+ 'ACTIVE_ATTRIBUTE_MAX_LENGTH',
+ 'ACTIVE_TEXTURE',
+ 'ACTIVE_UNIFORMS',
+ 'ACTIVE_UNIFORM_MAX_LENGTH',
+ 'ALIASED_LINE_WIDTH_RANGE',
+ 'ALIASED_POINT_SIZE_RANGE',
+ 'ALPHA',
+ 'ALPHA_BITS',
+ 'ALWAYS',
+ 'ARRAY_BUFFER',
+ 'ARRAY_BUFFER_BINDING',
+ 'ATTACHED_SHADERS',
+ 'BACK',
+ 'BLEND',
+ 'BLEND_COLOR',
+ 'BLEND_DST_ALPHA',
+ 'BLEND_DST_RGB',
+ 'BLEND_EQUATION',
+ 'BLEND_EQUATION_ALPHA',
+ 'BLEND_EQUATION_RGB',
+ 'BLEND_SRC_ALPHA',
+ 'BLEND_SRC_RGB',
+ 'BLUE_BITS',
+ 'BOOL',
+ 'BOOL_VEC2',
+ 'BOOL_VEC3',
+ 'BOOL_VEC4',
+ 'BROWSER_DEFAULT_WEBGL',
+ 'BUFFER_SIZE',
+ 'BUFFER_USAGE',
+ 'BYTE',
+ 'CCW',
+ 'CLAMP_TO_EDGE',
+ 'COLOR_ATTACHMENT0',
+ 'COLOR_BUFFER_BIT',
+ 'COLOR_CLEAR_VALUE',
+ 'COLOR_WRITEMASK',
+ 'COMPILE_STATUS',
+ 'COMPRESSED_TEXTURE_FORMATS',
+ 'CONSTANT_ALPHA',
+ 'CONSTANT_COLOR',
+ 'CONTEXT_LOST_WEBGL',
+ 'CULL_FACE',
+ 'CULL_FACE_MODE',
+ 'CURRENT_PROGRAM',
+ 'CURRENT_VERTEX_ATTRIB',
+ 'CW',
+ 'DECR',
+ 'DECR_WRAP',
+ 'DELETE_STATUS',
+ 'DEPTH_ATTACHMENT',
+ 'DEPTH_BITS',
+ 'DEPTH_BUFFER_BIT',
+ 'DEPTH_CLEAR_VALUE',
+ 'DEPTH_COMPONENT',
+ 'DEPTH_COMPONENT16',
+ 'DEPTH_FUNC',
+ 'DEPTH_RANGE',
+ 'DEPTH_STENCIL',
+ 'DEPTH_STENCIL_ATTACHMENT',
+ 'DEPTH_TEST',
+ 'DEPTH_WRITEMASK',
+ 'DITHER',
+ 'DONT_CARE',
+ 'DST_ALPHA',
+ 'DST_COLOR',
+ 'DYNAMIC_DRAW',
+ 'ELEMENT_ARRAY_BUFFER',
+ 'ELEMENT_ARRAY_BUFFER_BINDING',
+ 'EQUAL',
+ 'FASTEST',
+ 'FLOAT',
+ 'FLOAT_MAT2',
+ 'FLOAT_MAT3',
+ 'FLOAT_MAT4',
+ 'FLOAT_VEC2',
+ 'FLOAT_VEC3',
+ 'FLOAT_VEC4',
+ 'FRAGMENT_SHADER',
+ 'FRAMEBUFFER',
+ 'FRAMEBUFFER_ATTACHMENT_OBJECT_NAME',
+ 'FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE',
+ 'FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE',
+ 'FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL',
+ 'FRAMEBUFFER_BINDING',
+ 'FRAMEBUFFER_COMPLETE',
+ 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT',
+ 'FRAMEBUFFER_INCOMPLETE_DIMENSIONS',
+ 'FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT',
+ 'FRAMEBUFFER_UNSUPPORTED',
+ 'FRONT',
+ 'FRONT_AND_BACK',
+ 'FRONT_FACE',
+ 'FUNC_ADD',
+ 'FUNC_REVERSE_SUBTRACT',
+ 'FUNC_SUBTRACT',
+ 'GENERATE_MIPMAP_HINT',
+ 'GEQUAL',
+ 'GREATER',
+ 'GREEN_BITS',
+ 'HIGH_FLOAT',
+ 'HIGH_INT',
+ 'INCR',
+ 'INCR_WRAP',
+ 'INFO_LOG_LENGTH',
+ 'INT',
+ 'INT_VEC2',
+ 'INT_VEC3',
+ 'INT_VEC4',
+ 'INVALID_ENUM',
+ 'INVALID_FRAMEBUFFER_OPERATION',
+ 'INVALID_OPERATION',
+ 'INVALID_VALUE',
+ 'INVERT',
+ 'KEEP',
+ 'LEQUAL',
+ 'LESS',
+ 'LINEAR',
+ 'LINEAR_MIPMAP_LINEAR',
+ 'LINEAR_MIPMAP_NEAREST',
+ 'LINES',
+ 'LINE_LOOP',
+ 'LINE_STRIP',
+ 'LINE_WIDTH',
+ 'LINK_STATUS',
+ 'LOW_FLOAT',
+ 'LOW_INT',
+ 'LUMINANCE',
+ 'LUMINANCE_ALPHA',
+ 'MAX_COMBINED_TEXTURE_IMAGE_UNITS',
+ 'MAX_CUBE_MAP_TEXTURE_SIZE',
+ 'MAX_FRAGMENT_UNIFORM_VECTORS',
+ 'MAX_RENDERBUFFER_SIZE',
+ 'MAX_TEXTURE_IMAGE_UNITS',
+ 'MAX_TEXTURE_SIZE',
+ 'MAX_VARYING_VECTORS',
+ 'MAX_VERTEX_ATTRIBS',
+ 'MAX_VERTEX_TEXTURE_IMAGE_UNITS',
+ 'MAX_VERTEX_UNIFORM_VECTORS',
+ 'MAX_VIEWPORT_DIMS',
+ 'MEDIUM_FLOAT',
+ 'MEDIUM_INT',
+ 'MIRRORED_REPEAT',
+ 'NEAREST',
+ 'NEAREST_MIPMAP_LINEAR',
+ 'NEAREST_MIPMAP_NEAREST',
+ 'NEVER',
+ 'NICEST',
+ 'NONE',
+ 'NOTEQUAL',
+ 'NO_ERROR',
+ 'NUM_COMPRESSED_TEXTURE_FORMATS',
+ 'ONE',
+ 'ONE_MINUS_CONSTANT_ALPHA',
+ 'ONE_MINUS_CONSTANT_COLOR',
+ 'ONE_MINUS_DST_ALPHA',
+ 'ONE_MINUS_DST_COLOR',
+ 'ONE_MINUS_SRC_ALPHA',
+ 'ONE_MINUS_SRC_COLOR',
+ 'OUT_OF_MEMORY',
+ 'PACK_ALIGNMENT',
+ 'POINTS',
+ 'POLYGON_OFFSET_FACTOR',
+ 'POLYGON_OFFSET_FILL',
+ 'POLYGON_OFFSET_UNITS',
+ 'RED_BITS',
+ 'RENDERBUFFER',
+ 'RENDERBUFFER_ALPHA_SIZE',
+ 'RENDERBUFFER_BINDING',
+ 'RENDERBUFFER_BLUE_SIZE',
+ 'RENDERBUFFER_DEPTH_SIZE',
+ 'RENDERBUFFER_GREEN_SIZE',
+ 'RENDERBUFFER_HEIGHT',
+ 'RENDERBUFFER_INTERNAL_FORMAT',
+ 'RENDERBUFFER_RED_SIZE',
+ 'RENDERBUFFER_STENCIL_SIZE',
+ 'RENDERBUFFER_WIDTH',
+ 'RENDERER',
+ 'REPEAT',
+ 'REPLACE',
+ 'RGB',
+ 'RGB5_A1',
+ 'RGB565',
+ 'RGBA',
+ 'RGBA4',
+ 'SAMPLER_2D',
+ 'SAMPLER_CUBE',
+ 'SAMPLES',
+ 'SAMPLE_ALPHA_TO_COVERAGE',
+ 'SAMPLE_BUFFERS',
+ 'SAMPLE_COVERAGE',
+ 'SAMPLE_COVERAGE_INVERT',
+ 'SAMPLE_COVERAGE_VALUE',
+ 'SCISSOR_BOX',
+ 'SCISSOR_TEST',
+ 'SHADER_COMPILER',
+ 'SHADER_SOURCE_LENGTH',
+ 'SHADER_TYPE',
+ 'SHADING_LANGUAGE_VERSION',
+ 'SHORT',
+ 'SRC_ALPHA',
+ 'SRC_ALPHA_SATURATE',
+ 'SRC_COLOR',
+ 'STATIC_DRAW',
+ 'STENCIL_ATTACHMENT',
+ 'STENCIL_BACK_FAIL',
+ 'STENCIL_BACK_FUNC',
+ 'STENCIL_BACK_PASS_DEPTH_FAIL',
+ 'STENCIL_BACK_PASS_DEPTH_PASS',
+ 'STENCIL_BACK_REF',
+ 'STENCIL_BACK_VALUE_MASK',
+ 'STENCIL_BACK_WRITEMASK',
+ 'STENCIL_BITS',
+ 'STENCIL_BUFFER_BIT',
+ 'STENCIL_CLEAR_VALUE',
+ 'STENCIL_FAIL',
+ 'STENCIL_FUNC',
+ 'STENCIL_INDEX',
+ 'STENCIL_INDEX8',
+ 'STENCIL_PASS_DEPTH_FAIL',
+ 'STENCIL_PASS_DEPTH_PASS',
+ 'STENCIL_REF',
+ 'STENCIL_TEST',
+ 'STENCIL_VALUE_MASK',
+ 'STENCIL_WRITEMASK',
+ 'STREAM_DRAW',
+ 'SUBPIXEL_BITS',
+ 'TEXTURE',
+ 'TEXTURE0',
+ 'TEXTURE1',
+ 'TEXTURE2',
+ 'TEXTURE3',
+ 'TEXTURE4',
+ 'TEXTURE5',
+ 'TEXTURE6',
+ 'TEXTURE7',
+ 'TEXTURE8',
+ 'TEXTURE9',
+ 'TEXTURE10',
+ 'TEXTURE11',
+ 'TEXTURE12',
+ 'TEXTURE13',
+ 'TEXTURE14',
+ 'TEXTURE15',
+ 'TEXTURE16',
+ 'TEXTURE17',
+ 'TEXTURE18',
+ 'TEXTURE19',
+ 'TEXTURE20',
+ 'TEXTURE21',
+ 'TEXTURE22',
+ 'TEXTURE23',
+ 'TEXTURE24',
+ 'TEXTURE25',
+ 'TEXTURE26',
+ 'TEXTURE27',
+ 'TEXTURE28',
+ 'TEXTURE29',
+ 'TEXTURE30',
+ 'TEXTURE31',
+ 'TEXTURE_2D',
+ 'TEXTURE_BINDING_2D',
+ 'TEXTURE_BINDING_CUBE_MAP',
+ 'TEXTURE_CUBE_MAP',
+ 'TEXTURE_CUBE_MAP_NEGATIVE_X',
+ 'TEXTURE_CUBE_MAP_NEGATIVE_Y',
+ 'TEXTURE_CUBE_MAP_NEGATIVE_Z',
+ 'TEXTURE_CUBE_MAP_POSITIVE_X',
+ 'TEXTURE_CUBE_MAP_POSITIVE_Y',
+ 'TEXTURE_CUBE_MAP_POSITIVE_Z',
+ 'TEXTURE_MAG_FILTER',
+ 'TEXTURE_MIN_FILTER',
+ 'TEXTURE_WRAP_S',
+ 'TEXTURE_WRAP_T',
+ 'TRIANGLES',
+ 'TRIANGLE_FAN',
+ 'TRIANGLE_STRIP',
+ 'UNPACK_ALIGNMENT',
+ 'UNPACK_COLORSPACE_CONVERSION_WEBGL',
+ 'UNPACK_FLIP_Y_WEBGL',
+ 'UNPACK_PREMULTIPLY_ALPHA_WEBGL',
+ 'UNSIGNED_BYTE',
+ 'UNSIGNED_INT',
+ 'UNSIGNED_SHORT',
+ 'UNSIGNED_SHORT_4_4_4_4',
+ 'UNSIGNED_SHORT_5_5_5_1',
+ 'UNSIGNED_SHORT_5_6_5',
+ 'VALIDATE_STATUS',
+ 'VENDOR',
+ 'VERSION',
+ 'VERTEX_ATTRIB_ARRAY_BUFFER_BINDING',
+ 'VERTEX_ATTRIB_ARRAY_ENABLED',
+ 'VERTEX_ATTRIB_ARRAY_NORMALIZED',
+ 'VERTEX_ATTRIB_ARRAY_POINTER',
+ 'VERTEX_ATTRIB_ARRAY_SIZE',
+ 'VERTEX_ATTRIB_ARRAY_STRIDE',
+ 'VERTEX_ATTRIB_ARRAY_TYPE',
+ 'VERTEX_SHADER',
+ 'VIEWPORT',
+ 'ZERO'
+ ];
+
+
+ function WrappedGL (canvas, options) {
+ var gl = this.gl = canvas.getContext('webgl', options) || canvas.getContext('experimental-webgl', options);
+
+ for (var i = 0; i < CONSTANT_NAMES.length; i += 1) {
+ this[CONSTANT_NAMES[i]] = gl[CONSTANT_NAMES[i]];
+ };
+
+ this.changedParameters = {}; //parameters that aren't default
+
+ //each parameter is an object like
+ /*
+ {
+ defaults: [values],
+ setter: function (called with this set to gl)
+
+ //undefined flag means not used
+ usedInDraw: whether this state matters for drawing
+ usedInClear: whether this state matters for clearing
+ usedInRead: wheter this state matters for reading
+ }
+
+ //the number of parameters in each defaults array corresponds to the arity of the corresponding setter
+ */
+
+ this.parameters = {
+ 'framebuffer': {
+ defaults: [null],
+ setter: function (framebuffer) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
+ },
+ usedInDraw: true,
+ usedInClear: true,
+ usedInRead: true
+ },
+ 'program': {
+ defaults: [ {program: null} ],
+ setter: function (wrappedProgram) {
+ gl.useProgram(wrappedProgram.program);
+ },
+ usedInDraw: true
+ },
+ 'viewport': {
+ defaults: [0, 0, 0, 0],
+ setter: gl.viewport,
+ usedInDraw: true
+ },
+ 'indexBuffer': {
+ defaults: [null],
+ setter: function (buffer) {
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
+ },
+ usedInDraw: true
+ },
+ 'depthTest': {
+ defaults: [false],
+ setter: function (enabled) {
+ if (enabled) {
+ gl.enable(gl.DEPTH_TEST);
+ } else {
+ gl.disable(gl.DEPTH_TEST);
+ }
+ },
+ usedInDraw: true
+ },
+ 'depthFunc': {
+ defaults: [gl.LESS],
+ setter: gl.depthFunc,
+ usedInDraw: true
+ },
+ 'cullFace': {
+ defaults: [false],
+ setter: function (enabled) {
+ if (enabled) {
+ gl.enable(gl.CULL_FACE);
+ } else {
+ gl.disable(gl.CULL_FACE);
+ }
+ },
+ usedInDraw: true
+ },
+ 'frontFace': {
+ defaults: [gl.CCW],
+ setter: gl.frontFace
+ },
+ 'blend': {
+ defaults: [false],
+ setter: function (enabled) {
+ if (enabled) {
+ gl.enable(gl.BLEND);
+ } else {
+ gl.disable(gl.BLEND);
+ }
+ },
+ usedInDraw: true
+ },
+ 'blendEquation': {
+ defaults: [gl.FUNC_ADD, gl.FUNC_ADD],
+ setter: gl.blendEquationSeparate,
+ usedInDraw: true
+ },
+ 'blendFunc': {
+ defaults: [gl.ONE, gl.ZERO, gl.ONE, gl.ZERO],
+ setter: gl.blendFuncSeparate,
+ usedInDraw: true
+ },
+ 'polygonOffsetFill': {
+ defaults: [false],
+ setter: function (enabled) {
+ if (enabled) {
+ gl.enable(gl.POLYGON_OFFSET_FILL);
+ } else {
+ gl.disable(gl.POLYGON_OFFSET_FILL);
+ }
+ },
+ usedInDraw: true
+ },
+ 'polygonOffset': {
+ defaults: [0, 0],
+ setter: gl.polygonOffset,
+ usedInDraw: true
+ },
+ 'scissorTest': {
+ defaults: [false],
+ setter: function (enabled) {
+ if (enabled) {
+ gl.enable(gl.SCISSOR_TEST);
+ } else {
+ gl.disable(gl.SCISSOR_TEST);
+ }
+ },
+ usedInDraw: true,
+ usedInClear: true
+ },
+ 'scissor': {
+ defaults: [0, 0, 0, 0],
+ setter: gl.scissor,
+ usedInDraw: true,
+ usedInClear: true
+ },
+ 'colorMask': {
+ defaults: [true, true, true, true],
+ setter: gl.colorMask,
+ usedInDraw: true,
+ usedInClear: true
+ },
+ 'depthMask': {
+ defaults: [true],
+ setter: gl.depthMask,
+ usedInDraw: true,
+ usedInClear: true
+ },
+ 'clearColor': {
+ defaults: [0, 0, 0, 0],
+ setter: gl.clearColor,
+ usedInClear: true
+ },
+ 'clearDepth': {
+ defaults: [1],
+ setter: gl.clearDepth,
+ usedInClear: true
+ }
+ };
+
+
+ var maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
+ for (var i = 0; i < maxVertexAttributes; ++i) {
+ //we need to capture the index in a closure
+ this.parameters['attributeArray' + i.toString()] = {
+ defaults: [null, 0, null, false, 0, 0],
+ setter: (function () {
+ var index = i;
+
+ return function (buffer, size, type, normalized, stride, offset) {
+ if (buffer !== null) {
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+ gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
+
+ gl.enableVertexAttribArray(index); //TODO: cache this
+ }
+ }
+ }()),
+ usedInDraw: true
+ };
+ }
+
+ var maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
+ for (var i = 0; i < maxTextures; ++i) {
+ this.parameters['texture' + i.toString()] = {
+ defaults: [gl.TEXTURE_2D, null],
+ setter: (function () {
+ //we need to capture the unit in a closure
+ var unit = i;
+
+ return function (target, texture) {
+ gl.activeTexture(gl.TEXTURE0 + unit);
+ gl.bindTexture(target, texture);
+ }
+ }()),
+ usedInDraw: true
+ };
+ }
+
+
+ this.uniformSetters = {
+ '1i': gl.uniform1i,
+ '2i': gl.uniform2i,
+ '3i': gl.uniform3i,
+ '4i': gl.uniform4i,
+ '1f': gl.uniform1f,
+ '2f': gl.uniform2f,
+ '3f': gl.uniform3f,
+ '4f': gl.uniform4f,
+ '1fv': gl.uniform1fv,
+ '2fv': gl.uniform2fv,
+ '3fv': gl.uniform3fv,
+ '4fv': gl.uniform4fv,
+ 'matrix2fv': gl.uniformMatrix2fv,
+ 'matrix3fv': gl.uniformMatrix3fv,
+ 'matrix4fv': gl.uniformMatrix4fv
+ };
+
+
+ this.defaultTextureUnit = 0; //the texure unit we use for modifying textures
+
+ }
+
+ WrappedGL.checkWebGLSupport = function (successCallback, failureCallback) {
+ WrappedGL.checkWebGLSupportWithExtensions([], successCallback, function (hasWebGL, unsupportedExtensions) {
+ failureCallback();
+ });
+ }
+
+ WrappedGL.checkWebGLSupportWithExtensions = function (extensions, successCallback, failureCallback) { //successCallback(), failureCallback(hasWebGL, unsupportedExtensions)
+ var canvas = document.createElement('canvas');
+ var gl = null;
+ try {
+ gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+ } catch (e) {
+ failureCallback(false, []); //no webgl support
+ return;
+ }
+ if (gl === null) {
+ failureCallback(false, []); //no webgl support
+ return;
+ }
+
+ var unsupportedExtensions = [];
+ for (var i = 0; i < extensions.length; ++i) {
+ if (gl.getExtension(extensions[i]) === null) {
+ unsupportedExtensions.push(extensions[i]);
+ }
+ }
+ if (unsupportedExtensions.length > 0) {
+ failureCallback(true, unsupportedExtensions); //webgl support but no extensions
+ return;
+ }
+
+ //webgl support and all required extensions
+ successCallback();
+ };
+
+ WrappedGL.prototype.getSupportedExtensions = function () {
+ return this.gl.getSupportedExtensions();
+ };
+
+ //returns null if the extension is not supported, otherwise the extension object
+ WrappedGL.prototype.getExtension = function (name) {
+ var gl = this.gl;
+
+ //for certain extensions, we need to expose additional, wrapped rendering compatible, methods directly on WrappedGL and DrawState
+ if (name === 'ANGLE_instanced_arrays') {
+ var instancedExt = gl.getExtension('ANGLE_instanced_arrays');
+
+ if (instancedExt !== null) {
+ this.instancedExt = instancedExt;
+
+ var maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
+
+ for (var i = 0; i < maxVertexAttributes; ++i) {
+ this.parameters['attributeDivisor' + i.toString()] = {
+ defaults: [0],
+ setter: (function () {
+ var index = i;
+
+ return function (divisor) {
+ instancedExt.vertexAttribDivisorANGLE(index, divisor);
+ }
+ }()),
+ usedInDraw: true
+ };
+ }
+
+ //override vertexAttribPointer
+ DrawState.prototype.vertexAttribPointer = function (buffer, index, size, type, normalized, stride, offset) {
+ this.setParameter('attributeArray' + index.toString(), [buffer, size, type, normalized, stride, offset]);
+
+ if (this.changedParameters.hasOwnProperty('attributeDivisor' + index.toString())) {
+ //we need to have divisor information for any attribute location that has a bound buffer
+ this.setParameter('attributeDivisor' + index.toString(), [0]);
+ }
+
+ return this;
+ };
+
+ DrawState.prototype.vertexAttribDivisorANGLE = function (index, divisor) {
+ this.setParameter('attributeDivisor' + index.toString(), [divisor]);
+ return this;
+ };
+
+ this.drawArraysInstancedANGLE = function (drawState, mode, first, count, primcount) {
+ this.resolveDrawState(drawState);
+
+ this.instancedExt.drawArraysInstancedANGLE(mode, first, count, primcount);
+ };
+
+ this.drawElementsInstancedANGLE = function (drawState, mode, count, type, indices, primcount) {
+ this.resolveDrawState(drawState);
+
+ this.instancedExt.drawElementsInstancedANGLE(mode, count, type, indices, primcount);
+ };
+
+ return {};
+ } else {
+ return null;
+ }
+
+ } else { //all others, we can just return as is (we can treat them as simple enums)
+ return gl.getExtension(name);
+ }
+ };
+
+ //flag is one of usedInDraw, usedInClear, usedInRead
+ WrappedGL.prototype.resolveState = function (state, flag) {
+ var gl = this.gl;
+
+
+ //first let's revert all states to default that were set but now aren't set
+ for (var parameterName in this.changedParameters) {
+ if (this.changedParameters.hasOwnProperty(parameterName)) {
+ if (!state.changedParameters.hasOwnProperty(parameterName)) { //if this is not set in the incoming draw state then we need to go back to default
+ if (this.parameters[parameterName][flag]) {
+ this.parameters[parameterName].setter.apply(this.gl, this.parameters[parameterName].defaults);
+
+ delete this.changedParameters[parameterName];
+ }
+ }
+ }
+ }
+
+
+ //now we set all of the new incoming states
+
+ for (var parameterName in state.changedParameters) {
+ if (state.changedParameters.hasOwnProperty(parameterName)) {
+
+ if (!this.changedParameters.hasOwnProperty(parameterName) || //if this state is not currently set
+ !arraysEqual(this.changedParameters[parameterName], state.changedParameters[parameterName]) //or if it's changed
+ ) {
+
+ this.changedParameters[parameterName] = state.changedParameters[parameterName];
+
+ this.parameters[parameterName].setter.apply(this.gl, this.changedParameters[parameterName]);
+ }
+ }
+ }
+ }
+
+ WrappedGL.prototype.resolveDrawState = function (drawState) {
+ var gl = this.gl;
+
+ this.resolveState(drawState, 'usedInDraw');
+
+ //resolve uniform values
+ //we don't diff uniform values, it's just not worth it
+ var program = drawState.changedParameters.program[0]; //we assume a draw state has a program
+
+ for (var uniformName in drawState.uniforms) {
+ if (drawState.uniforms.hasOwnProperty(uniformName)) {
+ //this array creation is annoying....
+ var args = [program.uniformLocations[uniformName]].concat(drawState.uniforms[uniformName].value);
+
+ this.uniformSetters[drawState.uniforms[uniformName].type].apply(gl, args);
+ }
+ }
+
+ };
+
+ WrappedGL.prototype.drawArrays = function (drawState, mode, first, count) {
+ this.resolveDrawState(drawState);
+
+ this.gl.drawArrays(mode, first, count);
+ };
+
+ WrappedGL.prototype.drawElements = function (drawState, mode, count, type, offset) {
+ this.resolveDrawState(drawState);
+
+ this.gl.drawElements(mode, count, type, offset);
+ };
+
+ WrappedGL.prototype.resolveClearState = function (clearState) {
+ this.resolveState(clearState, 'usedInClear');
+ };
+
+ WrappedGL.prototype.clear = function (clearState, bit) {
+ this.resolveClearState(clearState);
+
+ this.gl.clear(bit);
+ };
+
+ WrappedGL.prototype.resolveReadState = function (readState) {
+ this.resolveState(readState, 'usedInRead');
+ };
+
+ WrappedGL.prototype.readPixels = function (readState, x, y, width, height, format, type, pixels) {
+ this.resolveReadState(readState);
+
+ this.gl.readPixels(x, y, width, height, format, type, pixels);
+ };
+
+ WrappedGL.prototype.finish = function () {
+ this.gl.finish();
+ return this;
+ };
+
+ WrappedGL.prototype.flush = function () {
+ this.gl.flush();
+ return this;
+ };
+
+ WrappedGL.prototype.getError = function () {
+ return this.gl.getError();
+ };
+
+ WrappedGL.prototype.createFramebuffer = function () {
+ return this.gl.createFramebuffer();
+ };
+
+ WrappedGL.prototype.framebufferTexture2D = function (framebuffer, target, attachment, textarget, texture, level) {
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
+ this.changedParameters['framebuffer'] = framebuffer;
+
+ this.gl.framebufferTexture2D(target, attachment, textarget, texture, level);
+
+ return this;
+ };
+
+ WrappedGL.prototype.framebufferRenderbuffer = function (framebuffer, target, attachment, renderbuffertarget, renderbuffer) {
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
+ this.changedParameters['framebuffer'] = framebuffer;
+
+ this.gl.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer);
+ };
+
+ WrappedGL.prototype.drawBuffers = function (framebuffer, buffers) {
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
+ this.changedParameters['framebuffer'] = framebuffer;
+
+ this.drawExt.drawBuffersWEBGL(buffers);
+ };
+
+ WrappedGL.prototype.createTexture = function () {
+ return this.gl.createTexture();
+ };
+
+ WrappedGL.prototype.bindTextureForEditing = function (target, texture) {
+ this.gl.activeTexture(this.gl.TEXTURE0 + this.defaultTextureUnit);
+ this.gl.bindTexture(target, texture);
+
+ this.changedParameters['texture' + this.defaultTextureUnit.toString()] = [target, texture];
+ };
+
+ //this function is overloaded, it can be either
+ //(target, texture, level, internalformat, width, height, border, format, type, pixels)
+ //(target, texture, level, internalformat, format, type, object)
+ WrappedGL.prototype.texImage2D = function (target, texture) {
+ var args = Array.prototype.slice.call(arguments, 2);
+ args.unshift(target); //add target to for texImage2D arguments list
+
+ this.bindTextureForEditing(target, texture);
+ this.gl.texImage2D.apply(this.gl, args);
+
+ return this;
+ };
+
+ WrappedGL.prototype.texParameteri = function(target, texture, pname, param) {
+ this.bindTextureForEditing(target, texture);
+ this.gl.texParameteri(target, pname, param);
+
+ return this;
+ };
+
+ WrappedGL.prototype.texParameterf = function(target, texture, pname, param) {
+ this.bindTextureForEditing(target, texture);
+ this.gl.texParameterf(target, pname, param);
+
+ return this;
+ };
+
+ WrappedGL.prototype.pixelStorei = function(target, texture, pname, param) {
+ this.bindTextureForEditing(target, texture);
+ this.gl.pixelStorei(pname, param);
+
+ return this;
+ };
+
+ WrappedGL.prototype.setTextureFiltering = function (target, texture, wrapS, wrapT, minFilter, magFilter) {
+ var gl = this.gl;
+
+ this.bindTextureForEditing(target, texture);
+
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);
+
+ return this;
+ };
+
+ WrappedGL.prototype.generateMipmap = function (target, texture) {
+ this.bindTextureForEditing(target, texture);
+ this.gl.generateMipmap(target);
+
+ return this;
+ };
+
+ WrappedGL.prototype.buildTexture = function (format, type, width, height, data, wrapS, wrapT, minFilter, magFilter) {
+ var texture = this.createTexture();
+ this.rebuildTexture(texture, format, type, width, height, data, wrapS, wrapT, minFilter, magFilter);
+
+ return texture;
+ };
+
+ WrappedGL.prototype.rebuildTexture = function (texture, format, type, width, height, data, wrapS, wrapT, minFilter, magFilter) {
+ this.texImage2D(this.TEXTURE_2D, texture, 0, format, width, height, 0, format, type, data)
+ .setTextureFiltering(this.TEXTURE_2D, texture, wrapS, wrapT, minFilter, magFilter);
+
+ return this;
+ };
+
+ WrappedGL.prototype.createRenderbuffer = function () {
+ return this.gl.createRenderbuffer();
+ };
+
+ WrappedGL.prototype.renderbufferStorage = function (renderbuffer, target, internalformat, width, height) {
+ this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, renderbuffer);
+ this.gl.renderbufferStorage(target, internalformat, width, height);
+
+ return this;
+ };
+
+ WrappedGL.prototype.createBuffer = function () {
+ return this.gl.createBuffer();
+ };
+
+ WrappedGL.prototype.bufferData = function (buffer, target, data, usage) {
+ var gl = this.gl;
+
+ if (target === gl.ARRAY_BUFFER) {
+ //we don't really care about the vertex buffer binding state...
+ } else if (target === gl.ELEMENT_ARRAY_BUFFER) {
+ this.changedParameters.indexBuffer = [buffer];
+ }
+
+ gl.bindBuffer(target, buffer);
+ gl.bufferData(target, data, usage);
+ };
+
+ WrappedGL.prototype.bufferSubData = function (buffer, target, offset, data) {
+ var gl = this.gl;
+
+ if (target === gl.ARRAY_BUFFER) {
+ //we don't really care about the vertex buffer binding state...
+ } else if (target === gl.ELEMENT_ARRAY_BUFFER) {
+ this.changedParameters.indexBuffer = [buffer];
+ }
+
+ gl.bindBuffer(target, buffer);
+ gl.bufferSubData(target, offset, data);
+ };
+
+ WrappedGL.prototype.createProgram = function (vertexShaderSource, fragmentShaderSource, attributeLocations) {
+ return new WrappedProgram(this, vertexShaderSource, fragmentShaderSource, attributeLocations);
+ };
+
+
+ //loads text files and calls callback with an object like this:
+ // { filename: 'content', otherFilename, 'morecontent' }
+ //TODO: error conditions...
+ function loadTextFiles (filenames, onLoaded) {
+ var loadedSoFar = 0;
+ var results = {};
+ for (var i = 0; i < filenames.length; ++i) {
+ var filename = filenames[i];
+ (function () {
+ var name = filename;
+
+ var request = new XMLHttpRequest();
+ request.onreadystatechange = function () {
+ if (request.readyState === 4) { //if this reqest is done
+ //add this file to the results object
+ var text = request.responseText;
+ results[name] = text;
+
+ loadedSoFar += 1;
+ if (loadedSoFar === filenames.length) { //if we've loaded all of the files
+ onLoaded(results);
+ }
+ }
+ }
+ request.open('GET', name, true);
+ request.send();
+
+ }());
+ }
+ };
+
+ //asynchronous
+ //successCallback is called with (program)
+ //vertex shader, fragment shader can either be strings or arrays of strings
+ //in the array case, the file contents will be concatenated
+ WrappedGL.prototype.createProgramFromFiles = function (vertexShaderPath, fragmentShaderPath, attributeLocations, successCallback, failureCallback) {
+ var that = this;
+
+ var filesToLoad = [];
+ if (Array.isArray(vertexShaderPath)) {
+ filesToLoad = filesToLoad.concat(vertexShaderPath);
+ } else {
+ filesToLoad.push(vertexShaderPath);
+ }
+
+ if (Array.isArray(fragmentShaderPath)) {
+ filesToLoad = filesToLoad.concat(fragmentShaderPath);
+ } else {
+ filesToLoad.push(fragmentShaderPath);
+ }
+
+ loadTextFiles(filesToLoad, function (files) {
+ var vertexShaderSources = [];
+ if (Array.isArray(vertexShaderPath)) {
+ for (var i = 0; i < vertexShaderPath.length; ++i) {
+ vertexShaderSources.push(files[vertexShaderPath[i]]);
+ }
+ } else {
+ vertexShaderSources.push(files[vertexShaderPath]);
+ }
+
+
+ var fragmentShaderSources = [];
+ if (Array.isArray(fragmentShaderPath)) {
+ for (var i = 0; i < fragmentShaderPath.length; ++i) {
+ fragmentShaderSources.push(files[fragmentShaderPath[i]]);
+ }
+ } else {
+ fragmentShaderSources.push(files[fragmentShaderPath]);
+ }
+
+ var program = that.createProgram(vertexShaderSources.join('\n'), fragmentShaderSources.join('\n'), attributeLocations);
+ successCallback(program);
+ });
+ };
+
+ /*
+ input:
+ {
+ firstProgram: {
+ vertexShader: 'first.vert',
+ fragmentShader: 'first.frag',
+ attributeLocations: {
+ 0: 'a_attribute'
+ }
+ },
+
+ secondProgram: {
+ vertexShader: 'second.vert',
+ fragmentShader: 'second.frag',
+ attributeLocations: {
+ 0: 'a_attribute'
+ }
+ }
+ }
+
+ output:
+ {
+ firstProgram: firstProgramObject,
+ secondProgram: secondProgramObject
+ */
+
+ function keysInObject (object) {
+ var count = 0;
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ count += 1;
+ }
+ }
+ return count;
+ }
+
+ //asynchronous
+ WrappedGL.prototype.createProgramsFromFiles = function (programParameters, successCallback, failureCallback) {
+ var programCount = keysInObject(programParameters);
+
+ var loadedSoFar = 0;
+ var programs = {};
+ for (var programName in programParameters) {
+ if (programParameters.hasOwnProperty(programName)) {
+ var parameters = programParameters[programName];
+
+ var that = this;
+ (function () {
+ var name = programName;
+
+ that.createProgramFromFiles(parameters.vertexShader, parameters.fragmentShader, parameters.attributeLocations, function (program) {
+ programs[name] = program;
+
+ loadedSoFar++;
+ if (loadedSoFar === programCount) { //if we've loaded all the programs
+ successCallback(programs);
+ }
+
+ });
+ }());
+ }
+ }
+ };
+
+ WrappedGL.prototype.createDrawState = function () {
+ return new DrawState(this);
+ };
+
+ WrappedGL.prototype.createClearState = function () {
+ return new ClearState(this);
+ };
+
+ WrappedGL.prototype.createReadState = function () {
+ return new ReadState(this);
+ };
+
+ WrappedGL.prototype.deleteBuffer = function (buffer) {
+ this.gl.deleteBuffer(buffer);
+ };
+
+ WrappedGL.prototype.deleteFramebuffer = function (buffer) {
+ this.gl.deleteFramebuffer(buffer);
+ };
+
+ WrappedGL.prototype.deleteTexture = function (texture) {
+ this.gl.deleteTexture(texture);
+ };
+
+ function buildShader (gl, type, source) {
+ var shader = gl.createShader(type);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ //log any errors
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ console.log(gl.getShaderInfoLog(shader));
+ }
+ return shader;
+ };
+
+ //we don't have to specify any or all attribute location bindings
+ //any unspecified bindings will be assigned automatically and can be queried with program.getAttribLocation(attributeName)
+ function WrappedProgram (wgl, vertexShaderSource, fragmentShaderSource, requestedAttributeLocations) {
+ this.uniformLocations = {};
+ this.uniforms = {}; //TODO: if we want to cache uniform values in the future
+
+ var gl = wgl.gl;
+
+ //build shaders from source
+ var vertexShader = buildShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
+ var fragmentShader = buildShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
+
+ //create program and attach shaders
+ var program = this.program = gl.createProgram();
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+
+ //bind the attribute locations that have been specified in attributeLocations
+ if (requestedAttributeLocations !== undefined) {
+ for (var attributeName in requestedAttributeLocations) {
+ gl.bindAttribLocation(program, requestedAttributeLocations[attributeName], attributeName);
+ }
+ }
+ gl.linkProgram(program);
+
+
+ //construct this.attributeLocations (maps attribute names to locations)
+ this.attributeLocations = {};
+ var numberOfAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
+ for (var i = 0; i < numberOfAttributes; ++i) {
+ var activeAttrib = gl.getActiveAttrib(program, i);
+ var attributeName = activeAttrib.name;
+ this.attributeLocations[attributeName] = gl.getAttribLocation(program, attributeName);
+ }
+
+ //cache uniform locations
+ var uniformLocations = this.uniformLocations = {};
+ var numberOfUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
+ for (var i = 0; i < numberOfUniforms; i += 1) {
+ var activeUniform = gl.getActiveUniform(program, i),
+ uniformLocation = gl.getUniformLocation(program, activeUniform.name);
+ uniformLocations[activeUniform.name] = uniformLocation;
+ }
+ };
+
+ //TODO: maybe this should be on WrappedGL?
+ WrappedProgram.prototype.getAttribLocation = function (name) {
+ return this.attributeLocations[name];
+ };
+
+ function State (wgl) {
+ this.wgl = wgl;
+
+ //all states that have been changed from defaults
+ this.changedParameters = {};
+ //map of state string to array of values
+ //eg
+ /*
+ 'framebuffer: [framebuffer],
+ 'viewport': [x, y, width, height],
+ 'blendMode': [rgb, alpha]
+ */
+ };
+
+ //assumes a and b are equal length
+ function arraysEqual (a, b) {
+ for (var i = 0; i < a.length; ++i) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+ };
+
+
+ State.prototype.setParameter = function (parameterName, values) {
+ if (!arraysEqual(values, this.wgl.parameters[parameterName].defaults)) { //if the state hasn't been set to the defaults
+ this.changedParameters[parameterName] = values;
+ } else { //if we're going back to defaults
+ if (this.changedParameters.hasOwnProperty(parameterName)) {
+ delete this.changedParameters[parameterName];
+ }
+ }
+ };
+
+ State.prototype.clone = function () {
+ var newState = new (this.constructor)(this.wgl);
+
+ for (var parameterName in this.changedParameters) {
+ if (this.changedParameters.hasOwnProperty(parameterName)) {
+ var parameterValues = this.changedParameters[parameterName];
+ var clonedValues = [];
+ for (var i = 0; i < parameterValues.length; ++i) {
+ clonedValues.push(parameterValues[i]);
+ }
+ newState.changedParameters[parameterName] = clonedValues;
+ }
+ }
+
+ return newState;
+ }
+
+
+ //inherits from State
+ function DrawState (wgl) {
+ State.call(this, wgl);
+
+ //we always set uniforms
+ this.uniforms = {}; //eg: {type: '3f', value: [x, y, z]}
+ }
+
+ DrawState.prototype = Object.create(State.prototype);
+ DrawState.prototype.constructor = State;
+
+
+ DrawState.prototype.bindFramebuffer = function (framebuffer) {
+ this.setParameter('framebuffer', [framebuffer]);
+ return this;
+ };
+
+ DrawState.prototype.viewport = function (x, y, width, height) {
+ this.setParameter('viewport', [x, y, width, height]);
+ return this;
+ };
+
+ DrawState.prototype.enable = function (cap) {
+ if (cap === this.wgl.DEPTH_TEST) {
+ this.setParameter('depthTest', [true]);
+ } else if (cap === this.wgl.BLEND) {
+ this.setParameter('blend', [true]);
+ } else if (cap === this.wgl.CULL_FACE) {
+ this.setParameter('cullFace', [true]);
+ } else if (cap === this.wgl.POLYGON_OFFSET_FILL) {
+ this.setParameter('polygonOffsetFill', [true]);
+ } else if (cap === this.wgl.SCISSOR_TEST) {
+ this.setParameter('scissorTest', [true]);
+ }
+
+ return this;
+ };
+
+ DrawState.prototype.disable = function (cap) {
+ if (cap === this.wgl.DEPTH_TEST) {
+ this.setParameter('depthTest', [false]);
+ } else if (cap === this.wgl.BLEND) {
+ this.setParameter('blend', [false]);
+ } else if (cap === this.wgl.CULL_FACE) {
+ this.setParameter('cullFace', [false]);
+ } else if (cap === this.wgl.POLYGON_OFFSET_FILL) {
+ this.setParameter('polygonOffsetFill', [false]);
+ } else if (cap === this.wgl.SCISSOR_TEST) {
+ this.setParameter('scissorTest', [false]);
+ }
+
+ return this;
+ };
+
+ DrawState.prototype.vertexAttribPointer = function (buffer, index, size, type, normalized, stride, offset) {
+ this.setParameter('attributeArray' + index.toString(), [buffer, size, type, normalized, stride, offset]);
+
+ if (this.instancedExt && this.changedParameters.hasOwnProperty('attributeDivisor' + index.toString())) {
+ //we need to have divisor information for any attribute location that has a bound buffer
+ this.setParameter('attributeDivisor' + index.toString(), [0]);
+ }
+
+ return this;
+ };
+
+ DrawState.prototype.bindIndexBuffer = function (buffer) {
+ this.setParameter('indexBuffer', [buffer]);
+ return this;
+ };
+
+ DrawState.prototype.depthFunc = function (func) {
+ this.setParameter('depthFunc', [func]);
+ return this;
+ };
+
+ DrawState.prototype.frontFace = function (mode) {
+ this.setParameter('frontFace', [mode]);
+ return this;
+ };
+
+ DrawState.prototype.blendEquation = function (mode) {
+ this.blendEquationSeparate(mode, mode);
+ return this;
+ };
+
+ DrawState.prototype.blendEquationSeparate = function (modeRGB, modeAlpha) {
+ this.setParameter('blendEquation', [modeRGB, modeAlpha]);
+
+ return this;
+ };
+
+ DrawState.prototype.blendFunc = function (sFactor, dFactor) {
+ this.blendFuncSeparate(sFactor, dFactor, sFactor, dFactor);
+ return this;
+ };
+
+ DrawState.prototype.blendFuncSeparate = function (srcRGB, dstRGB, srcAlpha, dstAlpha) {
+ this.setParameter('blendFunc', [srcRGB, dstRGB, srcAlpha, dstAlpha]);
+ return this;
+ };
+
+ DrawState.prototype.scissor = function (x, y, width, height) {
+ this.setParameter('scissor', [x, y, width, height]);
+ return this;
+ };
+
+ DrawState.prototype.useProgram = function (program) {
+ this.setParameter('program', [program]);
+ return this;
+ };
+
+ DrawState.prototype.bindTexture = function (unit, target, texture) {
+ this.setParameter('texture' + unit.toString(), [target, texture]);
+ return this;
+ };
+
+ DrawState.prototype.colorMask = function (r, g, b, a) {
+ this.setParameter('colorMask', [r, g, b, a]);
+ return this;
+ };
+
+ DrawState.prototype.depthMask = function (enabled) {
+ this.setParameter('depthMask', [enabled]);
+ return this;
+ };
+
+ DrawState.prototype.polygonOffset = function (factor, units) {
+ this.setParameter('polygonOffset', [factor, units]);
+ return this;
+ };
+
+ DrawState.prototype.uniformTexture = function (uniformName, unit, target, texture) {
+ this.uniform1i(uniformName, unit);
+ this.bindTexture(unit, target, texture);
+
+ return this;
+ };
+
+ DrawState.prototype.uniform1i = function (uniformName, value) {
+ this.uniforms[uniformName] = {type: '1i', value: [value]};
+ return this;
+ };
+
+ DrawState.prototype.uniform2i = function (uniformName, x, y) {
+ this.uniforms[uniformName] = {type: '2i', value: [x, y]};
+ return this;
+ };
+
+ DrawState.prototype.uniform3i = function (uniformName, x, y, z) {
+ this.uniforms[uniformName] = {type: '3i', value: [x, y, z]};
+ return this;
+ };
+
+ DrawState.prototype.uniform4i = function (uniformName, x, y, z ,w) {
+ this.uniforms[uniformName] = {type: '4i', value: [x, y, z, w]};
+ return this;
+ };
+
+ DrawState.prototype.uniform1f = function (uniformName, value) {
+ this.uniforms[uniformName] = {type: '1f', value: value};
+ return this;
+ };
+
+ DrawState.prototype.uniform2f = function (uniformName, x, y) {
+ this.uniforms[uniformName] = {type: '2f', value: [x, y]};
+ return this;
+ };
+
+ DrawState.prototype.uniform3f = function (uniformName, x, y, z) {
+ this.uniforms[uniformName] = {type: '3f', value: [x, y, z]};
+ return this;
+ };
+
+ DrawState.prototype.uniform4f = function (uniformName, x, y, z ,w) {
+ this.uniforms[uniformName] = {type: '4f', value: [x, y, z, w]};
+ return this;
+ };
+
+ DrawState.prototype.uniform1fv = function (uniformName, value) {
+ this.uniforms[uniformName] = {type: '1fv', value: [value]};
+ return this;
+ };
+
+ DrawState.prototype.uniform2fv = function (uniformName, value) {
+ this.uniforms[uniformName] = {type: '2fv', value: [value]};
+ return this;
+ };
+
+ DrawState.prototype.uniform3fv = function (uniformName, value) {
+ this.uniforms[uniformName] = {type: '3fv', value: [value]};
+ return this;
+ };
+
+ DrawState.prototype.uniform4fv = function (uniformName, value) {
+ this.uniforms[uniformName] = {type: '4fv', value: [value]};
+ return this;
+ };
+
+ DrawState.prototype.uniformMatrix2fv = function (uniformName, transpose, matrix) {
+ this.uniforms[uniformName] = {type: 'matrix2fv', value: [transpose, matrix]};
+ return this;
+ };
+
+ DrawState.prototype.uniformMatrix3fv = function (uniformName, transpose, matrix) {
+ this.uniforms[uniformName] = {type: 'matrix3fv', value: [transpose, matrix]};
+ return this;
+ };
+
+ DrawState.prototype.uniformMatrix4fv = function (uniformName, transpose, matrix) {
+ this.uniforms[uniformName] = {type: 'matrix4fv', value: [transpose, matrix]};
+ return this;
+ };
+
+
+ function ClearState (wgl) {
+ State.call(this, wgl);
+ };
+
+ ClearState.prototype = Object.create(State.prototype);
+ ClearState.prototype.constructor = ClearState;
+
+ ClearState.prototype.bindFramebuffer = function (framebuffer) {
+ this.setParameter('framebuffer', [framebuffer]);
+ return this;
+ };
+
+ ClearState.prototype.clearColor = function (r, g, b, a) {
+ this.setParameter('clearColor', [r, g, b, a]);
+ return this;
+ };
+
+ ClearState.prototype.clearDepth = function (depth) {
+ this.setParameter('clearDepth', [depth]);
+ return this;
+ }
+
+ ClearState.prototype.colorMask = function (r, g, b, a) {
+ this.setParameter('colorMask', [r, g, b, a]);
+ return this;
+ };
+
+ ClearState.prototype.depthMask = function (enabled) {
+ this.setParameter('depthMask', [enabled]);
+ return this;
+ };
+
+
+ function ReadState (wgl) {
+ State.call(this, wgl);
+ }
+
+ ReadState.prototype = Object.create(State.prototype);
+ ReadState.prototype.constructor = ReadState;
+
+ ReadState.prototype.bindFramebuffer = function (framebuffer) {
+ this.setParameter('framebuffer', [framebuffer]);
+ return this;
+ };
+
+
+
+ return WrappedGL;
+
+}());