vgl/shaderProgram.js

var vgl = require('./vgl');
var inherit = require('../inherit');
var timestamp = require('../timestamp');

/**
 * Create a new instance of class shaderProgram.
 *
 * @class
 * @alias vgl.shaderProgram
 * @returns {vgl.shaderProgram}
 */
vgl.shaderProgram = function () {
  'use strict';

  if (!(this instanceof vgl.shaderProgram)) {
    return new vgl.shaderProgram();
  }
  vgl.materialAttribute.call(
    this, vgl.materialAttributeType.ShaderProgram);

  var m_this = this,
      m_programHandle = 0,
      m_compileTimestamp = timestamp(),
      m_bindTimestamp = timestamp(),
      m_shaders = [],
      m_uniforms = [],
      m_vertexAttributes = {},
      m_uniformNameToLocation = {},
      m_vertexAttributeNameToLocation = {};

  /**
   * Query uniform location in the program.
   *
   * @param {vgl.renderState} renderState
   * @param {string} name
   * @returns {number}
   */
  this.queryUniformLocation = function (renderState, name) {
    return renderState.m_context.getUniformLocation(m_programHandle, name);
  };

  /**
   * Query attribute location in the program.
   *
   * @param {vgl.renderState} renderState
   * @param {string} name
   * @returns {number}
   */
  this.queryAttributeLocation = function (renderState, name) {
    return renderState.m_context.getAttribLocation(m_programHandle, name);
  };

  /**
   * Add a new shader to the program.
   *
   * @param {string} shader
   * @returns {boolean}
   */
  this.addShader = function (shader) {
    if (m_shaders.indexOf(shader) > -1) {
      return false;
    }

    var i;
    for (i = m_shaders.length - 2; i >= 0; i -= 1) {
      if (m_shaders[i].shaderType() === shader.shaderType()) {
        m_shaders.splice(i, 1);
      }
    }

    m_shaders.push(shader);
    m_this.modified();
    return true;
  };

  /**
   * Add a new uniform to the program.
   *
   * @param {vgl.uniform} uniform
   * @returns {boolean}
   */
  this.addUniform = function (uniform) {
    if (m_uniforms.indexOf(uniform) > -1) {
      return false;
    }

    m_uniforms.push(uniform);
    m_this.modified();
    return true;
  };

  /**
   * Add a new vertex attribute to the program.
   *
   * @param {vgl.vertexAttribute} attr
   * @param {string} key
   */
  this.addVertexAttribute = function (attr, key) {
    m_vertexAttributes[key] = attr;
    m_this.modified();
  };

  /**
   * Get uniform location.
   *
   * This method does not perform any query into the program but relies on
   * the fact that it depends on a call to queryUniformLocation earlier.
   *
   * @param {string} name
   * @returns {number}
   */
  this.uniformLocation = function (name) {
    return m_uniformNameToLocation[name];
  };

  /**
   * Get attribute location.
   *
   * This method does not perform any query into the program but relies on the
   * fact that it depends on a call to queryUniformLocation earlier.
   *
   * @param {string} name
   * @returns {number}
   */
  this.attributeLocation = function (name) {
    return m_vertexAttributeNameToLocation[name];
  };

  /**
   * Get uniform object using name as the key.
   *
   * @param {string} name
   * @returns {vgl.uniform}
   */
  this.uniform = function (name) {
    var i;
    for (i = 0; i < m_uniforms.length; i += 1) {
      if (m_uniforms[i].name() === name) {
        return m_uniforms[i];
      }
    }

    return null;
  };

  /**
   * Update all uniforms.
   *
   * This method should not be used directly unless required.
   *
   * @param {vgl.renderState} renderState
   */
  this.updateUniforms = function (renderState) {
    var i;

    for (i = 0; i < m_uniforms.length; i += 1) {
      m_uniforms[i].callGL(renderState,
                           m_uniformNameToLocation[m_uniforms[i].name()]);
    }
  };

  /**
   * Link shader program.
   *
   * @param {vgl.renderState} renderState
   * @returns {boolean}
   */
  this.link = function (renderState) {
    renderState.m_context.linkProgram(m_programHandle);

    // If creating the shader program failed, alert
    if (!renderState.m_context.getProgramParameter(m_programHandle,
                                                   vgl.GL.LINK_STATUS)) {
      console.log('[ERROR] Unable to initialize the shader program.');
      return false;
    }

    return true;
  };

  /**
   * Use the shader program.
   *
   * @param {vgl.renderState} renderState
   */
  this.use = function (renderState) {
    renderState.m_context.useProgram(m_programHandle);
  };

  /**
   * Perform any initialization required.
   *
   * @param {vgl.renderState} renderState
   */
  this._setup = function (renderState) {
    if (m_programHandle === 0) {
      m_programHandle = renderState.m_context.createProgram();
    }
  };

  /**
   * Perform any clean up required when the program gets deleted.
   *
   * @param {vgl.renderState} renderState
   */
  this._cleanup = function (renderState) {
    m_this.deleteVertexAndFragment(renderState);
    m_this.deleteProgram(renderState);
    m_this.modified();
  };

  /**
   * Delete the shader program.
   *
   * @param {vgl.renderState} renderState
   */
  this.deleteProgram = function (renderState) {
    if (m_programHandle) {
      renderState.m_context.deleteProgram(m_programHandle);
    }
    m_programHandle = 0;
  };

  /**
   * Delete vertex and fragment shaders.
   *
   * @param {vgl.renderState} renderState
   */
  this.deleteVertexAndFragment = function (renderState) {
    var i;
    for (i = 0; i < m_shaders.length; i += 1) {
      if (m_shaders[i].shaderHandle(renderState)) {
        renderState.m_context.detachShader(m_programHandle, m_shaders[i].shaderHandle(renderState));
      }
      renderState.m_context.deleteShader(m_shaders[i].shaderHandle(renderState));
      m_shaders[i].removeContext(renderState);
    }
  };

  /**
   * Compile and link a shader.
   *
   * @param {vgl.renderState} renderState
   */
  this.compileAndLink = function (renderState) {
    var i;

    if (m_compileTimestamp.getMTime() >= this.getMTime()) {
      return;
    }

    m_this._setup(renderState);

    // Compile shaders
    for (i = 0; i < m_shaders.length; i += 1) {
      m_shaders[i].compile(renderState);
      m_shaders[i].attachShader(renderState, m_programHandle);
    }

    m_this.bindAttributes(renderState);

    // link program
    if (!m_this.link(renderState)) {
      console.log('[ERROR] Failed to link Program');
      m_this._cleanup(renderState);
    }

    m_compileTimestamp.modified();
  };

  /**
   * Bind the program with its shaders.
   *
   * @param {vgl.renderState} renderState
   */
  this.bind = function (renderState) {
    var i = 0;

    if (m_bindTimestamp.getMTime() < m_this.getMTime()) {

      // Compile shaders
      m_this.compileAndLink(renderState);

      m_this.use(renderState);
      m_this.bindUniforms(renderState);
      m_bindTimestamp.modified();
    } else {
      m_this.use(renderState);
    }

    // Call update callback.
    for (i = 0; i < m_uniforms.length; i += 1) {
      m_uniforms[i].update(renderState, m_this);
    }

    // Now update values to GL.
    m_this.updateUniforms(renderState);
  };

  /**
   * Undo binding of the shader program.
   *
   * @param {vgl.renderState} renderState
   */
  this.undoBind = function (renderState) {
    // REF https://www.khronos.org/opengles/sdk/docs/man/xhtml/glUseProgram.xml
    // If program is 0, then the current rendering state refers to an invalid
    // program object, and the results of vertex and fragment shader execution
    // due to any glDrawArrays or glDrawElements commands are undefined
    renderState.m_context.useProgram(null);
  };

  /**
   * Bind vertex data.
   *
   * @param {vgl.renderState} renderState
   * @param {string} key
   */
  this.bindVertexData = function (renderState, key) {
    if (m_vertexAttributes.hasOwnProperty(key)) {
      m_vertexAttributes[key].bindVertexData(renderState, key);
    }
  };

  /**
   * Undo bind vertex data.
   *
   * @param {vgl.renderState} renderState
   * @param {string} key
   */
  this.undoBindVertexData = function (renderState, key) {
    if (m_vertexAttributes.hasOwnProperty(key)) {
      m_vertexAttributes[key].undoBindVertexData(renderState, key);
    }
  };

  /**
   * Bind uniforms.
   *
   * @param {vgl.renderState} renderState
   */
  this.bindUniforms = function (renderState) {
    var i;
    for (i = 0; i < m_uniforms.length; i += 1) {
      m_uniformNameToLocation[m_uniforms[i].name()] = this
          .queryUniformLocation(renderState, m_uniforms[i].name());
    }
  };

  /**
   * Bind vertex attributes.
   *
   * @param {vgl.renderState} renderState
   */
  this.bindAttributes = function (renderState) {
    var key, name;
    for (key in m_vertexAttributes) {
      if (m_vertexAttributes.hasOwnProperty(key)) {
        name = m_vertexAttributes[key].name();
        renderState.m_context.bindAttribLocation(m_programHandle, key, name);
        m_vertexAttributeNameToLocation[name] = key;
      }
    }
  };

  return m_this;
};

inherit(vgl.shaderProgram, vgl.materialAttribute);