Shader.js

'use strict';

const parallel = require('async/parallel');
const WebGLContext = require('./WebGLContext');
const ShaderParser = require('./ShaderParser');
const XHRLoader = require('../util/XHRLoader');

const UNIFORM_FUNCTIONS = {
	'bool': 'uniform1i',
	'bool[]': 'uniform1iv',
	'float': 'uniform1f',
	'float[]': 'uniform1fv',
	'int': 'uniform1i',
	'int[]': 'uniform1iv',
	'uint': 'uniform1i',
	'uint[]': 'uniform1iv',
	'vec2': 'uniform2fv',
	'vec2[]': 'uniform2fv',
	'ivec2': 'uniform2iv',
	'ivec2[]': 'uniform2iv',
	'vec3': 'uniform3fv',
	'vec3[]': 'uniform3fv',
	'ivec3': 'uniform3iv',
	'ivec3[]': 'uniform3iv',
	'vec4': 'uniform4fv',
	'vec4[]': 'uniform4fv',
	'ivec4': 'uniform4iv',
	'ivec4[]': 'uniform4iv',
	'mat2': 'uniformMatrix2fv',
	'mat2[]': 'uniformMatrix2fv',
	'mat3': 'uniformMatrix3fv',
	'mat3[]': 'uniformMatrix3fv',
	'mat4': 'uniformMatrix4fv',
	'mat4[]': 'uniformMatrix4fv',
	'sampler2D': 'uniform1i',
	'samplerCube': 'uniform1i'
};

/**
 * Given a map of existing attributes, find the lowest index that is not already
 * used. If the attribute ordering was already provided, use that instead.
 *
 * @private
 *
 * @param {Map} attributes - The existing attributes map.
 * @param {Object} declaration - The attribute declaration object.
 *
 * @returns {number} The attribute index.
 */
function getAttributeIndex(attributes, declaration) {
	// check if attribute is already declared, if so, use that index
	if (attributes.has(declaration.name)) {
		return attributes.get(declaration.name).index;
	}
	// return next available index
	return attributes.size;
}

/**
 * Given vertex and fragment shader source, parses the declarations and appends
 * information pertaining to the uniforms and attribtues declared.
 *
 * @private
 *
 * @param {Shader} shader - The shader object.
 * @param {string} vertSource - The vertex shader source.
 * @param {string} fragSource - The fragment shader source.
 *
 * @returns {Object} The attribute and uniform information.
 */
function setAttributesAndUniforms(shader, vertSource, fragSource) {
	const declarations = ShaderParser.parseDeclarations(
		[vertSource, fragSource],
		['uniform', 'attribute']);
	// for each declaration in the shader
	declarations.forEach(declaration => {
		// check if its an attribute or uniform
		if (declaration.qualifier === 'attribute') {
			// if attribute, store type and index
			const index = getAttributeIndex(shader.attributes, declaration);
			shader.attributes.set(declaration.name, {
				type: declaration.type,
				index: index
			});
		} else { // if (declaration.qualifier === 'uniform') {
			// if uniform, store type and buffer function name
			const type = declaration.type + (declaration.count > 1 ? '[]' : '');
			shader.uniforms.set(declaration.name, {
				type: declaration.type,
				func: UNIFORM_FUNCTIONS[type]
			});
		}
	});
}

/**
 * Given a lineNumber and max number of digits, pad the line accordingly.
 *
 * @private
 *
 * @param {number} lineNum - The line number.
 * @param {number} maxDigits - The max digits to pad.
 *
 * @returns {string} The padded string.
 */
function padLineNumber(lineNum, maxDigits) {
	lineNum = lineNum.toString();
	const diff = maxDigits - lineNum.length;
	lineNum += ':';
	for (let i=0; i<diff; i++) {
		lineNum += ' ';
	}
	return lineNum;
};

/**
 * Given a shader source string and shader type, compiles the shader and returns
 * the resulting WebGLShader object.
 *
 * @private
 *
 * @param {WebGLRenderingContext} gl - The webgl rendering context.
 * @param {string} shaderSource - The shader source.
 * @param {string} type - The shader type.
 *
 * @returns {WebGLShader} The compiled shader object.
 */
