Repository

js/Mobilizing/renderer/3D/three/scene/Camera.js

import * as THREE from 'three';

import Transform from './Transform';
import Vector3 from '../types/Vector3';
import Rect from '../types/Rect';
import * as _Math from '../../../../core/util/Math';
import * as debug from '../../../../core/util/Debug';

/**
* Camera class. Must be created by the user so that the current scene can be seen.
* If no camera is added to the scene, rendering is not done (nothing is seen, so nothing is rendered).
*
* A Camera contains a transform object to be moved and rotated in space.
* Viewport can be defined by the user to create multicam rendering.
*/
export default class Camera {
    /**
    * @example
    * //this is how to use a parameters object in order to instanciate a Mobilizing.js object
    *     var mobilizingObject = new Mobilizing.Class({paramName1: value, paramName2: value});
    *
    * @param {Object} params Parameters object, given by the constructor.
    * @param {Context} [params.context] mobilizing context to use
    * @param {String} [params.type="perspective"] One of "perspective", "ortho" or "cube"
    * @param {Number} [params.fov] vertical field of view
    * @param {Number} [params.cubeResolution=1024] if the type is "cube", defines the size of the cubemap, must be a power of 2
    * @param {Number} [params.near=1] near plane
    * @param {Number} [params.far=5000] far plane
    * @param {Rect} [params.viewport=new Rect()] the Rect defining the viewport in normalize % (0 ~ 1), that is the portion of the rendering canvas to be used as a rendering surface for this camera
    * @param {Context} [params.layer] the layer in which to render the camera, that is the scene it is
    * @param {Vector3} [params.position=Vector(0,0,0)] the position where to create the camera in space
    * @param {Number} [params.verticalshift=0] vertical lens shift in %
    * @param {Number} [params.horizontalshift=0] horizontal lens shift in %
    * @param {Boolean} [params.autoRender=true] Should the camera automatically renders itself or not
    * @param {Boolean} [params.autoClear=true] Should the camera automatically clears itself or not
    * @param {Boolean} [params.autoUpdateMatrix=true] Should the camera transformation matrix automatically updates itself or not
    */
    constructor({
        context = null,
        type = "perspective",
        fov = 35,
        cubeResolution = 1024,
        near = 0.1,
        far = 5000,
        viewport = new Rect(),
        layers = ["default"],
        verticalshift = 0,
        horizontalshift = 0,
        autoRender = true,
        autoClear = true,
        autoUpdateMatrix = false,
    } = {}) {
        this.context = context;
        this.type = type;
        this.fov = fov;
        this.cubeResolution = cubeResolution;
        this.near = near;
        this.far = far;
        this.viewport = viewport;
        this.layers = layers;
        this.verticalshift = verticalshift;
        this.horizontalshift = horizontalshift;
        this.autoRender = autoRender;
        this.autoClear = autoClear;
        this.autoUpdateMatrix = autoUpdateMatrix;

        this.autoClearColor = true;
        this.autoClearDepth = true;
        this.autoClearStencil = true;

        if (this.type === "perspective") {
            let size;

            if (this.context) {
                size = this.context.getCanvasSize();
            }
            else {
                size = { "width": window.innerWidth, "height": window.innerHeight };
            }

            this._camera = new THREE.PerspectiveCamera(this.fov, (size.width) / (size.height), this.near, this.far);

            debug.log("made perspective cam");
        }
        else if (this.type === "ortho") {
            let size;

            if (this.context) {
                size = this.context.getCanvasSize();
            }
            else {
                size = { "width": window.innerWidth, "height": window.innerHeight };
            }

            this._camera = new THREE.OrthographicCamera(size.width / -2, size.width / 2, size.height / 2, size.height / -2, this.near, this.far);
        }
        else if (this.type === "cube") {
            this._camera = new THREE.CubeCamera(0.5, 1000, this.cubeResolution);
        }

        this.transform = new Transform(this);

        /**
        @type Boolean flag to know if something has changed
        @default false
        */
        this.dirty = false;

        /**
         * flag to know if we used setToPixel method to auto adjust camera pos to mimic 2D graphical spaces, that is with (0,0) in the top-left corner.
         */
        this.isToPixel = false;
    }

    /**
    * @returns the Three.js native object used in this class
    */
    getNativeObject() {
        return this._camera;
    }

