MuJoCo를 위한 Three.js 렌더링
정적 지오메트리를 한 번 빌드하고, 각 프레임마다 트랜스폼만 업데이트하세요. MuJoCo는 Z-up, 하프 사이즈, Z-정렬 실린더와 캡슐, wxyz 쿼터니언을 사용합니다. Three.js는 기본적으로 Y-up, 풀 사이즈, Y-정렬 실린더와 xyzw 쿼터니언을 사용합니다. 거의 모든 렌더링 버그는 이 중 하나에서 발생합니다.
1. 장면 설정 (Z-up, 캡처 준비)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // 경로는 0.164 및 0.18x에서 유효
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); // 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입니다. 이렇게 설정하면 어디서도 축 변환이 필요하지 않습니다.
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; // XY 평면에 그리드 배치
camera.up은 OrbitControls를 생성하기 전에 설정하세요. 컨트롤이 up 벡터를 캐시하기 때문입니다.
2. 접근 방식 선택
| 접근 방식 | 방법 | 사용 시기 |
|---|---|---|
A. 바디당 하나의 Group (Franka 데모) | 각 바디 그룹은 자체 geom_pos/geom_quat에 지오메트리를 보유합니다. 각 프레임마다 xpos/xquat를 복사합니다. | 기본값. userData.bodyID를 통한 피킹. 모든 포즈 소스와 함께 작동합니다. |
B. mjv_updateScene (공식 데모) | scene.geoms 항목당 하나의 풀링된 메시를 동기화합니다. | MuJoCo 자체 데코레이션(접촉, 힘줄, 사이트)을 원하는 경우 |
| C. 원격 Python | Python이 단계를 진행하고 WebSocket을 통해 xpos/xquat를 스트리밍합니다. 브라우저는 A를 실행합니다. | GPU, ROS 또는 실제 로봇 브리지, 대규모 연산 |
Geom 변환: references/geom-conversion.md. Python 스트리밍(테스트된 서버): references/remote-python.md.
스냅샷, 인식 카메라, 비디오 및 픽셀↔3D: references/perception-and-video.md.
3. 프레임별 동기화 (접근 방식 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);
루프 순서: 입력 → mj_step (시뮬레이션 시간 예산이 허용하는 만큼 여러 번) → 동기화 → 렌더링
(MujocoSim.startLoop 참조). per-geom 대안은 data.geom_xpos 및 data.geom_xmat
(행 우선 3×3 → Matrix4.set(r00,r01,r02,0, r10,…))을 사용하며, 지오메트리는 항등 포즈에 있습니다.
4. ER 포인트 ↔ 3D
// 캡처된 이미지의 ER [y, x] 0-1000 -> 캡처한 동일한 카메라의 광선
const ndc = new THREE.Vector2((x / 1000) * 2 - 1, 1 - (y / 1000) * 2);
raycaster.setFromCamera(ndc, captureCamera);
const hit = raycaster.intersectObjects(simMeshes, false)[0]; // 또는 raycaster.ray.intersectPlane(table, out)
// 3D -> ER 스타일 [y, x] (기대치를 그리거나 프롬프트를 작성하기 위해)
const v = p.clone().project(captureCamera);
const yx = [(1 - v.y) * 500, (v.x + 1) * 500];
이것은 이미지가 정확히 카메라의 전체 뷰포트인 경우 (자르기 없음, 레터박스 없음) 그리고 카메라가 캡처와 레이캐스트 사이에 움직이지 않은 경우 모든 종횡비에 유효합니다. userData.bodyID까지 올라가서 로봇이나 바닥에 맞는 것은 건너뛰세요. 히트는 상단 표면이므로, 잡기 높이로 낮추세요.
5. 주의사항
| 증상 | 원인 |
|---|---|
| 로봇이 옆으로 누워 있고, 궤도가 흔들림 | camera.up이 (0,0,1)이 아니거나 OrbitControls 생성 후에 설정됨 |
| 링크가 꼬임 | wxyz→xyzw 재정렬 누락 |
| 상자가 두 배로 커 보임 | 크기가 반쪽 크기임을 잊음 |
| 실린더 또는 캡슐이 잘못된 방향을 가리킴 | geometry.rotateX(Math.PI/2) 누락 |
| 빈 스냅샷 또는 검은색 스냅샷 | preserveDrawingBuffer: false이거나 픽셀이 렌더링과 다른 작업에서 읽힘 |
| 인식 이미지가 화면보다 어두움 | 렌더 타겟이 선형 색상을 출력함 (three r186에서 확인됨). sRGB로 변환하세요 (인식 참조 파일 참조). |
| ER 마커가 오프셋됨 | 카메라가 움직였거나, [y, x]가 바뀌었거나, 이미지가 잘리거나 레터박스 처리되었거나, 장치 픽셀 비율 캔버스 크기가 다르게 조정됨 |
| 다시 로드할 때 메모리 증가 | geometry.dispose(), material.dispose(), renderTarget.dispose()를 호출하고 WASM 모델과 데이터를 삭제하세요 |
| 고속에서 끊김 | 프레임당 mj_step 호출이 너무 많음. 단계 수를 제한하거나 물리 엔진을 Worker로 이동하세요. |
6. Franka 데모에서
Franka 집어 옮기기 데모는 이러한 스킬이 빌드된 참조 앱입니다. 아래 파일 이름은 이를 참조합니다.
RenderSystem.ts는preserveDrawingBuffer: true, Z-upPerspectiveCamera(45), 감쇠OrbitControls,simGroup(바디당 하나의 그룹,userData.bodyID),erGroup(흔들리는 원뿔 마커), 인스턴스화된 접촉 마커, 카메라 트윈 (moveCameraTo) 및getCanvasSnapshot(w, h, mime)을 사용합니다.project2DTo3D(x, y, camPos, lookAt)은 주 카메라를 복제하여 캡처 시점 ((0, -0.01, 2.0)이 원점을 바라봄)에 배치하고simGroup메시를 레이캐스트합니다.rendering/GeomBuilder.ts는 geom→메시 팩토리를 처리합니다. Group-3 geom은 건너뛰고, 평면은Reflector바닥이 되며, 메시 geom은mesh_vert/mesh_face를 읽습니다.DragStateManager.ts와SelectionManager.ts는xfrc_applied를 통해 마우스 피킹 및 드래그 힘을 처리합니다.index.html임포트 맵은 three 0.164.1을 고정하는 반면,package.json은^0.181을 가집니다.three/examples/jsm/...임포트를 유지하세요. 두 버전에서 모두 작동합니다.