MuJoCo进阶——创建一辆自己的跑车
我们已经能够手写一个MJCF文件,通过简单的几何体比如长方体,胶囊体等创建出小车倒立摆模型,肉眼可见的丑,而且是极度简化的版本;同时我们也下载了宇树的G1和Go2模型文件,了解了他的模型结构,它是再assets文件夹中放了很多设计好的模型文件,在MJCF中导入,所以我们可以看到一个相对逼真的模型。所以我们如何自己能够创建一个自己的真实场景的模型呢?我想我们应该已经具备这个能力了,本文就准备干这个事情。
大家都有一个跑车梦,开着跑车去”炸街“,买不起就自己创建一个吧,本文就以跑车为例,一步一步创建出一个看上去比较好看的模型,然后让它跑起来。目的就是为后续我们自己的真实场景下的模型搭建打下一个基础,毕竟不是所有的模型都是别人帮我们做好的。
1. 最终效果与总体流程
完成本文后,我们的项目将具有以下结构:
- 一个可以自由移动的车身刚体;
- 四个可以绕轮轴转动的车轮;
- 两个前轮额外具有转向关节(嵌套在转向 body 中);
- 四个车轮由速度执行器驱动;
- 外观模型与碰撞模型分离;
- 可选的车身图片贴图;
- 一个 Python 脚本,控制车辆直行、转向和停止。
整体流程如下:
CAD 装配体或网上下载的整车 OBJ
↓
在 Blender 中整理
↓
拆分车身、车窗和四个车轮,共 6 个部件
(车窗是外观件,相对车身固定,也可并入车身)
↓
统一单位、坐标轴、零件原点和导出参数
↓
导出 6 个 OBJ,可选导出 UV 和材质
↓
MJCF:定义 body、joint、geom 和 actuator
↓
Python:加载模型并发送控制量
这里有一个非常重要的认识:
OBJ 只负责描述几何外观。车辆如何连接、绕哪根轴运动、质量是多少、由哪个电机驱动,都由 MJCF XML 定义,而不是由 OBJ 决定。
2. 模型从哪里来:网上下载与真实工程交付
2.1 网上下载的 OBJ 通常是什么
网上下载的整车模型通常是美术人员制作的渲染资产。它可能已经包含:
car.obj
car.mtl
textures/
├── body_diffuse.png
├── glass.png
└── wheel.png
也可能只有一个 car.obj。如果只有 OBJ,没有 MTL、贴图或 UV,那么它通常只能可靠地提供几何形状,不能自动还原下载页面上的渲染效果。
下载模型时需要检查:
本文我们就是在网上下载的一个现成的完整车身的obj/mtl/贴图文件,需要我们自己拆分,拆分的颗粒度不需要太细,与我们的实际需要符合就好,比如这里我们就拆成了6个模块,车身+车窗+4个轮子,车窗是相对车身固定的外观件,单独保留只是为了可以给它配独立的玻璃材质,也可以直接并入车身;而轮子是要独立运动的,所以必须单独拆出来。
给大家推荐一个免费的3D模型下载网站,Grab3D,截至目前还是免费的,大家可以去选一个自己喜欢的车模型,感谢免费资源。

