renderer/depthCamera/DepthCamera.tstypescript
import { Camera, type ICameraParams } from "../../camera/Camera";
import { PlanetCamera } from "../../camera/PlanetCamera";
import { Entity } from "../../entity/Entity";
import { LonLat } from "../../LonLat";
import { Vec2 } from "../../math/Vec2";
import { Vec3, type NumberArray3 } from "../../math/Vec3";
import { Vec4, type NumberArray4 } from "../../math/Vec4";
import { RADIANS_HALF } from "../../math";
import { Object3d } from "../../Object3d";
import type { Renderer } from "../Renderer";
import { VARIANCE_SHADOW_ENABLED } from "../shadows/ShadowManager";
import type { Planet } from "../../scene/Planet";
import type { Scene } from "../../scene/Scene";
import type { EntityCollection } from "../../entity/EntityCollection";
import { Framebuffer } from "../../webgl";
import { Vector } from "../../layer/Vector";
import type { ShadowMap } from "../shadows/ShadowMap";
import type { DepthCameraHandler } from "./DepthCameraHandler";

const CAM_WIDTH = 512;
const CAM_HEIGHT = 512;
const DEPTH_NEAR = 100;
const DEPTH_FAR = 100000;
const DEPTH_BIAS_WORLD = 1.0;
const DEPTH_NORMAL_BIAS = 0.45;
const TEXEL_SNAP_EPSILON = 1e-9;
const ORTHO_TEXEL_QUANTIZATION_STEPS = 4;
const ORTHO_TEXEL_QUANTIZATION_RATIO = Math.pow(2.0, 1.0 / ORTHO_TEXEL_QUANTIZATION_STEPS);
const DEFAULT_VERTICAL_VIEW_ANGLE = 45;
const PERIMETER_STEP_PX = 1;
const DEFAULT_CAMERA_FRUSTUM_LENGTH = 2.5;
const DEFAULT_CAMERA_FRUSTUM_COLOR = "rgb(155, 255, 155, 0.2)";
const RENDER_SKIRTS_SLOPE = 0.3;

const cameraFrustumObj = Object3d.createFrustum();

interface IPerimeterSegmentsData {
    segments: LonLat[][];
    isClosed: boolean;
}

export interface CameraPose {
    lonLat?: LonLat | NumberArray3;
    lookLonLat?: LonLat | NumberArray3;
    eye?: Vec3 | NumberArray3;
    look?: Vec3 | NumberArray3;
    up?: Vec3 | NumberArray3;
    pitch?: number;
    yaw?: number;
    roll?: number;
}

export interface IDepthCameraParams extends CameraPose {
    enabled?: boolean;
    width?: number;
    height?: number;
    near?: number;
    far?: number;
    verticalViewAngle?: number;
    horizontalViewAngle?: number;
    showFrustum?: boolean;
    showFootprint?: boolean;
    frustumLength?: number;
    frustumColor?: Vec4 | NumberArray4 | string;
    isOrthographic?: boolean;
    focusDistance?: number;
    enableSegmentSkirts?: boolean;
    enableSegmentFaceCulling?: boolean;
    excludeLayers?: Vector[];
    excludeEntityCollections?: EntityCollection[];
    planet?: Planet;
    depthBiasWorld?: number;
    normalBias?: number; // 0.2 .. 1.0
}

const toLonLat = (v: LonLat | NumberArray3): LonLat => (v instanceof LonLat ? v : LonLat.createFromArray(v));
const toVec3 = (v: Vec3 | NumberArray3): Vec3 => (v instanceof Vec3 ? v : Vec3.fromVec(v));

function getDistanceFromPixel(x: number, y: number, camera: Camera, framebuffer: Framebuffer): number {
    const px = new Vec2(x, y);
    const nx = px.x / framebuffer.width;
    const ny = (framebuffer.height - px.y) / framebuffer.height;
    const ddd = new Float32Array(4);

    framebuffer.readData(nx, ny, ddd, 0);

    if (ddd[0] === 0 || ddd[0] >= 1.0) {
        return 0;
    }

    const depth = ddd[0];
    const proj = camera.frustums[0].inverseProjectionMatrix;
    const screenPos = new Vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
    const viewPosition = proj.mulVec4(screenPos);
    const dir = camera.unproject(nx * camera.width, (1 - ny) * camera.height);

    return -(viewPosition.z / viewPosition.w) / dir.dot(camera.getForward());
}

