Home Reference Source

viewer/camera.js

import * as mat4 from "./glmatrix/mat4.js";
import * as mat3 from "./glmatrix/mat3.js";
import * as vec3 from "./glmatrix/vec3.js";
import * as vec4 from "./glmatrix/vec4.js";

import {AnimatedVec3} from "./animatedvec3.js";
import {Perspective} from "./perspective.js";
import {Orthographic} from "./orthographic.js";

/**
 A **Camera** defines viewing and projection transforms for its Viewer.
 */
export class Camera {

    constructor(viewer) {
        this.viewer = viewer;

        this.perspective = new Perspective(viewer);

        this.orthographic = new Orthographic(viewer);

        this._projection = this.perspective; // Currently active projection
        this._viewMatrix = mat4.create();
        this._viewProjMatrix = mat4.create();
        this._viewMatrixInverted = mat4.create();
        this._viewProjMatrixInverted = mat4.create();

        this._viewNormalMatrix = mat3.create();

        this._eye = new AnimatedVec3(0.0, 0.0, -10.0); // World-space eye position
        this._target = new AnimatedVec3(0.0, 0.0, 0.0); // World-space point-of-interest        
        
        this._up = vec3.fromValues(0.0, 1.0, 0.0); // Camera's "up" vector, always orthogonal to eye->target
        this._center = vec3.copy(vec3.create(), this._target.get());
        this._negatedCenter = vec3.create();
        vec3.negate(this._negatedCenter, this._center);

        this._worldAxis = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
        this._worldUp = vec3.fromValues(0.0, 1.0, 0.0); // Direction of "up" in World-space
        this._worldRight = vec3.fromValues(1, 0, 0); // Direction of "right" in World-space
        this._worldForward = vec3.fromValues(0, 0, -1); // Direction of "forward" in World-space

        this._gimbalLock = true; // When true, orbiting world-space "up", else orbiting camera's local "up"
        this._constrainPitch = true; // When true, will prevent camera} from being rotated upside-down

        this._dirty = true; // Lazy-builds view matrix
        this._locked = false;

        this._modelBounds = null;

        this.tempMat4 = mat4.create();
        this.tempMat3b = mat3.create();
        this.tempVec3 = vec3.create();
        this.tempVec3b = vec3.create();
        this.tempVec3c = vec3.create();
        this.tempVec3d = vec3.create();
        this.tempVec3e = vec3.create();
        this.tempVecBuild = vec3.create();

        this.tmp_modelBounds = vec3.create();

        this.yawMatrix = mat4.create();
        
        // Until there is a proper event handler mechanism, just do it manually.
        this.listeners = [];
        this.lowVolumeListeners = [];

        this._orbitting = false;

        this._tmp_interpolate_current_dir = vec3.create();
	    this._tmp_interpolate_new_dir = vec3.create();
	    this._tmp_interpolate_a = vec3.create();
	    this._tmp_interpolate_b = vec3.create();
	    this._tmp_interpolate_c = vec3.create();
	    this._tmp_interpolate_d = vec4.create();
	    this._tmp_interpolate_e = mat4.create();
        this._tmp_interpolate_f = vec3.create();
        
        this._tmp_eye = vec3.create();
        this._tmp_target = vec3.create();
    }

    lock() {
        this._locked = true;
    }

    unlock() {
        this._locked = false;
        this._build();
    }

    _setDirty() {
        this._dirty = true;
        this.viewer.dirty = 2;
    }

    setModelBounds(bounds) {
        this._modelBounds = [];

        this.perspective.setModelBounds(vec3.clone(bounds));
        this.orthographic.setModelBounds(vec3.clone(bounds));
        
        // Store aabb calculated from points
        let a = vec3.fromValues(+Infinity, +Infinity, +Infinity);
        let b = vec3.fromValues(-Infinity, -Infinity, -Infinity);

        let zero_one = [0,1];

        for (let i of zero_one) {
            for (let j of zero_one) {
                for (let k of zero_one) {
                    let v = vec3.fromValues(bounds[3*i+0], bounds[3*j+1], bounds[3*k+2]);
                    this._modelBounds.push(v);

                    for (let l = 0; l < 3; ++l) {
                        if (v[l] < a[l]) {
                            a[l] = v[l];
                        }
                        if (v[l] > b[l]) {
                            b[l] = v[l];
                        }
                    }
                }
            }   
        }

        vec3.add(a, a, b);
        vec3.scale(a, a, 0.5);

        this._center.set(a);
        vec3.negate(this._negatedCenter, this._center);
        this._dirty = true;
    }

