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

> Learn how GeoLibre renders 3D Tiles LiDAR point clouds and Gaussian splats using a single deck.gl overlay with the MapboxOverlay adapter. Visualize complex geospatial data efficiently.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-03

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
// 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:

- [`maplibre-3d-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-3d-tiles.ts) uses `forceI3sMercatorProjection()`
- [`maplibre-usgs-lidar.ts`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-usgs-lidar.ts) emits: `[maplibre-usgs-lidar] control failed to mount; deactivate the plugin to restore the projection`

## 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`](https://github.com/opengeos/GeoLibre/blob/main/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`:

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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:

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-usgs-lidar.ts)

**1. Lazy Module Loading**

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

```typescript
// 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:

```typescript
// 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:

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-components.ts)

**1. Plugin Resolution**

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

```typescript
// 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`:

```typescript
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**

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-3d-tiles.ts) → `Tile3DLayer` + `I3SLoader` → shared overlay.

### Adding LiDAR Point Cloud

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-usgs-lidar.ts) → `LidarControl` + `LidarLayerAdapter` → deck.gl `SimpleMeshLayer`.

### Adding Gaussian Splat Layer (Programmatic)

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-3d-tiles.ts), enforcing Mercator projection

- **LiDAR point clouds** stream through `LidarControl` and `LidarLayerAdapter` in [`maplibre-usgs-lidar.ts`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-usgs-lidar.ts), using `SimpleMeshLayer` with depth sorting

- **Gaussian splats** resolve through `createSplattingControl()` in [`maplibre-components.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-3d-tiles.ts) and [`maplibre-usgs-lidar.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.