    /**
    Set the RenderTexture to render on. By default, the camera renders on the Context canvas.
    @param {RenderTexture} renderTexture Mobilizing.RenderTexture object
    */
    setRenderTexture(renderTexture) {
        this.renderTexture = renderTexture;
    }

    /**
     * Set this camera layer's name, which should match a renderer's scene name
     * @param {String} name
     */
    setLayer(name) {
        this.layers = [name];
    }

    /**
     * @param {String} name
     */
    addLayer(name) {
        this.layers.push(name);
    }

    /**
    * set the vertical field of view in degrees
    * @param fov {Number} default to 35 degree
    */
    setFOV(fov) {
        this.fov = fov;

        if (this.type === "perspective") {
            this._camera.fov = this.fov;
            this.updateProjectionMatrix();
        }
    }

    /**
    * get the vertical field of view in degrees
    * @return {Number} Field Of View value
    */
    getFOV() {
        return this.fov;
    }

    /**
    * Change the autoClear property of this camera. Needed to had a "trail effect"
    * @param {Boolean} val
    */
    setAutoClear(val) {
        this.autoClear = val;
    }

    /**
    * Change the autoClearColor property of this camera. Needed to had a "trail effect"
    * @param {Boolean} val
    */
    setAutoClearColor(val) {
        this.autoClearColor = val;
    }

    /**
    * Change the setAutoClearDepth property of this camera.
    * @param {Boolean} val
    */
    setAutoClearDepth(val) {
        this.autoClearDepth = val;
    }

    /**
    * Change the setAutoClearStencil property of this camera.
    * @param {Boolean} val
    */
    setAutoClearStencil(val) {
        this.autoClearStencil = val;
    }

    /**
    * Set the clear color, which is the color used to paint the backgroud.
    * Note: this is camera independant, each cam on the scene can have a different
    * clear color, or a different background color.
    *
    * @param {Color} Color Mobilizing.Color object
    * @param {Number} Alpha A number >= 0 && <= 1
    * */
    setClearColor(color, alpha) {
        this.clearColor = color;
        this.clearColorAplha = alpha;
    }

    /**
    Gets the clear color, which is the color used to paint the backgroud.
    Note: this is camera independant, each cam on the scene can have a different
    clear color, or a different background color.
    @return {Color} Color object associated to this clearColor
    */
    getClearColor() {
        return this.clearColor;
    }

    getClearAlpha() {
        return this.clearColorAplha;
    }

    /**
    Set the autoRender flag, which means that the camera will render itself automatically.
    @param {Bool} val flag true/false
    */
    setAutoRender(val) {
        this.autoRender = val;
    }

    /**
    Makes the cam "looks at" the argements coordinates. Handy way to orient the cam
    or to make it follow an object in space.
    @param {Object} Vector3 the coordinates to look at.
    */
    lookAt(vec) {
        this._camera.lookAt(vec);
    }

    /**
    * @method getWolrdDirection
    * return {Vector3} the world direction vector of this camera
    */
    getWolrdDirection() {
        const result = new Vector3();
        this._camera.getWorldDirection(result);
        return result;
    }

    /**
    Sets the aspect ratio of the camera view
    @param {Number} the ratio (ex. 4/3)
    */
    setAspect(ratio) {
        this._camera.aspect = ratio;
        this.updateProjectionMatrix();
    }

    /**
    gets the aspect ratio of the camera view
    @return {Number} cam aspect value
    */
    getAspect() {
        return this._camera.aspect;
    }

    /**
    Sets the vertical shift ratio of the camera view
    @param {Number} the ratio (1 -> 100%)
    */
    setVerticalShift(ratio) {
        this.verticalshift = ratio;
        this.updateProjectionMatrix();
    }

    /**
    Sets the horizontal shift ratio of the camera view
    @param {Number} the ratio (1 -> 100%)
    */
    setHorizontalShift(ratio) {
        this.horizontalshift = ratio;
        this.updateProjectionMatrix();
    }

    /**
    * recompute the projection matrix of this camera to reflect properly properties changes
    * @private
    */
    updateProjectionMatrix() {
        //FIXME : this call does an updateProjectionMatrix internally :
        const w = 1;
        const h = 1 / this._camera.aspect;

        if (this.type !== "cube") {
            if (this.type !== "ortho") {
                this._camera.setViewOffset(w, h, w * this.horizontalshift, h * this.verticalshift, w, h);
            }

            if (this.autoUpdateMatrix) {
                this._camera.updateProjectionMatrix();
            }
        }

    }

