From 3213be38cc79ed4f28cd55c8a3e1a48ff50bc5d6 Mon Sep 17 00:00:00 2001 From: David Li Date: Sun, 12 Jun 2016 21:53:14 +0100 Subject: [PATCH] Initial commit --- LICENSE | 21 + README | 7 + boxeditor.js | 1095 +++++++++++++++++++++++ camera.js | 138 +++ flip.css | 189 ++++ fluidparticles.js | 377 ++++++++ index.html | 122 +++ renderer.js | 474 ++++++++++ shaders/addforce.frag | 42 + shaders/advect.frag | 59 ++ shaders/background.frag | 8 + shaders/background.vert | 10 + shaders/box.frag | 17 + shaders/box.vert | 17 + shaders/boxwireframe.frag | 5 + shaders/boxwireframe.vert | 13 + shaders/common.frag | 64 ++ shaders/composite.frag | 77 ++ shaders/copy.frag | 9 + shaders/divergence.frag | 36 + shaders/enforceboundaries.frag | 40 + shaders/extendvelocity.frag | 46 + shaders/fullscreen.vert | 11 + shaders/fxaa.frag | 48 + shaders/grid.frag | 5 + shaders/grid.vert | 12 + shaders/jacobi.frag | 34 + shaders/mark.frag | 5 + shaders/mark.vert | 24 + shaders/normalizegrid.frag | 30 + shaders/particle.frag | 9 + shaders/particle.vert | 23 + shaders/point.frag | 5 + shaders/point.vert | 14 + shaders/sphere.frag | 9 + shaders/sphere.vert | 32 + shaders/sphereao.frag | 53 ++ shaders/sphereao.vert | 29 + shaders/spheredepth.frag | 5 + shaders/spheredepth.vert | 21 + shaders/subtract.frag | 32 + shaders/transfertogrid.frag | 56 ++ shaders/transfertogrid.vert | 37 + shaders/transfertoparticles.frag | 50 ++ simulator.js | 591 ++++++++++++ simulatorrenderer.js | 115 +++ slider.js | 59 ++ utilities.js | 304 +++++++ wrappedgl.js | 1435 ++++++++++++++++++++++++++++++ 49 files changed, 5914 insertions(+) create mode 100644 LICENSE create mode 100644 README create mode 100644 boxeditor.js create mode 100644 camera.js create mode 100644 flip.css create mode 100644 fluidparticles.js create mode 100644 index.html create mode 100644 renderer.js create mode 100644 shaders/addforce.frag create mode 100644 shaders/advect.frag create mode 100644 shaders/background.frag create mode 100644 shaders/background.vert create mode 100644 shaders/box.frag create mode 100644 shaders/box.vert create mode 100644 shaders/boxwireframe.frag create mode 100644 shaders/boxwireframe.vert create mode 100644 shaders/common.frag create mode 100644 shaders/composite.frag create mode 100644 shaders/copy.frag create mode 100644 shaders/divergence.frag create mode 100644 shaders/enforceboundaries.frag create mode 100644 shaders/extendvelocity.frag create mode 100644 shaders/fullscreen.vert create mode 100644 shaders/fxaa.frag create mode 100644 shaders/grid.frag create mode 100644 shaders/grid.vert create mode 100644 shaders/jacobi.frag create mode 100644 shaders/mark.frag create mode 100644 shaders/mark.vert create mode 100644 shaders/normalizegrid.frag create mode 100644 shaders/particle.frag create mode 100644 shaders/particle.vert create mode 100644 shaders/point.frag create mode 100644 shaders/point.vert create mode 100644 shaders/sphere.frag create mode 100644 shaders/sphere.vert create mode 100644 shaders/sphereao.frag create mode 100644 shaders/sphereao.vert create mode 100644 shaders/spheredepth.frag create mode 100644 shaders/spheredepth.vert create mode 100644 shaders/subtract.frag create mode 100644 shaders/transfertogrid.frag create mode 100644 shaders/transfertogrid.vert create mode 100644 shaders/transfertoparticles.frag create mode 100644 simulator.js create mode 100644 simulatorrenderer.js create mode 100644 slider.js create mode 100644 utilities.js create mode 100644 wrappedgl.js 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; + +}());