three.jscoordinatesmaplibre-gl

Three.js camera position from MapLibre's Custom Layer?


I'm integrating a custom 3D layer using Three.js into a MapLibre GL JS map. However, the Three.js camera.position remains fixed at (0, 0, 0) even when the map is panned or rotated.

This is because MapLibre simulates camera movement by transforming the scene, not by moving the actual camera. As a result, map.getCenter() reflects the map's visual center in [lng, lat], but not the real camera position in Three.js space.

I want to calculate the true camera position in Three.js world coordinates that matches the current MapLibre view, accounting for center, pitch, and bearing.

camera illusion

Here’s a minimal example of the setup based on the Three.js Custom Layer to Maplibre documentation:

    onAdd(map, gl) {
        this.map = map;
        this.camera = new THREE.Camera();
        this.scene = new THREE.Scene();

        map.on('move', () => {

            // camera.position is always (0, 0, 0)
            console.log(this.camera.position);

            // map center in lng/lat
            console.log(this.map.getCenter());
        });
    },

Solution

  • To sync MapLibre's map center with a Three.js camera position in the custom layer, you need to:

    1. Convert map.getCenter() to Mercator coordinates
    const { lng, lat } = map.getCenter(); 
    const coord = maplibregl.MercatorCoordinate.fromLngLat([lng, lat], 0);
    

    Note: MercatorCoordinate values are normalized from the projection with (0,0) being at the equator and prime meridian (Null Island).

    1. Convert lng/lat to local Cartesian coordinates

    The function below converts a lng/lat coordinate to Three.js world coordinates relative to your modelOrigin.

    function lngLatToLocalCartesian(lng: number, lat: number) {
    
        const coord = maplibregl.MercatorCoordinate.fromLngLat([lng, lat], 0);
    
        return {
            x: (coord.x - modelTransform.translateX) / modelTransform.scale,
            y: 0,
            z: (coord.y - modelTransform.translateY) / modelTransform.scale
        };
    }
    

    This gives you the local (x, z) position on the map, where modelOrigin is (0, 0).

    1. Compute the camera position using map view parameters

    Using MapLibre's pitch, bearing, and zoom, you can calculate the camera's full 3D position in the scene.

    function getCameraPosition(map: maplibregl.Map) {
    
        const { lng, lat } = map.getCenter();
        let worldPosition = lngLatToLocalCartesian(lng, lat);
    
        const pitchInRadians = map.getPitch() * (Math.PI / 180);
        const bearingInRadians = map.getBearing() * (Math.PI / 180);
        
        // distance from camera to map center in pixels
        const d = map.transform.cameraToCenterDistance;
        
        // horizontal distance and height in pixels
        const r = d * Math.sin(pitchInRadians);
        const altitudePixels = Math.cos(pitchInRadians) * d;
        
        // convert pixels to world units
        const pixelsToWorldUnits = 1 / (map.transform.worldSize * modelTransform.scale);
        
        // compute camera displacement in world units
        const dxWorld = -r * Math.sin(bearingInRadians) * pixelsToWorldUnits;
        const dzWorld = r * Math.cos(bearingInRadians) * pixelsToWorldUnits;
        const altitudeWorld = altitudePixels * pixelsToWorldUnits;
    
        // add Y (altitude) to world position
        worldPosition.y = altitudeWorld;
    
        // final camera position
        const viewPosition = new THREE.Vector3(
            worldPosition.x + dxWorld,
            worldPosition.y,
            worldPosition.z + dzWorld
        );
    
        return { viewPosition, worldPosition };
    }