Skip to main content

Overview

Threebox handles three different coordinate systems:
  1. Geographic coordinates (longitude/latitude)
  2. Meters (real-world metric units)
  3. World units (Three.js scene units)
Understanding how to convert between these systems is essential for positioning and scaling 3D objects correctly.

Coordinate System Types

1. Geographic Coordinates (lnglat)

Format: [longitude, latitude, altitude]
  • Longitude: -180 to 180 (west to east)
  • Latitude: -90 to 90 (south to north)
  • Altitude: meters above sea level (optional)
const coords = [-122.4194, 37.7749, 0]; // San Francisco

2. Meters

Real-world metric measurements for object dimensions:
const building = {
  width: 50,  // meters
  length: 30, // meters
  height: 100 // meters
};

3. World Units

Three.js scene coordinates using Mercator projection:
  • Based on Spherical Mercator projection
  • World size: 536870912 units (2^29)
  • Centered at [0, 0]
const worldPos = new THREE.Vector3(x, y, z);

Key Constants

From source (constants.js):
const WORLD_SIZE = 1024000;
const MERCATOR_A = 6378137.0; // WGS84 ellipsoid semi-major axis
const DEG2RAD = Math.PI / 180;
const PROJECTION_WORLD_SIZE = WORLD_SIZE / (MERCATOR_A * DEG2RAD * 2);
const EARTH_CIRCUMFERENCE = 40075017; // meters at equator

Converting Coordinates

lnglat → World Units

Use projectToWorld(coords) to convert geographic coordinates to Three.js world coordinates. From source (utils.js:98-118):
projectToWorld: function (coords) {
  // Spherical Mercator forward projection
  var projected = [
    -MERCATOR_A * DEG2RAD * coords[0] * PROJECTION_WORLD_SIZE,
    -MERCATOR_A * Math.log(
      Math.tan((Math.PI * 0.25) + (0.5 * DEG2RAD * coords[1]))
    ) * PROJECTION_WORLD_SIZE
  ];
  
  // Z dimension (altitude)
  if (!coords[2]) {
    projected.push(0)
  } else {
    var pixelsPerMeter = this.projectedUnitsPerMeter(coords[1]);
    projected.push(coords[2] * pixelsPerMeter);
  }
  
  var result = new THREE.Vector3(projected[0], projected[1], projected[2]);
  return result;
}
Example:
const lnglat = [-122.4194, 37.7749, 10];
const worldPos = tb.projectToWorld(lnglat);
// Returns THREE.Vector3(x, y, z)

World Units → lnglat

Use unprojectFromWorld(worldUnits) to convert Three.js world coordinates back to geographic coordinates. From source (utils.js:151-166):
unprojectFromWorld: function (worldUnits) {
  var unprojected = [
    -worldUnits.x / (MERCATOR_A * DEG2RAD * PROJECTION_WORLD_SIZE),
    2 * (Math.atan(Math.exp(
      worldUnits.y / (PROJECTION_WORLD_SIZE * (-MERCATOR_A))
    )) - Math.PI / 4) / DEG2RAD
  ];
  
  var pixelsPerMeter = this.projectedUnitsPerMeter(unprojected[1]);
  
  // Z dimension (altitude in meters)
  var height = worldUnits.z || 0;
  unprojected.push(height / pixelsPerMeter);
  
  return unprojected;
}
Example:
const worldPos = new THREE.Vector3(1000, 2000, 100);
const lnglat = tb.unprojectFromWorld(worldPos);
// Returns [lng, lat, altitude]

Units Per Meter Scaling

Projected Units Per Meter

Convert meters to world units at a specific latitude:
projectedUnitsPerMeter: function (latitude) {
  return Math.abs(
    WORLD_SIZE / 
    Math.cos(DEG2RAD * latitude) / 
    EARTH_CIRCUMFERENCE
  );
}
Why latitude matters: The Mercator projection stretches as you move away from the equator. A meter at the poles represents more projection units than at the equator. Example:
// At equator (lat = 0)
const unitsPerMeterEquator = tb.projectedUnitsPerMeter(0);
// ≈ 25.56

// At San Francisco (lat = 37.7749)
const unitsPerMeterSF = tb.projectedUnitsPerMeter(37.7749);
// ≈ 32.39 (larger because of Mercator distortion)

