Skip to content

Instantly share code, notes, and snippets.

@bradley
Created August 11, 2015 05:07
Show Gist options
  • Save bradley/638583ee76ff9ee7283e to your computer and use it in GitHub Desktop.
Save bradley/638583ee76ff9ee7283e to your computer and use it in GitHub Desktop.
/* requestAnimationFrame polyfill
---------------------------------------------------------------------------------*/
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
}());
var DEAD = {};
/* POSITION
---------------------------------------------------------------------------------*/
DEAD.Position = function () {
}
DEAD.Position.prototype = {
constructor: DEAD.Position,
x: 0.0, y: 0.0, z: 0.0,
set: function(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
},
getVec3: function() {
return [this.x, this.y, this.z];
}
}
/* RENDERER
---------------------------------------------------------------------------------*/
DEAD.Renderer = function (canvas, opts) {
this.canvas = canvas;
this.errors = [];
if (!canvas || canvas.nodeName != "CANVAS") {
this.errors.push("No valid canvas passed to DEAD.Renderer");
return;
}
this.gl = this.load(opts);
this.clear();
}
DEAD.Renderer.prototype = {
constructor: DEAD.Renderer,
load : function(options) {
var gl = null;
try {
// Try to grab the standard context. If it fails, fallback to experimental.
gl = this.canvas.getContext("webgl", options) || cthis.anvas.getContext("experimental-webgl", options);
}
catch(e) {}
// If we don't have a GL context, give up now
if (!gl) {
this.errors.push("Unable to initialize WebGL. Your browser may not support it.");
}
return gl;
},
clear : function(color) {
color = color || [0.0, 0.0, 0.0, 1.0]
this.gl.clearColor.apply(this.gl, color); // Set clear color to black, fully opaque
this.gl.enable(this.gl.DEPTH_TEST); // Enable depth testing (hide pixels that should be behind others)
this.gl.depthFunc(this.gl.LEQUAL); // Near things obscure far things
this.gl.clear(this.gl.COLOR_BUFFER_BIT|this.gl.DEPTH_BUFFER_BIT); // Clear the color as well as the depth buffer.
},
draw : function(scene, opts) {
opts = opts || {};
_.defaults(opts, {
clear: true
});
if (!(scene instanceof DEAD.Scene)) {
return console.error("scene must be of type DEAD.Scene");
}
if (opts.clear) {
this.clear();
}
scene.render(this);
}
}
/* RENDER CONTEXT
---------------------------------------------------------------------------------*/
DEAD.RenderContext = function () {
}
DEAD.RenderContext.prototype = (function() {
var _renderer;
return {
constructor: DEAD.RenderContext,
set: function(renderer) {
if (!(renderer instanceof DEAD.Renderer)) {
return console.error("renderer must be of type DEAD.Renderer");
}
_renderer = renderer;
},
getGl : function() {
return this.getRenderer().gl;
},
getRenderer : function() {
if (!(_renderer instanceof DEAD.Renderer)) {
return console.error("renderer of type DEAD.Renderer has not yet been set for DEAD.RenderContext");
}
return _renderer;
}
}
})();
/* SCENE
---------------------------------------------------------------------------------*/
DEAD.Scene = function () {
this.materials = [];
this.camera;
}
DEAD.Scene.prototype = (function() {
function drawChildren(renderer) {
var gl = renderer.gl;
_.each(this.materials, _.bind(function(material) {
material.context.set(renderer);
material.draw(this.camera);
}, this));
}
return {
constructor: DEAD.Scene,
add : function(addon) {
if (addon instanceof DEAD.Material) {
this.materials.push(addon);
}
else if (addon instanceof DEAD.Camera) {
this.camera = addon;
}
else {
return console.error("elements added to DEAD.Scene may only be of type DEAD.Material or DEAD.Camera");
}
},
remove : function(addon) {
if (addon instanceof DEAD.Material) {
var index = this.materials.indexOf(material);
if (index > -1) {
this.materials.splice(index, 1);
}
}
else if (addon instanceof DEAD.Camera) {
this.camera = null;
}
},
render : function(renderer) {
if (!(renderer instanceof DEAD.Renderer)) {
return console.error("scene cannot render without a valid DEAD.Renderer");
}
drawChildren.apply(this, [renderer]);
}
}
})();
/* CAMERA
---------------------------------------------------------------------------------*/
DEAD.Camera = function(fov, aspectRatio, near, far) {
this.position = new DEAD.Position();
this.position.set(0, 0, 6);
this.focus = new DEAD.Position();
this.viewMatrix = mat4.create();
this.projectionMatrix = this.setProjectionMatrix(fov, aspectRatio, near, far);
this.setProjectionMatrix();
this.update();
}
DEAD.Camera.prototype = (function() {
return {
constructor: DEAD.Camera,
setProjectionMatrix: function(fov, aspectRatio, near, far) {
var matrix = mat4.create();
mat4.perspective(matrix, fov, aspectRatio, near, far);
return matrix;
},
update: function() {
var position = this.position.getVec3(),
focus = this.focus.getVec3(),
up = vec3.fromValues(0, 1, 0);
// Update View Matrix (camera translation).
mat4.lookAt(this.viewMatrix, position, focus, up);
}
}
})();
/* PROGRAM
---------------------------------------------------------------------------------*/
DEAD.Program = function (vertexSrc, fragmentSrc, opts) {
opts = opts || {}
_.defaults(opts, {
defaultUniforms : {
time: true
}
});
this.vertexSrc = vertexSrc;
this.fragmentSrc = fragmentSrc;
this.opts = opts;
this.glslProgram;
this.programAttributes;
this.programUniforms;
this.context = new DEAD.RenderContext();
this.buffersToBind = [];
this.startTime = null;
}
DEAD.Program.prototype = (function() {
function compileShader(gl, type, shaderSrc) {
var shader = gl.createShader(type);
gl.shaderSource(shader, shaderSrc);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader;
}
function buildProgram(gl, vertexSrc, fragmentSrc) {
var vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSrc),
fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc),
program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}
return program;
}
function buildAttributesObject(gl, program) {
var attributesObject = {},
numAttributes;
numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
for (var i = 0; i < numAttributes; i++) {
var name = gl.getActiveAttrib(program, i).name;
attributesObject[name] = gl.getAttribLocation(program, name);
}
return attributesObject;
}
function buildUniformsObject(gl, program) {
var uniformsObject = {},
numUniforms;
numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (var i = 0; i < numUniforms; i++) {
var name = gl.getActiveUniform(program, i).name;
uniformsObject[name] = gl.getUniformLocation(program, name);
}
return uniformsObject;
}
function bindBuffers(gl, buffersToBind, attributes) {
_.each(buffersToBind, function(bufferData) {
var buffer = gl.createBuffer();
if (!buffer) {
return console.error("unable to create vertex buffer");
}
if (!attributes[bufferData.attribute]) {
return console.error("attribute " + bufferData.attribute + " not found in program");
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, bufferData.data, gl.STATIC_DRAW);
gl.vertexAttribPointer(attributes[bufferData.attribute], bufferData.elemPerVertex, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(attributes[bufferData.attribute]);
});
}
return {
constructor: DEAD.Program,
build : function() {
var gl = this.context.getGl();
this.glslProgram = buildProgram(gl, this.vertexSrc, this.fragmentSrc);
this.programAttributes = buildAttributesObject(gl, this.glslProgram);
this.programUniforms = buildUniformsObject(gl, this.glslProgram);
},
bindBuffer : function(data, elemPerVertex, attribute) {
// Ex:
// program.bindBuffer(new Float32Array([
// 1.0, 0.0, 0.0, 0.0,
// 0.0, 1.0, 0.0, 0.0,
// 0.1, 0.4, 0.6, 0.0,
// 0.0, 0.0, 1.0, 0.0]), 4, program.attributes.aVertColor);
if (!(data instanceof Float32Array)) {
return console.error("data must be of type Float32Array");
}
this.buffersToBind.push({
data: data,
elemPerVertex: elemPerVertex,
attribute: attribute
});
},
update : function() {
if (!this.startTime) {
this.startTime = new Date().getTime()
}
bindBuffers(this.context.getGl(), this.buffersToBind, this.programAttributes);
if (this.opts.defaultUniforms.time && this.programUniforms.uTime) {
var time = ((new Date()).getTime() - this.startTime) / 1000;
this.context.getGl().uniform1f(this.programUniforms.uTime, time);
}
}
}
})();
/* MATERIAL
---------------------------------------------------------------------------------*/
DEAD.Material = function (program, geometry) {
this.program = program;
this.geometry = geometry;
this.context = new DEAD.RenderContext();
if (!this.program || !(this.program instanceof DEAD.Program)) {
return console.error("program must be of type DEAD.Program");
}
if (!this.geometry || !(this.geometry instanceof DEAD.Geometry)) {
return console.error("geometry must be of type DEAD.Geometry");
}
}
DEAD.Material.prototype = (function() {
function bindBuffers() {
var gl = this.context.getGl(),
bytes = this.geometry.vertices.BYTES_PER_ELEMENT,
stride = 10 * bytes,
vertexBuffer = gl.createBuffer(),
indexBuffer = gl.createBuffer();
if (!vertexBuffer) {
return console.error("unable to create vertex buffer");
}
if (!vertexBuffer) {
return console.error("unable to create index buffer");
}
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.geometry.vertices, gl.STATIC_DRAW);
// TODO: If we add a 'prefix' attribute to DEAD.Material, we could mitigate
// the risk that these attributes may be overwritten by other materials working
// within the same program.
setVertexAttribute(gl, this.program.programAttributes.aPosition, 4, stride, 0);
setVertexAttribute(gl, this.program.programAttributes.aColor, 4, stride, 4 * bytes);
setVertexAttribute(gl, this.program.programAttributes.aTexCoord, 2, stride, 8 * bytes);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.geometry.indices, gl.STATIC_DRAW);
}
function setVertexAttribute(gl, attributeIndex, size, stride, offset) {
gl.vertexAttribPointer(attributeIndex, size, gl.FLOAT, false, stride, offset);
gl.enableVertexAttribArray(attributeIndex);
}
function updateModelViewProjectionMatrixForCamera(camera) {
if (camera instanceof DEAD.Camera && this.program.programUniforms.uMVPMatrix) {
var matrix = this.geometry.modelViewProjectionMatrix(
camera.viewMatrix,
camera.projectionMatrix,
this.context.getRenderer().delta
);
this.context.getGl().uniformMatrix4fv(this.program.programUniforms.uMVPMatrix, false, matrix)
}
}
return {
constructor: DEAD.Material,
draw: function(camera) {
var gl = this.context.getGl();
this.program.context = this.context;
this.program.build();
gl.useProgram(this.program.glslProgram);
this.program.update();
bindBuffers.apply(this);
updateModelViewProjectionMatrixForCamera.apply(this, [camera]);
gl.drawElements(gl.TRIANGLES, this.geometry.indices.length, gl.UNSIGNED_SHORT, 0);
}
}
})();
/* GEOMETRIES
---------------------------------------------------------------------------------*/
DEAD.Geometry = function () {
this.type = "Geometry"
this.vertices = new Float32Array([]);
this.indices = new Int16Array([]);
this.position = new DEAD.Position();
this.rotation = new DEAD.Position();
this.scale = new DEAD.Position();
this.scale.set(1, 1, 1);
this.modelMatrix = this.currentModelMatrix();
}
DEAD.Geometry.prototype = (function() {
function degreesToRadians(d) {
return d * Math.PI / 180;
}
return {
constructor: DEAD.Geometry,
currentModelMatrix: function(delta) {
var m = mat4.create(),
rotation = this.rotation.getVec3();
position = this.position.getVec3();
scale = this.scale.getVec3();
mat4.rotateX(m, m, degreesToRadians(rotation[0]));
mat4.rotateY(m, m, degreesToRadians(rotation[1]));
mat4.rotateZ(m, m, degreesToRadians(rotation[2]));
mat4.translate(m, m, position);
mat4.scale(m, m, scale);
return m;
},
modelViewProjectionMatrix: function(viewMatrix, projectionMatrix, delta) {
var m = mat4.create();
mat4.multiply(m, projectionMatrix, viewMatrix);
mat4.multiply(m, m, this.currentModelMatrix(delta));
return m;
}
}
})();
DEAD.PlaneGeometry = function () {
DEAD.Geometry.call(this);
this.type = 'PlaneGeometry';
this.vertices = new Float32Array([
// x y z r g b a uvX uvY
-0.5, -0.5, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0,
0.5, -0.5, 0.0, 1.0, 0.1, 0.4, 0.6, 1.0, 1.0, 1.0,
0.5, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0,
-0.5, 0.5, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0
]);
/*
3.......2
. ..
. . .
.. .
0.......1
*/
this.indices = new Int16Array([
0, 1, 2, 2, 3, 0
]);
};
DEAD.PlaneGeometry.prototype = Object.create( DEAD.Geometry.prototype );
DEAD.PlaneGeometry.prototype.constructor = DEAD.PlaneGeometry;
DEAD.TriangleGeometry = function () {
DEAD.Geometry.call(this);
this.type = 'TriangleGeometry';
this.vertices = new Float32Array([
// x y z r g b a uvX uvY
-0.5, -0.5, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0,
0.5, -0.5, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0,
0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0
]);
/*
2
..
. .
. .
0......1
*/
this.indices = new Int16Array([
0, 1, 2
]);
}
DEAD.TriangleGeometry.prototype = Object.create( DEAD.Geometry.prototype );
DEAD.TriangleGeometry.prototype.constructor = DEAD.TriangleGeometry;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment