SDK · JavaScript & TypeScript
Bitruvius SDK for JavaScript
High-performance codecs for reality capture and imagery. BVC, our flagship container for quantized gaussian splats and point clouds. RIPT, our flagship codec for scientific rasters. TurboWebP, TurboJXL, TurboLERC, TurboLEPCC and TurboSPZ across images, elevation, point clouds and splats. Every one memory-safe and decoding off the main thread, so scenes that used to need a desktop client hold interactive framerates in a browser tab.
Decode anything, anywhere: gaussian splats, LiDAR point clouds, photogrammetry meshes,
city-scale models, elevation, satellite and scientific rasters, with or without a map.
When you want one, the MapLibre plugin makes any of it an ordinary custom layer:
one import, one addLayer, georeferenced and terrain-aware,
no native viewer and no build step. OGC 3D Tiles and Esri I3S stream on top.
Plays in place. Nothing downloads until you press play. Open the video directly ↗
Quick start
Three ways in, all on the same v0.3.1 release line. The CDN needs no tooling at all. npm carries the same modules with TypeScript types, tree-shaking and offline builds.
- Per-codec bundles are self-contained. Pull one decoder, not the whole SDK.
- The wasm URL is derived from the bundle's own location, so there is nothing to configure.
- Release directories are immutable and every file is published with an SRI hash.
No install, no bundler. Import a single codec bundle straight from the CDN and decode in the browser.
<canvas id="preview"></canvas>
<script type="module">
// One codec, one file — 5 KB of JavaScript. The wasm module is fetched from
// the same pinned release directory the first time you decode.
import { JxlDecoder } from 'https://cdn.bitruvius.com/sdk/v0.3.1/bundles/bitruvius-turbo-jxl.esm.min.js';
const decoder = new JxlDecoder();
const response = await fetch('/imagery/tile.jxl');
const bytes = new Uint8Array(await response.arrayBuffer());
// { width, height, rgba, hasAlpha }
const image = await decoder.decode(bytes);
const canvas = document.getElementById('preview');
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext('2d').putImageData(
new ImageData(
new Uint8ClampedArray(image.rgba.buffer, image.rgba.byteOffset, image.rgba.length),
image.width,
image.height,
),
0,
0
);
</script><script src="https://cdn.bitruvius.com/sdk/v0.3.1/bundles/bitruvius.iife.min.js"></script>
<script>
var decoder = new Bitruvius.JxlDecoder();
decoder.decode(bytes).then(function (image) {
console.log(image.width, image.height, image.hasAlpha);
});
</script>npm install @bitruvius/turbo-jxl@0.3.1
# bun add @bitruvius/turbo-jxl@0.3.1
# pnpm add @bitruvius/turbo-jxl@0.3.1import { JxlDecoder, ensureWasm } from '@bitruvius/turbo-jxl';
// Optional: warm the wasm module during app boot so the first decode is not
// the one that pays for instantiation.
await ensureWasm();
const decoder = new JxlDecoder();
export async function decodeJxl(bytes: Uint8Array): Promise<ImageData> {
const { width, height, rgba } = await decoder.decode(bytes);
return new ImageData(new Uint8ClampedArray(rgba.buffer, rgba.byteOffset, rgba.length), width, height);
}What ships in a release
Eighteen library bundles (plus two I3S decode workers), a manifest.json with an SRI hash per file, and the wasm codecs, under https://cdn.bitruvius.com/sdk/v0.3.1/. The same modules are on npm as @bitruvius/* packages at the matching version.
Umbrella build
everything, lazily loadedStart here when you do not know which formats you will meet. Codecs load on first use, so the wasm you never touch is never fetched.
bitruvius.esm.min.js345 KBEvery decoder in one module: BvcDecoder, BvcWorkerDecoder, JxlDecoder, LercDecoder, LepccDecoder, COLORMAPS, BitruviusError.
bitruvius.iife.min.jsThe same surface on window.Bitruvius, for classic script tags with no module syntax.
Single-codec bundles
smallest possible payloadSelf-contained. If you only ever decode one format, ship one file.
bitruvius-turbo-webp.esm.min.js5 KBTurboWebP: our WebP decoder. Reads anything libwebp 1.6 does. Exports WebpDecoder and ensureWasm.
bitruvius-turbo-jxl.esm.min.jsTurboJXL: our JPEG XL decoder. Reads anything libjxl 0.11 does.
bitruvius-turbo-lerc.esm.min.jsTurboLERC: our Esri LERC decoder for elevation and scientific rasters.
bitruvius-turbo-lepcc.esm.min.jsTurboLEPCC: our Esri LEPCC decoder for I3S point-cloud payloads.
bitruvius-turbo-spz.esm.min.jsTurboSPZ: our SPZ v4 decoder for 3D gaussian splats.
MapLibre plugin
layers & widgetsThe rendering half of the SDK. Peer-depends on maplibre-gl v5.0.0 through v6.0.0+; the examples pin 5.24.0.
bitruvius-sdk-maplibre.esm.min.js1.9 MBEvery MapLibre layer and widget, for bundlers and import maps.
bitruvius-sdk-maplibre.iife.min.jsThe same plugin for classic script tags.
Composable modules
for bundler buildsThe umbrella build is assembled from these. Published individually so a bundler
can take exactly the graph your app touches. Most apps never import them
directly. Two are worth knowing by name: bitruvius-bvc is our flagship container for
quantized gaussian splats and point clouds, and bitruvius-ript is our scientific raster codec
(lossless and lossy, 14 data types, for elevation, satellite, SAR and medical
imagery).
bitruvius-bvc.esm.min.jsbitruvius-ript.esm.min.jsbitruvius-splats.esm.min.jsbitruvius-ptcloud.esm.min.jsbitruvius-raster.esm.min.jsbitruvius-geo-core.esm.min.jsbitruvius-render-runtime.esm.min.jsbitruvius-foundation.esm.min.jsbitruvius-codec-runtime.esm.min.js
@bitruvius/sdk-maplibre
3D geospatial, inside the map you already have
Every layer type is a genuine MapLibre custom layer: it obeys the camera, the terrain, the layer order and the style. There is no second canvas floating over your map, no separate globe, and no fork of maplibre-gl. You keep your basemap, your controls and your event handlers exactly as they are.
The plugin peer-depends on maplibre-gl v5.0.0 through v6.0.0+. Layers are standard MapLibre custom layers — add and remove them like any other.
{
"dependencies": {
"@bitruvius/sdk-maplibre": "0.3.1",
"maplibre-gl": "5.24.0"
}
}import maplibregl from 'maplibre-gl';
import { Tiles3DSplatLayer } from '@bitruvius/sdk-maplibre';
import 'maplibre-gl/dist/maplibre-gl.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/bright',
center: [-122.4013, 37.7893],
zoom: 16,
pitch: 65
});
map.on('load', () => {
// An OGC 3D Tiles gaussian-splat tileset, streamed and sorted on the GPU.
map.addLayer(
new Tiles3DSplatLayer({
id: 'downtown-splats',
url: 'https://tiles.arcgis.com/tiles/x5wCko8UnSi4h0CB/arcgis/rest/services/CU_Campus_Area3_GaussianSplat/3DTilesServer/tileset.json'
})
);
});<script type="importmap">
{
"imports": {
"@bitruvius/sdk-maplibre": "https://cdn.bitruvius.com/sdk/v0.3.1/bundles/bitruvius-sdk-maplibre.esm.min.js",
"maplibre-gl": "https://esm.sh/maplibre-gl@5.24.0?target=es2022"
}
}
</script>
<script type="module">
import maplibregl from 'maplibre-gl';
import { I3sMeshLayer } from '@bitruvius/sdk-maplibre';
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/bright',
pitch: 60
});
map.on('load', () => {
// Loaded cross-origin from the CDN, so decode inline — see "Workers".
map.addLayer(
new I3sMeshLayer({
id: 'buildings',
url: 'https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/SanFrancisco_Bldgs/SceneServer',
workers: false
})
);
});
</script>import { BvcMapLibreLayer, SpzMapLibreLayer } from '@bitruvius/sdk-maplibre';
// Where the scene sits on Earth. A BVC asset carries model-space geometry, so
// the anchor is what places it — without one there is nothing to draw.
const ANCHOR = { lng: -77.0353, lat: 38.8895, altitude: 0 };
// A BVC asset: the container tells the layer what it is holding.
map.addLayer(
new BvcMapLibreLayer({
id: 'site-scan',
url: 'https://cdn.bitruvius.com/demo-assets/splat-v3/bvc-splat-small-hornedlizard.bvc',
anchor: ANCHOR,
onLoaded: () => console.log('scene loaded')
})
);
// Or a raw SPZ v4 gaussian-splat file.
map.addLayer(
new SpzMapLibreLayer({
id: 'statue',
url: 'https://cdn.bitruvius.com/demo-assets/splat-v1/hornedlizard_v4.spz',
anchor: ANCHOR
})
);Layer types
Tiles3DSplatLayerTiles3DMeshLayerTiles3DPointCloudLayerI3sIntegratedMeshLayerI3s3dObjectLayerI3sMeshLayerI3sBuildingLayerI3sPointCloudLayerI3sPointLayerBvcMapLibreLayerSpzMapLibreLayerLepccMapLibreLayerModelInstancesLayerMultiModelInstancesLayerBillboardInstancesLayerTerrainShadowReceiverLayer
Controllers & widgets
BasemapControllerDrawControllerMeasurementWidgetElevationProfileWidgetFirstPersonNavWidgetSliceWidgetSunControlWidget
Shipping it
Three things surprise people the first time they put the SDK into a hardened application. All three are one line of configuration.
The short version. Allow 'wasm-unsafe-eval', allow worker-src blob:, and pass workers: false when the SDK itself is loaded
cross-origin from the CDN.
Decoding happens in WebAssembly, usually inside a worker. If your app sends a CSP, these directives are the minimum.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'wasm-unsafe-eval' https://cdn.bitruvius.com;
worker-src blob:;
connect-src 'self' https://cdn.bitruvius.com;const TILESET = 'https://tiles.arcgis.com/tiles/x5wCko8UnSi4h0CB/arcgis/rest/services/CU_Campus_Area3_GaussianSplat/3DTilesServer/tileset.json';
// Same-origin build (npm bundle, or the SDK copied into your own /assets):
// worker decoding is on by default and keeps the main thread free.
map.addLayer(new Tiles3DSplatLayer({ id: 'splats', url: TILESET }));
// Loaded cross-origin straight from cdn.bitruvius.com: the browser refuses to
// start a worker from another origin, so decode inline instead.
map.addLayer(new Tiles3DSplatLayer({ id: 'splats', url: TILESET, workers: false }));<!-- Pinning is the primary control. /sdk/v0.3.1/ is immutable:
a new release is a new directory, never an edit to this one. -->
<script type="module">
import { JxlDecoder } from 'https://cdn.bitruvius.com/sdk/v0.3.1/bundles/bitruvius-turbo-jxl.esm.min.js';
</script>
<!-- SRI is defence in depth against a compromised CDN, and it applies to the
load paths that accept an integrity attribute. A bare ESM `import` is not
one of them, so reach for it on script tags and preloads: -->
<link
rel="modulepreload"
href="https://cdn.bitruvius.com/sdk/v0.3.1/bundles/bitruvius-turbo-jxl.esm.min.js"
integrity="sha384-..."
crossorigin="anonymous"
/># You never compute these. Every file in the release is published with its
# hash, so read the one you need straight out of the manifest.
curl -s https://cdn.bitruvius.com/sdk/v0.3.1/manifest.json | jq '.files["bundles/bitruvius-turbo-jxl.esm.min.js"]'
# { "bytes": 4964, "sri": "sha384-..." }
# Confirm what you are pinned to.
curl -sI https://cdn.bitruvius.com/sdk/v0.3.1/bundles/bitruvius-turbo-jxl.esm.min.jsLive examples
The examples are editable, and they run
Every example opens in a real editor next to a live preview. Change the tileset URL, flip a layer option, break something on purpose, then press Run and it executes in your browser, against the same v0.3.1 bundles documented on this page. Nothing is a screenshot.
Edit, then run
A real editor beside a live preview. Change a tileset URL, flip a layer option, break it on purpose, then press Run and it executes in your browser.
The same bundles
Every example loads the v0.3.1 bundles documented on this page from the CDN, not a vendored copy and not a screenshot.
Copy the whole file
Each example ships its full source and a README, in both JavaScript and TypeScript, ready to lift into your own project.
Live examples
Every layer type running against real published services: Gaussian splats, LiDAR, photogrammetry and city-scale models, with the source for each.
Open the gallery →API reference
Generated for v0.3.1 and published in the same immutable tree as the bundles it documents, so it cannot drift from the release.
Open the reference ↗Support
Bugs, feature requests and integration questions on GitHub Issues and Discussions. It is a public tracker with no SDK source in it — licensing and anything customer-specific goes through the contact form instead.
Open the tracker ↗