2.2 如果有 CAD 工程师,应该要什么格式
真实工程场景中,设计工程师通常使用 SolidWorks、CATIA、Creo、UG NX 等 CAD 软件。它们主要处理精确曲面和实体,而 OBJ、STL 是网格格式,mujoco也是只能导入网格文件。
如果条件允许,优先让 CAD 工程师提供:
完整装配体的 STEP 文件,而不是只给 STL 或 OBJ。
三者的职责不同:
| STEP | CAD 精确曲面、零件和装配结构 | 不适合承载完整贴图工作流 | CAD 源头交付、后续重新网格化 |
| STL | 三角形面片 | 标准 STL 不保存 UV、MTL 和贴图引用 | 简单外观、碰撞网格 |
| OBJ | 顶点、面、法线,可包含 UV | 可配套 MTL 和图片贴图 | 带纹理的视觉网格 |
为什么不能只要 STL?因为一旦网格化参数不合适,例如面数太高、圆弧质量太差或零件拆分不合理,我们很难从 STL 恢复原始 CAD 曲面。保留 STEP 后,可以重新生成不同精度的网格。
实际流程可以是:
SolidWorks 完整装配体
↓
STEP 完整装配体
↓
FreeCAD 或其他 CAD 转换工具进行网格化(本文实际上直接从这一步开始的,但是前面的步骤大家可能也需要知道)
↓
Blender:拆分、减面、UV、材质、坐标整理
↓
MuJoCo 使用的 OBJ/STL
Blender 通常不原生导入 STEP,因此中间可能需要 FreeCAD 或其他 CAD 转换工具。
2.3 工程师应该给完整装配,还是直接给 6 个零件
通常建议先交付完整装配体,由仿真建模人员拆分,毕竟如何使用模型,分几个模块是仿真人员需要考虑的,不能把工作前置到设计端,做好职责解耦和切分,原因是:
例如,一辆车可以拆成:
- 车身+四个车轮,共 5 个刚体(本文方案,车窗作为外观件挂在车身上);
- 车身+四个转向节+四个车轮,共 9 个刚体;
- 更完整的悬架、转向和传动系统。
本文选择第一种,用嵌套在两个前轮内的转向 body 模拟转向,不单独建立转向节。
2.4 除了模型,还应该向工程师要什么
模型文件并不包含全部仿真信息。建议同时索取:
| 单位与坐标系说明 | 避免毫米/米混用和轴向错误 |
| 轴距、轮距、轮半径 | 设置车轮位置和碰撞体 |
| 轮轴中心与转向轴位置 | 设置关节原点和轴向 |
| 各刚体质量、质心、转动惯量 | 设置动力学参数 |
| 零件材料与外观说明 | 重建纯色或纹理材质 |
| Logo、拉花等矢量源文件 | 制作车身贴图 |
这些参数往往比高精细的外观模型更影响仿真结果。
3. MuJoCo 中的网格、材质与贴图
3.1 MuJoCo 一般使用什么网格格式
MuJoCo 可以加载二进制 STL、OBJ、MSH,以及 XML 中直接定义的网格数据。STEP 不能直接作为普通 mesh 资源加载。
在本教程中:
- 6 个外观零件使用 OBJ,只负责显示(contype="0" conaffinity="0");
- 碰撞体使用简单的 box(车身)和 cylinder(车轮);
- 不直接使用高面数外观网格参与碰撞;
- 材质先在 XML 中用纯色表示,之后再演示图片贴图。
这是一种适合入门、也便于排查问题的配置。
3.2 为什么宇树等机器人模型只用 STL,也能正常显示
因为“材质”和“贴图”不是同一个概念。
- 材质可以只是颜色、透明度、高光等参数;
- 贴图是图片,用于让模型不同位置呈现不同颜色或图案。
一个 STL 没有图片贴图,仍然可以通过 XML 设置颜色:
<geom type="mesh"
mesh="robot_body"
rgba="0.65 0.65 0.68 1"/>
再配合灯光和阴影,就能得到正常的金属灰、黑色和明暗效果。机器人身体上的文字和标志也可能是实际几何,而不是图片。
因此:
没有贴图文件,不等于模型不能显示颜色;有材质,也不等于一定有图片贴图。
3.3 MTL 起什么作用
MTL 是 OBJ 的材质库文件。OBJ 可以通过 mtllib 引用它,再通过 usemtl 将某些面分配给指定材质。
例如,car.obj 中包含:
mtllib car.mtl
v …
vt …
vn …
usemtl car_paint
f …
car.mtl 中包含:
newmtl car_paint
Kd 0.800 0.020 0.020
Ks 0.500 0.500 0.500
Ns 200.0
d 1.0
map_Kd car_paint.png
常见字段如下:
- newmtl:材质名称;
- Kd:漫反射颜色;
- Ks:高光颜色;
- Ns:高光参数;
- d:不透明度;
- map_Kd:漫反射贴图路径。
完整关系是:
car.obj
├── v、vn、f:几何和法线
├── vt:UV 坐标
├── mtllib:指向 MTL
└── usemtl:指定面使用的材质
↓
car.mtl
└── map_Kd:指向图片
↓
car_paint.png
└── 真正的图片像素
MTL 不保存几何、不保存 UV,也不嵌入图片本身。
3.4 在 MuJoCo 中不要依赖 MTL 自动生效
Blender 等通用图形软件可能会在加载 OBJ 时自动读取 MTL,但在 MuJoCo 中不要依赖 MTL 自动生效, MJCF 的标准做法是:
因此,即使 Blender 导出了 MTL,也建议在 MJCF 中显式重新定义材质。不要因为 OBJ 旁边存在 MTL,就默认 MuJoCo 会完整还原 Blender 的材质效果。
注意:<texture> 引用的图片文件必须真实存在,否则模型加载直接报错。如果你的模型没有贴图,记得把 XML 里的 <texture>、<material> 两行以及 geom 上的 material 属性一起去掉(本文第 8 章的 XML 中有对应注释)。
4. 在 Blender 中拆分整车
首先介绍一下Blender这个软件,它是一个免费的软件,对于处理网格模型非常方便,推荐给大家使用。我在下载这个软件的时候有个界面是自愿捐赠,如果财力雄厚的朋友可以支持一下,当然不捐赠也是可以下载和免费试用的哈,等我开上本文设计的跑车的时候一定要捐赠一下[苦笑]😀
4.1 先确定运动学结构
本文将车辆拆成:
car_body 车身
windows 车窗(外观件,固定在车身)
wheel_fl 左前轮
wheel_fr 右前轮
wheel_rl 左后轮
wheel_rr 右后轮
拆分原则不是“每个外观零件都成为一个刚体”,而是:
彼此之间存在独立运动的部件,才需要拆成不同刚体。
车灯、后视镜、车门等如果相对车身固定,就可以继续合并到车身网格中。
4.2 保留一份完整装配母版
在开始移动零件之前,建议保存:
car_assembly_master.blend
这份文件始终保留正确的整车装配状态。后续所有局部零件文件都从它派生,不要在唯一一份模型上反复移动、撤销和覆盖。
4.3 拆分网格
如果网格模型本身已经是拆分好的,就不需要我们再做拆分,但如果是一体的,需要做拆分,常见操作是:
拆分后先不要导出,还需要统一坐标系和零件原点。
在blender中打开的样子是这样的,注意如果是打开obj文件,需要在文件菜单下选择导入,如果是blender文件可以直接打开:

