From 70b38cc8efd86aa8890f20a1dbbe639ab157c147 Mon Sep 17 00:00:00 2001 From: Jelle Hak Date: Fri, 11 Sep 2020 10:00:25 +0200 Subject: [PATCH] converted to es6 --- .eslintrc.json | 5 + app.js | 35 + boxeditor.js | 1774 ++++++++++++++--------------- camera.js | 198 ++-- fluid.feature.js | 58 + fluidparticles.js | 621 +++++----- index.html | 45 +- package.json | 20 + renderer.js | 760 ++++++------- simulator.js | 922 +++++++-------- simulatorrenderer.js | 191 ++-- slider.js | 84 +- utilities.js | 610 +++++----- wrappedgl.js | 2570 +++++++++++++++++++++--------------------- 14 files changed, 3871 insertions(+), 4022 deletions(-) create mode 100644 .eslintrc.json create mode 100644 app.js create mode 100644 fluid.feature.js create mode 100644 package.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bcdde66 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "standard" + ] +} \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..a16b34f --- /dev/null +++ b/app.js @@ -0,0 +1,35 @@ +// import Utilities from "./utilities.js" +import WrappedGL from './wrappedgl.js' +import FluidParticles from './fluidparticles.js' + +function concatenateWords (list) { + if (list.length === 0) { + return '' + } else if (list.length === 1) { + return "'" + list[0] + "'" + } else { + var result = '' + for (var i = 0; i < list.length; ++i) { + result += "'" + list[i] + "'" + if (i < list.length - 1) { + result += i < list.length - 2 ? ', ' : ' and ' + } + } + + return result + } +} + +WrappedGL.checkWebGLSupportWithExtensions(['ANGLE_instanced_arrays', 'WEBGL_depth_texture', 'OES_texture_float', 'OES_texture_float_linear', 'OES_texture_half_float', 'OES_texture_half_float_linear'], + function () { // we have webgl + document.getElementById('placeholder').outerHTML = document.getElementById('main').innerHTML + var fluidBox = new FluidParticles() + }, function (hasWebGL, unsupportedExtensions) { + document.getElementById('placeholder').outerHTML = document.getElementById('no-support').innerHTML + if (!hasWebGL) { // webgl not supported + document.getElementById('error').textContent = 'Unfortunately, your browser does not support WebGL' + } else { + document.getElementById('error').textContent = 'Unfortunately, your browser does not support the ' + concatenateWords(unsupportedExtensions) + ' WebGL extension' + (unsupportedExtensions.length > 1 ? 's.' : '.') + } + } +) diff --git a/boxeditor.js b/boxeditor.js index 98a41be..9fd9f6f 100644 --- a/boxeditor.js +++ b/boxeditor.js @@ -1,68 +1,74 @@ -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); +import Utilities from './utilities.js' + +// return { +// BoxEditor: BoxEditor, +// AABB: AABB, +// InteractionMode: InteractionMode +// } + +// min and max are both number[3] +export class AABB { + constructor (min, max) { + this.min = [min[0], min[1], min[2]] + this.max = [max[0], max[1], max[2]] + } + + computeVolume () { + var volume = 1 + for (var i = 0; i < 3; ++i) { + volume *= (this.max[i] - this.min[i]) } - - //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; + return volume + } + + computeSurfaceArea () { + 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) + clone () { + return new AABB( + [this.min[0], this.min[1], this.min[2]], + [this.max[0], this.max[1], this.max[2]] + ) + } + + randomPoint () { + 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, +export const 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 - }; + 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; - +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]; - } +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, @@ -75,1021 +81,937 @@ var BoxEditor = (function () { 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; +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 - //if we've reached this far then there is an intersection + // the part of the line we have clipped so far + var lowT = -Infinity + var highT = Infinity - var intersection = []; - for (var i = 0; i < 3; ++i) { - intersection[i] = rayOrigin[i] + rayDirection[i] * lowT; - } + 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 - return { - aabb: aabb, - t: lowT, - axis: intersectionAxis, - side: rayDirection[intersectionAxis] > 0 ? -1 : 1, - point: intersection - }; + // ensure t1 < t2 (swap if necessary) + if (t1 > t2) { + var temp = t1 + t1 = t2 + t2 = temp } - //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); + // 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 - var t1 = (b * e - c * d) / (a * c - b * b); - var t2 = (a * e - b * d) / (a * c - b * b); + // further clip the line between the planes in this axis + if (t1 > lowT) { + lowT = t1 - return [ - Utilities.addVectors([], line1Origin, Utilities.multiplyVectorByScalar([], line1Direction, t1)), - Utilities.addVectors([], line2Origin, Utilities.multiplyVectorByScalar([], line2Direction, t2)) - ]; + intersectionAxis = i // if we needed to futher clip in this axis then this is the closest intersection axis } - //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)); + 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 +export class BoxEditor { + constructor (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, + 4, 5, 6, 4, 6, 7, + 8, 9, 10, 8, 10, 11, + 12, 13, 14, 12, 14, 15, + 16, 17, 18, 16, 18, 19, + 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) } - function quantize (x, step) { - return Math.round(x / step) * step; - } + 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) - function quantizeVector (v, step) { - for (var i = 0; i < v.length; ++i) { - v[i] = quantize(v[i], step); - } + 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) - return v; - } + /// ////////////////////////////////////////////// + // box state + this.boxes = [] - BoxEditor.prototype.onKeyDown = function (event) { - this.keyPressed[event.keyCode] = true; - } + /// ///////////////////////////////////////////// + // interaction stuff + // mouse x and y are in [-1, 1] (clip space) + this.mouseX = 999 + this.mouseY = 999 - BoxEditor.prototype.onKeyUp = function (event) { - this.keyPressed[event.keyCode] = false; + 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 } - 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; - } + /* + 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 - - 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; - } - } - } - } + click and drag on side of boxes to resize - } else if (this.interactionState.mode === InteractionMode.DRAWING) { - - var mouseRay = this.getMouseRay(); + click and drag on side of boxes whilst holding shift to move - //get the mouse ray intersection with the drawing plane + //while we're not interacting, this is null + //while we are interacting this contains an object + /* - var axis = this.interactionState.axis, - side = this.interactionState.side, - startPoint = this.interactionState.point; + { + mode: the interaction mode, - var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; - var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis]; + 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 - if (t > 0) { //if the mouse ray misses the drawing plane then the box just stays the same size as it was before + during translation we also have: + startMax: the starting max along the interaction axis + startMin: the starting min along the interaction axis - var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)); - quantizeVector(intersection, STEP); + 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 - 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]); } + */ + 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)) + } + + onKeyDown (event) { + this.keyPressed[event.keyCode] = true + } + + onKeyUp (event) { + this.keyPressed[event.keyCode] = false + } + + onMouseMove (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 + var side = this.interactionState.side + var 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]) + } - 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(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 + var side = this.interactionState.side + var 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 + } - //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]; - } - } - } - } + if (box.max[axis] > this.gridDimensions[axis]) { + box.max[axis] = this.gridDimensions[axis] + box.min[axis] = this.gridDimensions[axis] - length + } - this.interactionState.box.min = min; - this.interactionState.box.max = max; + 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 } - this.camera.onMouseMove(event); - } + var sweptBox = box.clone() // we sweep out translating AABB for collision detection to prevent ghosting through boxes - //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 + // reset swept box to original box location before translation + sweptBox.min[axis] = this.interactionState.startMin + sweptBox.max[axis] = this.interactionState.startMax - var bestIntersectionSoFar = { - aabb: null, - t: Infinity + // 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 box = this.boxes[i]; - - var intersection = rayAABBIntersection(rayOrigin, rayDirection, box); + 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 + var side = this.interactionState.side + var 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 !== 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 (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] } + } } - } + } - if (bestIntersectionSoFar.aabb === null) { //if we didn't intersect any boxes - return null; - } else { - return bestIntersectionSoFar; + this.interactionState.box.min = min + this.interactionState.box.max = max } + } } - //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) { + this.camera.onMouseMove(event) + } - //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 + // returns the closest box intersection data (same as rayAABBIntersection) for the given ray + // if there is no intersection it returns null + getBoxIntersection (rayOrigin, rayDirection) { + // find the closest box that this collides with + var bestIntersectionSoFar = { + aabb: null, + t: Infinity + } - var t = (planeCoordinate - rayOrigin[axis]) / rayDirection[axis]; + for (var i = 0; i < this.boxes.length; ++i) { + var box = this.boxes[i] + var intersection = rayAABBIntersection(rayOrigin, rayDirection, box) - if (t > 0) { - var intersection = Utilities.addVectors([], rayOrigin, Utilities.multiplyVectorByScalar([], rayDirection, t)); + 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 we're still within the bounds of the grid - if (intersection[0] >= 0.0 && intersection[0] <= this.gridDimensions[0] && + 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 + */ + getBoundingPlaneIntersection (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 { + axis: axis, + side: side, + point: intersection + } } + } } - - return null; //no intersection found + } } + 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; + onMouseDown (event) { + event.preventDefault() - this.onChange(); + this.onMouseMove(event) - 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[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 - 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, + this.onChange() - startMax: intersection.aabb.max[intersection.axis], - startMin: intersection.aabb.min[intersection.axis] - }; - } else { //otherwise we start resizing + return + } else { + var mouseRay = this.getMouseRay() - this.interactionState = { - mode: InteractionMode.RESIZING, - box: intersection.aabb, - axis: intersection.axis, - side: intersection.side, - point: intersection.point - }; - } - } + // 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 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(); + 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, - var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction); + 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 (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); + // 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 newBox = new AABB(point, point); - this.boxes.push(newBox); + var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction) - this.interactionState = { - mode: InteractionMode.DRAWING, - box: newBox, - axis: planeIntersection.axis, - side: planeIntersection.side, - point: planeIntersection.point - }; - } + 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) - this.onChange(); - } + 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); - } + } + } + if (this.interactionState === null) { + this.camera.onMouseDown(event) } + } - BoxEditor.prototype.onMouseUp = function (event) { - event.preventDefault(); + onMouseUp (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); - } + 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; + 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 + var side = this.interactionState.side + var 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 + } + } - } 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 + this.onChange() + } - if (this.interactionState.box.computeSurfaceArea() > 0) { //make sure we have something to extrude + if (this.interactionState === null) { + this.camera.onMouseUp(event) + } + } + + // returns an object + /* + { + origin: [x, y, z], + direction: [x, y, z] //normalized + } + */ + getMouseRay () { + 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() - var mouseRay = this.getMouseRay(); + return { + origin: rayOrigin, + direction: mouseRay + } + } - var axis = this.interactionState.axis, - side = this.interactionState.side, - startPoint = this.interactionState.point; + draw () { + var wgl = this.wgl - var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; - var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis]; + wgl.clear( + wgl.createClearState().bindFramebuffer(null).clearColor(0.9, 0.9, 0.9, 1.0), + wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT) - 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]); - } + /// ////////////////////////////////////////// + // draw background + var backgroundDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) + .useProgram(this.backgroundProgram) - //go into extrusion mode - this.interactionState = { - mode: InteractionMode.EXTRUDING, - box: this.interactionState.box, - axis: this.interactionState.axis, - side: this.interactionState.side * -1, - point: intersection - }; + .vertexAttribPointer(this.quadVertexBuffer, this.backgroundProgram.getAttribLocation('a_position'), 2, wgl.FLOAT, wgl.FALSE, 0, 0) - } 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; - } - } + wgl.drawArrays(backgroundDrawState, wgl.TRIANGLE_STRIP, 0, 4) - this.onChange(); - } + /// ////////////////////////////////////////// + // 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] - if (this.interactionState === null) { - this.camera.onMouseUp(event); - } - } + var cameraDirection = Utilities.subtractVectors([], planePosition, cameraPosition) + var gridDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) - //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]); + .useProgram(this.gridProgram) - var viewSpaceMouseRay = [ - this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height), - this.mouseY * Math.tan(fov / 2.0), - -1.0]; + .vertexAttribPointer(this.gridVertexBuffers[axis], this.gridProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()); - var mouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix); - Utilities.normalizeVector(mouseRay, mouseRay); + .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) + var translation = [0, 0, 0] + translation[axis] = side * this.gridDimensions[axis] - var rayOrigin = this.camera.getPosition(); + gridDrawState.uniform3f('u_translation', translation[0], translation[1], translation[2]) - return { - origin: rayOrigin, - direction: mouseRay - }; + if (side === 0 && cameraDirection[axis] <= 0 || side === 1 && cameraDirection[axis] >= 0) { + wgl.drawArrays(gridDrawState, wgl.LINES, 0, 8) + } + } } - 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 boxes and point + var boxDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) - ///////////////////////////////////////////// - //draw background + .enable(wgl.DEPTH_TEST) + .enable(wgl.CULL_FACE) - 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); + .useProgram(this.boxProgram) - wgl.drawArrays(backgroundDrawState, wgl.TRIANGLE_STRIP, 0, 4); + .vertexAttribPointer(this.cubeVertexBuffer, this.boxProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) + .bindIndexBuffer(this.cubeIndexBuffer) - ///////////////////////////////////////////// - //draw grid + .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) - for (var axis = 0; axis < 3; ++axis) { - for (var side = 0; side <= 1; ++side) { - var cameraPosition = this.camera.getPosition(); + .enable(wgl.POLYGON_OFFSET_FILL) + .polygonOffset(1, 1) - 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 boxToHighlight = null + var sideToHighlight = null + var highlightColor = null - var gridDrawState = wgl.createDrawState() - .bindFramebuffer(null) - .viewport(0, 0, this.canvas.width, this.canvas.height) + 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 - .useProgram(this.gridProgram) + 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() - .vertexAttribPointer(this.gridVertexBuffers[axis], this.gridProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) + var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction) - .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) - .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()); + // 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 - var translation = [0, 0, 0]; - translation[axis] = side * this.gridDimensions[axis]; + highlightColor = [0.9, 0.9, 0.9] + } - 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); - } - } - } + // 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) - /////////////////////////////////////////////// - //draw boxes and point + 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 boxDrawState = wgl.createDrawState() + var pointDrawState = wgl.createDrawState() .bindFramebuffer(null) .viewport(0, 0, this.canvas.width, this.canvas.height) .enable(wgl.DEPTH_TEST) - .enable(wgl.CULL_FACE) - .useProgram(this.boxProgram) + .useProgram(this.pointProgram) - .vertexAttribPointer(this.cubeVertexBuffer, this.boxProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - - .bindIndexBuffer(this.cubeIndexBuffer) + .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()) - .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]; - } - + .uniform3f('u_position', pointPosition[0], pointPosition[1], pointPosition[2]) - //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); + .uniformMatrix3fv('u_rotation', false, rotation) - 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); - } - } + 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]); + for (var i = 0; i < this.boxes.length; ++i) { + var box = this.boxes[i] - 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); - } + 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]) - wgl.drawElements(boxDrawState, wgl.TRIANGLES, 36, wgl.UNSIGNED_SHORT); - } + 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) - var boxWireframeDrawState = wgl.createDrawState() - .bindFramebuffer(null) - .viewport(0, 0, this.canvas.width, this.canvas.height) + .enable(wgl.DEPTH_TEST) - .enable(wgl.DEPTH_TEST) + .useProgram(this.boxWireframeProgram) - .useProgram(this.boxWireframeProgram) + .vertexAttribPointer(this.cubeWireframeVertexBuffer, this.boxWireframeProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - .vertexAttribPointer(this.cubeWireframeVertexBuffer, this.boxWireframeProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) + .bindIndexBuffer(this.cubeWireframeIndexBuffer) - .bindIndexBuffer(this.cubeWireframeIndexBuffer) + .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) - .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] - - 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]); + 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); - } + wgl.drawElements(boxWireframeDrawState, wgl.LINES, 24, wgl.UNSIGNED_SHORT) + } + } +} +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 { - BoxEditor: BoxEditor, - AABB: AABB, - InteractionMode: InteractionMode - }; -}()); + return v +} diff --git a/camera.js b/camera.js index 50168c2..37debbf 100644 --- a/camera.js +++ b/camera.js @@ -1,138 +1,134 @@ -'use strict' +import Utilities from './utilities.js' -var Camera = (function () { - var SENSITIVITY = 0.005; +var SENSITIVITY = 0.005 - var MIN_DISTANCE = 25.0; - var MAX_DISTANCE = 60.0; +var MIN_DISTANCE = 25.0 +var MAX_DISTANCE = 60.0 - function Camera (element, orbitPoint) { - this.element = element; - this.distance = 40.0; - this.orbitPoint = orbitPoint; +export default class Camera { + constructor (element, orbitPoint) { + this.element = element + this.distance = 40.0 + this.orbitPoint = orbitPoint - this.azimuth = 0.0, - this.elevation = 0.25 + this.azimuth = 0.0 + this.elevation = 0.25 - this.minElevation = -Math.PI / 4; - this.maxElevation = Math.PI / 4; + this.minElevation = -Math.PI / 4 + this.maxElevation = Math.PI / 4 - this.currentMouseX = 0, - this.currentMouseY = 0; + this.currentMouseX = 0 + this.currentMouseY = 0 - this.lastMouseX = 0, - this.lastMouseY = 0; + this.lastMouseX = 0 + this.lastMouseY = 0 - this.mouseDown = false; + this.mouseDown = false - this.viewMatrix = new Float32Array(16); + this.viewMatrix = new Float32Array(16) + this.recomputeViewMatrix() - 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 } - element.addEventListener('wheel', (function (event) { - var scrollDelta = event.deltaY; - this.distance += ((scrollDelta > 0) ? 1 : -1) * 2.0; + this.recomputeViewMatrix() + }.bind(this)) + } - if (this.distance < MIN_DISTANCE) this.distance = MIN_DISTANCE; - if (this.distance > MAX_DISTANCE) this.distance = MAX_DISTANCE; + recomputeViewMatrix () { + var xRotationMatrix = new Float32Array(16) + var yRotationMatrix = new Float32Array(16) + var distanceTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)) + var orbitTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)) - this.recomputeViewMatrix(); - }).bind(this)); - }; + Utilities.makeIdentityMatrix(this.viewMatrix) - 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.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.makeIdentityMatrix(this.viewMatrix); + 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) + } - 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]; + getPosition () { + 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] + ] - 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); - }; + return position + } - 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] - ]; + isMouseDown () { + return this.mouseDown + } - return position; - }; + getViewMatrix () { + return this.viewMatrix + } - Camera.prototype.isMouseDown = function () { - return this.mouseDown; - }; + setBounds (minElevation, maxElevation) { + this.minElevation = minElevation + this.maxElevation = maxElevation - Camera.prototype.getViewMatrix = function () { - return this.viewMatrix; - }; + if (this.elevation > this.maxElevation) { this.elevation = this.maxElevation } + if (this.elevation < this.minElevation) { this.elevation = this.minElevation } - Camera.prototype.setBounds = function (minElevation, maxElevation) { - this.minElevation = minElevation; - this.maxElevation = maxElevation; + this.recomputeViewMatrix() + } - if (this.elevation > this.maxElevation) this.elevation = this.maxElevation; - if (this.elevation < this.minElevation) this.elevation = this.minElevation; + onMouseDown (event) { + event.preventDefault() - this.recomputeViewMatrix(); - }; + var x = Utilities.getMousePosition(event, this.element).x + var y = Utilities.getMousePosition(event, this.element).y - Camera.prototype.onMouseDown = function (event) { - event.preventDefault(); + this.mouseDown = true + this.lastMouseX = x + this.lastMouseY = y + } - var x = Utilities.getMousePosition(event, this.element).x; - var y = Utilities.getMousePosition(event, this.element).y; + onMouseUp (event) { + event.preventDefault() - this.mouseDown = true; - this.lastMouseX = x; - this.lastMouseY = y; - }; + this.mouseDown = false + } - Camera.prototype.onMouseUp = function (event) { - event.preventDefault(); + onMouseMove (event) { + event.preventDefault() - this.mouseDown = false; - }; + var x = Utilities.getMousePosition(event, this.element).x + var y = Utilities.getMousePosition(event, this.element).y - Camera.prototype.onMouseMove = function (event) { - event.preventDefault(); + if (this.mouseDown) { + this.currentMouseX = x + this.currentMouseY = y - var x = Utilities.getMousePosition(event, this.element).x; - var y = Utilities.getMousePosition(event, this.element).y; + var deltaAzimuth = (this.currentMouseX - this.lastMouseX) * SENSITIVITY + var deltaElevation = (this.currentMouseY - this.lastMouseY) * SENSITIVITY - if (this.mouseDown) { - this.currentMouseX = x; - this.currentMouseY = y; + this.azimuth += deltaAzimuth + this.elevation += deltaElevation - var deltaAzimuth = (this.currentMouseX - this.lastMouseX) * SENSITIVITY; - var deltaElevation = (this.currentMouseY - this.lastMouseY) * SENSITIVITY; + if (this.elevation > this.maxElevation) { this.elevation = this.maxElevation } + if (this.elevation < this.minElevation) { this.elevation = this.minElevation } - this.azimuth += deltaAzimuth; - this.elevation += deltaElevation; + this.recomputeViewMatrix() - 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; -}()); + this.lastMouseX = this.currentMouseX + this.lastMouseY = this.currentMouseY + } + } +} diff --git a/fluid.feature.js b/fluid.feature.js new file mode 100644 index 0000000..1bc7073 --- /dev/null +++ b/fluid.feature.js @@ -0,0 +1,58 @@ +import { Water } from 'three/examples/jsm/objects/Water.js' + +// +// +// +// +// +// +// +// +// + +function concatenateWords (list) { + if (list.length === 0) { + return '' + } else if (list.length === 1) { + return "'" + list[0] + "'" + } else { + var result = '' + for (var i = 0; i < list.length; ++i) { + result += "'" + list[i] + "'" + if (i < list.length - 1) { + result += i < list.length - 2 ? ', ' : ' and ' + } + } + + return result + } +} + +WrappedGL.checkWebGLSupportWithExtensions(['ANGLE_instanced_arrays', 'WEBGL_depth_texture', 'OES_texture_float', 'OES_texture_float_linear', 'OES_texture_half_float', 'OES_texture_half_float_linear'], + function () { // we have webgl + document.getElementById('placeholder').outerHTML = document.getElementById('main').innerHTML + var fluidBox = new FluidParticles() + }, function (hasWebGL, unsupportedExtensions) { + document.getElementById('placeholder').outerHTML = document.getElementById('no-support').innerHTML + if (!hasWebGL) { // webgl not supported + document.getElementById('error').textContent = 'Unfortunately, your browser does not support WebGL' + } else { + document.getElementById('error').textContent = 'Unfortunately, your browser does not support the ' + concatenateWords(unsupportedExtensions) + ' WebGL extension' + (unsupportedExtensions.length > 1 ? 's.' : '.') + } + } +) + +export default { + props: { + oceanSide: { type: 'number', default: 2000 }, + size: { type: 'number', default: 1.0 }, + distortionScale: { type: 'number', default: 3.7 }, + alpha: { type: 'number', default: 1.0 }, + sunColor: { type: 'color', default: 0xffffff }, + waterColor: { type: 'color', default: 0x001e0f } + }, + + render ({ scene, THREE }) { + + } +} diff --git a/fluidparticles.js b/fluidparticles.js index 52c338a..9e655cb 100644 --- a/fluidparticles.js +++ b/fluidparticles.js @@ -1,377 +1,364 @@ -'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 +import Utilities from './utilities.js' +import Camera from './camera.js' +import { BoxEditor, AABB, InteractionMode } from './boxeditor.js' +import WrappedGL from './wrappedgl.js' +import SimulatorRenderer from './simulatorrenderer.js' +import Slider from './slider.js' + +var FOV = Math.PI / 3 + +var State = { + EDITING: 0, + SIMULATING: 1 +} + +var GRID_WIDTH = 40 +var GRID_HEIGHT = 20 +var GRID_DEPTH = 20 + +var PARTICLES_PER_CELL = 10 + +export default class FluidParticles { + constructor () { + 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 + var simulatorRendererLoaded = false + + this.boxEditor = new 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 AABB([0, 0, 0], [15, 20, 20]) + ], + + // block drop + [ + new AABB([0, 0, 0], [40, 7, 20]), + new AABB([12, 12, 5], [28, 20, 15]) + ], + + // double splash + [ + new AABB([0, 0, 0], [10, 20, 15]), + new 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()) + } - //using gridCellDensity ensures a linear relationship to particle count - this.gridCellDensity = 0.5; //simulation grid cell density per world space unit volume + this.currentPresetIndex = (this.currentPresetIndex + 1) % PRESETS.length - this.timeStep = 1.0 / 60.0; + this.redrawUI() + }.bind(this)) - this.densitySlider = new Slider(document.getElementById('density-slider'), this.gridCellDensity, 0.2, 3.0, (function (value) { - this.gridCellDensity = value; + /// ///////////////////////////////////////////////////// + // 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.redrawUI(); - }).bind(this)); + this.timeStep = 1.0 / 60.0 - 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.densitySlider = new Slider(document.getElementById('density-slider'), this.gridCellDensity, 0.2, 3.0, function (value) { + this.gridCellDensity = value - 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() + }.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.redrawUI(); + 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(); + this.presetButton.click() - /////////////////////////////////////////////////////// - // interaction state stuff + /// //////////////////////////////////////////////////// + // interaction state stuff + canvas.addEventListener('mousemove', this.onMouseMove.bind(this)) + canvas.addEventListener('mousedown', this.onMouseDown.bind(this)) + document.addEventListener('mouseup', this.onMouseUp.bind(this)) - 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)) - document.addEventListener('keydown', this.onKeyDown.bind(this)); - document.addEventListener('keyup', this.onKeyUp.bind(this)); + window.addEventListener('resize', this.onResize.bind(this)) + this.onResize() - 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) - //////////////////////////////////////////////////// - // start the update loop + requestAnimationFrame(update) + }.bind(this) + update() + } + } - var lastTime = 0; - var update = (function (currentTime) { - var deltaTime = currentTime - lastTime || 0; - lastTime = currentTime; + onResize (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.update(deltaTime); + this.simulatorRenderer.onResize(event) + } - requestAnimationFrame(update); - }).bind(this); - update(); + onMouseMove (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.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); + onMouseDown (event) { + event.preventDefault() - this.simulatorRenderer.onResize(event); + if (this.state === State.EDITING) { + this.boxEditor.onMouseDown(event) + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.onMouseDown(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'); + onMouseUp (event) { + event.preventDefault() + if (this.state === State.EDITING) { + this.boxEditor.onMouseUp(event) + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.onMouseUp(event) + } + } - if (this.state === State.SIMULATING) { - for (var i = 0; i < simulatingElements.length; ++i) { - simulatingElements[i].style.display = 'block'; - } + onKeyDown (event) { + if (this.state === State.EDITING) { + this.boxEditor.onKeyDown(event) + } + } - for (var i = 0; i < editingElements.length; ++i) { - editingElements[i].style.display = 'none'; - } + onKeyUp (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 + redrawUI () { + 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 !== InteractionMode.EXTRUDING && + this.boxEditor.interactionState.mode !== 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() + } - 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'; - } + // compute the number of particles for the current boxes and grid density + getParticleCount () { + var boxEditor = this.boxEditor - for (var i = 0; i < editingElements.length; ++i) { - editingElements[i].style.display = 'block'; - } + var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity - document.getElementById('particle-count').innerHTML = this.getParticleCount().toFixed(0) + ' particles'; + // 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 - 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'; - } + var totalGridCells = gridResolutionX * gridResolutionY * gridResolutionZ - this.startButton.textContent = 'Start'; + 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) - if (this.editedSinceLastPreset) { - this.presetButton.innerHTML = 'Use Preset'; - } else { - this.presetButton.innerHTML = 'Next Preset'; - } - } + for (var i = 0; i < boxEditor.boxes.length; ++i) { + var box = boxEditor.boxes[i] + var volume = box.computeVolume() - this.flipnessSlider.redraw(); - this.densitySlider.redraw(); - this.speedSlider.redraw(); + totalVolume += volume + cumulativeVolume[i] = totalVolume } + var fractionFilled = totalVolume / (GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH) - //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 desiredParticleCount = fractionFilled * totalGridCells * PARTICLES_PER_CELL // theoretical number of particles + return desiredParticleCount + } - 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) + // begin simulation using boxes from box editor + // EDITING -> SIMULATING + startSimulation () { + this.state = State.SIMULATING - for (var i = 0; i < boxEditor.boxes.length; ++i) { - var box = boxEditor.boxes[i]; - var volume = box.computeVolume(); + 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 - totalVolume += volume; - cumulativeVolume[i] = totalVolume; - } - - var fractionFilled = totalVolume / (GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH); + var particleCount = particlesWidth * particlesHeight + var particlePositions = [] - var desiredParticleCount = fractionFilled * totalGridCells * PARTICLES_PER_CELL; //theoretical number of particles + var boxEditor = this.boxEditor - return desiredParticleCount; + var totalVolume = 0 + for (var i = 0; i < boxEditor.boxes.length; ++i) { + totalVolume += boxEditor.boxes[i].computeVolume() } - //begin simulation using boxes from box editor - //EDITING -> SIMULATING - FluidParticles.prototype.startSimulation = function () { - this.state = State.SIMULATING; + var particlesCreatedSoFar = 0 + for (var i = 0; i < boxEditor.boxes.length; ++i) { + var box = boxEditor.boxes[i] - 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 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 + } - var particleCount = particlesWidth * particlesHeight; - var particlePositions = []; - - var boxEditor = this.boxEditor; + for (var j = 0; j < particlesInBox; ++j) { + var position = box.randomPoint() + particlePositions.push(position) + } - 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; - } + particlesCreatedSoFar += particlesInBox + } - var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity; + 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; + // 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 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) - var sphereRadius = 7.0 / gridResolutionX; - this.simulatorRenderer.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, PARTICLES_PER_CELL, sphereRadius); + this.camera.setBounds(0, Math.PI / 2) + } - this.camera.setBounds(0, Math.PI / 2); - } + // go back to box editing + // SIMULATING -> EDITING + stopSimulation () { + this.state = State.EDITING - //go back to box editing - //SIMULATING -> EDITING - FluidParticles.prototype.stopSimulation = function () { - this.state = State.EDITING; + this.camera.setBounds(-Math.PI / 4, Math.PI / 4) + } - this.camera.setBounds(-Math.PI / 4, Math.PI / 4); + update () { + if (this.state === State.EDITING) { + this.boxEditor.draw() + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.update(this.timeStep) } - - 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 index d4c88f9..ec354cc 100644 --- a/index.html +++ b/index.html @@ -75,50 +75,7 @@
- - - - - - - - - - - - + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2be1794 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "fluid", + "version": "1.0.0", + "description": "Real-time particle-based 3D fluid simulation and rendering in WebGL.", + "main": "index.js", + "scripts": { + "start": "npx browser-sync ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dli/fluid.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/dli/fluid/issues" + }, + "homepage": "https://github.com/dli/fluid#readme" +} diff --git a/renderer.js b/renderer.js index 1b2463e..6078ae4 100644 --- a/renderer.js +++ b/renderer.js @@ -1,474 +1,434 @@ -'use strict' +import Utilities from './utilities.js' -var Renderer = (function () { +var SHADOW_MAP_WIDTH = 256 +var SHADOW_MAP_HEIGHT = 256 - 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 - } +// returns {vertices, normals, indices} +function generateSphereGeometry (iterations) { + var vertices = [] + var 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 + } } - - //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)); + 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]) } - 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); + faces = faces2 + } + + var packedVertices = [] + var packedNormals = [] + var 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 +export default class Renderer { + constructor (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)) + } + + onResize (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) + } + + reset (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) + } - Renderer.prototype.reset = function (particlesWidth, particlesHeight, sphereRadius) { - this.particlesWidth = particlesWidth; - this.particlesHeight = particlesHeight; + // you need to call reset() with the correct parameters before drawing anything + // projectionMatrix and viewMatrix are both expected to be Float32Array(16) + draw (simulator, projectionMatrix, viewMatrix) { + var wgl = this.wgl - this.sphereRadius = sphereRadius; + /// ////////////////////////////////////////// + // draw particles + var projectionViewMatrix = Utilities.premultiplyMatrix(new Float32Array(16), viewMatrix, projectionMatrix) - /////////////////////////////////////////////////////////// - // create particle data - - var particleCount = this.particlesWidth * this.particlesHeight; + /// //////////////////////////////////////////// + // 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) - //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.clear( + wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(-99999.0, -99999.0, -99999.0, -99999.0), + wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT) - wgl.bufferData(this.particleVertexBuffer, wgl.ARRAY_BUFFER, particleTextureCoordinates, wgl.STATIC_DRAW); - } + var sphereDrawState = wgl.createDrawState() + .bindFramebuffer(this.renderingFramebuffer) + .viewport(0, 0, this.canvas.width, this.canvas.height) - //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; + .enable(wgl.DEPTH_TEST) + .enable(wgl.CULL_FACE) - ///////////////////////////////////////////// - // draw particles + .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) - var projectionViewMatrix = Utilities.premultiplyMatrix(new Float32Array(16), viewMatrix, projectionMatrix); + .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) - /////////////////////////////////////////////// - //draw rendering data (normal, speed, depth) + .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, viewMatrix) - 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); + .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture) + .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture) - wgl.clear( - wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(-99999.0, -99999.0, -99999.0, -99999.0), - wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT); + .uniform1f('u_sphereRadius', this.sphereRadius) + wgl.drawElementsInstancedANGLE(sphereDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight) - var sphereDrawState = wgl.createDrawState() - .bindFramebuffer(this.renderingFramebuffer) - .viewport(0, 0, this.canvas.width, this.canvas.height) + /// //////////////////////////////////////////////// + // draw occlusion + wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.occlusionTexture, 0) - .enable(wgl.DEPTH_TEST) - .enable(wgl.CULL_FACE) + wgl.clear( + wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(0.0, 0.0, 0.0, 0.0), + wgl.COLOR_BUFFER_BIT) - .useProgram(this.sphereProgram) + var fov = 2.0 * Math.atan(1.0 / projectionMatrix[5]) - .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) + var occlusionDrawState = wgl.createDrawState() + .bindFramebuffer(this.renderingFramebuffer) + .viewport(0, 0, this.canvas.width, this.canvas.height) - .vertexAttribPointer(this.particleVertexBuffer, this.sphereProgram.getAttribLocation('a_textureCoordinates'), 2, wgl.FLOAT, wgl.FALSE, 0, 0) - .vertexAttribDivisorANGLE(this.sphereProgram.getAttribLocation('a_textureCoordinates'), 1) + .enable(wgl.DEPTH_TEST) + .depthMask(false) - .bindIndexBuffer(this.sphereIndexBuffer) + .enable(wgl.CULL_FACE) - .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix) - .uniformMatrix4fv('u_viewMatrix', false, viewMatrix) + .enable(wgl.BLEND) + .blendEquation(wgl.FUNC_ADD) + .blendFuncSeparate(wgl.ONE, wgl.ONE, wgl.ONE, wgl.ONE) - .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture) - .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture) + .useProgram(this.sphereAOProgram) - .uniform1f('u_sphereRadius', this.sphereRadius) + .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) - wgl.drawElementsInstancedANGLE(sphereDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight); + .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) - /////////////////////////////////////////////////// - // draw occlusion + .uniform1f('u_sphereRadius', this.sphereRadius) - wgl.framebufferTexture2D(this.renderingFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.occlusionTexture, 0); + wgl.drawElementsInstancedANGLE(occlusionDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight) - wgl.clear( - wgl.createClearState().bindFramebuffer(this.renderingFramebuffer).clearColor(0.0, 0.0, 0.0, 0.0), - wgl.COLOR_BUFFER_BIT); + /// ///////////////////////////////////////////// + // 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) - var fov = 2.0 * Math.atan(1.0 / projectionMatrix[5]); + wgl.clear( + wgl.createClearState().bindFramebuffer(this.depthFramebuffer).clearColor(0, 0, 0, 0), + wgl.DEPTH_BUFFER_BIT) - var occlusionDrawState = wgl.createDrawState() - .bindFramebuffer(this.renderingFramebuffer) - .viewport(0, 0, this.canvas.width, this.canvas.height) + var depthDrawState = wgl.createDrawState() + .bindFramebuffer(this.depthFramebuffer) + .viewport(0, 0, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT) - .enable(wgl.DEPTH_TEST) - .depthMask(false) + .enable(wgl.DEPTH_TEST) + .depthMask(true) - .enable(wgl.CULL_FACE) + // 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) - .enable(wgl.BLEND) - .blendEquation(wgl.FUNC_ADD) - .blendFuncSeparate(wgl.ONE, wgl.ONE, wgl.ONE, wgl.ONE) + .colorMask(false, false, false, false) - .useProgram(this.sphereAOProgram) + .enable(wgl.CULL_FACE) - .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) + .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) + .bindIndexBuffer(this.sphereIndexBuffer) - .uniformMatrix4fv('u_projectionMatrix', false, projectionMatrix) - .uniformMatrix4fv('u_viewMatrix', false, viewMatrix) + .uniformMatrix4fv('u_projectionViewMatrix', false, this.lightProjectionViewMatrix) - .uniformTexture('u_positionsTexture', 0, wgl.TEXTURE_2D, simulator.particlePositionTexture) - .uniformTexture('u_velocitiesTexture', 1, wgl.TEXTURE_2D, simulator.particleVelocityTexture) + .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(depthDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight) - .uniform1f('u_sphereRadius', this.sphereRadius) + /// //////////////////////////////////////// + // 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.drawElementsInstancedANGLE(occlusionDrawState, wgl.TRIANGLES, this.sphereGeometry.indices.length, wgl.UNSIGNED_SHORT, 0, this.particlesWidth * this.particlesHeight); + 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) - //////////////////////////////////////////////// - // draw depth map + .useProgram(this.compositeProgram) - 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); + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - wgl.clear( - wgl.createClearState().bindFramebuffer(this.depthFramebuffer).clearColor(0, 0, 0, 0), - wgl.DEPTH_BUFFER_BIT); + .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) - var depthDrawState = wgl.createDrawState() - .bindFramebuffer(this.depthFramebuffer) - .viewport(0, 0, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT) + .uniformTexture('u_shadowDepthTexture', 2, wgl.TEXTURE_2D, this.depthTexture) + .uniform2f('u_shadowResolution', SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT) + .uniformMatrix4fv('u_lightProjectionViewMatrix', false, this.lightProjectionViewMatrix) - .enable(wgl.DEPTH_TEST) - .depthMask(true) + wgl.drawArrays(compositeDrawState, wgl.TRIANGLE_STRIP, 0, 4) - //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) + /// /////////////////////////////////// + // FXAA + var inverseViewMatrix = Utilities.invertMatrix(new Float32Array(16), viewMatrix) - .colorMask(false, false, false, false) + wgl.clear( + wgl.createClearState().bindFramebuffer(null).clearColor(0, 0, 0, 0), + wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT) - .enable(wgl.CULL_FACE) + var fxaaDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) - .useProgram(this.sphereDepthProgram) + .useProgram(this.fxaaProgram) - .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) + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - .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); - } + .uniformTexture('u_input', 0, wgl.TEXTURE_2D, this.compositingTexture) + .uniform2f('u_resolution', this.canvas.width, this.canvas.height) - return Renderer; -}()); + wgl.drawArrays(fxaaDrawState, wgl.TRIANGLE_STRIP, 0, 4) + } +} diff --git a/simulator.js b/simulator.js index ccd9d73..ae8192f 100644 --- a/simulator.js +++ b/simulator.js @@ -1,21 +1,16 @@ -'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]) - //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]) - //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] - //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] @@ -24,261 +19,237 @@ var Simulator = (function () { 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)); +// 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 swap (object, a, b) { + var temp = object[a] + object[a] = object[b] + object[b] = temp +} + +export default class Simulator { + constructor (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 + reset (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 + } } + const { wgl } = this + + 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 + } - //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.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.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); + 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) - function swap (object, a, b) { - var temp = object[a]; - object[a] = object[b]; - object[b] = temp; - } + 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) + } - //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; + // you need to call reset() with correct parameters before simulating + // mouseVelocity, mouseRayOrigin, mouseRayDirection are all expected to be arrays of 3 values + simulate (timeStep, mouseVelocity, mouseRayOrigin, mouseRayDirection) { + if (timeStep === 0.0) { return } - this.frameNumber += 1; + this.frameNumber += 1 - var wgl = this.wgl; + var wgl = this.wgl - /* + /* the simulation process transfer particle velocities to velocity grid save this velocity grid @@ -288,304 +259,273 @@ var Simulator = (function () { 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) - ////////////////////////////////////////////////////// - //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) + transferToGridDrawState.uniform1i('u_accumulate', 1) - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) + for (let 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) + } - .useProgram(this.normalizeGridProgram) - .uniformTexture('u_weightTexture', 0, wgl.TEXTURE_2D, this.weightTexture) - .uniformTexture('u_accumulatedVelocityTexture', 1, wgl.TEXTURE_2D, this.tempVelocityTexture) + // 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) - wgl.drawArrays(normalizeDrawState, wgl.TRIANGLE_STRIP, 0, 4); + 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) - ////////////////////////////////////////////////////// - // mark cells with fluid + .useProgram(this.normalizeGridProgram) + .uniformTexture('u_weightTexture', 0, wgl.TEXTURE_2D, this.weightTexture) + .uniformTexture('u_accumulatedVelocityTexture', 1, wgl.TEXTURE_2D, this.tempVelocityTexture) - 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); + wgl.drawArrays(normalizeDrawState, wgl.TRIANGLE_STRIP, 0, 4) - var markDrawState = wgl.createDrawState() - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) + /// /////////////////////////////////////////////////// + // 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) - .vertexAttribPointer(this.particleVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) + var markDrawState = wgl.createDrawState() + .bindFramebuffer(this.simulationFramebuffer) + .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) - .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); + .vertexAttribPointer(this.particleVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - wgl.drawArrays(markDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight); + .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) - //////////////////////////////////////////////////// - // save our original velocity grid + wgl.drawArrays(markDrawState, wgl.POINTS, 0, this.particlesWidth * this.particlesHeight) - wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.originalVelocityTexture, 0); + /// ///////////////////////////////////////////////// + // 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) + 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) + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - .useProgram(this.copyProgram) - .uniformTexture('u_texture', 0, wgl.TEXTURE_2D, this.velocityTexture) + .useProgram(this.copyProgram) + .uniformTexture('u_texture', 0, wgl.TEXTURE_2D, this.velocityTexture) - wgl.drawArrays(copyDrawState, wgl.TRIANGLE_STRIP, 0, 4); + 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) - ///////////////////////////////////////////////////// - // add forces to velocity grid + 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) - wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); + .useProgram(this.addForceProgram) + .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture) - var addForceDrawState = wgl.createDrawState() - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) + .uniform1f('u_timeStep', timeStep) - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) + .uniform3f('u_mouseVelocity', mouseVelocity[0], mouseVelocity[1], mouseVelocity[2]) - .useProgram(this.addForceProgram) - .uniformTexture('u_velocityTexture', 0, 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) + .uniform3f('u_mouseRayOrigin', mouseRayOrigin[0], mouseRayOrigin[1], mouseRayOrigin[2]) + .uniform3f('u_mouseRayDirection', mouseRayDirection[0], mouseRayDirection[1], mouseRayDirection[2]) - .uniform3f('u_mouseVelocity', mouseVelocity[0], mouseVelocity[1], mouseVelocity[2]) + wgl.drawArrays(addForceDrawState, wgl.TRIANGLE_STRIP, 0, 4) - .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) - .uniform3f('u_gridSize', this.gridWidth, this.gridHeight, this.gridDepth) + swap(this, 'velocityTexture', 'tempVelocityTexture') - .uniform3f('u_mouseRayOrigin', mouseRayOrigin[0], mouseRayOrigin[1], mouseRayOrigin[2]) - .uniform3f('u_mouseRayDirection', mouseRayDirection[0], mouseRayDirection[1], mouseRayDirection[2]) + /// ////////////////////////////////////////////////// + // 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) - wgl.drawArrays(addForceDrawState, wgl.TRIANGLE_STRIP, 0, 4); + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - swap(this, 'velocityTexture', 'tempVelocityTexture'); + .useProgram(this.enforceBoundariesProgram) + .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture) + .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ) - - ///////////////////////////////////////////////////// - // enforce boundary velocity conditions + wgl.drawArrays(enforceBoundariesDrawState, wgl.TRIANGLE_STRIP, 0, 4) - wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); + swap(this, 'velocityTexture', 'tempVelocityTexture') - var enforceBoundariesDrawState = wgl.createDrawState() - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) + /// ////////////////////////////////////////////////// + // update velocityTexture for non divergence + // compute divergence for pressure projection + var divergenceDrawState = wgl.createDrawState() - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) + .bindFramebuffer(this.simulationFramebuffer) + .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) - .useProgram(this.enforceBoundariesProgram) - .uniformTexture('u_velocityTexture', 0, wgl.TEXTURE_2D, this.velocityTexture) - .uniform3f('u_gridResolution', this.gridResolutionX, this.gridResolutionY, this.gridResolutionZ); + .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) - wgl.drawArrays(enforceBoundariesDrawState, wgl.TRIANGLE_STRIP, 0, 4); + .uniform1f('u_maxDensity', this.particleDensity) - swap(this, 'velocityTexture', 'tempVelocityTexture'); + .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) - ///////////////////////////////////////////////////// - // update velocityTexture for non divergence + 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) - //compute divergence for pressure projection + .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) - var divergenceDrawState = wgl.createDrawState() - - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) - .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) + 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) - .uniform1f('u_maxDensity', this.particleDensity) + var PRESSURE_JACOBI_ITERATIONS = 50 + 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) - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) + wgl.drawArrays(jacobiDrawState, wgl.TRIANGLE_STRIP, 0, 4) - 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 + swap(this, 'pressureTexture', 'tempSimulationTexture') + } - var jacobiDrawState = wgl.createDrawState() - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.scalarTextureWidth, this.scalarTextureHeight) + // subtract pressure gradient from velocity + wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0) - .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) + var subtractDrawState = wgl.createDrawState() + .bindFramebuffer(this.simulationFramebuffer) + .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) + .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.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 = 50; - 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.drawArrays(subtractDrawState, wgl.TRIANGLE_STRIP, 0, 4) - wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.tempVelocityTexture, 0); + swap(this, 'velocityTexture', 'tempVelocityTexture') - var subtractDrawState = wgl.createDrawState() - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.velocityTextureWidth, this.velocityTextureHeight) + /// ////////////////////////////////////////////////////////// + // transfer velocities back to particles + wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.particleVelocityTextureTemp, 0) - .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) + var transferToParticlesDrawState = wgl.createDrawState() + .bindFramebuffer(this.simulationFramebuffer) + .viewport(0, 0, this.particlesWidth, this.particlesHeight) - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, false, 0, 0) - - wgl.drawArrays(subtractDrawState, wgl.TRIANGLE_STRIP, 0, 4); - - swap(this, 'velocityTexture', 'tempVelocityTexture'); + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - ///////////////////////////////////////////////////////////// - // transfer velocities back to particles + .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) - wgl.framebufferTexture2D(this.simulationFramebuffer, wgl.FRAMEBUFFER, wgl.COLOR_ATTACHMENT0, wgl.TEXTURE_2D, this.particleVelocityTextureTemp, 0); + .uniform1f('u_flipness', this.flipness) - var transferToParticlesDrawState = wgl.createDrawState() - .bindFramebuffer(this.simulationFramebuffer) - .viewport(0, 0, this.particlesWidth, this.particlesHeight) + wgl.drawArrays(transferToParticlesDrawState, wgl.TRIANGLE_STRIP, 0, 4) - .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) + swap(this, 'particleVelocityTextureTemp', 'particleVelocityTexture') - .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) + /// //////////////////////////////////////////// + // 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) - .uniform1f('u_flipness', this.flipness) + var advectDrawState = wgl.createDrawState() + .bindFramebuffer(this.simulationFramebuffer) + .viewport(0, 0, this.particlesWidth, this.particlesHeight) - wgl.drawArrays(transferToParticlesDrawState, wgl.TRIANGLE_STRIP, 0, 4); + .vertexAttribPointer(this.quadVertexBuffer, 0, 2, wgl.FLOAT, wgl.FALSE, 0, 0) - 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); + .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) - swap(this, 'particlePositionTextureTemp', 'particlePositionTexture'); - } + wgl.drawArrays(advectDrawState, wgl.TRIANGLE_STRIP, 0, 4) - return Simulator; -}()); + swap(this, 'particlePositionTextureTemp', 'particlePositionTexture') + } +} diff --git a/simulatorrenderer.js b/simulatorrenderer.js index 5c182b2..93d7313 100644 --- a/simulatorrenderer.js +++ b/simulatorrenderer.js @@ -1,115 +1,114 @@ -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); - } +import Utilities from './utilities.js' +import Renderer from './renderer.js' +import Simulator from './simulator.js' + +export default class SimulatorRenderer { + constructor (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 + var 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; + onMouseMove (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.mouseX = normalizedX * 2.0 - 1.0 + this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0 - this.camera.onMouseMove(event); - }; + this.camera.onMouseMove(event) + } - SimulatorRenderer.prototype.onMouseDown = function (event) { - this.camera.onMouseDown(event); - }; + onMouseDown (event) { + this.camera.onMouseDown(event) + } - SimulatorRenderer.prototype.onMouseUp = function (event) { - this.camera.onMouseUp(event); - }; + onMouseUp (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); - } + reset (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]); + update (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 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; + var mousePlaneX = viewSpaceMouseRay[0] * this.camera.distance + var mousePlaneY = viewSpaceMouseRay[1] * this.camera.distance - if (this.camera.isMouseDown()) { - mouseVelocityX = 0.0; - mouseVelocityY = 0.0; - } + var mouseVelocityX = mousePlaneX - this.lastMousePlaneX + var mouseVelocityY = mousePlaneY - this.lastMousePlaneY - this.lastMousePlaneX = mousePlaneX; - this.lastMousePlaneY = mousePlaneY; - - var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()); - var worldSpaceMouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix); - Utilities.normalizeVector(worldSpaceMouseRay, worldSpaceMouseRay); + if (this.camera.isMouseDown()) { + mouseVelocityX = 0.0 + mouseVelocityY = 0.0 + } + this.lastMousePlaneX = mousePlaneX + this.lastMousePlaneY = mousePlaneY - var cameraViewMatrix = this.camera.getViewMatrix(); - var cameraRight = [cameraViewMatrix[0], cameraViewMatrix[4], cameraViewMatrix[8]]; - var cameraUp = [cameraViewMatrix[1], cameraViewMatrix[5], cameraViewMatrix[9]]; + var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()) + var worldSpaceMouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix) + Utilities.normalizeVector(worldSpaceMouseRay, worldSpaceMouseRay) - var mouseVelocity = []; - for (var i = 0; i < 3; ++i) { - mouseVelocity[i] = mouseVelocityX * cameraRight[i] + mouseVelocityY * cameraUp[i]; - } + var cameraViewMatrix = this.camera.getViewMatrix() + var cameraRight = [cameraViewMatrix[0], cameraViewMatrix[4], cameraViewMatrix[8]] + var cameraUp = [cameraViewMatrix[1], cameraViewMatrix[5], cameraViewMatrix[9]] - this.simulator.simulate(timeStep, mouseVelocity, this.camera.getPosition(), worldSpaceMouseRay); - this.renderer.draw(this.simulator, this.projectionMatrix, this.camera.getViewMatrix()); + var mouseVelocity = [] + for (var i = 0; i < 3; ++i) { + mouseVelocity[i] = mouseVelocityX * cameraRight[i] + mouseVelocityY * cameraUp[i] } - SimulatorRenderer.prototype.onResize = function (event) { - this.renderer.onResize(event); - } + this.simulator.simulate(timeStep, mouseVelocity, this.camera.getPosition(), worldSpaceMouseRay) + this.renderer.draw(this.simulator, this.projectionMatrix, this.camera.getViewMatrix()) + } - return SimulatorRenderer; -}()); + onResize (event) { + this.renderer.onResize(event) + } +} diff --git a/slider.js b/slider.js index fc72434..bdf4766 100644 --- a/slider.js +++ b/slider.js @@ -1,59 +1,55 @@ -'use strict' +import Utilities from './utilities.js' -var Slider = (function () { +// changeCallback is called with the new value +export default class Slider { + constructor (element, initial, min, max, changeCallback) { + this.value = initial - //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.min = min; - this.max = max; + this.div = element - this.div = element; + this.innerDiv = document.createElement('div') + this.innerDiv.style.position = 'absolute' + this.innerDiv.style.height = this.div.offsetHeight + 'px' - this.innerDiv = document.createElement('div'); - this.innerDiv.style.position = 'absolute'; - this.innerDiv.style.height = this.div.offsetHeight + 'px'; + this.div.appendChild(this.innerDiv) - this.div.appendChild(this.innerDiv); + this.changeCallback = changeCallback - this.changeCallback = changeCallback; + this.mousePressed = false - this.mousePressed = false; + this.redraw() - this.redraw(); + this.div.addEventListener('mousedown', function (event) { + this.mousePressed = true + this.onChange(event) + }.bind(this)) - 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('mouseup', (function (event) { - this.mousePressed = false; - }).bind(this)); + document.addEventListener('mousemove', function (event) { + if (this.mousePressed) { + this.onChange(event) + } + }.bind(this)) + } - document.addEventListener('mousemove', (function (event) { - if (this.mousePressed) { - this.onChange(event); - } - }).bind(this)); + redraw () { + 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' + } - }; + onChange (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) - 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'; - } + this.redraw() - 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; -}()); + this.changeCallback(this.value) + } +} diff --git a/utilities.js b/utilities.js index 4d9f2fc..36cd2cf 100644 --- a/utilities.js +++ b/utilities.js @@ -1,304 +1,308 @@ -'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; - }, +const magnitudeOfVector = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) + +export default { + clamp (x, min, max) { + return Math.max(min, Math.min(max, x)) + }, + + getMousePosition (event, element) { + var boundingRect = element.getBoundingClientRect() + return { + x: event.clientX - boundingRect.left, + y: event.clientY - boundingRect.top + } + }, + + addVectors (out, a, b) { + out[0] = a[0] + b[0] + out[1] = a[1] + b[1] + out[2] = a[2] + b[2] + return out + }, + + subtractVectors (out, a, b) { + out[0] = a[0] - b[0] + out[1] = a[1] - b[1] + out[2] = a[2] - b[2] + return out + }, + + magnitudeOfVector, + + dotVectors (a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + }, + + multiplyVectorByScalar (out, v, k) { + out[0] = v[0] * k + out[1] = v[1] * k + out[2] = v[2] * k + return out + }, + + normalizeVector (out, v) { + var inverseMagnitude = 1.0 / magnitudeOfVector(v) + out[0] = v[0] * inverseMagnitude + out[1] = v[1] * inverseMagnitude + out[2] = v[2] * inverseMagnitude + return out + }, + + makePerspectiveMatrix (out, fovy, aspect, near, far) { + var f = 1.0 / Math.tan(fovy / 2) + var 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 (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 (out, matrixA, matrixB) { // out = matrixB * matrixA + var b0 = matrixB[0]; var b4 = matrixB[4]; var b8 = matrixB[8]; var b12 = matrixB[12] + var b1 = matrixB[1]; var b5 = matrixB[5]; var b9 = matrixB[9]; var b13 = matrixB[13] + var b2 = matrixB[2]; var b6 = matrixB[6]; var b10 = matrixB[10]; var b14 = matrixB[14] + var b3 = matrixB[3]; var b7 = matrixB[7]; var b11 = matrixB[11]; var b15 = matrixB[15] + + var aX = matrixA[0]; var aY = matrixA[1]; var aZ = matrixA[2]; var 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 (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 (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 (out, v, m) { + var x = v[0]; var y = v[1]; var 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 (out, m) { + var m0 = m[0]; var m4 = m[4]; var m8 = m[8]; var m12 = m[12] + var m1 = m[1]; var m5 = m[5]; var m9 = m[9]; var m13 = m[13] + var m2 = m[2]; var m6 = m[6]; var m10 = m[10]; var m14 = m[14] + var m3 = m[3]; var m7 = m[7]; var m11 = m[11]; var m15 = m[15] + + var temp0 = m10 * m15 + var temp1 = m14 * m11 + var temp2 = m6 * m15 + var temp3 = m14 * m7 + var temp4 = m6 * m11 + var temp5 = m10 * m7 + var temp6 = m2 * m15 + var temp7 = m14 * m3 + var temp8 = m2 * m11 + var temp9 = m10 * m3 + var temp10 = m2 * m7 + var temp11 = m6 * m3 + var temp12 = m8 * m13 + var temp13 = m12 * m9 + var temp14 = m4 * m13 + var temp15 = m12 * m5 + var temp16 = m4 * m9 + var temp17 = m8 * m5 + var temp18 = m0 * m13 + var temp19 = m12 * m1 + var temp20 = m0 * m9 + var temp21 = m8 * m1 + var temp22 = m0 * m5 + var temp23 = m4 * m1 + + var t0 = (temp0 * m5 + temp3 * m9 + temp4 * m13) - (temp1 * m5 + temp2 * m9 + temp5 * m13) + var t1 = (temp1 * m1 + temp6 * m9 + temp9 * m13) - (temp0 * m1 + temp7 * m9 + temp8 * m13) + var t2 = (temp2 * m1 + temp7 * m5 + temp10 * m13) - (temp3 * m1 + temp6 * m5 + temp11 * m13) + var t3 = (temp5 * m1 + temp8 * m5 + temp11 * m9) - (temp4 * m1 + temp9 * m5 + temp10 * m9) + + var 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 (matrix, eye, target, up) { // up is assumed to be normalized + var forwardX = eye[0] - target[0] + var forwardY = eye[1] - target[1] + var 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 (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 index ce98a24..1569d4e 100644 --- a/wrappedgl.js +++ b/wrappedgl.js @@ -1,974 +1,1008 @@ -'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 - }; +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' +] + +export default class WrappedGL { + constructor (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 - this.defaultTextureUnit = 0; //the texure unit we use for modifying textures + 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 + } } - WrappedGL.checkWebGLSupport = function (successCallback, failureCallback) { - WrappedGL.checkWebGLSupportWithExtensions([], successCallback, function (hasWebGL, unsupportedExtensions) { - failureCallback(); - }); + 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 + } } - 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; - } + 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 + } - 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; - } + this.defaultTextureUnit = 0 // the texure unit we use for modifying textures + } - //webgl support and all required extensions - successCallback(); - }; + getSupportedExtensions () { + return this.gl.getSupportedExtensions() + } - WrappedGL.prototype.getSupportedExtensions = function () { - return this.gl.getSupportedExtensions(); - }; + // returns null if the extension is not supported, otherwise the extension object + getExtension (name) { + var gl = this.gl - //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') - //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 - if (instancedExt !== null) { - this.instancedExt = instancedExt; + var maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS) - 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 + } + } - for (var i = 0; i < maxVertexAttributes; ++i) { - this.parameters['attributeDivisor' + i.toString()] = { - defaults: [0], - setter: (function () { - var index = i; + // override vertexAttribPointer + DrawState.prototype.vertexAttribPointer = function (buffer, index, size, type, normalized, stride, offset) { + this.setParameter('attributeArray' + index.toString(), [buffer, size, type, normalized, stride, offset]) - return function (divisor) { - instancedExt.vertexAttribDivisorANGLE(index, divisor); - } - }()), - usedInDraw: true - }; - } + 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]) + } - //override vertexAttribPointer - DrawState.prototype.vertexAttribPointer = function (buffer, index, size, type, normalized, stride, offset) { - this.setParameter('attributeArray' + index.toString(), [buffer, size, type, normalized, stride, offset]); + return this + } - 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]); - } + DrawState.prototype.vertexAttribDivisorANGLE = function (index, divisor) { + this.setParameter('attributeDivisor' + index.toString(), [divisor]) + return this + } - return this; - }; + this.drawArraysInstancedANGLE = function (drawState, mode, first, count, primcount) { + this.resolveDrawState(drawState) - DrawState.prototype.vertexAttribDivisorANGLE = function (index, divisor) { - this.setParameter('attributeDivisor' + index.toString(), [divisor]); - return this; - }; + this.instancedExt.drawArraysInstancedANGLE(mode, first, count, primcount) + } - this.drawArraysInstancedANGLE = function (drawState, mode, first, count, primcount) { - this.resolveDrawState(drawState); + this.drawElementsInstancedANGLE = function (drawState, mode, count, type, indices, primcount) { + this.resolveDrawState(drawState) - this.instancedExt.drawArraysInstancedANGLE(mode, first, count, primcount); - }; + this.instancedExt.drawElementsInstancedANGLE(mode, count, type, indices, primcount) + } - this.drawElementsInstancedANGLE = function (drawState, mode, count, type, indices, primcount) { - this.resolveDrawState(drawState); + return {} + } else { + return null + } + } else { // all others, we can just return as is (we can treat them as simple enums) + return gl.getExtension(name) + } + } - this.instancedExt.drawElementsInstancedANGLE(mode, count, type, indices, primcount); - }; + // flag is one of usedInDraw, usedInClear, usedInRead + resolveState (state, flag) { + var gl = this.gl - return {}; - } else { - return null; - } + // 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) - } else { //all others, we can just return as is (we can treat them as simple enums) - return gl.getExtension(name); + delete this.changedParameters[parameterName] + } } - }; + } + } - //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]) + } + } + } + } - //now we set all of the new incoming states + resolveDrawState (drawState) { + var gl = this.gl - for (var parameterName in state.changedParameters) { - if (state.changedParameters.hasOwnProperty(parameterName)) { + this.resolveState(drawState, 'usedInDraw') - if (!this.changedParameters.hasOwnProperty(parameterName) || //if this state is not currently set - !arraysEqual(this.changedParameters[parameterName], state.changedParameters[parameterName]) //or if it's changed - ) { + // 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 - this.changedParameters[parameterName] = state.changedParameters[parameterName]; + 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.parameters[parameterName].setter.apply(this.gl, this.changedParameters[parameterName]); - } - } - } + this.uniformSetters[drawState.uniforms[uniformName].type].apply(gl, args) + } } + } - WrappedGL.prototype.resolveDrawState = function (drawState) { - var gl = this.gl; + drawArrays (drawState, mode, first, count) { + this.resolveDrawState(drawState) - this.resolveState(drawState, 'usedInDraw'); + this.gl.drawArrays(mode, first, count) + } - //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 + drawElements (drawState, mode, count, type, offset) { + this.resolveDrawState(drawState) - 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.gl.drawElements(mode, count, type, offset) + } - this.uniformSetters[drawState.uniforms[uniformName].type].apply(gl, args); - } - } + resolveClearState (clearState) { + this.resolveState(clearState, 'usedInClear') + } - }; + clear (clearState, bit) { + this.resolveClearState(clearState) - WrappedGL.prototype.drawArrays = function (drawState, mode, first, count) { - this.resolveDrawState(drawState); + this.gl.clear(bit) + } - this.gl.drawArrays(mode, first, count); - }; + resolveReadState (readState) { + this.resolveState(readState, 'usedInRead') + } - WrappedGL.prototype.drawElements = function (drawState, mode, count, type, offset) { - this.resolveDrawState(drawState); + readPixels (readState, x, y, width, height, format, type, pixels) { + this.resolveReadState(readState) - this.gl.drawElements(mode, count, type, offset); - }; + this.gl.readPixels(x, y, width, height, format, type, pixels) + } - WrappedGL.prototype.resolveClearState = function (clearState) { - this.resolveState(clearState, 'usedInClear'); - }; + finish () { + this.gl.finish() + return this + } - WrappedGL.prototype.clear = function (clearState, bit) { - this.resolveClearState(clearState); + flush () { + this.gl.flush() + return this + } - this.gl.clear(bit); - }; + getError () { + return this.gl.getError() + } - WrappedGL.prototype.resolveReadState = function (readState) { - this.resolveState(readState, 'usedInRead'); - }; + createFramebuffer () { + return this.gl.createFramebuffer() + } - WrappedGL.prototype.readPixels = function (readState, x, y, width, height, format, type, pixels) { - this.resolveReadState(readState); + framebufferTexture2D (framebuffer, target, attachment, textarget, texture, level) { + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer) + this.changedParameters['framebuffer'] = framebuffer - this.gl.readPixels(x, y, width, height, format, type, pixels); - }; + this.gl.framebufferTexture2D(target, attachment, textarget, texture, level) - WrappedGL.prototype.finish = function () { - this.gl.finish(); - return this; - }; + return this + } - WrappedGL.prototype.flush = function () { - this.gl.flush(); - return this; - }; + framebufferRenderbuffer (framebuffer, target, attachment, renderbuffertarget, renderbuffer) { + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer) + this.changedParameters['framebuffer'] = framebuffer - WrappedGL.prototype.getError = function () { - return this.gl.getError(); - }; + this.gl.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer) + } - WrappedGL.prototype.createFramebuffer = function () { - return this.gl.createFramebuffer(); - }; + drawBuffers (framebuffer, buffers) { + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer) + this.changedParameters['framebuffer'] = framebuffer - WrappedGL.prototype.framebufferTexture2D = function (framebuffer, target, attachment, textarget, texture, level) { - this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer); - this.changedParameters['framebuffer'] = framebuffer; + this.drawExt.drawBuffersWEBGL(buffers) + } - this.gl.framebufferTexture2D(target, attachment, textarget, texture, level); + createTexture () { + return this.gl.createTexture() + } - return this; - }; + bindTextureForEditing (target, texture) { + this.gl.activeTexture(this.gl.TEXTURE0 + this.defaultTextureUnit) + this.gl.bindTexture(target, texture) - WrappedGL.prototype.framebufferRenderbuffer = function (framebuffer, target, attachment, renderbuffertarget, renderbuffer) { - this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer); - this.changedParameters['framebuffer'] = framebuffer; + this.changedParameters['texture' + this.defaultTextureUnit.toString()] = [target, texture] + } - this.gl.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer); - }; + // 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) + texImage2D (target, texture) { + var args = Array.prototype.slice.call(arguments, 2) + args.unshift(target) // add target to for texImage2D arguments list - WrappedGL.prototype.drawBuffers = function (framebuffer, buffers) { - this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer); - this.changedParameters['framebuffer'] = framebuffer; + this.bindTextureForEditing(target, texture) + this.gl.texImage2D.apply(this.gl, args) - this.drawExt.drawBuffersWEBGL(buffers); - }; + return this + } - WrappedGL.prototype.createTexture = function () { - return this.gl.createTexture(); - }; + texParameteri (target, texture, pname, param) { + this.bindTextureForEditing(target, texture) + this.gl.texParameteri(target, pname, param) - WrappedGL.prototype.bindTextureForEditing = function (target, texture) { - this.gl.activeTexture(this.gl.TEXTURE0 + this.defaultTextureUnit); - this.gl.bindTexture(target, texture); + return this + } - this.changedParameters['texture' + this.defaultTextureUnit.toString()] = [target, texture]; - }; + texParameterf (target, texture, pname, param) { + this.bindTextureForEditing(target, texture) + this.gl.texParameterf(target, pname, param) - //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 + return this + } - this.bindTextureForEditing(target, texture); - this.gl.texImage2D.apply(this.gl, args); + pixelStorei (target, texture, pname, param) { + this.bindTextureForEditing(target, texture) + this.gl.pixelStorei(pname, param) - return this; - }; + return this + } - WrappedGL.prototype.texParameteri = function(target, texture, pname, param) { - this.bindTextureForEditing(target, texture); - this.gl.texParameteri(target, pname, param); + setTextureFiltering (target, texture, wrapS, wrapT, minFilter, magFilter) { + var gl = this.gl - return this; - }; + this.bindTextureForEditing(target, texture) - WrappedGL.prototype.texParameterf = function(target, texture, pname, param) { - this.bindTextureForEditing(target, texture); - this.gl.texParameterf(target, pname, param); + 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; - }; + return this + } - WrappedGL.prototype.pixelStorei = function(target, texture, pname, param) { - this.bindTextureForEditing(target, texture); - this.gl.pixelStorei(pname, param); + generateMipmap (target, texture) { + this.bindTextureForEditing(target, texture) + this.gl.generateMipmap(target) - return this; - }; + return this + } - WrappedGL.prototype.setTextureFiltering = function (target, texture, wrapS, wrapT, minFilter, magFilter) { - var gl = this.gl; + buildTexture (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) - this.bindTextureForEditing(target, texture); + return 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); + rebuildTexture (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; - }; + return this + } - WrappedGL.prototype.generateMipmap = function (target, texture) { - this.bindTextureForEditing(target, texture); - this.gl.generateMipmap(target); + createRenderbuffer () { + return this.gl.createRenderbuffer() + } - return this; - }; + renderbufferStorage (renderbuffer, target, internalformat, width, height) { + this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, renderbuffer) + this.gl.renderbufferStorage(target, internalformat, width, height) - 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 this + } - return texture; - }; + createBuffer () { + return this.gl.createBuffer() + } - 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); + bufferData (buffer, target, data, usage) { + var gl = this.gl - return this; - }; + 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] + } - WrappedGL.prototype.createRenderbuffer = function () { - return this.gl.createRenderbuffer(); - }; + gl.bindBuffer(target, buffer) + gl.bufferData(target, data, usage) + } - WrappedGL.prototype.renderbufferStorage = function (renderbuffer, target, internalformat, width, height) { - this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, renderbuffer); - this.gl.renderbufferStorage(target, internalformat, width, height); + bufferSubData (buffer, target, offset, data) { + var gl = this.gl - return this; - }; + 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] + } - WrappedGL.prototype.createBuffer = function () { - return this.gl.createBuffer(); - }; + gl.bindBuffer(target, buffer) + gl.bufferSubData(target, offset, data) + } + + createProgram (vertexShaderSource, fragmentShaderSource, attributeLocations) { + return new WrappedProgram(this, vertexShaderSource, fragmentShaderSource, attributeLocations) + } + + // 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 + createProgramFromFiles (vertexShaderPath, fragmentShaderPath, attributeLocations, successCallback, failureCallback) { + var that = this + + var filesToLoad = [] + if (Array.isArray(vertexShaderPath)) { + filesToLoad = filesToLoad.concat(vertexShaderPath) + } else { + filesToLoad.push(vertexShaderPath) + } - WrappedGL.prototype.bufferData = function (buffer, target, data, usage) { - var gl = this.gl; + if (Array.isArray(fragmentShaderPath)) { + filesToLoad = filesToLoad.concat(fragmentShaderPath) + } else { + filesToLoad.push(fragmentShaderPath) + } - 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]; + loadTextFiles(filesToLoad, function (files) { + var vertexShaderSources = [] + if (Array.isArray(vertexShaderPath)) { + for (var i = 0; i < vertexShaderPath.length; ++i) { + vertexShaderSources.push(files[vertexShaderPath[i]]) } - - 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]; + } 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]) + } - gl.bindBuffer(target, buffer); - gl.bufferSubData(target, offset, data); - }; - - WrappedGL.prototype.createProgram = function (vertexShaderSource, fragmentShaderSource, attributeLocations) { - return new WrappedProgram(this, vertexShaderSource, fragmentShaderSource, attributeLocations); - }; + var program = that.createProgram(vertexShaderSources.join('\n'), fragmentShaderSources.join('\n'), attributeLocations) + successCallback(program) + }) + } + // asynchronous + createProgramsFromFiles (programParameters, successCallback, failureCallback) { + var programCount = keysInObject(programParameters) - //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(); - - }()); - } - }; + var loadedSoFar = 0 + var programs = {} + for (var programName in programParameters) { + if (programParameters.hasOwnProperty(programName)) { + var parameters = programParameters[programName] - //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; + (function () { + var name = programName - 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]); - } - + that.createProgramFromFiles(parameters.vertexShader, parameters.fragmentShader, parameters.attributeLocations, function (program) { + programs[name] = program - var fragmentShaderSources = []; - if (Array.isArray(fragmentShaderPath)) { - for (var i = 0; i < fragmentShaderPath.length; ++i) { - fragmentShaderSources.push(files[fragmentShaderPath[i]]); - } - } else { - fragmentShaderSources.push(files[fragmentShaderPath]); + loadedSoFar++ + if (loadedSoFar === programCount) { // if we've loaded all the programs + successCallback(programs) } + }) + }()) + } + } + } + + createDrawState () { + return new DrawState(this) + } + + createClearState () { + return new ClearState(this) + } + + createReadState () { + return new ReadState(this) + } + + deleteBuffer (buffer) { + this.gl.deleteBuffer(buffer) + } + + deleteFramebuffer (buffer) { + this.gl.deleteFramebuffer(buffer) + } + + deleteTexture (texture) { + this.gl.deleteTexture(texture) + } + + static checkWebGLSupport (successCallback, failureCallback) { + WrappedGL.checkWebGLSupportWithExtensions([], successCallback, function (hasWebGL, unsupportedExtensions) { + failureCallback() + }) + } + + static checkWebGLSupportWithExtensions (extensions, successCallback, failureCallback) { + 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 program = that.createProgram(vertexShaderSources.join('\n'), fragmentShaderSources.join('\n'), attributeLocations); - successCallback(program); - }); - }; + 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() + } +} + +// 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() + }()) + } +}; + +/* input: { firstProgram: { @@ -994,442 +1028,378 @@ var WrappedGL = (function () { secondProgram: secondProgramObject */ - function keysInObject (object) { - var count = 0; - for (var key in object) { - if (object.hasOwnProperty(key)) { - count += 1; - } - } - return count; +function keysInObject (object) { + var count = 0 + for (var key in object) { + if (object.hasOwnProperty(key)) { + count += 1 } - - //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 - /* + } + return count +} + +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) + var 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; +}; + +// 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] } - - - //inherits from State - function DrawState (wgl) { - State.call(this, wgl); - - //we always set uniforms - this.uniforms = {}; //eg: {type: '3f', value: [x, y, z]} + } +} + +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 } - - 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; - -}()); + } + + 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 +}