camera_Frustum.ts

import { Box } from "../bv/Box";
import { Mat4, type NumberArray16 } from "../math/Mat4";
import type { NumberArray4 } from "../math/Vec4";
import { Sphere } from "../bv/Sphere";
import { Vec3 } from "../math/Vec3";
import { RADIANS_HALF } from "../math";

function planeNormalize(plane: NumberArray4) {
    let t = 1.0 / Math.sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
    plane[0] *= t;
    plane[1] *= t;
    plane[2] *= t;
    plane[3] *= t;
}

interface IFrustumParams {
    cameraFrustumIndex?: number;
    fov?: number;
    aspect?: number;
    near?: number;
    far?: number;
    reverseDepth?: boolean;
    depthZeroToOne?: boolean;
}

/**
 * Frustum object, part of the camera object.
 * @class
 * @param {*} options
 */
class Frustum {
    protected _f: [NumberArray4, NumberArray4, NumberArray4, NumberArray4, NumberArray4, NumberArray4];
    protected _isOrthographic: boolean;
    protected _aspect: number;
    protected _tanViewAngle_hrad: number;
    protected _reverseDepth: boolean;
    protected _depthZeroToOne: boolean;

    /**
     * Camera projection matrix.
     * @protected
     * @type {Mat4}
     */
    public projectionMatrix: Mat4;

    /**
     * Camera inverse projection matrix.
     * @protected
     * @type {Mat4}
     */
    public inverseProjectionMatrix: Mat4;

    /**
     * Product of projection and view matrices.
     * @protected
     * @type {Mat4}
     */
    public projectionViewMatrix: Mat4;

    public projectionViewRTEMatrix: Mat4;

    /**
     * Inverse projectionView Matrix.
     * @protected
     * @type {Mat4}
     */
    public inverseProjectionViewMatrix: Mat4;

    /**
     * Projection frustum left value.
     * @public
     */
    public left: number;

    /**
     * Projection frustum right value.
     * @public
     */
    public right: number;

    /**
     * Projection frustum bottom value.
     * @public
     */
    public bottom: number;

    /**
     * Projection frustum top value.
     * @public
     */
    public top: number;
    /**
     * Projection frustum near value.
     * @public
     */
    public near: number;

    /**
     * Near-plane safety value used by depth offset logic in shaders.
     * Updated together with `near`.
     * @public
     */
    public depthOffsetNear: number;

    /**
     * Projection frustum far value.
     * @public
     */
    public far: number;

    public cameraFrustumIndex: number;

    public _pickingColorU: Float32Array = new Float32Array([0, 0, 0]);

    constructor(options: IFrustumParams = {}) {
        this._f = [
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0]
        ];

        this.projectionMatrix = new Mat4();

        this.inverseProjectionMatrix = new Mat4();

        this.projectionViewMatrix = new Mat4();

        this.projectionViewRTEMatrix = new Mat4();

        this.inverseProjectionViewMatrix = new Mat4();

        this._isOrthographic = false;

        this._aspect = 1.0;

        this._tanViewAngle_hrad = 0.0;

        this._reverseDepth = false;
        this._depthZeroToOne = false;

        this.left = 0.0;

        this.right = 0.0;

        this.bottom = 0.0;

        this.top = 0.0;

        this.near = 0.0;
        this.depthOffsetNear = 0.0;

        this.far = 0.0;

        this.cameraFrustumIndex = options.cameraFrustumIndex != undefined ? options.cameraFrustumIndex : -1;