    /**
    Method to recompute the frame of ortho cam. Is used internally for window resizing.
    @param {Number} left
    @param {Number} right
    @param {Number} top
    @param {Number} bottom
    */
    setOrthoPlanes(left, right, top, bottom) {
        if (this.type === "ortho") {
            this._camera.left = left;
            this._camera.right = right;
            this._camera.top = top;
            this._camera.bottom = bottom;
        }
        else {
            console.warn("setOrthoBounds() can't be used on perspective cams!");
        }
    }

    /**
    Zoom is for ortho cam and mimics the Z translation of perspective cams.
    This is expressed like a scale, zoom = 2 will double, .5 make it half.
    @param {Number} zoom value
    */
    setZoom(val) {
        if (this.type === "ortho") {
            this._camera.zoom = val;
        }
        else {
            console.warn("setZoom() can't be used on perspective cams!");
        }
    }

    /**
    Gets current zoom value.
    @return {Number} zoom value
    */
    getZoom() {
        if (this.type === "ortho") {
            return this._camera.zoom;
        }

        console.warn("getZoom() can't be used on perspective cams!");
        return null;
    }

    /**
    Sets cam far plane
    @param {Number} far plane value
    */
    setFarPlane(far) {
        this.far = far;

        if (this.type !== "cube") {
            this._camera.far = this.far;
            this.updateProjectionMatrix();
        }
        else if (this.type === "cube") {
            this._camera.children.forEach((cam) => {
                cam.far = this.far;
                cam.updateProjectionMatrix();
            });
        }
    }

    /**
    Gets cam far plane
    @return {Number} far plane value
    */
    getFarPlane() {
        this.far = this._camera.far;
        return this.far;
    }

    /**
    Sets cam near plane
    @param {Number} near plane value
    */
    setNearPlane(near) {
        this.near = near;

        if (this.type !== "cube") {
            this._camera.near = this.near;
            this.updateProjectionMatrix();
        }
        else if (this.type === "cube") {
            this._camera.children.forEach((cam) => {
                cam.near = this.near;
                cam.updateProjectionMatrix();
            });
        }
    }

    /**
    Gets cam near plane
    @return {Number} far plane value
    */
    getNearPlane() {
        this.near = this._camera.near;
        return this.near;
    }

    /**
    Sets cam far and near planes
    @param {Number} near near plane value
    @param {Number} far far plane value
    */
    setPlanes(near, far) {
        this.near = near;
        this.far = far;

        this._camera.far = this.far;
        this._camera.near = this.near;

        this.updateProjectionMatrix();
    }

    /**
    Tries to adjust the cam z distance so that 1 world unit == 1 screen pixel.
    Useful to make object move at the mouse or touch position x and y.
    For perspective cam only.
    */
    setToPixel() {
        if (this.type === "perspective") {
            let newPos;

            if (this.context) {
                const canvasSize = this.context.getCanvasSize();
                newPos = new Vector3(canvasSize.width / 2, -canvasSize.height / 2, 1 / (2 * Math.tan((_Math.degToRad(this.fov / 2.0)) / canvasSize.height)));
            }
            else {
                newPos = new Vector3(window.innerWidth / 2, -window.innerHeight / 2, 1 / (2 * Math.tan((_Math.degToRad(this.fov / 2.0)) / window.innerHeight)));
            }

            this.transform.setLocalPosition(newPos);
            this.isToPixel = true;
        }
        else {
            debug.error("only perspective camera can be setted to screen pixel z position");
        }
    }

    /**
    * Compute a perspective with offsets matrix
    * @param {Number} left
    * @param {Number} right
    * @param {Number} bottom
    * @param {Number} top
    * @param {Number} near
    * @param {Number} far
    */
    perspectiveOffCenter(left, right, bottom, top, near, far) {
        const x = 2.0 * near / (right - left);
        const y = 2.0 * near / (top - bottom);
        const a = (right + left) / (right - left);
        const b = (top + bottom) / (top - bottom);
        const c = -(far + near) / (far - near);
        const d = -(2.0 * far * near) / (far - near);
        const e = -1.0;

        const m = this._camera.projectionMatrix;
        m.set(x, 0, a, 0, 0, y, b, 0, 0, 0, c, d, 0, 0, e, 0);
    }
}