Source

Camera.js

import * as mat4 from 'gl-matrix/mat4';
import * as vec2 from 'gl-matrix/vec2';
import * as vec3 from 'gl-matrix/vec3';
import * as vec4 from 'gl-matrix/vec4';
import * as quat from 'gl-matrix/quat';
import { glMatrix } from 'gl-matrix';
import { project, unproject } from './Math/gl-matrix-extension';

/**
 * A camera
 *
 * @category Scene
 */
class Camera {
    /**
     * Constructor
     *
     * @param {Camera.Type} [type] Type of camera
     */
    constructor(type = Camera.Type.Perspective) {
        /**
         * Camera's direction
         *
         * @type {glMatrix.vec3}
         * @private
         */
        this.direction = vec3.create();

        /**
         * Field of view
         *
         * @type {number}
         * @private
         */
        this.fov = 45.0;

        /**
         * Visibility limits: min (x) and max (y)
         *
         * @type {glMatrix.vec2}
         * @private
         */
        this.limits = vec2.fromValues(0.1, 100.0);

        /**
         * Projection matrix
         *
         * @type {glMatrix.mat4}
         * @private
         */
        this.matrixProjection = mat4.create();

        /**
         * View matrix
         *
         * @type {glMatrix.mat4}
         * @private
         */
        this.matrixView = mat4.create();

        /**
         * Indicates if the view matrix need an update
         *
         * @type {boolean}
         * @private
         */
        this.matrixViewNeedUpdate = true;

        /**
         * Resulting matrix with camera's transformations
         *
         * @type {glMatrix.mat4}
         * @private
         */
        this.matrixViewProjection = mat4.create();

        /**
         * Indicates if the view matrix need an update
         *
         * @type {boolean}
         * @private
         */
        this.matrixViewProjectionNeedUpdate = true;

        /**
         * Camera's position
         *
         * @type {glMatrix.vec3}
         * @private
         */
        this.position = vec3.fromValues(0.0, 0.0, 3.0);

        /**
         * Ratio: 16/9, 4/3, …
         *
         * @type {number}
         * @private
         */
        this.ratio = 16.0 / 9.0;

        /**
         * Camera's rotation
         *
         * @type {glMatrix.quat}
         * @private
         */
        this.rotation = quat.fromValues(0.0, 0.0, 0.0, 1.0);

        /**
         * Type of camera
         *
         * @type {Camera.Type}
         * @private
         */
        this.type = type;

        /**
         * View size with x, y, w and h values
         *
         * @type {glMatrix.vec4}
         * @private
         */
        this.viewport = vec4.create();

        /**
         * Zoom
         *
         * @type {number}
         * @default 1.0
         * @private
         */
        this.zoomScale = 1.0;

        // Force projection matrix computation
        this.setType(this.type);
    }

    /**
     * Set camera's direction: Point to look at
     *
     * @param {number} x Direction on X
     * @param {number} y Direction on Y
     * @param {number} z Direction on Z
     * @return {Camera} A reference to the instance
     */
    lookAt(x, y, z) {
        vec3.set(this.direction, x, y, z);
        this.matrixViewNeedUpdate = true;

        return this;
    }

    /**
     * Set camera's position
     *
     * @param {number} x Position on X
     * @param {number} y Position on Y
     * @param {number} z Position on Z
     * @return {Camera} A reference to the instance
     */
    move(x, y, z) {
        vec3.set(this.position, x, y, z);
        this.matrixViewNeedUpdate = true;

        return this;
    }

    /**
     * Set camera's rotation
     *
     * @param {number} yaw A floating value
     * @param {number} pitch A floating value
     * @return {Camera} A reference to the instance
     */
    rotate(yaw, pitch) {
        const yawQuat = quat.fromValues(0.0, 0.0, 0.0, 1.0);
        const pitchQuat = quat.fromValues(0.0, 0.0, 0.0, 1.0);

        quat.setAxisAngle(yawQuat, [0.0, 1.0, 0.0], yaw);
        quat.setAxisAngle(pitchQuat, [1.0, 0.0, 0.0], -pitch);
        quat.multiply(this.rotation, yawQuat, pitchQuat);

        /**
         * Multiply two vec4
         *
         * @param {quat} q1 First vector
         * @param {quat} q2 Second vector
         */
        function multiply(q1, q2) {
            return [q1[3] * q2[0] + q1[0] * q2[3] + q1[2] * q2[1] - q1[1] * q2[2],
                q1[3] * q2[1] + q1[1] * q2[3] + q1[0] * q2[2] - q1[2] * q2[0],
                q1[3] * q2[2] + q1[2] * q2[3] + q1[1] * q2[0] - q1[0] * q2[1],
                q1[3] * q2[3] + q1[0] * q2[0] + q1[1] * q2[1] - q1[2] * q2[2]];
        }

        const d = multiply(this.rotation, [this.direction[0], this.direction[1], this.direction[2], 0.0]);
        const p = multiply(this.rotation, [this.position[0], this.position[1], this.position[2], 0.0]);

        vec3.set(this.direction, d[0], d[1], d[2]);
        vec3.set(this.position, p[0], p[1], p[2]);

        this.matrixViewNeedUpdate = true;

        return this;
    }

