/* * lightgl.js * http://github.com/evanw/lightgl.js/ * * Copyright 2011 Evan Wallace * Released under the MIT license */ var GL = (function() { // 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; gl.bindTexture(gl.TEXTURE_2D, this.id); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, options.filter || options.magFilter || gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, options.filter || options.minFilter || gl.LINEAR); 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, 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); }, // ### .drawTo(callback[, options]) // // 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. The viewport is // temporarily changed to the size of the texture. // // The depth buffer can be omitted via `options` as shown in the // example below: // // texture.drawTo(function() { // gl.clearColor(1, 0, 0, 1); // gl.clear(gl.COLOR_BUFFER_BIT); // }, { depth: false }); drawTo: function(callback, options) { options = options || {}; var v = gl.getParameter(gl.VIEWPORT); gl.viewport(0, 0, this.width, this.height); framebuffer = framebuffer || gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0); if(options.depth !== false) { renderbuffer = renderbuffer || gl.createRenderbuffer(); 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.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); } 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 `` tag. Texture.fromImage = function(image, options) { options = options || {}; var texture = new Texture(image.width, image.height, options); try { gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.format, texture.type, image); } catch(e) { if(window.location.protocol == 'file:') { throw 'image not loaded for security reasons (serve this page over "http://" instead)'; } else { throw 'image not loaded for security reasons (image must originate from the same ' + 'domain as this page or use Cross-Origin Resource Sharing)'; } } if(options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { gl.generateMipmap(gl.TEXTURE_2D); } return texture; }; // ### GL.Texture.fromURL(url[, options]) // // Returns a checkerboard texture that will switch to the correct texture when // it loads. Texture.fromURL = function(url, options) { checkerboardCanvas = checkerboardCanvas || (function() { var c = document.createElement('canvas').getContext('2d'); c.canvas.width = c.canvas.height = 128; for(var y = 0; y < c.canvas.height; y += 16) { for(var x = 0; x < c.canvas.width; x += 16) { c.fillStyle = (x ^ y) & 16 ? '#FFF' : '#DDD'; c.fillRect(x, y, 16, 16); } } return c.canvas; })(); var texture = Texture.fromImage(checkerboardCanvas, options); var image = new Image(); var context = gl; image.onload = function() { context.makeCurrent(); Texture.fromImage(image, options).swapWith(texture); }; image.src = url; return texture; }; // 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 '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) { 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/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()) }; }, 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]); }; // 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) { var result; 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 '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 '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]'; } 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 '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 '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; } }; // ### GL.Shader.fromURL(vsURL, fsURL) // // Compiles a shader program using the provided vertex and fragment // shaders. The shaders are loaded synchronously from the given URLs. // Shader.fromURL = function(vsURL, fsURL) { var XMLHttpRequestGet = function(uri) { var mHttpReq = new XMLHttpRequest(); mHttpReq.open("GET", uri, false); mHttpReq.send(null); if(mHttpReq.status !== 200) { throw 'could not load ' + uri; } return mHttpReq.responseText; }; var vsSource = XMLHttpRequestGet(vsURL); var fsSource = XMLHttpRequestGet(fsURL); return new Shader(vsSource, fsSource); }; Shader.from = function(vsURLorID, fsURLorID) { try { return new Shader(vsURLorID, fsURLorID); } catch(e) { return Shader.fromURL(vsURLorID, fsURLorID); } }; // 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. Uses the HTML canvas given in 'options' or creates // a new one if necessary. The alpha channel is disabled by default // because it usually causes unintended transparencies in the // canvas. create: function(options) { options = options || {}; var canvas = options.canvas; if(!canvas) { canvas = document.createElement('canvas'); canvas.width = options.width || 800; canvas.height = options.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 'WebGL not supported'; 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 '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 '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 '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 augmentTouchEvent(original) { 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; if(e.targetTouches.length > 0) { var touch = e.targetTouches[0]; e.x = touch.pageX; e.y = touch.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 = true; } 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 mousewheel(e) { gl = context; e = augment(e); if(gl.onmousewheel) gl.onmousewheel(e); e.preventDefault(); } function touchstart(e) { resetAll(); // Expand the event handlers to the document to handle dragging off canvas. on(document, 'touchmove', touchmove); on(document, 'touchend', touchend); off(gl.canvas, 'touchmove', touchmove); off(gl.canvas, 'touchend', touchend); gl = context; e = augmentTouchEvent(e); if(gl.ontouchstart) gl.ontouchstart(e); e.preventDefault(); } function touchmove(e) { gl = context; if(e.targetTouches.length === 0) { touchend(e); } e = augmentTouchEvent(e); if(gl.ontouchmove) gl.ontouchmove(e); e.preventDefault(); } function touchend(e) { // Shrink the event handlers back to the canvas when dragging ends. off(document, 'touchmove', touchmove); off(document, 'touchend', touchend); on(gl.canvas, 'touchmove', touchmove); on(gl.canvas, 'touchend', touchend); gl = context; e = augmentTouchEvent(e); if(gl.ontouchend) gl.ontouchend(e); e.preventDefault(); } function reset() { hasOld = false; } function resetAll() { buttons = {}; hasOld = false; } // We can keep mouse and touch events enabled at the same time, // because Google Chrome will apparently never fire both of them. on(gl.canvas, 'mousedown', mousedown); on(gl.canvas, 'mousemove', mousemove); on(gl.canvas, 'mouseup', mouseup); on(gl.canvas, 'mousewheel', mousewheel); on(gl.canvas, 'DOMMouseScroll', mousewheel); on(gl.canvas, 'mouseover', reset); on(gl.canvas, 'mouseout', reset); on(gl.canvas, 'touchstart', touchstart); on(gl.canvas, 'touchmove', touchmove); on(gl.canvas, 'touchend', touchend); 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 '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.onresize) gl.onresize(); 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; // 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/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; }; return GL; })();