MuJoCo 接触力实战:在控制器回调里拿到当前帧接触力
【免费下载链接】mujocoMulti-Joint dynamics with Contact. A general purpose physics simulator.项目地址: https://gitcode.com/GitHub_Trending/mu/mujoco
在 MuJoCo 力反馈控制中,控制器回调里读到的接触力往往比姿态慢一帧,抓取力控制因此抖动难稳。读完这篇你能说清力读数滞后的原因,并用两段式 step 与efc_force字段取到当前帧力,直接上手机器人抓取力闭环。
现象与误区:力读数总比姿态慢一拍
做抓取控制时,最常见的抱怨是:回调里读到的接触位置、法向力总是"上一刻"的,据此算出的力闭环一会儿抓松一会儿抓破,物体掉下去又弹起来。
还有一个隐蔽的误区。不少代码直接拿contact[i].friction[0]当力幅值用——但按 include/mujoco/mjdata.h 里的定义,friction[5]存的是摩擦系数(切向1、切向2、自旋、滚转1、2),量级是 0.6、0.05 这种小数,根本不是牛顿。拿它闭环,读数自然忽大忽小。
力读数为什么总比姿态慢一帧?因为接触力在这条流水线里本来就产生得晚。
原理:接触力在正向动力学链路的哪个环节诞生
MuJoCo 的一帧由mj_step1+mj_step2组成(mj_step就是它俩的打包)。白话说:
- step1 阶段:
mj_fwdPosition更新运动学并做接触检测,填充d->ncon(当前接触数量)和d->contact数组;随后mj_fwdVelocity算速度相关量。 - step2 阶段:
mj_fwdAcceleration算平滑力,mj_fwdConstraint跑约束求解器,此时才产出真正的力——存在d->efc_force(约束空间里的力数组);最后积分状态,时间走一帧。
关键认知:mjContact数组只存"几何信息"——位置pos、法向所在的frame矩阵、双方几何体geom[2]。每个接触点还有一个efc_address字段,是它在efc_force中的下标;-1表示该接触在间隙内、没参与求解,也就没有力。求解器没跑之前,你看到的力只能是上一帧的旧值。
打个比方:接触检测像安防系统"上报哪里碰了",求解器才是"开力量罚单"的人。回调发生在开罚单之前,读到的自然是旧账单。
🔧 实操:两种取当前帧接触力的办法
方案 A:拆分主循环,在求解后读力(推荐,改动最小)
// 主循环: 把 mj_step 拆成两段, 在求解完成后读力 for (int i = 0; i < 10000; i++) { mj_step1(m, d); // 位置+速度阶段: 完成本帧接触检测 mj_step2(m, d); // 加速度+约束求解+状态积分 // 此刻 efc_force 就是刚模拟完这一帧的接触力 graspController(m, d); // 读当前帧力, 写出下一帧的 ctrl }为什么这么改:接触力只在mj_step2的求解环节生成,把"读力"挪到它之后,数据就对应上一行刚走完的那一帧。要说明的是,力对应已模拟帧、新ctrl对应下一帧,这是一步延迟的标准力反馈结构,工程上完全够用。
拿到力向量还需要两步:按下标取三分量,再用接触帧矩阵转到世界坐标系。
// 取接触点 i 在世界系下的三维力, 写入 out void contactForce(mjData* d, int i, mjtNum out[3]) { mjContact* con = &d->contact[i]; if (con->efc_address < 0) { // 在间隙内, 未参与求解 out[0] = out[1] = out[2] = 0; return; } mjtNum f[3]; f[0] = d->efc_force[con->efc_address]; // 法向力 f[1] = d->efc_force[con->efc_address + 1]; // 摩擦力1 f[2] = d->efc_force[con->efc_address + 2]; // 摩擦力2 // frame 是 3x3 接触帧(法向在第一行), 转置相乘即变换到世界系 mju_mulMatTVec3(out, con->frame, f); }方案 B:回调内部手动补一次求解
如果架构上只能在回调里拿力,就让它自己把"开罚单"环节补跑一遍:
// 回调内手动补跑 加速度+约束求解, 得到当前 (状态, ctrl) 组合下的力 void graspController(const mjModel* m, mjData* d) { mj_fwdAcceleration(m, d); // 按当前 ctrl 重算平滑力与加速度 mj_fwdConstraint(m, d); // 跑约束求解器, 刷新 efc_force mjtNum fn = 0; for (int i = 0; i < d->ncon; i++) { if (d->contact[i].efc_address >= 0) fn += d->efc_force[d->contact[i].efc_address]; // 累加法向力 } d->ctrl[0] = 0.5 + 0.01 * fn; // 比例反馈: 离目标越远抓得越紧 if (d->ctrl[0] < 0.0) d->ctrl[0] = 0.0; // 限幅 if (d->ctrl[0] > 1.0) d->ctrl[0] = 1.0; }代价是每帧多一次约束求解,仿真会变慢——所以默认先用方案 A,方案 B 留给"回调必须自给自足"的场景。
⚠️ 避坑:常见问题与对策
| 错误现象 | 原因 | 对策 |
|---|---|---|
| 力读数总比姿态慢一帧 | 在约束求解完成前读了力 | 拆 step1/step2,step2 后读;或回调内手动补求解 |
| 抓取力忽大忽小、控制震荡 | 接触状态开关与求解器迭代带来逐帧波动 | 对力做一阶低通滤波,或改用触觉传感器输出 |
| 力值是不到 1 的小数 | 把contact[i].friction[0](摩擦系数)当成力 | 用efc_force[efc_address]取力 |
| 方案 B 后仿真明显变慢 | 每帧多跑一次约束求解 | 降频读力(每 N 帧一次),或改用方案 A |
| 多接触点合力对不上 | 每行 efc 是一个接触点的力,方向在接触局部系 | 按geom对筛选累加,经frame矩阵转到世界系 |
接触力是正向动力学的输出而不是输入,抓住"检测在前、求解出力、积分收尾"这个顺序,回调里取力就不会再对错位。方案 A 一行改动就能消除一帧滞后,方案 B 覆盖必须自包含的回调场景,配合限幅与滤波即可搭起稳定的抓取力闭环。
延伸阅读(仓库内路径):
- include/mujoco/mjdata.h:
mjData、mjContact、efc_force字段定义 - doc/programming/simulation.rst:仿真流程与正向动力学文档
- sample/basic.cc:主循环与回调的完整示例
- model/tactile/tactile.xml:触觉传感器模型示例
【免费下载链接】mujocoMulti-Joint dynamics with Contact. A general purpose physics simulator.项目地址: https://gitcode.com/GitHub_Trending/mu/mujoco
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考