    /**
     * Set field of view
     *
     * @param {number} value Value in degrees (default: 45)
     * @return {Camera} A reference to the instance
     */
    setFieldOfView(value) {
        this.fov = value;
        this.setType(this.type); // Force projection matrix update

        return this;
    }

    /**
     * Set screen's ratio
     *
     * @param {number} ratio Ratio to assign (4/3, 16/9, …)
     * @return {Camera} A reference to the instance
     */
    setRatio(ratio) {
        this.ratio = ratio;
        this.setType(this.type); // Force projection matrix update

        return this;
    }

    /**
     * Set camera's distances
     *
     * @param {Camera.Type} type Type asked, for 2D you should use "Orthographic"
     * @return {Camera} A reference to the instance
     */
    setType(type) {
        // Save type
        this.type = type;

        // Compute projection matrix
        if (type === Camera.Type.Perspective) {
            mat4.perspective(this.matrixProjection, glMatrix.toRadian(this.fov * this.zoomScale), this.ratio, this.limits[0], this.limits[1]);
        } else {
            mat4.ortho(this.matrixProjection,
                (-1.5 * this.ratio) * this.zoomScale,
                (+1.5 * this.ratio) * this.zoomScale,
                (-1.5 * this.zoomScale),
                (+1.5 * this.zoomScale),
                this.limits[0],
                this.limits[1]);
        }

        this.matrixViewProjectionNeedUpdate = true;

        return this;
    }

    /**
     * Set camera's distances
     *
     * @param {number} min Minimum distance to show
     * @param {number} max Maximum distance to show
     * @return {Camera} A reference to the instance
     */
    setViewDistances(min, max) {
        vec2.set(this.limits, min, max);
        this.setType(this.type); // Force projection matrix update

        return this;
    }

    /**
     * Set camera's viewport
     *
     * @param {number} x View start position on X
     * @param {number} y View start position on Y
     * @param {number} w View size on X
     * @param {number} h View size on Y
     * @return {Camera} A reference to the instance
     */
    setViewport(x, y, w, h) {
        vec4.set(this.viewport, x, y, w, h);
        this.setRatio(w / h);

        return this;
    }

    /**
     * Zoom
     *
     * @param {number} zoomValue Zoom scale to apply
     * @return {Camera} A reference to the instance
     */
    zoom(zoomValue) {
        this.zoomScale = 1.0 / zoomValue;
        this.setType(this.type); // Force projection matrix update

        return this;
    }

    /**
     * Get camera's position
     *
     * @return {!Array.<number>} A vector with three values: x, y and z
     */
    getPosition() {
        return [this.position[0], this.position[1], this.position[2]];
    }

    /**
     * Get camera's projection matrix
     *
     * @return {!glMatrix.mat4} A matrix
     */
    getProjectionMatrix() {
        return this.matrixProjection;
    }

    /**
     * Get camera's matrix
     *
     * @return {!glMatrix.mat4} A matrix
     */
    getViewMatrix() {
        if (this.matrixViewNeedUpdate) {
            mat4.lookAt(this.matrixView, this.position, this.direction, vec3.fromValues(0.0, 1.0, 0.0));
            this.matrixViewNeedUpdate = false;
            this.matrixViewProjectionNeedUpdate = true;
        }

        return this.matrixView;
    }

    /**
     * Get camera's viewport
     *
     * @return {!glMatrix.vec3} A vector with four values: x, y, w and h
     */
    getViewport() {
        return this.viewport;
    }

    /**
     * Get camera's matrix
     *
     * @return {!glMatrix.mat4} A matrix
     */
    getViewProjectionMatrix() {
        if (this.matrixViewProjectionNeedUpdate || this.matrixViewNeedUpdate) {
            mat4.multiply(this.matrixViewProjection, this.getProjectionMatrix(), this.getViewMatrix());
            this.matrixViewProjectionNeedUpdate = false;
        }

        return this.matrixViewProjection;
    }

    /**
     * Convert a point in 2D space to the 3D space
     *
     * Z value must have one of this two values:
     * - 0 for near plane
     * - 1 for far plane
     *
     * @param {Array.<number>} position Position in 2D space/a vec3
     * @return {!glMatrix.vec3} An array with position in 3D
     */
    screenToWorldPoint(position) {
        return unproject([position[0], position[1], position[2]],
            this.getViewMatrix(),
            this.getProjectionMatrix(),
            this.viewport);
    }

    /**
     * Convert a point in 3D space to the 2D space
     *
     * @param {Array.<number>} position Position in 3D space/a vec3
     * @return {!glMatrix.vec2} An array with position in 2D
     */
    worldToScreenPoint(position) {
        return project([position[0], position[1], position[2]],
            this.getViewMatrix(),
            this.getProjectionMatrix(),
            this.viewport);
    }
}

/**
 * Types
 *
 * @type {{Perspective: number, Orthographic: number}}
 */
Camera.Type = { Perspective: 0, Orthographic: 1 };

export default Camera;