How to Render 3D Tiles, LiDAR Point Clouds, and Gaussian Splats in GeoLibre Using deck.gl

GeoLibre renders 3D Tiles, LiDAR point clouds, and Gaussian splats through a single interleaved deck.gl overlay (map.__deck) that is shared across all visualization types using the MapboxOverlay adapter from @deck.gl/mapbox

This article explains how the open-source GeoLibre project (built on MapLibre) integrates deck.gl to render high-performance 3D geospatial data. All three visualization types—I3S 3D Tiles, USGS LiDAR point clouds, and Gaussian splats—draw into a unified overlay system rather than creating separate WebGL contexts.

The Shared Deck.gl Overlay Architecture

GeoLibre centralizes deck.gl rendering through a singleton overlay attached to each MapLibre map instance.

How the Overlay Is Created

The overlay lifecycle is managed in packages/plugins/src/plugins/shared-deck-overlay.ts. When any 3D visualization plugin activates, it calls app.getDeckGL() to retrieve or create the shared MapboxOverlay instance.

// From shared-deck-overlay.ts
// The overlay is stored at map.__deck for persistence
const overlay = new MapboxOverlay({
  // deck.gl props
  layers: [],
  // Interleaved rendering: deck.gl draws into MapLibre's GL context
  interleaved: true
});
map.__deck = overlay;
map.addControl(overlay, "top-left");

Interleaved rendering means deck.gl shares WebGL context with MapLibre, eliminating z-fighting and synchronization issues between the two libraries.

Projection Constraint: Mercator-Only Rendering

All three plugin types enforce Mercator projection while active. Deck.gl's tile-based rendering does not support other projections, so each plugin temporarily locks the map:

Rendering 3D Tiles (ArcGIS I3S)

The ArcGIS I3S plugin renders indexed 3D scene layers using deck.gl's Tile3DLayer.

Implementation in maplibre-3d-tiles.ts

1. Layer Construction

When a user adds a 3D Tiles source, the plugin constructs a Tile3DLayer from @deck.gl/geo-layers:

// packages/plugins/src/plugins/maplibre-3d-tiles.ts
import { Tile3DLayer } from '@deck.gl/geo-layers';
import { I3SLoader } from '@loaders.gl/i3s';

const layer = new Tile3DLayer({
  id: `i3s-layer-${sourceId}`,
  data: tilesetUrl,
  loader: I3SLoader,
  // Streaming: tiles load on-demand based on viewport
  onTilesetLoad: (tileset) => {
    tileset.setOptions({ maximumScreenSpaceError: 4 });
  }
});

2. Data Pipeline

Stage Component Purpose
Tileset parsing I3SLoader Parses tileset.json and node pages
Geometry extraction @loaders.gl/i3s Draco-compressed meshes → deck.gl geometry
Texture loading Shared I3S material definitions PBR materials, embedded textures
Rendering Tile3DLayer LOD-aware culling and streaming

3. Overlay Integration

The plugin mounts the layer to the shared overlay:

// Get or create the singleton overlay
const deckOverlay = app.getDeckGL();

// Add layer to deck.gl's layer stack
deckOverlay.setProps({
  layers: [...existingLayers, i3sLayer]
});

Key source: packages/plugins/src/plugins/maplibre-3d-tiles.ts — implements lazy loading of @loaders.gl/i3s, Mercator projection lock, and overlay lifecycle management.

Rendering USGS LiDAR Point Clouds

The LiDAR plugin streams LAS/LAZ files and renders them as colored point clouds with depth-sorted splatting.

Implementation in maplibre-usgs-lidar.ts

1. Lazy Module Loading

The plugin dynamically imports maplibre-gl-usgs-lidar to reduce initial bundle size:

// packages/plugins/src/plugins/maplibre-usgs-lidar.ts
const { LidarControl, LidarLayerAdapter } = await import(
  'maplibre-gl-usgs-lidar'
);

2. Control and Adapter Pattern

The LidarControl manages UI state (progress, classification filters), while LidarLayerAdapter translates LiDAR data into deck.gl layers:

// LidarLayerAdapter creates deck.gl layers
const lidarLayer = new SimpleMeshLayer({
  id: 'lidar-points',
  data: copcReader, // Cloud-Optimized Point Cloud iterator
  mesh: new CubeGeometry({ size: pointSize }),
  getPosition: d => [d.x, d.y, d.z],
  getColor: d => d.classification ? CLASSIFICATION_COLORS[d.classification] : [255, 255, 255],
  // Depth sorting for correct transparency
  depthSort: true
});

3. COPC and LAZ Streaming

The plugin uses @loaders.gl/las and @loaders.gl/copc for progressive loading of cloud-optimized point clouds:

// Streaming from S3 or HTTP range requests
const copcSource = await load(lazUrl, COPCLoader);
for await (const chunk of copcSource) {
  // Yield points to deck.gl as they're decompressed
  adapter.addPoints(chunk);
}

Key source: packages/plugins/src/plugins/maplibre-usgs-lidar.ts — handles control creation, point-cloud streaming, and shared overlay integration.

Rendering Gaussian Splats

Gaussian splatting renders 3D scenes as collections of oriented, textured ellipsoids—billboarded quads with Gaussian alpha falloff. GeoLibre supports this through an optional plugin bundle.

Implementation in maplibre-components.ts

1. Plugin Resolution

The Gaussian Splat plugin loads from maplibre-gl-splat with a fallback to a bundled version:

// packages/plugins/src/plugins/maplibre-components.ts
async function resolveSplatModule() {
  try {
    // Prefer external, newer version
    return await import('maplibre-gl-splat');
  } catch {
    // Fallback to bundled implementation
    return import('./fallback-splat-bundle');
  }
}