// Convert 100 meters to world units in SF
const worldUnits = 100 * unitsPerMeterSF;

Scaling Vertices to Meters

From source (utils.js:132-141):
_scaleVerticesToMeters: function (centerLatLng, vertices) {
  var pixelsPerMeter = this.projectedUnitsPerMeter(centerLatLng[1]);
  var centerProjected = this.projectToWorld(centerLatLng);
  
  for (var i = 0; i < vertices.length; i++) {
    vertices[i].multiplyScalar(pixelsPerMeter);
  }
  
  return vertices;
}
This scales Three.js geometry vertices from meters to world units at a specific location.

Object Positioning

Using ‘meters’ Units

When creating objects with units: 'meters', Threebox automatically handles scaling:
tb.loadObj({
  obj: '/models/building.glb',
  type: 'gltf',
  scale: 1,
  units: 'meters', // Object dimensions in real-world meters
  rotation: { x: 90, y: 0, z: 0 }
}, function (model) {
  model.setCoords([-122.4194, 37.7749, 0]);
  tb.add(model);
});
Threebox will:
  1. Convert lnglat to world units
  2. Scale the object based on latitude
  3. Update scale when zooming to maintain real-world size

Using ‘scene’ Units

With units: 'scene', objects use raw Three.js units without latitude scaling:
const obj = tb.Object3D({
  obj: mesh,
  units: 'scene' // No automatic scaling
});
Useful for:
  • Non-geographic objects
  • Custom coordinate systems
  • Objects that shouldn’t scale with latitude

Altitude Handling

Altitude in Meters

Altitudes are always specified in meters above sea level:
// Place object 50 meters above ground
model.setCoords([-122.4194, 37.7749, 50]);

Mercator Z from Altitude

From source (utils.js:128-130):
mercatorZfromAltitude: function (altitude, lat) {
  return altitude / this._circumferenceAtLatitude(lat);
}
Where:
_circumferenceAtLatitude: function (latitude) {
  return EARTH_CIRCUMFERENCE * Math.cos(latitude * Math.PI / 180);
}
This converts altitude in meters to Mercator Z coordinate.

Positioning Objects

setCoords Method

All Threebox objects have a setCoords method:
obj.setCoords([lng, lat, altitude]);
This:
  1. Converts lnglat to world units
  2. Positions the object in the Three.js scene
  3. Stores coordinates in obj.coordinates

Getting Object Position

// Get geographic coordinates
const coords = obj.coordinates;
// [lng, lat, altitude]

// Get Three.js world position
const worldPos = obj.position;
// THREE.Vector3(x, y, z)

Feature Center Calculation

For GeoJSON features, calculate the center point: From source (utils.js:188-214):
getFeatureCenter: function (feature, model, level) {
  let center = [];
  let latitude = 0;
  let longitude = 0;
  let height = 0;
  
  let coordinates = [...feature.geometry.coordinates[0]];
  
  if (feature.geometry.type === "Point") {
    center = [...coordinates[0]];
  } else {
    // For Polygon/MultiPolygon
    if (feature.geometry.type === "MultiPolygon") 
      coordinates = coordinates[0];
    
    // Remove duplicate first/last coordinate
    coordinates.splice(-1, 1);
    
    coordinates.forEach(function (c) {
      latitude += c[0];
      longitude += c[1];
    });
    
    center = [
      latitude / coordinates.length, 
      longitude / coordinates.length
    ];
  }
  
  height = this.getObjectHeightOnFloor(feature, model, level);
  (center.length < 3 ? center.push(height) : center[2] = height);
  
  return center;
}
Example:
const feature = {
  type: 'Feature',
  geometry: {
    type: 'Polygon',
    coordinates: [[...]]
  },
  properties: {
    height: 50,
    base_height: 0
  }
};

const center = tb.getFeatureCenter(feature);
// Returns [lng, lat, altitude]

Object Height on Floor