    forceBuild() {
        let eye = this._eye.get();
        let target = this._target.get();

   		vec3.set(this._up, 0, 0, 1);
        vec3.subtract(this.tempVecBuild, target, eye);
        vec3.normalize(this.tempVecBuild, this.tempVecBuild);
        vec3.cross(this._up, this.tempVecBuild, this._up);
        vec3.cross(this._up, this._up, this.tempVecBuild);
        if (vec3.equals(this._up, vec3.fromValues(0, 0, 0))) {
        	// Not good, choose something
        	vec3.set(this._up, 0, 1, 0);
        }

        mat4.lookAt(this._viewMatrix, eye, target, this._up);
        mat3.fromMat4(this.tempMat3b, this._viewMatrix);
        mat3.invert(this.tempMat3b, this.tempMat3b);
        mat3.transpose(this._viewNormalMatrix, this.tempMat3b);
        
        let [near, far] = [+Infinity, -Infinity];

        if (!this.viewer.geospatialMode && this._modelBounds) {
        	for (var v of this._modelBounds) {
                vec3.transformMat4(this.tmp_modelBounds, v, this._viewMatrix);
                let z = -this.tmp_modelBounds[2];
                if (z < near) {
                    near = z;
                }
                if (z > far) {
                    far = z;
                }
            }

            if (near < 1.e-3) {
                near = far / 1000.;
            }
        } else {
            [near, far] = [+1000, +200000. + vec3.length(this.eye)];
        }

        this.perspective.near = near - 1e-2;
        this.perspective.far = far + 1e-2;
        this.orthographic.near = near - 1e-2;
        this.orthographic.far = far + 1e-2;        

        mat4.invert(this._viewMatrixInverted, this._viewMatrix);
        mat4.multiply(this._viewProjMatrix, this.projMatrix, this._viewMatrix);
        mat4.invert(this._viewProjMatrixInverted, this._viewProjMatrix);

        this._dirty = false;
        
        for (var listener of this.listeners) {
        	listener();
        }
    }
    
    _build() {
        if (this._dirty && !this._locked && this._modelBounds) {
        	this.forceBuild();
        }
    }

    /**
     Gets the current viewing transform matrix.

     @return {Float32Array} 4x4 column-order matrix as an array of 16 contiguous floats.
     */
    get viewMatrix() {
        if (this._dirty) {
            this._build();
        }
        return this._viewMatrix;
    }

    /**
     Gets the current view projection matrix.

     @return {Float32Array} 4x4 column-order matrix as an array of 16 contiguous floats.
     */
    get viewProjMatrix() {
        if (this._dirty) {
            this._build();
        }
        return this._viewProjMatrix;
    }

    /**
     Gets the current inverted view projection matrix.

     @return {Float32Array} 4x4 column-order matrix as an array of 16 contiguous floats.
     */
    get viewProjMatrixInverted() {
        if (this._dirty) {
            this._build();
        }
        return this._viewProjMatrixInverted;
    }

    get viewMatrixInverted() {
        if (this._dirty) {
            this._build();
        }
        return this._viewMatrixInverted;
    }

    /**
     Gets the current viewing transform matrix for normals.

     This is the transposed inverse of the view matrix.

     @return {Float32Array} 4x4 column-order matrix as an array of 16 contiguous floats.
     */
    get viewNormalMatrix() {
        if (this._dirty) {
            this._build();
        }
        return this._viewNormalMatrix;
    }

    /**
     Gets the current projection transform matrix.

     @return {Float32Array} 4x4 column-order matrix as an array of 16 contiguous floats.
     */
    get projMatrix() {
        return this._projection.projMatrix;
    }

    /**
     Selects the current projection type.

     @param {String} projectionType Accepted values are "persp" or "ortho".
     */
    set projectionType(projectionType) {
        if (projectionType.toLowerCase().startsWith("persp")) {
            this._projection = this.perspective;
        } else if (projectionType.toLowerCase().startsWith("ortho")) {
            this._projection = this.orthographic;
        } else {
            console.error("Unsupported projectionType: " + projectionType);
        }
        this.viewer.dirty = 2;
    }

    /**
     Gets the current projection type.

     @return {String} projectionType "persp" or "ortho".
     */
    get projectionType() {
        return this._projection.constructor.name.substr(0,5).toLowerCase();
    }

