MuJoCo SDF 插件实战:隐式几何体的定义、五个内置实现与自定义开发指南
【免费下载链接】mujocoMulti-Joint dynamics with Contact. A general purpose physics simulator.项目地址: https://gitcode.com/GitHub_Trending/mu/mujoco
本篇基于 MuJoCo 仓库中 plugin/sdf/README.md 展开,讲解如何用 Signed Distance Function(SDF,有符号距离函数)插件为geom和mesh(位于 asset 段)赋予隐式几何形状,覆盖 bolt、nut、bowl、gear、torus 五个一方实现的参数与用法,并结合 plugin/sdf 目录下的源码拆解插件注册、距离/梯度计算与碰撞迭代的可视化机制,最后给出开发自定义 SDF 插件的完整接口模板。读完本文,你可以直接复制仓库中的 XML 示例运行 SDF 碰撞模型,也能按照 README 的接口规范 写出并注册属于自己的隐式几何插件。
一、SDF 插件是什么,能应用在哪里
根据 plugin/sdf/README.md,这些一方插件使用 SDF 实现隐式几何(implicit geometries)。与 mesh 显式给出三角形网格不同,SDF 几何体由一个解析的距离场函数d(p)描述:对空间中任意查询点p,返回其到物体表面的有符号距离(外部为正、内部为负)。MuJoCo 的碰撞求解器通过在这个距离场上做梯度下降迭代来搜索接触点,因此 SDF 特别适合参数化、可解析表达的复杂曲面(如带螺纹的螺栓、齿轮),且无需离散的网格数据。
README 明确指出这些插件可以应用到geoms和meshes(在asset段中),示例模型统一放在 model/plugin/sdf/ 目录下,包含 nutbolt.xml、bowl.xml、gear.xml、torus.xml 等可直接用 simulate/studio 打开的场景。
XML 中的标准使用模式:extension 声明 → asset 挂 mesh → geom 引用
以 nutbolt.xml 为例,一个 SDF 几何体的接入分三步:
- 在
<extension>段声明插件实例并配置参数。plugin属性是插件的全名,<config key value>逐一对应插件的属性名:
<extension> <plugin plugin="mujoco.sdf.nut"> <instance name="nut"> <config key="radius" value="0.26"/> </instance> </plugin> <plugin plugin="mujoco.sdf.bolt"> <instance name="bolt"> <config key="radius" value="0.255"/> </instance> </plugin> </extension>- 在
<asset>段把插件实例挂到具名 mesh 上,一个插件实例可以对应多个 mesh:
<asset> <mesh name="nut"> <plugin instance="nut"/> </mesh> <mesh name="bolt"> <plugin instance="bolt"/> </mesh> </asset>- 在
<worldbody>中创建type="sdf"的 geom 并绑定 mesh 与插件实例:
<geom type="sdf" name="bolt" mesh="bolt" rgba="0.7 0.7 0.7 1"> <plugin instance="bolt"/> </geom><option>段的sdf_iterations和sdf_initpoints控制碰撞搜索过程:每次迭代执行的梯度下降次数和初始采样点数。四个示例中的取值分别为:nutbolt.xml 用sdf_iterations="10" sdf_initpoints="20",bowl.xml 用sdf_iterations="5" sdf_initpoints="20",gear.xml 用sdf_iterations="5" sdf_initpoints="20",torus.xml 用sdf_iterations="10" sdf_initpoints="40"。
从源码结构看,Gradient回调中记录的每一步查询点会被送入SdfVisualizer(见下文第五节),开启可视化标志mjVIS_SDFITER后可以在渲染器中看到这些梯度下降轨迹,这正是调参sdf_iterations时的直观依据。
二、五个内置 SDF 插件的参数详解
README 为每个插件给出了参数表、实现文件与示例模型。下表汇总,并注明各参数的默认值与实现位置。
| 插件 | 实现文件 | 示例模型 | 参数(默认值) |
|---|---|---|---|
| Bolt(六角头螺栓) | bolt.cc | nutbolt.xml | radius[m](0.26) |
| Bowl(切割空球碗) | bowl.cc | bowl.xml | height[m](0.4)、radius[m](1)、thickness[m](0.02) |
| Gear(齿轮) | gear.cc | gear.xml | alpha(0)、diameter[m](2.8)、teeth(25) |
| Nut(六角螺母) | nut.cc | nutbolt.xml | radius[m](0.26) |
| Torus(环面) | torus.cc | torus.xml | 主半径(0.35)、次半径(0.15) |
2.1 Bolt 与 Nut:螺纹 + 六角头组合
Bolt 实现了一个带六角头的螺栓,Nut 实现了与螺栓头部完全相同的六角螺母。nutbolt.xml 让一个带 free joint 的螺母与一个固定螺栓相互作用,并特意把 nut 半径设为 0.26、bolt 半径设为 0.255(略小),以便螺纹配合。
从 bolt.cc 的distance函数可以看出 SDF 组合的典型手法:先算出到螺纹柱面的距离thread(用Fract生成的三角波绕 Oy 轴旋转),再用Subtraction裁剪上下端、减去一个cone做斜切;六角头部分通过对查询点做旋转矩阵投影后求平面距离,再用两次Intersection分别用圆锥削顶和用水平平面封顶,最后用Union把柱体与头部合并。这些 Union/Intersection/Subtraction 原语都定义在 sdf.h:
inline mjtNum Union(mjtNum a, mjtNum b) { return mju_min(a, b); } inline mjtNum Intersection(mjtNum a, mjtNum b) { return mju_max(a, b); } inline mjtNum Subtraction(mjtNum a, mjtNum b) { return mju_max(a, -b); }2.2 Bowl:切割出的空心球
Bowl 的参数含义见 README 的参数表;示例 bowl.xml 显式配置了height=0.4、radius=1.0、thickness=0.02,让碗口位于 z=0.4 处,并放置三个自由关节小球演示落入碗中的碰撞。值得注意的是该示例的 geom 使用了condim="1"(纯摩擦接触),以匹配曲面碰撞的求解稳定性。
2.3 Gear:两个相互咬合的齿轮
gear.xml 是两个mujoco.sdf.gear实例的演示:gear1用默认参数且alpha=0,gear2设置alpha=15以错开齿形实现啮合;两者分别位于 z 轴铰链关节上,间距 2.85(略大于直径 2.8),并由一个gear="1500"的 motor actuator 驱动。这体现了 SDF 参数化几何的价值:同一个插件实例,仅改变alpha就能得到齿形相位不同的两个齿轮。
从 gear.h 的GearAttribute结构看,Gear 实际暴露了 5 个属性:alpha(默认 0)、diameter(默认 2.8)、teeth(默认 25)、thickness(默认 0.2)和innerdiameter(默认 -1),其中后两个未在 README 的参数表中列出,但同样可以通过<config key="thickness" value="..."/>方式配置。
2.4 Torus:最简单的参数化环面
torus.xml 用同一个 torus 插件实例创建了三个带 freejoint 的环面 geom 与两根 cylinder 轴,构成穿过环面的穿插碰撞演示。参数名以 torus.h 为准:radius1(主半径,默认 0.35)与radius2(次半径,默认 0.15)。这里提示一处 README 的笔误:其参数表将次半径也写成了radius1,实际应为radius2。
三、插件的注册机制与生命周期(源码级剖析)
以 bolt.cc 中的RegisterPlugin为主线,可以看到一个 SDF 插件需要向 MuJoCo 提供的全部信息:
mjpPlugin plugin; mjp_defaultPlugin(&plugin); plugin.name = "mujoco.sdf.bolt"; plugin.capabilityflags |= mjPLUGIN_SDF; plugin.nattribute = BoltAttribute::nattribute; plugin.attributes = BoltAttribute::names; plugin.nstate = +[](const mjModel* m, int instance) { return 0; };- 插件名即 XML
<extension>中plugin属性的取值,如mujoco.sdf.bolt。 - capabilityflags:置
mjPLUGIN_SDF位(定义为1<<3,见 mjplugin.h)声明本插件提供有符号距离场能力;nstate = 0表示插件没有运行时状态。
随后填充一组回调:
| 回调 | 作用 |
|---|---|
init | 调用工厂Bolt::Create构造对象,把指针存入d->plugin_data[instance];失败返回 -1 |
destroy | delete释放实例并清零plugin_data |
reset | 重置内部可视化计数器 |
compute | 每个求解周期调用,通知SdfVisualizer开始记录新一轮迭代 |
visualize | 把记录的梯度下降点画进mjvScene |
sdf_distance | 返回查询点的有符号距离 |
sdf_gradient | 返回距离场梯度,并把查询点送入SdfVisualizer::AddPoint |
sdf_staticdistance | 仅依赖属性数组的静态距离函数,不绑定实例 |
sdf_aabb | 返回几何体的轴对齐包围盒(bolt 为[0, 0.6]² × [0, 1]) |
sdf_attribute | 把 XML 中的字符串属性解析为数值并填充attribute[] |
mjPLUGIN_SDF回调槽位(sdf_gradient、sdf_staticdistance等)都定义在 include/mujoco/mjplugin.h 的mjpPlugin结构中。
梯度的实现是有限差分:bolt.cc 的Gradient以eps = 1e-8在 x、y、z 三方向各取一个偏移点,用一阶前向差分(distX - dist0)/eps近似梯度。由于解析 SDF 距离场通常非光滑(Union/Intersection 会产生折线),有限差分是最通用的求梯度方式,代价是每次梯度查询需要 4 次距离求值。
参数解析由SdfDefault<T>模板统一完成(sdf.h):构造时把T::names与T::defaults装入 map;GetDefault(name, value)在 XML 未提供值时回落到默认值,提供了值则strtod解析,非法值会调用mju_error终止。每个插件的属性结构(如BoltAttribute)必须保证names[]顺序与类中attribute[]数组顺序一致,这是 README 接口约定中强调的关键点。工厂函数Create还会先用 CheckAttr 校验数值属性字符串合法性,不合法时以mju_warning("Invalid parameter specification in Bolt plugin")告警并返回空。
注册入口在 register.cc:
mjPLUGIN_LIB_INIT(sdf) { Bolt::RegisterPlugin(); Bowl::RegisterPlugin(); Gear::RegisterPlugin(); Nut::RegisterPlugin(); Torus::RegisterPlugin(); }mjPLUGIN_LIB_INIT(n)宏(同样定义于 mjplugin.h)在动态库加载时触发,一次注册本库内的全部插件。构建层面,plugin/sdf/CMakeLists.txt 将上述全部源文件编译为共享库sdf_plugin(add_library(sdf_plugin SHARED))并链接mujoco主库,因此该插件目录本身就是"新增插件时该放什么文件、如何接入构建"的完整样板。
碰撞迭代轨迹可视化:SdfVisualizer
sdf.h 与 sdf.cc 中的SdfVisualizer记录了每次梯度下降的完整轨迹:Next()开新轨迹、AddPoint()逐点追加、Reset()清空。Visualize仅在可视化选项opt->flags[mjVIS_SDFITER]打开时生效,并把局部坐标轨迹变换到世界系(mju_mulMatMatT组合geom_xmat与静态geom_mat再平移geom_pos)。轨迹起点用红色球、终点用绿色球标记(rgba中通道 0/2 随j==0/j>0切换),中间连线颜色沿轨迹从红渐变到绿,方便观察迭代是否收敛到正确的接触面。缓冲区按mjMAXCONPAIR预分配,超出scn->maxgeom时给出 "Increase maxgeom" 警告,这是大规模调试迭代时的实际限制。
四、如何开发自己的 SDF 插件
README 给出的开发流程是:在 plugin/sdf 目录(本 README 所在目录)中创建MySDF.h与MySDF.cc,按以下接口实现,然后在 register.cc 中追加一个注册调用。接口模板如下(摘自 plugin/sdf/README.md):
struct MySDFAttribute { static constexpr int nattribute = /* insert the number of attributes */; static constexpr char const* names[nattribute] = /* an array of attributes with the same order as the attribute array in your SDF class */; static constexpr mjtNum defaults[nattribute] = /* an array of default values for your attributes */; }; class MySDF { public: // creates a new MySDF instance or returns null on failure. static std::optional<MySDF> Create(const mjModel* m, mjData* d, int instance); MySDF(MySDF&&) = default; ~MySDF() = default; // functions that return the SDF and its gradient at a query point mjtNum Distance(const mjtNum point[3]) const; void Gradient(mjtNum grad[3], const mjtNum point[3]) const; // a call to this needs to be added to register.cc static void RegisterPlugin(); // an array of attributes with the same order as in the struct above mjtNum attribute[MySDFAttribute::nattribute]; private: MySDF(const mjModel* m, mjData* d, int instance); };对照五个内置实现,可以把模板落成以下可执行清单:
- 属性结构:仿照 BoltAttribute,
nattribute、names、defaults三者数量一致,且names的顺序必须与类中attribute[]的填充顺序一致(SdfDefault::GetDefaults按位置取用)。 - 工厂函数
Create:先用CheckAttr校验 XML 数值属性,通过则走私有构造函数(内部用SdfDefault<MySDFAttribute>+mj_getPluginConfig(m, instance, name)读取每个属性),否则mju_warning并返回std::nullopt。 Distance/Gradient:实现解析距离场;梯度可直接复用 bolt 的有限差分写法(eps=1e-8的三方向前向差分),也可对解析光滑部分给出解析梯度。RegisterPlugin:设置plugin.name(建议mujoco.sdf.*命名空间)、plugin.capabilityflags |= mjPLUGIN_SDF、nattribute/attributes,以及init(new并写入d->plugin_data[instance])、destroy、reset、visualize、compute、sdf_distance、sdf_gradient、sdf_staticdistance、sdf_aabb、sdf_attribute回调,最后mjp_registerPlugin(&plugin)。- 接入注册入口:在 register.cc 的
mjPLUGIN_LIB_INIT(sdf)块中增加MySDF::RegisterPlugin();,并按 CMakeLists.txt 把新源文件加入MUJOCO_SDF_SRCS列表,重新构建sdf_plugin共享库即可。 - 写一个示例 XML 验证:套用 nutbolt.xml 的三段式结构(
<extension>声明实例 →<asset>挂 mesh →type="sdf"的 geom),配置sdf_iterations/sdf_initpoints后打开观察碰撞行为与mjVIS_SDFITER迭代轨迹。
五、小结与注意事项
- 五个插件全部位于
mujoco::plugin::sdf命名空间,共享 sdf.h 提供的布尔运算原语(Union/Intersection/Subtraction/Fract)、属性解析模板SdfDefault与迭代可视化器SdfVisualizer,是"一个插件目录 = 一个可加载 SDF 插件库"的完整范式。 - README 的参数表是权威入口,但个别细节以源码为准:Torus 的次半径属性名是
radius2(README 中误写为radius1);Gear 除 README 列出的三个参数外还有thickness(默认 0.2)与innerdiameter(默认 -1)两个属性。 - SDF 几何的质量取决于距离场在表面的光滑程度;碰撞搜索是数值梯度下降,若发现接触点漂移或穿透,可先调大
<option>的sdf_iterations与sdf_initpoints(如 torus.xml 使用 40 个初始点),并借助mjVIS_SDFITER可视化确认迭代轨迹是否落在预期表面。 - 所有路径均相对仓库根目录:插件源码在 plugin/sdf/,可运行示例在 model/plugin/sdf/,插件 API 头文件为 include/mujoco/mjplugin.h。
【免费下载链接】mujocoMulti-Joint dynamics with Contact. A general purpose physics simulator.项目地址: https://gitcode.com/GitHub_Trending/mu/mujoco
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考