Home Reference Source Repository

js/base/Camera.js

'use strict'

import Vector3 from './Vector3'
import Matrix from './Matrix'

/**
 * Camera class
 * @access public
 */
export default class Camera {
  /**
   * constructor
   * @access public
   * @constructor
   */
  constructor() {
    /** @type {Vector3} */
    this.position = new Vector3()

    /** @type {Matrix} */
    this.projMat = new Matrix()

    /** @type {Matrix} */
    this.viewMat = new Matrix()


    /** @type {DH3DObject} */
    this.bindObj = null

    /** @type {float} */
    this.distance = 20.0

    /** @type {float} */
    this.bindXAngle = 0.0

    /** @type {float} */
    this.bindYAngle = 0.0

    /** @type {Vector3} */
    this.bindOffset = new Vector3()

    /** @type {float} */
    this._aspect = 1.0
    /** @type {string} */
    this._mode = ''

    /** @type {Motion} */
    this._motion = null

    /** @type {Animator} */
    this._animator = null

    /** @type {boolean} */
    this._animating = false

    /** @type {float} */
    this._animationTime = 0

    /** @type {float} */
    this._animationFrame = 0
    // this._motionBlendStep = 0
    // this._motionBlendCount = 0

    /** @type {Matrix} */
    this._projViewMat = new Matrix()

    this.identity()
  }

  /**
   * reset camera position and matrix
   * @access public
   * @returns {void}
   */
  identity() {
    this.position.setValue(0, 0, 0)
    this.projMat.identity()
    this.viewMat.identity()
  }

  /**
   * set motion to object
   * @access public
   * @param {Motion} motion -
   * @returns {void}
   */
  setMotion(motion) {
    this._motion = motion
  }

  /**
   * get motion of object
   * @access public
   * @returns {Motion} - motion
   */
  getMotion() {
    return this._motion
  }

  /**
   * set animation
   * @access public
   * @param {bool} animating - 
   * @returns {void}
   */
  setAnimating(animating = true) {
    if(animating){
      this._animating = true
    }else{
      this._animating = false
    }
  }

  /**
   * get animation state
   * @access public
   * @returns {bool} - true if it's animating
   */
  getAnimating() {
    return this._animating
  }

  /**
   * set animation loop
   * @access public
   * @param {bool} loop - 
   * @returns {void}
   */
  setLoop(loop = true) {
    if(loop) {
      this._loop = true
    }else{
      this._loop = false
    }
  }

  /**
   * get loop state
   * @access public
   * @returns {bool} - true if loop is enabled
   */
  getLoop() {
    return this._loop
  }

  /**
   * set animator to object
   * @access public
   * @param {Animator} animator -
   * @returns {void}
   */
  setAnimator(animator) {
    this._animator = animator
  }

  /**
   * set animation time (sec)
   * @access public
   * @param {float} time - animation time
   * @returns {void}
   */
  setAnimationTime(time) {
    this._animationTime = time
  }

  /**
   * get animation time (sec)
   * @access public
   * @returns {float} - animation time
   */
  getAnimationTime() {
    return this._animationTime
  }


  /**
   * set camera position
   * @access public
   * @param {float} x - x position of camera
   * @param {float} y - y position of camera
   * @param {float} z - z position of camera
   * @returns {void}
   */
  setPosition(x, y, z) {
    this.translate(x - this.position.x, y - this.position.y, z - this.position.z)
  }

  /**
   * move camera position
   * @access public
   * @param {float} x - x value of camera transition
   * @param {float} y - y value of camera transition
   * @param {float} z - z value of camera transition
   * @returns {void}
   */
  translate(x, y, z) {
    this.viewMat.translate(this.viewMat, -x, -y, -z)
    this.position.x += x
    this.position.y += y
    this.position.z += z
  }

  /**
   * rotate camera
   * @access public
   * @param {float} angle - rotation angle in radian
   * @param {float} x - x value of axis of rotation
   * @param {float} y - y value of axis of rotation
   * @param {float} z - z value of axis of rotation
   * @returns {void}
   */
  rotate(angle, x, y, z) {
    this.viewMat.rotate(this.viewMat, angle, x, y, z)
  }

  /**
   * scale view matrix: zoom up/down 
   * @access public
   * @param {float} x - x value of scale
   * @param {float} y - y value of scale
   * @param {float} z - z value of scale
   * @returns {void}
   */
  scale(x, y, z) {
    this.viewMat.scale(this.viewMat, x, y, z)
  }

