/*global console */
var utils = require('../../../utils');
/**
* Base shader class for PIXI managed shaders.
*
* @class
* @memberof PIXI
* @param shaderManager {PIXI.ShaderManager} The webgl shader manager this shader works for.
* @param [vertexSrc] {string} The source of the vertex shader.
* @param [fragmentSrc] {string} The source of the fragment shader.
* @param [uniforms] {object} Uniforms for this shader.
* @param [attributes] {object} Attributes for this shader.
*/
function Shader(shaderManager, vertexSrc, fragmentSrc, uniforms, attributes)
{
if (!vertexSrc || !fragmentSrc)
{
throw new Error('Pixi.js Error. Shader requires vertexSrc and fragmentSrc');
}
/**
* A unique id
* @member {number}
* @readonly
*/
this.uid = utils.uid();
/**
* The current WebGL drawing context
* @member {WebGLRenderingContext}
* @readonly
*/
this.gl = shaderManager.renderer.gl;
//TODO maybe we should pass renderer rather than shader manger?? food for thought..
this.shaderManager = shaderManager;
/**
* The WebGL program.
*
* @member {WebGLProgram}
* @readonly
*/
this.program = null;
/**
* The uniforms as an object
* @member {object}
* @private
*/
this.uniforms = uniforms || {};
/**
* The attributes as an object
* @member {object}
* @private
*/
this.attributes = attributes || {};
/**
* Internal texture counter
* @member {number}
* @private
*/
this.textureCount = 1;
/**
* The vertex shader as an array of strings
*
* @member {string}
*/
this.vertexSrc = vertexSrc;
/**
* The fragment shader as an array of strings
*
* @member {string}
*/
this.fragmentSrc = fragmentSrc;
this.init();
}
Shader.prototype.constructor = Shader;
module.exports = Shader;
/**
* Creates the shader and uses it
*
*/
Shader.prototype.init = function ()
{
this.compile();
this.gl.useProgram(this.program);
this.cacheUniformLocations(Object.keys(this.uniforms));
this.cacheAttributeLocations(Object.keys(this.attributes));
};
/**
* Caches the locations of the uniform for reuse.
*
* @param keys {string} the uniforms to cache
*/
Shader.prototype.cacheUniformLocations = function (keys)
{
for (var i = 0; i < keys.length; ++i)
{
this.uniforms[keys[i]]._location = this.gl.getUniformLocation(this.program, keys[i]);
}
};
/**
* Caches the locations of the attribute for reuse.
*
* @param keys {string} the attributes to cache
*/
Shader.prototype.cacheAttributeLocations = function (keys)
{
for (var i = 0; i < keys.length; ++i)
{
this.attributes[keys[i]] = this.gl.getAttribLocation(this.program, keys[i]);
}
// TODO: Check if this is needed anymore...
// Begin worst hack eva //
// WHY??? ONLY on my chrome pixel the line above returns -1 when using filters?
// maybe its something to do with the current state of the gl context.
// I'm convinced this is a bug in the chrome browser as there is NO reason why this should be returning -1 especially as it only manifests on my chrome pixel
// If theres any webGL people that know why could happen please help :)
// if (this.attributes.aColor === -1){
// this.attributes.aColor = 2;
// }
// End worst hack eva //
};
/**
* Attaches the shaders and creates the program.
*
* @return {WebGLProgram}
*/
Shader.prototype.compile = function ()
{
var gl = this.gl;
var glVertShader = this._glCompile(gl.VERTEX_SHADER, this.vertexSrc);
var glFragShader = this._glCompile(gl.FRAGMENT_SHADER, this.fragmentSrc);
var program = gl.createProgram();
gl.attachShader(program, glVertShader);
gl.attachShader(program, glFragShader);
gl.linkProgram(program);
// if linking fails, then log and cleanup
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
{
console.error('Pixi.js Error: Could not initialize shader.');
console.error('gl.VALIDATE_STATUS', gl.getProgramParameter(program, gl.VALIDATE_STATUS));
console.error('gl.getError()', gl.getError());
// if there is a program info log, log it
if (gl.getProgramInfoLog(program) !== '')
{
console.warn('Pixi.js Warning: gl.getProgramInfoLog()', gl.getProgramInfoLog(program));
}
gl.deleteProgram(program);
program = null;
}
// clean up some shaders
gl.deleteShader(glVertShader);
gl.deleteShader(glFragShader);
return (this.program = program);
};
/*
Shader.prototype.buildSync = function ()
{
// var str = ""
// str = "Shader.prototype.syncUniforms = function()";
// str += "{\n";
for (var key in this.uniforms)
{
var uniform = this.uniforms[key];
Object.defineProperty(this, key, {
get: function ()
{
return uniform.value
},
set: function (value)
{
this.setUniform(uniform, value);
}
});
console.log( makePropSetter( key, " bloop", uniform.type ) )
// Object.def
// location = uniform._location,
// value = uniform.value,
//i, il;
// str += "gl.uniform1i(this.uniforms."+ key +"._location, this.uniforms." + key + ".value );\n"
}
}*/
/**
* Adds a new uniform
*
* @param uniform {object} the new uniform to attach
*/
Shader.prototype.syncUniform = function (uniform)
{
var location = uniform._location,
value = uniform.value,
gl = this.gl,
i, il;
switch (uniform.type)
{
case 'b':
case 'bool':
case 'boolean':
gl.uniform1i(location, value ? 1 : 0);
break;
// single int value
case 'i':
case '1i':
gl.uniform1i(location, value);
break;
// single float value
case 'f':
case '1f':
gl.uniform1f(location, value);
break;
// Float32Array(2) or JS Arrray
case '2f':
gl.uniform2f(location, value[0], value[1]);
break;
// Float32Array(3) or JS Arrray
case '3f':
gl.uniform3f(location, value[0], value[1], value[2]);
break;
// Float32Array(4) or JS Arrray
case '4f':
gl.uniform4f(location, value[0], value[1], value[2], value[3]);
break;
// a 2D Point object
case 'v2':
gl.uniform2f(location, value.x, value.y);
break;
// a 3D Point object
case 'v3':
gl.uniform3f(location, value.x, value.y, value.z);
break;
// a 4D Point object
case 'v4':
gl.uniform4f(location, value.x, value.y, value.z, value.w);
break;
// Int32Array or JS Array
case '1iv':
gl.uniform1iv(location, value);
break;
// Int32Array or JS Array
case '2iv':
gl.uniform2iv(location, value);
break;
// Int32Array or JS Array
case '3iv':
gl.uniform3iv(location, value);
break;
// Int32Array or JS Array
case '4iv':
gl.uniform4iv(location, value);
break;
// Float32Array or JS Array
case '1fv':
gl.uniform1fv(location, value);
break;
// Float32Array or JS Array
case '2fv':
gl.uniform2fv(location, value);
break;
// Float32Array or JS Array
case '3fv':
gl.uniform3fv(location, value);
break;
// Float32Array or JS Array
case '4fv':
gl.uniform4fv(location, value);
break;
// Float32Array or JS Array
case 'm2':
case 'mat2':
case 'Matrix2fv':
gl.uniformMatrix2fv(location, uniform.transpose, value);
break;
// Float32Array or JS Array
case 'm3':
case 'mat3':
case 'Matrix3fv':
gl.uniformMatrix3fv(location, uniform.transpose, value);
break;
// Float32Array or JS Array
case 'm4':
case 'mat4':
case 'Matrix4fv':
gl.uniformMatrix4fv(location, uniform.transpose, value);
break;
// a Color Value
case 'c':
if (typeof value === 'number')
{
value = utils.hex2rgb(value);
}
gl.uniform3f(location, value[0], value[1], value[2]);
break;
// flat array of integers (JS or typed array)
case 'iv1':
gl.uniform1iv(location, value);
break;
// flat array of integers with 3 x N size (JS or typed array)
case 'iv':
gl.uniform3iv(location, value);
break;
// flat array of floats (JS or typed array)
case 'fv1':
gl.uniform1fv(location, value);
break;
// flat array of floats with 3 x N size (JS or typed array)
case 'fv':
gl.uniform3fv(location, value);
break;
// array of 2D Point objects
case 'v2v':
if (!uniform._array)
{
uniform._array = new Float32Array(2 * value.length);
}
for (i = 0, il = value.length; i < il; ++i)
{
uniform._array[i * 2] = value[i].x;
uniform._array[i * 2 + 1] = value[i].y;
}
gl.uniform2fv(location, uniform._array);
break;
// array of 3D Point objects
case 'v3v':
if (!uniform._array)
{
uniform._array = new Float32Array(3 * value.length);
}
for (i = 0, il = value.length; i < il; ++i)
{
uniform._array[i * 3] = value[i].x;
uniform._array[i * 3 + 1] = value[i].y;
uniform._array[i * 3 + 2] = value[i].z;
}
gl.uniform3fv(location, uniform._array);
break;
// array of 4D Point objects
case 'v4v':
if (!uniform._array)
{
uniform._array = new Float32Array(4 * value.length);
}
for (i = 0, il = value.length; i < il; ++i)
{
uniform._array[i * 4] = value[i].x;
uniform._array[i * 4 + 1] = value[i].y;
uniform._array[i * 4 + 2] = value[i].z;
uniform._array[i * 4 + 3] = value[i].w;
}
gl.uniform4fv(location, uniform._array);
break;
// PIXI.Texture
case 't':
case 'sampler2D':
if (!uniform.value || !uniform.value.baseTexture.hasLoaded)
{
break;
}
// activate this texture
gl.activeTexture(gl['TEXTURE' + this.textureCount]);
var texture = uniform.value.baseTexture._glTextures[gl.id];
if (!texture)
{
this.initSampler2D(uniform);
// set the textur to the newly created one..
texture = uniform.value.baseTexture._glTextures[gl.id];
}
// bind the texture
gl.bindTexture(gl.TEXTURE_2D, texture);
// set uniform to texture index
gl.uniform1i(uniform._location, this.textureCount);
// increment next texture id
this.textureCount++;
break;
default:
console.warn('Pixi.js Shader Warning: Unknown uniform type: ' + uniform.type);
}
};
/**
* Updates the shader uniform values.
*
*/
Shader.prototype.syncUniforms = function ()
{
this.textureCount = 1;
for (var key in this.uniforms)
{
this.syncUniform(this.uniforms[key]);
}
};
/**
* Initialises a Sampler2D uniform (which may only be available later on after initUniforms once the texture has loaded)
*
*/
Shader.prototype.initSampler2D = function (uniform)
{
var gl = this.gl;
var texture = uniform.value.baseTexture;
if(!texture.hasLoaded)
{
return;
}
if (uniform.textureData)
{
//TODO move this...
var data = uniform.textureData;
texture._glTextures[gl.id] = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultipliedAlpha);
// GLTexture = mag linear, min linear_mipmap_linear, wrap repeat + gl.generateMipmap(gl.TEXTURE_2D);
// GLTextureLinear = mag/min linear, wrap clamp
// GLTextureNearestRepeat = mag/min NEAREST, wrap repeat
// GLTextureNearest = mag/min nearest, wrap clamp
// AudioTexture = whatever + luminance + width 512, height 2, border 0
// KeyTexture = whatever + luminance + width 256, height 2, border 0
// magFilter can be: gl.LINEAR, gl.LINEAR_MIPMAP_LINEAR or gl.NEAREST
// wrapS/T can be: gl.CLAMP_TO_EDGE or gl.REPEAT
gl.texImage2D(gl.TEXTURE_2D, 0, data.luminance ? gl.LUMINANCE : gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, data.magFilter ? data.magFilter : gl.LINEAR );
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, data.wrapS ? data.wrapS : gl.CLAMP_TO_EDGE );
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, data.wrapS ? data.wrapS : gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, data.wrapT ? data.wrapT : gl.CLAMP_TO_EDGE);
}
else
{
this.shaderManager.renderer.updateTexture(texture);
}
};
/**
* Destroys the shader.
*
*/
Shader.prototype.destroy = function ()
{
this.gl.deleteProgram(this.program);
this.gl = null;
this.uniforms = null;
this.attributes = null;
this.vertexSrc = null;
this.fragmentSrc = null;
};
Shader.prototype._glCompile = function (type, src)
{
var shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS))
{
console.log(this.gl.getShaderInfoLog(shader));
return null;
}
return shader;
};