camera.js

import { Transform } from './transform.js';
import { Mat4 } from './mat4.js';
import { Framebuffer2D } from './framebuffer2D.js';

/**
 * Object containing both view and projection matrix. This makes moving the view around more intuituve.
 * @extends {Transform}
 */
export class Camera extends Transform{
    constructor(){
        super();
        this.isCamera = true;
        /**
         * Render target for this camera. if null then gl context canvas is used as target.
         * @type {Framebuffer2D}
         */
        this._target = null;
        /** width is set by target or canvas size automatically @readonly */
        this._width = 100;
        /** height is set by target or canvas size automatically @readonly */
        this._height = 100;
        this.clearColor = [0,0,0,1];
        /**
         * Field of view in degrees
         */
        this._fov = 90;
        this._near = 0.1;
        this._far = 1000.0;
        this._left = -100;
        this._right = 100;
        this._top = -100;
        this._bottom = 100;
        this._perspective = true;
        this.projectionMatrix = new Mat4();
        this.viewProjectionMatrix = new Mat4();
        this.inverseViewProjectionMatrix = new Mat4();
        /**
         * If true, the projection matrix is updated the next time this camera is set active.<br>
         * This is set true if any of the projection realted parameters have been changed.
         * @name Camera#projectionNeedsUpdate
         * @type {Boolean}
         * @default true
         */
        this.projectionNeedsUpdate = true;
    }
    get target(){return this._target;}
    set target(value){ this._target = value;}
    get width(){return this._width;}
    set width(value){
        if(value != this._width){
            this._width = value;
            this.projectionNeedsUpdate = true;
        }
    }
    get height(){return this._height;}
    set height(value){
        if(value != this._height){
            this._height = value;
            this.projectionNeedsUpdate = true;
        }
    }
    get fov(){ return this._fov; }
    set fov(value){ this._fov = value; this.projectionNeedsUpdate = true; }
    get near(){ return this._near; }
    set near(value){ this._near = value; this.projectionNeedsUpdate = true; }
    get far(){ return this._far; }
    set far(value){ this._far = value; this.projectionNeedsUpdate = true; }
    get left(){ return this._left; }
    set left(value){ this._left = value; this.projectionNeedsUpdate = true; }
    get right(){ return this._right; }
    set right(value){ this._right = value; this.projectionNeedsUpdate = true; }
    get top(){ return this._top; }
    set top(value){ this._top = value; this.projectionNeedsUpdate = true; }
    get bottom(){ return this._bottom; }
    set bottom(value){ this._bottom = value; this.projectionNeedsUpdate = true; }
    get perspective(){ return this._perspective; }
    set perspective(value){ this._perspective = value; this.projectionNeedsUpdate = true; }
    
    /**
     * Updates the viewProjection combined matrix. this is done when camera transformation matrix is updated and if the projection matrix is updated.
     */
    updateViewProjectionMatrix(){
        this.viewProjectionMatrix.copy(this.worldToLocal);
        this.viewProjectionMatrix.multiply(this.projectionMatrix);
        this.inverseViewProjectionMatrix.copy(this.viewProjectionMatrix);
        this.inverseViewProjectionMatrix.invert();
    }

    /**
     * Updates the projection matrix. This is done automatically if {@link Camera#projectionNeedsUpdate} is true when {@link Camera#setActive} is called.
     */
    updateProjectionMatrix(){
        if(this.perspective){
            this.projectionMatrix.perspective( this.fov * 0.0174532925, this.width/this.height, this.near, this.far );
        }else{
            this.projectionMatrix.orthogonal( this.left, this.right, this.bottom, this.top, this.near, this.far);
        }
        this.updateViewProjectionMatrix();
        this.projectionNeedsUpdate = false;
    }

    updateMatrix(){
        super.updateMatrix();
        this.updateViewProjectionMatrix();
    }

    /**
     * Call this before drawing to set up the gl viewport and update projection matrix if needed.
     * @param {WebGLRenderingContext} gl 
     */
    setActive(gl){
        if(this._target == null){
            this.width = gl.canvas.width;
            this.height = gl.canvas.height;
        }else{
            this.width = this.target.width;
            this.height = this.target.height;
            this._target.setActive(gl);
        }
        if(this.projectionNeedsUpdate){
            this.updateProjectionMatrix();
        }
        gl.clearColor( ...this.clearColor );
        gl.viewport(0,0,this.width, this.height);
        gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    }

    /**
     * Transforms the vector from NDC to world space.
     * @param {Vec3} point Screen point in NDC(normalized device coordinates).
     */
    NDCToWorld(point){
        point.transformMat4( this.localToWorld );
    }

    /**
     * Transforms the vector from screen pixel coordinates to world space.
     * @param {Vec3} point Screen point in pixel coordinates.
     */
    screenToWorld(point){
        // point to NDC
        // z is unchanged
        point.data[0] /= this._width;
        point.data[0] = point.data[0] * 2 - 1;
        
        point.data[1] /= this._height;
        point.data[1] = point.data[1] * 2 - 1;
        point.data[1]*= -1;

        // apply aspect ratio
        point.data[0] *= this._width / this._height;

        this.NDCToWorld(point);
    }
}