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; + +}());