Files
RC_WheelLeg/05_software/real/sim2real/web/static/viewer/RobotViewer3D.js
T

182 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* RobotViewer3D — sim2real 3D 可视化(基于 robot_viewer 的 MJCFAdapter + Three.js
*
* 加载 wheelleg.xml → MJCFAdapter.parse → Three.js 场景树
* 建立 jointName → THREE.Object3D 映射,通过 updateJoints(pos16) 实时更新。
* 支持 OrbitControls 旋转/缩放/平移。
*
* 用法:
* const viewer = new RobotViewer3D(canvasElement);
* await viewer.load('/mjcf/wheelleg.xml');
* viewer.updateJoints(jointPositions16);
*/
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { MJCFAdapter } from './MJCFAdapter.js';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
// 16 关节的标准顺序(与 motor_mapping.py:SIM_JOINT_ORDER 对齐)
const JOINT_ORDER = [
'fl_hip_abduction_joint', 'fl_hip_pitch_joint', 'fl_knee_joint',
'fr_hip_abduction_joint', 'fr_hip_pitch_joint', 'fr_knee_joint',
'rl_hip_abduction_joint', 'rl_hip_pitch_joint', 'rl_knee_joint',
'rr_hip_abduction_joint', 'rr_hip_pitch_joint', 'rr_knee_joint',
'fl_wheel_joint', 'fr_wheel_joint', 'rl_wheel_joint', 'rr_wheel_joint',
];
// MJCF → Three.js 坐标轴转换:让 MJCF 的 Z 轴(向上) 映射到 Three.js 的 Y 轴(向上)
const MJCF_TO_THREE = new THREE.Matrix4().makeRotationX(-Math.PI / 2);
// 或直接用 euler: (0, PI, 0)
export class RobotViewer3D {
/**
* @param {HTMLCanvasElement} canvas
* @param {object} [opts]
* @param {string} [opts.meshBaseUrl='/meshes/'] STL mesh 文件的 HTTP 路径前缀
* @param {string} [opts.mjcfUrl='/mjcf/wheelleg.xml']
* @param {string} [opts.backgroundColor='#1a1d24']
*/
constructor(canvas, opts = {}) {
this.canvas = canvas;
this.meshBaseUrl = opts.meshBaseUrl || '/meshes/';
this.mjcfUrl = opts.mjcfUrl || '/mjcf/wheelleg.xml';
// Three.js 核心
const w = canvas.clientWidth, h = canvas.clientHeight;
this.scene = new THREE.Scene();
// 移除背景色,使用透明背景,由 CSS 控制
// this.scene.background = new THREE.Color(opts.backgroundColor || '#1a1d24');
this.camera = new THREE.PerspectiveCamera(55, w / h, 0.05, 50);
this.camera.position.set(0.5, 0.35, 0.65);
this.camera.lookAt(0.2, 0, 0);
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
this.renderer.setSize(w, h);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
// OrbitControls
this.controls = new OrbitControls(this.camera, canvas);
this.controls.target.set(0.15, 0.08, 0.0);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.12;
this.controls.update();
// 灯光
this._setupLights();
// 地面
const grid = new THREE.GridHelper(2, 20, 0x444444, 0x222222);
grid.position.y = -0.35;
this.scene.add(grid);
// 状态
this.model = null;
this.rootGroup = null;
this.jointMap = new Map(); // jointName → { joint, group }
this._isLoaded = false;
this._rafId = null;
this._stlCache = new Map(); // filename → BufferGeometry
}
_setupLights() {
const ambient = new THREE.AmbientLight(0x606060, 1.5);
this.scene.add(ambient);
const dir1 = new THREE.DirectionalLight(0xffffff, 2.5);
dir1.position.set(2, 3, 2);
this.scene.add(dir1);
const dir2 = new THREE.DirectionalLight(0x8899cc, 1.0);
dir2.position.set(-1, 1, -1);
this.scene.add(dir2);
const hemi = new THREE.HemisphereLight(0x8899cc, 0x334455, 1.2);
this.scene.add(hemi);
}
// ---- 加载模型 ----
async load(mjcfUrlOverride) {
const url = mjcfUrlOverride || this.mjcfUrl;
console.log('[RobotViewer3D] loading MJCF:', url);
const resp = await fetch(url);
if (!resp.ok) throw new Error(`MJCF 404: ${url}`);
const xmlText = await resp.text();
// 用 MJCFAdapter 解析 → UnifiedRobotModel
// fileMap 为空时不传;MeshLoader 会自动 fallback 到 URL 加载
const model = await MJCFAdapter.parse(xmlText, null);
this.model = model;
console.log('[RobotViewer3D] parsed:', model.links.size, 'links,', model.joints.size, 'joints');
// 取 rootGroupMJCFAdapter.createThreeObject 已构建完整 hierarchy
this.rootGroup = model.threeObject;
// 坐标轴转换:MJCF → Three.js
this.rootGroup.applyMatrix4(MJCF_TO_THREE);
this.scene.add(this.rootGroup);
// 遍历 joints,建立索引
this.jointMap.clear();
for (const [jointName, joint] of model.joints) {
if (joint.threeObject) {
this.jointMap.set(jointName, joint);
}
}
// 已建立映射的关节列表
const mapped = Array.from(this.jointMap.keys()).sort();
console.log('[RobotViewer3D] joint map:', mapped.length, 'joints');
this._isLoaded = true;
this._startRenderLoop();
}
// ---- 渲染循环(按需 + 持续) ----
_startRenderLoop() {
if (this._rafId) return;
const loop = () => {
this.controls.update();
this.renderer.render(this.scene, this.camera);
this._rafId = requestAnimationFrame(loop);
};
loop();
}
// ---- 实时更新关节角度 ----
/**
* @param {Float64Array|number[]} pos16 — 16 关节角度 (rad),顺序同 SIM_JOINT_ORDER
* 索引 0-11: 腿关节 (fl_abd,fl_pitch,fl_knee,fr...,rl...,rr...)
* 索引 12-15: 轮子关节 (fl_wheel,fr_wheel,rl_wheel,rr_wheel)
*/
updateJoints(pos16) {
if (!this._isLoaded) return;
for (let i = 0; i < JOINT_ORDER.length && i < pos16.length; i++) {
const name = JOINT_ORDER[i];
const joint = this.jointMap.get(name);
if (joint) {
MJCFAdapter.setJointAngle(joint, pos16[i]);
}
}
}
// ---- 重置相机 ----
resetCamera() {
this.camera.position.set(0.5, 0.35, 0.65);
this.controls.target.set(0.15, 0.08, 0.0);
this.controls.update();
}
// ---- 调整大小 ----
resize() {
const w = this.canvas.clientWidth, h = this.canvas.clientHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
}
dispose() {
if (this._rafId) cancelAnimationFrame(this._rafId);
this.renderer.dispose();
}
}