/**
 * Camera that renders a depth frame of the renderer scenes for projectors and shadow maps. The planets give it
 * their terrain nodes, the scenes their 3D objects.
 * @constructor
 * @param {Object} [options] - Depth camera options:
 * @param {boolean} [options.enabled=true] - Renders the depth frame. A disabled camera gets no planet nodes either.
 * @param {number} [options.width=512] - Depth frame width in pixels. Projectors and shadow maps need a square frame.
 * @param {number} [options.height=512] - Depth frame height in pixels.
 * @param {number} [options.near=100] - Near plane distance in meters.
 * @param {number} [options.far=100000] - Far plane distance in meters.
 * @param {number} [options.verticalViewAngle=45] - Vertical view angle in degrees.
 * @param {number} [options.horizontalViewAngle] - Horizontal view angle in degrees. When set, the vertical one is
 * taken from it and the frame aspect.
 * @param {boolean} [options.isOrthographic=false] - Orthographic projection.
 * @param {number} [options.focusDistance] - Distance at which the orthographic frame size is taken from the view
 * angle. Default is the far distance.
 * @param {boolean} [options.showFrustum=false] - Shows the camera frustum. Without a planet it is shown too.
 * @param {number} [options.frustumLength=2.5] - Size of the shown frustum, it is scaled with the distance.
 * @param {Vec4|Array.<number>|string} [options.frustumColor="rgb(155, 255, 155, 0.2)"] - Shown frustum color.
 * @param {boolean} [options.showFootprint=false] - Shows the frame border on the ground, only with a planet. The
 * depth frame is read back every frame for it.
 * @param {boolean} [options.enableSegmentFaceCulling=true] - Culls the back faces of the terrain segments.
 * @param {boolean} [options.enableSegmentSkirts=false] - Not used for now: the terrain segments are drawn without
 * skirts.
 * @param {Array.<Vector>} [options.excludeLayers] - Vector layers whose 3D objects are not drawn into the frame.
 * @param {Array.<EntityCollection>} [options.excludeEntityCollections] - Entity collections whose 3D objects are
 * not drawn into the frame.
 * @param {Planet} [options.planet] - Planet the camera is bound to: PlanetCamera, lon/lat coordinates, footprint.
 * Default is the first planet of the renderer at its start, a planet added after the start has to be given here.
 * The terrain nodes come from all the planets anyway.
 * @param {number} [options.depthBiasWorld=1] - Occlusion bias of the receivers in meters.
 * @param {number} [options.normalBias=0.45] - Offset of the receiver point along its normal in meters.
 * @param {LonLat|Array.<number>} [options.lonLat] - Start position with a planet, `[lon, lat, height]`. Without
 * lonLat and eye the camera looks down at lon 0, lat 0 from the focus distance.
 * @param {LonLat|Array.<number>} [options.lookLonLat] - Point to look at with a planet, `[lon, lat, height]`.
 * @param {Vec3|Array.<number>} [options.eye] - Start position in cartesian coordinates, also without a planet.
 * @param {Vec3|Array.<number>} [options.look] - Point to look at in cartesian coordinates.
 * @param {Vec3|Array.<number>} [options.up] - Up direction.
 * @param {number} [options.pitch] - Start pitch in radians, applied after the position. With a planet the angles
 * are in the planet local frame at the eye.
 * @param {number} [options.yaw] - Start yaw in radians.
 * @param {number} [options.roll] - Start roll in radians.
 */
export class DepthCamera {
    protected static __counter__ = 0;

    public readonly id: number;

    public readonly width: number;
    public readonly height: number;
    public readonly near: number;
    public readonly far: number;
    public readonly verticalViewAngle: number;
    public readonly horizontalViewAngle?: number;
    public readonly excludeLayers: Vector[];
    public readonly excludeEntityCollections: EntityCollection[];
    public depthBiasWorld: number;
    public normalBias: number;

    public enabled: boolean;
    public enableSegmentSkirts: boolean;
    public enableSegmentFaceCulling: boolean;

    public frustumMask: number;

    public camera!: Camera;
    public framebuffer!: Framebuffer;
    public shadowMap: ShadowMap | null;

    public _handler: DepthCameraHandler | null;
    public _handlerIndex: number;

