Source: modules/render/ThirdPersonOnGround.js

import * as Cesium from "cesium";
import cesiumMan from "@/assets/sedan.glb";

//import cesiumMan from "@/assets/cesium-man.glb";

/**
 * ThirdPersonOnGround
 * This class provides a third-person view controller for navigating a 3D scene in CesiumJS.
 * @class
 */
export class ThirdPersonOnGround {
    constructor(viewer) {
        this.viewer = viewer;
        this.canvas = viewer.canvas;
        this.isActive = false;
        this.handler = new Cesium.ScreenSpaceEventHandler();
        this.f = undefined;
        this.flags = {
            isActive: false,
            speedUp: false,
            speedDown: false,
            moveForward: false,
            moveBackward: false,
            fastMove: false,
            moveUp: false,
            moveDown: false,
            moveLeft: false,
            moveRight: false,
            moveRate: 1.0,
        };

        this.pysicalState = {
            isJumping: false,
            verticalVelocity: 0,
            gravity: -9.8 * 2.0,
            moveSpeed: 20.0,
            jumpSpeed: 10.0,
            lastUpdateTime: performance.now(),
        };
    }

    yUpToZUp() {
        return Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(-90)));
    }

    finalModelMatrix(modelMatrix) {
        return Cesium.Matrix4.multiply(
            modelMatrix,
            this.yUpToZUp(),
            new Cesium.Matrix4(),
        );
    }

    init() {
        let modelMatrix = Cesium.Matrix4.inverse(this.viewer.camera.viewMatrix, new Cesium.Matrix4());
        modelMatrix = this.finalModelMatrix(modelMatrix);
        console.log("modelMatrix", modelMatrix);

        const camera = this.viewer.camera;
        const position = camera.positionWC;
        const direction = camera.directionWC;

        const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
        const zAxis = Cesium.Matrix4.getColumn(enuTransform, 2, new Cesium.Cartesian3());

        Cesium.Model.fromGltfAsync({
            url: cesiumMan,
            modelMatrix: modelMatrix,
            scale: 1.0,
            shadows: Cesium.ShadowMode.DISABLED,
            minimumPixelSize: 16,
            allowPicking: false,
            show: true,
        }).then((model) => {
            console.log("added model", model);
            this.model = model;
            this.viewer.scene.primitives.add(model);
        });
    }

    toggle() {
        if (this.isActive) {
            this.deactivate();
            this.isActive = false;
        } else {
            this.activate(true);
            this.isActive = true;
        }
    }

    lookAtCamera() {
        if (!this.model || !this.viewer) {
            return;
        }

        const modelMatrix = this.model.modelMatrix;
        const targetCamera = this.viewer.camera;

        const targetPos = targetCamera.positionWC;
        const refPos = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3());

        if (Cesium.Cartesian3.equals(targetPos, refPos)) {
            //console.warn("Target camera position is the same as reference camera position. No adjustment needed.");
            return;
        }

        const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(targetPos);
        //const xAxis = Cesium.Matrix4.getColumn(enuTransform, 0, new Cesium.Cartesian3()); // east → right
        //const yAxis = Cesium.Matrix4.getColumn(enuTransform, 1, new Cesium.Cartesian3()); // north → up
        const zAxis = Cesium.Matrix4.getColumn(enuTransform, 2, new Cesium.Cartesian3()); // up → back (보정 필요)

        const direction = Cesium.Cartesian3.subtract(refPos, targetPos, new Cesium.Cartesian3());
        Cesium.Cartesian3.normalize(direction, direction);

        const right = Cesium.Cartesian3.cross(direction, zAxis, new Cesium.Cartesian3());
        Cesium.Cartesian3.normalize(right, right);

        const up = Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3());
        Cesium.Cartesian3.normalize(up, up);

        targetCamera.direction = direction;
        targetCamera.up = up;
        targetCamera.right = right;
    }

    preRenderEvent() {
        const viewer = this.viewer;
        const model = this.model;
        if (!model) {
            console.warn("Model not initialized. Call init() first.");
            return;
        }

        const now = performance.now();
        const dt = (now - this.pysicalState.lastUpdateTime) / 1000;
        this.pysicalState.lastUpdateTime = now;

        const camera = viewer.camera;

        const modelMatrix = model.modelMatrix;
        const position = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3());
        const rotation = Cesium.Matrix4.getRotation(modelMatrix, new Cesium.Matrix3());
        //let direction = Cesium.Matrix4.getColumn(modelMatrix, 2, new Cesium.Cartesian3());
        //let right = Cesium.Matrix4.getColumn(modelMatrix, 0, new Cesium.Cartesian3());

        const carto = Cesium.Cartographic.fromCartesian(position);

        // vertical position adjustment
        const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
        const zAxis = Cesium.Matrix4.getColumn(enuTransform, 2, new Cesium.Cartesian3());
        let direction = camera.directionWC;
        let right = camera.rightWC;

        // vector normalization
        Cesium.Cartesian3.normalize(direction, direction);
        Cesium.Cartesian3.normalize(right, right);

        // cross product to adjust right and direction vectors
        const tempRight = Cesium.Cartesian3.cross(zAxis, direction, new Cesium.Cartesian3());
        Cesium.Cartesian3.negate(tempRight, tempRight);
        Cesium.Cartesian3.normalize(right, right);
        const tempDirection = Cesium.Cartesian3.cross(zAxis, right, new Cesium.Cartesian3());
        Cesium.Cartesian3.normalize(direction, direction);

        direction = tempDirection;
        right = tempRight;

        const moveVec = new Cesium.Cartesian3();
        if (this.flags.moveForward) {
            Cesium.Cartesian3.add(moveVec, direction, moveVec);
        }
        if (this.flags.moveBackward) {
            Cesium.Cartesian3.subtract(moveVec, direction, moveVec);
        }
        if (this.flags.moveRight) {
            Cesium.Cartesian3.add(moveVec, right, moveVec);
        }
        if (this.flags.moveLeft) {
            Cesium.Cartesian3.subtract(moveVec, right, moveVec);
        }

        if (moveVec.x !== 0 && moveVec.y !== 0 && moveVec.z !== 0) {
            Cesium.Cartesian3.normalize(moveVec, moveVec);

            let speed = this.pysicalState.moveSpeed;
            if (this.flags.fastMove) {
                speed *= 2.0;
            }
            Cesium.Cartesian3.multiplyByScalar(moveVec, speed * dt, moveVec);
        }

        // gravity and jump handling
        this.pysicalState.verticalVelocity += this.pysicalState.gravity * dt;
        carto.height += this.pysicalState.verticalVelocity * dt;

        const groundHeight = viewer.scene.globe.getHeight(carto) ?? 0;
        const cameraHeight = groundHeight + 1.7;

        if (carto.height <= cameraHeight) {
            carto.height = cameraHeight;
            this.pysicalState.verticalVelocity = 0;
            this.pysicalState.isJumping = false;
        }

        // update camera position
        const newPos = Cesium.Cartesian3.fromRadians(
            carto.longitude,
            carto.latitude,
            carto.height,
        );
        Cesium.Cartesian3.add(newPos, moveVec, newPos);

        let modelRotationMatrix = rotation.clone();
        const subPos = Cesium.Cartesian3.subtract(newPos, position, new Cesium.Cartesian3());
        let directionLength = Cesium.Cartesian3.magnitude(subPos);

        if (!Cesium.Cartesian3.equals(newPos, position)) {
            let newDirection = subPos;
            Cesium.Cartesian3.normalize(newDirection, newDirection);

            let tempRight = Cesium.Cartesian3.cross(newDirection, zAxis, new Cesium.Cartesian3());
            Cesium.Cartesian3.normalize(tempRight, tempRight);

            newDirection = Cesium.Cartesian3.cross(zAxis, tempRight, new Cesium.Cartesian3());
            Cesium.Cartesian3.normalize(newDirection, newDirection);

            const newRight = Cesium.Cartesian3.cross(newDirection, zAxis, new Cesium.Cartesian3());
            Cesium.Cartesian3.normalize(newRight, newRight);

            const newUp = Cesium.Cartesian3.cross(newRight, newDirection, new Cesium.Cartesian3());
            Cesium.Cartesian3.normalize(newUp, newUp);

            /*let newRight = Cesium.Cartesian3.cross(direction, zAxis, new Cesium.Cartesian3());
            Cesium.Cartesian3.normalize(newRight, newRight);

            let newDirection = Cesium.Cartesian3.cross(zAxis, newRight, new Cesium.Cartesian3());
            Cesium.Cartesian3.normalize(newDirection, newDirection);*/

            modelRotationMatrix = Cesium.Matrix3.setColumn(modelRotationMatrix, 0, newDirection, modelRotationMatrix);
            modelRotationMatrix = Cesium.Matrix3.setColumn(modelRotationMatrix, 1, newRight, modelRotationMatrix);
            modelRotationMatrix = Cesium.Matrix3.setColumn(modelRotationMatrix, 2, zAxis, modelRotationMatrix);
        }

        let newModelMatrix = Cesium.Matrix4.IDENTITY.clone();
        newModelMatrix = Cesium.Matrix4.multiplyByMatrix3(newModelMatrix, modelRotationMatrix, newModelMatrix);
        newModelMatrix = Cesium.Matrix4.setColumn(newModelMatrix, 3, new Cesium.Cartesian4(newPos.x, newPos.y, newPos.z, 1.0), newModelMatrix);

        model.modelMatrix = newModelMatrix;

        this.lookAtCamera();
    }

    /**
     * Activate the third-person view controller
     * @method
     * @description This method activates the third-person view controller, enabling keyboard and mouse controls for navigation.
     * @param lockMode {boolean} - If true, enables pointer lock mode for mouse movement.
     */
    activate(lockMode = false) {
        if (!this.model) {
            this.init();
        }
        const scene = this.viewer.scene;
        scene.screenSpaceCameraController.enableRotate = false;
        scene.screenSpaceCameraController.enableTranslate = false;
        scene.screenSpaceCameraController.enableZoom = false;
        scene.screenSpaceCameraController.enableTilt = false;
        scene.screenSpaceCameraController.enableLook = false;

        this.viewer.clock.onTick.addEventListener(this.keyboardEventHandler, this);
        this.viewer.scene.preRender.addEventListener(this.preRenderEvent, this);
        document.addEventListener("keydown", this.keyDownEventHandler, false);
        document.addEventListener("keyup", this.keyUpEventHandler, false);

        /*const viewer = this.viewer;


        if (lockMode) {
            viewer.canvas.addEventListener("click", this.mouseClickWithPointerLockHandler, false);
            viewer.canvas.addEventListener("mousemove", this.mouseMoveWithPointerLockHandler, false);
        } else {
            this.handler.setInputAction(this.mouseMoveHandler, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
            this.handler.setInputAction(this.mouseMoveHandler, Cesium.ScreenSpaceEventType.MOUSE_MOVE, Cesium.KeyboardEventModifier.SHIFT);
        }*/
    }

    /**
     * Deactivate the first-person view controller
     * @method
     * @description This method deactivates the first-person view controller, restoring the default camera controls and removing event listeners.
     */
    deactivate() {
        const scene = this.viewer.scene;
        scene.screenSpaceCameraController.enableRotate = true;
        scene.screenSpaceCameraController.enableTranslate = true;
        scene.screenSpaceCameraController.enableZoom = true;
        scene.screenSpaceCameraController.enableTilt = true;
        scene.screenSpaceCameraController.enableLook = true;

        this.viewer.clock.onTick.removeEventListener(this.keyboardEventHandler);
        this.viewer.scene.preRender.addEventListener(this.preRenderEvent, this);
        document.removeEventListener("keydown", this.keyDownEventHandler, false);
        document.removeEventListener("keyup", this.keyUpEventHandler, false);

        /*this.viewer.canvas.removeEventListener("click", this.mouseClickWithPointerLockHandler, false);
        this.viewer.canvas.removeEventListener("mousemove", this.mouseMoveWithPointerLockHandler, false);

        this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
        this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE, Cesium.KeyboardEventModifier.SHIFT);*/

        this.flags = {
            isActive: false,
            speedUp: false,
            speedDown: false,
            moveForward: false,
            moveBackward: false,
            fastMove: false,
            moveUp: false,
            moveDown: false,
            moveLeft: false,
            moveRight: false,
            moveRate: 1.0,
        };

        this.pysicalState = {
            isJumping: false,
            verticalVelocity: 0,
            gravity: -9.8 * 2.0,
            moveSpeed: 20.0,
            jumpSpeed: 10.0,
            lastUpdateTime: performance.now(),
        };

        this.viewer.scene.primitives.remove(this.model);
        this.model = undefined;
    }

    keyboardEventHandler = () => {
        const flags = this.flags;
        if (flags.speedUp) {
            flags.moveRate += 0.025;
        }
        if (flags.speedDown) {
            flags.moveRate -= 0.025;
            if (flags.moveRate < 0.01) {
                flags.moveRate = 0.01;
            }
        }
    };

    keyDownEventHandler = (e) => {
        const flags = this.flags;
        const flagName = this.getFlagForKeyCode(e.code);
        if (typeof flagName !== "undefined") {
            flags[flagName] = true;
        }

        if (e.key === " " && !this.pysicalState.isJumping) {
            this.pysicalState.verticalVelocity = this.pysicalState.jumpSpeed;
            this.pysicalState.isJumping = true;
        }
        if (e.key === "Shift") {
            this.flags.fastMove = true;
        }
    };

    keyUpEventHandler = (event) => {
        const flags = this.flags;
        const flagName = this.getFlagForKeyCode(event.code);
        if (typeof flagName !== "undefined") {
            flags[flagName] = false;
        }
        if (event.key === "Shift") {
            this.flags.fastMove = false;
        }
    };

    mouseMoveHandler(moveEvent) {
        const viewer = this.viewer;
        if (this.flags.mouseStatus) {
            const intensity = 2.0;
            const width = viewer.canvas.clientWidth;
            const height = viewer.canvas.clientHeight;
            const x = moveEvent.endPosition.x - moveEvent.startPosition.x;
            const y = moveEvent.endPosition.y - moveEvent.startPosition.y;
            const angleX = (-x / width) * intensity;
            const angleY = (y / height) * intensity;

            const camera = viewer.camera;
            camera.setView({
                destination: camera.position,
                orientation: {
                    heading: camera.heading + angleX,
                    pitch: camera.pitch + angleY,
                    roll: camera.roll,
                },
            });
        }
    };

    mouseClickWithPointerLockHandler = () => {
        const viewer = this.viewer;
        console.log("Requesting pointer lock");
        if (document.pointerLockElement !== viewer.canvas) {
            viewer.canvas.requestPointerLock();
        }
    };

    mouseMoveWithPointerLockHandler = (moveEvent) => {
        if (document.pointerLockElement === this.viewer.canvas) {
            const viewer = this.viewer;
            const intensity = 2.0;
            const width = viewer.canvas.clientWidth;
            const height = viewer.canvas.clientHeight;
            const x = -moveEvent.movementX;
            const y = -moveEvent.movementY;
            const angleX = (-x / width) * intensity;
            const angleY = (y / height) * intensity;

            const camera = viewer.camera;
            camera.setView({
                destination: camera.position,
                orientation: {
                    heading: camera.heading + angleX,
                    pitch: camera.pitch + angleY,
                    roll: camera.roll,
                },
            });
        }
    };

    getFlagForKeyCode(code) {
        if (code === "KeyO") {
            return "speedDown";
        } else if (code === "KeyP") {
            return "speedUp";
        } else if (code === "KeyW") {
            return "moveForward";
        } else if (code === "KeyS") {
            return "moveBackward";
        } else if (code === "KeyQ") {
            return "moveUp";
        } else if (code === "KeyE") {
            return "moveDown";
        } else if (code === "KeyD") {
            return "moveRight";
        } else if (code === "KeyA") {
            return "moveLeft";
        } else {
            return undefined;
        }
    }

    clear() {
        if (this.viewer) {
            this.viewer.destroy();
            this.viewer = undefined;
        }
    }
}