Generate Cesium 3D Tiles in the browser. Drag in a LAS / GeoTIFF / GLB,
get back a streaming tileset.json + pnts / quantized-mesh tiles β no
server, no upload, no Cesium ion quota. The data never leaves the user's
machine.
π Live Demo Β· π¦ npm Β· π API Docs Β· π Head-to-head vs py3dtiles Β· πΊοΈ Roadmap
π§ͺ 30-second try (drop a .las file, get a Cesium tileset):
<script type="module">
import init, { parsePointCloudAuto, generateTileset }
from 'https://esm.run/wasm-spatial-core';
await init();
// User drops a LAS file β parse β octree β 3D Tiles.
// All in the browser. Bytes never touch a server.
const cloud = parsePointCloudAuto(lasFileBytes);
const tileset = generateTileset(cloud.positions(), 50000, 10);
// β { tilesetJson, tiles: Uint8Array[], ... } β feed straight into Cesium.C3DTileset
</script>
Building something with wasm-spatial-core? Add your project here β open an issue using the feature request template or edit this section directly.
(No production adopters listed yet β be the first. This section grows as the project gets real-world use.)
If you've ever tried to view a customer's LAS scan in Cesium, you've hit one of:
wasm-spatial-core is a Rust β WebAssembly engine that does the whole
LAS β octree β 3D Tiles pipeline client-side. The output is byte-compatible
with what Cesium.C3DTileset consumes β so you can build a zero-backend,
zero-upload Cesium viewer that runs on a static host.
| Cesium ion (SaaS) | Desktop tools | wasm-spatial-core | |
|---|---|---|---|
| Customer data leaves the browser | βοΈ yes (uploaded) | π» no (local) | π» no (local) |
| Deployable on intranet / air-gap | β | per-machine | β static files |
| Per-GB / per-user cost | β yes | license fee | β free (MIT) |
| Works inside a web app | β | β | β |
| Streams multi-GB scans | partial | n/a | β COPC range fetch |
Not a PROJ / QGIS replacement. CRS coverage is intentionally focused (WGS-84 / Web Mercator / UTM / China offsets). For arbitrary EPSG reprojection, pre-transform with PROJ and feed WGS-84 / ENU.
Core (the killer use case):
CesiumTerrainProvider-compatible (proven by headless test, see W3.6 in ROADMAP_V2.md)Also in the box (used internally, exposed as public APIs):
npm install wasm-spatial-core ships a prebuilt 1.2 MB WASM binary β no
native deps, no postinstall, works on any static host (GitHub Pages, S3,
nginx, even python -m http.server).
Live, drag-and-drop, no backend:
| Demo | What it shows |
|---|---|
| Cesium 3D Tiles | Drop a .las / .tif / .glb β octree β 3D Tiles β Cesium globe. The killer demo. |
| COPC streaming | Range-fetch a multi-GB .copc scan byte-range by byte-range (no full download). |
| Terrain viewer | GeoTIFF β quantized-mesh, with cut / flatten / fill edits. |
| Demo hub | All examples + benchmarks. |
Run any of them locally with npm run demo.
npm install wasm-spatial-core
import init, { parsePointCloudAuto, generateTileset } from 'wasm-spatial-core';
// Cesium loaded separately (CDN or npm) β we only emit the tile bytes.
await init();
// 1. User drops a .las file. Parse it.
const cloud = parsePointCloudAuto(lasFileBytes);
// cloud.positions() β Float32Array [x,y,z, ...]
// 2. Build octree β 3D Tiles (pnts + tileset.json).
const tileset = generateTileset(cloud.positions(), 50000 /* max pts/node */, 10 /* depth */);
// tileset.tilesetJson() β string
// tileset.tile(i) β Uint8Array (one pnts blob per tile)
// 3. Hand the tiles to Cesium via blob URLs (no server needed).
const json = JSON.parse(tileset.tilesetJson());
for (let i = 0; i < tileset.tileCount; i++) {
const url = URL.createObjectURL(new Blob([tileset.tile(i)]));
json.root.content.uri = json.root.content.uri.replace(`tile_${i}.pnts`, url);
// (in practice, walk the tree and rewrite each leaf's content.uri)
}
const cesiumTileset = await Cesium.C3DTileset.fromUrl(
URL.createObjectURL(new Blob([JSON.stringify(json)], { type: 'application/json' })),
);
viewer.scene.primitives.add(cesiumTileset);
That's the whole story β no backend, no upload, no ion token. For a working drag-and-drop demo, see the Point Cloud + Cesium example.
npm install draco3d
import { compressTilesetWithDraco } from 'wasm-spatial-core';
import { createEncoderModule } from 'draco3d';
const encoder = await createEncoderModule({});
const compressed = compressTilesetWithDraco(tileset, encoder, { quantizationBits: 11 });
// Typical ratio: ~20% of original size, position-color pairing preserved.
npm install wasm-spatial-core ships a prebuilt WASM binary compiled with
point-cloud + geotiff. That gives you:
| Included in npm | Not in npm (custom wasm-pack build) |
|---|---|
| LAS, PLY, OBJ, PCD parsing | LAZ / COPC (laz-support) |
| Octree + 3D Tiles (pnts) | E57 (e57-support) |
| GeoTIFF β quantized-mesh terrain | Terrain deformation (terrain-edit) |
| Coordinates, GeoJSON, MVT, spatial analysis | Spatial IR + GLB ingest (mesh-ingest) |
Mesh QEM / clip / OBB split (mesh-edit, needs mesh-ingest) |
|
WebGPU compute kernels (webgpu) |
Format counts: 10+ read/write paths in the default npm build (LAS/PLY/OBJ/PCD, GeoJSON, MVT, WKT/WKB, GeoTIFF, GPX, TopoJSON, 3D Tiles/glTF output, β¦). 15+ when optional format features are enabled (LAZ/COPC, E57, GLB ingest, β¦).
Runtime checks: supportsLaz(), supportsGeotiff(), lazStatus().
CI runs cargo test --all-features β 857 tests pass (plus 34 #[ignore]d performance benchmarks) across the full feature matrix. The live count is printed by CI on every run; see the latest rust job log for the current number.
LAS / PLY / OBJ (npm default)
LAZ / COPC / E57 (optional build features β see table above)
β
βΌ
ββββββββββββββββ
β WASM Parser β Browser-side; format set depends on build features
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ
β Octree Build β 8-way spatial partitioning
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ
β pnts Encoder β 3D Tiles Point Cloud binary
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ ββββββββββββββββ
β tileset.json β β Draco Compress β Optional (~20% ratio)
ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β
βΌ βΌ
Cesium / Three.js β interactive 3D
GeoTIFF (.tif)
β
βΌ
ββββββββββββββββ
β WASM Parser β Float32/16/8, strip/tile, DEFLATE
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ
β Quantized-Mesh β Cesium terrain binary format
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ
β tileset.json β LOD pyramid (zoom 0..N)
βββββββββββββββββ
The whole reason this exists: do in-browser what previously needed a server. On Apple M2, single-thread WASM (no WebGPU):
| Dataset | Points | Parse | Octree + Tileset | Total |
|---|---|---|---|---|
| sample.las | 1K | β | < 1 ms | β |
| Synthetic | 500K | 36 ms | 166 ms | 205 ms |
| Synthetic | 10M | 1.1 s | 3.6 s | 4.8 s |
| Synthetic | 100M | 0.4 s | 6.8 s | 8.5 s |
100M-point number is native release (Rust); WASM is ~1.5Γ slower but still well under 30 seconds. Compare: potree (JS) takes ~3 s just for octree at 1M.
Coordinate conversion (the same engine, different workload):
| Operation | Pure JS | WASM | Speedup |
|---|---|---|---|
| WGS84 β GCJ-02 | ~1,200 ms | ~45 ms | ~27Γ |
| WGS84 β Mercator | ~800 ms | ~12 ms | ~67Γ |
See PERFORMANCE.md for full methodology + comparison with potree / three.js Octree / las-js.
| Format | Read | Feature Flag |
|---|---|---|
| LAS (1.2β1.4, Format 0β6) | β | point-cloud |
| LAZ (compressed) | β | laz-support |
| COPC (Cloud Optimized) | β | laz-support |
| PLY (ASCII + binary) | β | point-cloud |
| OBJ | β | point-cloud |
| PCD (ASCII + binary) | β | point-cloud |
| E57 | β | e57-support |
| Format | Read | Write |
|---|---|---|
| GeoJSON | β | β |
| MVT (Vector Tiles) | β | β |
| WKT / WKB | β | β |
| GeoTIFF (Terrain) | β | β |
| glTF 2.0 / GLB | β | β |
| 3D Tiles (pnts) | β | β |
| 3D Tiles (b3dm) | β | β |
| 3D Tiles (quantized-mesh) | β | β |
| System | Direction |
|---|---|
| WGS-84 β GCJ-02 / BD-09 | β |
| WGS-84 β Web Mercator (EPSG:3857) | β |
| WGS-84 β CGCS2000 | β |
| WGS-84 β UTM | β |
R-Tree / Octree indexing, bounding box / KNN queries, haversine / vincenty distance, polygon boolean ops, Douglas-Peucker simplification, convex / concave hull, DBSCAN / grid clustering, TIN interpolation, and more.
import { loadSpatialCore } from 'wasm-spatial-core';
const wasm = await loadSpatialCore();
// Auto-detect format
const cloud = wasm.parsePointCloudAuto(bytes);
console.log(cloud.count()); // point count
console.log(cloud.positions()); // Float32Array [x,y,z,...]
console.log(cloud.colors()); // Uint8Array [r,g,b,...] | null
// Octree
const octree = wasm.buildOctree(cloud.positions(), 50000, 10);
console.log(octree.nodeCount()); // node count
console.log(octree.depth()); // tree depth
// 3D Tiles tileset
const tileset = wasm.generateTileset(cloud.positions(), 50000, 10);
console.log(tileset.tileCount()); // tile count
console.log(tileset.tilesetJson()); // tileset.json string
import { compressTilesetWithDraco, buildDracoTileset } from 'wasm-spatial-core';
import { createEncoderModule } from 'draco3d';
const encoderModule = await createEncoderModule({});
// Compress all tiles
const results = compressTilesetWithDraco(tileset, encoderModule, {
quantizationBits: 11, // 8β18, default 11
encodeSpeed: 5, // 0β10, default 5
decodeSpeed: 5, // 0β10, default 5
compressColors: false, // also compress RGB (default false)
});
// Or build a complete compressed tileset
const { tiles, totalCompressedSize, compressionRatio } =
buildDracoTileset(tileset, encoderModule);
const coords = new Float64Array([116.404, 39.915, 121.474, 31.230]);
const gcj02 = wasm.batchWgs84ToGcj02(coords); // batch transform
wasm.batchWgs84ToGcj02InPlace(mutable); // zero-copy in-place
const [zone, easting, northing] = wasm.wgs84ToUtm(116.404, 39.915);
// Chunked output: parses the full JSON first, then emits coordinate batches
// (progress callbacks + lower peak coord memory β not byte-stream input).
wasm.parseGeoJsonStream(hugeGeojson, 500, (chunk, processed, total) => { /* ... */ });
// Lower memory per iteration: one feature at a time (input string still required)
const iter = wasm.parseGeoJsonLazy(hugeGeojson);
git clone https://github.com/reed-soul/wasm-spatial-core.git
cd wasm-spatial-core
# Point cloud + GeoTIFF
wasm-pack build --target web --release --out-dir pkg -- --features point-cloud,geotiff
# Run demos
npm run demo
| Feature | In npm | Default crate | Description |
|---|---|---|---|
single-thread |
β | β | Zero-config, works everywhere |
point-cloud |
β | β | LAS/PLY/OBJ/PCD + octree + 3D Tiles |
geotiff |
β | β | GeoTIFF terrain + quantized-mesh |
multi-thread |
β | β | Web Workers + SharedArrayBuffer |
laz-support |
β | β | LAZ/COPC decompression (+ ~400 KB WASM) |
e57-support |
β | β | E57 format |
terrain-edit |
β | β | Heightfield flatten/deform (requires geotiff) |
mesh-ingest |
β | β | Spatial IR + GLB ingest (Wave 2) |
mesh-edit |
β | β | Mesh QEM / OBB split (requires mesh-ingest) |
draco-support |
β | β | Draco compression API (JS-side via draco3d) |
| Doc | Scope |
|---|---|
| VISION.md | Product vision β Web3D spatial compute engine (core only) |
| ROADMAP_V2.md | Active plan β Waves 1β5 (runtime, IR, terrain, GPU, mesh edit) |
| docs/ENGINE_BOUNDARY.md | What is in the engine vs your application |
| ROADMAP_V1.md | β Completed β point cloud β 3D Tiles browser pipeline |
V1 highlights (done): LAS β octree β 3D Tiles (npm) Β· LAZ/COPC/E57 (optional builds) Β· GeoTIFF terrain Β· Draco Β· multi-thread WASM Β· Node.js batch API
V2 next: spatial IR Β· terrain/mesh edit (source available; not in default npm) Β· WebGPU Β· incremental tiles β see issue templates (start at W2)
See CONTRIBUTING.md.
MIT License β Β© 2026 Zhiqi Weilai
Built with π¦ Rust + πΈοΈ WebAssembly
Native spatial computing in every browser.