/**
* @module kami
*/
var Class = require('klasse');
var ShaderProgram = new Class({
/**
* Creates a new ShaderProgram from the given source, and an optional map of attribute
* locations as <name, index> pairs.
*
* _Note:_ Chrome version 31 was giving me issues with attribute locations -- you may
* want to omit this to let the browser pick the locations for you.
*
* @class ShaderProgram
* @constructor
* @param {WebGLContext} context the context to manage this object
* @param {String} vertSource the vertex shader source
* @param {String} fragSource the fragment shader source
* @param {Object} attributeLocations the attribute locations
*/
initialize: function ShaderProgram(context, vertSource, fragSource, attributeLocations) {
if (!vertSource || !fragSource)
throw "vertex and fragment shaders must be defined";
if (typeof context !== "object")
throw "GL context not specified to ShaderProgram";
this.context = context;
this.vertShader = null;
this.fragShader = null;
this.program = null;
this.log = "";
this.uniformCache = null;
this.attributeCache = null;
this.attributeLocations = attributeLocations;
//We trim (ECMAScript5) so that the GLSL line numbers are
//accurate on shader log
this.vertSource = vertSource.trim();
this.fragSource = fragSource.trim();
//Adds this shader to the context, to be managed
this.context.addManagedObject(this);
this.create();
},
/**
* This is called during the ShaderProgram constructor,
* and may need to be called again after context loss and restore.
*
* @method create
*/
create: function() {
this.gl = this.context.gl;
this._compileShaders();
},
//Compiles the shaders, throwing an error if the program was invalid.
_compileShaders: function() {
var gl = this.gl;
this.log = "";
this.vertShader = this._loadShader(gl.VERTEX_SHADER, this.vertSource);
this.fragShader = this._loadShader(gl.FRAGMENT_SHADER, this.fragSource);
if (!this.vertShader || !this.fragShader)
throw "Error returned when calling createShader";
this.program = gl.createProgram();
gl.attachShader(this.program, this.vertShader);
gl.attachShader(this.program, this.fragShader);
//TODO: This seems not to be working on my OSX -- maybe a driver bug?
if (this.attributeLocations) {
for (var key in this.attributeLocations) {
if (this.attributeLocations.hasOwnProperty(key)) {
gl.bindAttribLocation(this.program, Math.floor(this.attributeLocations[key]), key);
}
}
}
gl.linkProgram(this.program);
this.log += gl.getProgramInfoLog(this.program) || "";
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
throw "Error linking the shader program:\n"
+ this.log;
}
this._fetchUniforms();
this._fetchAttributes();
},
_fetchUniforms: function() {
var gl = this.gl;
this.uniformCache = {};
var len = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
if (!len) //null or zero
return;
for (var i=0; i<len; i++) {
var info = gl.getActiveUniform(this.program, i);
if (info === null)
continue;
var name = info.name;
var location = gl.getUniformLocation(this.program, name);
this.uniformCache[name] = {
size: info.size,
type: info.type,
location: location
};
}
},
_fetchAttributes: function() {
var gl = this.gl;
this.attributeCache = {};
var len = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
if (!len) //null or zero
return;
for (var i=0; i<len; i++) {
var info = gl.getActiveAttrib(this.program, i);
if (info === null)
continue;
var name = info.name;
//the attrib location is a simple index
var location = gl.getAttribLocation(this.program, name);
this.attributeCache[name] = {
size: info.size,
type: info.type,
location: location
};
}
},
_loadShader: function(type, source) {
var gl = this.gl;
var shader = gl.createShader(type);
if (!shader) //should not occur...
return -1;
gl.shaderSource(shader, source);
gl.compileShader(shader);
var logResult = gl.getShaderInfoLog(shader) || "";
if (logResult) {
//we do this so the user knows which shader has the error
var typeStr = (type === gl.VERTEX_SHADER) ? "vertex" : "fragment";
logResult = "Error compiling "+ typeStr+ " shader:\n"+logResult;
}
this.log += logResult;
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) ) {
throw this.log;
}
return shader;
},
/**
* Called to bind this shader. Note that there is no "unbind" since
* technically such a thing is not possible in the programmable pipeline.
*
* You must bind a shader before settings its uniforms.
*
* @method bind
*/
bind: function() {
this.gl.useProgram(this.program);
},
/**
* Destroys this shader and its resources. You should not try to use this
* after destroying it.
* @method destroy
*/
destroy: function() {
if (this.context)
this.context.removeManagedObject(this);
if (this.gl && this.program) {
var gl = this.gl;
gl.detachShader(this.program, this.vertShader);
gl.detachShader(this.program, this.fragShader);
gl.deleteShader(this.vertShader);
gl.deleteShader(this.fragShader);
gl.deleteProgram(this.program);
}
this.attributeCache = null;
this.uniformCache = null;
this.vertShader = null;
this.fragShader = null;
this.program = null;
this.gl = null;
this.context = null;
},
/**
* Returns the cached uniform info (size, type, location).
* If the uniform is not found in the cache, it is assumed
* to not exist, and this method returns null.
*
* This may return null even if the uniform is defined in GLSL:
* if it is _inactive_ (i.e. not used in the program) then it may
* be optimized out.
*
* @method getUniformInfo
* @param {String} name the uniform name as defined in GLSL
* @return {Object} an object containing location, size, and type
*/
getUniformInfo: function(name) {
return this.uniformCache[name] || null;
},
/**
* Returns the cached attribute info (size, type, location).
* If the attribute is not found in the cache, it is assumed
* to not exist, and this method returns null.
*
* This may return null even if the attribute is defined in GLSL:
* if it is _inactive_ (i.e. not used in the program or disabled)
* then it may be optimized out.
*
* @method getAttributeInfo
* @param {String} name the attribute name as defined in GLSL
* @return {object} an object containing location, size and type
*/
getAttributeInfo: function(name) {
return this.attributeCache[name] || null;
},
/**
* Returns the cached uniform location object.
* If the uniform is not found, this method returns null.
*
* @method getAttributeLocation
* @param {String} name the uniform name as defined in GLSL
* @return {GLint} the location object
*/
getAttributeLocation: function(name) { //TODO: make faster, don't cache
var info = this.getAttributeInfo(name);
return info ? info.location : null;
},
/**
* Returns the cached uniform location object, assuming it exists
* and is active. Note that uniforms may be inactive if
* the GLSL compiler deemed them unused.
*
* @method getUniformLocation
* @param {String} name the uniform name as defined in GLSL
* @return {WebGLUniformLocation} the location object
*/
getUniformLocation: function(name) {
var info = this.getUniformInfo(name);
return info ? info.location : null;
},
/**
* Returns true if the uniform is active and found in this
* compiled program. Note that uniforms may be inactive if
* the GLSL compiler deemed them unused.
*
* @method hasUniform
* @param {String} name the uniform name
* @return {Boolean} true if the uniform is found and active
*/
hasUniform: function(name) {
return this.getUniformInfo(name) !== null;
},
/**
* Returns true if the attribute is active and found in this
* compiled program.
*
* @method hasAttribute
* @param {String} name the attribute name
* @return {Boolean} true if the attribute is found and active
*/
hasAttribute: function(name) {
return this.getAttributeInfo(name) !== null;
},
/**
* Returns the uniform value by name.
*
* @method getUniform
* @param {String} name the uniform name as defined in GLSL
* @return {any} The value of the WebGL uniform
*/
getUniform: function(name) {
return this.gl.getUniform(this.program, this.getUniformLocation(name));
},
/**
* Returns the uniform value at the specified WebGLUniformLocation.
*
* @method getUniformAt
* @param {WebGLUniformLocation} location the location object
* @return {any} The value of the WebGL uniform
*/
getUniformAt: function(location) {
return this.gl.getUniform(this.program, location);
},
/**
* A convenience method to set uniformi from the given arguments.
* We determine which GL call to make based on the number of arguments
* passed. For example, `setUniformi("var", 0, 1)` maps to `gl.uniform2i`.
*
* @method setUniformi
* @param {String} name the name of the uniform
* @param {GLint} x the x component for ints
* @param {GLint} y the y component for ivec2
* @param {GLint} z the z component for ivec3
* @param {GLint} w the w component for ivec4
*/
setUniformi: function(name, x, y, z, w) {
'use strict';
var gl = this.gl;
var loc = this.getUniformLocation(name);
if (loc === null)
return false;
switch (arguments.length) {
case 2: gl.uniform1i(loc, x); return true;
case 3: gl.uniform2i(loc, x, y); return true;
case 4: gl.uniform3i(loc, x, y, z); return true;
case 5: gl.uniform4i(loc, x, y, z, w); return true;
default:
throw "invalid arguments to setUniformi";
}
},
/**
* A convenience method to set uniformf from the given arguments.
* We determine which GL call to make based on the number of arguments
* passed. For example, `setUniformf("var", 0, 1)` maps to `gl.uniform2f`.
*
* @method setUniformf
* @param {String} name the name of the uniform
* @param {GLfloat} x the x component for floats
* @param {GLfloat} y the y component for vec2
* @param {GLfloat} z the z component for vec3
* @param {GLfloat} w the w component for vec4
*/
setUniformf: function(name, x, y, z, w) {
'use strict';
var gl = this.gl;
var loc = this.getUniformLocation(name);
if (loc === null)
return false;
switch (arguments.length) {
case 2: gl.uniform1f(loc, x); return true;
case 3: gl.uniform2f(loc, x, y); return true;
case 4: gl.uniform3f(loc, x, y, z); return true;
case 5: gl.uniform4f(loc, x, y, z, w); return true;
default:
throw "invalid arguments to setUniformf";
}
},
//I guess we won't support sequence<GLfloat> .. whatever that is ??
/////
/**
* A convenience method to set uniformNfv from the given ArrayBuffer.
* We determine which GL call to make based on the length of the array
* buffer (for 1-4 component vectors stored in a Float32Array). To use
* this method to upload data to uniform arrays, you need to specify the
* 'count' parameter; i.e. the data type you are using for that array. If
* specified, this will dictate whether to call uniform1fv, uniform2fv, etc.
*
* @method setUniformfv
* @param {String} name the name of the uniform
* @param {ArrayBuffer} arrayBuffer the array buffer
* @param {Number} count optional, the explicit data type count, e.g. 2 for vec2
*/
setUniformfv: function(name, arrayBuffer, count) {
'use strict';
count = count || arrayBuffer.length;
var gl = this.gl;
var loc = this.getUniformLocation(name);
if (loc === null)
return false;
switch (count) {
case 1: gl.uniform1fv(loc, arrayBuffer); return true;
case 2: gl.uniform2fv(loc, arrayBuffer); return true;
case 3: gl.uniform3fv(loc, arrayBuffer); return true;
case 4: gl.uniform4fv(loc, arrayBuffer); return true;
default:
throw "invalid arguments to setUniformf";
}
},
/**
* A convenience method to set uniformNiv from the given ArrayBuffer.
* We determine which GL call to make based on the length of the array
* buffer (for 1-4 component vectors stored in a int array). To use
* this method to upload data to uniform arrays, you need to specify the
* 'count' parameter; i.e. the data type you are using for that array. If
* specified, this will dictate whether to call uniform1fv, uniform2fv, etc.
*
* @method setUniformiv
* @param {String} name the name of the uniform
* @param {ArrayBuffer} arrayBuffer the array buffer
* @param {Number} count optional, the explicit data type count, e.g. 2 for ivec2
*/
setUniformiv: function(name, arrayBuffer, count) {
'use strict';
count = count || arrayBuffer.length;
var gl = this.gl;
var loc = this.getUniformLocation(name);
if (loc === null)
return false;
switch (count) {
case 1: gl.uniform1iv(loc, arrayBuffer); return true;
case 2: gl.uniform2iv(loc, arrayBuffer); return true;
case 3: gl.uniform3iv(loc, arrayBuffer); return true;
case 4: gl.uniform4iv(loc, arrayBuffer); return true;
default:
throw "invalid arguments to setUniformf";
}
},
/**
* This is a convenience function to pass a Matrix3 (from vecmath,
* kami's preferred math library) or a Float32Array (e.g. gl-matrix)
* to a shader. If mat is an object with "val", it is considered to be
* a Matrix3, otherwise assumed to be a typed array being passed directly
* to the shader.
*
* @param {String} name the uniform name
* @param {Matrix3|Float32Array} mat a Matrix3 or Float32Array
* @param {Boolean} transpose whether to transpose the matrix, default false
*/
setUniformMatrix3: function(name, mat, transpose) {
'use strict';
var arr = typeof mat === "object" && mat.val ? mat.val : mat;
transpose = !!transpose; //to boolean
var gl = this.gl;
var loc = this.getUniformLocation(name);
if (loc === null)
return false;
gl.uniformMatrix3fv(loc, transpose, arr)
},
/**
* This is a convenience function to pass a Matrix4 (from vecmath,
* kami's preferred math library) or a Float32Array (e.g. gl-matrix)
* to a shader. If mat is an object with "val", it is considered to be
* a Matrix4, otherwise assumed to be a typed array being passed directly
* to the shader.
*
* @param {String} name the uniform name
* @param {Matrix4|Float32Array} mat a Matrix4 or Float32Array
* @param {Boolean} transpose whether to transpose the matrix, default false
*/
setUniformMatrix4: function(name, mat, transpose) {
'use strict';
var arr = typeof mat === "object" && mat.val ? mat.val : mat;
transpose = !!transpose; //to boolean
var gl = this.gl;
var loc = this.getUniformLocation(name);
if (loc === null)
return false;
gl.uniformMatrix4fv(loc, transpose, arr)
}
});
//Some default attribute names that parts of kami will use
//when creating a standard shader.
ShaderProgram.POSITION_ATTRIBUTE = "Position";
ShaderProgram.NORMAL_ATTRIBUTE = "Normal";
ShaderProgram.COLOR_ATTRIBUTE = "Color";
ShaderProgram.TEXCOORD_ATTRIBUTE = "TexCoord";
module.exports = ShaderProgram;