gecko-dev/browser/devtools/tilt/tilt-gl.js

1596 lines
46 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 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_STATUS_OK ||
gfxInfo.getFeatureStatus(opengl) === gfxInfo.FEATURE_STATUS_OK;
} 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 = [];
};