Skip to content

Three.js + USDC no navegador

Então você tem um arquivo .usdc vindo do Maya ou Houdini e quer exibi-lo em uma cena do Three.js. Eu já passei por isso. Até pouco tempo atrás, as opções não eram muito boas. Você podia converter para glTF offline, criar uma compilação WASM do OpenUSD ou simplesmente desistir e usar outro formato.

Este guia mostra como analisar USDC diretamente em JavaScript usando @cinevva/usdjs e como levar esses dados para o Three.js.

O que é USDC?

Os arquivos USD têm três variações. USDA é a versão em texto, legível por humanos e fácil de depurar, mas verbosa. USDC (às vezes chamado de "Crate") é o formato binário realmente usado em pipelines de produção porque é compacto e carrega rapidamente em ferramentas nativas. USDZ é um arquivo zip que contém USDC e texturas, usado pela Apple no AR Quick Look.

O problema é o seguinte: USDC é um formato binário proprietário. A Pixar escreveu a implementação de referência em C++ e, até agora, não havia uma forma de lê-lo usando JavaScript puro. Isso está começando a mudar: em dezembro de 2025, a Alliance for OpenUSD publicou a Core Specification 1.0, o primeiro padrão ratificado que documenta como os dados de cena do OpenUSD são estruturados, compostos e trocados, com uma revisão 1.1 já em andamento. Porém, a implementação de referência ainda está na base de código C++ da Pixar, então um leitor separado continua sendo a forma mais prática de acessar o formato com JavaScript.

Por que isso pode ser importante para você

Se você está criando aplicativos web 3D que precisam trabalhar com conteúdo vindo de pipelines de cinema ou efeitos visuais, inevitavelmente encontrará USD. Artistas exportam do Maya, Houdini ou Blender, e essas exportações frequentemente estão em USDC.

Antes do @cinevva/usdjs, você tinha três opções. Podia executar usdcat para converter USDC para o formato de texto, o que adiciona uma etapa ao processo de build e elimina a compactação do formato binário. Podia compilar OpenUSD ou TinyUSDZ para WebAssembly, o que adiciona megabytes ao seu bundle e exige cabeçalhos especiais no servidor para threading. Ou podia usar o USDLoader integrado do Three.js, que aceita USDZ, mas tem suporte limitado a USDC e recursos mínimos de composição.

Agora existe uma quarta opção: analisar USDC nativamente em JavaScript.

Como funciona

A biblioteca @cinevva/usdjs reimplementa as principais funcionalidades do USD em TypeScript. Criei o analisador de USDC estudando o código-fonte C++ da Pixar e traduzindo a especificação do formato Crate para JavaScript. Quando há alguma ambiguidade de comportamento, reproduzimos o que o OpenUSD faz.

Na prática, fica assim:

typescript
import { parseUsdcToLayer } from '@cinevva/usdjs';

// Busca o arquivo USDC
const response = await fetch('/model.usdc');
const buffer = await response.arrayBuffer();

// Analisa o arquivo
const layer = parseUsdcToLayer(buffer, { identifier: 'model.usdc' });

// Agora você tem uma camada estruturada com prims, propriedades e metadados
console.log(layer.pseudoRoot.children);

Sem WASM. Sem código nativo. Apenas JavaScript lendo bytes e construindo uma representação estruturada.

Integrando ao Three.js

Analisar USD é apenas metade do problema. Você também precisa converter os conceitos do USD (prims, transformações e esquemas de malha) em objetos do Three.js.

Aqui está um exemplo mínimo que carrega um arquivo USDC e cria malhas do Three.js:

typescript
import * as THREE from 'three';
import { UsdStage, parseUsdcToLayer } from '@cinevva/usdjs';

async function loadUsdcToThree(url: string, scene: THREE.Scene) {
  // 1. Busca e analisa
  const buffer = await fetch(url).then(r => r.arrayBuffer());
  const layer = parseUsdcToLayer(buffer, { identifier: url });
  
  // 2. Cria um stage (gerencia a composição se houver referências)
  const stage = UsdStage.open(layer);
  
  // 3. Percorre a árvore de prims
  for (const prim of stage.traverse()) {
    // Ignora o que não for geometria
    if (prim.typeName !== 'Mesh') continue;
    
    // Obtém os dados da geometria
    const points = prim.getAttribute('points')?.value;
    const faceVertexIndices = prim.getAttribute('faceVertexIndices')?.value;
    const faceVertexCounts = prim.getAttribute('faceVertexCounts')?.value;
    
    if (!points || !faceVertexIndices || !faceVertexCounts) continue;
    
    // Converte para a geometria do Three.js
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', 
      new THREE.Float32BufferAttribute(points.flat(), 3)
    );
    
    // USD usa faceVertexCounts + faceVertexIndices; o Three.js espera um array plano de índices
    // Para malhas trianguladas, isso é simples:
    geometry.setIndex(Array.from(faceVertexIndices));
    geometry.computeVertexNormals();
    
    // Cria a malha
    const material = new THREE.MeshStandardMaterial({ color: 0x888888 });
    const mesh = new THREE.Mesh(geometry, material);
    
    // Aplica a transformação
    const xform = getWorldTransform(prim);
    mesh.matrix.fromArray(xform);
    mesh.matrixAutoUpdate = false;
    
    scene.add(mesh);
  }
}

function getWorldTransform(prim): number[] {
  // Simplificado: na prática, você faria a composição das transformações dos pais
  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];
}

