浏览器中的 Three.js + USDC
你从Maya或Houdini拿到了一个 .usdc 文件,想把它显示在Three.js场景里。我也走过这条路。直到不久之前,你的选择都不太好。你可以离线转成glTF,或者起一个OpenUSD的WASM构建,或者干脆放弃换格式。
这份指南讲怎么用 @cinevva/usdjs 直接在JavaScript里解析USDC,以及怎么把数据塞进Three.js。
USDC是什么?
USD文件有三种风味。USDA是文本版,可读、好调试,但啰嗦。USDC(有时叫"Crate")是二进制格式,生产管线实际在用的就是它,因为它紧凑、在原生工具里加载快。USDZ是包含USDC加贴图的zip包,Apple用它做AR Quick Look。
问题是:USDC是专有二进制格式。Pixar用C++写了参考实现,到现在为止都没有纯JavaScript的方式去读它。
你为什么会在意
如果你在做要对接电影或VFX管线内容的3D Web应用,你迟早会碰到USD。美术师从Maya、Houdini或Blender导出,导出的常常就是USDC。
在 @cinevva/usdjs 出现之前你有三个选项。你可以跑 usdcat 把USDC转成文本格式,多一步构建,丢掉二进制的紧凑性。你可以把OpenUSD或TinyUSDZ编到WebAssembly,给你的Bundle加几MB,并且要为线程开启特殊的服务器头。或者你可以用Three.js自带的USDLoader,它处理USDZ但对USDC支持有限,组合能力也少。
现在有第四个选项:在JavaScript里原生解析USDC。
它怎么工作
@cinevva/usdjs 库用TypeScript重新实现了USD的核心功能。我读了Pixar的C++源码并把Crate格式规范翻译到JavaScript,做出了USDC解析器。行为有歧义时,我们和OpenUSD保持一致。
实际用起来是这样:
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);没有WASM,没有原生代码。只是JavaScript在读字节、构建结构化表示。
接到Three.js里
解析USD只解决了一半问题。你还得把USD概念(Prim、变换、Mesh Schema)转成Three.js对象。
下面是一个最小示例,加载USDC文件并创建Three.js Mesh:
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];
}这个例子是简化的。真实代码要处理变换叠加(USD的Prim可以有多个 xformOp 属性,它们组合到一起)、细分曲面(带 catmullClark 细分的Mesh需要真正做细分)、材质(UsdPreviewSurface 参数映射到Three.js的 MeshStandardMaterial)、纹理(资源路径要解析并加载)、骨骼动画(UsdSkel 绑定要转成 SkinnedMesh)。
要看这一切是怎么结合在一起的,看 @cinevva/usdjs-viewer。它是一个完整的参考实现。
组合比你想的更重要
解析单个USDC文件是简单的部分。真实USD场景用组合,这意味着子层(sublayers)、引用(references)、payload、变体(variants)一起协作。
想象一辆汽车模型。车身引用 body.usdc,车轮引用 wheel.usdc,还有"运动"和"轿车"两种变体配置。最终场景是在运行时由这些片段拼装出来的。
这里是怎么处理:
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
}Resolver按需取被引用的USDC文件。组合在JavaScript里发生,产生你用 usdcat --flatten 会得到的扁平化场景。
性能
实话说:在JavaScript里解析USDC比原生C++慢。这是绕过WASM和构建步骤的代价。
实际情况下,对于典型的Web用例——10MB以下、不到10万三角形的模型——在现代硬件上解析耗时50-200ms。如果你显示一个加载指示器,作为初次加载是没问题的。
你能让它更快。不要一开始就加载整个场景。加载根层,能渲染什么先渲染,然后按需取payload。把解析挪到Web Worker,让UI保持响应。在IndexedDB里缓存解析过的层,让回访玩家瞬开。在完整场景流入时先显示一个低清预览。
下面是一个Web Worker的写法:
// 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
};什么能用,什么不能
大多数常见的东西都能用。几何(Mesh、点、曲线)能正确解析,包括压缩的顶点数组。变换支持完整的 xformOp 叠加。材质从 UsdPreviewSurface 映射到PBR。组合处理子层、引用、payload、变体和继承。如果你提供Resolver,纹理能解析并加载。常见角色绑定的基础骨骼动画能用。
但也有缺口。这不是Hydra实现,所以你要自己把USD数据转到你用的渲染引擎里去。没有像 UsdGeomMesh 那样带便捷方法的类型化Schema API,你要处理的是通用Prim和属性。某些组合特性比如specializes、relocates和value clips还没实现。超出简单 UsdPreviewSurface 的复杂材质网络需要自己处理。
什么时候用这套
当你需要在Web应用里加载USD文件但又不想要WASM构建步骤、你的管线输出USDC而你又不想转成glTF、你在为浏览器做USD查看器或编辑器、或者你想检视USD结构而不只是渲染它——这套就有意义。
别用它,如果你需要和OpenUSD每个边界情况都对齐、你的场景巨大(100MB+)需要原生性能、或者你已经在用WASM构建并且接受了那种复杂度。
资源
三个包:@cinevva/usdjs 做核心解析和组合,@cinevva/usdjs-viewer 是基于Three.js的浏览器查看器,@cinevva/usdjs-renderer 在测试里做无头PNG渲染。
文档可看 usdjs API 参考、Pixar OpenUSD 规范 和 Three.js 文档。
想了解USDC解析器怎么对齐Pixar实现,看usdjs仓库里的 src/usdc/PIXAR_PARITY.md。
试试看
最快的方式是访问 usdjs-viewer demo 然后把USDC文件拖进去。
把它集成到你自己的项目里,安装包然后从上面的代码示例开始:
npm install @cinevva/usdjsUSD能在浏览器里跑。它不一定总是对的选择,但当你需要它时,纯JavaScript解析意味着少一步构建、少一个依赖要操心。
相关阅读
- 游戏开发者的WebGL基础 —— 驱动Three.js场景的渲染API
- 2026年网页游戏技术栈 —— Three.js在WebGL/WebGPU/Wasm版图里的位置
- 浏览器3D开放世界技术 —— 开放世界中3D资源(包括USD)的流式传输
- 哪里可以找到免费游戏资源 —— 兼容Three.js的3D模型来源