Module: USD command-buffer loader
Package path:
packages/babylon-lite/src/loader-usd/
Purpose
Load USD, USDA, USDC and USDZ using the same prebuilt OpenUSD WebAssembly extractor as Babylon.js. There is no GLB or Babylon JSON intermediate and no dependency on Babylon.js. The C++ implementation and protocol-v5 runtime are shared with Babylon.js from https://github.com/BabylonJS/babylon-usd-importer. Lite does not fork or recompile the extractor.
The loader creates ordinary Lite scene nodes, meshes and PBR materials. It
never mutates a scene: the caller owns addToScene(scene, result). Public
results are plain state and operations are standalone functions.
Public API surface
loadUsd(engine: EngineContext, source: string | UsdBinaryInput, options?: LoadUsdOptions): Promise<UsdAssetContainer>;disposeUsd(container: UsdAssetContainer): void;LoadUsdOptions accepts rootFileName, a files map of relative virtual paths
to ArrayBuffer/ArrayBufferView/Blob data, resolveByFileName (default true),
runtimeBaseUrl (default
https://cdn.babylonjs.com/babylonUsdImporter/5/), an AbortSignal,
onProgress and onLog. The root name is inferred from URL/File names;
extensionless binary input is identified by USDZ ZIP or USDC crate magic.
Source buffers are copied before transferring, never detached from their owner.
Paths preserve case and directory structure; absolute paths and .. are rejected.
Sidecars are explicitly supplied, never silently fetched from arbitrary URLs.
UsdAssetContainer extends AssetContainer with import diagnostics (worker
timings, statistics, missing assets and materialization duration) and internal
ownership state. disposeUsd releases the container's GPU resources
idempotently; remove it from any scenes first. It does not dispose the engine
or change scene-global material/texture caches.
Internal architecture
Worker boundary
Each load starts one module worker, posts an extract request with ID 1, and
terminates it after extraction or on any failure/cancellation. The shipped
worker stages files in its own Emscripten filesystem, opens the composed stage
(without flattening), and returns two transferable ArrayBuffers. Log/progress
events are forwarded. Worker exceptions and messageerror reject the load.
Cross-origin worker URLs use a revocable same-origin module bootstrap URL.
The default worker, JS glue, WASM and data files are the same immutable
protocol-v5 assets used by Babylon.js. They are runtime downloads, not imports
from Lite's root entry or inline JS assets. Applications may self-host the four
files together and set runtimeBaseUrl; mixing protocol versions rejects
before any GPU resources are allocated.
Protocol
Little-endian command header: magic 0x42445355, uint16 version 5,
uint16 reserved, uint32 command count, uint32 reserved (16 bytes total).
Each command has uint16 opcode, uint16 flags, uint32 payload length, then
its payload. Unknown versions/opcodes, incorrect payload lengths, truncation,
trailing bytes, invalid IDs and out-of-range/alignment references reject.
The missing-reference sentinel is 0xffffffff. Strings are UTF-8 slices.
Numbers and matrices reference aligned spans of the raw-data buffer.
OpenUSD GfMatrix uses row vectors and the native writer serializes its rows in
order. Lite uses column vectors and column-major storage, so the same flat
16-float sequence is already the required mathematical transpose: translations
occupy indices 12-14 in both representations. Node, joint, animation and thin
instance matrices therefore use zero-copy views; rearranging rows and columns
would transpose them a second time.
| Opcode | Payload bytes | Contents |
|---|---|---|
| 1 Scene | 12 | Z-up flag, meters per unit, time codes per second |
| 2 Texture | 48 | ID, name slice, MIME, image slice, UV set, UV transform, U/V wrapping, source color space and float4 scale/bias |
| 3 Material | 96 | ID, name, factors and flags followed by seven independent texture IDs and output channels |
| 4 Node | 20 | ID, parent ID, name slice, affine local matrix offset |
| 5 Skeleton | 20 | ID, name, joint count and six-word joint-record offset (parent, bone ID, name, rest matrix, bind matrix) |
| 6 Geometry | 60 | ID, counts, flags, positions/normals/tangents/UV/colors, two skin streams, indices, influences |
| 7 Mesh | 40 | ID, node, geometry, material, name, flags, skeleton, subset records |
| 8 Instance | 16 | Source mesh ID, placement node ID, name |
| 9 Animation | 32 | Target kind/ID/property, group, key count, time/value offsets, stride |
| 10 Analytic | 44 | ID, node, shape, material, name, flags, axis, radius/size, height, tessellation |
| 11 ThinInstances | 12 | Source mesh ID, contiguous matrix offset and instance count |
| 12 MorphTarget | 32 | ID, source mesh, name, vertex count, position/normal offsets and initial influence |
Read commands once into immutable records, then construct resource maps. The core dispatches optional commands to lazily imported feature modules: textures/material features, analytic geometry, classic instances, thin instances, skeletons, morph targets and animation. Static untextured meshes do not fetch those feature modules.
Geometry and hierarchy
Node matrices use createSceneNodeFromMatrix to preserve shear. A synthetic
root applies meters-per-unit, USD Z-up rotation and right-to-left-handed
conversion exactly once. Meshes are identity children of placement nodes.
Bounds remain geometry-local. Winding is converted once into Lite's existing
CCW pipeline convention, not hidden by globally disabling culling.
Geometry arrays are typed views where possible. Single-material meshes take
the direct upload path. Material subsets become child meshes, using compact
indexed streams rather than uploading the entire source mesh for every subset.
Repeated placements share the same immutable MeshGPU and CPU streams with
Lite's existing retain/release ownership. Each placement retains its own
transform and name. Point-instancer batches attach the extractor's contiguous
float4x4 span directly with setThinInstances, producing one instanced draw per
prototype rather than one Lite mesh per point.
Analytic cube, sphere, cylinder and cone records use Lite's geometry builders; axis correction is local to the shape, not applied to its children. Lite's builders are left-handed, so their indices are reversed only for the protocol's default right-handed primitive records.
Materials and textures
Create Lite PBR metallic/roughness props directly. Optional alpha testing, emission, unlit and UV transforms go through their existing opt-in setters. Image decoding is asynchronous, uses straight alpha and explicit source color space, and closes decoded bitmaps. Texture objects are shared by protocol ID. USD's seven independent Preview Surface bindings are represented by a loader-owned PBR material plugin. The plugin samples each authored output channel independently, applies its float4 scale/bias and UV transform, and overrides base color, opacity, metallic, roughness, occlusion and emissive values before lighting. The occlusion override runs after the native PBR path initializes its mutable occlusion value, including metallic-reflectance variants, so plugin composition never assigns before declaration. Normal bindings apply their authored transform in Lite's cotangent-frame normal path. Plugin registration occurs only when a loaded USD material needs it, so non-USD and untextured USD scenes retain no plugin bridge. The process-wide plugin signature table is append-only: registering a USD plugin cannot invalidate the cached signature of a material already rendered by another scene. Cache entries retain only generated shader fragments, never material-owned plugin instances or their extraction-buffer views, so disposed containers remain collectible while signature indices stay reusable. A positive opacity cutoff disables alpha blending and uses the alpha-test path, matching Babylon.js depth-write behavior.
Skinning and animation
Joint records store parent index, global protocol bone ID, name, local rest matrix and local bind matrix. Parents precede children. The loader accumulates the local bind hierarchy before inversion, so inverse bind matrices come from the authored bind pose rather than being inferred from the rest pose; mesh skin streams are retained through subset remapping.
The extractor applies each skinned mesh's geomBindTransform to its vertices
and changes its placement node to the bound Skeleton prim. Joint world matrices
are therefore Skeleton-object-space matrices and the shader computes
skeletonNodeWorld * (jointObject * inverseBind) * position. Applying the
generic glTF inverse(meshWorld) term would incorrectly cancel the authored
Skeleton prim placement.
Morph records contain absolute target positions/normals. The loader remaps them
through each compact material subset, converts them to Lite deltas and creates
the existing storage-buffer-backed MorphTargetData. There is no fixed target
count. Classic instances retain the source skeleton and morph buffers; point
instances retain them on the prototype draw.
Animation modules convert time codes to seconds, preserve target IDs and local
affine matrices, and return ordinary AnimationGroups. Each group retains its
authored first and last sample times, including positive nonzero and negative
ranges; direct and manager-driven playback both start, clamp and loop within
that interval. A singleton track has a zero-length interval and evaluates its
sole sample at the authored start time instead of becoming a no-op.
goToFrame continues to address authored time codes. USD
channels also expose Lite property-mixer tracks with stable target identities and names, so
AnimationGroupMask, zero/partial weights and the existing
enableAnimationBlending manager path apply normally. Native 16-float matrix
tracks blend all affine components against the authored pose and normalize
overweight sums. This preserves exact full-weight matrices, including shear,
while keeping partial-weight matrices affine. If public reparenting followed by
a TRS edit hands a matrix-animated target back to TRS, its next matrix sample
recreates raw matrix storage and invalidates the world matrix and affected rig.
Track writers update CPU state first; a deduplicated post-write publication uploads each affected rig and morph
buffer once after all groups have been evaluated. Mixer scratch is released
when its groups leave the manager, so a cleared long-lived manager does not
retain a disposed USD context. Paused groups whose sample has not changed
perform no GPU upload. Installing USD's weighted-property integration does not
change ordinary full-weight property groups: uncontested tracks retain direct
controller behavior, paused-frame behavior and registration-order last-write
semantics relative to glTF groups, even when another track in the same group is
blended. A full-weight track joins weighted accumulation only when a fractional
track genuinely contests the same target property, preventing the fractional
bucket from overwriting its contribution. Groups are not
automatically played by the loader.
Pipeline configuration and shader logic
No USD-specific renderer or pipeline family is introduced. Lite's existing
PBR, skinning, morph and instancing builders own pipeline creation. Textured
Preview Surface materials attach a loader-owned MaterialPlugin; its generated
WGSL samples only the authored channels and its signature participates in the
existing material pipeline cache. Untextured USD assets load no plugin bridge
or plugin shader code.
State machine / lifecycle
Fetch/stage -> worker extraction -> validate commands -> materialize -> return. Every asynchronous boundary observes cancellation. On abort or error, terminate the worker, revoke bootstrap/image URLs, close decoded images, and release all partially created meshes, skin resources and textures. Late decode completion must release its result rather than attach resources to an abandoned load. Successful results transfer ownership to the caller.
Babylon.js equivalence map
TransformNode -> Lite SceneNode with raw matrix; Mesh/InstancedMesh -> distinct Lite Mesh nodes with shared geometry; MultiMaterial/SubMesh -> child Mesh per material subset; PBRMaterial -> native PbrMaterialProps; Skeleton/AnimationGroup -> native Lite skeleton data and animation groups. No Babylon.js TypeScript classes or scene registration machinery are copied.
Dependencies
Existing Lite scene, mesh, PBR, texture, math and resource helpers. Optional feature modules depend on their matching Lite subsystems. OpenUSD binaries are external runtime assets with their original license notices. No new runtime package dependency or package subpath export is required.
Test specification
Unit tests cover command validation, typed-array bounds, hierarchy/units/up-axis, winding, material subsets, independent material channels, shared geometry and ownership, analytic shapes, thin instances, bind/rest skeletons, morph targets and masked/weighted influence animation, cancellation, malformed worker messages and virtual file sets. Browser plumbing tests use the real shared worker with public synthetic USDA, USDC and USDZ fixtures.
A paired lab scene loads the same point-instanced USD through Babylon.js and Lite. Its machine-readable counters assert semantic parity (source meshes, instances, translations and triangles), while the existing Lite-vs-Babylon.js RAF benchmark collects initialization time, frame cost, draw calls and JS heap usage for the same scene. Paired runtime screenshot tests compare Lite directly against Babylon.js for independent Preview Surface texture channels, right/left analytic winding, and a posed skinned animation. This equivalent MAD coverage does not add or modify committed golden baselines. Root-entry build tests prove unrelated imports do not retain the USD loader or external runtime.
File manifest
load-usd.ts (public entry and lifetime), usd-types.ts (public state),
usd-worker-client.ts (extraction transport), usd-protocol.ts (checked binary
reader), usd-materialize.ts (core scene assembly), optional usd-*.ts feature
modules, unit tests under tests/lite/unit/, browser comparison tests under
tests/lite/plumbing/, and paired lab scenes under lab/lite/src/.