Source: gfx/core/renderers/webgl/shaders/Shader.js

/* global console */
var utils = require('../../../utils');

/**
 * Base shader class for PIXI managed shaders.
 *
 * @class
 * @param shaderManager {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;

  // 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;
};