Este exemplo é simplificado. Um código real precisa lidar com o empilhamento de transformações (os prims do USD podem ter vários atributos xformOp compostos entre si), superfícies de subdivisão (malhas com subdivisão catmullClark precisam de subdivisão real), materiais (parâmetros de UsdPreviewSurface são mapeados para MeshStandardMaterial do Three.js), texturas (caminhos de assets precisam ser resolvidos e carregados) e animação esquelética (vínculos de UsdSkel precisam ser convertidos em SkinnedMesh).

Se quiser ver como tudo isso funciona em conjunto, confira o @cinevva/usdjs-viewer. Ele é uma implementação de referência completa.

A composição é mais importante do que parece

Analisar um único arquivo USDC é a parte fácil. Cenas USD reais usam composição, o que significa que subcamadas, referências, payloads e variantes trabalham em conjunto.

Imagine o modelo de um carro. A carroceria referencia body.usdc, as rodas referenciam wheel.usdc e há variantes para as configurações "sport" e "sedan". A cena final é montada com todas essas partes em tempo de execução.

Veja como lidar com isso:

typescript
import { UsdStage, parseUsdcToLayer, FetchResolver } from '@cinevva/usdjs';

// Cria um resolver que sabe como buscar os arquivos referenciados
const resolver = new FetchResolver({
  baseUrl: '/assets/',
});

// Abre com composição
const stage = UsdStage.open(rootLayer, { resolver });

// Define a seleção de uma variante
stage.setVariantSelection('/Car', 'bodyStyle', 'sport');

// Agora percorre a cena composta
for (const prim of stage.traverse()) {
  // Você verá as referências resolvidas e a variante selecionada
}

O resolver busca os arquivos USDC referenciados sob demanda. A composição acontece em JavaScript, produzindo a cena achatada que você obteria com usdcat --flatten.

Desempenho

Vamos ser honestos: analisar USDC em JavaScript é mais lento do que em C++ nativo. Essa é a contrapartida de dispensar WASM e etapas de build.

Na prática, para casos de uso típicos na web, com modelos de menos de 10 MB e menos de 100 mil triângulos, a análise leva de 50 a 200 ms em hardware moderno. Isso é aceitável para o carregamento inicial se você exibir um indicador de carregamento.

Você pode tornar o processo mais rápido. Não carregue a cena inteira de uma vez. Carregue a camada raiz, renderize o que for possível e depois busque os payloads sob demanda. Mova a análise para um Web Worker para manter a interface responsiva. Armazene as camadas analisadas em cache no IndexedDB para que visitas posteriores sejam instantâneas. Exiba uma prévia em baixa resolução enquanto a cena completa é transmitida.

Veja uma configuração com Web Worker:

typescript
// worker.ts
import { parseUsdcToLayer } from '@cinevva/usdjs';

self.onmessage = async (e) => {
  const { buffer, identifier } = e.data;
  const layer = parseUsdcToLayer(buffer, { identifier });
  
  // Serializa os dados da camada (não os métodos)
  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;
  // Constrói a cena do Three.js usando layerData
};

O que funciona e o que não funciona

A maioria dos recursos comuns funciona. Geometrias (malhas, pontos e curvas) são analisadas corretamente, incluindo arrays de vértices compactados. As transformações funcionam com o empilhamento completo de xformOp. Materiais são mapeados de UsdPreviewSurface para PBR. A composição aceita subcamadas, referências, payloads, variantes e herança. Texturas são resolvidas e carregadas se você fornecer um resolver. A animação esquelética básica funciona para rigs comuns de personagens.

Porém, ainda há lacunas. Esta não é uma implementação do Hydra, então você é responsável por converter os dados USD para o mecanismo de renderização que estiver usando. Não há APIs de esquemas tipados, como UsdGeomMesh, com métodos utilitários. Você trabalha com prims e atributos genéricos. Alguns recursos de composição, como specializes, relocates e value clips, ainda não foram implementados. Redes de materiais complexas que vão além de um UsdPreviewSurface simples precisam de tratamento personalizado.

Quando usar esta solução

Esta solução faz sentido quando você precisa carregar arquivos USD em um aplicativo web sem uma etapa de build com WASM; quando seu pipeline gera USDC e você não quer converter para glTF; quando está criando um visualizador ou editor de USD para o navegador; ou quando quer inspecionar a estrutura do USD, em vez de apenas renderizá-lo.

Use outra solução se precisar de compatibilidade total com o OpenUSD em todos os casos extremos, se suas cenas forem enormes (mais de 100 MB) e exigirem desempenho nativo ou se você já estiver usando uma compilação WASM e a complexidade for aceitável para seu projeto.

Recursos

Os três pacotes são @cinevva/usdjs, para análise e composição principais; @cinevva/usdjs-viewer, para um visualizador no navegador baseado em Three.js; e @cinevva/usdjs-renderer, para renderização headless de PNG em testes.

Para consultar a documentação, veja a Referência da API do usdjs, a Especificação do OpenUSD da Pixar e a Documentação do Three.js.

Se quiser entender como o analisador de USDC corresponde à implementação da Pixar, consulte src/usdc/PIXAR_PARITY.md no repositório do usdjs.

Experimente

A maneira mais rápida de ver isso funcionando é acessar a demonstração do usdjs-viewer e soltar um arquivo USDC nela.

Para integrar ao seu próprio projeto, instale o pacote e comece pelos exemplos de código acima:

bash
npm install @cinevva/usdjs

USD no navegador é possível. Nem sempre é a escolha certa, mas, quando você precisa dele, a análise com JavaScript puro significa uma etapa de build a menos e uma dependência a menos com que se preocupar.

Conteúdo relacionado