layers_ProjectedLayer.js

import { Layer } from './Layer.js';
import * as THREE from 'three';
import { llhToCartesianFastSFCT } from '../GeoUtils.js';

const cartesianLocation = new THREE.Vector3();
const Up = new THREE.Vector3();
const East = new THREE.Vector3();
const North = new THREE.Vector3();
const globalNorth = new THREE.Vector3(0,0,1);
const quaternionToEarthNormalOrientation = new THREE.Quaternion();
const quaternionSelfRotation = new THREE.Quaternion();
const rotationMatrix = new THREE.Matrix4();
const rotation = new THREE.Euler(0,0,0, "ZYX");
/**
 * Projected layers are draped onto other data from the given perspective
 */
class ProjectedLayer extends Layer {
    /**
     * construct a projected layer that will project a three.js onto other loaded geospatial data from the given perspective.
     * @param {Object} properties 
     * @param {String|Number} properties.id layer id should be unique
     * @param {String} properties.name the name can be anything you want and is intended for labeling
     * @param {THREE.Texture} [properties.texture = null] a three.js texture
     * @param {Number} [properties.fov = 20] frustum vertical field of view of the video camera.
     * @param {Number} [properties.far = 3000] maximum distance for projecting the texture in meters 
     * @param {THREE.Vector3} [properties.cameraLLH = new THREE.Vector3(0,0,0)] the position of the camera in longitude (degrees) latitude (degrees) height (meters)
     * @param {Number} [properties.yaw = 0] - Yaw angle in degrees
     * @param {Number} [properties.pitch = -90] - Pitch angle in degrees.
     * @param {Number} [properties.roll = 0] - Roll angle in degrees.
     * @param {Boolean} [properties.depthTest = true] depth test prevents drawing the projected texture behind occluders but the precision is limitted at long distances
     * @param {Boolean} [properties.visible = true] layer will be rendered if true (true by default)
     * @param {Boolean} [showFrustum = true] show the projection camera frustum
     * @param {Boolean} [chromaKeying = false] use chroma key (green screen)
     * @param {THREE.Vector3} [chromaKey = new THREE.Vector3(0.5,1.0,0.5)] chroma key color
     * @param {Number} [chromaKeyTolerance = 0.5] tolerance for chroma key (0 to sqrt(3))
     */
    constructor(properties) {
        super(properties);
        this.isProjectedLayer = true;
        this.depthTest = properties.depthTest != undefined ? properties.depthTest : true;
        
        this._cameraLLH = properties.cameraLLH!=undefined ? properties.cameraLLH : new THREE.Vector3(0, 0, 0);
        this._yaw = properties.yaw!=undefined ? properties.yaw : 0;
        this._pitch = properties.pitch!=undefined ? properties.pitch : -90;
        this._roll = properties.roll!=undefined ? properties.roll : 0;
        this._fov = properties.fov!=undefined ? properties.fov : 20;
        this._far = properties.far!=undefined ? properties.far : 3000;

        this.showFrustum = properties.showFrustum!=undefined?properties.showFrustum:true;

        this.chromaKeying = properties.chromaKeying != undefined? properties.chromaKeying:false;
        this.chromaKey = properties.chromaKey != undefined? properties.chromaKey:new THREE.Vector3(0.5,1.0,0.5);
        this.chromaKeyTolerance = properties.chromaKeyTolerance != undefined? properties.chromaKeyTolerance:0.5;

        if(properties.texture){
            this.setTexture(properties.texture);
        }
    }

    /**
     * Set a texture to be projected
     */
    setTexture(texture) {
        const self = this;
        self.texture = texture;
        self.isInitialized = false;
        if(this.renderTarget) this.renderTarget.dispose();
        this.renderTarget = undefined;
        
        // Check if the texture is loaded
        if (texture.image && texture.image.width && texture.image.height) {
            self.width = texture.image.width;
            self.height = texture.image.height;
            self._initCameraAndTarget();
        }
    }

    isReady(){
        if(this.isInitialized) return true;
        if(!this.renderTarget && this.texture){
            if (this.texture.image && this.texture.image.width > 0 && this.texture.image.height > 0) {
                this.width = this.texture.image.width;
                this.height = this.texture.image.height;
                this._initCameraAndTarget();
                if(this.isInitialized) return true;
            }
            if (this.texture.image && this.texture.image.videoWidth > 0 && this.texture.image.videoHeight > 0) {
                this.width = this.texture.image.videoWidth;
                this.height = this.texture.image.videoHeight;
                this._initCameraAndTarget();
                if(this.isInitialized) return true;
            }
        }
        return false;
    }