关于"导入后皮肤丢了":这通常不是导入失败,而是两个原因之一——
另外注意,OBJ 导入 Blender 时导入器会给每个物体加一个 Rotation X = 90° 的对象级旋转(OBJ 是 Y-up 约定,Blender 是 Z-up),这个旋转会在第 5 章的"应用变换"步骤里烘掉,不要忽略它。
5. 坐标系:很重要
5.1 OBJ 只保存了网格信息和数值
OBJ 文件只保存坐标数值,不会可靠地告诉下游程序:
- 数值单位是毫米还是米;
- 哪个轴向上;
- 哪个轴是车头方向;
- 零件原点是否位于轮轴中心;
- 零件在整车中的安装位置。
这些约定必须由建模者明确记录。
本文统一采用(与 MuJoCo 场景约定一致):
单位:米
坐标系:右手系
+X:车头前方
+Y:车辆左侧
+Z:竖直向上
车轮滚动轴:局部 Y 轴(axis="0 1 0",正值 = 前进)
前轮转向轴:局部 Z 轴(axis="0 0 1")
MuJoCo 默认场景也采用 Z 向上的重力方向。整条链路(Blender 导出 → XML 轮心坐标 → 关节轴向 → 控制量符号)必须遵守同一个约定,否则就会出现"倒车""轮子装反"这类问题——本文最初的版本就吃过这个亏。
5.2 两种零件坐标方案
方案 A:所有零件保留整车坐标
拆分后不移动零件原点,直接导出。每个 OBJ 中的顶点仍然处于整车坐标系。
优点是:所有零件都放在 (0, 0, 0),就能重新拼成整车。
缺点是:车轮顶点远离自身轮轴,旋转时需要额外的位置补偿。
这正是"车一加载就散架"的典型病根:摆放看的是顶点坐标(方案 A 下 pos 全写 0 摆得对),但旋转看的是 body 原点——关节 pivot 永远在 body 原点上。轮子的 body 原点在整车中心,"自转"就变成绕整车中心公转,轮子立刻满天飞。
方案 B:每个零件使用自身局部坐标
将零件原点移动到自身关节中心:
- 车身原点位于整车参考点;
- 每个车轮原点位于轮轴中心;
- 车轮局部 Y 轴与轮轴重合。
XML 中的 body pos 再描述零件安装位置。
优点是:车轮绕局部轴旋转自然,XML 结构清晰,也便于设置惯量(网格居中后,MuJoCo 从几何自动推断的惯量才是对的,不过本文仍建议显式填写 <inertial>,把质量当作设计参数而不是网格体积的副产品)。
本文采用方案 B。
5.3 设置车轮原点
对每个车轮执行:
注意:仅仅修改对象原点,不一定意味着最终 OBJ 已使用局部坐标。Blender 导出时可能仍应用对象的世界变换。
因此,导出局部零件时建议:
如果直接把仍处于整车世界位置的零件导出,就可能重新变回方案 A。
5.4 统一单位
CAD 常用毫米,MuJoCo 通常使用米。如果模型在 Blender 中显示为一辆长度为 4000 的车,应整体缩放为约 4,而不是在 XML 中随意补偿。
建议:
不要同时依赖“Blender 中缩放一次、OBJ 导出再缩放一次、XML 的 mesh scale 又缩放一次”,否则后续很难排查。
5.5 导出 OBJ
对 6 个零件分别导出,并保持完全相同的参数:
- 只导出选中对象;
- 应用修改器;
- 导出法线;
- 需要图片贴图时导出 UV;
- 需要三角网格时进行三角化;
- Scale = 1.0;
- 轴向设置应保留 Blender 的 Z-up 坐标约定。对本文这辆在 Blender 中车头朝 −Y 的车,设置 Forward = X、Up = Z 后,导出的文件中车头正好朝 +X,符合 5.1 的约定。

