# G20 左手 · MuJoCo 仿真实现说明 本文记录 **MuJoCo G20 PLAN 名义弧度桥** 从 URDF 到可跑仿真的完整实现过程,以及相对原始 URDF 的全部具体改动。 - **函数调用与逐步逻辑**:见 [`G20_MuJoCo_技术详解.md`](./G20_MuJoCo_技术详解.md)(推荐先读此文理解代码) - **Isaac 对照**:[`G20_Isaac_实现说明.md`](./G20_Isaac_实现说明.md) --- ## 1. 目标与数据流 ### 1.1 目标 - 在 MuJoCo 中仿真 LinkerHand G20 **左手**。 - 默认兼容真机 SDK 话题协议:`sensor_msgs/JointState`,`position` 为 **0~255**。 - 可选订阅 PLAN 名义弧度(按关节名)。 - 关节名、0~255→弧度映射、mimic、拇指限位、对比视角与 Isaac 侧对齐。 - 默认 **运动学硬写** `qpos`(跟手紧);用碰撞投影减轻穿模,而不是改成默认 PD。 ### 1.2 端到端数据流 ```text /g20/cb_left_hand_control_cmd (默认,0~255) 或 /retarget/g20/left/joint_target_nominal (--plan-nominal,弧度+关节名) │ ▼ utils/g20_joints.py · range_to_arc_g20_left / sdk_range_to_urdf_positions · SDK15:thumb_mcp = arc/1.02 · SDK16..19:*_pip = arc/0.89 · complete_g20_mimic_positions(dip/ip) │ ▼ g20_plan_bridge.py · ROS 回调只写 goal(不碰 MjData) · 仿真线程:slew → [collision_project] → 硬写 qpos/ctrl + mj_forward │ ▼ /sim/mujoco/g20/left/joint_state @ 50 Hz + MuJoCo viewer(可选) ``` --- ## 2. 关键文件路径 | 角色 | 绝对路径 | |------|----------| | 启动脚本 | `/home/lxqs/linker_hand_mujoco_ros2/run_g20_mujoco_plan.sh` | | 桥主程序 | `.../linker_hand_mujoco_ros2/g20_plan_bridge.py` | | 关节映射 | `.../linker_hand_mujoco_ros2/utils/g20_joints.py` | | Launch | `.../launch/g20_plan_bridge.launch.py` | | URDF | `.../urdf/G20/linker_hand_g20_left/linkerhand_g20_left.urdf` | | MJCF | `.../urdf/G20/linker_hand_g20_left/linker_hand_g20_left.xml` | | 网格 | `.../urdf/G20/linker_hand_g20_left/meshes/*.STL`(22 个) | | entry_point | `setup.py` → `g20_plan_bridge=linker_hand_mujoco_ros2.g20_plan_bridge:main` | 包根目录: `/home/lxqs/linker_hand_mujoco_ros2/src/linkerhand-sim/linker_hand_mujoco_ros2/linker_hand_mujoco_ros2/` --- ## 3. 从 URDF 到 MJCF ### 3.1 URDF 来源 1. 原始模型由 **SolidWorks → URDF Exporter** 导出(URDF 头注释:Commit `1.6.0-4-g7f85cfe`)。 2. 本仓库资产来自 retarget 工程拷贝(非运行时自动下载),与 Isaac 侧网格 **MD5 一致**(22/22)。 3. 入库后的 URDF 文件名:`linkerhand_g20_left.urdf`。 ### 3.2 URDF 上我们改过什么 相对更早的 G20 URDF(历史上限曾为 `thumb_cmc_roll` **1.4**),当前仓库 URDF **已写入**: | 改动项 | 具体内容 | |--------|----------| | `thumb_cmc_roll` 上限 | `upper="1.47"`(rad);与映射表 `G20_L_MAX[5]=1.47`、Isaac 同步试调 | | mimic(保留自原 URDF) | `thumb_ip` ← `thumb_mcp` × **1.02**;四指 `*_dip` ← `*_pip` × **0.89** | | 其它关节限位 | 与 Isaac URDF 一致(见下表),未再改数值 | **当前 URDF 关节限位(rad,与 MJCF `range` / actuator `ctrlrange` 一致):** | 关节 | lower | upper | |------|-------|-------| | `thumb_cmc_roll` | 0 | **1.47** | | `thumb_cmc_yaw` | 0 | 1.57 | | `thumb_cmc_pitch` | 0 | 0.84 | | `thumb_mcp` | 0 | 1.26 | | `thumb_ip` | 0 | 1.29 | | 四指 `*_mcp_roll` | -0.23 | 0.23 | | 四指 `*_mcp_pitch` | 0 | 1.22 | | 四指 `*_pip` | 0 | 1.74 | | 四指 `*_dip` | 0 | 1.55 | 说明:MuJoCo **不读取** URDF 的 ``。mimic 在 Python 里用 `complete_g20_mimic_positions` 补全后硬写进 `qpos`。 ### 3.3 转换步骤(URDF → MJCF) 仓库内 **没有** 保留可一键重跑的转换脚本;实际流程是: 1. **初转**:用 MuJoCo `MjSpec.from_file(urdf)` 编译得到骨架 MJCF(关节树、mesh、惯性)。 2. **手改定稿** 为当前 `linker_hand_g20_left.xml`(下列改动全部是相对「纯 URDF 导入结果」的人工修改)。 ### 3.4 MJCF 相对 URDF / 纯导入结果的全部改动清单 #### A. 仿真选项与默认参数 | 项 | 写入值 | |----|--------| | `compiler` | `angle="radian" meshdir="meshes" balanceinertia="true"` | | `option` | `timestep="0.002"`,`gravity="0 0 -9.81"`,`integrator="implicitfast"` | | 默认 joint | `damping="0.15"`,`armature="0.01"` | | 默认 position 执行器 | `kp="200"`,`kv="8"`,`forcelimited="true"`,`forcerange="-8 8"` | #### B. 显示 / 碰撞分层(相对 URDF「全身 mesh 碰撞」) | 类型 | 改动 | |------|------| | `class="visual"` | `type="mesh"`,`contype="0"`,`conaffinity="0"`,`group="2"` → **只显示,不参与碰撞** | | 碰撞体 | **同视觉 STL 的凸包 mesh**(`type="mesh"` + `class="collision"`):掌心 + 拇指三段 + 四指近/中/远节;比 AABB 更贴外形 | | collision 默认 | `margin="0.0005"`,`friction="1 0.5 0.01"`,`solref="0.004 1"`,`solimp="0.9 0.95 0.001"` | **当前碰撞体(与视觉共用同一 `meshes/*.STL`,MuJoCo 按凸包做碰撞):** | body | 类型 | 说明 | |------|------|------| | `hand_base` | mesh 凸包 | `hand_base_link.STL` | | `thumb_metacarpals` / `thumb_proximal` / `thumb_distal` | mesh 凸包 | 同名 STL | | 四指 `*_proximal` / `*_middle` / `*_distal` | mesh 凸包 | 同名 STL | | 掌骨 metacarpals、拇指 base1/base2 | 无碰撞 geom | 仅 visual;与掌心假重叠靠 `exclude` | 说明:MuJoCo 对 mesh 碰撞使用 **凸包**,不能还原 STL 凹坑,但整体轮廓远比 AABB/手写胶囊贴近视觉;张开/半握无假碰,握拳可检出指尖↔掌心穿透。 #### C. ``(共 28 条) 用途:排除 **mesh/几何本就嵌套** 造成的假碰撞(张开姿态假重叠)。 - 掌心 `hand_base` ↔ 拇指 base1/base2/metacarpals、四指 metacarpals/proximal - 拇指链相邻 / 隔代相邻 - 四指各自 metacarpals–proximal、proximal–middle、middle–distal 未 exclude 的对(如指尖↔掌心、邻指远端)仍可产生接触,供投影使用。 #### D. 执行器 - 为 **全部 21 个 hinge**(含 5 个 mimic 关节)各加一个 ``。 - `thumb_cmc_roll` 的 `ctrlrange="0.0 1.47"`(与 URDF 上限一致)。 - 默认硬写模式下主要靠写 `qpos`;`--pd-drive` 时才真正用这些执行器做 `mj_step`。 #### E. 命名差异 | URDF | MJCF | |------|------| | `` | `` | | link `hand_base_link` | body 名 `hand_base`(geom 仍引用 mesh `hand_base_link`) | --- ## 4. 关节映射 `g20_joints.py`(相对 SDK / Isaac) 文件:`utils/g20_joints.py`。声明与 Isaac `joint_mapping.py` 的 G20 左手表对齐。 ### 4.1 SDK 通道 → URDF 关节名 | SDK 索引 | 关节名 | |----------|--------| | 0 | `thumb_cmc_pitch` | | 1–4 | `index/middle/ring/pinky_mcp_pitch` | | 5 | `thumb_cmc_roll` | | 6–9 | `index/middle/ring/pinky_mcp_roll` | | 10 | `thumb_cmc_yaw` | | 11–14 | **预留**(转换时跳过) | | 15 | `thumb_mcp`(见 4.3) | | 16–19 | `index/middle/ring/pinky_pip`(见 4.3) | ### 4.2 线性插值表(我们写入的数值) ```text G20_L_MIN = [0,0,0,0,0, 0.0, -0.23,-0.23,-0.23,-0.23, 0.0, 0×4, 0×5] G20_L_MAX = [0.84, 1.22×4, 1.47, 0.23×4, 1.57, 0×4, 1.29, 1.55×4] G20_L_DIR = [-1×6, +1,+1,+1,+1, -1, 0×4, -1×5] ``` 含义: - `DIR == -1`:SDK `0 → MAX`,`255 → MIN`(多数弯曲通道)。 - `DIR == +1`(仅侧摆 6..9):SDK `0 → MIN`,`255 → MAX`(左手侧摆与 retarget/`g20_mapping` 一致)。 - 通道 5 上限 **1.47**(相对历史 1.4 的试调,已写进 URDF/MJCF/Isaac)。 ### 4.3 SDK 15–19 的「除 mimic」规则(与 Isaac 相同,必须写清) SDK 通道语义不是「直接写主动关节弧度」,而是电机/指尖弧度空间: | SDK | 先算出的弧度 `arc` | 写入主动关节 | |-----|-------------------|--------------| | 15 | 按 `G20_L_*` 插值得到 | `thumb_mcp = arc / 1.02` | | 16–19 | 同上 | `*_pip = arc / 0.89` | 然后再: - `thumb_ip = thumb_mcp * 1.02` - `*_dip = *_pip * 0.89` 若漏掉「除 mimic」,DIP/IP 会整体偏大,表现为关节对不上 Isaac。 ### 4.4 张开预设 ```text G20_OPEN_CMD = [ 255,255,255,255,255,255, 193,148,105,42, # 四指侧摆 245, # thumb_cmc_yaw 255,255,255,255, # 预留 255,255,255,255,255 # 15..19 ] ``` `open_hand_positions()` / `half_fist_positions()` 基于此生成测试姿态。 --- ## 5. 桥节点 `g20_plan_bridge.py` 做了哪些事 ### 5.1 话题 | 方向 | 默认话题 | 内容 | |------|----------|------| | 订阅(默认) | `/g20/cb_left_hand_control_cmd` | `position` 0~255,取前 20,不足补 255 | | 订阅(`--plan-nominal`) | `/retarget/g20/left/joint_target_nominal` | 按关节名弧度 | | 发布 | `/sim/mujoco/g20/left/joint_state` | 全部 hinge 的 name/position/velocity,50 Hz | ### 5.2 默认驱动:运动学硬写 - **默认**(无 `--pd-drive`):把 `applied` 写入 `data.qpos` 与对应 `ctrl`,`qvel=0`,调用 `mj_forward`;**不调用** `mj_step`。 - **`--pd-drive`**:写 `ctrl` → `mj_step` → 用真实 `qpos` 回写 `applied`(跟手变差,接触可挡,一般不作为默认)。 ### 5.3 速度相关默认值(相对「瞬移」做过的调参) | 常量 | 值 | 含义 | |------|-----|------| | `DEFAULT_SPEED` | `1.35` | 相对墙钟倍率 | | `DEFAULT_SLEW_RAD_S` | `5.0` | 关节逼近 goal 的最大角速度 (rad/s);`0` = 瞬移 | | `sim_dt` | `0.002`(来自 MJCF) | 每步最大转角 ≈ `5.0 * 0.002 = 0.01` rad | ### 5.4 防穿模:`collision_project`(硬写专用,默认开) 相对早期实现,做过这些关键修正: | 版本问题 | 改动 | |----------|------| | 仅靠 STL 自碰 | 改为 visual(不碰)+ **同 STL 凸包 collision** | | 「相对上一帧更深才算穿」 | 改为 **绝对** 判据:`min(contact.dist) < -5e-4` 即穿 | | 关节空间整体二分 | 拇指撞掌心会拖死四指 → 改为 **按主动关节、Δq 从大到小** 逐关节接受或二分 | | 仍穿模 | 回退 `_last_safe_pose` | 开关: - 默认:开(且仅在硬写模式生效) - `--no-collision-project`:关 - `--track-test`:强制关投影,且 `slew=0`,方便验收跟手误差 ≤ 0.02 rad 常量:`PENETRATION_TOL_M = 5e-4`,`COLLISION_PROJECT_ITERS = 12`。 ### 5.5 线程安全(修过闪退) - `threading.Lock` 保护 `MjData`。 - ROS 回调 **只** 更新 `goal` / `track_nominal`(`_set_goal_only`),**禁止**在回调里碰 `MjData`。 - 主循环持锁调用 `step_simulation_unlocked()`;发 state 时持锁读 `qpos/qvel`。 ### 5.6 对比视角(与 Isaac 对齐) | 常量 | 值 | |------|-----| | `COMPARE_LOOKAT` | `(0.04, -0.04, 0.12)` | | `COMPARE_DISTANCE` | `0.50` | | `COMPARE_AZIMUTH` | `180.0`(从 +X 看向掌心) | | `COMPARE_ELEVATION` | `-12.0` | 手模型放在世界原点;Isaac 手有安装偏置,但 lookat 本地坐标与 elevation/distance 相同。 ### 5.7 启动时校验 - 用 `G20_ALL_JOINT_NAMES` 检查 MJCF 是否缺关节;缺则 **直接退出**。 - 缺 position actuator 也会报错退出。 ### 5.8 其它明确改动 - **不** import 真机包 `linker_hand_ros2_sdk`(只兼容同话题协议)。 - **不** 写死 `ROS_DOMAIN_ID`(启动脚本里曾有过,已去掉)。 - 退出用 `os._exit`,规避 rclpy/mujoco 析构 abort。 --- ## 6. 如何启动 ```bash cd /home/lxqs/linker_hand_mujoco_ros2 ./run_g20_mujoco_plan.sh ``` 脚本会:`source /opt/ros/jazzy` → 可选 `install/setup.bash` → 把 `venv` site-packages 与包源码加入 `PYTHONPATH` → `python3 -m linker_hand_mujoco_ros2.g20_plan_bridge`。 常用参数: ```bash ./run_g20_mujoco_plan.sh --plan-nominal ./run_g20_mujoco_plan.sh --pd-drive ./run_g20_mujoco_plan.sh --no-collision-project ./run_g20_mujoco_plan.sh --speed 0.5 --slew-rad-s 3 ./run_g20_mujoco_plan.sh --headless --track-test --max-steps 800 ``` Launch(无参数透传,调参请用 shell): ```bash ros2 launch linker_hand_mujoco_ros2 g20_plan_bridge.launch.py ``` 测试半握示例: ```bash ros2 topic pub -r 20 /g20/cb_left_hand_control_cmd sensor_msgs/msg/JointState \ "{position: [80,80,80,80,80,255,193,148,105,42,245,255,255,255,255,80,80,80,80,80]}" ``` --- ## 7. 已知限制(实现层面) 1. 硬写 + 投影 **不是** 真接触动力学;碰到凸包后关节会停在接触边界,不会「挤过去」。 2. 碰撞是 **凸包**,凹进去的视觉细节不会产生接触;比 AABB 贴,但仍可能与非凸视觉有细微差别。 3. 大量 `exclude` 关掉了掌心–近端假碰;真实可碰区域依赖开启碰撞的那些 link。 4. mimic 只在软件层保证;物理引擎内无 URDF mimic 约束。 5. 仅左手 G20;右手未做。 --- ## 8. 改动 chronologically(便于对照 Git / 联调) 1. 拷贝 G20 URDF + meshes,生成带 position actuator 的 MJCF。 2. 实现 `g20_joints.py`(对齐 Isaac;含 SDK15–19 除 mimic)。 3. 实现桥:名校验、订 nominal / legacy、发 `/sim/mujoco/...`。 4. 修回调碰 `MjData` 导致的闪退 → 回调只改 goal。 5. 话题改为 `/g20/cb_left_hand_control_cmd`;去掉脚本里 DOMAIN。 6. 调 `speed` / `slew` 到当前默认。 7. 拇指 `max=1.47` 写入映射 + URDF/MJCF。 8. 视角 `COMPARE_*` 与 Isaac 对齐。 9. 默认保持硬写;加碰撞投影;后改为绝对穿透 + **mesh 凸包碰撞** + 按关节投影。 --- ## 9. 与 Isaac 的对照摘要 | 项 | MuJoCo(本文) | Isaac | |----|----------------|-------| | 模型格式 | MJCF | 预烘焙 USD(非运行时 URDF 导入) | | 状态话题 | `/sim/mujoco/g20/left/joint_state` | `/sim/isaac/g20/left/joint_state` | | 映射表 | `g20_joints.py` | `joint_mapping.py`(G20 左手数值一致) | | 默认驱动 | 硬写 qpos | 硬写 `set_dof_positions` | | 防穿模 | mesh 凸包 + `collision_project` | USD 选择性 collider + PhysX 自碰 | | slew | 默认 5 rad/s | 默认 0(每物理步瞬达) |