Convert GIS data (GeoTIFF + GeoJSON) into:
- A 3D mesh GLB
- A Gaussian Splat file
- An Aholo LOD chunk hierarchy for streaming
This repository contains a full pipeline implemented in Node.js:
- Step 1: GIS -> GLB
- Step 2: GLB -> Gaussian Splat (.splat)
- Step 3: Splat -> Aholo LOD chunks (.spz/.sog/.ply + lod-meta.json)
NOTE! Step 3 is not producing a consumable asset for Aholo, and needs some more work.
Troubleshooting:
- LoD meta.json wrong?
- Node.js >= 22
- pnpm ^11.17.0
- Windows, macOS, or Linux
This project uses:
- gdal-async
- @gltf-transform/core
- earcut
- @manycore/aholo-splat-transform
- assets/
- geotiff.tif
- geojson/
- src/
- step1.js
- step2.js
- step3.js
- index.js
- output/
- output.glb
- output.splat
- output-lod/
- assets/hurdal/geotiff.tif (auto-detected default when present)
- assets/geotiff.tif (fallback default)
The DEM is read with GDAL and sampled with bilinear interpolation.
At minimum, Step 1 expects:
- assets/hurdal/geojson/hurdal_kommuneomrade.geojson (auto-detected default when present)
- assets/geojson/hurdal_kommuneomrade.geojson (fallback default)
The layer processor currently attempts these polygon layers:
- hurdal_vann.geojson
- hurdal_myr.geojson
- hurdal_dyrketmark.geojson
- hurdal_tettbebyggelse.geojson
If a layer file is missing, it is skipped with a warning.
pnpm installIf you use the shared asset repository as a git submodule:
git submodule add https://github.com/gnimmelf/gis-assets assets
git submodule update --init --recursiveWhen cloning this repo elsewhere:
git clone --recurse-submodules <repo-url>Or after clone:
git submodule update --init --recursiveRun full pipeline (all three steps):
pnpm buildEquivalent:
node src/index.jsOutputs:
- output/output.glb
- output/output.splat
- output/output-lod/lod-meta.json
- output/output-lod/chunk_*.spz (or selected chunk type)
pnpm step1or
node src/step1.js --building-height 8 --terrain-grid 512 --output output/output.glbUse a different asset root (submodule mounted elsewhere):
node src/step1.js --assets-dir ../gis-assetspnpm step2or
node src/step2.js --glb output/output.glb --splat output/output.splat --density 0.1pnpm step3or
node src/step3.js --splat output/output.splat --lod-output output/output-lod --chunk-size 400000 --chunk-type spzUse these with:
node src/index.js [options]-
--building-height Building extrusion height in meters (forwarded to Step 1)
-
--terrain-grid Terrain mesh grid resolution NxN (forwarded to Step 1)
-
--glb Input GLB path for Step 2
-
--assets-dir Asset root for Step 1 (default: GIS_ASSETS_DIR, else assets/hurdal if present, else assets)
-
--dem DEM path for Step 1 (overrides assets-dir default)
-
--geojson-dir GeoJSON dir for Step 1 (overrides assets-dir default)
-
--density Splats per square meter for Step 2
-
--lod-output LOD output directory for Step 3
-
--chunk-size Max gaussians per chunk for Step 3
-
--chunk-type Chunk format: spz | sog | ply
-
--skip-lod Run only Steps 1-2 and skip Step 3
Example:
node src/index.js --terrain-grid 768 --density 0.2 --chunk-size 300000 --chunk-type spz- Opens DEM and prepares elevation sampler
- Loads municipality polygon and builds regular terrain grid
- Samples elevations and clips terrain triangles to boundary
- Processes selected polygon layers from GeoJSON
- Writes a multi-node GLB with per-layer materials
Coordinate handling:
- Input GeoJSON is treated as WGS84 lon/lat
- Geometry is transformed to UTM 32N (meters)
- Mesh is exported with north mapped to negative Z for scene orientation
- Reads GLB primitives
- For each triangle:
- Computes area
- Spawns splats proportional to area x density
- Samples random barycentric positions
- Derives gaussian orientation from triangle normal
- Writes binary .splat records (32 bytes per gaussian)
- Invokes local splat-transform binary from node_modules/.bin
- Uses auto-chunk LOD strategy
- Validates generated lod-meta.json
- Reports chunk count, LOD levels, and output size
To host output files with CORS:
pnpm serve-outputDefault server:
- Higher terrain grid increases GLB triangle count quickly.
- Higher density increases splat count and file size significantly.
- Smaller chunk-size creates more LOD files and finer streaming granularity.
Good tuning flow:
- Start with terrain-grid 256-512
- Start with density 0.05-0.2
- Keep chunk-size around 300k-500k
- Increase quality incrementally after checking output size
Run Step 1 first, or point Step 2 to a valid --glb path.
Run Step 2 first, or point Step 3 to a valid --splat path.
Check that splat-transform completed successfully and that output directory is writable.
The pipeline resolves and runs the local .CMD shim from node_modules/.bin on Windows. If install is incomplete, re-run:
pnpm installgdal-async is native. Ensure Node version compatibility and rebuild dependencies if needed:
pnpm install --forceFrom package.json:
- pnpm build
- pnpm step1
- pnpm step2
- pnpm step3
- pnpm serve-output
ISC