Renderização Three.js para MuJoCo
Construa a geometria estática uma vez, depois atualize apenas as transformações a cada quadro. MuJoCo usa Z-up, half-sizes, cilindros e cápsulas alinhados em Z, e quatérnios wxyz. Three.js usa Y-up, tamanhos completos, cilindros alinhados em Y e quatérnios xyzw. Quase todo bug de renderização vem de um desses.
1. Configuração da cena (Z-up, pronta para captura)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // caminho válido em 0.164 e 0.18x
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // necessário para toDataURL
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
const camera = new THREE.PerspectiveCamera(45, w / h, 0.01, 100);
camera.up.set(0, 0, 1); // MuJoCo é Z-up. Com isso, nenhuma conversão de eixo é necessária.
camera.position.set(2, -1.5, 2.5);
const controls = new OrbitControls(camera, renderer.domElement);
const grid = new THREE.GridHelper(5, 50); grid.rotation.x = Math.PI / 2; // coloca a grade no XY
Defina camera.up antes de criar OrbitControls, pois os controles armazenam em cache o vetor up.
2. Escolha uma abordagem
| Abordagem | Como | Quando usar |
|---|---|---|
A. Um Group por corpo (demo Franka) | Cada grupo de corpo contém seus geoms em geom_pos/geom_quat locais. Copie xpos/xquat a cada quadro. | Padrão. Seleção via userData.bodyID. Funciona com qualquer fonte de pose. |
B. mjv_updateScene (demo oficial) | Sincroniza uma malha por entrada em scene.geoms | Você quer as decorações próprias do MuJoCo (contatos, tendões, sites) |
| C. Python remoto | Python executa passos e envia xpos/xquat via WebSocket. O navegador executa a abordagem A. | Pontes GPU, ROS ou robô real, computação pesada |
Conversão de geometria: references/geom-conversion.md. Streaming Python (servidor testado): references/remote-python.md.
Snapshots, câmera de percepção, vídeo e pixel↔3D: references/perception-and-video.md.
3. Sincronização por quadro (abordagem A)
for (let i = 0; i < model.nbody; i++) {
bodies[i].position.set(data.xpos[3*i], data.xpos[3*i+1], data.xpos[3*i+2]);
bodies[i].quaternion.set(data.xquat[4*i+1], data.xquat[4*i+2], data.xquat[4*i+3], data.xquat[4*i]); // wxyz -> xyzw
}
renderer.render(scene, camera);
Ordem do loop: entradas → mj_step (quantas vezes o orçamento de tempo da simulação permitir) → sincronização → renderização
(veja MujocoSim.startLoop). A alternativa por geom usa data.geom_xpos e data.geom_xmat
(3×3 linha principal → Matrix4.set(r00,r01,r02,0, r10,…)), com geometria na pose identidade.
4. Pontos ER ↔ 3D
// ER [y, x] 0-1000 na imagem capturada -> raio da MESMA câmera que capturou
const ndc = new THREE.Vector2((x / 1000) * 2 - 1, 1 - (y / 1000) * 2);
raycaster.setFromCamera(ndc, captureCamera);
const hit = raycaster.intersectObjects(simMeshes, false)[0]; // ou raycaster.ray.intersectPlane(table, out)
// 3D -> ER-style [y, x] (para desenhar expectativas ou construir prompts)
const v = p.clone().project(captureCamera);
const yx = [(1 - v.y) * 500, (v.x + 1) * 500];
Isso é válido para qualquer proporção de aspecto desde que a imagem seja exatamente o viewport completo da câmera (sem corte,
sem letterbox) e a câmera não tenha se movido entre a captura e o raycast. Pule acertos no robô ou no chão
subindo até userData.bodyID. O acerto é na superfície superior, então abaixe-o até a altura da garra.
5. Problemas comuns
| Sintoma | Causa |
|---|---|
| Robô deitado de lado, órbita instável | camera.up não é (0,0,1), ou definido depois de OrbitControls ser criado |
| Links torcidos | Falta reordenar wxyz→xyzw |
| Caixas duas vezes maiores | Esqueceu que os tamanhos são half-extents |
| Cilindros ou cápsulas apontando para o lado errado | Falta geometry.rotateX(Math.PI/2) |
| Snapshot em branco ou preto | preserveDrawingBuffer: false, ou os pixels foram lidos em uma tarefa diferente da renderização |
| Imagem de percepção mais escura que a tela | Os render targets geram cor linear (verificado no three r186). Converta para sRGB (veja a referência de percepção). |
| Marcadores ER deslocados | Câmera moveu, [y, x] trocado, imagem cortada ou com letterbox, ou o canvas com device-pixel-ratio foi redimensionado de forma diferente |
| Memória cresce ao recarregar | geometry.dispose(), material.dispose(), renderTarget.dispose(), e delete o modelo e dados WASM |
| Travamentos em alta velocidade | Muitas chamadas mj_step por quadro. Limite os passos, ou mova a física para um Worker. |
6. No demo Franka
O demo Franka de pick-and-place é o aplicativo de referência a partir do qual estas skills foram construídas. Os nomes de arquivos abaixo se referem a ele.
RenderSystem.tsusapreserveDrawingBuffer: true, umaPerspectiveCamera(45)Z-up,OrbitControlsamortecidos,simGroup(um grupo por corpo,userData.bodyID),erGroup(marcadores cônicos oscilantes), marcadores de contato instanciados, animações de câmera (moveCameraTo), egetCanvasSnapshot(w, h, mime).project2DTo3D(x, y, camPos, lookAt)clona a câmera principal, a posiciona no ponto de vista da captura ((0, -0.01, 2.0)olhando para a origem), e faz raycast nas malhas desimGroup.rendering/GeomBuilder.tslida com a fábrica geom→mesh. Geoms do grupo 3 são ignorados, planos viram um pisoReflector, e geoms de malha leemmesh_vert/mesh_face.DragStateManager.tseSelectionManager.tslidam com seleção por mouse e forças de arrasto através dexfrc_applied.- O
index.htmlimport map fixa three 0.164.1, enquantopackage.jsontem^0.181. Mantenha importsthree/examples/jsm/..., que funcionam em ambos.