    protected _planet: Planet | null;
    protected _renderer: Renderer | null;
    protected _initialized: boolean;
    protected _cameraPose: CameraPose;
    protected _showFrustum: boolean;
    protected _showFootprint: boolean;
    protected _isOrthographic: boolean;
    protected _focusDistance: number;
    protected _orthoTexelSizeX: number;
    protected _orthoTexelSizeY: number;

    protected _frustumLength: number;
    protected _frustumColor: Vec4 | NumberArray4 | string;
    protected _cameraFrustumEntity: Entity | null;
    protected _cameraFootprintEntity: Entity | null;
    protected _cameraFootprintSegmentPointCounts: number[];
    protected _cameraFootprintClosedState: boolean;

    protected _prevCameraPos: Vec3;
    protected _prevCameraPitch: number;
    protected _prevCameraYaw: number;
    protected _prevCameraRoll: number;
    protected _prevCameraFrustumEntityPos: Vec3;
    protected _prevCameraFrustumEntityPitch: number;
    protected _prevCameraFrustumEntityYaw: number;
    protected _prevCameraFrustumEntityRoll: number;
    protected _prevFrustumScale: Vec3;

    constructor(params: IDepthCameraParams = {}) {
        this.id = DepthCamera.__counter__++;

        this.enabled = params.enabled ?? true;
        this.frustumMask = -1;
        this.enableSegmentSkirts = params.enableSegmentSkirts ?? false;
        this.enableSegmentFaceCulling = params.enableSegmentFaceCulling ?? true;
        this.width = params.width ?? CAM_WIDTH;
        this.height = params.height ?? CAM_HEIGHT;
        this.near = params.near ?? DEPTH_NEAR;
        this.far = params.far ?? DEPTH_FAR;
        this.verticalViewAngle = params.verticalViewAngle ?? DEFAULT_VERTICAL_VIEW_ANGLE;
        this.horizontalViewAngle = params.horizontalViewAngle;
        this.excludeLayers = params.excludeLayers ? [...params.excludeLayers] : [];
        this.excludeEntityCollections = params.excludeEntityCollections ? [...params.excludeEntityCollections] : [];
        this.depthBiasWorld = params.depthBiasWorld ?? DEPTH_BIAS_WORLD;
        this.normalBias = params.normalBias ?? DEPTH_NORMAL_BIAS;

        this._planet = params.planet ?? null;
        this._renderer = null;
        this._initialized = false;
        this._cameraPose = {
            lonLat: params.lonLat,
            lookLonLat: params.lookLonLat,
            eye: params.eye,
            look: params.look,
            up: params.up,
            pitch: params.pitch,
            yaw: params.yaw,
            roll: params.roll
        };
        this._showFrustum = params.showFrustum ?? false;
        this._showFootprint = params.showFootprint ?? false;
        this._isOrthographic = params.isOrthographic ?? false;
        this._focusDistance = params.focusDistance ?? this.far;
        this._orthoTexelSizeX = 0.0;
        this._orthoTexelSizeY = 0.0;

        this._frustumLength = params.frustumLength ?? DEFAULT_CAMERA_FRUSTUM_LENGTH;
        this._frustumColor = params.frustumColor ?? DEFAULT_CAMERA_FRUSTUM_COLOR;

        this._cameraFootprintEntity = this._showFootprint ? this._createCameraFootprintEntity() : null;
        this._cameraFrustumEntity = this._showFrustum ? this._createCameraFrustumEntity() : null;

        this._cameraFootprintSegmentPointCounts = [];
        this._cameraFootprintClosedState = false;

        this._handler = null;
        this._handlerIndex = -1;
        this.shadowMap = null;

        this._prevCameraPos = new Vec3();
        this._prevCameraPitch = 0;
        this._prevCameraYaw = 0;
        this._prevCameraRoll = 0;
        this._prevCameraFrustumEntityPos = new Vec3();
        this._prevCameraFrustumEntityPitch = 0;
        this._prevCameraFrustumEntityYaw = 0;
        this._prevCameraFrustumEntityRoll = 0;
        this._prevFrustumScale = new Vec3();
    }

    public get initialized(): boolean {
        return this._initialized;
    }

    /**
     * Planet the camera is geographically bound to (PlanetCamera, footprint, lon/lat), null without a planet.
     */
    public get planet(): Planet | null {
        return this._planet;
    }

    public get showFrustum(): boolean {
        return this._showFrustum;
    }

