diff --git a/.gitignore b/.gitignore index a093e1d..cdb1e75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .DS_Store *.pyc docs -lightgl.js diff --git a/README.md b/README.md index 4a4945b..536f21b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ This library makes it easier to quickly prototype WebGL applications. It's lower level than many other WebGL libraries and while it doesn't provide a scene graph, it re-implements OpenGL's modelview/projection matrix stack to provide similar functionality. It also re-introduces some built-in uniforms from GLSL (such as `gl_Vertex` and `gl_ModelViewProjectionMatrix`) and OpenGL's immediate mode. +## Install from npm + +```shell +npm install lightgl.js +``` + ## Building the library * `python build.py`: build `lightgl.js` from the files in the `src` directory diff --git a/lightgl.js b/lightgl.js new file mode 100644 index 0000000..d911af1 --- /dev/null +++ b/lightgl.js @@ -0,0 +1,2071 @@ +/* + * lightgl.js + * http://github.com/evanw/lightgl.js/ + * + * Copyright 2011 Evan Wallace + * Released under the MIT license + */ +var GL = (function() { + +// src/mesh.js +// Represents indexed triangle geometry with arbitrary additional attributes. +// You need a shader to draw a mesh; meshes can't draw themselves. +// +// A mesh is a collection of `GL.Buffer` objects which are either vertex buffers +// (holding per-vertex attributes) or index buffers (holding the order in which +// vertices are rendered). By default, a mesh has a position vertex buffer called +// `vertices` and a triangle index buffer called `triangles`. New buffers can be +// added using `addVertexBuffer()` and `addIndexBuffer()`. Two strings are +// required when adding a new vertex buffer, the name of the data array on the +// mesh instance and the name of the GLSL attribute in the vertex shader. +// +// Example usage: +// +// var mesh = new GL.Mesh({ coords: true, lines: true }); +// +// // Default attribute "vertices", available as "gl_Vertex" in +// // the vertex shader +// mesh.vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]; +// +// // Optional attribute "coords" enabled in constructor, +// // available as "gl_TexCoord" in the vertex shader +// mesh.coords = [[0, 0], [1, 0], [0, 1], [1, 1]]; +// +// // Custom attribute "weights", available as "weight" in the +// // vertex shader +// mesh.addVertexBuffer('weights', 'weight'); +// mesh.weights = [1, 0, 0, 1]; +// +// // Default index buffer "triangles" +// mesh.triangles = [[0, 1, 2], [2, 1, 3]]; +// +// // Optional index buffer "lines" enabled in constructor +// mesh.lines = [[0, 1], [0, 2], [1, 3], [2, 3]]; +// +// // Upload provided data to GPU memory +// mesh.compile(); + +// ### new GL.Indexer() +// +// Generates indices into a list of unique objects from a stream of objects +// that may contain duplicates. This is useful for generating compact indexed +// meshes from unindexed data. +function Indexer() { + this.unique = []; + this.indices = []; + this.map = {}; +} + +Indexer.prototype = { + // ### .add(v) + // + // Adds the object `obj` to `unique` if it hasn't already been added. Returns + // the index of `obj` in `unique`. + add: function(obj) { + var key = JSON.stringify(obj); + if (!(key in this.map)) { + this.map[key] = this.unique.length; + this.unique.push(obj); + } + return this.map[key]; + } +}; + +// ### new GL.Buffer(target, type) +// +// Provides a simple method of uploading data to a GPU buffer. Example usage: +// +// var vertices = new GL.Buffer(gl.ARRAY_BUFFER, Float32Array); +// var indices = new GL.Buffer(gl.ELEMENT_ARRAY_BUFFER, Uint16Array); +// vertices.data = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]; +// indices.data = [[0, 1, 2], [2, 1, 3]]; +// vertices.compile(); +// indices.compile(); +// +function Buffer(target, type) { + this.buffer = null; + this.target = target; + this.type = type; + this.data = []; +} + +Buffer.prototype = { + // ### .compile(type) + // + // Upload the contents of `data` to the GPU in preparation for rendering. The + // data must be a list of lists where each inner list has the same length. For + // example, each element of data for vertex normals would be a list of length three. + // This will remember the data length and element length for later use by shaders. + // The type can be either `gl.STATIC_DRAW` or `gl.DYNAMIC_DRAW`, and defaults to + // `gl.STATIC_DRAW`. + // + // This could have used `[].concat.apply([], this.data)` to flatten + // the array but Google Chrome has a maximum number of arguments so the + // concatenations are chunked to avoid that limit. + compile: function(type) { + var data = []; + for (var i = 0, chunk = 10000; i < this.data.length; i += chunk) { + data = Array.prototype.concat.apply(data, this.data.slice(i, i + chunk)); + } + var spacing = this.data.length ? data.length / this.data.length : 0; + if (spacing != Math.round(spacing)) throw new Error('buffer elements not of consistent size, average size is ' + spacing); + this.buffer = this.buffer || gl.createBuffer(); + this.buffer.length = data.length; + this.buffer.spacing = spacing; + gl.bindBuffer(this.target, this.buffer); + gl.bufferData(this.target, new this.type(data), type || gl.STATIC_DRAW); + } +}; + +// ### new GL.Mesh([options]) +// +// Represents a collection of vertex buffers and index buffers. Each vertex +// buffer maps to one attribute in GLSL and has a corresponding property set +// on the Mesh instance. There is one vertex buffer by default: `vertices`, +// which maps to `gl_Vertex`. The `coords`, `normals`, and `colors` vertex +// buffers map to `gl_TexCoord`, `gl_Normal`, and `gl_Color` respectively, +// and can be enabled by setting the corresponding options to true. There are +// two index buffers, `triangles` and `lines`, which are used for rendering +// `gl.TRIANGLES` and `gl.LINES`, respectively. Only `triangles` is enabled by +// default, although `computeWireframe()` will add a normal buffer if it wasn't +// initially enabled. +function Mesh(options) { + options = options || {}; + this.vertexBuffers = {}; + this.indexBuffers = {}; + this.addVertexBuffer('vertices', 'gl_Vertex'); + if (options.coords) this.addVertexBuffer('coords', 'gl_TexCoord'); + if (options.normals) this.addVertexBuffer('normals', 'gl_Normal'); + if (options.colors) this.addVertexBuffer('colors', 'gl_Color'); + if (!('triangles' in options) || options.triangles) this.addIndexBuffer('triangles'); + if (options.lines) this.addIndexBuffer('lines'); +} + +Mesh.prototype = { + // ### .addVertexBuffer(name, attribute) + // + // Add a new vertex buffer with a list as a property called `name` on this object + // and map it to the attribute called `attribute` in all shaders that draw this mesh. + addVertexBuffer: function(name, attribute) { + var buffer = this.vertexBuffers[attribute] = new Buffer(gl.ARRAY_BUFFER, Float32Array); + buffer.name = name; + this[name] = []; + }, + + // ### .addIndexBuffer(name) + // + // Add a new index buffer with a list as a property called `name` on this object. + addIndexBuffer: function(name) { + var buffer = this.indexBuffers[name] = new Buffer(gl.ELEMENT_ARRAY_BUFFER, Uint16Array); + this[name] = []; + }, + + // ### .compile() + // + // Upload all attached buffers to the GPU in preparation for rendering. This + // doesn't need to be called every frame, only needs to be done when the data + // changes. + compile: function() { + for (var attribute in this.vertexBuffers) { + var buffer = this.vertexBuffers[attribute]; + buffer.data = this[buffer.name]; + buffer.compile(); + } + + for (var name in this.indexBuffers) { + var buffer = this.indexBuffers[name]; + buffer.data = this[name]; + buffer.compile(); + } + }, + + // ### .transform(matrix) + // + // Transform all vertices by `matrix` and all normals by the inverse transpose + // of `matrix`. + transform: function(matrix) { + this.vertices = this.vertices.map(function(v) { + return matrix.transformPoint(Vector.fromArray(v)).toArray(); + }); + if (this.normals) { + var invTrans = matrix.inverse().transpose(); + this.normals = this.normals.map(function(n) { + return invTrans.transformVector(Vector.fromArray(n)).unit().toArray(); + }); + } + this.compile(); + return this; + }, + + // ### .computeNormals() + // + // Computes a new normal for each vertex from the average normal of the + // neighboring triangles. This means adjacent triangles must share vertices + // for the resulting normals to be smooth. + computeNormals: function() { + if (!this.normals) this.addVertexBuffer('normals', 'gl_Normal'); + for (var i = 0; i < this.vertices.length; i++) { + this.normals[i] = new Vector(); + } + for (var i = 0; i < this.triangles.length; i++) { + var t = this.triangles[i]; + var a = Vector.fromArray(this.vertices[t[0]]); + var b = Vector.fromArray(this.vertices[t[1]]); + var c = Vector.fromArray(this.vertices[t[2]]); + var normal = b.subtract(a).cross(c.subtract(a)).unit(); + this.normals[t[0]] = this.normals[t[0]].add(normal); + this.normals[t[1]] = this.normals[t[1]].add(normal); + this.normals[t[2]] = this.normals[t[2]].add(normal); + } + for (var i = 0; i < this.vertices.length; i++) { + this.normals[i] = this.normals[i].unit().toArray(); + } + this.compile(); + return this; + }, + + // ### .computeWireframe() + // + // Populate the `lines` index buffer from the `triangles` index buffer. + computeWireframe: function() { + var indexer = new Indexer(); + for (var i = 0; i < this.triangles.length; i++) { + var t = this.triangles[i]; + for (var j = 0; j < t.length; j++) { + var a = t[j], b = t[(j + 1) % t.length]; + indexer.add([Math.min(a, b), Math.max(a, b)]); + } + } + if (!this.lines) this.addIndexBuffer('lines'); + this.lines = indexer.unique; + this.compile(); + return this; + }, + + // ### .getAABB() + // + // Computes the axis-aligned bounding box, which is an object whose `min` and + // `max` properties contain the minimum and maximum coordinates of all vertices. + getAABB: function() { + var aabb = { min: new Vector(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE) }; + aabb.max = aabb.min.negative(); + for (var i = 0; i < this.vertices.length; i++) { + var v = Vector.fromArray(this.vertices[i]); + aabb.min = Vector.min(aabb.min, v); + aabb.max = Vector.max(aabb.max, v); + } + return aabb; + }, + + // ### .getBoundingSphere() + // + // Computes a sphere that contains all vertices (not necessarily the smallest + // sphere). The returned object has two properties, `center` and `radius`. + getBoundingSphere: function() { + var aabb = this.getAABB(); + var sphere = { center: aabb.min.add(aabb.max).divide(2), radius: 0 }; + for (var i = 0; i < this.vertices.length; i++) { + sphere.radius = Math.max(sphere.radius, + Vector.fromArray(this.vertices[i]).subtract(sphere.center).length()); + } + return sphere; + } +}; + +// ### GL.Mesh.plane([options]) +// +// Generates a square 2x2 mesh the xy plane centered at the origin. The +// `options` argument specifies options to pass to the mesh constructor. +// Additional options include `detailX` and `detailY`, which set the tesselation +// in x and y, and `detail`, which sets both `detailX` and `detailY` at once. +// Two triangles are generated by default. +// Example usage: +// +// var mesh1 = GL.Mesh.plane(); +// var mesh2 = GL.Mesh.plane({ detail: 5 }); +// var mesh3 = GL.Mesh.plane({ detailX: 20, detailY: 40 }); +// +Mesh.plane = function(options) { + options = options || {}; + var mesh = new Mesh(options); + detailX = options.detailX || options.detail || 1; + detailY = options.detailY || options.detail || 1; + + for (var y = 0; y <= detailY; y++) { + var t = y / detailY; + for (var x = 0; x <= detailX; x++) { + var s = x / detailX; + mesh.vertices.push([2 * s - 1, 2 * t - 1, 0]); + if (mesh.coords) mesh.coords.push([s, t]); + if (mesh.normals) mesh.normals.push([0, 0, 1]); + if (x < detailX && y < detailY) { + var i = x + y * (detailX + 1); + mesh.triangles.push([i, i + 1, i + detailX + 1]); + mesh.triangles.push([i + detailX + 1, i + 1, i + detailX + 2]); + } + } + } + + mesh.compile(); + return mesh; +}; + +var cubeData = [ + [0, 4, 2, 6, -1, 0, 0], // -x + [1, 3, 5, 7, +1, 0, 0], // +x + [0, 1, 4, 5, 0, -1, 0], // -y + [2, 6, 3, 7, 0, +1, 0], // +y + [0, 2, 1, 3, 0, 0, -1], // -z + [4, 5, 6, 7, 0, 0, +1] // +z +]; + +function pickOctant(i) { + return new Vector((i & 1) * 2 - 1, (i & 2) - 1, (i & 4) / 2 - 1); +} + +// ### GL.Mesh.cube([options]) +// +// Generates a 2x2x2 box centered at the origin. The `options` argument +// specifies options to pass to the mesh constructor. +Mesh.cube = function(options) { + var mesh = new Mesh(options); + + for (var i = 0; i < cubeData.length; i++) { + var data = cubeData[i], v = i * 4; + for (var j = 0; j < 4; j++) { + var d = data[j]; + mesh.vertices.push(pickOctant(d).toArray()); + if (mesh.coords) mesh.coords.push([j & 1, (j & 2) / 2]); + if (mesh.normals) mesh.normals.push(data.slice(4, 7)); + } + mesh.triangles.push([v, v + 1, v + 2]); + mesh.triangles.push([v + 2, v + 1, v + 3]); + } + + mesh.compile(); + return mesh; +}; + +// ### GL.Mesh.sphere([options]) +// +// Generates a geodesic sphere of radius 1. The `options` argument specifies +// options to pass to the mesh constructor in addition to the `detail` option, +// which controls the tesselation level. The detail is `6` by default. +// Example usage: +// +// var mesh1 = GL.Mesh.sphere(); +// var mesh2 = GL.Mesh.sphere({ detail: 2 }); +// +Mesh.sphere = function(options) { + function tri(a, b, c) { return flip ? [a, c, b] : [a, b, c]; } + function fix(x) { return x + (x - x * x) / 2; } + options = options || {}; + var mesh = new Mesh(options); + var indexer = new Indexer(); + detail = options.detail || 6; + + for (var octant = 0; octant < 8; octant++) { + var scale = pickOctant(octant); + var flip = scale.x * scale.y * scale.z > 0; + var data = []; + for (var i = 0; i <= detail; i++) { + // Generate a row of vertices on the surface of the sphere + // using barycentric coordinates. + for (var j = 0; i + j <= detail; j++) { + var a = i / detail; + var b = j / detail; + var c = (detail - i - j) / detail; + var vertex = { vertex: new Vector(fix(a), fix(b), fix(c)).unit().multiply(scale).toArray() }; + if (mesh.coords) vertex.coord = scale.y > 0 ? [1 - a, c] : [c, 1 - a]; + data.push(indexer.add(vertex)); + } + + // Generate triangles from this row and the previous row. + if (i > 0) { + for (var j = 0; i + j <= detail; j++) { + var a = (i - 1) * (detail + 1) + ((i - 1) - (i - 1) * (i - 1)) / 2 + j; + var b = i * (detail + 1) + (i - i * i) / 2 + j; + mesh.triangles.push(tri(data[a], data[a + 1], data[b])); + if (i + j < detail) { + mesh.triangles.push(tri(data[b], data[a + 1], data[b + 1])); + } + } + } + } + } + + // Reconstruct the geometry from the indexer. + mesh.vertices = indexer.unique.map(function(v) { return v.vertex; }); + if (mesh.coords) mesh.coords = indexer.unique.map(function(v) { return v.coord; }); + if (mesh.normals) mesh.normals = mesh.vertices; + mesh.compile(); + return mesh; +}; + +// ### GL.Mesh.load(json[, options]) +// +// Creates a mesh from the JSON generated by the `convert/convert.py` script. +// Example usage: +// +// var data = { +// vertices: [[0, 0, 0], [1, 0, 0], [0, 1, 0]], +// triangles: [[0, 1, 2]] +// }; +// var mesh = GL.Mesh.load(data); +// +Mesh.load = function(json, options) { + options = options || {}; + if (!('coords' in options)) options.coords = !!json.coords; + if (!('normals' in options)) options.normals = !!json.normals; + if (!('colors' in options)) options.colors = !!json.colors; + if (!('triangles' in options)) options.triangles = !!json.triangles; + if (!('lines' in options)) options.lines = !!json.lines; + var mesh = new Mesh(options); + mesh.vertices = json.vertices; + if (mesh.coords) mesh.coords = json.coords; + if (mesh.normals) mesh.normals = json.normals; + if (mesh.colors) mesh.colors = json.colors; + if (mesh.triangles) mesh.triangles = json.triangles; + if (mesh.lines) mesh.lines = json.lines; + mesh.compile(); + return mesh; +}; + +// src/matrix.js +// Represents a 4x4 matrix stored in row-major order that uses Float32Arrays +// when available. Matrix operations can either be done using convenient +// methods that return a new matrix for the result or optimized methods +// that store the result in an existing matrix to avoid generating garbage. + +var hasFloat32Array = (typeof Float32Array != 'undefined'); + +// ### new GL.Matrix([elements]) +// +// This constructor takes 16 arguments in row-major order, which can be passed +// individually, as a list, or even as four lists, one for each row. If the +// arguments are omitted then the identity matrix is constructed instead. +function Matrix() { + var m = Array.prototype.concat.apply([], arguments); + if (!m.length) { + m = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]; + } + this.m = hasFloat32Array ? new Float32Array(m) : m; +} + +Matrix.prototype = { + // ### .inverse() + // + // Returns the matrix that when multiplied with this matrix results in the + // identity matrix. + inverse: function() { + return Matrix.inverse(this, new Matrix()); + }, + + // ### .transpose() + // + // Returns this matrix, exchanging columns for rows. + transpose: function() { + return Matrix.transpose(this, new Matrix()); + }, + + // ### .multiply(matrix) + // + // Returns the concatenation of the transforms for this matrix and `matrix`. + // This emulates the OpenGL function `glMultMatrix()`. + multiply: function(matrix) { + return Matrix.multiply(this, matrix, new Matrix()); + }, + + // ### .transformPoint(point) + // + // Transforms the vector as a point with a w coordinate of 1. This + // means translations will have an effect, for example. + transformPoint: function(v) { + var m = this.m; + return new Vector( + m[0] * v.x + m[1] * v.y + m[2] * v.z + m[3], + m[4] * v.x + m[5] * v.y + m[6] * v.z + m[7], + m[8] * v.x + m[9] * v.y + m[10] * v.z + m[11] + ).divide(m[12] * v.x + m[13] * v.y + m[14] * v.z + m[15]); + }, + + // ### .transformPoint(vector) + // + // Transforms the vector as a vector with a w coordinate of 0. This + // means translations will have no effect, for example. + transformVector: function(v) { + var m = this.m; + return new Vector( + m[0] * v.x + m[1] * v.y + m[2] * v.z, + m[4] * v.x + m[5] * v.y + m[6] * v.z, + m[8] * v.x + m[9] * v.y + m[10] * v.z + ); + } +}; + +// ### GL.Matrix.inverse(matrix[, result]) +// +// Returns the matrix that when multiplied with `matrix` results in the +// identity matrix. You can optionally pass an existing matrix in `result` +// to avoid allocating a new matrix. This implementation is from the Mesa +// OpenGL function `__gluInvertMatrixd()` found in `project.c`. +Matrix.inverse = function(matrix, result) { + result = result || new Matrix(); + var m = matrix.m, r = result.m; + + r[0] = m[5]*m[10]*m[15] - m[5]*m[14]*m[11] - m[6]*m[9]*m[15] + m[6]*m[13]*m[11] + m[7]*m[9]*m[14] - m[7]*m[13]*m[10]; + r[1] = -m[1]*m[10]*m[15] + m[1]*m[14]*m[11] + m[2]*m[9]*m[15] - m[2]*m[13]*m[11] - m[3]*m[9]*m[14] + m[3]*m[13]*m[10]; + r[2] = m[1]*m[6]*m[15] - m[1]*m[14]*m[7] - m[2]*m[5]*m[15] + m[2]*m[13]*m[7] + m[3]*m[5]*m[14] - m[3]*m[13]*m[6]; + r[3] = -m[1]*m[6]*m[11] + m[1]*m[10]*m[7] + m[2]*m[5]*m[11] - m[2]*m[9]*m[7] - m[3]*m[5]*m[10] + m[3]*m[9]*m[6]; + + r[4] = -m[4]*m[10]*m[15] + m[4]*m[14]*m[11] + m[6]*m[8]*m[15] - m[6]*m[12]*m[11] - m[7]*m[8]*m[14] + m[7]*m[12]*m[10]; + r[5] = m[0]*m[10]*m[15] - m[0]*m[14]*m[11] - m[2]*m[8]*m[15] + m[2]*m[12]*m[11] + m[3]*m[8]*m[14] - m[3]*m[12]*m[10]; + r[6] = -m[0]*m[6]*m[15] + m[0]*m[14]*m[7] + m[2]*m[4]*m[15] - m[2]*m[12]*m[7] - m[3]*m[4]*m[14] + m[3]*m[12]*m[6]; + r[7] = m[0]*m[6]*m[11] - m[0]*m[10]*m[7] - m[2]*m[4]*m[11] + m[2]*m[8]*m[7] + m[3]*m[4]*m[10] - m[3]*m[8]*m[6]; + + r[8] = m[4]*m[9]*m[15] - m[4]*m[13]*m[11] - m[5]*m[8]*m[15] + m[5]*m[12]*m[11] + m[7]*m[8]*m[13] - m[7]*m[12]*m[9]; + r[9] = -m[0]*m[9]*m[15] + m[0]*m[13]*m[11] + m[1]*m[8]*m[15] - m[1]*m[12]*m[11] - m[3]*m[8]*m[13] + m[3]*m[12]*m[9]; + r[10] = m[0]*m[5]*m[15] - m[0]*m[13]*m[7] - m[1]*m[4]*m[15] + m[1]*m[12]*m[7] + m[3]*m[4]*m[13] - m[3]*m[12]*m[5]; + r[11] = -m[0]*m[5]*m[11] + m[0]*m[9]*m[7] + m[1]*m[4]*m[11] - m[1]*m[8]*m[7] - m[3]*m[4]*m[9] + m[3]*m[8]*m[5]; + + r[12] = -m[4]*m[9]*m[14] + m[4]*m[13]*m[10] + m[5]*m[8]*m[14] - m[5]*m[12]*m[10] - m[6]*m[8]*m[13] + m[6]*m[12]*m[9]; + r[13] = m[0]*m[9]*m[14] - m[0]*m[13]*m[10] - m[1]*m[8]*m[14] + m[1]*m[12]*m[10] + m[2]*m[8]*m[13] - m[2]*m[12]*m[9]; + r[14] = -m[0]*m[5]*m[14] + m[0]*m[13]*m[6] + m[1]*m[4]*m[14] - m[1]*m[12]*m[6] - m[2]*m[4]*m[13] + m[2]*m[12]*m[5]; + r[15] = m[0]*m[5]*m[10] - m[0]*m[9]*m[6] - m[1]*m[4]*m[10] + m[1]*m[8]*m[6] + m[2]*m[4]*m[9] - m[2]*m[8]*m[5]; + + var det = m[0]*r[0] + m[1]*r[4] + m[2]*r[8] + m[3]*r[12]; + for (var i = 0; i < 16; i++) r[i] /= det; + return result; +}; + +// ### GL.Matrix.transpose(matrix[, result]) +// +// Returns `matrix`, exchanging columns for rows. You can optionally pass an +// existing matrix in `result` to avoid allocating a new matrix. +Matrix.transpose = function(matrix, result) { + result = result || new Matrix(); + var m = matrix.m, r = result.m; + r[0] = m[0]; r[1] = m[4]; r[2] = m[8]; r[3] = m[12]; + r[4] = m[1]; r[5] = m[5]; r[6] = m[9]; r[7] = m[13]; + r[8] = m[2]; r[9] = m[6]; r[10] = m[10]; r[11] = m[14]; + r[12] = m[3]; r[13] = m[7]; r[14] = m[11]; r[15] = m[15]; + return result; +}; + +// ### GL.Matrix.multiply(left, right[, result]) +// +// Returns the concatenation of the transforms for `left` and `right`. You can +// optionally pass an existing matrix in `result` to avoid allocating a new +// matrix. This emulates the OpenGL function `glMultMatrix()`. +Matrix.multiply = function(left, right, result) { + result = result || new Matrix(); + var a = left.m, b = right.m, r = result.m; + + r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; + r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; + r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; + r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; + + r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; + r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; + r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; + r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; + + r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; + r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; + r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; + r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; + + r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; + r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; + r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; + r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; + + return result; +}; + +// ### GL.Matrix.identity([result]) +// +// Returns an identity matrix. You can optionally pass an existing matrix in +// `result` to avoid allocating a new matrix. This emulates the OpenGL function +// `glLoadIdentity()`. +Matrix.identity = function(result) { + result = result || new Matrix(); + var m = result.m; + m[0] = m[5] = m[10] = m[15] = 1; + m[1] = m[2] = m[3] = m[4] = m[6] = m[7] = m[8] = m[9] = m[11] = m[12] = m[13] = m[14] = 0; + return result; +}; + +// ### GL.Matrix.perspective(fov, aspect, near, far[, result]) +// +// Returns a perspective transform matrix, which makes far away objects appear +// smaller than nearby objects. The `aspect` argument should be the width +// divided by the height of your viewport and `fov` is the top-to-bottom angle +// of the field of view in degrees. You can optionally pass an existing matrix +// in `result` to avoid allocating a new matrix. This emulates the OpenGL +// function `gluPerspective()`. +Matrix.perspective = function(fov, aspect, near, far, result) { + var y = Math.tan(fov * Math.PI / 360) * near; + var x = y * aspect; + return Matrix.frustum(-x, x, -y, y, near, far, result); +}; + +// ### GL.Matrix.frustum(left, right, bottom, top, near, far[, result]) +// +// Sets up a viewing frustum, which is shaped like a truncated pyramid with the +// camera where the point of the pyramid would be. You can optionally pass an +// existing matrix in `result` to avoid allocating a new matrix. This emulates +// the OpenGL function `glFrustum()`. +Matrix.frustum = function(l, r, b, t, n, f, result) { + result = result || new Matrix(); + var m = result.m; + + m[0] = 2 * n / (r - l); + m[1] = 0; + m[2] = (r + l) / (r - l); + m[3] = 0; + + m[4] = 0; + m[5] = 2 * n / (t - b); + m[6] = (t + b) / (t - b); + m[7] = 0; + + m[8] = 0; + m[9] = 0; + m[10] = -(f + n) / (f - n); + m[11] = -2 * f * n / (f - n); + + m[12] = 0; + m[13] = 0; + m[14] = -1; + m[15] = 0; + + return result; +}; + +// ### GL.Matrix.ortho(left, right, bottom, top, near, far[, result]) +// +// Returns an orthographic projection, in which objects are the same size no +// matter how far away or nearby they are. You can optionally pass an existing +// matrix in `result` to avoid allocating a new matrix. This emulates the OpenGL +// function `glOrtho()`. +Matrix.ortho = function(l, r, b, t, n, f, result) { + result = result || new Matrix(); + var m = result.m; + + m[0] = 2 / (r - l); + m[1] = 0; + m[2] = 0; + m[3] = -(r + l) / (r - l); + + m[4] = 0; + m[5] = 2 / (t - b); + m[6] = 0; + m[7] = -(t + b) / (t - b); + + m[8] = 0; + m[9] = 0; + m[10] = -2 / (f - n); + m[11] = -(f + n) / (f - n); + + m[12] = 0; + m[13] = 0; + m[14] = 0; + m[15] = 1; + + return result; +}; + +// ### GL.Matrix.scale(x, y, z[, result]) +// +// This emulates the OpenGL function `glScale()`. You can optionally pass an +// existing matrix in `result` to avoid allocating a new matrix. +Matrix.scale = function(x, y, z, result) { + result = result || new Matrix(); + var m = result.m; + + m[0] = x; + m[1] = 0; + m[2] = 0; + m[3] = 0; + + m[4] = 0; + m[5] = y; + m[6] = 0; + m[7] = 0; + + m[8] = 0; + m[9] = 0; + m[10] = z; + m[11] = 0; + + m[12] = 0; + m[13] = 0; + m[14] = 0; + m[15] = 1; + + return result; +}; + +// ### GL.Matrix.translate(x, y, z[, result]) +// +// This emulates the OpenGL function `glTranslate()`. You can optionally pass +// an existing matrix in `result` to avoid allocating a new matrix. +Matrix.translate = function(x, y, z, result) { + result = result || new Matrix(); + var m = result.m; + + m[0] = 1; + m[1] = 0; + m[2] = 0; + m[3] = x; + + m[4] = 0; + m[5] = 1; + m[6] = 0; + m[7] = y; + + m[8] = 0; + m[9] = 0; + m[10] = 1; + m[11] = z; + + m[12] = 0; + m[13] = 0; + m[14] = 0; + m[15] = 1; + + return result; +}; + +// ### GL.Matrix.rotate(a, x, y, z[, result]) +// +// Returns a matrix that rotates by `a` degrees around the vector `x, y, z`. +// You can optionally pass an existing matrix in `result` to avoid allocating +// a new matrix. This emulates the OpenGL function `glRotate()`. +Matrix.rotate = function(a, x, y, z, result) { + if (!a || (!x && !y && !z)) { + return Matrix.identity(result); + } + + result = result || new Matrix(); + var m = result.m; + + var d = Math.sqrt(x*x + y*y + z*z); + a *= Math.PI / 180; x /= d; y /= d; z /= d; + var c = Math.cos(a), s = Math.sin(a), t = 1 - c; + + m[0] = x * x * t + c; + m[1] = x * y * t - z * s; + m[2] = x * z * t + y * s; + m[3] = 0; + + m[4] = y * x * t + z * s; + m[5] = y * y * t + c; + m[6] = y * z * t - x * s; + m[7] = 0; + + m[8] = z * x * t - y * s; + m[9] = z * y * t + x * s; + m[10] = z * z * t + c; + m[11] = 0; + + m[12] = 0; + m[13] = 0; + m[14] = 0; + m[15] = 1; + + return result; +}; + +// ### GL.Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz[, result]) +// +// Returns a matrix that puts the camera at the eye point `ex, ey, ez` looking +// toward the center point `cx, cy, cz` with an up direction of `ux, uy, uz`. +// You can optionally pass an existing matrix in `result` to avoid allocating +// a new matrix. This emulates the OpenGL function `gluLookAt()`. +Matrix.lookAt = function(ex, ey, ez, cx, cy, cz, ux, uy, uz, result) { + result = result || new Matrix(); + var m = result.m; + + var e = new Vector(ex, ey, ez); + var c = new Vector(cx, cy, cz); + var u = new Vector(ux, uy, uz); + var f = e.subtract(c).unit(); + var s = u.cross(f).unit(); + var t = f.cross(s).unit(); + + m[0] = s.x; + m[1] = s.y; + m[2] = s.z; + m[3] = -s.dot(e); + + m[4] = t.x; + m[5] = t.y; + m[6] = t.z; + m[7] = -t.dot(e); + + m[8] = f.x; + m[9] = f.y; + m[10] = f.z; + m[11] = -f.dot(e); + + m[12] = 0; + m[13] = 0; + m[14] = 0; + m[15] = 1; + + return result; +}; + +// src/vector.js +// Provides a simple 3D vector class. Vector operations can be done using member +// functions, which return new vectors, or static functions, which reuse +// existing vectors to avoid generating garbage. +function Vector(x, y, z) { + this.x = x || 0; + this.y = y || 0; + this.z = z || 0; +} + +// ### Instance Methods +// The methods `add()`, `subtract()`, `multiply()`, and `divide()` can all +// take either a vector or a number as an argument. +Vector.prototype = { + negative: function() { + return new Vector(-this.x, -this.y, -this.z); + }, + add: function(v) { + if (v instanceof Vector) return new Vector(this.x + v.x, this.y + v.y, this.z + v.z); + else return new Vector(this.x + v, this.y + v, this.z + v); + }, + subtract: function(v) { + if (v instanceof Vector) return new Vector(this.x - v.x, this.y - v.y, this.z - v.z); + else return new Vector(this.x - v, this.y - v, this.z - v); + }, + multiply: function(v) { + if (v instanceof Vector) return new Vector(this.x * v.x, this.y * v.y, this.z * v.z); + else return new Vector(this.x * v, this.y * v, this.z * v); + }, + divide: function(v) { + if (v instanceof Vector) return new Vector(this.x / v.x, this.y / v.y, this.z / v.z); + else return new Vector(this.x / v, this.y / v, this.z / v); + }, + equals: function(v) { + return this.x == v.x && this.y == v.y && this.z == v.z; + }, + dot: function(v) { + return this.x * v.x + this.y * v.y + this.z * v.z; + }, + cross: function(v) { + return new Vector( + this.y * v.z - this.z * v.y, + this.z * v.x - this.x * v.z, + this.x * v.y - this.y * v.x + ); + }, + length: function() { + return Math.sqrt(this.dot(this)); + }, + unit: function() { + return this.divide(this.length()); + }, + min: function() { + return Math.min(Math.min(this.x, this.y), this.z); + }, + max: function() { + return Math.max(Math.max(this.x, this.y), this.z); + }, + toAngles: function() { + return { + theta: Math.atan2(this.z, this.x), + phi: Math.asin(this.y / this.length()) + }; + }, + angleTo: function(a) { + return Math.acos(this.dot(a) / (this.length() * a.length())); + }, + toArray: function(n) { + return [this.x, this.y, this.z].slice(0, n || 3); + }, + clone: function() { + return new Vector(this.x, this.y, this.z); + }, + init: function(x, y, z) { + this.x = x; this.y = y; this.z = z; + return this; + } +}; + +// ### Static Methods +// `Vector.randomDirection()` returns a vector with a length of 1 and a +// statistically uniform direction. `Vector.lerp()` performs linear +// interpolation between two vectors. +Vector.negative = function(a, b) { + b.x = -a.x; b.y = -a.y; b.z = -a.z; + return b; +}; +Vector.add = function(a, b, c) { + if (b instanceof Vector) { c.x = a.x + b.x; c.y = a.y + b.y; c.z = a.z + b.z; } + else { c.x = a.x + b; c.y = a.y + b; c.z = a.z + b; } + return c; +}; +Vector.subtract = function(a, b, c) { + if (b instanceof Vector) { c.x = a.x - b.x; c.y = a.y - b.y; c.z = a.z - b.z; } + else { c.x = a.x - b; c.y = a.y - b; c.z = a.z - b; } + return c; +}; +Vector.multiply = function(a, b, c) { + if (b instanceof Vector) { c.x = a.x * b.x; c.y = a.y * b.y; c.z = a.z * b.z; } + else { c.x = a.x * b; c.y = a.y * b; c.z = a.z * b; } + return c; +}; +Vector.divide = function(a, b, c) { + if (b instanceof Vector) { c.x = a.x / b.x; c.y = a.y / b.y; c.z = a.z / b.z; } + else { c.x = a.x / b; c.y = a.y / b; c.z = a.z / b; } + return c; +}; +Vector.cross = function(a, b, c) { + c.x = a.y * b.z - a.z * b.y; + c.y = a.z * b.x - a.x * b.z; + c.z = a.x * b.y - a.y * b.x; + return c; +}; +Vector.unit = function(a, b) { + var length = a.length(); + b.x = a.x / length; + b.y = a.y / length; + b.z = a.z / length; + return b; +}; +Vector.fromAngles = function(theta, phi) { + return new Vector(Math.cos(theta) * Math.cos(phi), Math.sin(phi), Math.sin(theta) * Math.cos(phi)); +}; +Vector.randomDirection = function() { + return Vector.fromAngles(Math.random() * Math.PI * 2, Math.asin(Math.random() * 2 - 1)); +}; +Vector.min = function(a, b) { + return new Vector(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.min(a.z, b.z)); +}; +Vector.max = function(a, b) { + return new Vector(Math.max(a.x, b.x), Math.max(a.y, b.y), Math.max(a.z, b.z)); +}; +Vector.lerp = function(a, b, fraction) { + return b.subtract(a).multiply(fraction).add(a); +}; +Vector.fromArray = function(a) { + return new Vector(a[0], a[1], a[2]); +}; +Vector.angleBetween = function(a, b) { + return a.angleTo(b); +}; + +// src/shader.js +// Provides a convenient wrapper for WebGL shaders. A few uniforms and attributes, +// prefixed with `gl_`, are automatically added to all shader sources to make +// simple shaders easier to write. +// +// Example usage: +// +// var shader = new GL.Shader('\ +// void main() {\ +// gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\ +// }\ +// ', '\ +// uniform vec4 color;\ +// void main() {\ +// gl_FragColor = color;\ +// }\ +// '); +// +// shader.uniforms({ +// color: [1, 0, 0, 1] +// }).draw(mesh); + +function regexMap(regex, text, callback) { + while ((result = regex.exec(text)) != null) { + callback(result); + } +} + +// Non-standard names beginning with `gl_` must be mangled because they will +// otherwise cause a compiler error. +var LIGHTGL_PREFIX = 'LIGHTGL'; + +// ### new GL.Shader(vertexSource, fragmentSource) +// +// Compiles a shader program using the provided vertex and fragment shaders. +function Shader(vertexSource, fragmentSource) { + // Allow passing in the id of an HTML script tag with the source + function followScriptTagById(id) { + var element = document.getElementById(id); + return element ? element.text : id; + } + vertexSource = followScriptTagById(vertexSource); + fragmentSource = followScriptTagById(fragmentSource); + + // Headers are prepended to the sources to provide some automatic functionality. + var header = '\ + uniform mat3 gl_NormalMatrix;\ + uniform mat4 gl_ModelViewMatrix;\ + uniform mat4 gl_ProjectionMatrix;\ + uniform mat4 gl_ModelViewProjectionMatrix;\ + uniform mat4 gl_ModelViewMatrixInverse;\ + uniform mat4 gl_ProjectionMatrixInverse;\ + uniform mat4 gl_ModelViewProjectionMatrixInverse;\ + '; + var vertexHeader = header + '\ + attribute vec4 gl_Vertex;\ + attribute vec4 gl_TexCoord;\ + attribute vec3 gl_Normal;\ + attribute vec4 gl_Color;\ + vec4 ftransform() {\ + return gl_ModelViewProjectionMatrix * gl_Vertex;\ + }\ + '; + var fragmentHeader = '\ + precision highp float;\ + ' + header; + + // Check for the use of built-in matrices that require expensive matrix + // multiplications to compute, and record these in `usedMatrices`. + var source = vertexSource + fragmentSource; + var usedMatrices = {}; + regexMap(/\b(gl_[^;]*)\b;/g, header, function(groups) { + var name = groups[1]; + if (source.indexOf(name) != -1) { + var capitalLetters = name.replace(/[a-z_]/g, ''); + usedMatrices[capitalLetters] = LIGHTGL_PREFIX + name; + } + }); + if (source.indexOf('ftransform') != -1) usedMatrices.MVPM = LIGHTGL_PREFIX + 'gl_ModelViewProjectionMatrix'; + this.usedMatrices = usedMatrices; + + // The `gl_` prefix must be substituted for something else to avoid compile + // errors, since it's a reserved prefix. This prefixes all reserved names with + // `_`. The header is inserted after any extensions, since those must come + // first. + function fix(header, source) { + var replaced = {}; + var match = /^((\s*\/\/.*\n|\s*#extension.*\n)+)[^]*$/.exec(source); + source = match ? match[1] + header + source.substr(match[1].length) : header + source; + regexMap(/\bgl_\w+\b/g, header, function(result) { + if (!(result in replaced)) { + source = source.replace(new RegExp('\\b' + result + '\\b', 'g'), LIGHTGL_PREFIX + result); + replaced[result] = true; + } + }); + return source; + } + vertexSource = fix(vertexHeader, vertexSource); + fragmentSource = fix(fragmentHeader, fragmentSource); + + // Compile and link errors are thrown as strings. + function compileSource(type, source) { + var shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error('compile error: ' + gl.getShaderInfoLog(shader)); + } + return shader; + } + this.program = gl.createProgram(); + gl.attachShader(this.program, compileSource(gl.VERTEX_SHADER, vertexSource)); + gl.attachShader(this.program, compileSource(gl.FRAGMENT_SHADER, fragmentSource)); + gl.linkProgram(this.program); + if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { + throw new Error('link error: ' + gl.getProgramInfoLog(this.program)); + } + this.attributes = {}; + this.uniformLocations = {}; + + // Sampler uniforms need to be uploaded using `gl.uniform1i()` instead of `gl.uniform1f()`. + // To do this automatically, we detect and remember all uniform samplers in the source code. + var isSampler = {}; + regexMap(/uniform\s+sampler(1D|2D|3D|Cube)\s+(\w+)\s*;/g, vertexSource + fragmentSource, function(groups) { + isSampler[groups[2]] = 1; + }); + this.isSampler = isSampler; +} + +function isArray(obj) { + var str = Object.prototype.toString.call(obj); + return str == '[object Array]' || str == '[object Float32Array]'; +} + +function isNumber(obj) { + var str = Object.prototype.toString.call(obj); + return str == '[object Number]' || str == '[object Boolean]'; +} + +var tempMatrix = new Matrix(); +var resultMatrix = new Matrix(); + +Shader.prototype = { + // ### .uniforms(uniforms) + // + // Set a uniform for each property of `uniforms`. The correct `gl.uniform*()` method is + // inferred from the value types and from the stored uniform sampler flags. + uniforms: function(uniforms) { + gl.useProgram(this.program); + + for (var name in uniforms) { + var location = this.uniformLocations[name] || gl.getUniformLocation(this.program, name); + if (!location) continue; + this.uniformLocations[name] = location; + var value = uniforms[name]; + if (value instanceof Vector) { + value = [value.x, value.y, value.z]; + } else if (value instanceof Matrix) { + value = value.m; + } + if (isArray(value)) { + switch (value.length) { + case 1: gl.uniform1fv(location, new Float32Array(value)); break; + case 2: gl.uniform2fv(location, new Float32Array(value)); break; + case 3: gl.uniform3fv(location, new Float32Array(value)); break; + case 4: gl.uniform4fv(location, new Float32Array(value)); break; + // Matrices are automatically transposed, since WebGL uses column-major + // indices instead of row-major indices. + case 9: gl.uniformMatrix3fv(location, false, new Float32Array([ + value[0], value[3], value[6], + value[1], value[4], value[7], + value[2], value[5], value[8] + ])); break; + case 16: gl.uniformMatrix4fv(location, false, new Float32Array([ + value[0], value[4], value[8], value[12], + value[1], value[5], value[9], value[13], + value[2], value[6], value[10], value[14], + value[3], value[7], value[11], value[15] + ])); break; + default: throw new Error('don\'t know how to load uniform "' + name + '" of length ' + value.length); + } + } else if (isNumber(value)) { + (this.isSampler[name] ? gl.uniform1i : gl.uniform1f).call(gl, location, value); + } else { + throw new Error('attempted to set uniform "' + name + '" to invalid value ' + value); + } + } + + return this; + }, + + // ### .draw(mesh[, mode]) + // + // Sets all uniform matrix attributes, binds all relevant buffers, and draws the + // mesh geometry as indexed triangles or indexed lines. Set `mode` to `gl.LINES` + // (and either add indices to `lines` or call `computeWireframe()`) to draw the + // mesh in wireframe. + draw: function(mesh, mode) { + this.drawBuffers(mesh.vertexBuffers, + mesh.indexBuffers[mode == gl.LINES ? 'lines' : 'triangles'], + arguments.length < 2 ? gl.TRIANGLES : mode); + }, + + // ### .drawBuffers(vertexBuffers, indexBuffer, mode) + // + // Sets all uniform matrix attributes, binds all relevant buffers, and draws the + // indexed mesh geometry. The `vertexBuffers` argument is a map from attribute + // names to `Buffer` objects of type `gl.ARRAY_BUFFER`, `indexBuffer` is a `Buffer` + // object of type `gl.ELEMENT_ARRAY_BUFFER`, and `mode` is a WebGL primitive mode + // like `gl.TRIANGLES` or `gl.LINES`. This method automatically creates and caches + // vertex attribute pointers for attributes as needed. + drawBuffers: function(vertexBuffers, indexBuffer, mode) { + // Only construct up the built-in matrices we need for this shader. + var used = this.usedMatrices; + var MVM = gl.modelviewMatrix; + var PM = gl.projectionMatrix; + var MVMI = (used.MVMI || used.NM) ? MVM.inverse() : null; + var PMI = (used.PMI) ? PM.inverse() : null; + var MVPM = (used.MVPM || used.MVPMI) ? PM.multiply(MVM) : null; + var matrices = {}; + if (used.MVM) matrices[used.MVM] = MVM; + if (used.MVMI) matrices[used.MVMI] = MVMI; + if (used.PM) matrices[used.PM] = PM; + if (used.PMI) matrices[used.PMI] = PMI; + if (used.MVPM) matrices[used.MVPM] = MVPM; + if (used.MVPMI) matrices[used.MVPMI] = MVPM.inverse(); + if (used.NM) { + var m = MVMI.m; + matrices[used.NM] = [m[0], m[4], m[8], m[1], m[5], m[9], m[2], m[6], m[10]]; + } + this.uniforms(matrices); + + // Create and enable attribute pointers as necessary. + var length = 0; + for (var attribute in vertexBuffers) { + var buffer = vertexBuffers[attribute]; + var location = this.attributes[attribute] || + gl.getAttribLocation(this.program, attribute.replace(/^(gl_.*)$/, LIGHTGL_PREFIX + '$1')); + if (location == -1 || !buffer.buffer) continue; + this.attributes[attribute] = location; + gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer); + gl.enableVertexAttribArray(location); + gl.vertexAttribPointer(location, buffer.buffer.spacing, gl.FLOAT, false, 0, 0); + length = buffer.buffer.length / buffer.buffer.spacing; + } + + // Disable unused attribute pointers. + for (var attribute in this.attributes) { + if (!(attribute in vertexBuffers)) { + gl.disableVertexAttribArray(this.attributes[attribute]); + } + } + + // Draw the geometry. + if (length && (!indexBuffer || indexBuffer.buffer)) { + if (indexBuffer) { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer.buffer); + gl.drawElements(mode, indexBuffer.buffer.length, gl.UNSIGNED_SHORT, 0); + } else { + gl.drawArrays(mode, 0, length); + } + } + + return this; + } +}; + +// src/main.js +// The internal `gl` variable holds the current WebGL context. +var gl; + +var GL = { + // ### Initialization + // + // `GL.create()` creates a new WebGL context and augments it with more + // methods. The alpha channel is disabled by default because it usually causes + // unintended transparencies in the canvas. + create: function(options) { + options = options || {}; + var canvas = document.createElement('canvas'); + canvas.width = 800; + canvas.height = 600; + if (!('alpha' in options)) options.alpha = false; + try { gl = canvas.getContext('webgl', options); } catch (e) {} + try { gl = gl || canvas.getContext('experimental-webgl', options); } catch (e) {} + if (!gl) throw new Error('WebGL not supported'); + gl.HALF_FLOAT_OES = 0x8D61; + addMatrixStack(); + addImmediateMode(); + addEventListeners(); + addOtherMethods(); + return gl; + }, + + // `GL.keys` contains a mapping of key codes to booleans indicating whether + // that key is currently pressed. + keys: {}, + + // Export all external classes. + Matrix: Matrix, + Indexer: Indexer, + Buffer: Buffer, + Mesh: Mesh, + HitTest: HitTest, + Raytracer: Raytracer, + Shader: Shader, + Texture: Texture, + Vector: Vector +}; + +// ### Matrix stack +// +// Implement the OpenGL modelview and projection matrix stacks, along with some +// other useful GLU matrix functions. + +function addMatrixStack() { + gl.MODELVIEW = ENUM | 1; + gl.PROJECTION = ENUM | 2; + var tempMatrix = new Matrix(); + var resultMatrix = new Matrix(); + gl.modelviewMatrix = new Matrix(); + gl.projectionMatrix = new Matrix(); + var modelviewStack = []; + var projectionStack = []; + var matrix, stack; + gl.matrixMode = function(mode) { + switch (mode) { + case gl.MODELVIEW: + matrix = 'modelviewMatrix'; + stack = modelviewStack; + break; + case gl.PROJECTION: + matrix = 'projectionMatrix'; + stack = projectionStack; + break; + default: + throw new Error('invalid matrix mode ' + mode); + } + }; + gl.loadIdentity = function() { + Matrix.identity(gl[matrix]); + }; + gl.loadMatrix = function(m) { + var from = m.m, to = gl[matrix].m; + for (var i = 0; i < 16; i++) { + to[i] = from[i]; + } + }; + gl.multMatrix = function(m) { + gl.loadMatrix(Matrix.multiply(gl[matrix], m, resultMatrix)); + }; + gl.perspective = function(fov, aspect, near, far) { + gl.multMatrix(Matrix.perspective(fov, aspect, near, far, tempMatrix)); + }; + gl.frustum = function(l, r, b, t, n, f) { + gl.multMatrix(Matrix.frustum(l, r, b, t, n, f, tempMatrix)); + }; + gl.ortho = function(l, r, b, t, n, f) { + gl.multMatrix(Matrix.ortho(l, r, b, t, n, f, tempMatrix)); + }; + gl.scale = function(x, y, z) { + gl.multMatrix(Matrix.scale(x, y, z, tempMatrix)); + }; + gl.translate = function(x, y, z) { + gl.multMatrix(Matrix.translate(x, y, z, tempMatrix)); + }; + gl.rotate = function(a, x, y, z) { + gl.multMatrix(Matrix.rotate(a, x, y, z, tempMatrix)); + }; + gl.lookAt = function(ex, ey, ez, cx, cy, cz, ux, uy, uz) { + gl.multMatrix(Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz, tempMatrix)); + }; + gl.pushMatrix = function() { + stack.push(Array.prototype.slice.call(gl[matrix].m)); + }; + gl.popMatrix = function() { + var m = stack.pop(); + gl[matrix].m = hasFloat32Array ? new Float32Array(m) : m; + }; + gl.project = function(objX, objY, objZ, modelview, projection, viewport) { + modelview = modelview || gl.modelviewMatrix; + projection = projection || gl.projectionMatrix; + viewport = viewport || gl.getParameter(gl.VIEWPORT); + var point = projection.transformPoint(modelview.transformPoint(new Vector(objX, objY, objZ))); + return new Vector( + viewport[0] + viewport[2] * (point.x * 0.5 + 0.5), + viewport[1] + viewport[3] * (point.y * 0.5 + 0.5), + point.z * 0.5 + 0.5 + ); + }; + gl.unProject = function(winX, winY, winZ, modelview, projection, viewport) { + modelview = modelview || gl.modelviewMatrix; + projection = projection || gl.projectionMatrix; + viewport = viewport || gl.getParameter(gl.VIEWPORT); + var point = new Vector( + (winX - viewport[0]) / viewport[2] * 2 - 1, + (winY - viewport[1]) / viewport[3] * 2 - 1, + winZ * 2 - 1 + ); + return Matrix.inverse(Matrix.multiply(projection, modelview, tempMatrix), resultMatrix).transformPoint(point); + }; + gl.matrixMode(gl.MODELVIEW); +} + +// ### Immediate mode +// +// Provide an implementation of OpenGL's deprecated immediate mode. This is +// depricated for a reason: constantly re-specifying the geometry is a bad +// idea for performance. You should use a `GL.Mesh` instead, which specifies +// the geometry once and caches it on the graphics card. Still, nothing +// beats a quick `gl.begin(gl.POINTS); gl.vertex(1, 2, 3); gl.end();` for +// debugging. This intentionally doesn't implement fixed-function lighting +// because it's only meant for quick debugging tasks. + +function addImmediateMode() { + var immediateMode = { + mesh: new Mesh({ coords: true, colors: true, triangles: false }), + mode: -1, + coord: [0, 0, 0, 0], + color: [1, 1, 1, 1], + pointSize: 1, + shader: new Shader('\ + uniform float pointSize;\ + varying vec4 color;\ + varying vec4 coord;\ + void main() {\ + color = gl_Color;\ + coord = gl_TexCoord;\ + gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\ + gl_PointSize = pointSize;\ + }\ + ', '\ + uniform sampler2D texture;\ + uniform float pointSize;\ + uniform bool useTexture;\ + varying vec4 color;\ + varying vec4 coord;\ + void main() {\ + gl_FragColor = color;\ + if (useTexture) gl_FragColor *= texture2D(texture, coord.xy);\ + }\ + ') + }; + gl.pointSize = function(pointSize) { + immediateMode.shader.uniforms({ pointSize: pointSize }); + }; + gl.begin = function(mode) { + if (immediateMode.mode != -1) throw new Error('mismatched gl.begin() and gl.end() calls'); + immediateMode.mode = mode; + immediateMode.mesh.colors = []; + immediateMode.mesh.coords = []; + immediateMode.mesh.vertices = []; + }; + gl.color = function(r, g, b, a) { + immediateMode.color = (arguments.length == 1) ? r.toArray().concat(1) : [r, g, b, a || 1]; + }; + gl.texCoord = function(s, t) { + immediateMode.coord = (arguments.length == 1) ? s.toArray(2) : [s, t]; + }; + gl.vertex = function(x, y, z) { + immediateMode.mesh.colors.push(immediateMode.color); + immediateMode.mesh.coords.push(immediateMode.coord); + immediateMode.mesh.vertices.push(arguments.length == 1 ? x.toArray() : [x, y, z]); + }; + gl.end = function() { + if (immediateMode.mode == -1) throw new Error('mismatched gl.begin() and gl.end() calls'); + immediateMode.mesh.compile(); + immediateMode.shader.uniforms({ + useTexture: !!gl.getParameter(gl.TEXTURE_BINDING_2D) + }).draw(immediateMode.mesh, immediateMode.mode); + immediateMode.mode = -1; + }; +} + +// ### Improved mouse events +// +// This adds event listeners on the `gl.canvas` element that call +// `gl.onmousedown()`, `gl.onmousemove()`, and `gl.onmouseup()` with an +// augmented event object. The event object also has the properties `x`, `y`, +// `deltaX`, `deltaY`, and `dragging`. +function addEventListeners() { + var context = gl, oldX = 0, oldY = 0, buttons = {}, hasOld = false; + var has = Object.prototype.hasOwnProperty; + function isDragging() { + for (var b in buttons) { + if (has.call(buttons, b) && buttons[b]) return true; + } + return false; + } + function augment(original) { + // Make a copy of original, a native `MouseEvent`, so we can overwrite + // WebKit's non-standard read-only `x` and `y` properties (which are just + // duplicates of `pageX` and `pageY`). We can't just use + // `Object.create(original)` because some `MouseEvent` functions must be + // called in the context of the original event object. + var e = {}; + for (var name in original) { + if (typeof original[name] == 'function') { + e[name] = (function(callback) { + return function() { + callback.apply(original, arguments); + }; + })(original[name]); + } else { + e[name] = original[name]; + } + } + e.original = original; + e.x = e.pageX; + e.y = e.pageY; + for (var obj = gl.canvas; obj; obj = obj.offsetParent) { + e.x -= obj.offsetLeft; + e.y -= obj.offsetTop; + } + if (hasOld) { + e.deltaX = e.x - oldX; + e.deltaY = e.y - oldY; + } else { + e.deltaX = 0; + e.deltaY = 0; + hasOld = true; + } + oldX = e.x; + oldY = e.y; + e.dragging = isDragging(); + e.preventDefault = function() { + e.original.preventDefault(); + }; + e.stopPropagation = function() { + e.original.stopPropagation(); + }; + return e; + } + function mousedown(e) { + gl = context; + if (!isDragging()) { + // Expand the event handlers to the document to handle dragging off canvas. + on(document, 'mousemove', mousemove); + on(document, 'mouseup', mouseup); + off(gl.canvas, 'mousemove', mousemove); + off(gl.canvas, 'mouseup', mouseup); + } + buttons[e.which] = true; + e = augment(e); + if (gl.onmousedown) gl.onmousedown(e); + e.preventDefault(); + } + function mousemove(e) { + gl = context; + e = augment(e); + if (gl.onmousemove) gl.onmousemove(e); + e.preventDefault(); + } + function mouseup(e) { + gl = context; + buttons[e.which] = false; + if (!isDragging()) { + // Shrink the event handlers back to the canvas when dragging ends. + off(document, 'mousemove', mousemove); + off(document, 'mouseup', mouseup); + on(gl.canvas, 'mousemove', mousemove); + on(gl.canvas, 'mouseup', mouseup); + } + e = augment(e); + if (gl.onmouseup) gl.onmouseup(e); + e.preventDefault(); + } + function reset() { + hasOld = false; + } + function resetAll() { + buttons = {}; + hasOld = false; + } + on(gl.canvas, 'mousedown', mousedown); + on(gl.canvas, 'mousemove', mousemove); + on(gl.canvas, 'mouseup', mouseup); + on(gl.canvas, 'mouseover', reset); + on(gl.canvas, 'mouseout', reset); + on(document, 'contextmenu', resetAll); +} + +// ### Automatic keyboard state +// +// The current keyboard state is stored in `GL.keys`, a map of integer key +// codes to booleans indicating whether that key is currently pressed. Certain +// keys also have named identifiers that can be used directly, such as +// `GL.keys.SPACE`. Values in `GL.keys` are initially undefined until that +// key is pressed for the first time. If you need a boolean value, you can +// cast the value to boolean by applying the not operator twice (as in +// `!!GL.keys.SPACE`). + +function mapKeyCode(code) { + var named = { + 8: 'BACKSPACE', + 9: 'TAB', + 13: 'ENTER', + 16: 'SHIFT', + 27: 'ESCAPE', + 32: 'SPACE', + 37: 'LEFT', + 38: 'UP', + 39: 'RIGHT', + 40: 'DOWN' + }; + return named[code] || (code >= 65 && code <= 90 ? String.fromCharCode(code) : null); +} + +function on(element, name, callback) { + element.addEventListener(name, callback); +} + +function off(element, name, callback) { + element.removeEventListener(name, callback); +} + +on(document, 'keydown', function(e) { + if (!e.altKey && !e.ctrlKey && !e.metaKey) { + var key = mapKeyCode(e.keyCode); + if (key) GL.keys[key] = true; + GL.keys[e.keyCode] = true; + } +}); + +on(document, 'keyup', function(e) { + if (!e.altKey && !e.ctrlKey && !e.metaKey) { + var key = mapKeyCode(e.keyCode); + if (key) GL.keys[key] = false; + GL.keys[e.keyCode] = false; + } +}); + +function addOtherMethods() { + // ### Multiple contexts + // + // When using multiple contexts in one web page, `gl.makeCurrent()` must be + // called before issuing commands to a different context. + (function(context) { + gl.makeCurrent = function() { + gl = context; + }; + })(gl); + + // ### Animation + // + // Call `gl.animate()` to provide an animation loop that repeatedly calls + // `gl.onupdate()` and `gl.ondraw()`. + gl.animate = function() { + var post = + window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + function(callback) { setTimeout(callback, 1000 / 60); }; + var time = new Date().getTime(); + var context = gl; + function update() { + gl = context; + var now = new Date().getTime(); + if (gl.onupdate) gl.onupdate((now - time) / 1000); + if (gl.ondraw) gl.ondraw(); + post(update); + time = now; + } + update(); + }; + + // ### Fullscreen + // + // Provide an easy way to get a fullscreen app running, including an + // automatic 3D perspective projection matrix by default. This should be + // called once. + // + // Just fullscreen, no automatic camera: + // + // gl.fullscreen({ camera: false }); + // + // Adjusting field of view, near plane distance, and far plane distance: + // + // gl.fullscreen({ fov: 45, near: 0.1, far: 1000 }); + // + // Adding padding from the edge of the window: + // + // gl.fullscreen({ paddingLeft: 250, paddingBottom: 60 }); + // + gl.fullscreen = function(options) { + options = options || {}; + var top = options.paddingTop || 0; + var left = options.paddingLeft || 0; + var right = options.paddingRight || 0; + var bottom = options.paddingBottom || 0; + if (!document.body) { + throw new Error('document.body doesn\'t exist yet (call gl.fullscreen() from ' + + 'window.onload() or from inside the
tag)'); + } + document.body.appendChild(gl.canvas); + document.body.style.overflow = 'hidden'; + gl.canvas.style.position = 'absolute'; + gl.canvas.style.left = left + 'px'; + gl.canvas.style.top = top + 'px'; + function resize() { + gl.canvas.width = window.innerWidth - left - right; + gl.canvas.height = window.innerHeight - top - bottom; + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + if (options.camera || !('camera' in options)) { + gl.matrixMode(gl.PROJECTION); + gl.loadIdentity(); + gl.perspective(options.fov || 45, gl.canvas.width / gl.canvas.height, + options.near || 0.1, options.far || 1000); + gl.matrixMode(gl.MODELVIEW); + } + if (gl.ondraw) gl.ondraw(); + } + on(window, 'resize', resize); + resize(); + }; +} + +// A value to bitwise-or with new enums to make them distinguishable from the +// standard WebGL enums. +var ENUM = 0x12340000; + +if (typeof module === 'object' && module.exports) { + module.exports = GL; +} +// src/raytracer.js +// Provides a convenient raytracing interface. + +// ### new GL.HitTest([t, hit, normal]) +// +// This is the object used to return hit test results. If there are no +// arguments, the constructed argument represents a hit infinitely far +// away. +function HitTest(t, hit, normal) { + this.t = arguments.length ? t : Number.MAX_VALUE; + this.hit = hit; + this.normal = normal; +} + +// ### .mergeWith(other) +// +// Changes this object to be the closer of the two hit test results. +HitTest.prototype = { + mergeWith: function(other) { + if (other.t > 0 && other.t < this.t) { + this.t = other.t; + this.hit = other.hit; + this.normal = other.normal; + } + } +}; + +// ### new GL.Raytracer() +// +// This will read the current modelview matrix, projection matrix, and viewport, +// reconstruct the eye position, and store enough information to later generate +// per-pixel rays using `getRayForPixel()`. +// +// Example usage: +// +// var tracer = new GL.Raytracer(); +// var ray = tracer.getRayForPixel( +// gl.canvas.width / 2, +// gl.canvas.height / 2); +// var result = GL.Raytracer.hitTestSphere( +// tracer.eye, ray, new GL.Vector(0, 0, 0), 1); +function Raytracer() { + var v = gl.getParameter(gl.VIEWPORT); + var m = gl.modelviewMatrix.m; + + var axisX = new Vector(m[0], m[4], m[8]); + var axisY = new Vector(m[1], m[5], m[9]); + var axisZ = new Vector(m[2], m[6], m[10]); + var offset = new Vector(m[3], m[7], m[11]); + this.eye = new Vector(-offset.dot(axisX), -offset.dot(axisY), -offset.dot(axisZ)); + + var minX = v[0], maxX = minX + v[2]; + var minY = v[1], maxY = minY + v[3]; + this.ray00 = gl.unProject(minX, minY, 1).subtract(this.eye); + this.ray10 = gl.unProject(maxX, minY, 1).subtract(this.eye); + this.ray01 = gl.unProject(minX, maxY, 1).subtract(this.eye); + this.ray11 = gl.unProject(maxX, maxY, 1).subtract(this.eye); + this.viewport = v; +} + +Raytracer.prototype = { + // ### .getRayForPixel(x, y) + // + // Returns the ray originating from the camera and traveling through the pixel `x, y`. + getRayForPixel: function(x, y) { + x = (x - this.viewport[0]) / this.viewport[2]; + y = 1 - (y - this.viewport[1]) / this.viewport[3]; + var ray0 = Vector.lerp(this.ray00, this.ray10, x); + var ray1 = Vector.lerp(this.ray01, this.ray11, x); + return Vector.lerp(ray0, ray1, y).unit(); + } +}; + +// ### GL.Raytracer.hitTestBox(origin, ray, min, max) +// +// Traces the ray starting from `origin` along `ray` against the axis-aligned box +// whose coordinates extend from `min` to `max`. Returns a `HitTest` with the +// information or `null` for no intersection. +// +// This implementation uses the [slab intersection method](http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter3.htm). +Raytracer.hitTestBox = function(origin, ray, min, max) { + var tMin = min.subtract(origin).divide(ray); + var tMax = max.subtract(origin).divide(ray); + var t1 = Vector.min(tMin, tMax); + var t2 = Vector.max(tMin, tMax); + var tNear = t1.max(); + var tFar = t2.min(); + + if (tNear > 0 && tNear < tFar) { + var epsilon = 1.0e-6, hit = origin.add(ray.multiply(tNear)); + min = min.add(epsilon); + max = max.subtract(epsilon); + return new HitTest(tNear, hit, new Vector( + (hit.x > max.x) - (hit.x < min.x), + (hit.y > max.y) - (hit.y < min.y), + (hit.z > max.z) - (hit.z < min.z) + )); + } + + return null; +}; + +// ### GL.Raytracer.hitTestSphere(origin, ray, center, radius) +// +// Traces the ray starting from `origin` along `ray` against the sphere defined +// by `center` and `radius`. Returns a `HitTest` with the information or `null` +// for no intersection. +Raytracer.hitTestSphere = function(origin, ray, center, radius) { + var offset = origin.subtract(center); + var a = ray.dot(ray); + var b = 2 * ray.dot(offset); + var c = offset.dot(offset) - radius * radius; + var discriminant = b * b - 4 * a * c; + + if (discriminant > 0) { + var t = (-b - Math.sqrt(discriminant)) / (2 * a), hit = origin.add(ray.multiply(t)); + return new HitTest(t, hit, hit.subtract(center).divide(radius)); + } + + return null; +}; + +// ### GL.Raytracer.hitTestTriangle(origin, ray, a, b, c) +// +// Traces the ray starting from `origin` along `ray` against the triangle defined +// by the points `a`, `b`, and `c`. Returns a `HitTest` with the information or +// `null` for no intersection. +Raytracer.hitTestTriangle = function(origin, ray, a, b, c) { + var ab = b.subtract(a); + var ac = c.subtract(a); + var normal = ab.cross(ac).unit(); + var t = normal.dot(a.subtract(origin)) / normal.dot(ray); + + if (t > 0) { + var hit = origin.add(ray.multiply(t)); + var toHit = hit.subtract(a); + var dot00 = ac.dot(ac); + var dot01 = ac.dot(ab); + var dot02 = ac.dot(toHit); + var dot11 = ab.dot(ab); + var dot12 = ab.dot(toHit); + var divide = dot00 * dot11 - dot01 * dot01; + var u = (dot11 * dot02 - dot01 * dot12) / divide; + var v = (dot00 * dot12 - dot01 * dot02) / divide; + if (u >= 0 && v >= 0 && u + v <= 1) return new HitTest(t, hit, normal); + } + + return null; +}; + +// src/texture.js +// Provides a simple wrapper around WebGL textures that supports render-to-texture. + +// ### new GL.Texture(width, height[, options]) +// +// The arguments `width` and `height` give the size of the texture in texels. +// WebGL texture dimensions must be powers of two unless `filter` is set to +// either `gl.NEAREST` or `gl.LINEAR` and `wrap` is set to `gl.CLAMP_TO_EDGE` +// (which they are by default). +// +// Texture parameters can be passed in via the `options` argument. +// Example usage: +// +// var t = new GL.Texture(256, 256, { +// // Defaults to gl.LINEAR, set both at once with "filter" +// magFilter: gl.NEAREST, +// minFilter: gl.LINEAR, +// +// // Defaults to gl.CLAMP_TO_EDGE, set both at once with "wrap" +// wrapS: gl.REPEAT, +// wrapT: gl.REPEAT, +// +// format: gl.RGB, // Defaults to gl.RGBA +// type: gl.FLOAT // Defaults to gl.UNSIGNED_BYTE +// }); +function Texture(width, height, options) { + options = options || {}; + this.id = gl.createTexture(); + this.width = width; + this.height = height; + this.format = options.format || gl.RGBA; + this.type = options.type || gl.UNSIGNED_BYTE; + var magFilter = options.filter || options.magFilter || gl.LINEAR; + var minFilter = options.filter || options.minFilter || gl.LINEAR; + if (this.type === gl.FLOAT) { + if (!Texture.canUseFloatingPointTextures()) { + throw new Error('OES_texture_float is required but not supported'); + } + if ((minFilter !== gl.NEAREST || magFilter !== gl.NEAREST) && + !Texture.canUseFloatingPointLinearFiltering()) { + throw new Error('OES_texture_float_linear is required but not supported'); + } + } else if (this.type === gl.HALF_FLOAT_OES) { + if (!Texture.canUseHalfFloatingPointTextures()) { + throw new Error('OES_texture_half_float is required but not supported'); + } + if ((minFilter !== gl.NEAREST || magFilter !== gl.NEAREST) && + !Texture.canUseHalfFloatingPointLinearFiltering()) { + throw new Error('OES_texture_half_float_linear is required but not supported'); + } + } + gl.bindTexture(gl.TEXTURE_2D, this.id); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, options.wrap || options.wrapS || gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, options.wrap || options.wrapT || gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, this.type, options.data || null); +} + +var framebuffer; +var renderbuffer; +var checkerboardCanvas; + +Texture.prototype = { + // ### .bind([unit]) + // + // Bind this texture to the given texture unit (0-7, defaults to 0). + bind: function(unit) { + gl.activeTexture(gl.TEXTURE0 + (unit || 0)); + gl.bindTexture(gl.TEXTURE_2D, this.id); + }, + + // ### .unbind([unit]) + // + // Clear the given texture unit (0-7, defaults to 0). + unbind: function(unit) { + gl.activeTexture(gl.TEXTURE0 + (unit || 0)); + gl.bindTexture(gl.TEXTURE_2D, null); + }, + + // ### .canDrawTo() + // + // Check if rendering to this texture is supported. It may not be supported + // for floating-point textures on some configurations. + canDrawTo: function() { + framebuffer = framebuffer || gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0); + var result = gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE; + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + return result; + }, + + // ### .drawTo(callback) + // + // Render all draw calls in `callback` to this texture. This method sets up + // a framebuffer with this texture as the color attachment and a renderbuffer + // as the depth attachment. It also temporarily changes the viewport to the + // size of the texture. + // + // Example usage: + // + // texture.drawTo(function() { + // gl.clearColor(1, 0, 0, 1); + // gl.clear(gl.COLOR_BUFFER_BIT); + // }); + drawTo: function(callback) { + var v = gl.getParameter(gl.VIEWPORT); + framebuffer = framebuffer || gl.createFramebuffer(); + renderbuffer = renderbuffer || gl.createRenderbuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); + if (this.width != renderbuffer.width || this.height != renderbuffer.height) { + renderbuffer.width = this.width; + renderbuffer.height = this.height; + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height); + } + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); + if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { + throw new Error('Rendering to this texture is not supported (incomplete framebuffer)'); + } + gl.viewport(0, 0, this.width, this.height); + + callback(); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.viewport(v[0], v[1], v[2], v[3]); + }, + + // ### .swapWith(other) + // + // Switch this texture with `other`, useful for the ping-pong rendering + // technique used in multi-stage rendering. + swapWith: function(other) { + var temp; + temp = other.id; other.id = this.id; this.id = temp; + temp = other.width; other.width = this.width; this.width = temp; + temp = other.height; other.height = this.height; this.height = temp; + } +}; + +// ### GL.Texture.fromImage(image[, options]) +// +// Return a new image created from `image`, an `