246b48b1fb
Bring in kinematic hard-write control with mesh-convex collision projection, SDK mapping aligned to Isaac, launch/run scripts, and implementation/tech writeups.
363 lines
14 KiB
Markdown
363 lines
14 KiB
Markdown
# 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>`。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. `<contact><exclude>`(共 28 条)
|
||
|
||
用途:排除 **mesh/几何本就嵌套** 造成的假碰撞(张开姿态假重叠)。
|
||
|
||
- 掌心 `hand_base` ↔ 拇指 base1/base2/metacarpals、四指 metacarpals/proximal
|
||
- 拇指链相邻 / 隔代相邻
|
||
- 四指各自 metacarpals–proximal、proximal–middle、middle–distal
|
||
|
||
未 exclude 的对(如指尖↔掌心、邻指远端)仍可产生接触,供投影使用。
|
||
|
||
#### D. 执行器
|
||
|
||
- 为 **全部 21 个 hinge**(含 5 个 mimic 关节)各加一个 `<position name="*_pos" joint="...">`。
|
||
- `thumb_cmc_roll` 的 `ctrlrange="0.0 1.47"`(与 URDF 上限一致)。
|
||
- 默认硬写模式下主要靠写 `qpos`;`--pd-drive` 时才真正用这些执行器做 `mj_step`。
|
||
|
||
#### E. 命名差异
|
||
|
||
| URDF | MJCF |
|
||
|------|------|
|
||
| `<robot name="...">` | `<mujoco model="linker_hand_g20_left">` |
|
||
| 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(每物理步瞬达) |
|