    public set showFrustum(showFrustum: boolean) {
        this._showFrustum = showFrustum;

        if (showFrustum && !this._cameraFrustumEntity) {
            this._cameraFrustumEntity = this._createCameraFrustumEntity();
        }

        if (this._handler && this._handlerIndex !== -1 && this._cameraFrustumEntity) {
            this._handler._showFrustumEntity(this._cameraFrustumEntity, showFrustum);
        }
    }

    public get showFootprint(): boolean {
        return this._showFootprint;
    }

    public set showFootprint(showFootprint: boolean) {
        this._showFootprint = showFootprint;

        if (showFootprint && !this._cameraFootprintEntity) {
            this._cameraFootprintEntity = this._createCameraFootprintEntity();
        }

        if (this._handler && this._handlerIndex !== -1 && this._cameraFootprintEntity) {
            this._handler._showFootprintEntity(this._cameraFootprintEntity, showFootprint);
        }
    }

    public get texelScale(): number {
        const height = Math.max(this.height, 1);

        if (this.camera.isOrthographic) {
            const frustum = this.camera.frustums[0];
            return Math.max(frustum.right - frustum.left, frustum.top - frustum.bottom) / height;
        }

        return (2.0 * Math.tan(this.camera.verticalViewAngle * RADIANS_HALF)) / height;
    }

    public get isOrthographic(): boolean {
        return this._initialized ? this.camera.isOrthographic : this._isOrthographic;
    }

    public set isOrthographic(isOrthographic: boolean) {
        this._isOrthographic = isOrthographic;

        if (this._initialized) {
            if (isOrthographic) {
                this.camera.focusDistance = this._focusDistance;
            }
            this.camera.isOrthographic = isOrthographic;
        }
    }

    public get focusDistance(): number {
        return this._initialized ? this.camera.focusDistance : this._focusDistance;
    }

    public set focusDistance(focusDistance: number) {
        if (!Number.isFinite(focusDistance) || focusDistance <= 0) return;

        this._focusDistance = focusDistance;

        if (this._initialized) {
            this.camera.focusDistance = focusDistance;
        }
    }

    public get cameraFootprintEntity(): Entity | null {
        return this._cameraFootprintEntity;
    }

    public get cameraFrustumEntity(): Entity | null {
        return this._cameraFrustumEntity;
    }

    public get frustumLength(): number {
        return this._frustumLength;
    }

    public set frustumLength(length: number) {
        this._frustumLength = length > 0 ? length : DEFAULT_CAMERA_FRUSTUM_LENGTH;
    }

    public get frustumColor(): Vec4 | NumberArray4 | string {
        return this._frustumColor;
    }

    public set frustumColor(color: Vec4 | NumberArray4 | string) {
        this._frustumColor = color;

        const geoObject = this._cameraFrustumEntity?.geoObject;

        if (!geoObject) return;

        if (typeof color === "string") {
            geoObject.setColorHTML(color);
        } else {
            const c = color instanceof Vec4 ? color : new Vec4(color[0], color[1], color[2], color[3]);
            geoObject.setColor(c.x, c.y, c.z, c.w);
        }
    }

    public get frustumScale(): Vec3 {
        return Object3d.getFrustumScaleByCameraAngles(
            this._frustumLength,
            this.camera.horizontalViewAngle,
            this.camera.verticalViewAngle
        );
    }

    /**
     * @param {Planet | null} planet - Default planet to bind the camera to when it has none.
     * @param {Renderer} renderer - Renderer.
     */
    public init(planet: Planet | null, renderer: Renderer): void {
        if (this._initialized) return;

        this._planet = this._planet || planet;
        this._renderer = renderer;
        this.camera = this._createCamera(this._planet);
        this._initPose();
        this.framebuffer = this._createFramebuffer(renderer);
        this.framebuffer.init();

        this._initialized = true;
    }

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

        const cam = this.camera;

        this._cameraPose = {
            eye: cam.eye.clone(),
            look: cam.eye.add(cam.getForward()),
            up: cam.getUp()
        };

        this.framebuffer.destroy();

