/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cc, Ci, Cu} = require("chrome"); let TiltUtils = require("devtools/tilt/tilt-utils"); let {TiltMath, mat4} = require("devtools/tilt/tilt-math"); Cu.import("resource://gre/modules/Services.jsm"); const WEBGL_CONTEXT_NAME = "experimental-webgl"; /** * Module containing thin wrappers around low-level WebGL functions. */ let TiltGL = {}; module.exports = TiltGL; /** * Contains commonly used helper methods used in any 3D application. * * @param {HTMLCanvasElement} aCanvas * the canvas element used for rendering * @param {Function} onError * optional, function called if initialization failed * @param {Function} onLoad * optional, function called if initialization worked */ TiltGL.Renderer = function TGL_Renderer(aCanvas, onError, onLoad) { /** * The WebGL context obtained from the canvas element, used for drawing. */ this.context = TiltGL.create3DContext(aCanvas); // check if the context was created successfully if (!this.context) { TiltUtils.Output.alert("Firefox", TiltUtils.L10n.get("initTilt.error")); TiltUtils.Output.error(TiltUtils.L10n.get("initWebGL.error")); if ("function" === typeof onError) { onError(); } return; } // set the default clear color and depth buffers this.context.clearColor(0, 0, 0, 0); this.context.clearDepth(1); /** * Variables representing the current framebuffer width and height. */ this.width = aCanvas.width; this.height = aCanvas.height; this.initialWidth = this.width; this.initialHeight = this.height; /** * The current model view matrix. */ this.mvMatrix = mat4.identity(mat4.create()); /** * The current projection matrix. */ this.projMatrix = mat4.identity(mat4.create()); /** * The current fill color applied to any objects which can be filled. * These are rectangles, circles, boxes, 2d or 3d primitives in general. */ this._fillColor = []; /** * The current stroke color applied to any objects which can be stroked. * This property mostly refers to lines. */ this._strokeColor = []; /** * Variable representing the current stroke weight. */ this._strokeWeightValue = 0; /** * A shader useful for drawing vertices with only a color component. */ this._colorShader = new TiltGL.Program(this.context, { vs: TiltGL.ColorShader.vs, fs: TiltGL.ColorShader.fs, attributes: ["vertexPosition"], uniforms: ["mvMatrix", "projMatrix", "fill"] }); // create helper functions to create shaders, meshes, buffers and textures this.Program = TiltGL.Program.bind(TiltGL.Program, this.context); this.VertexBuffer = TiltGL.VertexBuffer.bind(TiltGL.VertexBuffer, this.context); this.IndexBuffer = TiltGL.IndexBuffer.bind(TiltGL.IndexBuffer, this.context); this.Texture = TiltGL.Texture.bind(TiltGL.Texture, this.context); // set the default mvp matrices, tint, fill, stroke and other visual props. this.defaults(); // the renderer was created successfully if ("function" === typeof onLoad) { onLoad(); } }; TiltGL.Renderer.prototype = { /** * Clears the color and depth buffers. */ clear: function TGLR_clear() { let gl = this.context; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }, /** * Sets if depth testing should be enabled or not. * Disabling could be useful when handling transparency (for example). * * @param {Boolean} aEnabledFlag * true if depth testing should be enabled */ depthTest: function TGLR_depthTest(aEnabledFlag) { let gl = this.context; if (aEnabledFlag) { gl.enable(gl.DEPTH_TEST); } else { gl.disable(gl.DEPTH_TEST); } }, /** * Sets if stencil testing should be enabled or not. * * @param {Boolean} aEnabledFlag * true if stencil testing should be enabled */ stencilTest: function TGLR_stencilTest(aEnabledFlag) { let gl = this.context; if (aEnabledFlag) { gl.enable(gl.STENCIL_TEST); } else { gl.disable(gl.STENCIL_TEST); } }, /** * Sets cull face, either "front", "back" or disabled. * * @param {String} aModeFlag * blending mode, either "front", "back", "both" or falsy */ cullFace: function TGLR_cullFace(aModeFlag) { let gl = this.context; switch (aModeFlag) { case "front": gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT); break; case "back": gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); break; case "both": gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT_AND_BACK); break; default: gl.disable(gl.CULL_FACE); } }, /** * Specifies the orientation of front-facing polygons. * * @param {String} aModeFlag * either "cw" or "ccw" */ frontFace: function TGLR_frontFace(aModeFlag) { let gl = this.context; switch (aModeFlag) { case "cw": gl.frontFace(gl.CW); break; case "ccw": gl.frontFace(gl.CCW); break; } }, /** * Sets blending, either "alpha" or "add" (additive blending). * Anything else disables blending. * * @param {String} aModeFlag * blending mode, either "alpha", "add" or falsy */ blendMode: function TGLR_blendMode(aModeFlag) { let gl = this.context; switch (aModeFlag) { case "alpha": gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); break; case "add": gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE); break; default: gl.disable(gl.BLEND); } }, /** * Helper function to activate the color shader. * * @param {TiltGL.VertexBuffer} aVerticesBuffer * a buffer of vertices positions * @param {Array} aColor * the color fill to be used as [r, g, b, a] with 0..1 range * @param {Array} aMvMatrix * the model view matrix * @param {Array} aProjMatrix * the projection matrix */ useColorShader: function TGLR_useColorShader( aVerticesBuffer, aColor, aMvMatrix, aProjMatrix) { let program = this._colorShader; // use this program program.use(); // bind the attributes and uniforms as necessary program.bindVertexBuffer("vertexPosition", aVerticesBuffer); program.bindUniformMatrix("mvMatrix", aMvMatrix || this.mvMatrix); program.bindUniformMatrix("projMatrix", aProjMatrix || this.projMatrix); program.bindUniformVec4("fill", aColor || this._fillColor); }, /** * Draws bound vertex buffers using the specified parameters. * * @param {Number} aDrawMode * WebGL enum, like TRIANGLES * @param {Number} aCount * the number of indices to be rendered */ drawVertices: function TGLR_drawVertices(aDrawMode, aCount) { this.context.drawArrays(aDrawMode, 0, aCount); }, /** * Draws bound vertex buffers using the specified parameters. * This function also makes use of an index buffer. * * @param {Number} aDrawMode * WebGL enum, like TRIANGLES * @param {TiltGL.IndexBuffer} aIndicesBuffer * indices for the vertices buffer */ drawIndexedVertices: function TGLR_drawIndexedVertices( aDrawMode, aIndicesBuffer) { let gl = this.context; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, aIndicesBuffer._ref); gl.drawElements(aDrawMode, aIndicesBuffer.numItems, gl.UNSIGNED_SHORT, 0); }, /** * Sets the current fill color. * * @param {Array} aColor * the color fill to be used as [r, g, b, a] with 0..1 range * @param {Number} aMultiplyAlpha * optional, scalar to multiply the alpha element with */ fill: function TGLR_fill(aColor, aMultiplyAlpha) { let fill = this._fillColor; fill[0] = aColor[0]; fill[1] = aColor[1]; fill[2] = aColor[2]; fill[3] = aColor[3] * (aMultiplyAlpha || 1); }, /** * Sets the current stroke color. * * @param {Array} aColor * the color stroke to be used as [r, g, b, a] with 0..1 range * @param {Number} aMultiplyAlpha * optional, scalar to multiply the alpha element with */ stroke: function TGLR_stroke(aColor, aMultiplyAlpha) { let stroke = this._strokeColor; stroke[0] = aColor[0]; stroke[1] = aColor[1]; stroke[2] = aColor[2]; stroke[3] = aColor[3] * (aMultiplyAlpha || 1); }, /** * Sets the current stroke weight (line width). * * @param {Number} aWeight * the stroke weight */ strokeWeight: function TGLR_strokeWeight(aWeight) { if (this._strokeWeightValue !== aWeight) { this._strokeWeightValue = aWeight; this.context.lineWidth(aWeight); } }, /** * Sets a default perspective projection, with the near frustum rectangle * mapped to the canvas width and height bounds. */ perspective: function TGLR_perspective() { let fov = 45; let w = this.width; let h = this.height; let x = w / 2; let y = h / 2; let z = y / Math.tan(TiltMath.radians(fov) / 2); let aspect = w / h; let znear = z / 10; let zfar = z * 10; mat4.perspective(fov, aspect, znear, zfar, this.projMatrix, -1); mat4.translate(this.projMatrix, [-x, -y, -z]); mat4.identity(this.mvMatrix); }, /** * Sets a default orthographic projection (recommended for 2d rendering). */ ortho: function TGLR_ortho() { mat4.ortho(0, this.width, this.height, 0, -1, 1, this.projMatrix); mat4.identity(this.mvMatrix); }, /** * Sets a custom projection matrix. * @param {Array} matrix: the custom projection matrix to be used */ projection: function TGLR_projection(aMatrix) { mat4.set(aMatrix, this.projMatrix); mat4.identity(this.mvMatrix); }, /** * Resets the model view matrix to identity. * This is a default matrix with no rotation, no scaling, at (0, 0, 0); */ origin: function TGLR_origin() { mat4.identity(this.mvMatrix); }, /** * Transforms the model view matrix with a new matrix. * Useful for creating custom transformations. * * @param {Array} matrix: the matrix to be multiply the model view with */ transform: function TGLR_transform(aMatrix) { mat4.multiply(this.mvMatrix, aMatrix); }, /** * Translates the model view by the x, y and z coordinates. * * @param {Number} x * the x amount of translation * @param {Number} y * the y amount of translation * @param {Number} z * optional, the z amount of translation */ translate: function TGLR_translate(x, y, z) { mat4.translate(this.mvMatrix, [x, y, z || 0]); }, /** * Rotates the model view by a specified angle on the x, y and z axis. * * @param {Number} angle * the angle expressed in radians * @param {Number} x * the x axis of the rotation * @param {Number} y * the y axis of the rotation * @param {Number} z * the z axis of the rotation */ rotate: function TGLR_rotate(angle, x, y, z) { mat4.rotate(this.mvMatrix, angle, [x, y, z]); }, /** * Rotates the model view by a specified angle on the x axis. * * @param {Number} aAngle * the angle expressed in radians */ rotateX: function TGLR_rotateX(aAngle) { mat4.rotateX(this.mvMatrix, aAngle); }, /** * Rotates the model view by a specified angle on the y axis. * * @param {Number} aAngle * the angle expressed in radians */ rotateY: function TGLR_rotateY(aAngle) { mat4.rotateY(this.mvMatrix, aAngle); }, /** * Rotates the model view by a specified angle on the z axis. * * @param {Number} aAngle * the angle expressed in radians */ rotateZ: function TGLR_rotateZ(aAngle) { mat4.rotateZ(this.mvMatrix, aAngle); }, /** * Scales the model view by the x, y and z coordinates. * * @param {Number} x * the x amount of scaling * @param {Number} y * the y amount of scaling * @param {Number} z * optional, the z amount of scaling */ scale: function TGLR_scale(x, y, z) { mat4.scale(this.mvMatrix, [x, y, z || 1]); }, /** * Performs a custom interpolation between two matrices. * The result is saved in the first operand. * * @param {Array} aMat * the first matrix * @param {Array} aMat2 * the second matrix * @param {Number} aLerp * interpolation amount between the two inputs * @param {Number} aDamping * optional, scalar adjusting the interpolation amortization * @param {Number} aBalance * optional, scalar adjusting the interpolation shift ammount */ lerp: function TGLR_lerp(aMat, aMat2, aLerp, aDamping, aBalance) { if (aLerp < 0 || aLerp > 1) { return; } // calculate the interpolation factor based on the damping and step let f = Math.pow(1 - Math.pow(aLerp, aDamping || 1), 1 / aBalance || 1); // interpolate each element from the two matrices for (let i = 0, len = this.projMatrix.length; i < len; i++) { aMat[i] = aMat[i] + f * (aMat2[i] - aMat[i]); } }, /** * Resets the drawing style to default. */ defaults: function TGLR_defaults() { this.depthTest(true); this.stencilTest(false); this.cullFace(false); this.frontFace("ccw"); this.blendMode("alpha"); this.fill([1, 1, 1, 1]); this.stroke([0, 0, 0, 1]); this.strokeWeight(1); this.perspective(); this.origin(); }, /** * Draws a quad composed of four vertices. * Vertices must be in clockwise order, or else drawing will be distorted. * Do not abuse this function, it is quite slow. * * @param {Array} aV0 * the [x, y, z] position of the first triangle point * @param {Array} aV1 * the [x, y, z] position of the second triangle point * @param {Array} aV2 * the [x, y, z] position of the third triangle point * @param {Array} aV3 * the [x, y, z] position of the fourth triangle point */ quad: function TGLR_quad(aV0, aV1, aV2, aV3) { let gl = this.context; let fill = this._fillColor; let stroke = this._strokeColor; let vert = new TiltGL.VertexBuffer(gl, [aV0[0], aV0[1], aV0[2] || 0, aV1[0], aV1[1], aV1[2] || 0, aV2[0], aV2[1], aV2[2] || 0, aV3[0], aV3[1], aV3[2] || 0], 3); // use the necessary shader and draw the vertices this.useColorShader(vert, fill); this.drawVertices(gl.TRIANGLE_FAN, vert.numItems); this.useColorShader(vert, stroke); this.drawVertices(gl.LINE_LOOP, vert.numItems); TiltUtils.destroyObject(vert); }, /** * Function called when this object is destroyed. */ finalize: function TGLR_finalize() { if (this.context) { TiltUtils.destroyObject(this._colorShader); } } }; /** * Creates a vertex buffer containing an array of elements. * * @param {Object} aContext * a WebGL context * @param {Array} aElementsArray * an array of numbers (floats) * @param {Number} aItemSize * how many items create a block * @param {Number} aNumItems * optional, how many items to use from the array */ TiltGL.VertexBuffer = function TGL_VertexBuffer( aContext, aElementsArray, aItemSize, aNumItems) { /** * The parent WebGL context. */ this._context = aContext; /** * The array buffer. */ this._ref = null; /** * Array of number components contained in the buffer. */ this.components = null; /** * Variables defining the internal structure of the buffer. */ this.itemSize = 0; this.numItems = 0; // if the array is specified in the constructor, initialize directly if (aElementsArray) { this.initBuffer(aElementsArray, aItemSize, aNumItems); } }; TiltGL.VertexBuffer.prototype = { /** * Initializes buffer data to be used for drawing, using an array of floats. * The "aNumItems" param can be specified to use only a portion of the array. * * @param {Array} aElementsArray * an array of floats * @param {Number} aItemSize * how many items create a block * @param {Number} aNumItems * optional, how many items to use from the array */ initBuffer: function TGLVB_initBuffer(aElementsArray, aItemSize, aNumItems) { let gl = this._context; // the aNumItems parameter is optional, we can compute it if not specified aNumItems = aNumItems || aElementsArray.length / aItemSize; // create the Float32Array using the elements array this.components = new Float32Array(aElementsArray); // create an array buffer and bind the elements as a Float32Array this._ref = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this._ref); gl.bufferData(gl.ARRAY_BUFFER, this.components, gl.STATIC_DRAW); // remember some properties, useful when binding the buffer to a shader this.itemSize = aItemSize; this.numItems = aNumItems; }, /** * Function called when this object is destroyed. */ finalize: function TGLVB_finalize() { if (this._context) { this._context.deleteBuffer(this._ref); } } }; /** * Creates an index buffer containing an array of indices. * * @param {Object} aContext * a WebGL context * @param {Array} aElementsArray * an array of unsigned integers * @param {Number} aNumItems * optional, how many items to use from the array */ TiltGL.IndexBuffer = function TGL_IndexBuffer( aContext, aElementsArray, aNumItems) { /** * The parent WebGL context. */ this._context = aContext; /** * The element array buffer. */ this._ref = null; /** * Array of number components contained in the buffer. */ this.components = null; /** * Variables defining the internal structure of the buffer. */ this.itemSize = 0; this.numItems = 0; // if the array is specified in the constructor, initialize directly if (aElementsArray) { this.initBuffer(aElementsArray, aNumItems); } }; TiltGL.IndexBuffer.prototype = { /** * Initializes a buffer of vertex indices, using an array of unsigned ints. * The item size will automatically default to 1, and the "numItems" will be * equal to the number of items in the array if not specified. * * @param {Array} aElementsArray * an array of numbers (unsigned integers) * @param {Number} aNumItems * optional, how many items to use from the array */ initBuffer: function TGLIB_initBuffer(aElementsArray, aNumItems) { let gl = this._context; // the aNumItems parameter is optional, we can compute it if not specified aNumItems = aNumItems || aElementsArray.length; // create the Uint16Array using the elements array this.components = new Uint16Array(aElementsArray); // create an array buffer and bind the elements as a Uint16Array this._ref = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._ref); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.components, gl.STATIC_DRAW); // remember some properties, useful when binding the buffer to a shader this.itemSize = 1; this.numItems = aNumItems; }, /** * Function called when this object is destroyed. */ finalize: function TGLIB_finalize() { if (this._context) { this._context.deleteBuffer(this._ref); } } }; /** * A program is composed of a vertex and a fragment shader. * * @param {Object} aProperties * optional, an object containing the following properties: * {String} vs: the vertex shader source code * {String} fs: the fragment shader source code * {Array} attributes: an array of attributes as strings * {Array} uniforms: an array of uniforms as strings */ TiltGL.Program = function(aContext, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; /** * The parent WebGL context. */ this._context = aContext; /** * A reference to the actual GLSL program. */ this._ref = null; /** * Each program has an unique id assigned. */ this._id = -1; /** * Two arrays: an attributes array, containing all the cached attributes * and a uniforms array, containing all the cached uniforms. */ this._attributes = null; this._uniforms = null; // if the sources are specified in the constructor, initialize directly if (aProperties.vs && aProperties.fs) { this.initProgram(aProperties); } }; TiltGL.Program.prototype = { /** * Initializes a shader program, using specified source code as strings. * * @param {Object} aProperties * an object containing the following properties: * {String} vs: the vertex shader source code * {String} fs: the fragment shader source code * {Array} attributes: an array of attributes as strings * {Array} uniforms: an array of uniforms as strings */ initProgram: function TGLP_initProgram(aProperties) { this._ref = TiltGL.ProgramUtils.create(this._context, aProperties); // cache for faster access this._id = this._ref.id; this._attributes = this._ref.attributes; this._uniforms = this._ref.uniforms; // cleanup delete this._ref.id; delete this._ref.attributes; delete this._ref.uniforms; }, /** * Uses the shader program as current one for the WebGL context; it also * enables vertex attributes necessary to enable when using this program. * This method also does some useful caching, as the function "useProgram" * could take quite a lot of time. */ use: function TGLP_use() { let id = this._id; let utils = TiltGL.ProgramUtils; // check if the program wasn't already active if (utils._activeProgram !== id) { utils._activeProgram = id; // use the the program if it wasn't already set this._context.useProgram(this._ref); this.cleanupVertexAttrib(); // enable any necessary vertex attributes using the cache for each (let attribute in this._attributes) { this._context.enableVertexAttribArray(attribute); utils._enabledAttributes.push(attribute); } } }, /** * Disables all currently enabled vertex attribute arrays. */ cleanupVertexAttrib: function TGLP_cleanupVertexAttrib() { let utils = TiltGL.ProgramUtils; for each (let attribute in utils._enabledAttributes) { this._context.disableVertexAttribArray(attribute); } utils._enabledAttributes = []; }, /** * Binds a vertex buffer as an array buffer for a specific shader attribute. * * @param {String} aAtribute * the attribute name obtained from the shader * @param {Float32Array} aBuffer * the buffer to be bound */ bindVertexBuffer: function TGLP_bindVertexBuffer(aAtribute, aBuffer) { // get the cached attribute value from the shader let gl = this._context; let attr = this._attributes[aAtribute]; let size = aBuffer.itemSize; gl.bindBuffer(gl.ARRAY_BUFFER, aBuffer._ref); gl.vertexAttribPointer(attr, size, gl.FLOAT, false, 0, 0); }, /** * Binds a uniform matrix to the current shader. * * @param {String} aUniform * the uniform name to bind the variable to * @param {Float32Array} m * the matrix to be bound */ bindUniformMatrix: function TGLP_bindUniformMatrix(aUniform, m) { this._context.uniformMatrix4fv(this._uniforms[aUniform], false, m); }, /** * Binds a uniform vector of 4 elements to the current shader. * * @param {String} aUniform * the uniform name to bind the variable to * @param {Float32Array} v * the vector to be bound */ bindUniformVec4: function TGLP_bindUniformVec4(aUniform, v) { this._context.uniform4fv(this._uniforms[aUniform], v); }, /** * Binds a simple float element to the current shader. * * @param {String} aUniform * the uniform name to bind the variable to * @param {Number} v * the variable to be bound */ bindUniformFloat: function TGLP_bindUniformFloat(aUniform, f) { this._context.uniform1f(this._uniforms[aUniform], f); }, /** * Binds a uniform texture for a sampler to the current shader. * * @param {String} aSampler * the sampler name to bind the texture to * @param {TiltGL.Texture} aTexture * the texture to be bound */ bindTexture: function TGLP_bindTexture(aSampler, aTexture) { let gl = this._context; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, aTexture._ref); gl.uniform1i(this._uniforms[aSampler], 0); }, /** * Function called when this object is destroyed. */ finalize: function TGLP_finalize() { if (this._context) { this._context.useProgram(null); this._context.deleteProgram(this._ref); } } }; /** * Utility functions for handling GLSL shaders and programs. */ TiltGL.ProgramUtils = { /** * Initializes a shader program, using specified source code as strings, * returning the newly created shader program, by compiling and linking the * vertex and fragment shader. * * @param {Object} aContext * a WebGL context * @param {Object} aProperties * an object containing the following properties: * {String} vs: the vertex shader source code * {String} fs: the fragment shader source code * {Array} attributes: an array of attributes as strings * {Array} uniforms: an array of uniforms as strings */ create: function TGLPU_create(aContext, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; // compile the two shaders let vertShader = this.compile(aContext, aProperties.vs, "vertex"); let fragShader = this.compile(aContext, aProperties.fs, "fragment"); let program = this.link(aContext, vertShader, fragShader); aContext.deleteShader(vertShader); aContext.deleteShader(fragShader); return this.cache(aContext, aProperties, program); }, /** * Compiles a shader source of a specific type, either vertex or fragment. * * @param {Object} aContext * a WebGL context * @param {String} aShaderSource * the source code for the shader * @param {String} aShaderType * the shader type ("vertex" or "fragment") * * @return {WebGLShader} the compiled shader */ compile: function TGLPU_compile(aContext, aShaderSource, aShaderType) { let gl = aContext, shader, status; // make sure the shader source is valid if ("string" !== typeof aShaderSource || aShaderSource.length < 1) { TiltUtils.Output.error( TiltUtils.L10n.get("compileShader.source.error")); return null; } // also make sure the necessary shader mime type is valid if (aShaderType === "vertex") { shader = gl.createShader(gl.VERTEX_SHADER); } else if (aShaderType === "fragment") { shader = gl.createShader(gl.FRAGMENT_SHADER); } else { TiltUtils.Output.error( TiltUtils.L10n.format("compileShader.type.error", [aShaderSource])); return null; } // set the shader source and compile it gl.shaderSource(shader, aShaderSource); gl.compileShader(shader); // remember the shader source (useful for debugging and caching) shader.src = aShaderSource; // verify the compile status; if something went wrong, log the error if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { status = gl.getShaderInfoLog(shader); TiltUtils.Output.error( TiltUtils.L10n.format("compileShader.compile.error", [status])); return null; } // return the newly compiled shader from the specified source return shader; }, /** * Links two compiled vertex or fragment shaders together to form a program. * * @param {Object} aContext * a WebGL context * @param {WebGLShader} aVertShader * the compiled vertex shader * @param {WebGLShader} aFragShader * the compiled fragment shader * * @return {WebGLProgram} the newly created and linked shader program */ link: function TGLPU_link(aContext, aVertShader, aFragShader) { let gl = aContext, program, status; // create a program and attach the compiled vertex and fragment shaders program = gl.createProgram(); // attach the vertex and fragment shaders to the program gl.attachShader(program, aVertShader); gl.attachShader(program, aFragShader); gl.linkProgram(program); // verify the link status; if something went wrong, log the error if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { status = gl.getProgramInfoLog(program); TiltUtils.Output.error( TiltUtils.L10n.format("linkProgram.error", [status])); return null; } // generate an id for the program program.id = this._count++; return program; }, /** * Caches shader attributes and uniforms as properties for a program object. * * @param {Object} aContext * a WebGL context * @param {Object} aProperties * an object containing the following properties: * {Array} attributes: optional, an array of attributes as strings * {Array} uniforms: optional, an array of uniforms as strings * @param {WebGLProgram} aProgram * the shader program used for caching * * @return {WebGLProgram} the same program */ cache: function TGLPU_cache(aContext, aProperties, aProgram) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; // make sure the attributes and uniforms cache objects are created aProgram.attributes = {}; aProgram.uniforms = {}; Object.defineProperty(aProgram.attributes, "length", { value: 0, writable: true, enumerable: false, configurable: true }); Object.defineProperty(aProgram.uniforms, "length", { value: 0, writable: true, enumerable: false, configurable: true }); let attr = aProperties.attributes; let unif = aProperties.uniforms; if (attr) { for (let i = 0, len = attr.length; i < len; i++) { // try to get a shader attribute from the program let param = attr[i]; let loc = aContext.getAttribLocation(aProgram, param); if ("number" === typeof loc && loc > -1) { // if we get an attribute location, store it // bind the new parameter only if it was not already defined if (aProgram.attributes[param] === undefined) { aProgram.attributes[param] = loc; aProgram.attributes.length++; } } } } if (unif) { for (let i = 0, len = unif.length; i < len; i++) { // try to get a shader uniform from the program let param = unif[i]; let loc = aContext.getUniformLocation(aProgram, param); if ("object" === typeof loc && loc) { // if we get a uniform object, store it // bind the new parameter only if it was not already defined if (aProgram.uniforms[param] === undefined) { aProgram.uniforms[param] = loc; aProgram.uniforms.length++; } } } } return aProgram; }, /** * The total number of programs created. */ _count: 0, /** * Represents the current active shader, identified by an id. */ _activeProgram: -1, /** * Represents the current enabled attributes. */ _enabledAttributes: [] }; /** * This constructor creates a texture from an Image. * * @param {Object} aContext * a WebGL context * @param {Object} aProperties * optional, an object containing the following properties: * {Image} source: the source image for the texture * {String} format: the format of the texture ("RGB" or "RGBA") * {String} fill: optional, color to fill the transparent bits * {String} stroke: optional, color to draw an outline * {Number} strokeWeight: optional, the width of the outline * {String} minFilter: either "nearest" or "linear" * {String} magFilter: either "nearest" or "linear" * {String} wrapS: either "repeat" or "clamp" * {String} wrapT: either "repeat" or "clamp" * {Boolean} mipmap: true if should generate mipmap */ TiltGL.Texture = function(aContext, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; /** * The parent WebGL context. */ this._context = aContext; /** * A reference to the WebGL texture object. */ this._ref = null; /** * Each texture has an unique id assigned. */ this._id = -1; /** * Variables specifying the width and height of the texture. * If these values are less than 0, the texture hasn't loaded yet. */ this.width = -1; this.height = -1; /** * Specifies if the texture has loaded or not. */ this.loaded = false; // if the image is specified in the constructor, initialize directly if ("object" === typeof aProperties.source) { this.initTexture(aProperties); } else { TiltUtils.Output.error( TiltUtils.L10n.get("initTexture.source.error")); } }; TiltGL.Texture.prototype = { /** * Initializes a texture from a pre-existing image or canvas. * * @param {Image} aImage * the source image or canvas * @param {Object} aProperties * an object containing the following properties: * {Image} source: the source image for the texture * {String} format: the format of the texture ("RGB" or "RGBA") * {String} fill: optional, color to fill the transparent bits * {String} stroke: optional, color to draw an outline * {Number} strokeWeight: optional, the width of the outline * {String} minFilter: either "nearest" or "linear" * {String} magFilter: either "nearest" or "linear" * {String} wrapS: either "repeat" or "clamp" * {String} wrapT: either "repeat" or "clamp" * {Boolean} mipmap: true if should generate mipmap */ initTexture: function TGLT_initTexture(aProperties) { this._ref = TiltGL.TextureUtils.create(this._context, aProperties); // cache for faster access this._id = this._ref.id; this.width = this._ref.width; this.height = this._ref.height; this.loaded = true; // cleanup delete this._ref.id; delete this._ref.width; delete this._ref.height; delete this.onload; }, /** * Function called when this object is destroyed. */ finalize: function TGLT_finalize() { if (this._context) { this._context.deleteTexture(this._ref); } } }; /** * Utility functions for creating and manipulating textures. */ TiltGL.TextureUtils = { /** * Initializes a texture from a pre-existing image or canvas. * * @param {Object} aContext * a WebGL context * @param {Image} aImage * the source image or canvas * @param {Object} aProperties * an object containing some of the following properties: * {Image} source: the source image for the texture * {String} format: the format of the texture ("RGB" or "RGBA") * {String} fill: optional, color to fill the transparent bits * {String} stroke: optional, color to draw an outline * {Number} strokeWeight: optional, the width of the outline * {String} minFilter: either "nearest" or "linear" * {String} magFilter: either "nearest" or "linear" * {String} wrapS: either "repeat" or "clamp" * {String} wrapT: either "repeat" or "clamp" * {Boolean} mipmap: true if should generate mipmap * * @return {WebGLTexture} the created texture */ create: function TGLTU_create(aContext, aProperties) { // make sure the properties argument is an object aProperties = aProperties || {}; if (!aProperties.source) { return null; } let gl = aContext; let width = aProperties.source.width; let height = aProperties.source.height; let format = gl[aProperties.format || "RGB"]; // make sure the image is power of two before binding to a texture let source = this.resizeImageToPowerOfTwo(aProperties); // first, create the texture to hold the image data let texture = gl.createTexture(); // attach the image data to the newly create texture gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, format, format, gl.UNSIGNED_BYTE, source); this.setTextureParams(gl, aProperties); // do some cleanup gl.bindTexture(gl.TEXTURE_2D, null); // remember the width and the height texture.width = width; texture.height = height; // generate an id for the texture texture.id = this._count++; return texture; }, /** * Sets texture parameters for the current texture binding. * Optionally, you can also (re)set the current texture binding manually. * * @param {Object} aContext * a WebGL context * @param {Object} aProperties * an object containing the texture properties */ setTextureParams: function TGLTU_setTextureParams(aContext, aProperties) { // make sure the properties argument is an object aProperties = aProperties || {}; let gl = aContext; let minFilter = gl.TEXTURE_MIN_FILTER; let magFilter = gl.TEXTURE_MAG_FILTER; let wrapS = gl.TEXTURE_WRAP_S; let wrapT = gl.TEXTURE_WRAP_T; // bind a new texture if necessary if (aProperties.texture) { gl.bindTexture(gl.TEXTURE_2D, aProperties.texture.ref); } // set the minification filter if ("nearest" === aProperties.minFilter) { gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.NEAREST); } else if ("linear" === aProperties.minFilter && aProperties.mipmap) { gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.LINEAR_MIPMAP_LINEAR); } else { gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.LINEAR); } // set the magnification filter if ("nearest" === aProperties.magFilter) { gl.texParameteri(gl.TEXTURE_2D, magFilter, gl.NEAREST); } else { gl.texParameteri(gl.TEXTURE_2D, magFilter, gl.LINEAR); } // set the wrapping on the x-axis for the texture if ("repeat" === aProperties.wrapS) { gl.texParameteri(gl.TEXTURE_2D, wrapS, gl.REPEAT); } else { gl.texParameteri(gl.TEXTURE_2D, wrapS, gl.CLAMP_TO_EDGE); } // set the wrapping on the y-axis for the texture if ("repeat" === aProperties.wrapT) { gl.texParameteri(gl.TEXTURE_2D, wrapT, gl.REPEAT); } else { gl.texParameteri(gl.TEXTURE_2D, wrapT, gl.CLAMP_TO_EDGE); } // generate mipmap if necessary if (aProperties.mipmap) { gl.generateMipmap(gl.TEXTURE_2D); } }, /** * This shim renders a content window to a canvas element, but clamps the * maximum width and height of the canvas to the WebGL MAX_TEXTURE_SIZE. * * @param {Window} aContentWindow * the content window to get a texture from * @param {Number} aMaxImageSize * the maximum image size to be used * * @return {Image} the new content window image */ createContentImage: function TGLTU_createContentImage( aContentWindow, aMaxImageSize) { // calculate the total width and height of the content page let size = TiltUtils.DOM.getContentWindowDimensions(aContentWindow); // use a custom canvas element and a 2d context to draw the window let canvas = TiltUtils.DOM.initCanvas(null); canvas.width = TiltMath.clamp(size.width, 0, aMaxImageSize); canvas.height = TiltMath.clamp(size.height, 0, aMaxImageSize); // use the 2d context.drawWindow() magic let ctx = canvas.getContext("2d"); ctx.drawWindow(aContentWindow, 0, 0, canvas.width, canvas.height, "#fff"); return canvas; }, /** * Scales an image's width and height to next power of two. * If the image already has power of two sizes, it is immediately returned, * otherwise, a new image is created. * * @param {Image} aImage * the image to be scaled * @param {Object} aProperties * an object containing the following properties: * {Image} source: the source image to resize * {Boolean} resize: true to resize the image if it has npot dimensions * {String} fill: optional, color to fill the transparent bits * {String} stroke: optional, color to draw an image outline * {Number} strokeWeight: optional, the width of the outline * * @return {Image} the resized image */ resizeImageToPowerOfTwo: function TGLTU_resizeImageToPowerOfTwo(aProperties) { // make sure the properties argument is an object aProperties = aProperties || {}; if (!aProperties.source) { return null; } let isPowerOfTwoWidth = TiltMath.isPowerOfTwo(aProperties.source.width); let isPowerOfTwoHeight = TiltMath.isPowerOfTwo(aProperties.source.height); // first check if the image is not already power of two if (!aProperties.resize || (isPowerOfTwoWidth && isPowerOfTwoHeight)) { return aProperties.source; } // calculate the power of two dimensions for the npot image let width = TiltMath.nextPowerOfTwo(aProperties.source.width); let height = TiltMath.nextPowerOfTwo(aProperties.source.height); // create a canvas, then we will use a 2d context to scale the image let canvas = TiltUtils.DOM.initCanvas(null, { width: width, height: height }); let ctx = canvas.getContext("2d"); // optional fill (useful when handling transparent images) if (aProperties.fill) { ctx.fillStyle = aProperties.fill; ctx.fillRect(0, 0, width, height); } // draw the image with power of two dimensions ctx.drawImage(aProperties.source, 0, 0, width, height); // optional stroke (useful when creating textures for edges) if (aProperties.stroke) { ctx.strokeStyle = aProperties.stroke; ctx.lineWidth = aProperties.strokeWeight; ctx.strokeRect(0, 0, width, height); } return canvas; }, /** * The total number of textures created. */ _count: 0 }; /** * A color shader. The only useful thing it does is set the gl_FragColor. * * @param {Attribute} vertexPosition: the vertex position * @param {Uniform} mvMatrix: the model view matrix * @param {Uniform} projMatrix: the projection matrix * @param {Uniform} color: the color to set the gl_FragColor to */ TiltGL.ColorShader = { /** * Vertex shader. */ vs: [ "attribute vec3 vertexPosition;", "uniform mat4 mvMatrix;", "uniform mat4 projMatrix;", "void main() {", " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);", "}" ].join("\n"), /** * Fragment shader. */ fs: [ "#ifdef GL_ES", "precision lowp float;", "#endif", "uniform vec4 fill;", "void main() {", " gl_FragColor = fill;", "}" ].join("\n") }; TiltGL.isWebGLForceEnabled = function TGL_isWebGLForceEnabled() { return Services.prefs.getBoolPref("webgl.force-enabled"); }; /** * Tests if the WebGL OpenGL or Angle renderer is available using the * GfxInfo service. * * @return {Boolean} true if WebGL is available */ TiltGL.isWebGLSupported = function TGL_isWebGLSupported() { let supported = false; try { let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); let angle = gfxInfo.FEATURE_WEBGL_ANGLE; let opengl = gfxInfo.FEATURE_WEBGL_OPENGL; // if either the Angle or OpenGL renderers are available, WebGL should work supported = gfxInfo.getFeatureStatus(angle) === gfxInfo.FEATURE_NO_INFO || gfxInfo.getFeatureStatus(opengl) === gfxInfo.FEATURE_NO_INFO; } catch(e) { if (e && e.message) { TiltUtils.Output.error(e.message); } return false; } return supported; }; /** * Helper function to create a 3D context. * * @param {HTMLCanvasElement} aCanvas * the canvas to get the WebGL context from * @param {Object} aFlags * optional, flags used for initialization * * @return {Object} the WebGL context, or null if anything failed */ TiltGL.create3DContext = function TGL_create3DContext(aCanvas, aFlags) { TiltGL.clearCache(); // try to get a valid context from an existing canvas let context = null; try { context = aCanvas.getContext(WEBGL_CONTEXT_NAME, aFlags); } catch(e) { if (e && e.message) { TiltUtils.Output.error(e.message); } return null; } return context; }; /** * Clears the cache and sets all the variables to default. */ TiltGL.clearCache = function TGL_clearCache() { TiltGL.ProgramUtils._activeProgram = -1; TiltGL.ProgramUtils._enabledAttributes = []; };