Overview
Camera synchronization is one of the most complex parts of Threebox. It ensures that Three.js objects appear correctly positioned and scaled as the Mapbox camera moves, rotates, and zooms.
The CameraSync class handles all camera transformations between Mapbox GL JS and Three.js coordinate systems.
The CameraSync Class
Initialization
From source (CameraSync.js:9-40):
function CameraSync ( map , camera , world ) {
this . map = map ;
this . camera = camera ;
this . active = true ;
// We control the camera completely
this . camera . matrixAutoUpdate = false ;
// Position the world group
this . world = world || new THREE . Group ();
this . world . position . x = this . world . position . y = WORLD_SIZE / 2 ;
this . world . matrixAutoUpdate = false ;
// Camera state
this . state = {
translateCenter: new THREE . Matrix4 ()
. makeTranslation ( WORLD_SIZE / 2 , - WORLD_SIZE / 2 , 0 ),
worldSizeRatio: TILE_SIZE / WORLD_SIZE ,
worldSize: TILE_SIZE * this . map . transform . scale
};
// Listen for map events
this . map
. on ( 'move' , () => { this . updateCamera (); })
. on ( 'resize' , () => { this . setupCamera (); });
this . setupCamera ();
}
Key Properties
Disabled because Threebox manually controls the camera matrix
Disabled because the world matrix is manually calculated each frame
Camera Setup
The setupCamera method calculates camera parameters from the Mapbox transform:
setupCamera : function () {
const t = this . map . transform ;
this . camera . aspect = t . width / t . height ;
this . halfFov = t . _fov / 2 ;
this . cameraToCenterDistance = 0.5 / Math . tan ( this . halfFov ) * t . height ;
const maxPitch = t . _maxPitch * Math . PI / 180 ;
this . acuteAngle = Math . PI / 2 - maxPitch ;
this . updateCamera ();
}
Camera Distance Calculation
The camera distance from the map center is calculated to match Mapbox’s perspective:
this . cameraToCenterDistance = 0.5 / Math . tan ( this . halfFov ) * t . height ;
This ensures the Three.js camera has the same field of view as Mapbox.
Camera Update
The updateCamera method is called on every map move event. It synchronizes:
Projection matrix (perspective or orthographic)
Camera world matrix (position and rotation)
World group matrix (scaling and translation)
Projection Matrix
Perspective Camera
From source (CameraSync.js:111):
this . camera . projectionMatrix = utils . makePerspectiveMatrix (
t . _fov ,
w / h ,
nearZ ,
farZ
);
The makePerspectiveMatrix function (utils.js:17-32):
makePerspectiveMatrix : function ( fovy , aspect , near , far ) {
var out = new THREE . Matrix4 ();
var f = 1.0 / Math . tan ( fovy / 2 ),
nf = 1 / ( near - far );
var newMatrix = [
f / aspect , 0 , 0 , 0 ,
0 , f , 0 , 0 ,
0 , 0 , ( far + near ) * nf , - 1 ,
0 , 0 , ( 2 * far * near ) * nf , 0
]
out . elements = newMatrix
return out ;
}
Orthographic Camera
From source (CameraSync.js:109):
this . camera . projectionMatrix = utils . makeOrthographicMatrix (
w / - 2 , w / 2 ,
h / 2 , h / - 2 ,
nearZ , farZ
);
Near and Far Plane Calculation
The near and far clipping planes are calculated based on pitch and field of view:
const nz = ( t . height / 50 ); // Min near z
const nearZ = Math . max ( nz * pitchAngle , nz );
const topHalfSurfaceDistance =
Math . sin ( this . halfFov ) * this . cameraToCenterDistance /
Math . sin ( Math . PI - groundAngle - this . halfFov );
const furthestDistance =
pitchAngle * topHalfSurfaceDistance + this . cameraToCenterDistance ;
const farZ = furthestDistance * 1.01 ;
This ensures that objects are clipped appropriately based on camera angle.
Mapbox v2.0+ Terrain Support
For Mapbox GL JS v2.0+ with terrain:
if ( this . map . tb . mapboxVersion >= 2.0 ) {
const pixelsPerMeter =
this . mercatorZfromAltitude ( 1 , t . center . lat ) * worldSize ;
const fovAboveCenter =
t . _fov * ( 0.5 + t . centerOffset . y / t . height );
// Adjust for minimum elevation
const minElevationInPixels =
t . elevation ? t . elevation . getMinElevationBelowMSL () * pixelsPerMeter : 0 ;
const cameraToSeaLevelDistance =
(( t . _camera . position [ 2 ] * worldSize ) - minElevationInPixels ) /
Math . cos ( t . _pitch );
const topHalfSurfaceDistance =
Math . sin ( fovAboveCenter ) * cameraToSeaLevelDistance /
Math . sin ( utils . clamp ( Math . PI - groundAngle - fovAboveCenter , 0.01 , Math . PI - 0.01 ));
furthestDistance =
pitchAngle * topHalfSurfaceDistance + cameraToSeaLevelDistance ;
const horizonDistance = cameraToSeaLevelDistance * ( 1 / t . _horizonShift );
farZ = Math . min ( furthestDistance * 1.01 , horizonDistance );
}
Camera World Matrix
The camera’s position and rotation are set via its world matrix:
let cameraWorldMatrix = this . calcCameraMatrix ( t . _pitch , t . angle );
// Adjust for terrain height
if ( t . elevation )
cameraWorldMatrix . elements [ 14 ] = t . _camera . position [ 2 ] * worldSize ;
this . camera . matrixWorld . copy ( cameraWorldMatrix );
calcCameraMatrix
From source (CameraSync.js:170-180):
calcCameraMatrix ( pitch , angle , trz ) {
const t = this . map . transform ;
const _pitch = ( pitch === undefined ) ? t . _pitch : pitch ;
const _angle = ( angle === undefined ) ? t . angle : angle ;
const _trz = ( trz === undefined ) ? this . cameraTranslateZ : trz ;
return new THREE . Matrix4 ()
. premultiply ( _trz )
. premultiply ( new THREE . Matrix4 (). makeRotationX ( _pitch ))
. premultiply ( new THREE . Matrix4 (). makeRotationZ ( _angle ));
}
This applies transformations in order:
Translate camera back from center
Rotate around X-axis (pitch)
Rotate around Z-axis (bearing)
World Matrix
The world group matrix handles:
Scaling objects based on zoom level
Translating the world based on map center
Rotating to match coordinate systems
From source (CameraSync.js:124-141):
let zoomPow = t . scale * this . state . worldSizeRatio ;
let scale = new THREE . Matrix4 ();
let translateMap = new THREE . Matrix4 ();
let rotateMap = new THREE . Matrix4 ();
scale . makeScale ( zoomPow , zoomPow , zoomPow );
let x = t . x || t . point . x ;
let y = t . y || t . point . y ;
translateMap . makeTranslation ( - x , y , 0 );
rotateMap . makeRotationZ ( Math . PI );
this . world . matrix = new THREE . Matrix4 ()
. premultiply ( rotateMap )
. premultiply ( this . state . translateCenter )
. premultiply ( scale )
. premultiply ( translateMap )
Translate by map position (-x, y, 0)
Scale by zoom level
Translate to center
Rotate 180° to flip Y-axis
This ensures Three.js objects align with Mapbox’s coordinate system.
Coordinate System Alignment
Mapbox uses a different coordinate system than Three.js:
Axis Mapbox Three.js X East → East → Y South ↓ North ↑ Z Up ↑ Up ↑
The 180° Z-rotation flips the Y-axis to match Mapbox’s south-down orientation.
Zoom Scaling
Objects scale with zoom level:
let zoomPow = t . scale * this . state . worldSizeRatio ;
scale . makeScale ( zoomPow , zoomPow , zoomPow );
Where:
t.scale = Math.pow(2, zoom)
worldSizeRatio = TILE_SIZE / WORLD_SIZE = 512 / 536870912
This ensures objects maintain consistent size relative to map features.
Fixed Zoom Objects
Some objects can maintain fixed size regardless of zoom:
// From Threebox.js:865-866
setObjectsScale : function () {
this . world . children
. filter ( o => ( o . fixedZoom != null ))
. forEach (( o ) => { o . setObjectScale ( this . map . transform . scale ); });
}
Called on zoom events to rescale fixedZoom objects.
Camera Synchronization Event
After each update, a CameraSynced event is fired:
this . map . fire ( 'CameraSynced' , {
detail: {
nearZ: nearZ ,
farZ: farZ ,
pitch: t . _pitch ,
angle: t . angle ,
furthestDistance: furthestDistance ,
cameraToCenterDistance: this . cameraToCenterDistance ,
t: this . map . transform ,
tbProjMatrix: this . camera . projectionMatrix . elements ,
tbWorldMatrix: this . world . matrix . elements
}
});
You can listen to this event:
map . on ( 'CameraSynced' , function ( e ) {
console . log ( 'Near:' , e . detail . nearZ , 'Far:' , e . detail . farZ );
console . log ( 'Pitch:' , e . detail . pitch , 'Bearing:' , e . detail . angle );
});
Raycasting
For mouse interactions, the camera’s projection matrix is used for raycasting:
// From Threebox.js:790-804
queryRenderedFeatures : function ( point ) {
let mouse = new THREE . Vector2 ();
// Scale to normalized device coordinates (-1 to +1)
mouse . x = ( point . x / this . map . transform . width ) * 2 - 1 ;
mouse . y = 1 - ( point . y / this . map . transform . height ) * 2 ;
this . raycaster . setFromCamera ( mouse , this . camera );
// Calculate intersecting objects
let intersects = this . raycaster . intersectObjects ( this . world . children , true );
return intersects
}
Camera Type Switching
You can switch between perspective and orthographic cameras:
// Switch to orthographic
tb . orthographic = true ;
// Switch to perspective
tb . orthographic = false ;
// Change FOV (perspective only)
tb . fov = 35 ;
From source (Threebox.js:599-620):
set orthographic ( value ) {
const h = this . map . getCanvas (). clientHeight ;
const w = this . map . getCanvas (). clientWidth ;
if ( value ) {
this . map . transform . fov = 0 ;
this . camera = new THREE . OrthographicCamera (
w / - 2 , w / 2 , h / 2 , h / - 2 , 0.1 , 1e21
);
} else {
this . map . transform . fov = this . fov ;
this . camera = new THREE . PerspectiveCamera (
this . map . transform . fov , w / h , 0.1 , 1e21
);
}
this . camera . layers . enable ( 0 );
this . camera . layers . enable ( 1 );
this . cameraSync = new CameraSync ( this . map , this . camera , this . world );
this . map . repaint = true ;
this . options . orthographic = value ;
}
Camera updates are optimized:
Matrix auto-update disabled : Manual control prevents redundant calculations
Event-driven updates : Only updates on move and resize events
Shared context : Uses Mapbox’s WebGL context
Common Issues
Objects appear in wrong position
Ensure coordinates are in [lng, lat] format, not [lat, lng]
Check that units option is set correctly (‘meters’ or ‘scene’)
Verify camera synchronization is running (check CameraSynced event)
Objects disappear at certain angles
Near/far plane clipping issue
Increase far plane or adjust object positions
Check pitch angle is within valid range
Raycasting not working
Ensure camera.aspect is updated on resize
Check that camera.matrixWorld is updated
Verify raycaster is using correct camera instance
Next Steps
Coordinate Systems Learn about coordinate conversions between lnglat, meters, and units
Threebox Instance Back to Threebox instance documentation