        this._renderer = null;
        this._initialized = false;
    }

    /**
     * Renders the depth frame of all renderer scenes. Planets give the nodes they have collected for this
     * camera, so it runs after the planet collection, and prepareFrame before it.
     */
    public frame(): void {
        if (!this.enabled || !this._initialized || !this._renderer) return;

        const framebuffer = this.framebuffer;
        const gl = framebuffer.handler.gl!;
        const mainCam = this._renderer.activeCamera;
        const depthCamera = this.camera;

        framebuffer.activate();

        this._renderer.applyDepthForCamera(depthCamera);

        depthCamera.updateCameraSlope();

        gl.clearColor(1.0, 1.0, 1.0, 0.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        gl.disable(gl.BLEND);

        this._scenesPass(depthCamera);

        gl.enable(gl.BLEND);

        framebuffer.deactivate();

        this.renderFootprint();

        if (VARIANCE_SHADOW_ENABLED) {
            this.shadowMap?.blur();
        }

        this._renderer.applyDepthForCamera(mainCam);
        this.finishFrame();
    }

    public prepareFrame(): void {
        this._prepareOrthographicProjection();
        this._prepareCameraEntitySync();
    }

    protected _scenesPass(camera: Camera): void {
        const scenes = this._renderer!._scenesArr;
        for (let i = 0; i < scenes.length; i++) {
            const scene = scenes[i];
            if (!scene.isActive() || scene.disableDepthCamera) {
                continue;
            }

            const planet = scene as Planet;

            if (planet.quadTreeStrategy) {
                this._segmentsPass(camera, planet);
                this._vectorLayersPass(camera, planet);
            }

            this._entityCollectionsPass(camera, scene);
        }
    }

    protected _prepareCameraEntitySync(): void {
        const cameraFrustumEntity = this._cameraFrustumEntity;
        if (!this._showFrustum || !cameraFrustumEntity) {
            return;
        }

        const cam = this.camera;
        const cameraFrustumEntityPos = cameraFrustumEntity.getAbsoluteCartesian();
        const cameraFrustumEntityPitch = cameraFrustumEntity.getPitch();
        const cameraFrustumEntityYaw = cameraFrustumEntity.getYaw();
        const cameraFrustumEntityRoll = cameraFrustumEntity.getRoll();
        let cameraUpdated = false;

        if (this._prevCameraPos.equal(cam.eye) && !this._prevCameraPos.equal(cameraFrustumEntityPos)) {
            cam.eye.copy(cameraFrustumEntityPos);
            cameraUpdated = true;
        }

        const cameraPitch = cam.getPitch();
        const cameraYaw = cam.getYaw();
        const cameraRoll = cam.getRoll();

        if (
            this._prevCameraPitch === cameraPitch &&
            this._prevCameraYaw === cameraYaw &&
            this._prevCameraRoll === cameraRoll &&
            (this._prevCameraFrustumEntityPitch !== cameraFrustumEntityPitch ||
                this._prevCameraFrustumEntityYaw !== cameraFrustumEntityYaw ||
                this._prevCameraFrustumEntityRoll !== cameraFrustumEntityRoll)
        ) {
            cam.setPitchYawRoll(cameraFrustumEntityPitch, cameraFrustumEntityYaw, cameraFrustumEntityRoll);
            cameraUpdated = true;
        }

        if (
            cameraUpdated ||
            !this._prevCameraPos.equal(cam.eye) ||
            this._prevCameraPitch !== cam.getPitch() ||
            this._prevCameraYaw !== cam.getYaw() ||
            this._prevCameraRoll !== cam.getRoll()
        ) {
            cam.update();
        }
    }

    protected _finishCameraEntitySync(): void {
        const cameraFrustumEntity = this._cameraFrustumEntity;
        if (!this._showFrustum || !cameraFrustumEntity) return;

        const cam = this.camera;
        const scale = this.frustumScale;

        if (
            this._prevCameraPos.equal(cam.eye) &&
            this._prevCameraPitch === cam.getPitch() &&
            this._prevCameraYaw === cam.getYaw() &&
            this._prevCameraRoll === cam.getRoll() &&
            this._prevFrustumScale.equal(scale) &&
            this._prevCameraFrustumEntityPos.equal(cameraFrustumEntity.getAbsoluteCartesian()) &&
            this._prevCameraFrustumEntityPitch === cameraFrustumEntity.getPitch() &&
            this._prevCameraFrustumEntityYaw === cameraFrustumEntity.getYaw() &&
            this._prevCameraFrustumEntityRoll === cameraFrustumEntity.getRoll()
        ) {
            return;
        }

        cameraFrustumEntity.setScale3v(scale);
        this._prevFrustumScale.copy(scale);
        cameraFrustumEntity.setCartesian3v(cam.eye);
        cameraFrustumEntity.setAbsolutePitch(cam.getPitch());
        cameraFrustumEntity.setAbsoluteYaw(cam.getYaw());
        cameraFrustumEntity.setAbsoluteRoll(cam.getRoll());

        this._prevCameraPitch = cam.getPitch();
        this._prevCameraYaw = cam.getYaw();
        this._prevCameraRoll = cam.getRoll();
        this._prevCameraPos.copy(cam.eye);

        this._prevCameraFrustumEntityPos.copy(cameraFrustumEntity.getAbsoluteCartesian());
        this._prevCameraFrustumEntityPitch = cameraFrustumEntity.getPitch();
        this._prevCameraFrustumEntityYaw = cameraFrustumEntity.getYaw();
        this._prevCameraFrustumEntityRoll = cameraFrustumEntity.getRoll();

        if (this._planet && this._planet.ellipsoid) {
            cameraFrustumEntity._lonLat.copy(
                this._planet.ellipsoid.cartesianToLonLat(cameraFrustumEntity.getAbsoluteCartesian())
            );
        }
    }

    public finishFrame(): void {
        this._finishCameraEntitySync();
    }

    public renderFootprint(): void {
        const cameraFootprintEntity = this._cameraFootprintEntity;
        if (!this._showFootprint || !cameraFootprintEntity) return;

        const framebuffer = this.framebuffer;
        framebuffer.readPixelBuffers();

        const perimeterData = this._collectPerimeterLonLats(framebuffer.width, framebuffer.height);
        const segments = perimeterData.segments;
        const nextPointCounts = segments.map((segment) => segment.length);
        const nextClosedState = perimeterData.isClosed;

        const isSameTopology =
            this._cameraFootprintSegmentPointCounts !== null &&
            this._cameraFootprintClosedState === nextClosedState &&
            this._cameraFootprintSegmentPointCounts.length === nextPointCounts.length &&
            this._cameraFootprintSegmentPointCounts.every((count, index) => count === nextPointCounts[index]);

        const polyline = cameraFootprintEntity.polyline!;
        if (polyline.isClosed !== nextClosedState) {
            polyline.isClosed = nextClosedState;
        }

        if (isSameTopology) {
            polyline.setPathLonLatFast(segments);
        } else {
            polyline.setPathLonLat(segments);
        }

        this._cameraFootprintSegmentPointCounts = nextPointCounts;
        this._cameraFootprintClosedState = nextClosedState;
    }

    public getCamera(): Camera {
        return this.camera;
    }

    public getFramebuffer(): Framebuffer {
        return this.framebuffer;
    }

    public getDepthTexture(): WebGLTexture {
        return this.framebuffer.textures[0]!;
    }

    protected _prepareOrthographicProjection(): void {
        if (!this.camera.isMoving) return;
        if (this._snapOrthographicProjectionToTexelGrid()) {
            this.camera.update();
        }
    }

    /**
     * Quantizes the orthographic extent and keeps the previous size while it still fits to prevent jitter.
     */
    protected _quantizeOrthoTexelSize(extent: number, resolution: number, prevTexelSize: number): number {
        const texelSize = extent / resolution;

        if (!(texelSize > 0.0)) {
            return 0.0;
        }

        if (
            prevTexelSize > 0.0 &&
            texelSize <= prevTexelSize &&
            texelSize > prevTexelSize / ORTHO_TEXEL_QUANTIZATION_RATIO
        ) {
            return prevTexelSize;
        }

        const step = Math.ceil(Math.log2(texelSize) * ORTHO_TEXEL_QUANTIZATION_STEPS);

        return Math.pow(2.0, step / ORTHO_TEXEL_QUANTIZATION_STEPS);
    }

    protected _snapOrthographicProjectionToTexelGrid(): boolean {
        const cam = this.camera;

        if (!cam.isOrthographic) {
            return false;
        }

        const frustum = cam.frustums[0];
        const framebuffer = this.framebuffer;

        const texelSizeX = this._quantizeOrthoTexelSize(
            frustum.right - frustum.left,
            framebuffer.width,
            this._orthoTexelSizeX
        );
        const texelSizeY = this._quantizeOrthoTexelSize(
            frustum.top - frustum.bottom,
            framebuffer.height,
            this._orthoTexelSizeY
        );

        if (texelSizeX <= 0.0 || texelSizeY <= 0.0) {
            return false;
        }

        this._orthoTexelSizeX = texelSizeX;
        this._orthoTexelSizeY = texelSizeY;

        const width = texelSizeX * framebuffer.width;
        const height = texelSizeY * framebuffer.height;

        const centerX = (frustum.left + frustum.right) * 0.5;
        const centerY = (frustum.bottom + frustum.top) * 0.5;

        const eyeX = cam.eye.dot(cam._r);
        const eyeY = cam.eye.dot(cam._u);

        const snappedMinX = Math.floor((eyeX + centerX - width * 0.5) / texelSizeX) * texelSizeX;
        const snappedMinY = Math.floor((eyeY + centerY - height * 0.5) / texelSizeY) * texelSizeY;

        const left = snappedMinX - eyeX;
        const right = left + width;
        const bottom = snappedMinY - eyeY;
        const top = bottom + height;

        if (
            Math.abs(left - frustum.left) <= TEXEL_SNAP_EPSILON &&
            Math.abs(right - frustum.right) <= TEXEL_SNAP_EPSILON &&
            Math.abs(bottom - frustum.bottom) <= TEXEL_SNAP_EPSILON &&
            Math.abs(top - frustum.top) <= TEXEL_SNAP_EPSILON
        ) {
            return false;
        }

        frustum.setOrthoBounds(left, right, bottom, top);

        return true;
    }

    public getCartesianFromPixelTerrain(x: number, y: number): Vec3 | undefined {
        const distance = getDistanceFromPixel(x, y, this.camera, this.framebuffer);
        if (distance === 0) return;

        const nx = x / this.framebuffer.width;
        const ny = (this.framebuffer.height - y) / this.framebuffer.height;

        if (this.camera.isOrthographic) {
            const position = new Vec3();
            this.camera.unproject(nx * this.camera.width, (1 - ny) * this.camera.height, distance, position);
            return position;
        }

        const direction = this.camera.unproject(nx * this.camera.width, (1 - ny) * this.camera.height);
        return direction.scaleTo(distance).addA(this.camera.eye);
    }

    public getLonLatFromPixelTerrain(x: number, y: number): LonLat | undefined {
        const coords = this.getCartesianFromPixelTerrain(x, y);
        return coords && this._planet ? this._planet.ellipsoid.cartesianToLonLat(coords) : undefined;
    }

    protected _createCameraFootprintEntity(): Entity {
        return new Entity({
            polyline: {
                color: "rgba(255,0,0,0.82)",
                thickness: 5.0,
                isClosed: true
            }
        });
    }

    protected _createCameraFrustumEntity(): Entity {
        return new Entity({
            visibility: true,
            scale: new Vec3(1, 1, 1),
            geoObject: {
                tag: "depth-camera-frustum",
                color: this._frustumColor,
                object3d: cameraFrustumObj
            },
            properties: {
                depthCamera: this
            }
        });
    }

    protected _initPose(): void {
        const cam = this.camera;
        const p = this._cameraPose;

        if (p.lonLat && cam instanceof PlanetCamera) {
            cam.setLonLat(
                toLonLat(p.lonLat),
                p.lookLonLat ? toLonLat(p.lookLonLat) : undefined,
                p.up ? toVec3(p.up) : undefined
            );
        } else if (p.eye) {
            cam.set(toVec3(p.eye), p.look ? toVec3(p.look) : undefined, p.up ? toVec3(p.up) : undefined);
        } else if (cam instanceof PlanetCamera) {
            cam.setLonLat(new LonLat(0, 0, this._focusDistance));
        }

        if (p.pitch != null || p.yaw != null || p.roll != null) {
            cam.setPitchYawRoll(p.pitch ?? 0, p.yaw ?? 0, p.roll ?? 0);
        }

        cam.update();
    }

    protected _createCamera(planet: Planet | null): Camera {
        const params: ICameraParams = {
            frustums: [[this.near, this.far]],
            width: this.width,
            height: this.height,
            viewAngle: this.verticalViewAngle,
            isOrthographic: this._isOrthographic,
            focusDistance: this._focusDistance,
            reverseDepth: false
        };

        const camera = planet ? new PlanetCamera(planet, params) : new Camera(params);

        if (this.horizontalViewAngle != null) {
            camera.setHorizontalViewAngle(this.horizontalViewAngle);
        }

        return camera;
    }

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

    protected _segmentsPass(camera: Camera, planet: Planet): void {
        const h = this._renderer!.handler;
        const gl = h.gl!;

        if (this._handlerIndex === -1) return;

        let checkSlope = camera.isOrthographic && camera.slope < RENDER_SKIRTS_SLOPE;

        h.programs.depth_camera.activate();
        const sh = h.programs.depth_camera;
        const shu = sh.uniforms;

        gl.uniformMatrix4fv(shu.viewMatrix, false, camera.getViewMatrix());
        gl.uniformMatrix4fv(shu.projectionMatrix, false, camera.getProjectionMatrix());

        if (this.enableSegmentFaceCulling) {
            gl.enable(gl.CULL_FACE);
        } else if (checkSlope) {
            gl.disable(gl.CULL_FACE);
        }

        //const isEq = planet.terrain!.equalizeVertices;
        const baseLayerSlice = planet.visibleTileLayers.length ? [planet.visibleTileLayers[0]] : undefined;
        const rn = planet.quadTreeStrategy.getViewCameraRenderedNodes(this._handlerIndex, camera.getCurrentFrustum());

        let i = rn.length;
        while (i--) {
            const s = rn[i].segment;
            if (!s || !s.node) continue;
            //isEq && s.equalize();
            s.readyToEngage && s.engage();
            s.ensureIndexBuffer();
            s.updateRTCEyePosition(camera);
            s.depthRendering(sh, baseLayerSlice);
        }

        gl.enable(gl.CULL_FACE);
    }

    protected _vectorLayersPass(camera: Camera, planet: Planet): void {
        const layers = planet.layers;

        for (let i = 0; i < layers.length; i++) {
            const layer = layers[i];
            if (!(layer instanceof Vector) || !layer.getVisibility()) {
                continue;
            }

            if (this.excludeLayers.includes(layer)) {
                continue;
            }

            layer._geoObjectEntityCollection.geoObjectHandler.drawDepthCameraPass(camera);
        }
    }

    protected _entityCollectionsPass(camera: Camera, scene: Scene): void {
        const ecs = scene.entityCollections;

        for (let i = 0; i < ecs.length; i++) {
            const ec = ecs[i];
            if (ec.getVisibility() && !this.excludeEntityCollections.includes(ec)) {
                ec.geoObjectHandler.drawDepthCameraPass(camera);
            }
        }
    }

    protected _collectPerimeterLonLats(width: number, height: number): IPerimeterSegmentsData {
        const segments: LonLat[][] = [];
        let currentSegment: LonLat[] | null = null;
        let hasMissingData = false;
        let firstPointHasData = false;
        let lastPointHasData = false;
        let sampledPointIndex = 0;

        const addPoint = (x: number, y: number) => {
            const lonLat = this.getLonLatFromPixelTerrain(x, y);
            const hasData = !!lonLat;

            if (sampledPointIndex === 0) {
                firstPointHasData = hasData;
            }
            lastPointHasData = hasData;
            sampledPointIndex++;

            if (lonLat) {
                if (!currentSegment) {
                    currentSegment = [];
                    segments.push(currentSegment);
                }
                currentSegment.push(new LonLat(lonLat.lon, lonLat.lat, lonLat.height));
            } else {
                hasMissingData = true;
                currentSegment = null;
            }
        };

        for (let x = 1; x < width; x += PERIMETER_STEP_PX) addPoint(x, 1);
        for (let y = 2; y < height; y += PERIMETER_STEP_PX) addPoint(width - 1, y);
        for (let x = width - 2; x >= 1; x -= PERIMETER_STEP_PX) addPoint(x, height - 1);
        for (let y = height - 2; y >= 2; y -= PERIMETER_STEP_PX) addPoint(1, y);

        if (segments.length > 1 && firstPointHasData && lastPointHasData) {
            const firstSegment = segments[0];
            const lastSegment = segments[segments.length - 1];

            lastSegment.push(...firstSegment);
            segments[0] = lastSegment;
            segments.pop();
        }

        const normalizedSegments = segments.filter((segment) => segment.length > 1);
        const isClosed = !hasMissingData && normalizedSegments.length === 1;

        return {
            segments: normalizedSegments,
            isClosed
        };
    }
}