    /**
     Gets the component that represents the current projection type.

     @return {Perspective|Orthographic}
     */
    get projection() {
        return this._projection;
    }

    /**
     Sets the position of the camera.
     @param {Float32Array} eye 3D position of the camera in World space.
     */
    set eye(eye) {
    	if (!vec3.equals(this._eye.get(), eye)) {
    		this._eye.get().set(eye);
    		this._setDirty();
    		for (var listener of this.lowVolumeListeners) {
    			listener();
    		}
    	}
    }

    /**
     Gets the position of the camera.
     @return {Float32Array} 3D position of the camera in World space.
     */
    get eye() {
        return this._eye.get();
    }

    /**
     Sets the point the camera is looking at.
     @param {Float32Array} target 3D position of the point of interest in World space.
     */
    set target(target) {
        if (!vec3.equals(this._target.get(), target)) {
    		this._target.get().set(target);
    		this._setDirty();
    		for (var listener of this.lowVolumeListeners) {
    			listener();
    		}
    	}
    }

    /**
     Gets the point tha camera is looking at.
     @return {Float32Array} 3D position of the point of interest in World space.
     */
    get target() {
        return this._target.get();
    }

    set center(v) {
    	if (!vec3.equals(this._center, v)) {
    		this._center.set(v);
    		vec3.negate(this._negatedCenter, this._center);
    		this.listeners.forEach((fn) => { fn(); });
    	}
    }

    get center() {
        return this._center;
    }

    /**
     Sets the camera's "up" direction.
     @param {Float32Array} up 3D vector indicating the camera's "up" direction in World-space.
     */
    set up(up) {
        this._up.set(up || [0.0, 1.0, 0.0]);
        this._setDirty();
    }

    /**
     Gets the camera's "up" direction.
     @return {Float32Array} 3D vector indicating the camera's "up" direction in World-space.
     */
    get up() {
        return this._up;
    }

    /**
     Sets whether camera rotation is gimbal locked.

     When true, yaw rotation will always pivot about the World-space "up" axis.

     @param {Boolean} gimbalLock Whether or not to enable gimbal locking.
     */
    set gimbalLock(gimbalLock) {
        this._gimbalLock = gimbalLock;
    }

    /**
     Sets whether camera rotation is gimbal locked.

     When true, yaw rotation will always pivot about the World-space "up" axis.

     @return {Boolean} True if gimbal locking is enabled.
     */
    get gimbalLock() {
        return this._gimbalLock;
    }

    /**
     Sets whether its currently possible to pitch the camera to look at the model upside-down.

     When this is true, camera will ignore attempts to orbit (camera or model) about the horizontal axis
     that would result in the model being viewed upside-down.

     @param {Boolean} constrainPitch Whether or not to activate the constraint.
     */
    set constrainPitch(constrainPitch) {
        this._constrainPitch = constrainPitch;
    }

    /**
     Gets whether its currently possible to pitch the camera to look at the model upside-down.

     @return {Boolean}
     */
    get constrainPitch() {
        return this._constrainPitch;
    }

    /**
     Indicates the up, right and forward axis of the World coordinate system.

     This is used for deriving rotation axis for yaw orbiting, and for moving camera to axis-aligned positions.

     Has format: ````[rightX, rightY, rightZ, upX, upY, upZ, forwardX, forwardY, forwardZ]````

     @type {Float32Array}
     */
    set worldAxis(worldAxis) {
        this._worldAxis.set(worldAxis || [1, 0, 0, 0, 1, 0, 0, 0, 1]);
        this._worldRight[0] = this._worldAxis[0];
        this._worldRight[1] = this._worldAxis[1];
        this._worldRight[2] = this._worldAxis[2];
        this._worldUp[0] = this._worldAxis[3];
        this._worldUp[1] = this._worldAxis[4];
        this._worldUp[2] = this._worldAxis[5];
        this._worldForward[0] = this._worldAxis[6];
        this._worldForward[1] = this._worldAxis[7];
        this._worldForward[2] = this._worldAxis[8];
        this._setDirty();
    }

    /**
     Indicates the up, right and forward axis of the World coordinate system.

     This is used for deriving rotation axis for yaw orbiting, and for moving camera to axis-aligned positions.

     Has format: ````[rightX, rightY, rightZ, upX, upY, upZ, forwardX, forwardY, forwardZ]````

     @type {Float32Array}
     */
    get worldAxis() {
        return this._worldAxis;
    }

