renderer/cascadeShadows/CascadeShadowManager.tstypescript
import { cascade_shadow_depth } from "./cascade_shadow_depth";
import type { Planet } from "../../scene/Planet";
import type { ShaderProgram } from "../../webgl/ShaderProgram";
import type { Renderer } from "../Renderer";
import { DEFAULT_CASCADE_SHADOW_TEXTURE_UNIT_START } from "../textureUnits";
import { CascadeShadowMap } from "./CascadeShadowMap";

// Must match MAX_CASCADE_COUNT in cascadeShadows.glsl.
const MAX_CASCADE_COUNT = 4;

export class CascadeShadowManager {
    protected _renderer: Renderer;
    protected _planet: Planet | null;
    protected _cascadeShadowMaps: CascadeShadowMap[];
    protected _activeCascadeShadowMaps: CascadeShadowMap[];

    protected _viewProjData: Float32Array;
    protected _eyeRelData: Float32Array;
    protected _paramsData: Float32Array;
    protected _splitsData: Float32Array;
    protected _viewForwardData: Float32Array;
    protected _layerData: Int32Array;
    protected _updateActiveCascadeShadowMaps: boolean;
    protected _initialized: boolean;

    constructor(renderer: Renderer) {
        this._renderer = renderer;
        this._planet = null;
        this._cascadeShadowMaps = [];
        this._activeCascadeShadowMaps = [];

        this._viewProjData = new Float32Array(MAX_CASCADE_COUNT * 16);
        this._eyeRelData = new Float32Array(MAX_CASCADE_COUNT * 3);
        this._paramsData = new Float32Array(MAX_CASCADE_COUNT * 4);
        this._splitsData = new Float32Array(MAX_CASCADE_COUNT * 4);
        this._viewForwardData = new Float32Array(3);
        this._layerData = new Int32Array(MAX_CASCADE_COUNT);
        this._updateActiveCascadeShadowMaps = true;
        this._initialized = false;
    }

    public get activeCount(): number {
        return this._getActiveCascadeShadowMaps().length;
    }

    public get active(): CascadeShadowMap[] {
        return this._getActiveCascadeShadowMaps().slice();
    }

    public init(): void {
        if (this._initialized) return;

        this._renderer.addProgram(cascade_shadow_depth());
        this._renderer.events.on("predraw", this._onDraw, this, 0);
        this._initialized = true;

        for (let i = 0; i < this._cascadeShadowMaps.length; i++) {
            this._initCascadeShadowMap(this._cascadeShadowMaps[i]);
        }
    }

    public bindPlanet(planet: Planet): void {
        if (this._planet === planet) return;

        if (this._planet) {
            this.unbindPlanet();
        }

        this._planet = planet;

        for (let i = 0; i < this._cascadeShadowMaps.length; i++) {
            this._initCascadeShadowMap(this._cascadeShadowMaps[i]);
        }
    }

    public unbindPlanet(): void {
        for (let i = 0; i < this._cascadeShadowMaps.length; i++) {
            this._cascadeShadowMaps[i].unbindPlanet();
        }

        this._planet = null;
    }

    public add(cascadeShadowMap: CascadeShadowMap): number {
        if (cascadeShadowMap._manager) {
            return cascadeShadowMap.id;
        }

        cascadeShadowMap._manager = this;
        this._cascadeShadowMaps.push(cascadeShadowMap);
        this._updateActiveCascadeShadowMaps = true;

        this._initCascadeShadowMap(cascadeShadowMap);

        return cascadeShadowMap.id;
    }

    public update(cascadeShadowMap: CascadeShadowMap): boolean {
        const index = this._cascadeShadowMaps.findIndex((s) => s.id === cascadeShadowMap.id);
        if (index === -1) return false;

        this._cascadeShadowMaps[index] = cascadeShadowMap;
        this._updateActiveCascadeShadowMaps = true;
        return true;
    }

    public remove(cascadeShadowMap: CascadeShadowMap): boolean {
        const index = this._cascadeShadowMaps.findIndex((s) => s.id === cascadeShadowMap.id);
        if (index === -1) return false;

        cascadeShadowMap.destroy();
        cascadeShadowMap._manager = null;
        this._cascadeShadowMaps.splice(index, 1);
        this._updateActiveCascadeShadowMaps = true;
        return true;
    }

    public clear(): void {
        for (let i = 0; i < this._cascadeShadowMaps.length; i++) {
            const csm = this._cascadeShadowMaps[i];
            csm.destroy();
            csm._manager = null;
        }

        this._cascadeShadowMaps.length = 0;
        this._activeCascadeShadowMaps.length = 0;
        this._updateActiveCascadeShadowMaps = false;
    }