function compileShader(gl, shaderSource, type) {
	const shader = gl.createShader(gl[type]);
	gl.shaderSource(shader, shaderSource);
	gl.compileShader(shader);
	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
		const split = shaderSource.split('\n');
		const maxDigits = (split.length).toString().length + 1;
		const srcByLines = split.map((line, index) => {
			return `${padLineNumber(index+1, maxDigits)} ${line}`;
		}).join('\n');
		const shaderLog = gl.getShaderInfoLog(shader);
		throw `An error occurred compiling the shader:\n\n${shaderLog.slice(0, shaderLog.length-1)}\n${srcByLines}`;
	}
	return shader;
}

/**
 * Binds the attribute locations for the Shader object.
 *
 * @private
 *
 * @param {Shader} shader - The Shader object.
 */
function bindAttributeLocations(shader) {
	const gl = shader.gl;
	shader.attributes.forEach((attribute, name) => {
		// bind the attribute location
		gl.bindAttribLocation(
			shader.program,
			attribute.index,
			name);
	});
}

/**
 * Queries the webgl rendering context for the uniform locations.
 *
 * @private
 *
 * @param {Shader} shader - The Shader object.
 */
function getUniformLocations(shader) {
	const gl = shader.gl;
	const uniforms = shader.uniforms;
	uniforms.forEach((uniform, name) => {
		// get the uniform location
		const location = gl.getUniformLocation(shader.program, name);
		// check if null, parse may detect uniform that is compiled out due to
		// not being used, or due to a preprocessor evaluation.
		if (location === null) {
			uniforms.delete(name);
		} else {
			uniform.location = location;
		}
	});
}

/**
 * Returns a function to load shader source from a url.
 *
 * @private
 *
 * @param {string} url - The url to load the resource from.
 *
 * @returns {Function} The function to load the shader source.
 */
function loadShaderSource(url) {
	return function(done) {
		XHRLoader.load({
			url: url,
			responseType: 'text',
			success: function(res) {
				done(null, res);
			},
			error: function(err) {
				done(err, null);
			}
		});
	};
}

/**
 * Returns a function to pass through the shader source.
 *
 * @private
 *
 * @param {string} source - The source of the shader.
 *
 * @returns {Function} The function to pass through the shader source.
 */
function passThroughSource(source) {
	return function(done) {
		done(null, source);
	};
}

/**
 * Returns a function that takes an array of GLSL source strings and URLs, and
 * resolves them into and array of GLSL source.
 *
 * @private
 *
 * @param {Array} sources - The shader sources.
 *
 * @returns {Function} A function to resolve the shader sources.
 */
function resolveSources(sources) {
	return function(done) {
		const tasks = [];
		sources = sources || [];
		sources = !Array.isArray(sources) ? [sources] : sources;
		sources.forEach(source => {
			if (ShaderParser.isGLSL(source)) {
				tasks.push(passThroughSource(source));
			} else {
				tasks.push(loadShaderSource(source));
			}
		});
		parallel(tasks, done);
	};
}

/**
 * Injects the defines into the shader source.
 *
 * @private
 *
 * @param {Array} defines - The shader defines.
 *
 * @returns {Function} A function to resolve the shader sources.
 */
const createDefines = function(defines = {}) {
	const res = [];
	Object.keys(defines).forEach(name => {
		res.push(`#define ${name} ${defines[name]}`);
	});
	return res.join('\n');
};

/**
 * Creates the shader program object from source strings. This includes:
 *	1) Compiling and linking the shader program.
 *	2) Parsing shader source for attribute and uniform information.
 *	3) Binding attribute locations, by order of delcaration.
 *	4) Querying and storing uniform location.
 *
 * @private
 *
 * @param {Shader} shader - The Shader object.
 * @param {Object} sources - A map containing sources under 'vert' and 'frag' attributes.
 *
 * @returns {Shader} The shader object, for chaining.
 */
