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.
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());
});
},
To sync MapLibre's map center with a Three.js camera position in the custom layer, you need to:
map.getCenter()
to Mercator coordinatesconst { 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).
lng/lat
to local Cartesian coordinatesThe 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)
.
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 };
}