Calculate altitude for objects on building levels: From source (utils.js:216-225):
getObjectHeightOnFloor: function (feature, obj, level = feature.properties.level || 0) {
  let floorHeightMin = (level * (feature.properties.levelHeight || 0));
  let base = (feature.properties.base_height || feature.properties.min_height || 0);
  let height = ((obj && obj.model) ? 0 : (feature.properties.height - base));
  let objectHeight = height + base;
  let modelHeightFloor = floorHeightMin + objectHeight;
  return modelHeightFloor;
}
Example:
const altitude = tb.getObjectHeightOnFloor(feature, model, 2);
// Returns altitude in meters for level 2

Coordinate System Conversions Summary

FromToMethod
lnglatWorld unitstb.projectToWorld(coords)
World unitslnglattb.unprojectFromWorld(vector3)
MetersWorld unitsmeters * tb.projectedUnitsPerMeter(lat)
World unitsMetersworldUnits / tb.projectedUnitsPerMeter(lat)
AltitudeMercator Ztb.utils.mercatorZfromAltitude(altitude, lat)

Practical Examples

Example 1: Place Object at Address

// Geocode address to lnglat (using external service)
const address = "Golden Gate Bridge, San Francisco";
const lnglat = [-122.4783, 37.8199];
const altitude = 67; // meters above sea level

// Load and position model
tb.loadObj({
  obj: '/models/landmark.glb',
  type: 'gltf',
  scale: 1,
  units: 'meters'
}, function (model) {
  model.setCoords([...lnglat, altitude]);
  tb.add(model);
});

Example 2: Create Grid of Objects

const centerLat = 37.7749;
const centerLng = -122.4194;
const spacing = 100; // meters

for (let x = -2; x <= 2; x++) {
  for (let y = -2; y <= 2; y++) {
    // Calculate offset in degrees (approximate)
    const latOffset = (y * spacing) / 111320; // degrees per meter lat
    const lngOffset = (x * spacing) / (111320 * Math.cos(centerLat * Math.PI / 180));
    
    const position = [
      centerLng + lngOffset,
      centerLat + latOffset,
      0
    ];
    
    // Create object at position
    const cube = createCube();
    cube.setCoords(position);
    tb.add(cube);
  }
}

Example 3: Convert Screen to World Coordinates

map.on('click', function(e) {
  // e.point = screen coordinates {x, y}
  // e.lngLat = geographic coordinates
  
  // Convert to world units
  const worldPos = tb.projectToWorld([
    e.lngLat.lng,
    e.lngLat.lat,
    0
  ]);
  
  console.log('Screen:', e.point);
  console.log('Geographic:', e.lngLat);
  console.log('World:', worldPos);
});

Example 4: Measure Distance Between Objects

const obj1 = model1.coordinates; // [lng, lat, alt]
const obj2 = model2.coordinates;

// Convert to world units
const world1 = tb.projectToWorld(obj1);
const world2 = tb.projectToWorld(obj2);

// Calculate distance in world units
const worldDistance = world1.distanceTo(world2);

// Convert back to meters
const avgLat = (obj1[1] + obj2[1]) / 2;
const pixelsPerMeter = tb.projectedUnitsPerMeter(avgLat);
const metersDistance = worldDistance / pixelsPerMeter;

console.log(`Distance: ${metersDistance.toFixed(2)} meters`);

Common Issues

Objects appear too large/small
  • Check units option is set correctly (‘meters’ vs ‘scene’)
  • Verify scale parameter
  • Ensure coordinates are [lng, lat], not [lat, lng]
Objects distorted at high latitudes
  • Mercator projection stretches near poles
  • Use units: 'meters' for automatic compensation
  • Consider alternative projection for polar regions
Altitude doesn’t match expected height
  • Altitude is meters above sea level, not ground level
  • Account for base_height in features
  • Terrain elevation affects perceived altitude

Best Practices

  1. Always use meters for real-world objects
    units: 'meters' // Recommended for geographic accuracy
    
  2. Store original coordinates
    obj.userData.originalCoords = [lng, lat, alt];
    
  3. Use helper methods
    // Don't manually calculate
    const world = tb.projectToWorld(lnglat); // Use helper
    
  4. Account for latitude scaling
    const scale = tb.projectedUnitsPerMeter(latitude);
    

Next Steps

Threebox Instance

Learn about Threebox initialization and configuration

Custom Layers

Integrate Threebox with Mapbox custom layers

Camera Synchronization

Understand camera synchronization between Three.js and Mapbox

Build docs developers (and LLMs) love