Three.js + USDC im Browser
Du hast also eine .usdc-Datei aus Maya oder Houdini und möchtest sie in einer Three.js-Szene darstellen. Das kenne ich. Bis vor Kurzem waren die Möglichkeiten nicht besonders gut. Du konntest sie offline in glTF konvertieren, einen WASM-Build von OpenUSD aufsetzen oder einfach aufgeben und ein anderes Format verwenden.
Dieser Leitfaden zeigt dir, wie du USDC mit @cinevva/usdjs direkt in JavaScript parst und die Daten in Three.js überführst.
Was ist USDC?
USD-Dateien gibt es in drei Varianten. USDA ist die Textversion – menschenlesbar und leicht zu debuggen, aber ausführlich. USDC (manchmal „Crate“ genannt) ist das Binärformat, das Produktionspipelines tatsächlich verwenden, weil es kompakt ist und in nativen Tools schnell geladen wird. USDZ ist ein ZIP-Archiv, das USDC und Texturen enthält und von Apple für AR Quick Look verwendet wird.
Der Haken: USDC ist ein proprietäres Binärformat. Pixar hat die Referenzimplementierung in C++ geschrieben, und bis vor Kurzem gab es keine reine JavaScript-Lösung, um es einzulesen. Das beginnt sich zu ändern: Im Dezember 2025 veröffentlichte die Alliance for OpenUSD die Core Specification 1.0, den ersten ratifizierten Standard, der dokumentiert, wie OpenUSD-Szenendaten strukturiert, zusammengesetzt und ausgetauscht werden. Eine Revision 1.1 ist bereits in Arbeit. Die Referenzimplementierung befindet sich jedoch weiterhin in Pixars C++-Codebasis, sodass ein separater Reader nach wie vor der praktikable Weg ist, um aus JavaScript auf das Format zuzugreifen.
Warum das für dich relevant sein könnte
Wenn du 3D-Webanwendungen entwickelst, die mit Inhalten aus Film- oder VFX-Pipelines arbeiten müssen, wirst du auf USD stoßen. Artists exportieren aus Maya, Houdini oder Blender, und diese Exporte liegen häufig als USDC vor.
Vor @cinevva/usdjs hattest du drei Möglichkeiten. Du konntest usdcat ausführen, um USDC in das Textformat umzuwandeln. Das fügt einen Build-Schritt hinzu und beseitigt den Kompaktheitsvorteil des Binärformats. Du konntest OpenUSD oder TinyUSDZ zu WebAssembly kompilieren, was dein Bundle um mehrere Megabyte vergrößert und spezielle Server-Header für Threading erfordert. Oder du konntest den integrierten USDLoader von Three.js verwenden, der USDZ unterstützt, aber nur eingeschränkten USDC-Support und minimale Composition-Funktionen bietet.
Jetzt gibt es eine vierte Möglichkeit: USDC nativ in JavaScript parsen.
So funktioniert es
Die Bibliothek @cinevva/usdjs implementiert die Kernfunktionen von USD in TypeScript neu. Ich habe den USDC-Parser entwickelt, indem ich Pixars C++-Quellcode gelesen und die Spezifikation des Crate-Formats in JavaScript übertragen habe. Wenn ein Verhalten nicht eindeutig ist, orientieren wir uns an OpenUSD.
In der Praxis sieht das so aus:
import { parseUsdcToLayer } from '@cinevva/usdjs';
// Fetch the USDC file
const response = await fetch('/model.usdc');
const buffer = await response.arrayBuffer();
// Parse it
const layer = parseUsdcToLayer(buffer, { identifier: 'model.usdc' });
// Now you have a structured layer with prims, properties, and metadata
console.log(layer.pseudoRoot.children);Kein WASM. Kein nativer Code. Nur JavaScript, das Bytes liest und daraus eine strukturierte Repräsentation erstellt.
Integration in Three.js
USD zu parsen ist nur die Hälfte der Aufgabe. Du musst außerdem USD-Konzepte wie Prims, Transformationen und Mesh-Schemas in Three.js-Objekte umwandeln.
Hier ist ein minimales Beispiel, das eine USDC-Datei lädt und Three.js-Meshes erstellt:
import * as THREE from 'three';
import { UsdStage, parseUsdcToLayer } from '@cinevva/usdjs';
async function loadUsdcToThree(url: string, scene: THREE.Scene) {
// 1. Fetch and parse
const buffer = await fetch(url).then(r => r.arrayBuffer());
const layer = parseUsdcToLayer(buffer, { identifier: url });
// 2. Create a stage (handles composition if there are references)
const stage = UsdStage.open(layer);
// 3. Walk the prim tree
for (const prim of stage.traverse()) {
// Skip non-geometry
if (prim.typeName !== 'Mesh') continue;
// Get geometry data
const points = prim.getAttribute('points')?.value;
const faceVertexIndices = prim.getAttribute('faceVertexIndices')?.value;
const faceVertexCounts = prim.getAttribute('faceVertexCounts')?.value;
if (!points || !faceVertexIndices || !faceVertexCounts) continue;
// Convert to Three.js geometry
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position',
new THREE.Float32BufferAttribute(points.flat(), 3)
);
// USD uses faceVertexCounts + faceVertexIndices, Three.js wants a flat index array
// For triangulated meshes, this is straightforward:
geometry.setIndex(Array.from(faceVertexIndices));
geometry.computeVertexNormals();
// Create mesh
const material = new THREE.MeshStandardMaterial({ color: 0x888888 });
const mesh = new THREE.Mesh(geometry, material);
// Apply transform
const xform = getWorldTransform(prim);
mesh.matrix.fromArray(xform);
mesh.matrixAutoUpdate = false;
scene.add(mesh);
}
}
function getWorldTransform(prim): number[] {
// Simplified: in practice you'd compose parent transforms
const xformOp = prim.getAttribute('xformOp:transform');
if (xformOp?.value) {
return xformOp.value.flat();
}
return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1];
}Dieses Beispiel ist vereinfacht. Echter Produktionscode muss Transform-Stacking berücksichtigen (USD-Prims können mehrere xformOp-Attribute besitzen, die miteinander kombiniert werden), Subdivision Surfaces verarbeiten (Meshes mit catmullClark-Subdivision benötigen eine tatsächliche Unterteilung), Materialien abbilden (UsdPreviewSurface-Parameter werden MeshStandardMaterial von Three.js zugeordnet), Texturen laden (Asset-Pfade müssen aufgelöst und geladen werden) und Skelettanimationen konvertieren (UsdSkel-Bindings müssen in SkinnedMesh überführt werden).
Wenn du sehen möchtest, wie all das zusammenspielt, sieh dir @cinevva/usdjs-viewer an. Es ist eine vollständige Referenzimplementierung.
Composition ist wichtiger, als du vielleicht denkst
Eine einzelne USDC-Datei zu parsen, ist der einfache Teil. Echte USD-Szenen verwenden Composition. Das bedeutet, dass Sublayers, Referenzen, Payloads und Varianten zusammenwirken.
Stell dir ein Automodell vor. Die Karosserie referenziert body.usdc, die Räder referenzieren wheel.usdc, und es gibt Varianten für „Sport“- und „Limousine“-Konfigurationen. Die endgültige Szene wird zur Laufzeit aus all diesen Teilen zusammengesetzt.
So gehst du damit um:
import { UsdStage, parseUsdcToLayer, FetchResolver } from '@cinevva/usdjs';
// Create a resolver that knows how to fetch referenced files
const resolver = new FetchResolver({
baseUrl: '/assets/',
});
// Open with composition
const stage = UsdStage.open(rootLayer, { resolver });
// Set a variant selection
stage.setVariantSelection('/Car', 'bodyStyle', 'sport');
// Now traverse the composed scene
for (const prim of stage.traverse()) {
// You'll see resolved references and the selected variant
}Der Resolver lädt referenzierte USDC-Dateien bei Bedarf. Die Composition erfolgt in JavaScript und erzeugt die flach zusammengeführte Szene, die du auch mit usdcat --flatten erhalten würdest.
Performance
Seien wir ehrlich: USDC in JavaScript zu parsen ist langsamer als in nativem C++. Das ist der Kompromiss, wenn du auf WASM und zusätzliche Build-Schritte verzichtest.
In der Praxis dauert das Parsen bei typischen Web-Anwendungsfällen mit Modellen unter 10 MB und weniger als 100.000 Dreiecken auf moderner Hardware etwa 50 bis 200 ms. Für den initialen Ladevorgang ist das in Ordnung, wenn du eine Ladeanzeige einblendest.
Du kannst es beschleunigen. Lade nicht die gesamte Szene im Voraus. Lade zunächst den Root-Layer, rendere, was bereits verfügbar ist, und rufe Payloads anschließend bei Bedarf ab. Verlege das Parsen in einen Web Worker, damit die Benutzeroberfläche reaktionsfähig bleibt. Speichere geparste Layer in IndexedDB, damit wiederkehrende Besuche sofort geladen werden. Zeige eine niedrig aufgelöste Vorschau an, während die vollständige Szene gestreamt wird.
Hier ist eine Web-Worker-Konfiguration:
// worker.ts
import { parseUsdcToLayer } from '@cinevva/usdjs';
self.onmessage = async (e) => {
const { buffer, identifier } = e.data;
const layer = parseUsdcToLayer(buffer, { identifier });
// Serialize layer data (not the methods)
const data = serializeLayer(layer);
self.postMessage(data);
};
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage({ buffer, identifier: 'model.usdc' });
worker.onmessage = (e) => {
const layerData = e.data;
// Build Three.js scene from layerData
};Was funktioniert und was nicht?
Die meisten gängigen Funktionen werden unterstützt. Geometrie – darunter Meshes, Punkte und Kurven – wird korrekt geparst, einschließlich komprimierter Vertex-Arrays. Transformationen funktionieren mit vollständigem xformOp-Stacking. Materialien werden von UsdPreviewSurface auf PBR abgebildet. Die Composition unterstützt Sublayers, Referenzen, Payloads, Varianten und Vererbung. Texturen werden aufgelöst und geladen, wenn du einen Resolver bereitstellst. Grundlegende Skelettanimationen funktionieren für gängige Charakter-Rigs.
Es gibt jedoch Lücken. Dies ist keine Hydra-Implementierung, daher bist du selbst dafür verantwortlich, USD-Daten in das Format der von dir verwendeten Rendering-Engine umzuwandeln. Es gibt keine typisierten Schema-APIs wie UsdGeomMesh mit Komfortmethoden. Du arbeitest mit generischen Prims und Attributen. Einige Composition-Funktionen wie Specializes, Relocates und Value Clips sind noch nicht implementiert. Komplexe Materialnetzwerke, die über ein einfaches UsdPreviewSurface hinausgehen, erfordern eine individuelle Verarbeitung.
Wann du das verwenden solltest
Diese Lösung ist sinnvoll, wenn du USD-Dateien ohne WASM-Build-Schritt in einer Webanwendung laden musst, deine Pipeline USDC ausgibt und du nicht in glTF konvertieren möchtest, du einen USD-Viewer oder -Editor für den Browser entwickelst oder du die USD-Struktur untersuchen und nicht nur rendern möchtest.
Verwende eine andere Lösung, wenn du vollständige OpenUSD-Kompatibilität für jeden Sonderfall benötigst, deine Szenen sehr groß sind (über 100 MB) und native Performance erfordern oder du bereits einen WASM-Build verwendest und dessen Komplexität für dein Projekt akzeptabel ist.
Ressourcen
Die drei Pakete sind @cinevva/usdjs für das grundlegende Parsen und die Composition, @cinevva/usdjs-viewer für einen Three.js-basierten Browser-Viewer und @cinevva/usdjs-renderer für das Headless-Rendering von PNG-Dateien in Tests.
Weitere Informationen findest du in der usdjs-API-Referenz, der OpenUSD-Spezifikation von Pixar und der Three.js-Dokumentation.
Wenn du verstehen möchtest, wie der USDC-Parser Pixars Implementierung abbildet, sieh dir src/usdc/PIXAR_PARITY.md im usdjs-Repository an.
Probiere es aus
Am schnellsten kannst du die Funktionsweise testen, indem du die usdjs-viewer-Demo aufrufst und eine USDC-Datei darauf ziehst.
Um das Paket in dein eigenes Projekt zu integrieren, installiere es und beginne mit den obigen Codebeispielen:
npm install @cinevva/usdjsUSD im Browser ist möglich. Es ist nicht immer die richtige Wahl, aber wenn du es benötigst, bedeutet reines JavaScript-Parsing einen Build-Schritt und eine Abhängigkeit weniger, um die du dich kümmern musst.
Verwandte Themen
- WebGL-Grundlagen für Spieleentwickler — die Rendering-API hinter Three.js-Szenen
- Technologie-Stack für Webspiele im Jahr 2026 — wo Three.js in die WebGL-/WebGPU-/Wasm-Landschaft passt
- Technik für offene 3D-Welten im Browser — Streaming von 3D-Assets einschließlich USD für offene Welten
- Wo du kostenlose Spiel-Assets findest — Quellen für mit Three.js kompatible 3D-Modelle