toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
335 lines (306 loc) • 10.9 kB
JavaScript
;
/**
* Creating a WebGL program for shaders is painful. This class
* simplifies the process.
*
* @class Program
*
* Object properties starting with `$` are WebGL uniforms or attributes.
* Uniforms behave as expected: you can read/write a value.
* Attributes when read, return the location. And when written, enable/disabled
* this attribute. So you read integers and writte booleans.
*
* @param gl - WebGL context.
* @param codes - Object with two mandatory attributes: `vert` for
* vertex shader and `frag` for fragment shader.
* @param includes - (optional) If defined, the `#include foo`
* directives of shaders will be replaced by the content of
* `includes.foo`.
*/
function Program(gl, codes, includes) {
if (typeof codes.vert !== 'string') {
throw Error('[webgl.program] Missing attribute `vert` in argument `codes`!');
}
if (typeof codes.frag !== 'string') {
throw Error('[webgl.program] Missing attribute `frag` in argument `codes`!');
}
codes = parseIncludes(codes, includes);
this.gl = gl;
Object.freeze(this.gl);
this._typesNamesLookup = getTypesNamesLookup(gl);
var shaderProgram = gl.createProgram();
var shaderVert = getVertexShader(gl, codes.vert);
var shaderFrag = getFragmentShader(gl, codes.frag);
gl.attachShader(shaderProgram, shaderVert);
gl.attachShader(shaderProgram, shaderFrag);
gl.linkProgram(shaderProgram);
/* Si on exécute les 4 lignes suivantes, on ne peut pas debugger les shaders.
gl.detachShader( shaderProgram, shaderVert );
gl.deleteShader( shaderVert );
gl.detachShader( shaderProgram, shaderFrag );
gl.deleteShader( shaderFrag );
*/
this.program = shaderProgram;
Object.freeze(this.program);
this.use();
createAttributes(this, gl, shaderProgram);
createUniforms(this, gl, shaderProgram);
}
Program.prototype.use = function() {
this.gl.useProgram(this.program);
};
Program.prototype.getTypeName = function(typeId) {
return this._typesNamesLookup[typeId];
};
var BPE = (new Float32Array()).BYTES_PER_ELEMENT;
Program.prototype.bindAttribs = function(buffer) {
var gl = this.gl;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
var names = Array.prototype.slice.call(arguments, 1);
var totalSize = 0;
names.forEach(function(name) {
var attrib = this.attribs[name];
if (!attrib) {
throw "Cannot find attribute \"" + name + "\"!\n" +
"It may be not active because unused in the shader.\n" +
"Available attributes are: " + Object.keys(this.attribs).map(function(name) {
return '"' + name + '"';
}).join(", ");
}
totalSize += (attrib.size * attrib.length) * BPE;
}, this);
var offset = 0;
names.forEach(function(name) {
var attrib = this.attribs[name];
gl.enableVertexAttribArray(attrib.location);
gl.vertexAttribPointer(
attrib.location,
attrib.size * attrib.length,
gl.FLOAT,
false, // No normalisation.
totalSize,
offset
);
offset += (attrib.size * attrib.length) * BPE;
}, this);
};
module.exports = Program;
function createAttributes(that, gl, shaderProgram) {
const
attribs = {},
attribsCount = gl.getProgramParameter(shaderProgram, gl.ACTIVE_ATTRIBUTES);
for (let index = 0; index < attribsCount; index++) {
const item = gl.getActiveAttrib(shaderProgram, index);
item.typeName = that.getTypeName(item.type);
item.length = getSize(gl, item);
item.location = gl.getAttribLocation(shaderProgram, item.name);
attribs[item.name] = item;
}
that.attribs = attribs;
Object.freeze(that.attribs);
}
function createAttributeSetter(gl, item, shaderProgram) {
console.info("item=", item);
var name = item.name;
return function(v) {
if (typeof v !== 'boolean') {
throw "[webgl.program::$" + name +
"] Value must be a boolean: true if you want to enable this attribute, and false to disable it.";
}
if (v) {
console.log("enableVertexAttribArray(", gl.getAttribLocation(shaderProgram, name), ")");
gl.enableVertexAttribArray(
gl.getAttribLocation(shaderProgram, name)
);
} else {
gl.disableVertexAttribArray(
gl.getAttribLocation(shaderProgram, name)
);
}
};
}
function createAttributeGetter(gl, item, shaderProgram) {
var loc = gl.getAttribLocation(shaderProgram, item.name);
return function() {
return loc;
};
}
function createUniforms(that, gl, shaderProgram) {
var index, item;
var uniforms = {};
var uniformsCount = gl.getProgramParameter(shaderProgram, gl.ACTIVE_UNIFORMS);
for (index = 0; index < uniformsCount; index++) {
item = gl.getActiveUniform(shaderProgram, index);
uniforms[item.name] = gl.getUniformLocation(shaderProgram, item.name);
Object.defineProperty(that, '$' + item.name, {
set: createUniformSetter(gl, item, uniforms[item.name], that._typesNamesLookup),
get: createUniformGetter(item),
enumerable: true,
configurable: false
});
}
that.uniforms = uniforms;
Object.freeze(that.uniforms);
}
/**
* This is a preprocessor for shaders.
* Directives `#include` will be replaced by the content of the
* correspondent attribute in `includes`.
*/
function parseIncludes(codes, includes) {
const result = {};
for (const id of Object.keys(codes)) {
const code = codes[id];
result[id] = code.split('\n').map(function(line) {
if (line.trim().substr(0, 8) !== '#include') return line;
const pos = line.indexOf('#include') + 8;
let includeName = line.substr(pos).trim();
// We accept all this systaxes:
// #include foo
// #include 'foo'
// #include <foo>
// #include "foo"
if ("'<\"".indexOf(includeName.charAt(0)) > -1) {
includeName = includeName.substr(1, includeName.length - 2);
}
const snippet = includes[includeName];
if (typeof snippet !== 'string') {
console.error("Include <" + includeName + "> not found in ", includes);
throw Error("Include not found in shader: " + includeName);
}
return snippet;
}).join("\n");
}
return result;
}
function createUniformSetter(gl, item, nameGL, lookup) {
var nameJS = '_$' + item.name;
switch (item.type) {
case gl.BYTE:
case gl.UNSIGNED_BYTE:
case gl.SHORT:
case gl.UNSIGNED_SHORT:
case gl.INT:
case gl.UNSIGNED_INT:
case gl.SAMPLER_2D: // For textures, we specify the texture unit.
if (item.size == 1) {
return function(v) {
gl.uniform1i(nameGL, v);
this[nameJS] = v;
};
} else {
return function(v) {
gl.uniform1iv(nameGL, v);
this[nameJS] = v;
};
}
break;
case gl.FLOAT:
if (item.size == 1) {
return function(v) {
gl.uniform1f(nameGL, v);
this[nameJS] = v;
};
} else {
return function(v) {
gl.uniform1fv(nameGL, v);
this[nameJS] = v;
};
}
break;
case gl.FLOAT_VEC3:
if (item.size == 1) {
return function(v) {
gl.uniform3fv(nameGL, v);
this[nameJS] = v;
};
} else {
throw Error(
"[webgl.program.createWriter] Don't know how to deal arrays of FLOAT_VEC3 in uniform `" +
item.name + "'!'"
);
}
break;
case gl.FLOAT_MAT3:
if (item.size == 1) {
return function(v) {
gl.uniformMatrix3fv(nameGL, false, v);
this[nameJS] = v;
};
} else {
throw Error(
"[webgl.program.createWriter] Don't know how to deal arrays of FLOAT_MAT3 in uniform `" +
item.name + "'!'"
);
}
break;
case gl.FLOAT_MAT4:
if (item.size == 1) {
return function(v) {
if (!v) throw Error(`I can't assign an undefined value to "${item.name}"!`);
gl.uniformMatrix4fv(nameGL, false, v);
this[nameJS] = v;
};
} else {
throw Error(
"[webgl.program.createWriter] Don't know how to deal arrays of FLOAT_MAT4 in uniform `" +
item.name + "'!'"
);
}
break;
default:
throw Error(
"[webgl.program.createWriter] Don't know how to deal with uniform `" +
item.name + "` of type " + lookup[item.type] + "!"
);
}
}
function createUniformGetter(item) {
var name = '_$' + item.name;
return function() {
return this[name];
};
}
function getShader(type, gl, code) {
var shader = gl.createShader(type);
gl.shaderSource(shader, code);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.log(code);
console.error("An error occurred compiling the shader: " + gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
function getFragmentShader(gl, code) {
return getShader(gl.FRAGMENT_SHADER, gl, code);
}
function getVertexShader(gl, code) {
return getShader(gl.VERTEX_SHADER, gl, code);
}
function getTypesNamesLookup(gl) {
var lookup = {};
var k, v;
for (k in gl) {
v = gl[k];
if (typeof v === 'number') {
lookup[v] = k;
}
}
return lookup;
}
function getSize(gl, item) {
switch (item.type) {
case gl.FLOAT_VEC4:
return 4;
case gl.FLOAT_VEC3:
return 3;
case gl.FLOAT_VEC2:
return 2;
case gl.FLOAT:
return 1;
default:
throw "[webgl.program:getSize] I don't know the size of the attribute '" + item.name +
"' because I don't know the type " + getTypeName(item.type) + "!";
}
}