Three.js-Rendering für MuJoCo
Statische Geometrie einmal aufbauen, dann pro Frame nur die Transformationen aktualisieren. MuJoCo verwendet Z-up, Halbgrößen, Z-ausgerichtete Zylinder und Kapseln sowie wxyz-Quaternionen. Three.js verwendet standardmäßig Y-up, Vollgrößen, Y-ausgerichtete Zylinder und xyzw-Quaternionen. Fast alle Rendering-Fehler gehen auf einen dieser Unterschiede zurück.
1. Szenenaufbau (Z-up, aufnahmebereit)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // Pfad gültig ab 0.164 und 0.18x
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // benötigt für 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 ist Z-up. Damit ist keine Achsenkonvertierung nötig.
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; // Gitter auf XY legen
Setze camera.up vor der Erstellung von OrbitControls, da die Controls den up-Vektor cachen.
2. Ansatz wählen
| Ansatz | Wie | Verwendung wenn |
|---|---|---|
A. Eine Group pro Körper (die Franka-Demo) | Jede Körper-Gruppe enthält ihre Geome an deren lokaler geom_pos/geom_quat. Kopiere xpos/xquat pro Frame. | Standard. Auswahl via userData.bodyID. Funktioniert mit jeder Pose-Quelle. |
B. mjv_updateScene (offizielle Demo) | Synchronisiere einen gepoolten Mesh pro scene.geoms-Eintrag | Du möchtest MuJoCos eigene Dekorationen (Kontakte, Sehnen, Sites) |
| C. Remote Python | Python führt Schritte aus und streamt xpos/xquat per WebSocket. Der Browser nutzt A. | GPU-, ROS- oder Realroboter-Brücken, rechenintensive Aufgaben |
Geom-Konvertierung: references/geom-conversion.md. Python-Streaming (getesteter Server): references/remote-python.md.
Snapshots, Perzeptionskamera, Video und Pixel↔3D: references/perception-and-video.md.
3. Synchronisation pro Frame (Ansatz 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);
Schleifenreihenfolge: Eingaben → mj_step (so oft wie das Sim-Zeitbudget erlaubt) → sync → render
(siehe MujocoSim.startLoop). Die Alternative pro Geom verwendet data.geom_xpos und data.geom_xmat
(zeilenweise 3×3 → Matrix4.set(r00,r01,r02,0, r10,…)), mit Geometrie an der Identitätspose.
4. ER-Punkte ↔ 3D
// ER [y, x] 0-1000 auf dem aufgenommenen Bild -> Strahl von derselben Kamera, die es aufgenommen hat
const ndc = new THREE.Vector2((x / 1000) * 2 - 1, 1 - (y / 1000) * 2);
raycaster.setFromCamera(ndc, captureCamera);
const hit = raycaster.intersectObjects(simMeshes, false)[0]; // oder raycaster.ray.intersectPlane(table, out)
// 3D -> ER-artiges [y, x] (um Erwartungen zu zeichnen oder Prompts zu bauen)
const v = p.clone().project(captureCamera);
const yx = [(1 - v.y) * 500, (v.x + 1) * 500];
Dies gilt für jedes Seitenverhältnis, solange das Bild exakt das gesamte Viewport der Kamera ist (kein Zuschnitt,
kein Letterbox) und die Kamera sich zwischen Aufnahme und Raycast nicht bewegt hat. Überspringe Treffer auf dem Roboter
oder dem Boden, indem du bis zu userData.bodyID durchgehst. Der Treffer ist die obere Fläche; senke ihn auf Greifhöhe ab.
5. Häufige Fehler
| Symptom | Ursache |
|---|---|
| Roboter liegt seitlich, Orbit kippt | camera.up ist nicht (0,0,1) oder wurde nach der Erstellung von OrbitControls gesetzt |
| Links verdreht | Fehlende wxyz→xyzw-Umordnung |
| Boxen doppelt so groß | Vergessen, dass Größen Halbextents sind |
| Zylinder oder Kapseln zeigen in die falsche Richtung | Fehlendes geometry.rotateX(Math.PI/2) |
| Leeres oder schwarzes Snapshot | preserveDrawingBuffer: false, oder die Pixel wurden in einer anderen Task als dem Render ausgelesen |
| Perzeptionsbild dunkler als der Bildschirm | Render-Targets geben lineare Farbe aus (bestätigt in three r186). In sRGB konvertieren (siehe Perzeptionsreferenz). |
| ER-Marken versetzt | Kamera bewegt, [y, x] vertauscht, Bild zugeschnitten oder mit Letterbox versehen, oder die Leinwand mit Geräte-Pixel-Verhältnis wurde anders skaliert |
| Speicher wächst bei Neuladen | geometry.dispose(), material.dispose(), renderTarget.dispose(), und das WASM-Modell und die Daten löschen |
| Ruckeln bei hoher Geschwindigkeit | Zu viele mj_step-Aufrufe pro Frame. Anzahl der Schritte begrenzen oder Physik in einen Worker auslagern. |
6. In der Franka-Demo
Die Franka-Pick-and-Place-Demo ist die Referenz-App, aus der diese Skills entwickelt wurden. Die folgenden Dateinamen beziehen sich auf diese Demo.
RenderSystem.tsverwendetpreserveDrawingBuffer: true, eine Z-upPerspectiveCamera(45), gedämpfteOrbitControls,simGroup(eine Gruppe pro Körper,userData.bodyID),erGroup(wippende Kegelmarker), instanziierte Kontaktmarker, Kamera-Tweens (moveCameraTo) undgetCanvasSnapshot(w, h, mime).project2DTo3D(x, y, camPos, lookAt)klont die Hauptkamera, positioniert sie am Aufnahme-Viewpoint ((0, -0.01, 2.0)blickend auf den Ursprung) und führt Raycast aufsimGroup-Meshes durch.rendering/GeomBuilder.tsbehandelt die Geom→Mesh-Factory. Geome der Gruppe 3 werden übersprungen, Ebenen werden zu einemReflector-Boden, und Mesh-Geome lesenmesh_vert/mesh_face.DragStateManager.tsundSelectionManager.tsbehandeln Mausauswahl und Zugkräfte überxfrc_applied.- Die
index.html-Import-Map pinnt three 0.164.1, währendpackage.json^0.181hat. Verwendethree/examples/jsm/...-Imports, die in beiden Versionen funktionieren.