    /**
     Direction of World-space "up".

     @type Float32Array
     */
    get worldUp() {
        return this._worldUp;
    }

    /**
     Direction of World-space "right".

     @type Float32Array
     */
    get worldRight() {
        return this._worldRight;
    }

    /**
     Direction of World-space "forwards".

     @type Float32Array
     */
    get worldForward() {
        return this._worldForward;
    }

    set orbitting(orbitting) {
    	if (this._orbitting != orbitting) {
        	for (var listener of this.lowVolumeListeners) {
        		listener();
        	}
    	}
    	this._orbitting = orbitting;
    }

    get orbitting() {
    	return this._orbitting;
    }
    
    /**
     Rotates the eye position about the target position, pivoting around the up vector.

     @param {Number} degrees Angle of rotation in degrees
     */
    orbitYaw(degrees) {

        let eye = this._eye.get();
        let target = this._target.get();

        // @todo, these functions are not efficient nor numerically stable, but simple to understand.
        
    	mat4.identity(this.yawMatrix);
    	mat4.translate(this.yawMatrix, this.yawMatrix, this._center);
    	mat4.rotate(this.yawMatrix, this.yawMatrix, degrees * 0.0174532925 * 2, this._worldUp);
    	mat4.translate(this.yawMatrix, this.yawMatrix, this._negatedCenter);
    	
        vec3.transformMat4(eye, eye, this.yawMatrix);
        vec3.transformMat4(target, target, this.yawMatrix);

        this._setDirty();
        return;
    }