function createProgram(shader, sources) {
	const gl = shader.gl;
	const defines = createDefines(sources.define);
	const common = defines + (sources.common || '');
	const vert = sources.vert.join('');
	const frag = sources.frag.join('');
	// compile shaders
	const vertexShader = compileShader(gl, common + vert, 'VERTEX_SHADER');
	const fragmentShader = compileShader(gl, common + frag, 'FRAGMENT_SHADER');
	// parse source for attribute and uniforms
	setAttributesAndUniforms(shader, vert, frag);
	// create the shader program
	shader.program = gl.createProgram();
	// attach vertex and fragment shaders
	gl.attachShader(shader.program, vertexShader);
	gl.attachShader(shader.program, fragmentShader);
	// bind vertex attribute locations BEFORE linking
	bindAttributeLocations(shader);
	// link shader
	gl.linkProgram(shader.program);
	// If creating the shader program failed, alert
	if (!gl.getProgramParameter(shader.program, gl.LINK_STATUS)) {
		throw 'An error occured linking the shader:\n' + gl.getProgramInfoLog(shader.program);
	}
	// get shader uniform locations
	getUniformLocations(shader);
}

/**
 * A shader class to assist in compiling and linking webgl shaders, storing
 * attribute and uniform locations, and buffering uniforms.
 */
class Shader {

	/**
	 * Instantiates a Shader object.
	 *
	 * @param {Object} spec - The shader specification object.
	 * @param {String|String[]|Object} spec.common - Sources / URLs to be shared by both vertex and fragment shaders.
	 * @param {String|String[]|Object} spec.vert - The vertex shader sources / URLs.
	 * @param {String|String[]|Object} spec.frag - The fragment shader sources / URLs.
	 * @param {Object} spec.define - Any `#define` definitions to be injected into the glsl.
	 * @param {String[]} spec.attributes - The attribute index orderings.
	 * @param {Function} callback - The callback function to execute once the shader has been successfully compiled and linked.
	 */
	constructor(spec = {}, callback = null) {
		// check source arguments
		if (!spec.vert) {
			throw 'Vertex shader argument `vert` has not been provided';
		}
		if (!spec.frag) {
			throw 'Fragment shader argument `frag` has not been provided';
		}
		this.program = 0;
		this.gl = WebGLContext.get();
		this.version = spec.version || '1.00';
		this.attributes = new Map();
		this.uniforms = new Map();
		// if attribute ordering is provided, use those indices
		if (spec.attributes) {
			spec.attributes.forEach((attr, index) => {
				this.attributes.set(attr, {
					index: index
				});
			});
		}
		// create the shader
		parallel({
			common: resolveSources(spec.common),
			vert: resolveSources(spec.vert),
			frag: resolveSources(spec.frag),
		}, (err, sources) => {
			if (err) {
				if (callback) {
					setTimeout(() => {
						callback(err, null);
					});
				}
				return;
			}
			// append defines
			sources.define = spec.define;
			// once all shader sources are loaded
			createProgram(this, sources);
			if (callback) {
				setTimeout(() => {
					callback(null, this);
				});
			}
		});
	}

	/**
	 * Binds the shader program for use.
	 *
	 * @returns {Shader} The shader object, for chaining.
	 */
	use() {
		// use the shader
		this.gl.useProgram(this.program);
		return this;
	}

	/**
	 * Buffer a uniform value by name.
	 *
	 * @param {string} name - The uniform name in the shader source.
	 * @param {*} value - The uniform value to buffer.
	 *
	 * @returns {Shader} - The shader object, for chaining.
	 */
	setUniform(name, value) {
		const uniform = this.uniforms.get(name);
		// ensure that the uniform spec exists for the name
		if (!uniform) {
			throw `No uniform found under name \`${name}\``;
		}
		// check value
		if (value === undefined || value === null) {
			// ensure that the uniform argument is defined
			throw `Value passed for uniform \`${name}\` is undefined or null`;
		}
		// set the uniform
		// NOTE: checking type by string comparison is faster than wrapping
		// the functions.
		if (uniform.type === 'mat2' || uniform.type === 'mat3' || uniform.type === 'mat4') {
			this.gl[uniform.func](uniform.location, false, value);
		} else {
			this.gl[uniform.func](uniform.location, value);
		}
		return this;
	}

	/**
	 * Buffer a map of uniform values.
	 *
	 * @param {Object} uniforms - The map of uniforms keyed by name.
	 *
	 * @returns {Shader} The shader object, for chaining.
	 */
	setUniforms(uniforms) {
		Object.keys(uniforms).forEach(name => {
			this.setUniform(name, uniforms[name]);
		});
		return this;
	}
}

module.exports = Shader;