  /**
   * scale projection matrix 
   * @access public
   * @param {float} x - x value of scale
   * @param {float} y - y value of scale
   * @param {float} z - z value of scale
   * @returns {void}
   */
  scaleProjection(x, y, z) {
    this.projMat.scale(this.projMat, x, y, z)
  }

  /**
   * change camera matrix to look at the given point
   * @access public
   * @param {float} eyeX - 
   * @param {float} eyeY -
   * @param {float} eyeZ -
   * @param {float} centerX -
   * @param {float} centerY -
   * @param {float} centerZ -
   * @param {float} upX -
   * @param {float} upY -
   * @param {float} upZ -
   * @returns {void}
   */
  lookat(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ) {
    const s = new Vector3()
    const u = new Vector3()
    const f = new Vector3()
    const up = new Vector3()
    const mat = new Matrix()

    f.x = centerX - eyeX
    f.y = centerY - eyeY
    f.z = centerZ - eyeZ
    f.normalize()

    up.x = upX
    up.y = upY
    up.z = upZ
    //up.normalize()

    s.cross(f, up)
    u.cross(s, f)

    s.normalize()
    u.normalize()

    mat.m11 = s.x
    mat.m12 = s.y
    mat.m13 = s.z
    mat.m14 = 0
    mat.m21 = u.x
    mat.m22 = u.y
    mat.m23 = u.z
    mat.m24 = 0
    mat.m31 = -f.x
    mat.m32 = -f.y
    mat.m33 = -f.z
    mat.m34 = 0
    mat.m41 = 0
    mat.m42 = 0
    mat.m43 = 0
    mat.m44 = 1

    this.viewMat = mat
    this.viewMat.translate(this.viewMat, -eyeX, -eyeY, -eyeZ)
    this.position.setValue(eyeX, eyeY, eyeZ)
  }

  /**
   *
   * @access public
   * @param {float} left -
   * @param {float} right -
   * @param {float} bottom -
   * @param {float} top -
   * @param {float} nearVal -
   * @param {float} farVal -
   * @returns {void}
   */
  frustum(left, right, bottom, top, nearVal, farVal) {
    const mat = new Matrix()

    mat.m11 = 2 * nearVal / (right - left)
    mat.m12 = 0
    mat.m13 = (right + left) / (right - left)
    mat.m14 = 0
    mat.m21 = 0
    mat.m22 = 2 * nearVal / (top - bottom)
    mat.m23 = (top + bottom) / (top - bottom)
    mat.m24 = 0
    mat.m31 = 0
    mat.m32 = 0
    mat.m33 = -(farVal + nearVal) / (farVal - nearVal)
    mat.m34 = -2 * farVal * nearVal / (farVal - nearVal)
    mat.m41 = 0
    mat.m42 = 0
    mat.m43 = -1
    mat.m44 = 0

    this.projMat = mat
    this._mode = 'frustum'
  }

  /**
   *
   * @access public
   * @param {float} fovy -
   * @param {float} aspect -
   * @param {float} nearVal -
   * @param {float} farVal -
   * @returns {void}
   */
  perspective(fovy, aspect, nearVal, farVal) {
    const mat = new Matrix()
    const cot = 1.0 / Math.tan(fovy * Math.PI / 360.0)
    this._aspect = aspect

    mat.m11 = cot / aspect
    mat.m12 = 0
    mat.m13 = 0
    mat.m14 = 0
    mat.m21 = 0
    mat.m22 = cot
    mat.m23 = 0
    mat.m24 = 0
    mat.m31 = 0
    mat.m32 = 0
    mat.m33 = -(farVal + nearVal) / (farVal - nearVal)
    mat.m34 = -2 * farVal * nearVal / (farVal - nearVal)
    mat.m41 = 0
    mat.m42 = 0
    mat.m43 = -1
    mat.m44 = 0

    this.projMat = mat
    this._mode = 'perspective'
  }

  /**
   *
   * @access public
   * @param {float} left -
   * @param {float} right -
   * @param {float} bottom -
   * @param {float} top -
   * @param {float} nearVal -
   * @param {float} farVal -
   * @returns {void}
   */
  ortho(left, right, bottom, top, nearVal, farVal) {
    const mat = new Matrix()
    this._aspect = Math.abs((bottom - top) / (right - left))

    mat.m11 = 2 / (right - left)
    mat.m12 = 0
    mat.m13 = 0
    mat.m14 = -(right + left) / (right - left)
    mat.m21 = 0
    mat.m22 = 2 / (top - bottom)
    mat.m23 = 0
    mat.m24 = -(top + bottom) / (top - bottom)
    mat.m31 = 0
    mat.m32 = 0
    mat.m33 = -2 / (farVal - nearVal)
    mat.m34 = -(farVal + nearVal) / (farVal - nearVal)
    mat.m41 = 0
    mat.m42 = 0
    mat.m43 = 0
    mat.m44 = 1

    this.projMat = mat
    this._mode = 'ortho'
  }