    /**
     Rotates the eye position about the target position, pivoting around the right axis (orthogonal to up vector and eye->target vector).

     @param {Number} degrees Angle of rotation in degrees
     */
    orbitPitch(degrees) { // Rotate (pitch) 'eye' and 'up' about 'target', pivoting around vector ortho to (target->eye) and camera 'up'
        let currentPitch = Math.acos(this._viewMatrix[10]);
        let adjustment = - degrees * 0.0174532925 * 2;
        if (currentPitch + adjustment < 0.01) {
            adjustment = 0.01 - currentPitch;
        }
        if (currentPitch + adjustment > Math.PI - 0.01) {
            adjustment = Math.PI - 0.01 - currentPitch;
        }

        if (Math.abs(adjustment) < 1.e-5) {
            return;
        }

        let eye = this._eye.get();
        let target = this._target.get();

        var T1 = mat4.fromTranslation(mat4.create(), this._center);
        var R = mat4.fromRotation(mat4.create(), adjustment, this._viewMatrixInverted);
        var T2 = mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), this._center));

        vec3.transformMat4(eye, eye, T2);
        vec3.transformMat4(eye, eye, R);
        vec3.transformMat4(eye, eye, T1);

        vec3.transformMat4(target, target, T2);
        vec3.transformMat4(target, target, R);
        vec3.transformMat4(target, target, T1);

        this._setDirty();
        return;
    }

    /**
     Rotates the target position about the eye, pivoting around the right axis (orthogonal to up vector and eye->target vector).

     @param {Number} degrees Angle of rotation in degrees
     */
    pitch(degrees) { // Rotate (pitch) 'eye' and 'up' about 'target', pivoting around horizontal vector ortho to (target->eye) and camera 'up'
        let eye = this._eye.get();
        let target = this._target.get();

        var eyeToTarget = vec3.subtract(this.tempVec3, target, eye);
        var a = vec3.normalize(this.tempVec3c, eyeToTarget);
        var b = vec3.normalize(this.tempVec3d, this._up);
        var axis = vec3.cross(this.tempVec3b, a, b); // Pivot vector is orthogonal to target->eye
        mat4.fromRotation(this.tempMat4, degrees * 0.0174532925, axis);
        vec3.transformMat4(eyeToTarget, eyeToTarget, this.tempMat4); // Rotate vector
        var newUp = vec3.transformMat4(this.tempVec3d, this._up, this.tempMat4); // Rotate 'up' vector
        if (this._constrainPitch) {
            var angle = vec3.dot(newUp, this._worldUp) / 0.0174532925; // Don't allow 'up' to go up[side-down with respect to World 'up'
            if (angle < 1) {
                return;
            }
        }
        this._up.set(newUp);
        vec3.add(target, eye, eyeToTarget); // Derive 'target'} from eye and vector
        this._setDirty();
    }

    /**
     Pans the camera along the camera's local X, Y and Z axis.

     @param {Array} pan The pan vector
     */
    pan(pan) { // Translate 'eye' and 'target' along local camera axis
        let eye = this._eye.get();
        let target = this._target.get();

        var eyeToTarget = vec3.subtract(this.tempVec3, eye, target);
        var vec = [0, 0, 0];
        if (pan[0] !== 0) {
            let a = vec3.normalize(this.tempVec3b, eyeToTarget); // Get  vector orthogonal to 'up' and eye->target
            let b = vec3.normalize(this.tempVec3c, this._up);
            let v = vec3.cross(this.tempVec3d, a, b);
            vec3.scale(v, v, pan[0]);
            vec[0] += v[0];
            vec[1] += v[1];
            vec[2] += v[2];
        }
        if (pan[1] !== 0) {
            let v = vec3.scale(this.tempVec3, vec3.normalize(this.tempVec3b, this._up), pan[1]);
            vec[0] += v[0];
            vec[1] += v[1];
            vec[2] += v[2];
        }
        if (pan[2] !== 0) {
            let v = vec3.scale(this.tempVec3, vec3.normalize(this.tempVec3b, eyeToTarget), pan[2]);
            vec[0] += v[0];
            vec[1] += v[1];
            vec[2] += v[2];
        }
        vec3.add(eye, eye, vec);
        this.target = vec3.add(target, target, vec);
        this._setDirty();
    }

    /**
     Moves the camera along a ray through unprojected mouse coordinates

     @param {Number} delta Zoom increment
     @param canvasPos Mouse position relative to canvas to determine ray along which to move
     */
    zoom(delta, canvasPos) { // Translate 'eye' by given increment on (eye->target) vector
        // @todo: also not efficient

        let eye = this._eye.get();
        let target = this._target.get();

    	this.orthographic.zoom(delta);
    	
        let [x,y] = canvasPos;
        vec3.set(this.tempVec3, x / this.viewer.width * 2 - 1, - y / this.viewer.height * 2 + 1, 1.);
        vec3.transformMat4(this.tempVec3, this.tempVec3, this.projection.projMatrixInverted);
        vec3.transformMat4(this.tempVec3, this.tempVec3, this.viewMatrixInverted);
        vec3.subtract(this.tempVec3, this.tempVec3, eye);
        vec3.normalize(this.tempVec3, this.tempVec3);
        vec3.scale(this.tempVec3, this.tempVec3, -delta);

        vec3.add(eye, eye, this.tempVec3);
        vec3.add(target, target, this.tempVec3);

        this._setDirty();

        this.updateLowVolumeListeners();
    }
    
    updateLowVolumeListeners() {
        for (var listener of this.lowVolumeListeners) {
        	listener();
        }
    }

    calcViewFit(aabb, fitFOV, eye, target) {
        aabb = aabb || this.viewer.modelBounds;
        fitFOV = fitFOV || this.perspective.fov;
        var eyeToTarget = vec3.normalize(this.tempVec3b, vec3.subtract(this.tempVec3, eye, target));
        var diagonal = Math.sqrt(
            Math.pow(aabb[3] - aabb[0], 2) +
            Math.pow(aabb[4] - aabb[1], 2) +
            Math.pow(aabb[5] - aabb[2], 2));
        var center = [
            (aabb[3] + aabb[0]) / 2,
            (aabb[4] + aabb[1]) / 2,
            (aabb[5] + aabb[2]) / 2
        ];
        target.set(center);
        var sca = Math.abs(diagonal / Math.tan(fitFOV * 0.0174532925));
        eye[0] = target[0] + (eyeToTarget[0] * sca);
        eye[1] = target[1] + (eyeToTarget[1] * sca);
        eye[2] = target[2] + (eyeToTarget[2] * sca);

        this._setDirty();
    }

    interpolateView(newEye, newTarget) {
        this._eye.deanimate();
        this._target.deanimate();

        vec3.sub(this._tmp_interpolate_current_dir, this._target.get(), this._eye.get());
        vec3.normalize(this._tmp_interpolate_current_dir, this._tmp_interpolate_current_dir);

        vec3.sub(this._tmp_interpolate_new_dir, newTarget, newEye);
        vec3.normalize(this._tmp_interpolate_new_dir, this._tmp_interpolate_new_dir);

        let d = vec3.dot(this._tmp_interpolate_current_dir, this._tmp_interpolate_new_dir);

        if (d > 0.5) {          
            // More or less pointing in the same direction

            this._eye.b.set(newEye);
            this._target.b.set(newTarget);
            this._eye.animate(1000);
            this._target.animate(1000);
        } else {
            // Add an additional intermediate interpolation point
            // Add a point on the bisectrice of d1 d2

            let an = Math.acos(d);
            let d1 = vec3.subtract(this._tmp_interpolate_a, newEye, newTarget);
            let d2 = vec3.subtract(this._tmp_interpolate_b, this.eye, newTarget);
            let l1 = vec3.len(d1);
            let l2 = vec3.len(d2);
            vec3.normalize(d1, d1);
            vec3.normalize(d2, d2);

            let d3;
            if (d < -0.99) {
                // parallel view vecs, choose arbitrary axis
                let temp;
                if (Math.abs(d1[1]) > 0.99) {
                    temp = vec3.fromValues(1,0,0);
                } else {
                    temp = vec3.fromValues(0,1,0);
                }
                d3 = vec3.cross(this._tmp_interpolate_c, d1, temp);
            } else {
                d3 = vec3.cross(this._tmp_interpolate_c, d1, d2);
            }
            vec3.normalize(d3, d3);
            let rot = mat4.fromRotation(this._tmp_interpolate_e, an / 2., d3);
            let intermediate = this._tmp_interpolate_d;
            vec3.copy(intermediate, d1);
            vec4.transformMat4(intermediate, intermediate, rot);
            vec3.normalize(intermediate, intermediate);
            vec3.scale(intermediate, intermediate, (l1 + l2) / 2.);
            vec3.add(intermediate, intermediate, newTarget);
            let intermediate3 = intermediate.subarray(0,3);
            
            this._eye.b.set(intermediate3);
            this._target.b.set(vec3.lerp(this._tmp_interpolate_f, this.target, newTarget, 0.5));
            this._eye.c.set(newEye);
            this._target.c.set(newTarget);
            this._eye.animate(500, 500);
            this._target.animate(500, 500);
        }
    }

    /**
     Jumps the camera to look at the given axis-aligned World-space bounding box.

     @param {Float32Array} aabb The axis-aligned World-space bounding box (AABB).
     @param {Number} fitFOV Field-of-view occupied by the AABB when the camera has fitted it to view.
     */
    viewFit(params) {
        let eye, target, eye2, target2;
        
        if (params.animate) {
            eye = this._eye.get();
            target = this._target.get();

            eye2 = this._tmp_eye;
            target2 = this._tmp_target;
        } else {
            eye2 = eye = this._eye.get();
            target2 = target = this._target.get();
        }

		if (params.viewDirection) {
			eye = params.viewDirection;
		}

        const aabb = params.aabb || this.viewer.modelBounds;
        const fitFOV = this.perspective.fov;
        var eyeToTarget = vec3.normalize(this.tempVec3b, vec3.subtract(this.tempVec3, eye, target));
        var diagonal = Math.sqrt(
            Math.pow(aabb[3] - aabb[0], 2) +
            Math.pow(aabb[4] - aabb[1], 2) +
            Math.pow(aabb[5] - aabb[2], 2));
        var center = [
            (aabb[3] + aabb[0]) / 2,
            (aabb[4] + aabb[1]) / 2,
            (aabb[5] + aabb[2]) / 2
        ];
        target2.set(center);
        var sca = Math.abs(diagonal / Math.tan(fitFOV * 0.0174532925));
        eye2[0] = target2[0] + (eyeToTarget[0] * sca);
        eye2[1] = target2[1] + (eyeToTarget[1] * sca);
        eye2[2] = target2[2] + (eyeToTarget[2] * sca);

        if (params.animate) {
            this.interpolateView(this._tmp_eye, this._tmp_target);
        }

        this._setDirty();
    }

    restore(params) {
        if (params.type) {
            this.projectionType = params.type;
        }
        if (this._projection instanceof Perspective && params.fovy) {
            this._projection.fov = params.fovy;
        }
        ["eye", "target", "up"].forEach((k) => {
            if (params[k]) {
                let fn_set = Object.getOwnPropertyDescriptor(this, k).set;
                let fn_get = Object.getOwnPropertyDescriptor(this, k).get;
                let fn_update;
                if (params[k] instanceof AnimatedVec3) {
                    fn_update = (t, v) => { fn_get(t).set(v); };
                } else {
                    fn_update = fn_set;
                }
                fn_update(this, params[k]);
            }
        });
    }
}