        this.setProjectionMatrix(
            options.fov || 30.0,
            options.aspect || 1.0,
            options.near || 1.0,
            options.far || 1000.0,
            false,
            10,
            options.reverseDepth,
            options.depthZeroToOne
        );
    }

    /**
     * Returns right clipping plane.
     * @public
     * @returns {NumberArray4} Right clipping plane coefficients.
     */
    public getRightPlane(): NumberArray4 {
        return this._f[0];
    }

    /**
     * Returns left clipping plane.
     * @public
     * @returns {NumberArray4} Left clipping plane coefficients.
     */
    public getLeftPlane(): NumberArray4 {
        return this._f[1];
    }

    /**
     * Returns bottom clipping plane.
     * @public
     * @returns {NumberArray4} Bottom clipping plane coefficients.
     */
    public getBottomPlane(): NumberArray4 {
        return this._f[2];
    }

    /**
     * Returns top clipping plane.
     * @public
     * @returns {NumberArray4} Top clipping plane coefficients.
     */
    public getTopPlane(): NumberArray4 {
        return this._f[3];
    }

    /**
     * Returns backward clipping plane.
     * @public
     * @returns {NumberArray4} Backward clipping plane coefficients.
     */
    public getBackwardPlane(): NumberArray4 {
        return this._f[4];
    }

    /**
     * Returns forward clipping plane.
     * @public
     * @returns {NumberArray4} Forward clipping plane coefficients.
     */
    public getForwardPlane(): NumberArray4 {
        return this._f[5];
    }

    /**
     * Returns projection-view matrix.
     * @public
     * @returns {NumberArray16} Projection-view matrix values.
     */
    public getProjectionViewMatrix(): NumberArray16 {
        return this.projectionViewMatrix._m;
    }

    /**
     * Returns projection-view RTE matrix.
     * @public
     * @returns {NumberArray16} Projection-view RTE matrix values.
     */
    public getProjectionViewRTEMatrix(): NumberArray16 {
        return this.projectionViewRTEMatrix._m;
    }

    /**
     * Returns projection matrix.
     * @public
     * @returns {NumberArray16} Projection matrix values.
     */
    public getProjectionMatrix(): NumberArray16 {
        return this.projectionMatrix._m;
    }

    /**
     * Returns inverse projection matrix.
     * @public
     * @returns {NumberArray16} Inverse projection matrix values.
     */
    public getInverseProjectionMatrix(): NumberArray16 {
        return this.inverseProjectionMatrix._m;
    }

    /**
     * Sets up camera projection matrix.
     * @public
     * @param {number} viewAngle - Camera vertical field of view angle in degrees.
     * @param {number} aspect - Viewport aspect ratio (`width / height`).
     * @param {number} near - Near clipping plane distance.
     * @param {number} far - Far clipping plane distance.
     * @param {boolean} [isOrthographic=false] - Enables orthographic projection mode.
     * @param {number} [focusDistance=10] - Reference distance used to compute orthographic frustum size.
     * @param {boolean} [reverseDepth=false] - Enables reverse-Z infinite perspective projection.
     * @param {boolean} [depthZeroToOne=false] - Uses `[0, 1]` NDC depth range for reverse-Z projection.
     */
    public setProjectionMatrix(
        viewAngle: number,
        aspect: number,
        near: number,
        far: number,
        isOrthographic: boolean = false,
        focusDistance: number = 10,
        reverseDepth: boolean = false,
        depthZeroToOne: boolean = false
    ) {
        this._isOrthographic = isOrthographic;
        this._reverseDepth = reverseDepth;
        this._depthZeroToOne = depthZeroToOne;
        this._aspect = aspect;
        this._tanViewAngle_hrad = Math.tan(viewAngle * RADIANS_HALF);

        if (this._isOrthographic) {
            let h = focusDistance * this._tanViewAngle_hrad;
            let w = h * aspect;
            this._setFrustumParams(h, w, near, far);
            this.projectionMatrix.setOrthographic(this.left, this.right, this.bottom, this.top, this.near, this.far);
        } else {
            let h = near * this._tanViewAngle_hrad;
            let w = h * aspect;
            this._setFrustumParams(h, w, near, far);
            // reverseDepth: `far` on this frustum is for CPU culling only; projection uses infinite reverse-Z.
            if (reverseDepth) {
                this.projectionMatrix.setPerspectiveReverseInfinite(
                    this.left,
                    this.right,
                    this.bottom,
                    this.top,
                    this.near,
                    depthZeroToOne
                );
            } else {
                this.projectionMatrix.setPerspective(this.left, this.right, this.bottom, this.top, this.near, this.far);
            }
        }

        this.projectionMatrix.inverseTo(this.inverseProjectionMatrix);
    }

    /**
     * Updates near and far clipping planes.
     * @public
     * @param {number} near - Near clipping plane distance.
     * @param {number} [far=this.far] - Far clipping plane distance.
     */
    public setNearFar(near: number, far: number = this.far) {
        if (this._isOrthographic) {
            this.near = near;
            this.far = far;
            this.projectionMatrix.setOrthographic(this.left, this.right, this.bottom, this.top, this.near, this.far);
        } else {
            let h = near * this._tanViewAngle_hrad;
            let w = h * this._aspect;
            this._setFrustumParams(h, w, near, far);
            if (this._reverseDepth) {
                this.projectionMatrix.setPerspectiveReverseInfinite(
                    this.left,
                    this.right,
                    this.bottom,
                    this.top,
                    this.near,
                    this._depthZeroToOne
                );
            } else {
                this.projectionMatrix.setPerspective(this.left, this.right, this.bottom, this.top, this.near, this.far);
            }
        }

        this.projectionMatrix.inverseTo(this.inverseProjectionMatrix);
    }

    protected _setFrustumParams(top: number, right: number, near: number, far: number) {
        this.top = top;
        this.right = right;
        this.bottom = -this.top;
        this.left = -this.right;
        this.near = near;
        this.far = far;
        this.depthOffsetNear = near * 1.001 + 1e-6;
    }

    /**
     * Updates projection-view RTE matrix.
     * @public
     * @param {Mat4} viewRTEMatrix - View matrix in RTE coordinates.
     */
    public setProjectionViewRTEMatrix(viewRTEMatrix: Mat4) {
        this.projectionViewRTEMatrix = this.projectionMatrix.mul(viewRTEMatrix);
    }

    /**
     * Camera's projection matrix values.
     * @public
     * @param {Mat4} viewMatrix - View matrix.
     */
    public setViewMatrix(viewMatrix: Mat4) {
        this.projectionViewMatrix = this.projectionMatrix.mul(viewMatrix);
        this.projectionViewMatrix.inverseTo(this.inverseProjectionViewMatrix);

        let m = this.projectionViewMatrix._m;

        /* Right */
        this._f[0][0] = m[3] - m[0];
        this._f[0][1] = m[7] - m[4];
        this._f[0][2] = m[11] - m[8];
        this._f[0][3] = m[15] - m[12];
        planeNormalize(this._f[0]);

        /* Left */
        this._f[1][0] = m[3] + m[0];
        this._f[1][1] = m[7] + m[4];
        this._f[1][2] = m[11] + m[8];
        this._f[1][3] = m[15] + m[12];
        planeNormalize(this._f[1]);

        /* Bottom */
        this._f[2][0] = m[3] + m[1];
        this._f[2][1] = m[7] + m[5];
        this._f[2][2] = m[11] + m[9];
        this._f[2][3] = m[15] + m[13];
        planeNormalize(this._f[2]);

        /* Top */
        this._f[3][0] = m[3] - m[1];
        this._f[3][1] = m[7] - m[5];
        this._f[3][2] = m[11] - m[9];
        this._f[3][3] = m[15] - m[13];
        planeNormalize(this._f[3]);

        /* Backward */
        this._f[4][0] = m[3] - m[2];
        this._f[4][1] = m[7] - m[6];
        this._f[4][2] = m[11] - m[10];
        this._f[4][3] = m[15] - m[14];
        planeNormalize(this._f[4]);

        /* Forward */
        this._f[5][0] = m[3] + m[2];
        this._f[5][1] = m[7] + m[6];
        this._f[5][2] = m[11] + m[10];
        this._f[5][3] = m[15] + m[14];
        planeNormalize(this._f[5]);
    }

    /**
     * Returns true if the point is inside the frustum.
     * @public
     * @param {Vec3} point - Cartesian point.
     * @returns {boolean} `true` when the point is inside.
     */
    public containsPoint(point: Vec3): boolean {
        for (let p = 0; p < 6; p++) {
            let d = point.dotArr(this._f[p]) + this._f[p][3];
            if (d <= 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the sphere is inside the frustum, ignoring the bottom plane.
     * @public
     * @param {Sphere} sphere - Bounding sphere.
     * @returns {boolean} `true` when the sphere passes all checked planes.
     */
    public containsSphereBottomExc(sphere: Sphere): boolean {
        let r = -sphere.radius,
            f = this._f;
        if (sphere.center.dotArr(f[0]) + f[0][3] <= r) return false;
        if (sphere.center.dotArr(f[1]) + f[1][3] <= r) return false;
        if (sphere.center.dotArr(f[3]) + f[3][3] <= r) return false;
        if (sphere.center.dotArr(f[4]) + f[4][3] <= r) return false;
        return sphere.center.dotArr(f[5]) + f[5][3] > r;
    }

    /**
     * Checks sphere intersection with the bottom frustum plane only.
     * @public
     * @param {Sphere} sphere - Bounding sphere.
     * @returns {boolean} `true` when the sphere is not clipped by the bottom plane.
     */
    public containsSphereButtom(sphere: Sphere): boolean {
        let r = -sphere.radius,
            f = this._f;
        return sphere.center.dotArr(f[2]) + f[2][3] > r;
    }

    /**
     * Returns true if the sphere is inside the frustum.
     * @public
     * @param {Sphere} sphere - Bounding sphere.
     * @returns {boolean} `true` when the sphere is inside.
     */
    public containsSphere(sphere: Sphere): boolean {
        let r = -sphere.radius,
            f = this._f;
        if (sphere.center.dotArr(f[0]) + f[0][3] <= r) return false;
        if (sphere.center.dotArr(f[1]) + f[1][3] <= r) return false;
        if (sphere.center.dotArr(f[2]) + f[2][3] <= r) return false;
        if (sphere.center.dotArr(f[3]) + f[3][3] <= r) return false;
        if (sphere.center.dotArr(f[4]) + f[4][3] <= r) return false;
        return sphere.center.dotArr(f[5]) + f[5][3] > r;
    }

    /**
     * Returns true if the sphere is inside the frustum.
     * @public
     * @param {Vec3} center - Sphere center.
     * @param {number} radius - Sphere radius.
     * @returns {boolean} `true` when the sphere is inside.
     */
    public containsSphere2(center: Vec3, radius: number): boolean {
        let r = -radius;
        if (center.dotArr(this._f[0]) + this._f[0][3] <= r) return false;
        if (center.dotArr(this._f[1]) + this._f[1][3] <= r) return false;
        if (center.dotArr(this._f[2]) + this._f[2][3] <= r) return false;
        if (center.dotArr(this._f[3]) + this._f[3][3] <= r) return false;
        if (center.dotArr(this._f[4]) + this._f[4][3] <= r) return false;
        return center.dotArr(this._f[5]) + this._f[5][3] > r;
    }

    /**
     * Returns true if the box intersects or is inside the frustum.
     * @public
     * @param {Box} box - Bounding box.
     * @returns {boolean} `true` when the box is not fully outside.
     */
    public containsBox(box: Box): boolean {
        let result: boolean = true,
            cout: number,
            cin: number;

        for (let i = 0; i < 6; i++) {
            cout = 0;
            cin = 0;
            for (let k = 0; k < 8 && (cin === 0 || cout === 0); k++) {
                let d = box.vertices[k].dotArr(this._f[i]) + this._f[i][3];
                if (d < 0) {
                    cout++;
                } else {
                    cin++;
                }
            }

            if (cin === 0) {
                return false;
            } else if (cout > 0) {
                result = true;
            }
        }

        return result;
    }
}

export { Frustum };