Overview
geolith compiles raw drone capture into web-streamable 3D terrain through two subcommands:
| Track | Command | Input | Output |
|---|---|---|---|
| Terrain-RGB | geolith terrain | GeoTIFF DEM | raster-dem PMTiles for MapLibre setTerrain + hillshade |
| Gaussian splats | geolith splat-tiles | Trained 3DGS PLY | OGC 3D Tiles 1.1 (KHR_gaussian_splatting + KHR_gaussian_splatting_compression_spz_2) |
Capture Recipe (upstream of geolith)
drone video (5–10 min 4K, orbit or lawnmower grid)
→ frame extraction @ 2–5 fps (~200–500 frames)
→ OpenDroneMap: --dsm --dtm --orthophoto
→ GeoTIFF DEM (+ orthophoto)
For splats, continue from the same frames:
frames → COLMAP (`colmap mapper --global`)
→ gsplat / Nerfstudio `splatfacto` (Apache-2.0)
→ PLY splat export
Track A — GeoTIFF DEM → Terrain-RGB PMTiles
# Drone survey (UTM GeoTIFF from OpenDroneMap / Pix4D)
geolith terrain \
--dem ./data/belvedere_dsm.tif \
--output ./data/belvedere-terrain.pmtiles \
--min-zoom 8 --max-zoom 16 \
--encoding mapbox \
--name belvedere-glacier
# Regional demo from a public DEM (Copernicus GLO-30, EPSG:4326) —
# e.g. the Garhwal Himalaya (Kedarnath / Badrinath / Nanda Devi):
curl -LO https://copernicus-dem-30m.s3.amazonaws.com/Copernicus_DSM_COG_10_N30_00_E079_00_DEM/Copernicus_DSM_COG_10_N30_00_E079_00_DEM.tif
geolith terrain \
--dem Copernicus_DSM_COG_10_N30_00_E079_00_DEM.tif \
--output ./data/garhwal-himalaya.pmtiles \
--min-zoom 6 --max-zoom 12 \
--name garhwal-himalaya
A live archive built exactly this way serves at
https://samples.geolith.app/terrain/garhwal-himalaya.pmtiles (257 tiles,
z6-z12, the 79-80°E / 30-31°N Copernicus tile).
| Flag | Default | Purpose |
|---|---|---|
--dem | required | Input GeoTIFF DEM (Float32/Int16, tiled or stripped, LZW/Deflate) |
--output | required | Output PMTiles path |
--encoding | mapbox | mapbox (Terrain-RGB) or terrarium |
--min-zoom / --max-zoom | 0 / 14 | Zoom range |
--tile-size | 512 | Tile edge in pixels |
--dem-crs | auto | EPSG override when the GeoTIFF lacks geokeys |
--name | terrain | Layer name in archive metadata |
The CRS is read from the GeoTIFF GeoKeyDirectory. WGS84 (4326), Web Mercator (3857), and UTM zones on WGS84/ETRS89/RDN2008/NAD83 datums resolve automatically; anything else needs --dem-crs.
Tiles with no DEM coverage are skipped, nodata pixels encode as sea level, and elevations bilinearly resample from the source raster.
Rendering
map.addSource('terrain', {
type: 'raster-dem',
url: 'pmtiles://https://samples.geolith.app/terrain/garhwal-himalaya.pmtiles',
encoding: 'mapbox',
tileSize: 512,
});
map.setTerrain({ source: 'terrain', exaggeration: 1.2 });
map.addLayer({ id: 'hills', type: 'hillshade', source: 'terrain' });
Serving works through tileserver-rs or any PMTiles host — the archive declares tile_type PNG with no tile compression, per the PMTiles v3 spec.
Track B — 3DGS PLY → OGC 3D Tiles
geolith splat-tiles \
--ply ./data/scene.ply \
--output ./data/tileset \
--longitude 7.9797 --latitude 45.9756 --altitude 2250 \
--max-splats-per-tile 65536
| Flag | Default | Purpose |
|---|---|---|
--ply | required | Trained 3DGS PLY (INRIA / gsplat / splatfacto layout) |
--output | required | Output directory (tileset.json + tiles/*.glb + scene.spz) |
--longitude / --latitude | required | Geographic anchor of the scene |
--altitude | 0 | Anchor height above the ellipsoid in metres |
--max-splats-per-tile | 100000 | Octree leaf capacity |
--max-depth | 6 | Octree depth limit |
--root-geometric-error | 64 | Root tile geometric error |
--antialiased | false | Mark splats trained with antialiasing |
--emit-scene-spz | true | Also write a standalone scene.spz |
--crop-percentile | 0 | Trim this % from both ends of every axis, removing the hallucinated far-field shell of inward-facing captures |
--scale | 1 | Uniform scene scale factor (positions and splat footprints together) |
Pick assets that suit a map: outward-facing captures of a subject at true metric
scale render crisply from map viewpoints, while indoor inward-facing scans (the
MipNeRF-360 datasets) show their hallucinated shell from outside. For a bad
world scale, --scale normalises it; for a noisy far field, --crop-percentile
trims it.
The output shape matches the reference 3DGS-PLY-3DTiles-Converter: an octree of GLB tiles with refine: REPLACE, each carrying a gzip-compressed SPZ stream under KHR_gaussian_splatting_compression_spz_2, georeferenced by an east-north-up ECEF transform on the root tile. Parents hold decimated previews of their subtree so coarse zooms stay light.
Rendering
tileset.json + GLB tiles are static files — serve from tileserver-rs, R2, or any CDN. Render in MapLibre through @geoql/v-maplibre:
<!-- @geoql/v-maplibre/splat — single-file splat -->
<VLayerSplat url="https://samples.geolith.app/splat/cactus/scene.spz"
:longitude="7.9797" :latitude="45.9756" :altitude="2250" :lod="true" />
<!-- @geoql/v-maplibre/3d-tiles — streamed tileset with LOD -->
<VLayer3DTiles url="https://samples.geolith.app/splat/cactus/tileset.json" :error-target="6" />
Sample Data
The Belvedere Glacier open dataset provides real multi-year UAV DSMs and orthophotos for Track A; any INRIA-format splat export exercises Track B.