欢迎光临
我们一直在努力

MuJoCo进阶——创建一辆自己的跑车

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,那么它通常只能可靠地提供几何形状,不能自动还原下载页面上的渲染效果。

下载模型时需要检查:

  • 是否允许修改和再分发,许可证是否满足项目要求;
  • 是一个完整整车,还是已经拆好的多个零件;
  • 是否包含 MTL 和贴图;
  • OBJ 中是否存在 vt 行,即是否包含 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 个零件

    通常建议先交付完整装配体,由仿真建模人员拆分,毕竟如何使用模型,分几个模块是仿真人员需要考虑的,不能把工作前置到设计端,做好职责解耦和切分,原因是:

  • 拆成几个刚体取决于运动学结构,而不只是外观结构;
  • 工程师未必知道仿真模型需要哪些 link 和 joint;
  • 拆分后还要调整零件原点、轮轴方向和网格精度;
  • 完整装配体保留各零件的相对位置,便于核对安装关系。
  • 例如,一辆车可以拆成:

    • 车身+四个车轮,共 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 的标准做法是:

  • 用 <mesh> 加载 OBJ;
  • 用 <texture> 加载图片;
  • 用 <material> 创建材质;
  • 用 <geom material="…"> 把材质分配给网格。
  • 因此,即使 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 拆分网格

    如果网格模型本身已经是拆分好的,就不需要我们再做拆分,但如果是一体的,需要做拆分,常见操作是:

  • 进入编辑模式;
  • 选中某个车轮的面;
  • 使用 P → Selection 将其分离为独立对象;
  • 重复处理其余车轮和车身;
  • 按 car_body、wheel_fl 等规则重命名;
  • 将不参与独立运动的小零件合并到对应刚体。
  • 拆分后先不要导出,还需要统一坐标系和零件原点。

    在blender中打开的样子是这样的,注意如果是打开obj文件,需要在文件菜单下选择导入,如果是blender文件可以直接打开:

    在这里插入图片描述

    关于"导入后皮肤丢了":这通常不是导入失败,而是两个原因之一——

  • Blender 视口默认是实体(Solid)着色模式,不显示贴图。按 Z 切换到 Material Preview(材质预览)模式即可看到材质和贴图;
  • 如果切换后仍没有贴图,多半是 MTL 里 map_Kd 的贴图路径是相对路径,而贴图文件没有放在 OBJ 旁边的对应位置,把贴图放对路径再重新导入即可。
  • 另外注意,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 设置车轮原点

    对每个车轮执行:

  • 将 3D 游标定位到轮轴中心;
  • 使用 Object → Set Origin → Origin to 3D Cursor;
  • 检查车轮是否以轮轴为中心;
  • 调整局部朝向,使轮轴与局部 Y 轴一致;
  • 应用必要的旋转和缩放变换(Ctrl+A → All Transforms,这一步同时会烘掉 OBJ 导入时自动加的 90° 旋转)。
  • 注意:仅仅修改对象原点,不一定意味着最终 OBJ 已使用局部坐标。Blender 导出时可能仍应用对象的世界变换。

    因此,导出局部零件时建议:

  • 记录零件在整车中的安装位置(即轮心坐标,后面 XML 的 body pos 要用);
  • 将该零件临时移动到世界原点;
  • 按本文约定调整其朝向;
  • 只导出当前零件;
  • 撤销移动,再处理下一个零件。
  • 如果直接把仍处于整车世界位置的零件导出,就可能重新变回方案 A。

    5.4 统一单位

    CAD 常用毫米,MuJoCo 通常使用米。如果模型在 Blender 中显示为一辆长度为 4000 的车,应整体缩放为约 4,而不是在 XML 中随意补偿。

    建议:

  • 在 Blender 中设置单位为米;
  • 检查整车长度、宽度和轮半径;
  • 对网格应用缩放;
  • 导出 OBJ 时使用 Scale = 1.0。
  • 不要同时依赖“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 不能用“全部放到原点就会拼成整车”来验证,因为所有局部零件都放在原点时本应重叠。

    应该分别检查(括号内是本文这辆车的实测值,可直接对照):

  • 车轮导入后是否以轮轴中心为原点(顶点范围 X、Z 约 ±0.345 = 轮胎半径,Y 约 ±0.135 = 胎宽一半);
  • 车轮最薄的方向是否是局部 Y 轴(薄轴 = 轮轴);
  • 车身是否位于自身参考原点附近(车长沿 X,约 ±2.19;底面 Z ≈ 0.19;车头一端在 +X);
  • 尺寸是否以米为单位;
  • 所有零件是否都保持 Z 向上;
  • 将记录的安装位置重新应用到各零件后,能否恢复整车。
  • 本文这辆车的轮心坐标(即 XML 中的 body pos),建议在 Blender 母版中记录并核对:

    车轮xyz
    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,大家找找看吧🤭)。

    赞(0)
    未经允许不得转载:171主机测评 » MuJoCo进阶——创建一辆自己的跑车
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址