  /**
   * get projection array for WebGL
   * @access public
   * @returns {Float32Array} projection matrix value
   */
  getProjectionArray() {
    return this.projMat.getWebGLFloatArray()
  }

  /**
   * get view array for WebGL
   * @access public
   * @returns {Float32Array} view matrix value
   */
  getViewArray() {
    return this.viewMat.getWebGLFloatArray()
  }

  /**
   * get projection matrix
   * @access public
   * @returns {Float32Array} projection view matrix
   */
  getProjectionViewMatrix() {
    this._projViewMat.multiplyMatrix(this.projMat, this.viewMat)

    return this._projViewMat.getWebGLFloatArrayTransposed()
  }

  /**
   * get normal matrix
   * @access public
   * @returns {Float32Array} normal matrix
   */
  getNormalMatrix() {
    // モデルビュー行列の左上3x3の逆転置行列
    const m = this.viewMat
    let buf = 0
    const m11 = m.m11; let m12 = m.m12; let m13 = m.m13
    let m21 = m.m21;   let m22 = m.m22; let m23 = m.m23
    let m31 = m.m31;   let m32 = m.m32; let m33 = m.m33
    let r11 = 1.0;     let r12 = 0.0;   let r13 = 0.0
    let r21 = 0.0;     let r22 = 1.0;   let r23 = 0.0
    let r31 = 0.0;     let r32 = 0.0;   let r33 = 1.0

    let w1 = Math.abs(m11)
    let w2 = Math.abs(m21)
    let w3 = Math.abs(m31)
    const max = w1 > w2 ? w1 : w2

    if(max < w3){
      buf = 1.0 / m31
      // m
      w1 = m11
      w2 = m12
      w3 = m13
      // m11 = 1.0
      m12 = m32 * buf
      m13 = m33 * buf
      m31 = w1
      m32 = w2
      m33 = w3
      // r
      r11 = 0.0
      r13 = buf
      r31 = 1.0
      r33 = 0.0
    }else if(w1 < w2){
      buf = 1.0 / m21
      // m
      w1 = m11
      w2 = m12
      w3 = m13
      // m11 = 1.0
      m12 = m22 * buf
      m13 = m23 * buf
      m21 = w1
      m22 = w2
      m23 = w3
      // r
      r11 = 0.0
      r12 = buf
      r21 = 1.0
      r22 = 0.0
    }else{
      buf = 1.0 / m11
      m12 *= buf
      m13 *= buf
      r11 = buf
    }
    m22 -= m12 * m21
    m23 -= m13 * m21
    r21 -= r11 * m21
    r22 -= r12 * m21
    r23 -= r13 * m21

    m32 -= m12 * m31
    m33 -= m13 * m31
    r31 -= r11 * m31
    r32 -= r12 * m31
    r33 -= r13 * m31

    if(Math.abs(m22) > Math.abs(m32)){
      buf = 1.0 / m22
      // m
      // m22 = 1.0
      m23 *= buf
      r21 *= buf
      r22 *= buf
      r23 *= buf
    }else{
      buf = 1.0 / m32
      w2 = m22
      w3 = m23
      // m22 = 1.0
      m23 = m33 * buf
      m32 = w2
      m33 = w3
      w1 = r21
      w2 = r22
      w3 = r23
      r21 = r31 * buf
      r22 = r32 * buf
      r23 = r33 * buf
      r31 = w1
      r32 = w2
      r33 = w3
    }
    m13 -= m23 * m12
    r11 -= r21 * m12
    r12 -= r22 * m12
    r13 -= r23 * m12

    m33 -= m23 * m32
    r31 -= r21 * m32
    r32 -= r22 * m32
    r33 -= r23 * m32


    buf = 1.0 / m33
    r31 *= buf
    r32 *= buf
    r33 *= buf

    r11 -= r31 * m13
    r12 -= r32 * m13
    r13 -= r33 * m13

    r21 -= r31 * m23
    r22 -= r32 * m23
    r23 -= r33 * m23

    return new Float32Array([
      r11, r21, r31,
      r12, r22, r32,
      r13, r23, r33
    ])
  }

