renderer/cascadeShadows/CascadeShadowDepthCamera.tstypescript
import { Camera, type ICameraParams } from "../../camera/Camera";
import { PlanetCamera } from "../../camera/PlanetCamera";
import { DepthCamera } from "../depthCamera/DepthCamera";
import type { Planet } from "../../scene/Planet";
import { Framebuffer } from "../../webgl";
import type { Renderer } from "../Renderer";
import type { CascadeShadowMap } from "./CascadeShadowMap";

/**
 * Depth camera of a cascade shadow map: one orthographic camera, a frustum per cascade. The planet collects
 * the nodes for all the cascades in one traversal, and every cascade is drawn into its own layer of the
 * cascade shadow map depth texture array.
 */
export class CascadeShadowDepthCamera extends DepthCamera {
    public readonly cascadeShadowMap: CascadeShadowMap;

    constructor(cascadeShadowMap: CascadeShadowMap) {
        super({
            width: cascadeShadowMap.size,
            height: cascadeShadowMap.size,
            verticalViewAngle: cascadeShadowMap.verticalViewAngle,
            isOrthographic: true,
            focusDistance: cascadeShadowMap.maxDistance,
            excludeLayers: cascadeShadowMap.excludeLayers,
            showFrustum: false,
            showFootprint: false
        });

        this.cascadeShadowMap = cascadeShadowMap;
    }

    /**
     * Fits the cascades to the main camera and the light, before the planets collect the nodes.
     */
    public override prepareFrame(): void {
        const cascades = this.cascadeShadowMap.cascades;

        let mask = 0;
        for (let i = 0; i < cascades.length; i++) {
            if (cascades[i].enabled) {
                mask |= 1 << i;
            }
        }
        this.frustumMask = mask;

        this.cascadeShadowMap._fitDepthCamera();
    }

    public override frame(): void {
        const csm = this.cascadeShadowMap;
        const depthArrayTexture = csm.depthArrayTexture;

        if (!this.enabled || !this._initialized || !this._renderer || !depthArrayTexture) return;

        const framebuffer = this.framebuffer;
        const gl = framebuffer.handler.gl!;
        const camera = this.camera;
        const cascades = csm.cascades;

        this._renderer.applyDepthForCamera(camera);

        framebuffer.activate();

        for (let i = 0; i < cascades.length; i++) {
            if (!cascades[i].enabled) continue;

            framebuffer.bindOutputTextureLayer(depthArrayTexture, i);
            camera.setCurrentFrustum(i);

            // 0 is an empty texel, it reads as lit.
            gl.clearColor(0.0, 0.0, 0.0, 0.0);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
            gl.disable(gl.BLEND);

            this._scenesPass(camera);

            gl.enable(gl.BLEND);
        }

        framebuffer.deactivate();

        this._renderer.applyDepthForCamera(this._renderer.activeCamera);
    }

    protected override _createCamera(planet: Planet | null): Camera {
        const csm = this.cascadeShadowMap;
        const frustums: [number, number][] = [];

        for (let i = 0; i < csm.cascades.length; i++) {
            frustums.push([csm.cascades[i].splitNear, csm.cascades[i].splitFar]);
        }

        const params: ICameraParams = {
            frustums,
            width: this.width,
            height: this.height,
            viewAngle: this.verticalViewAngle,
            isOrthographic: true,
            focusDistance: csm.maxDistance,
            reverseDepth: false
        };

        return planet
            ? new PlanetCamera(planet, {
                  ...params,
                  maxAltitude: Math.max(csm.maxDistance * 4.0, planet.ellipsoid.getEquatorialSize() * 8.0)
              })
            : new Camera(params);
    }

    protected override _createFramebuffer(renderer: Renderer): Framebuffer {
        return new Framebuffer(renderer.handler, {
            width: this.width,
            height: this.height,
            depthComponent: "DEPTH_COMPONENT32F",
            targets: [
                {
                    internalFormat: "R32F",
                    attachment: "COLOR_ATTACHMENT"
                }
            ],
            useDepth: true
        });
    }
}