    _initCameraAndTarget() {
        this.projectionCamera = new THREE.PerspectiveCamera(this._fov, this.width / this.height, 0.1, 3000);
        this.projectionRenderCamera = this.projectionCamera.clone();
        this.setCameraFromLLHYawPitchRollFov(this._cameraLLH, this._yaw, this._pitch, this._roll, this._fov, this._far);
        if(this.renderTarget) this.renderTarget.dispose();
        this.renderTarget = new THREE.WebGLRenderTarget(Math.min(1080,this.width), Math.min(1080,this.height));
        this.renderTarget.texture.format = THREE.RGBAFormat;
        this.renderTarget.texture.type = THREE.FloatType;
        this.renderTarget.texture.colorSpace = THREE.NoColorSpace;
        this.renderTarget.texture.minFilter = THREE.NearestFilter;
        this.renderTarget.texture.magFilter = THREE.NearestFilter;
        this.renderTarget.texture.generateMipmaps = false;
        this.renderTarget.texture.premultiplyAlpha = false;
        this.renderTarget.stencilBuffer = false;
        this.renderTarget.depthBuffer = true;
        this.renderTarget.depthTexture = new THREE.DepthTexture();
        this.renderTarget.depthTexture.format = THREE.DepthFormat;
        this.renderTarget.depthTexture.type = THREE.FloatType;
        if(this.map){
            this._initWithMap();
        }
    }

    _initWithMap(){
        if(this.map && this.renderTarget){
            if(this.showFrustum){
                if(this.helper){
                    this.map.scene.remove(this.helper);
                    this.helper.traverse(o=>{
                        if(o.dispose) o.dispose();
                        if(o.material) o.material.dispose();
                    })
                }
                this.helper = new THREE.CameraHelper(this.projectionCamera);
                this.helper.layers.set(31);
                this.map.scene.add(this.helper);
            }
            this.isInitialized = true;
        }
        
    }

    _init(map) {
        this.map = map;
        this._initWithMap();
    }

    /**
  * Sets the video camera's position and orientation based on Longitude, Latitude, Height, Yaw, Pitch, Roll, and FOV.
  *
  * @param {THREE.Vector3} llh - A Vector3 where:
  *   - x = Longitude in degrees
  *   - y = Latitude in degrees
  *   - z = Height in meters
  * @param {number} yaw - Yaw angle in degrees. (0 points north ccw rotation)
  * @param {number} pitch - Pitch angle in degrees (-90 to 90)
  * @param {number} roll - Roll angle in degrees.
  *   - Rotation around the Forward vector (local Y-axis).
  * @param {number} fov - The camera's vertical field of view in degrees.
  * @param {number} far - The max distance to project the texture.
  */
    setCameraFromLLHYawPitchRollFov(llh, yaw, pitch, roll, fov, far) {
        if(llh)this._cameraLLH.copy(llh);
        if(yaw)this._yaw = yaw;
        if(pitch)this._pitch = pitch;
        if(roll)this._roll = roll;
        if(fov)this._fov = fov;
        if(far)this._far = far;
        if(!this.projectionCamera) return;

        
        rotation.set(
            pitch*0.0174533, yaw*0.0174533, roll*0.0174533, "ZYX");

        cartesianLocation.set(llh.x, llh.y, llh.z);
        llhToCartesianFastSFCT(cartesianLocation, false); // Convert LLH to Cartesian in-place

        Up.copy(cartesianLocation).normalize();
        East.crossVectors(globalNorth, Up).normalize();
        if (East.lengthSq() === 0) {
            East.set(1, 0, 0);
        }
        North.crossVectors(East, Up).normalize();

        
        rotationMatrix.makeBasis(East, Up, North);

        quaternionToEarthNormalOrientation.setFromRotationMatrix(rotationMatrix);

        quaternionSelfRotation.setFromEuler(rotation);
        this.projectionCamera.quaternion.copy(quaternionToEarthNormalOrientation).multiply(quaternionSelfRotation);
        this.projectionCamera.position.copy(cartesianLocation);
        

        // Step 12: Set FOV and update camera matrices
        this.projectionCamera.fov = fov;
        this.projectionCamera.far = this._far;
        this.projectionCamera.matrixWorldNeedsUpdate = true;
        this.projectionCamera.updateMatrix();
        this.projectionCamera.updateMatrixWorld(true);
        this.projectionCamera.updateProjectionMatrix();
    }

    
}
export { ProjectedLayer }