  /**
   * get position array for WebGL
   * @access public
   * @returns {Float32Array} position value
   */
  getPosition() {
    return this.position.getWebGLFloatArray()
  }

  /**
   * get screen position from world position
   * @access public
   * @param {Vector3} pos - world position
   * @returns {Vector3} screen position 
   */
  getScreenPosition(pos) {
    const sPos = new Vector3()

    this._projViewMat.multiplyMatrix(this.projMat, this.viewMat)
    const m = this._projViewMat

    sPos.x = m.m11 * pos.x
           + m.m12 * pos.y
           + m.m13 * pos.z
           + m.m14
    sPos.y = m.m21 * pos.x
           + m.m22 * pos.y
           + m.m23 * pos.z
           + m.m24
    sPos.z = m.m31 * pos.x
           + m.m32 * pos.y
           + m.m33 * pos.z
           + m.m34
    const w = 1.0 / (
             m.m41 * pos.x
           + m.m42 * pos.y
           + m.m43 * pos.z
           + m.m44)
    sPos.x *= w
    sPos.y *= w
    sPos.z *= w

    return sPos
  }

  /**
   * show camera matrix for debug
   * @access public
   * @returns {void}
   */
  showCameraMatrix() {
    /*
    console.log('cameraMatrix:\n'
            + 'projectionMatrix:\n'
            + this.projMat.m11 + ' ' + this.projMat.m12 + ' ' + this.projMat.m13 + ' ' + this.projMat.m14 + '\n'
            + this.projMat.m21 + ' ' + this.projMat.m22 + ' ' + this.projMat.m23 + ' ' + this.projMat.m24 + '\n'
            + this.projMat.m31 + ' ' + this.projMat.m32 + ' ' + this.projMat.m33 + ' ' + this.projMat.m34 + '\n'
            + this.projMat.m41 + ' ' + this.projMat.m42 + ' ' + this.projMat.m43 + ' ' + this.projMat.m44 + '\n'
            + 'viewMatrix:\n'
            + this.viewMat.m11 + ' ' + this.viewMat.m12 + ' ' + this.viewMat.m13 + ' ' + this.viewMat.m14 + '\n'
            + this.viewMat.m21 + ' ' + this.viewMat.m22 + ' ' + this.viewMat.m23 + ' ' + this.viewMat.m24 + '\n'
            + this.viewMat.m31 + ' ' + this.viewMat.m32 + ' ' + this.viewMat.m33 + ' ' + this.viewMat.m34 + '\n'
            + this.viewMat.m41 + ' ' + this.viewMat.m42 + ' ' + this.viewMat.m43 + ' ' + this.viewMat.m44 + '\n')
    */
  }

  /**
   * change coordination
   * @access public
   * @returns {void}
   */
  changeCoordination() {
    const vm = this.viewMat
    vm.m13 = -vm.m13
    vm.m23 = -vm.m23
    vm.m33 = -vm.m33
    vm.m43 = -vm.m43
  }

  /**
   * bind camera to object
   * @access public
   * @param {DH3DObject} dhObject -
   * @returns {void}
   */
  bind(dhObject) {
    this.bindObj = dhObject
  }

  /**
   * set offset between camera and binded object
   * @access public
   * @param {float} x -
   * @param {float} y -
   * @param {float} z -
   * @returns {void}
   */
  setBindOffset(x, y, z) {
    this.bindOffset.x = x
    this.bindOffset.y = y
    this.bindOffset.z = z
  }

  /**
   * unbind object
   * @access public
   * @returns {void}
   */
  unbind() {
    this.bindObj = null
  }

  /**
   * update camera position and matrix
   * @access public
   * @param {float} elapsedTime - elapsedTime
   * @returns {void}
   */
  update(elapsedTime) {
    if(this._animating){
      if(this._animator){
        this._animator.animate(this, elapsedTime)
      }
    }else if(this.bindObj){
      const objPos = this.bindObj._position
      const ox = objPos.x + this.bindOffset.x
      const oy = objPos.y + this.bindOffset.y
      const oz = objPos.z + this.bindOffset.z
      const cx = ox - this.distance * Math.cos(this.bindXAngle) * Math.sin(this.bindYAngle)
      const cy = oy - this.distance * Math.sin(this.bindXAngle)
      const cz = oz - this.distance * Math.cos(this.bindXAngle) * Math.cos(this.bindYAngle)

      this.lookat(cx, cy, cz,
                  ox, oy, oz,
                  0, 1, 0)
    }
  }
}