不同 Blender 版本的选项名称可能略有区别。关键不是死记某个下拉框,而是:
所有零件使用同一组参数,并通过重新导入或 MuJoCo 场景验证方向和位置。
最终得到:
car_body.obj
windows.obj
fl_wheel.obj
fr_wheel.obj
rl_wheel.obj
rr_wheel.obj
5.6 如何验证导出结果
方案 B 不能用“全部放到原点就会拼成整车”来验证,因为所有局部零件都放在原点时本应重叠。
应该分别检查(括号内是本文这辆车的实测值,可直接对照):
本文这辆车的轮心坐标(即 XML 中的 body pos),建议在 Blender 母版中记录并核对:
| fl(前左) | +1.399 | +0.785 | 0.345 |
| fr(前右) | +1.399 | −0.785 | 0.345 |
| rl(后左) | −1.261 | +0.810 | 0.345 |
| rr(后右) | −1.261 | −0.810 | 0.345 |
本文示例 XML 使用的就是这组测量值;换用自己的模型时,必须替换为你的测量值。
6. 外观、碰撞和惯量不要混为一谈
一个常见误区是:把 6 个高精细 OBJ 同时用于显示和碰撞,然后让 MuJoCo 自动推断全部动力学参数。
更稳妥的做法是将三类信息分开(第 8 章的 XML 就是按这个原则写的):
| 外观 | 6 个 OBJ,contype="0" conaffinity="0",只负责显示 |
| 碰撞 | 车身 box、车轮 cylinder |
| 质量与惯量 | 在 <inertial> 中显式设置 |
这样做有以下好处:
- 外观网格面数不会直接影响碰撞复杂度;
- 圆柱车轮与地面的接触比凹面高模更稳定;
- 视觉模型存在非流形、开口或重复面时,不影响碰撞;
- 可以独立调整外观和动力学参数。
本文中的质量和惯量只是演示值,不代表真实车辆。做动力学研究时,应替换为真实数据。
一个容易踩的坑——车轮和车身之间的接触:MuJoCo 只自动排除直接父子 body 之间的接触。本文结构里轮子是"车身 → 转向 body → 轮子"的孙子节点,车身和轮子之间的接触不会被自动排除。如果让高模车身参与碰撞,轮拱和轮胎的几何穿插会让前轮疯狂左右摇摆(shimmy)。即使改用简化碰撞体,车身 box 和轮胎圆柱仍然有几何重叠,所以 XML 中必须保留 <exclude> 显式排除,不能省略。
同理,转向伺服也不要给太硬:转向轴总惯量只有约 0.6 kg·m²,kp=5000、±2000 N·m 这种配置会把微小角度误差放大成剧烈振荡。本文用 kp=400, kv=80, forcerange=±200。
7. 项目目录
创建以下目录:
car_mujoco/
├── scene.xml
├── sports_car.xml
├── drive_car.py
├── assets/
│ ├── car_body.obj
│ ├── windows.obj
│ ├── fl_wheel.obj
│ ├── fr_wheel.obj
│ ├── rl_wheel.obj
│ ├── rr_wheel.obj
│ └── sports-car_body_basecolor.png # 可选,仅贴图部分需要
scene.xml 中的相对路径以该文件所在目录为基准。如果没有贴图文件,记得按 3.4 节的说明把 XML 中相关的 texture/material 引用去掉,否则模型加载会报文件不存在。
8. 编写完整的 MJCF 文件
将以下内容保存为 sports_car.xml:
<mujoco model="sports_car">
<compiler angle="radian" meshdir="assets" texturedir="assets"/>
<option timestep="0.002" gravity="0 0 -9.81" integrator="implicitfast"/>
<asset>
<texture name="car_body_tex" type="2d" file="sports-car_body_basecolor.png"/>
<material name="car_body" texture="car_body_tex" shininess="0.1" reflectance="0.5"/>
<!– 由 sports-car.obj 拆分而来:车身/车窗保持整车坐标,轮子已平移到轮心为原点 –>
<mesh name="car_body_mesh" file="car_body.obj"/>
<mesh name="fl_wheel_mesh" file="fl_wheel.obj"/>
<mesh name="fr_wheel_mesh" file="fr_wheel.obj"/>
<mesh name="rl_wheel_mesh" file="rl_wheel.obj"/>
<mesh name="rr_wheel_mesh" file="rr_wheel.obj"/>
<mesh name="windows_mesh" file="windows.obj"/>
</asset>
<!– 车身↔轮子不是直接父子,不会被自动排除接触;轮拱与轮胎几何有穿插,必须显式排除 –>
<contact>
<exclude body1="car_body" body2="fl_wheel"/>
<exclude body1="car_body" body2="fr_wheel"/>
<exclude body1="car_body" body2="rl_wheel"/>
<exclude body1="car_body" body2="rr_wheel"/>
</contact>
<!– 车头朝 -x;轮心: fl/fr x=-1.399 y=±0.785, rl/rr x=1.261 y=±0.81, z=0.345 –>
<worldbody>
<!– 抬高 0.02,避免初始与地面零间隙接触 –>
<body name="car_body" pos="0 0 0.02">
<joint type="free" name="root"/>
<inertial pos="0 0 0.5" mass="1200" diaginertia="500 2000 2200"/>
<geom name="car_body_geom" type="mesh" mesh="car_body_mesh" material="car_body"
contype="1" conaffinity="1"/>
<geom name="windows_geom" type="mesh" mesh="windows_mesh"
contype="1" conaffinity="1"/>
<!– 左前轮:转向 body 在轮心,轮子在转向 body 内(网格已居中,无需偏移) –>
<body name="fl_steer" pos="-1.399 -0.785 0.345">
<joint name="fl_steer_joint" type="hinge" axis="0 0 1"
range="-0.52 0.52" damping="5" armature="0.02"/>
<inertial pos="0 0 0" mass="5" diaginertia="0.05 0.05 0.05"/>
<body name="fl_wheel" pos="0 0 0">
<joint name="fl_wheel_joint" type="hinge" axis="0 -1 0" pos="0 0 0"/>
<inertial pos="0 0 0" mass="15" diaginertia="0.54 0.89 0.54"/>
<geom name="fl_wheel_geom" type="mesh" mesh="fl_wheel_mesh"
friction="1.2 0.02 0.001" contype="1" conaffinity="1"/>
</body>
</body>
<!– 右前轮 –>
<body name="fr_steer" pos="-1.399 0.785 0.345">
<joint name="fr_steer_joint" type="hinge" axis="0 0 1"
range="-0.52 0.52" damping="5" armature="0.02"/>
<inertial pos="0 0 0" mass="5" diaginertia="0.05 0.05 0.05"/>
<body name="fr_wheel" pos="0 0 0">
<joint name="fr_wheel_joint" type="hinge" axis="0 -1 0" pos="0 0 0"/>
<inertial pos="0 0 0" mass="15" diaginertia="0.54 0.89 0.54"/>
<geom name="fr_wheel_geom" type="mesh" mesh="fr_wheel_mesh"
friction="1.2 0.02 0.001" contype="1" conaffinity="1"/>
</body>
</body>
<!– 左后轮 –>
<body name="rl_wheel" pos="1.261 -0.810 0.345">
<joint name="rl_wheel_joint" type="hinge" axis="0 -1 0" pos="0 0 0"/>
<inertial pos="0 0 0" mass="15" diaginertia="0.54 0.89 0.54"/>
<geom name="rl_wheel_geom" type="mesh" mesh="rl_wheel_mesh"
friction="1.2 0.02 0.001" contype="1" conaffinity="1"/>
</body>
<!– 右后轮 –>
<body name="rr_wheel" pos="1.261 0.810 0.345">
<joint name="rr_wheel_joint" type="hinge" axis="0 -1 0" pos="0 0 0"/>
<inertial pos="0 0 0" mass="15" diaginertia="0.54 0.89 0.54"/>
<geom name="rr_wheel_geom" type="mesh" mesh="rr_wheel_mesh"
friction="1.2 0.02 0.001" contype="1" conaffinity="1"/>
</body>
</body>
</worldbody>
<!– ======================= 执行器 ======================= –>
<actuator>
<!– 四轮驱动:速度伺服(ctrl = 目标轮速 rad/s),正值 = 前进(-x 方向) –>
<velocity name="fl_drive" joint="fl_wheel_joint" kv="20"
ctrlrange="-80 80" forcerange="-250 250"/>
<velocity name="fr_drive" joint="fr_wheel_joint" kv="20"
ctrlrange="-80 80" forcerange="-250 250"/>
<velocity name="rl_drive" joint="rl_wheel_joint" kv="20"
ctrlrange="-80 80" forcerange="-250 250"/>
<velocity name="rr_drive" joint="rr_wheel_joint" kv="20"
ctrlrange="-80 80" forcerange="-250 250"/>
<!– 前轮转向:位置伺服(ctrl = 目标转角 rad)
转向轴惯量仅 ~0.6 kg·m²,增益和力矩上限都放低,避免振荡 –>
<position name="fl_steer" joint="fl_steer_joint" kp="400" kv="80"
ctrlrange="-0.52 0.52" forcerange="-200 200"/>
<position name="fr_steer" joint="fr_steer_joint" kp="400" kv="80"
ctrlrange="-0.52 0.52" forcerange="-200 200"/>
</actuator>
</mujoco>
将以下内容保存为 scene.xml:
<mujoco model="sports_car scene">
<include file = "sports_car.xml"/>
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.1 0.1 0.1" specular="0.9 0.9 0.9"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global azimuth="140" elevation="-20"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
</asset>
<worldbody>
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>
</worldbody>
</mujoco>
这里的前轮转向采用了简化结构:每个前轮由嵌套的两个 body 组成——外层转向 body(绕局部 Z 轴的 hinge)位于轮心,内层车轮 body(绕局部 Y 轴的 hinge)挂在转向 body 里。它适合演示,但不等同于真实的转向节、主销和阿克曼转向机构。
9. 用 Python 让车辆跑起来
将以下内容保存为 drive_car.py:
import time
import mujoco
import mujoco.viewer as viewer
# —————- 加载模型 —————-
model = mujoco.MjModel.from_xml_path("scene.xml")
data = mujoco.MjData(model)
# —————- 执行器句柄 —————-
drive_names = ["fl_drive", "fr_drive", "rl_drive", "rr_drive"]
steer_names = ["fl_steer", "fr_steer"]
actuator_ids = {
name: mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, name)
for name in drive_names + steer_names
}
missing = [name for name, idx in actuator_ids.items() if idx < 0]
if missing:
raise RuntimeError(f"找不到执行器:{missing}")
def set_drive(speed):
"""四轮目标轮速 rad/s。轮半径 0.345 m,10 rad/s ≈ 12 km/h。"""
for name in drive_names:
data.ctrl[actuator_ids[name]] = speed
def set_steer(angle):
"""前轮目标转角 rad,范围 ±0.52。"""
for name in steer_names:
data.ctrl[actuator_ids[name]] = angle
# —————- 初始状态检查 —————-
body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "car_body") # 修正:原名误写为 chassis
print("初始车身位置:", data.xpos[body_id])
for name in drive_names:
aid = actuator_ids[name]
print(f"{name}: ctrlrange={model.actuator_ctrlrange[aid]}, "
f"forcerange={model.actuator_forcerange[aid]}")
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
step_start = time.time()
t = data.time
if t < 1:
set_drive(100)
set_steer(0.0)
elif t < 8:
set_drive(100)
set_steer(2)
elif t < 12:
set_drive(100)
set_steer(–2)
else:
set_drive(0.0)
set_steer(0.0)
mujoco.mj_step(model, data)
viewer.sync()
# 与真实时间对齐
time.sleep(model.opt.timestep)
在当前目录运行(注意运行前先激活mujoco环境):
python drive_car.py
如果模型尺寸、坐标和轴向都正确,车辆应该先直行,然后以一个约 13 m 半径的圆弧转向,随后反打,最后主动刹停。

10. 小结
这里面还是有很多细节的,稍不留意就会出错,特别是坐标系,各个模块的坐标轴方向以及位置,开始各种轮子满天飞就是因为坐标系没搞对,当然这个没有固定的方法,每个模型的坐标系不一样需要具体情况具体分析了,只要知道物体摆的位置不对,或者动起来轮子乱跑,或者绕着莫名其妙的位置旋转,基本上就是坐标系错了,慢慢调吧。
坐标约定本身没有标准答案,但一旦选定,就要让 Blender 导出、XML 轮心坐标、关节轴向、控制脚本四个环节全部遵守同一个约定——绝大多数"灵异现象"都是其中某一环偷偷用了另一套约定(我这里就有一个方向上的小bug,大家找找看吧🤭)。






