Rendu Three.js pour MuJoCo
Construisez la géométrie statique une fois, puis mettez à jour uniquement les transformations à chaque frame. MuJoCo est Z-up, utilise des demi-tailles, des cylindres et capsules alignés sur Z, et des quaternions wxyz. Three.js utilise par défaut Y-up, des tailles pleines, des cylindres alignés sur Y et des quaternions xyzw. Presque tous les bogues de rendu proviennent de l'un de ces points.
1. Configuration de la scène (Z-up, prête pour la capture)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // chemin valide sur 0.164 et 0.18x
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // nécessaire pour 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 est Z-up. Avec cela, aucune conversion d'axe n'est nécessaire nulle part.
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; // poser la grille sur XY
Définissez camera.up avant de créer OrbitControls, car les contrôles mettent en cache le vecteur up.
2. Choisir une approche
| Approche | Comment | Quand l'utiliser |
|---|---|---|
A. Un Group par corps (la démo Franka) | Chaque groupe de corps contient ses geoms à leur geom_pos/geom_quat local. Copiez xpos/xquat à chaque frame. | Par défaut. Picking via userData.bodyID. Fonctionne avec n'importe quelle source de pose. |
B. mjv_updateScene (démo officielle) | Synchronisez un maillage mutualisé par entrée scene.geoms | Vous voulez les décorations propres à MuJoCo (contacts, tendons, sites) |
| C. Python distant | Python effectue les étapes et streame xpos/xquat via WebSocket. Le navigateur exécute A. | Ponts GPU, ROS ou robot réel, calcul lourd |
Conversion de geom : references/geom-conversion.md. Streaming Python (serveur testé) : references/remote-python.md.
Instantanés, caméra de perception, vidéo et pixel↔3D : references/perception-and-video.md.
3. Synchronisation par frame (approche 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);
Ordre de la boucle : entrées → mj_step (autant de fois que le budget de temps de simulation le permet) → synchronisation → rendu
(voir MujocoSim.startLoop). L'alternative par geom utilise data.geom_xpos et data.geom_xmat
(matrice 3×3 ligne-majeure → Matrix4.set(r00,r01,r02,0, r10,…)), avec la géométrie à la pose identité.
4. Points ER ↔ 3D
// ER [y, x] 0-1000 sur l'image capturée -> rayon depuis la MÊME caméra qui l'a capturée
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 -> [y, x] style ER (pour dessiner les attentes ou construire des prompts)
const v = p.clone().project(captureCamera);
const yx = [(1 - v.y) * 500, (v.x + 1) * 500];
Ceci est valable pour n'importe quel rapport d'aspect tant que l'image correspond exactement à la fenêtre d'affichage complète de la caméra (pas de recadrage, pas de letterbox) et que la caméra n'a pas bougé entre la capture et le raycast. Ignorez les hits sur le robot ou le sol en remontant jusqu'à userData.bodyID. Le hit est sur la surface supérieure, abaissez-le donc à la hauteur de préhension.
5. Pièges
| Symptôme | Cause |
|---|---|
| Le robot est couché sur le côté, l'orbite tangué | camera.up n'est pas (0,0,1), ou défini après la création d'OrbitControls |
| Les liaisons tordues | Réordonnancement wxyz→xyzw manquant |
| Boîtes deux fois plus grandes | Oubli que les tailles sont des demi-étendues |
| Cylindres ou capsules orientés du mauvais côté | geometry.rotateX(Math.PI/2) manquant |
| Instantané vide ou noir | preserveDrawingBuffer: false, ou les pixels ont été lus dans une tâche différente de celle du rendu |
| Image de perception plus sombre que l'écran | Les cibles de rendu produisent une couleur linéaire (vérifié dans three r186). Convertissez en sRGB (voir la référence de perception). |
| Marqueurs ER décalés | Caméra déplacée, [y, x] échangé, image recadrée ou letterboxée, ou le canvas avec ratio de pixels d'appareil a été redimensionné différemment |
| Mémoire qui augmente lors du rechargement | geometry.dispose(), material.dispose(), renderTarget.dispose(), et supprimez le modèle et les données WASM |
| À-coups à haute vitesse | Trop d'appels mj_step par frame. Limitez les étapes, ou déplacez la physique dans un Worker. |
6. Dans la démo Franka
La démo Franka pick-and-place est l'application de référence à partir de laquelle ces compétences ont été construites. Les noms de fichiers ci-dessous s'y réfèrent.
RenderSystem.tsutilisepreserveDrawingBuffer: true, unePerspectiveCamera(45)en Z-up, desOrbitControlsamortis,simGroup(un groupe par corps,userData.bodyID),erGroup(marqueurs coniques oscillants), marqueurs de contact instanciés, des transitions de caméra (moveCameraTo), etgetCanvasSnapshot(w, h, mime).project2DTo3D(x, y, camPos, lookAt)clone la caméra principale, la positionne au point de vue de capture ((0, -0.01, 2.0)regardant l'origine), et effectue un raycast sur les maillages desimGroup.rendering/GeomBuilder.tsgère la fabrique geom→maillage. Les geoms du groupe 3 sont ignorées, les plans deviennent un solReflector, et les geoms de maillage lisentmesh_vert/mesh_face.DragStateManager.tsetSelectionManager.tsgèrent le picking à la souris et les forces de glissement viaxfrc_applied.- L'import map de
index.htmlépingle three 0.164.1, tandis quepackage.jsoncontient^0.181. Conservez les importsthree/examples/jsm/..., qui fonctionnent dans les deux versions.