vgl/renderer.js

var vgl = require('./vgl');
var inherit = require('../inherit');
var mat4 = require('gl-mat4');

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

  this.m_context = null;
  this.m_modelViewMatrix = mat4.create();
  this.m_normalMatrix = mat4.create();
  this.m_projectionMatrix = null;
  this.m_material = null;
  this.m_mapper = null;
};

/**
 * Create a new instance of class renderer.
 *
 * @class
 * @alias vgl.renderer
 * @extends vgl.graphicsObject
 * @param {object} arg
 * @returns {vgl.renderer}
 */
vgl.renderer = function (arg) {
  'use strict';

  if (!(this instanceof vgl.renderer)) {
    return new vgl.renderer(arg);
  }
  vgl.graphicsObject.call(this);
  arg = arg || {};

  /** @private */
  var m_this = this;
  m_this.m_renderWindow = null;
  m_this.m_contextChanged = false;
  m_this.m_sceneRoot = new vgl.groupNode();
  m_this.m_camera = new vgl.camera(arg);
  m_this.m_nearClippingPlaneTolerance = null;
  m_this.m_x = 0;
  m_this.m_y = 0;
  m_this.m_width = 0;
  m_this.m_height = 0;
  m_this.m_resizable = true;
  m_this.m_resetScene = true;
  m_this.m_layer = 0;
  m_this.m_renderPasses = null;
  m_this.m_resetClippingRange = true;
  m_this.m_depthBits = null;

  m_this.m_camera.addChild(m_this.m_sceneRoot);

  /**
   * Get width of the renderer.
   *
   * @returns {number}
   */
  this.width = function () {
    return m_this.m_width;
  };

  /**
   * Get height of the renderer.
   *
   * @returns {number}
   */
  this.height = function () {
    return m_this.m_height;
  };

  /**
   * Get layer this renderer is associated with.
   *
   * @returns {number}
   */
  this.layer = function () {
    return m_this.m_layer;
  };

  /**
   * Set the layer this renderer is associated with.
   *
   * @param {number} layerNo
   */
  this.setLayer = function (layerNo) {
    m_this.m_layer = layerNo;
    m_this.modified();
  };

  /**
   * Return render window (owner) of the renderer.
   *
   * @returns {vgl.renderWindow}
   */
  this.renderWindow = function () {
    return m_this.m_renderWindow;
  };

  /**
   * Set render window for the renderer.
   *
   * @param {vgl.renderWindow} renWin
   */
  this.setRenderWindow = function (renWin) {
    if (m_this.m_renderWindow !== renWin) {
      if (m_this.m_renderWindow) {
        m_this.m_renderWindow.removeRenderer(this);
      }
      m_this.m_renderWindow = renWin;
      m_this.m_contextChanged = true;
      m_this.modified();
    }
  };

  /**
   * Get main camera of the renderer.
   *
   * @returns {vgl.camera}
   */
  this.camera = function () {
    return m_this.m_camera;
  };

  /**
   * Render the scene.
   */
  this.render = function () {
    var i, renSt, children, actor = null, sortedActors = [],
        mvMatrixInv = mat4.create(), clearColor = null;

    renSt = new vgl.renderState();
    renSt.m_renderer = m_this;
    renSt.m_context = m_this.renderWindow().context();
    if (!m_this.m_depthBits || m_this.m_contextChanged) {
      m_this.m_depthBits = renSt.m_context.getParameter(vgl.GL.DEPTH_BITS);
    }
    renSt.m_contextChanged = m_this.m_contextChanged;

    if (m_this.m_renderPasses) {
      for (i = 0; i < m_this.m_renderPasses.length; i += 1) {
        if (m_this.m_renderPasses[i].render(renSt)) {
          // Stop the rendering if render pass returns false
          console.log('returning');
          m_this.m_renderPasses[i].remove(renSt);
          return;
        }
        m_this.m_renderPasses[i].remove(renSt);
      }
    }

    renSt.m_context.enable(vgl.GL.DEPTH_TEST);
    renSt.m_context.depthFunc(vgl.GL.LEQUAL);

    if (m_this.m_camera.clearMask() & vgl.GL.COLOR_BUFFER_BIT) {
      clearColor = m_this.m_camera.clearColor();
      renSt.m_context.clearColor(clearColor[0], clearColor[1],
                                 clearColor[2], clearColor[3]);
    }

    if (m_this.m_camera.clearMask() & vgl.GL.DEPTH_BUFFER_BIT) {
      renSt.m_context.clearDepth(m_this.m_camera.clearDepth());
    }

    renSt.m_context.clear(m_this.m_camera.clearMask());

    // Set the viewport for this renderer
    renSt.m_context.viewport(m_this.m_x, m_this.m_y,
                             m_this.m_width, m_this.m_height);

    children = m_this.m_sceneRoot.children();

    if (children.length > 0 && m_this.m_resetScene) {
      m_this.m_resetScene = false;
    }

    for (i = 0; i < children.length; i += 1) {
      actor = children[i];

      // Compute the bounds even if the actor is not visible
      actor.computeBounds();

      // If bin number is < 0, then don't even bother
      // rendering the data
      if (actor.visible() && actor.material().binNumber() >= 0) {
        sortedActors.push([actor.material().binNumber(), actor]);
      }
    }

    // Now perform sorting
    sortedActors.sort(function (a, b) { return a[0] - b[0]; });

    for (i = 0; i < sortedActors.length; i += 1) {
      actor = sortedActors[i][1];
      if (actor.referenceFrame() ===
          vgl.boundingObject.ReferenceFrame.Relative) {
        var view = m_this.m_camera.viewMatrix();
        /* If the view matrix is a plain array, keep it as such.  This is
         * intended to preserve precision, and will only be the case if the
         * view matrix was created by deliberately setting it as an array. */
        if (view instanceof Array) {
          renSt.m_modelViewMatrix = new Array(16);
        }
        mat4.multiply(renSt.m_modelViewMatrix, view, actor.matrix());
        renSt.m_projectionMatrix = m_this.m_camera.projectionMatrix();
        renSt.m_modelViewAlignment = m_this.m_camera.viewAlignment();
      } else {
        renSt.m_modelViewMatrix = actor.matrix();
        renSt.m_modelViewAlignment = null;
        renSt.m_projectionMatrix = mat4.create();
        mat4.ortho(renSt.m_projectionMatrix,
                   0, m_this.m_width, 0, m_this.m_height, -1, 1);
      }

      mat4.invert(mvMatrixInv, renSt.m_modelViewMatrix);
      mat4.transpose(renSt.m_normalMatrix, mvMatrixInv);
      renSt.m_material = actor.material();
      renSt.m_mapper = actor.mapper();

      // TODO Fix this shortcut
      renSt.m_material.bind(renSt);
      renSt.m_mapper.render(renSt);
      renSt.m_material.undoBind(renSt);
    }

    renSt.m_context.finish();
    m_this.m_contextChanged = false;
    m_this.m_lastRenderState = renSt;
  };

  /**
   * Resize viewport given a width and height.
   *
   * @param {number} width
   * @param {number} height
   */
  this.resize = function (width, height) {
    if (!width || !height) {
      return;
    }
    // @note: where do m_this.m_x and m_this.m_y come from?
    m_this.positionAndResize(m_this.m_x, m_this.m_y, width, height);
  };

  /**
   * Resize viewport given a position, width and height.
   *
   * @param {number} x
   * @param {number} y
   * @param {number} width
   * @param {number} height
   */
  this.positionAndResize = function (x, y, width, height) {
    var i;

    // TODO move this code to camera
    if (x < 0 || y < 0 || width <= 0 || height <= 0) {
      console.log('[error] Invalid position and resize values',
                  x, y, width, height);
      return;
    }

    //If we're allowing this renderer to resize ...
    if (m_this.m_resizable) {
      m_this.m_width = width;
      m_this.m_height = height;

      m_this.m_camera.setViewAspect(width / height);
      m_this.m_camera.setParallelExtents({width: width, height: height});
      m_this.modified();
    }

    if (m_this.m_renderPasses) {
      for (i = 0; i < m_this.m_renderPasses.length; i += 1) {
        m_this.m_renderPasses[i].resize(width, height);
        m_this.m_renderPasses[i].renderer().positionAndResize(x, y, width, height);
      }
    }
  };

  /**
   * Add new actor to the collection.
   *
   * @param {vgl.actor} actor
   * @returns {boolean}
   */
  this.addActor = function (actor) {
    if (actor instanceof vgl.actor) {
      m_this.m_sceneRoot.addChild(actor);
      m_this.modified();
      return true;
    }

    return false;
  };

  /**
   * Return true if this renderer has this actor attached, false otherwise.
   *
   * @param {vgl.actor} actor
   * @returns {boolean}
   */
  this.hasActor = function (actor) {
    return m_this.m_sceneRoot.hasChild(actor);
  };

  /**
   * Remove the actor from the collection.
   *
   * @param {vgl.actor} actor
   * @returns {boolean}
   */
  this.removeActor = function (actor) {
    if (m_this.m_sceneRoot.children().indexOf(actor) !== -1) {
      /* When we remove an actor, free the VBOs of the mapper and mark the
       * mapper as modified; it will reallocate VBOs as necessary. */
      if (m_this.m_lastRenderState) {
        if (actor.mapper()) {
          actor.mapper()._cleanup(m_this.m_lastRenderState);
        }
        if (actor.material()) {
          actor.material()._cleanup(m_this.m_lastRenderState);
        }
      }
      actor.modified();
      m_this.m_sceneRoot.removeChild(actor);
      m_this.modified();
      return true;
    }

    return false;
  };

  /**
   * If true the scene will be reset, otherwise the scene will not be
   * automatically reset.
   *
   * @param {boolean} reset
   */
  this.setResetScene = function (reset) {
    if (m_this.m_resetScene !== reset) {
      m_this.m_resetScene = reset;
      m_this.modified();
    }
  };

  /**
   * Cleanup.
   *
   * @param {vgl.renderState} renderState
   */
  this._cleanup = function (renderState) {
    var children = m_this.m_sceneRoot.children();
    for (var i = 0; i < children.length; i += 1) {
      var actor = children[i];
      actor.material()._cleanup(renderState);
      actor.mapper()._cleanup(renderState);
    }

    m_this.m_sceneRoot.removeChildren();
    m_this.modified();
  };

  return m_this;
};

inherit(vgl.renderer, vgl.graphicsObject);