2. Control Factory

createSplattingControl() instantiates the control with predefined SPLATTING_OPTIONS:

export async function createSplattingControl(app) {
  const { GaussianSplatControl, GaussianSplatLayerAdapter } = 
    await resolveSplatModule();
  
  const control = new GaussianSplatControl({
    ...SPLATTING_OPTIONS, // Sh, ch levels, alpha threshold
    deck: app.getDeckGL() // Shared overlay instance
  });
  
  return control;
}

3. Splat Rendering Pipeline

Component Description
GaussianSplatLayerAdapter Creates SimpleMeshLayer with custom shaders
Splats Oriented quads with 3D covariance (mean, scale, rotation)
Blending Additive + depth-aware alpha for volume rendering
Sorting Per-frame sort by depth (handled in adapter)

4. Programmatic Usage

import { createSplattingControl } from "@geolibre/plugins";

const splatControl = await createSplattingControl(app);

// Load trained Gaussian splat model
await splatControl.loadFromUrl("https://example.com/model.ply");

// Or convert from point cloud
await splatControl.loadPointCloud("https://example.com/points.laz");

Key source: packages/plugins/src/plugins/maplibre-components.ts — resolves splat modules, configures GaussianSplatControl with SPLATTING_OPTIONS, and registers with the shared MapboxOverlay.

Practical Code Examples

Adding 3D Tiles Through UI

await app.addLayer({
  id: "i3s-tiles-01",
  source: { 
    type: "arcgis-i3s", 
    url: "https://tiles.arcgis.com/tiles/example/arcgis/rest/services/Buildings/SceneServer" 
  },
});

Behind the scenes: maplibre-3d-tiles.ts → Tile3DLayer + I3SLoader → shared overlay.

Adding LiDAR Point Cloud

await app.addLayer({
  id: "lidar-autzen",
  type: "lidar-url",
  source: { 
    url: "https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz" 
  },
  options: {
    classificationFilter: [2, 5, 6], // Ground, vegetation, building
    pointSize: 2
  }
});

Behind the scenes: maplibre-usgs-lidar.ts → LidarControl + LidarLayerAdapter → deck.gl SimpleMeshLayer.

Adding Gaussian Splat Layer (Programmatic)

import { createSplattingControl } from "@geolibre/plugins";

async function initSplat() {
  const control = await createSplattingControl(app);
  
  // Configure rendering quality
  control.setOptions({
    shDegree: 3,        // Spherical harmonics degree
    chDegree: 0,        // No chromatic aberration
    alphaThreshold: 0.01
  });
  
  await control.loadFromUrl(
    "https://example.com/gaussian-model.ply"
  );
  
  return control;
}

Performance and Architectural Considerations

Single Overlay Benefits

  • One WebGL context: Eliminates context overhead and resource duplication
  • Unified depth buffer: Correct occlusion between MapLibre basemap and deck.gl layers
  • Synchronized camera: No lag between map navigation and 3D overlays

Memory and Streaming

Visualization Streaming Strategy Memory Management
3D Tiles (I3S) LOD-based tile culling Tile cache with MRU eviction
LiDAR COPC range requests Chunked, progressive loading
Gaussian Splats Full model + adaptive rendering Shader-based splat culling

Summary

  • GeoLibre uses one shared deck.gl overlay (MapboxOverlay from @deck.gl/mapbox) for all 3D visualizations, stored at map.__deck

  • 3D Tiles render via Tile3DLayer with I3SLoader in maplibre-3d-tiles.ts, enforcing Mercator projection

  • LiDAR point clouds stream through LidarControl and LidarLayerAdapter in maplibre-usgs-lidar.ts, using SimpleMeshLayer with depth sorting

  • Gaussian splats resolve through createSplattingControl() in maplibre-components.ts, with fallback bundling and GaussianSplatLayerAdapter for oriented splat rendering

  • All three plugins rely on lazy loading, shared deck.gl instances, and Mercator projection locking to ensure consistent, performant 3D rendering on MapLibre maps

Frequently Asked Questions

Why does GeoLibre lock the map to Mercator projection for 3D visualizations?

Deck.gl's tile-based rendering—including Tile3DLayer and point-cloud layers—only supports Web Mercator projection. The plugins in maplibre-3d-tiles.ts and maplibre-usgs-lidar.ts temporarily force Mercator while active and restore the previous projection on deactivation. This ensures geometric correctness without duplicating deck.gl's projection logic.

Can multiple 3D layer types (3D Tiles, LiDAR, splats) render simultaneously?

Yes. All layers draw into the same MapboxOverlay instance retrieved via app.getDeckGL(). The overlay manages deck.gl's layer stack, so setProps({ layers: [...] }) can include any combination of Tile3DLayer, SimpleMeshLayer, and custom splat layers. Layer ordering follows the beforeId option relative to MapLibre layers.

How does GeoLibre handle large LiDAR point clouds without freezing the UI?

The LiDAR plugin uses Cloud-Optimized Point Cloud (COPC) format and @loaders.gl/copc for streaming. Points are yielded progressively through async iterators, and the LidarLayerAdapter batches updates to deck.gl. The viewer sees data appear incrementally while the main thread remains responsive.

What happens if maplibre-gl-splat is not installed?

The createSplattingControl() factory in maplibre-components.ts wraps the dynamic import in a try/catch block. If the external module fails to load, it falls back to a bundled (potentially older) version of the splatting implementation. This guarantees the UI control is always available even with partial dependency installation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →