@bitruvius/draco
v0.3.1
Published
Draco geometry decoder seam for the Bitruvius glTF, 3D Tiles and I3S pipelines: decode-only Google Draco wasm (KHR_draco_mesh_compression / 3DTILES_draco_point_compression), vendored and hardened
Readme
@bitruvius/draco
Draco geometry decode for glTF, 3D Tiles and I3S, behind one injectable seam.
Internal building block. This package exists so that
@bitruvius/sdk-maplibreand the Bitruvius codecs can resolve their dependencies on npm. It has no standalone product story. Unless you are deliberately building against it, install the SDK instead.
This package wraps the vendored, decode-only Google Draco wasm and exposes it as
the DracoGeometryDecoder contract from
@bitruvius/geo-core. It decodes
KHR_draco_mesh_compression, 3DTILES_draco_point_compression, and I3S Draco
mesh and point geometry into per-semantic typed arrays plus triangle indices.
Why it is its own package
The parsers must not know about the codec. @bitruvius/gltf is a pure
bytes-in parser with no wasm and no DOM. The contract it programs against
(DracoGeometryDecoder) lives in @bitruvius/geo-core, so the parser depends on
an interface and this package supplies the implementation. Inject it and the
compressed path lights up; leave it out and the parser still builds and still
decodes uncompressed tiles.
Three pipelines need Draco and none of them may depend on each other.
@bitruvius/tiles3d-mesh, @bitruvius/tiles3d-points and @bitruvius/i3s all
hit Draco payloads. The I3S engine does not go through the glTF parser at all,
so the decoder cannot live inside @bitruvius/gltf. Keeping it as a leaf
package with one dependency (@bitruvius/geo-core) is what lets all three share
it without a cycle.
One wasm, one instantiation, one license boundary. The Draco module is
initialized lazily and shared process-wide, so a map streaming a 3D Tiles mesh
layer, a pnts layer and an I3S scene layer at the same time downloads and
instantiates the wasm once. That also confines Google's Apache-2.0 code to a
single package with its own wasm/NOTICES, rather than copying it into every
format package.
What is in it
DracoDecoder: the seam implementation, with three entry points.decode()takes the glTF unique-id map from the extension'sattributes.decodeI3sMesh()anddecodeI3sPoints()take a bare buffer, because I3S ships no unique-id map.decodeDracoGeometry(): the synchronous core, for callers that own the module lifecycle themselves.ensureDraco()/dracoWasmUrl(): the wasm host.dracoWasmUrl()exists because a Web Worker cannot resolve the package's siblingwasm/asset, so the main thread hands it the absolute URL.- The wasm itself, published under the
./wasm/*subpath export.
import { DracoDecoder } from '@bitruvius/draco';
import { GltfMeshDecoder } from '@bitruvius/gltf';
// Inject the seam; the glTF parser then handles KHR_draco_mesh_compression for you.
const meshDecoder = new GltfMeshDecoder({ draco: new DracoDecoder() });The wasm resolves from, in precedence: bytes or a URL you pass, the
SDK-configured CDN base (configure({ wasmBaseUrl })), then the package-relative
asset. Point it explicitly when you are running cross-origin:
const draco = new DracoDecoder({
wasmInit: { wasmUrl: 'https://cdn.example.com/draco_decoder.wasm' },
});How it decodes
Attribute extraction uses the heap-pointer fast path (_malloc plus
GetAttributeDataArrayForAllPoints): one bulk copy per attribute, no
per-element marshalling. Geometry semantics come back as Float32Array,
_FEATURE_ID_* as Uint32Array, and the caller applies any glTF accessor
normalized scaling.
I3S is the awkward case and gets a dedicated path. Its attributes are addressed
by Draco type rather than by id, and feature-index and uv-region are both
Draco GENERIC, so a type lookup cannot tell them apart; each is resolved by its
i3s-attribute-type metadata entry. Two decisions there are deliberate and
worth knowing about:
- Normals are not decoded. I3S meshes are baked-lit photogrammetry or flat-shaded from screen-space derivatives, so the renderer never reads them. Skipping them cuts decompression time, the worker transfer, and about 12 bytes per vertex of VRAM on tiles that run 0.1M to 0.8M vertices.
i3s-scale_x/yis returned, not applied. It comes back asscaleXandscaleYfor the caller to fold into the node matrix, which is one matrix edit instead of a full pass over every vertex. Positions stay node-local; the i3s package adds the reference frame.
Who should depend on it
Inside the SDK: @bitruvius/tiles3d-mesh, @bitruvius/tiles3d-points,
@bitruvius/i3s and @bitruvius/sdk-maplibre.
Outside it: only if you are driving @bitruvius/gltf yourself and need the
compressed path. If you are using the SDK, the layers already wire this in, on a
worker, with the wasm URL forwarded for you.
Third-party attribution
The wasm is Google's Draco decoder (draco_decoder.wasm), Apache-2.0,
vendored decode-only (the encoder is absent) and hardened via
vendored-3d-wasm. See wasm/NOTICES, which ships in the published package
alongside the full Apache-2.0 and MIT licence texts. Decoding is free and
ungated, runs entirely client-side, and the encoder is not distributed.
Trademarks
Draco and Google are trademarks of Google LLC. glTF is a trademark of The Khronos Group Inc. Esri, ArcGIS and I3S are trademarks of Environmental Systems Research Institute, Inc. Cesium and 3D Tiles are trademarks of Cesium GS, Inc. MapLibre is a trademark of the MapLibre organization. All other marks are the property of their respective owners.
These names are used solely to describe the data formats this software interoperates with. Bitruvius is not affiliated with, sponsored by, or endorsed by any of them, and no such relationship is implied.
License
Proprietary. The full terms ship as LICENSE inside this package, and are readable
before installing at cdn.bitruvius.com/legal/sdk-license-v1.txt.
© Bitruvius, Inc.
