vgl/camera.js

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

/**
 * Create a new instance of class camera.
 *
 * @class
 * @alias vgl.camera
 * @extends vgl.groupNode
 * @param {object} arg
 * @param {boolean?} arg.parallelProjection Optionally start with a parallel
 *      projection.
 * @returns {vgl.camera}
 */
vgl.camera = function (arg) {
  'use strict';

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

  /** @private */
  var m_viewAngle = (Math.PI * 30) / 180.0,
      m_position = vec4.fromValues(0.0, 0.0, 1.0, 1.0),
      m_focalPoint = vec4.fromValues(0.0, 0.0, 0.0, 1.0),
      m_viewUp = vec4.fromValues(0.0, 1.0, 0.0, 0.0),
      m_near = 0.01,
      m_far = 10000.0,
      m_viewAspect = 1.0,
      m_directionOfProjection = vec4.fromValues(0.0, 0.0, -1.0, 0.0),
      m_viewMatrix = mat4.create(),
      m_projectionMatrix = mat4.create(),
      m_computeModelViewMatrixTime = timestamp(),
      m_computeProjectMatrixTime = timestamp(),
      m_left = -1.0,
      m_right = 1.0,
      m_top = +1.0,
      m_bottom = -1.0,
      m_parallelExtents = {zoom: 1, tilesize: 256},
      m_enableParallelProjection = false,
      m_clearColor = [0.0, 0.0, 0.0, 0.0],
      m_clearDepth = 1.0,
      /*jshint bitwise: false */
      m_clearMask = vgl.GL.COLOR_BUFFER_BIT |
                    vgl.GL.DEPTH_BUFFER_BIT;
  /*jshint bitwise: true */

  if (arg.parallelProjection !== undefined) {
    m_enableParallelProjection = arg.parallelProjection ? true : false;
  }

  /**
   * Set view aspect.
   *
   * @param {number} aspect Aspect ration (width / height).
   */
  this.setViewAspect = function (aspect) {
    m_viewAspect = aspect;
    this.modified();
  };

  /**
   * Get parallel projection extents parameters.
   *
   * @returns {object} Extents object with width, height, zoom, and tilesize.
   */
  this.parallelExtents = function () {
    return m_parallelExtents;
  };

  /**
   * Set parallel projection extents parameters.
   *
   * @param {object} extents Extents object with width, height, zoom, and
   *    tilesize.
   */
  this.setParallelExtents = function (extents) {
    var prop = ['width', 'height', 'zoom', 'tilesize'], mod = false, i, key;
    for (i = 0; i < prop.length; i += 1) {
      key = prop[i];
      if (extents[key] !== undefined &&
          extents[key] !== m_parallelExtents[key]) {
        m_parallelExtents[key] = extents[key];
        mod = true;
      }
    }
    if (mod && m_parallelExtents.width && m_parallelExtents.height &&
        m_parallelExtents.zoom !== undefined && m_parallelExtents.tilesize) {
      var unitsPerPixel = this.unitsPerPixel(m_parallelExtents.zoom,
                                             m_parallelExtents.tilesize);
      m_right = unitsPerPixel * m_parallelExtents.width / 2;
      m_left = -m_right;
      m_top = unitsPerPixel * m_parallelExtents.height / 2;
      m_bottom = -m_top;
      this.modified();
    }
  };

  /**
   * Compute the units per pixel.
   *
   * @param {number} zoom Tile zoom level.
   * @param {number} tilesize Number of pixels per tile (defaults to 256).
   * @returns {number} unitsPerPixel.
   */
  this.unitsPerPixel = function (zoom, tilesize) {
    tilesize = tilesize || 256;
    return 360.0 * Math.pow(2, -zoom) / tilesize;
  };

  /**
   * Return view-matrix for the camera This method does not compute the
   * view-matrix for the camera. It is assumed that a call to computeViewMatrix
   * has been made earlier.
   *
   * @returns {mat4}
   */
  this.viewMatrix = function () {
    return this.computeViewMatrix();
  };

  /**
   * Set the view-matrix for the camera and mark that it is up to date so that
   * it won't be recomputed unless something else changes.
   *
   * @param {mat4} view new view matrix.
   * @param {boolean} preserveType If true, clone the input using slice.  This
   *    can be used to ensure the array is a specific precision.
   */
  this.setViewMatrix = function (view, preserveType) {
    if (!preserveType) {
      mat4.copy(m_viewMatrix, view);
    } else {
      m_viewMatrix = view.slice();
    }
    m_computeModelViewMatrixTime.modified();
  };

  /**
   * Return camera projection matrix This method does not compute the
   * projection-matrix for the camera. It is assumed that a call to
   * computeProjectionMatrix has been made earlier.
   *
   * @returns {mat4}
   */
  this.projectionMatrix = function () {
    return this.computeProjectionMatrix();
  };

  /**
   * Set the projection-matrix for the camera and mark that it is up to date so
   * that it won't be recomputed unless something else changes.
   *
   * @param {mat4} proj New projection matrix.
   */
  this.setProjectionMatrix = function (proj) {
    mat4.copy(m_projectionMatrix, proj);
    m_computeProjectMatrixTime.modified();
  };

  /**
   * Return clear mask used by this camera.
   *
   * @returns {number}
   */
  this.clearMask = function () {
    return m_clearMask;
  };

  /**
   * Get clear color (background color) of the camera.
   *
   * @returns {Array}
   */
  this.clearColor = function () {
    return m_clearColor;
  };

  /**
   * Get the clear depth value.
   *
   * @returns {number}
   */
  this.clearDepth = function () {
    return m_clearDepth;
  };

  /**
   * Compute direction of projection.
   */
  this.computeDirectionOfProjection = function () {
    vec3.subtract(m_directionOfProjection, m_focalPoint, m_position);
    vec3.normalize(m_directionOfProjection, m_directionOfProjection);
    this.modified();
  };

  /**
   * Compute camera view matrix.
   *
   * @returns {mat4}
   */
  this.computeViewMatrix = function () {
    if (m_computeModelViewMatrixTime.getMTime() < this.getMTime()) {
      mat4.lookAt(m_viewMatrix, m_position, m_focalPoint, m_viewUp);
      m_computeModelViewMatrixTime.modified();
    }
    return m_viewMatrix;
  };

  /**
   * Compute camera projection matrix.
   *
   * @returns {mat4}
   */
  this.computeProjectionMatrix = function () {
    if (m_computeProjectMatrixTime.getMTime() < this.getMTime()) {
      if (!m_enableParallelProjection) {
        mat4.perspective(m_projectionMatrix, m_viewAngle, m_viewAspect, m_near, m_far);
      } else {
        mat4.ortho(m_projectionMatrix,
                   m_left, m_right, m_bottom, m_top, m_near, m_far);
      }

      m_computeProjectMatrixTime.modified();
    }

    return m_projectionMatrix;
  };

  this.computeDirectionOfProjection();

  return this;
};

inherit(vgl.camera, vgl.groupNode);