//============================================================ // // Copyright (C) 2013 Matthew Wagerfield // // Twitter: https://twitter.com/mwagerfield // // Permission is hereby granted, free of charge, to any // person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the // Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice // shall be included in all copies or substantial portions // of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY // OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO // EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN // AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE // OR OTHER DEALINGS IN THE SOFTWARE. // //============================================================ /** * Defines the Flat Surface Shader namespace for all the awesomeness to exist upon. * @author Matthew Wagerfield */ FSS = { FRONT : 0, BACK : 1, DOUBLE : 2, SVGNS : 'http://www.w3.org/2000/svg' }; /** * @class Array * @author Matthew Wagerfield */ FSS.Array = typeof Float32Array === 'function' ? Float32Array : Array; /** * @class Utils * @author Matthew Wagerfield */ FSS.Utils = { isNumber: function(value) { return !isNaN(parseFloat(value)) && isFinite(value); } }; /** * Request Animation Frame Polyfill. * @author Paul Irish * @see https://gist.github.com/paulirish/1579671 */ (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 currentTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currentTime - lastTime)); var id = window.setTimeout(function() { callback(currentTime + timeToCall); }, timeToCall); lastTime = currentTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }()); /** * @object Math Augmentation * @author Matthew Wagerfield */ Math.PIM2 = Math.PI*2; Math.PID2 = Math.PI/2; Math.randomInRange = function(min, max) { return min + (max - min) * Math.random(); }; Math.clamp = function(value, min, max) { value = Math.max(value, min); value = Math.min(value, max); return value; }; /** * @object Vector3 * @author Matthew Wagerfield */ FSS.Vector3 = { create: function(x, y, z) { var vector = new FSS.Array(3); this.set(vector, x, y, z); return vector; }, clone: function(a) { var vector = this.create(); this.copy(vector, a); return vector; }, set: function(target, x, y, z) { target[0] = x || 0; target[1] = y || 0; target[2] = z || 0; return this; }, setX: function(target, x) { target[0] = x || 0; return this; }, setY: function(target, y) { target[1] = y || 0; return this; }, setZ: function(target, z) { target[2] = z || 0; return this; }, copy: function(target, a) { target[0] = a[0]; target[1] = a[1]; target[2] = a[2]; return this; }, add: function(target, a) { target[0] += a[0]; target[1] += a[1]; target[2] += a[2]; return this; }, addVectors: function(target, a, b) { target[0] = a[0] + b[0]; target[1] = a[1] + b[1]; target[2] = a[2] + b[2]; return this; }, addScalar: function(target, s) { target[0] += s; target[1] += s; target[2] += s; return this; }, subtract: function(target, a) { target[0] -= a[0]; target[1] -= a[1]; target[2] -= a[2]; return this; }, subtractVectors: function(target, a, b) { target[0] = a[0] - b[0]; target[1] = a[1] - b[1]; target[2] = a[2] - b[2]; return this; }, subtractScalar: function(target, s) { target[0] -= s; target[1] -= s; target[2] -= s; return this; }, multiply: function(target, a) { target[0] *= a[0]; target[1] *= a[1]; target[2] *= a[2]; return this; }, multiplyVectors: function(target, a, b) { target[0] = a[0] * b[0]; target[1] = a[1] * b[1]; target[2] = a[2] * b[2]; return this; }, multiplyScalar: function(target, s) { target[0] *= s; target[1] *= s; target[2] *= s; return this; }, divide: function(target, a) { target[0] /= a[0]; target[1] /= a[1]; target[2] /= a[2]; return this; }, divideVectors: function(target, a, b) { target[0] = a[0] / b[0]; target[1] = a[1] / b[1]; target[2] = a[2] / b[2]; return this; }, divideScalar: function(target, s) { if (s !== 0) { target[0] /= s; target[1] /= s; target[2] /= s; } else { target[0] = 0; target[1] = 0; target[2] = 0; } return this; }, cross: function(target, a) { var x = target[0]; var y = target[1]; var z = target[2]; target[0] = y*a[2] - z*a[1]; target[1] = z*a[0] - x*a[2]; target[2] = x*a[1] - y*a[0]; return this; }, crossVectors: function(target, a, b) { target[0] = a[1]*b[2] - a[2]*b[1]; target[1] = a[2]*b[0] - a[0]*b[2]; target[2] = a[0]*b[1] - a[1]*b[0]; return this; }, min: function(target, value) { if (target[0] < value) { target[0] = value; } if (target[1] < value) { target[1] = value; } if (target[2] < value) { target[2] = value; } return this; }, max: function(target, value) { if (target[0] > value) { target[0] = value; } if (target[1] > value) { target[1] = value; } if (target[2] > value) { target[2] = value; } return this; }, clamp: function(target, min, max) { this.min(target, min); this.max(target, max); return this; }, limit: function(target, min, max) { var length = this.length(target); if (min !== null && length < min) { this.setLength(target, min); } else if (max !== null && length > max) { this.setLength(target, max); } return this; }, dot: function(a, b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }, normalise: function(target) { return this.divideScalar(target, this.length(target)); }, negate: function(target) { return this.multiplyScalar(target, -1); }, distanceSquared: function(a, b) { var dx = a[0] - b[0]; var dy = a[1] - b[1]; var dz = a[2] - b[2]; return dx*dx + dy*dy + dz*dz; }, distance: function(a, b) { return Math.sqrt(this.distanceSquared(a, b)); }, lengthSquared: function(a) { return a[0]*a[0] + a[1]*a[1] + a[2]*a[2]; }, length: function(a) { return Math.sqrt(this.lengthSquared(a)); }, setLength: function(target, l) { var length = this.length(target); if (length !== 0 && l !== length) { this.multiplyScalar(target, l / length); } return this; } }; /** * @object Vector4 * @author Matthew Wagerfield */ FSS.Vector4 = { create: function(x, y, z, w) { var vector = new FSS.Array(4); this.set(vector, x, y, z); return vector; }, set: function(target, x, y, z, w) { target[0] = x || 0; target[1] = y || 0; target[2] = z || 0; target[3] = w || 0; return this; }, setX: function(target, x) { target[0] = x || 0; return this; }, setY: function(target, y) { target[1] = y || 0; return this; }, setZ: function(target, z) { target[2] = z || 0; return this; }, setW: function(target, w) { target[3] = w || 0; return this; }, add: function(target, a) { target[0] += a[0]; target[1] += a[1]; target[2] += a[2]; target[3] += a[3]; return this; }, multiplyVectors: function(target, a, b) { target[0] = a[0] * b[0]; target[1] = a[1] * b[1]; target[2] = a[2] * b[2]; target[3] = a[3] * b[3]; return this; }, multiplyScalar: function(target, s) { target[0] *= s; target[1] *= s; target[2] *= s; target[3] *= s; return this; }, min: function(target, value) { if (target[0] < value) { target[0] = value; } if (target[1] < value) { target[1] = value; } if (target[2] < value) { target[2] = value; } if (target[3] < value) { target[3] = value; } return this; }, max: function(target, value) { if (target[0] > value) { target[0] = value; } if (target[1] > value) { target[1] = value; } if (target[2] > value) { target[2] = value; } if (target[3] > value) { target[3] = value; } return this; }, clamp: function(target, min, max) { this.min(target, min); this.max(target, max); return this; } }; /** * @class Color * @author Matthew Wagerfield */ FSS.Color = function(hex, opacity) { this.rgba = FSS.Vector4.create(); this.hex = hex || '#000000'; this.opacity = FSS.Utils.isNumber(opacity) ? opacity : 1; this.set(this.hex, this.opacity); }; FSS.Color.prototype = { set: function(hex, opacity) { hex = hex.replace('#', ''); var size = hex.length / 3; this.rgba[0] = parseInt(hex.substring(size*0, size*1), 16) / 255; this.rgba[1] = parseInt(hex.substring(size*1, size*2), 16) / 255; this.rgba[2] = parseInt(hex.substring(size*2, size*3), 16) / 255; this.rgba[3] = FSS.Utils.isNumber(opacity) ? opacity : this.rgba[3]; return this; }, hexify: function(channel) { var hex = Math.ceil(channel*255).toString(16); if (hex.length === 1) { hex = '0' + hex; } return hex; }, format: function() { var r = this.hexify(this.rgba[0]); var g = this.hexify(this.rgba[1]); var b = this.hexify(this.rgba[2]); this.hex = '#' + r + g + b; return this.hex; } }; /** * @class Object * @author Matthew Wagerfield */ FSS.Object = function() { this.position = FSS.Vector3.create(); }; FSS.Object.prototype = { setPosition: function(x, y, z) { FSS.Vector3.set(this.position, x, y, z); return this; } }; /** * @class Light * @author Matthew Wagerfield */ FSS.Light = function(ambient, diffuse) { FSS.Object.call(this); this.ambient = new FSS.Color(ambient || '#FFFFFF'); this.diffuse = new FSS.Color(diffuse || '#FFFFFF'); this.ray = FSS.Vector3.create(); }; FSS.Light.prototype = Object.create(FSS.Object.prototype); /** * @class Vertex * @author Matthew Wagerfield */ FSS.Vertex = function(x, y, z) { this.position = FSS.Vector3.create(x, y, z); }; FSS.Vertex.prototype = { setPosition: function(x, y, z) { FSS.Vector3.set(this.position, x, y, z); return this; } }; /** * @class Triangle * @author Matthew Wagerfield */ FSS.Triangle = function(a, b, c) { this.a = a || new FSS.Vertex(); this.b = b || new FSS.Vertex(); this.c = c || new FSS.Vertex(); this.vertices = [this.a, this.b, this.c]; this.u = FSS.Vector3.create(); this.v = FSS.Vector3.create(); this.centroid = FSS.Vector3.create(); this.normal = FSS.Vector3.create(); this.color = new FSS.Color(); this.polygon = document.createElementNS(FSS.SVGNS, 'polygon'); this.polygon.setAttributeNS(null, 'stroke-linejoin', 'round'); this.polygon.setAttributeNS(null, 'stroke-miterlimit', '1'); this.polygon.setAttributeNS(null, 'stroke-width', '1'); this.computeCentroid(); this.computeNormal(); }; FSS.Triangle.prototype = { computeCentroid: function() { this.centroid[0] = this.a.position[0] + this.b.position[0] + this.c.position[0]; this.centroid[1] = this.a.position[1] + this.b.position[1] + this.c.position[1]; this.centroid[2] = this.a.position[2] + this.b.position[2] + this.c.position[2]; FSS.Vector3.divideScalar(this.centroid, 3); return this; }, computeNormal: function() { FSS.Vector3.subtractVectors(this.u, this.b.position, this.a.position); FSS.Vector3.subtractVectors(this.v, this.c.position, this.a.position); FSS.Vector3.crossVectors(this.normal, this.u, this.v); FSS.Vector3.normalise(this.normal); return this; } }; /** * @class Geometry * @author Matthew Wagerfield */ FSS.Geometry = function() { this.vertices = []; this.triangles = []; this.dirty = false; }; FSS.Geometry.prototype = { update: function() { if (this.dirty) { var t,triangle; for (t = this.triangles.length - 1; t >= 0; t--) { triangle = this.triangles[t]; triangle.computeCentroid(); triangle.computeNormal(); } this.dirty = false; } return this; } }; /** * @class Plane * @author Matthew Wagerfield */ FSS.Plane = function(width, height, segments, slices) { FSS.Geometry.call(this); this.width = width || 100; this.height = height || 100; this.segments = segments || 4; this.slices = slices || 4; this.segmentWidth = this.width / this.segments; this.sliceHeight = this.height / this.slices; // Cache Variables var x, y, v0, v1, v2, v3, vertex, triangle, vertices = [], offsetX = this.width * -0.5, offsetY = this.height * 0.5; // Add Vertices for (x = 0; x <= this.segments; x++) { vertices.push([]); for (y = 0; y <= this.slices; y++) { vertex = new FSS.Vertex(offsetX + x*this.segmentWidth, offsetY - y*this.sliceHeight); vertices[x].push(vertex); this.vertices.push(vertex); } } // Add Triangles for (x = 0; x < this.segments; x++) { for (y = 0; y < this.slices; y++) { v0 = vertices[x+0][y+0]; v1 = vertices[x+0][y+1]; v2 = vertices[x+1][y+0]; v3 = vertices[x+1][y+1]; t0 = new FSS.Triangle(v0, v1, v2); t1 = new FSS.Triangle(v2, v1, v3); this.triangles.push(t0, t1); } } }; FSS.Plane.prototype = Object.create(FSS.Geometry.prototype); /** * @class Material * @author Matthew Wagerfield */ FSS.Material = function(ambient, diffuse) { this.ambient = new FSS.Color(ambient || '#444444'); this.diffuse = new FSS.Color(diffuse || '#FFFFFF'); this.slave = new FSS.Color(); }; /** * @class Mesh * @author Matthew Wagerfield */ FSS.Mesh = function(geometry, material) { FSS.Object.call(this); this.geometry = geometry || new FSS.Geometry(); this.material = material || new FSS.Material(); this.side = FSS.FRONT; this.visible = true; }; FSS.Mesh.prototype = Object.create(FSS.Object.prototype); FSS.Mesh.prototype.update = function(lights, calculate) { var t,triangle, l,light, illuminance; // Update Geometry this.geometry.update(); // Calculate the triangle colors if (calculate) { // Iterate through Triangles for (t = this.geometry.triangles.length - 1; t >= 0; t--) { triangle = this.geometry.triangles[t]; // Reset Triangle Color FSS.Vector4.set(triangle.color.rgba); // Iterate through Lights for (l = lights.length - 1; l >= 0; l--) { light = lights[l]; // Calculate Illuminance FSS.Vector3.subtractVectors(light.ray, light.position, triangle.centroid); FSS.Vector3.normalise(light.ray); illuminance = FSS.Vector3.dot(triangle.normal, light.ray); if (this.side === FSS.FRONT) { illuminance = Math.max(illuminance, 0); } else if (this.side === FSS.BACK) { illuminance = Math.abs(Math.min(illuminance, 0)); } else if (this.side === FSS.DOUBLE) { illuminance = Math.max(Math.abs(illuminance), 0); } // Calculate Ambient Light FSS.Vector4.multiplyVectors(this.material.slave.rgba, this.material.ambient.rgba, light.ambient.rgba); FSS.Vector4.add(triangle.color.rgba, this.material.slave.rgba); // Calculate Diffuse Light FSS.Vector4.multiplyVectors(this.material.slave.rgba, this.material.diffuse.rgba, light.diffuse.rgba); FSS.Vector4.multiplyScalar(this.material.slave.rgba, illuminance); FSS.Vector4.add(triangle.color.rgba, this.material.slave.rgba); } // Clamp & Format Color FSS.Vector4.clamp(triangle.color.rgba, 0, 1); } } return this; }; /** * @class Scene * @author Matthew Wagerfield */ FSS.Scene = function() { this.meshes = []; this.lights = []; }; FSS.Scene.prototype = { add: function(object) { if (object instanceof FSS.Mesh && !~this.meshes.indexOf(object)) { this.meshes.push(object); } else if (object instanceof FSS.Light && !~this.lights.indexOf(object)) { this.lights.push(object); } return this; }, remove: function(object) { if (object instanceof FSS.Mesh && ~this.meshes.indexOf(object)) { this.meshes.splice(this.meshes.indexOf(object), 1); } else if (object instanceof FSS.Light && ~this.lights.indexOf(object)) { this.lights.splice(this.lights.indexOf(object), 1); } return this; } }; /** * @class Renderer * @author Matthew Wagerfield */ FSS.Renderer = function() { this.width = 0; this.height = 0; this.halfWidth = 0; this.halfHeight = 0; }; FSS.Renderer.prototype = { setSize: function(width, height) { if (this.width === width && this.height === height) return; this.width = width; this.height = height; this.halfWidth = this.width * 0.5; this.halfHeight = this.height * 0.5; return this; }, clear: function() { return this; }, render: function(scene) { return this; } }; /** * @class Canvas Renderer * @author Matthew Wagerfield */ FSS.CanvasRenderer = function() { FSS.Renderer.call(this); this.element = document.createElement('canvas'); this.element.style.display = 'block'; this.context = this.element.getContext('2d'); this.setSize(this.element.width, this.element.height); }; FSS.CanvasRenderer.prototype = Object.create(FSS.Renderer.prototype); FSS.CanvasRenderer.prototype.setSize = function(width, height) { FSS.Renderer.prototype.setSize.call(this, width, height); this.element.width = width; this.element.height = height; this.context.setTransform(1, 0, 0, -1, this.halfWidth, this.halfHeight); return this; }; FSS.CanvasRenderer.prototype.clear = function() { FSS.Renderer.prototype.clear.call(this); this.context.clearRect(-this.halfWidth, -this.halfHeight, this.width, this.height); return this; }; FSS.CanvasRenderer.prototype.render = function(scene) { FSS.Renderer.prototype.render.call(this, scene); var m,mesh, t,triangle, color; // Clear Context this.clear(); // Configure Context this.context.lineJoin = 'round'; this.context.lineWidth = 1; // Update Meshes for (m = scene.meshes.length - 1; m >= 0; m--) { mesh = scene.meshes[m]; if (mesh.visible) { mesh.update(scene.lights, true); // Render Triangles for (t = mesh.geometry.triangles.length - 1; t >= 0; t--) { triangle = mesh.geometry.triangles[t]; color = triangle.color.format(); this.context.beginPath(); this.context.moveTo(triangle.a.position[0], triangle.a.position[1]); this.context.lineTo(triangle.b.position[0], triangle.b.position[1]); this.context.lineTo(triangle.c.position[0], triangle.c.position[1]); this.context.closePath(); this.context.strokeStyle = color; this.context.fillStyle = color; this.context.stroke(); this.context.fill(); } } } return this; }; /** * @class WebGL Renderer * @author Matthew Wagerfield */ FSS.WebGLRenderer = function() { FSS.Renderer.call(this); this.element = document.createElement('canvas'); this.element.style.display = 'block'; // Set initial vertex and light count this.vertices = null; this.lights = null; // Create parameters object var parameters = { preserveDrawingBuffer: false, premultipliedAlpha: true, antialias: true, stencil: true, alpha: true }; // Create and configure the gl context this.gl = this.getContext(this.element, parameters); // Set the internal support flag this.unsupported = !this.gl; // Setup renderer if (this.unsupported) { return 'WebGL is not supported by your browser.'; } else { this.gl.clearColor(0.0, 0.0, 0.0, 0.0); this.gl.enable(this.gl.DEPTH_TEST); this.setSize(this.element.width, this.element.height); } }; FSS.WebGLRenderer.prototype = Object.create(FSS.Renderer.prototype); FSS.WebGLRenderer.prototype.getContext = function(canvas, parameters) { var context = false; try { if (!(context = canvas.getContext('experimental-webgl', parameters))) { throw 'Error creating WebGL context.'; } } catch (error) { console.error(error); } return context; }; FSS.WebGLRenderer.prototype.setSize = function(width, height) { FSS.Renderer.prototype.setSize.call(this, width, height); if (this.unsupported) return; // Set the size of the canvas element this.element.width = width; this.element.height = height; // Set the size of the gl viewport this.gl.viewport(0, 0, width, height); return this; }; FSS.WebGLRenderer.prototype.clear = function() { FSS.Renderer.prototype.clear.call(this); if (this.unsupported) return; this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); return this; }; FSS.WebGLRenderer.prototype.render = function(scene) { FSS.Renderer.prototype.render.call(this, scene); if (this.unsupported) return; var m,mesh, t,tl,triangle, l,light, attribute, uniform, buffer, data, location, update = false, lights = scene.lights.length, index, v,vl,vetex,vertices = 0; // Clear context this.clear(); // Build the shader program if (this.lights !== lights) { this.lights = lights; if (this.lights > 0) { this.buildProgram(lights); } else { return; } } // Update program if (!!this.program) { // Increment vertex counter for (m = scene.meshes.length - 1; m >= 0; m--) { mesh = scene.meshes[m]; if (mesh.geometry.dirty) update = true; mesh.update(scene.lights, false); vertices += mesh.geometry.triangles.length*3; } // Compare vertex counter if (update || this.vertices !== vertices) { this.vertices = vertices; // Build buffers for (attribute in this.program.attributes) { buffer = this.program.attributes[attribute]; buffer.data = new FSS.Array(vertices*buffer.size); // Reset vertex index index = 0; // Update attribute buffer data for (m = scene.meshes.length - 1; m >= 0; m--) { mesh = scene.meshes[m]; for (t = 0, tl = mesh.geometry.triangles.length; t < tl; t++) { triangle = mesh.geometry.triangles[t]; for (v = 0, vl = triangle.vertices.length; v < vl; v++) { vertex = triangle.vertices[v]; switch (attribute) { case 'side': this.setBufferData(index, buffer, mesh.side); break; case 'position': this.setBufferData(index, buffer, vertex.position); break; case 'centroid': this.setBufferData(index, buffer, triangle.centroid); break; case 'normal': this.setBufferData(index, buffer, triangle.normal); break; case 'ambient': this.setBufferData(index, buffer, mesh.material.ambient.rgba); break; case 'diffuse': this.setBufferData(index, buffer, mesh.material.diffuse.rgba); break; } index++; } } } // Upload attribute buffer data this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.buffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, buffer.data, this.gl.DYNAMIC_DRAW); this.gl.enableVertexAttribArray(buffer.location); this.gl.vertexAttribPointer(buffer.location, buffer.size, this.gl.FLOAT, false, 0, 0); } } // Build uniform buffers this.setBufferData(0, this.program.uniforms.resolution, [this.width, this.height, this.width]); for (l = lights-1; l >= 0; l--) { light = scene.lights[l]; this.setBufferData(l, this.program.uniforms.lightPosition, light.position); this.setBufferData(l, this.program.uniforms.lightAmbient, light.ambient.rgba); this.setBufferData(l, this.program.uniforms.lightDiffuse, light.diffuse.rgba); } // Update uniforms for (uniform in this.program.uniforms) { buffer = this.program.uniforms[uniform]; location = buffer.location; data = buffer.data; switch (buffer.structure) { case '3f': this.gl.uniform3f(location, data[0], data[1], data[2]); break; case '3fv': this.gl.uniform3fv(location, data); break; case '4fv': this.gl.uniform4fv(location, data); break; } } } // Draw those lovely triangles this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertices); return this; }; FSS.WebGLRenderer.prototype.setBufferData = function(index, buffer, value) { if (FSS.Utils.isNumber(value)) { buffer.data[index*buffer.size] = value; } else { for (var i = value.length - 1; i >= 0; i--) { buffer.data[index*buffer.size+i] = value[i]; } } }; /** * Concepts taken from three.js WebGLRenderer * @see https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLRenderer.js */ FSS.WebGLRenderer.prototype.buildProgram = function(lights) { if (this.unsupported) return; // Create shader source var vs = FSS.WebGLRenderer.VS(lights); var fs = FSS.WebGLRenderer.FS(lights); // Derive the shader fingerprint var code = vs + fs; // Check if the program has already been compiled if (!!this.program && this.program.code === code) return; // Create the program and shaders var program = this.gl.createProgram(); var vertexShader = this.buildShader(this.gl.VERTEX_SHADER, vs); var fragmentShader = this.buildShader(this.gl.FRAGMENT_SHADER, fs); // Attach an link the shader this.gl.attachShader(program, vertexShader); this.gl.attachShader(program, fragmentShader); this.gl.linkProgram(program); // Add error handling if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { var error = this.gl.getError(); var status = this.gl.getProgramParameter(program, this.gl.VALIDATE_STATUS); console.error('Could not initialise shader.\nVALIDATE_STATUS: '+status+'\nERROR: '+error); return null; } // Delete the shader this.gl.deleteShader(fragmentShader); this.gl.deleteShader(vertexShader); // Set the program code program.code = code; // Add the program attributes program.attributes = { side: this.buildBuffer(program, 'attribute', 'aSide', 1, 'f' ), position: this.buildBuffer(program, 'attribute', 'aPosition', 3, 'v3'), centroid: this.buildBuffer(program, 'attribute', 'aCentroid', 3, 'v3'), normal: this.buildBuffer(program, 'attribute', 'aNormal', 3, 'v3'), ambient: this.buildBuffer(program, 'attribute', 'aAmbient', 4, 'v4'), diffuse: this.buildBuffer(program, 'attribute', 'aDiffuse', 4, 'v4') }; // Add the program uniforms program.uniforms = { resolution: this.buildBuffer(program, 'uniform', 'uResolution', 3, '3f', 1 ), lightPosition: this.buildBuffer(program, 'uniform', 'uLightPosition', 3, '3fv', lights), lightAmbient: this.buildBuffer(program, 'uniform', 'uLightAmbient', 4, '4fv', lights), lightDiffuse: this.buildBuffer(program, 'uniform', 'uLightDiffuse', 4, '4fv', lights) }; // Set the renderer program this.program = program; // Enable program this.gl.useProgram(this.program); // Return the program return program; }; FSS.WebGLRenderer.prototype.buildShader = function(type, source) { if (this.unsupported) return; // Create and compile shader var shader = this.gl.createShader(type); this.gl.shaderSource(shader, source); this.gl.compileShader(shader); // Add error handling if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { console.error(this.gl.getShaderInfoLog(shader)); return null; } // Return the shader return shader; }; FSS.WebGLRenderer.prototype.buildBuffer = function(program, type, identifier, size, structure, count) { var buffer = {buffer:this.gl.createBuffer(), size:size, structure:structure, data:null}; // Set the location switch (type) { case 'attribute': buffer.location = this.gl.getAttribLocation(program, identifier); break; case 'uniform': buffer.location = this.gl.getUniformLocation(program, identifier); break; } // Create the buffer if count is provided if (!!count) { buffer.data = new FSS.Array(count*size); } // Return the buffer return buffer; }; FSS.WebGLRenderer.VS = function(lights) { var shader = [ // Precision 'precision mediump float;', // Lights '#define LIGHTS ' + lights, // Attributes 'attribute float aSide;', 'attribute vec3 aPosition;', 'attribute vec3 aCentroid;', 'attribute vec3 aNormal;', 'attribute vec4 aAmbient;', 'attribute vec4 aDiffuse;', // Uniforms 'uniform vec3 uResolution;', 'uniform vec3 uLightPosition[LIGHTS];', 'uniform vec4 uLightAmbient[LIGHTS];', 'uniform vec4 uLightDiffuse[LIGHTS];', // Varyings 'varying vec4 vColor;', // Main 'void main() {', // Create color 'vColor = vec4(0.0);', // Calculate the vertex position 'vec3 position = aPosition / uResolution * 2.0;', // Iterate through lights 'for (int i = 0; i < LIGHTS; i++) {', 'vec3 lightPosition = uLightPosition[i];', 'vec4 lightAmbient = uLightAmbient[i];', 'vec4 lightDiffuse = uLightDiffuse[i];', // Calculate illuminance 'vec3 ray = normalize(lightPosition - aCentroid);', 'float illuminance = dot(aNormal, ray);', 'if (aSide == 0.0) {', 'illuminance = max(illuminance, 0.0);', '} else if (aSide == 1.0) {', 'illuminance = abs(min(illuminance, 0.0));', '} else if (aSide == 2.0) {', 'illuminance = max(abs(illuminance), 0.0);', '}', // Calculate ambient light 'vColor += aAmbient * lightAmbient;', // Calculate diffuse light 'vColor += aDiffuse * lightDiffuse * illuminance;', '}', // Clamp color 'vColor = clamp(vColor, 0.0, 1.0);', // Set gl_Position 'gl_Position = vec4(position, 1.0);', '}' // Return the shader ].join('\n'); return shader; }; FSS.WebGLRenderer.FS = function(lights) { var shader = [ // Precision 'precision mediump float;', // Varyings 'varying vec4 vColor;', // Main 'void main() {', // Set gl_FragColor 'gl_FragColor = vColor;', '}' // Return the shader ].join('\n'); return shader; }; /** * @class SVG Renderer * @author Matthew Wagerfield */ FSS.SVGRenderer = function() { FSS.Renderer.call(this); this.element = document.createElementNS(FSS.SVGNS, 'svg'); this.element.setAttribute('xmlns', FSS.SVGNS); this.element.setAttribute('version', '1.1'); this.element.style.display = 'block'; this.setSize(300, 150); }; FSS.SVGRenderer.prototype = Object.create(FSS.Renderer.prototype); FSS.SVGRenderer.prototype.setSize = function(width, height) { FSS.Renderer.prototype.setSize.call(this, width, height); this.element.setAttribute('width', width); this.element.setAttribute('height', height); return this; }; FSS.SVGRenderer.prototype.clear = function() { FSS.Renderer.prototype.clear.call(this); for (var i = this.element.childNodes.length - 1; i >= 0; i--) { this.element.removeChild(this.element.childNodes[i]); } return this; }; FSS.SVGRenderer.prototype.render = function(scene) { FSS.Renderer.prototype.render.call(this, scene); var m,mesh, t,triangle, points, style; // Update Meshes for (m = scene.meshes.length - 1; m >= 0; m--) { mesh = scene.meshes[m]; if (mesh.visible) { mesh.update(scene.lights, true); // Render Triangles for (t = mesh.geometry.triangles.length - 1; t >= 0; t--) { triangle = mesh.geometry.triangles[t]; if (triangle.polygon.parentNode !== this.element) { this.element.appendChild(triangle.polygon); } points = this.formatPoint(triangle.a)+' '; points += this.formatPoint(triangle.b)+' '; points += this.formatPoint(triangle.c); style = this.formatStyle(triangle.color.format()); triangle.polygon.setAttributeNS(null, 'points', points); triangle.polygon.setAttributeNS(null, 'style', style); } } } return this; }; FSS.SVGRenderer.prototype.formatPoint = function(vertex) { return (this.halfWidth+vertex.position[0])+','+(this.halfHeight-vertex.position[1]); }; FSS.SVGRenderer.prototype.formatStyle = function(color) { var style = 'fill:'+color+';'; style += 'stroke:'+color+';'; return style; };