Skip to main content

Overview

Threebox works by adding a Three.js scene to Mapbox GL JS through the CustomLayerInterface. This interface allows you to render custom 3D content using WebGL directly on the map.

CustomLayerInterface

Mapbox GL JS custom layers require three key methods:
  • id: Unique layer identifier
  • type: Must be 'custom'
  • renderingMode: Should be '3d' for Threebox
  • onAdd(map, gl): Initialize resources when layer is added
  • render(gl, matrix): Called on every frame to render content
  • onRemove(map, gl) (optional): Clean up resources when layer is removed

Basic Implementation

map.addLayer({
  id: 'custom_layer',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function (map, gl) {
    // Create Threebox instance
    window.tb = new Threebox(
      map,
      gl,
      { defaultLights: true }
    );
    
    // Add 3D objects to the scene
    var geometry = new THREE.BoxGeometry(30, 60, 120);
    let cube = new THREE.Mesh(
      geometry, 
      new THREE.MeshPhongMaterial({ color: 0x660000 })
    );
    cube = tb.Object3D({ obj: cube, units: 'meters' });
    cube.setCoords([-122.4194, 37.7749]);
    tb.add(cube);
  },
  
  render: function (gl, matrix) {
    // Update and render the Threebox scene
    tb.update();
  }
});

The onAdd Method

The onAdd method is called when the layer is added to the map. This is where you:
  1. Create the Threebox instance
  2. Load 3D models
  3. Add objects to the scene
  4. Set up event listeners

Example with 3D Model Loading

onAdd: function (map, gl) {
  window.tb = new Threebox(
    map,
    gl,
    { 
      defaultLights: true,
      enableSelectingObjects: true,
      enableDraggingObjects: true
    }
  );
  
  // Load a GLTF model
  tb.loadObj({
    obj: '/models/soldier/soldier.glb',
    type: 'gltf',
    scale: 1,
    units: 'meters',
    rotation: { x: 90, y: 0, z: 0 }
  }, function (model) {
    model.setCoords([-122.4194, 37.7749]);
    
    // Add event listeners
    model.addEventListener('SelectedChange', onSelectedChange, false);
    model.addEventListener('ObjectDragged', onObjectDragged, false);
    
    tb.add(model);
  });
}

The render Method

The render method is called every frame. It must call tb.update() to:
  1. Update animations via AnimationManager
  2. Reset renderer state
  3. Render the Three.js scene: renderer.render(scene, camera)
  4. Render CSS2D labels
  5. Trigger repaint if passiveRendering: false
render: function (gl, matrix) {
  tb.update();
}

What tb.update() Does

From the source code (Threebox.js:897-915):
update: function () {
  if (this.map.repaint) this.map.repaint = false
  
  var timestamp = Date.now();
  
  // Update any animations
  this.objects.animationManager.update(timestamp);
  
  this.updateLightHelper();
  
  // Render the scene and repaint the map
  this.renderer.resetState();
  this.renderer.render(this.scene, this.camera);
  
  // Render any label
  this.labelRenderer.render(this.scene, this.camera);
  
  if (this.options.passiveRendering === false) 
    this.map.triggerRepaint();
}

Multi-Layer Setup

When using multiple custom layers, enable the multiLayer option to avoid calling tb.update() in each layer:
// Initialize Threebox with multiLayer option
window.tb = new Threebox(
  map,
  map.getCanvas().getContext('webgl'),
  { 
    defaultLights: true,
    multiLayer: true // Creates default layer to manage updates
  }
);

map.on('style.load', function () {
  // Default layer 'threebox_layer' is created automatically
  // It handles tb.update() calls
  
  map.addLayer({
    id: 'buildings_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function (map, gl) {
      // Add building models
    },
    render: function (gl, matrix) {
      // No need to call tb.update() here
    }
  });
  
  map.addLayer({
    id: 'vehicles_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function (map, gl) {
      // Add vehicle models
    },
    render: function (gl, matrix) {
      // No need to call tb.update() here
    }
  });
});
From source (Threebox.js:113-116):
if (this.tb.options.multiLayer) 
  this.addLayer({ 
    id: "threebox_layer", 
    type: 'custom', 
    renderingMode: '3d', 
    onAdd: function (map, gl) { }, 
    render: function (gl, matrix) { 
      this.map.tb.update(); 
    } 
  })

Layer Positioning