    public destroy(): void {
        this.clear();
        this._planet = null;

        if (this._initialized) {
            this._renderer.events.off("predraw", this._onDraw);
            this._initialized = false;
        }
    }

    public bindForward(
        program: ShaderProgram,
        textureUnitStart: number = DEFAULT_CASCADE_SHADOW_TEXTURE_UNIT_START
    ): number {
        const gl = this._renderer.handler.gl;
        if (!gl) return 0;

        const u = program.uniforms!;
        const active = this._getActiveCascadeShadowMaps();
        const csm = active[0];
        const viewForward = this._renderer.activeCamera.getForward();

        this._viewForwardData[0] = viewForward.x;
        this._viewForwardData[1] = viewForward.y;
        this._viewForwardData[2] = viewForward.z;
        gl.uniform3fv(u.u_cascadeShadowViewForward, this._viewForwardData);

        if (!csm || !csm.depthArrayTexture || !csm.framebuffer) {
            gl.uniform1i(u.u_cascadeShadowCount, 0);
            gl.uniform1i(u.u_cascadeShadowDepthArray, textureUnitStart);
            return 0;
        }

        const size = Math.min(csm.cascades.length, MAX_CASCADE_COUNT);
        const activeCameraEye = this._renderer.activeCamera.eye;

        for (let i = 0; i < size; i++) {
            const cascade = csm.cascades[i];
            const camera = csm.depthCamera;
            const frustum = camera.frustums[i];
            const mOffset = i * 16;
            const eOffset = i * 3;
            const vOffset = i * 4;
            const depthRange = Math.max(frustum.far - frustum.near, 1e-6);
            const texelWorld =
                Math.max(frustum.right - frustum.left, frustum.top - frustum.bottom) / csm.framebuffer.width;

            this._viewProjData.set(frustum.projectionViewRTEMatrix._m, mOffset);

            this._eyeRelData[eOffset] = camera.eye.x - activeCameraEye.x;
            this._eyeRelData[eOffset + 1] = camera.eye.y - activeCameraEye.y;
            this._eyeRelData[eOffset + 2] = camera.eye.z - activeCameraEye.z;

            this._paramsData[vOffset] = cascade.depthBiasWorld;
            this._paramsData[vOffset + 1] = cascade.normalBias;
            this._paramsData[vOffset + 2] = texelWorld;
            this._paramsData[vOffset + 3] = depthRange;

            this._splitsData[vOffset] = cascade.splitNear;
            this._splitsData[vOffset + 1] = cascade.splitFar;
            this._splitsData[vOffset + 2] = 0.0;
            this._splitsData[vOffset + 3] = 0.0;

            this._layerData[i] = i;
        }

        gl.uniform1i(u.u_cascadeShadowCount, size);
        gl.uniform1iv(u.u_cascadeShadowLayer, this._layerData);
        gl.uniformMatrix4fv(u.u_cascadeShadowViewProjRTE, false, this._viewProjData);
        gl.uniform3fv(u.u_cascadeShadowEyeRel, this._eyeRelData);
        gl.uniform4fv(u.u_cascadeShadowParams, this._paramsData);
        gl.uniform4fv(u.u_cascadeShadowSplits, this._splitsData);

        gl.activeTexture(gl.TEXTURE0 + textureUnitStart);
        gl.bindTexture(gl.TEXTURE_2D_ARRAY, csm.depthArrayTexture);
        gl.uniform1i(u.u_cascadeShadowDepthArray, textureUnitStart);
        gl.activeTexture(gl.TEXTURE0);

        return size;
    }

    protected _initCascadeShadowMap(cascadeShadowMap: CascadeShadowMap): void {
        if (!this._initialized) {
            return;
        }

        cascadeShadowMap.init(this._renderer);

        if (this._planet) {
            cascadeShadowMap.bindPlanet(this._planet);
        }
    }

    protected _collectActiveCascadeShadowMaps(): CascadeShadowMap[] {
        const active: CascadeShadowMap[] = [];

        for (let i = 0; i < this._cascadeShadowMaps.length; i++) {
            const cascadeShadowMap = this._cascadeShadowMaps[i];
            if (!cascadeShadowMap.enabled) {
                continue;
            }
            active.push(cascadeShadowMap);
        }

        return active;
    }

    protected _getActiveCascadeShadowMaps(): CascadeShadowMap[] {
        if (!this._updateActiveCascadeShadowMaps) {
            return this._activeCascadeShadowMaps;
        }

        this._activeCascadeShadowMaps = this._collectActiveCascadeShadowMaps();
        this._updateActiveCascadeShadowMaps = false;

        return this._activeCascadeShadowMaps;
    }

    protected _onDraw = (): void => {
        const active = this._getActiveCascadeShadowMaps();
        const csm = active[0];
        if (csm) {
            csm.frame();
        }
    };
}