Place custom layers in the layer stack using the optional beforeId parameter:
// Add custom layer before labels
map.addLayer({
  id: 'custom_layer',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function (map, gl) { /* ... */ },
  render: function (gl, matrix) { tb.update(); }
}, 'waterway-label'); // beforeId parameter

Layer Management

Toggle Layer Visibility

// Hide/show layer and its 3D objects
tb.toggleLayer('custom_layer', false); // Hide
tb.toggleLayer('custom_layer', true);  // Show

// Or use setLayoutProperty
tb.setLayoutProperty('custom_layer', 'visibility', 'none');
tb.setLayoutProperty('custom_layer', 'visibility', 'visible');

Set Zoom Range

Custom layers don’t natively support minzoom and maxzoom. Use Threebox’s helper:
// Show layer only between zoom 10 and 18
tb.setLayerZoomRange('custom_layer', 10, 18);
From source (Threebox.js:831-836):
setLayerZoomRange: function (layerId, minZoomLayer, maxZoomLayer) {
  if (this.map.getLayer(layerId)) {
    this.map.setLayerZoomRange(layerId, minZoomLayer, maxZoomLayer);
    if (!this.zoomLayers.includes(layerId)) this.zoomLayers.push(layerId);
    this.toggleLayer(layerId);
  }
}

Remove Layer

// Remove layer and dispose all objects
tb.removeLayer('custom_layer');
This calls tb.clear(layerId, true) to dispose resources before removing the layer.

Layer-Specific Objects

Assign objects to specific layers for better organization:
// Add object to specific layer
tb.add(obj, 'custom_layer', 'source_id');

// Object now has layer and source properties
obj.layer; // 'custom_layer'
obj.source; // 'source_id'
From source (Threebox.js:917-931):
add: function (obj, layerId, sourceId) {
  if (!this.enableTooltips && obj.tooltip) { 
    obj.tooltip.visibility = false 
  };
  this.world.add(obj);
  if (layerId) {
    obj.layer = layerId;
    obj.source = sourceId;
    let l = this.map.getLayer(layerId);
    if (l) {
      let v = l.visibility;
      let u = typeof v === 'undefined';
      obj.visibility = (u || v === 'visible' ? true : false);
    }
  }
}

Clear Layer Objects

// Clear all objects from a specific layer
await tb.clear('custom_layer', true); // true = dispose resources

// Clear all objects from all layers
await tb.clear();

Common Patterns

Pattern 1: Single Layer Application

map.on('style.load', function () {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function (map, gl) {
      window.tb = new Threebox(map, gl, { defaultLights: true });
      // Add all objects here
    },
    render: function (gl, matrix) {
      tb.update();
    }
  });
});

Pattern 2: Multiple Layers (Manual)

window.tb = new Threebox(
  map,
  map.getCanvas().getContext('webgl'),
  { defaultLights: true }
);

map.addLayer({
  id: 'layer1',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function (map, gl) { /* Add objects */ },
  render: function (gl, matrix) { tb.update(); }
});

map.addLayer({
  id: 'layer2',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function (map, gl) { /* Add objects */ },
  render: function (gl, matrix) { /* Don't call update twice */ }
});

Pattern 3: Multiple Layers (Automatic)

window.tb = new Threebox(
  map,
  map.getCanvas().getContext('webgl'),
  { defaultLights: true, multiLayer: true }
);

map.addLayer({
  id: 'layer1',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function (map, gl) { /* Add objects */ },
  render: function (gl, matrix) { /* Auto-updated */ }
});

map.addLayer({
  id: 'layer2',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function (map, gl) { /* Add objects */ },
  render: function (gl, matrix) { /* Auto-updated */ }
});

Style Changes

When changing map styles, custom layers and their objects are removed:
// Use tb.setStyle to clean up properly
tb.setStyle('mapbox://styles/mapbox/dark-v10');

// This calls tb.clear(true) internally to dispose resources
From source (Threebox.js:869-873):
setStyle: function (styleId, options) {
  this.clear().then(() => {
    this.map.setStyle(styleId, options);
  });
}

Performance Considerations

  • Call tb.update() only once per frame
  • Use passiveRendering: true (default) unless continuous animation is needed
  • Dispose of objects when removing layers
  • Use multiLayer: true for multiple custom layers

Next Steps

Camera Synchronization

Learn how the camera stays synchronized between Three.js and Mapbox

Coordinate Systems

Understand coordinate conversions in Threebox

Build docs developers (and LLMs) love