简介:面向机器人、运动控制与强化学习领域的 C++ 开发者,这是一套以 CMake 搭建 MuJoCo 工程并执行基础动力学计算的完整示例包。包内展示了从 CMakeLists 配置、XML/URDF 模型定义到加载模型、初始化数据并反复调用 mj_step 推进仿真的关键流程,便于快速建立起多体仿真项目的编译与运行骨架。压缩包共 217 个文件,约 26.29MB,包含 55 个 txt 说明、15 份 CMake 配置、14 个头文件、8 个 cpp 源码,以及 stl/urdf/xml 模型文件和编译生成的 so/a 库与可执行程序,结构完整、可直接参考。目前已有 311 人学习下载。对于需要将 MuJoCo 集成进 CMake 体系、希望了解库文件与构建产物组织的初学者和中级工程师,这套资料能省去大量摸索时间。1. 用 CMake 调 MuJoCo 做动力学计算:一份能跑通的最小工程标题写的是「简单动力学计算」,但真正卡住大多数人的不是动力学公式,而是 C++ 工程怎么把 MuJoCo 正确链接进来,以及链接之后面对mjData里几十个数组时,到底该读写哪几个字段。这份资源解决的就是这件事:用 CMake 组织一个最小 C++ 工程,加载 MJCF 模型,跑通前向动力学和逆动力学的数值计算,并把结果落成可验证的数据。适合两类人:一类是刚接触 MuJoCo、想在 C++ 里复现书本上牛顿-欧拉公式的初学者;另一类是已经在用 Python 封装做仿真、想换成 C++ 写实时控制协议的熟手。前提是你对 CMake 的基本语法不至于陌生到连target_link_libraries都要查半天。2. CMake 集成 MuJoCo:两种落地方式与版本陷阱2.1 为什么要优先考虑源码编译而不是预编译包MuJoCo 从开源以后,官方维护的 C API 头文件和库文件是分离的。Windows 上直接下预编译包,解压后bin里有mujoco.dll,include里有mujoco.h,看起来很方便,但实际用 CMake 对接时会有两个隐患:一是预编译包使用 MSVC 工具链,如果你用 MinGW 或 Clang 编译自己的工程,链接阶段大概率报 ABI 不兼容;二是不同小版本的mjData结构体字段有增删,预编译包的头文件如果和自己的代码版本不一致,运行时踩内存的概率极高。我一般推荐的做法是把 MuJoCo 作为源码子目录直接编进自己的工程。MuJoCo 官方仓库里自带了一个mujoco.h和mujoco.cpp的单文件编译入口,这两个文件在include目录下,CMake 里只要把它们加进源文件列表,再链接系统的dl和m库,就完成了集成。这个做法的好处是:你的可执行文件和 MuJoCo 源码用同一套编译器、同一套标准库,不会出现 ABI 错位;坏处是首次编译时间稍微长一点,但相比排查链接错误的成本,完全值得。2.2 FetchContent 方式:CMakeLists 从零到可运行的完整写法用 FetchContent 把 MuJoCo 源码拉下来编译,是当前社区里最常见的做法,原因很简单:find_package依赖系统里预先装好的包,而FetchContent可以在 CMake 配置阶段自动下载并编译,换一台机器也能复现。下面这个CMakeLists.txt是我在模拟项目X里实际用过的结构,去掉了和具体业务相关的部分:cmake_minimum_required(VERSION 3.16) project(mujoco_dynamics_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关闭 MuJoCo 自带的测试和示例,减少编译负担 set(MUJOCO_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(MUJOCO_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) include(FetchContent) FetchContent_Declare( mujoco GIT_REPOSITORY https://github.com/google-deepmind/mujoco.git GIT_TAG 3.1.4 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(mujoco) add_executable(dynamics_demo src/main.cpp ) target_link_libraries(dynamics_demo PRIVATE mujoco::mujoco)这里有几个参数需要说明。MUJOCO_BUILD_TESTS和MUJOCO_BUILD_EXAMPLES必须设为OFF,否则会多编译出大量测试文件,耗时至少增加三分之一。GIT_SHALLOW TRUE表示只拉取指定 tag 的浅克隆,既能减少下载体积,也能避免拉到未发布的开发分支。GIT_TAG建议锁定一个你验证过的版本,MuJoCo 的小版本迭代里,传感器接口和mjData的字段排列都变过,锁版本后换机器才能稳定复现结果。mujoco::mujoco是 MuJoCo 官方 CMake 导出的 target 名称,链接它之后,头文件路径和编译选项都已经传递过来了,不需要再手动include_directories。2.3 系统安装版配合 find_package:使用场景与路径匹配如果公司内部已经有统一的 MuJoCo 安装路径,或者你想快速验证一个改动而不想每次重新编译 MuJoCo,也可以走find_package路线。前提是安装时确实把 CMake 配置文件写进了系统路径。官方在发布包中带了mujoco-config.cmake,安装后可以通过下面的方式查找:find_package(mujoco REQUIRED) if(NOT mujoco_FOUND) message(FATAL_ERROR "MuJoCo 未找到,请检查安装路径或设置 CMAKE_PREFIX_PATH") endif() add_executable(dynamics_demo src/main.cpp) target_link_libraries(dynamics_demo PRIVATE mujoco::mujoco)如果 CMake 提示找不到包,最常见的原因是安装路径不在搜索范围内。此时不要急着改系统环境变量,先在命令行里用cmake -DCMAKE_PREFIX_PATH=/your/mujoco/install/path ..指定一次,能通就把该路径写进工程的CMakeLists作为 fallback。需要特别注意的是,某些旧版本的 MuJoCo 导出的 target 名称是mujoco而不是mujoco::mujoco,如果链接报 target 不存在,去安装目录下的*.cmake文件里搜一下实际导出的名称。系统安装版的另一个坑是版本不匹配:find_package找到的版本和你代码里#include <mujoco/mujoco.h>对应的头文件可能是不同步的,编译期不出错,运行期mjData的字段偏移对不上就会产生脏数据。所以凡是走find_package,我都会在代码里打印mj_version()来核对实际链接的版本。3. 动力学计算的核心:mjData 字段结构与正逆动力学3.1 qpos、qvel、qfrc 系列数组的索引对应关系MuJoCo 的动力学计算不直接操作刚体的速度和加速度,而是统一走广义坐标和广义速度。模型里每个关节按类型被分配一段索引,qpos是广义位置,qvel是广义速度。最容易出错的地方就在这里:不同类型的关节占用的维度不一样,而且qpos和qvel的维度并不相等。关节类型qpos 维度qvel 维度含义free(自由飞行)76四元数+位置 / 角速度+线速度ball(球铰)43四元数 / 角速度slide(滑动铰)11位移 / 线速度hinge(转动铰)11角度 / 角速度拿free关节举例,它的qpos是 7 维,前 4 个是表示姿态的四元数,后 3 个是全局位置;而qvel是 6 维,前 3 个是全局坐标系下的角速度,后 3 个是线速度。如果你手动给一个自由飞行体的qpos赋值,只塞了 3 个位置坐标进去,后面的四元数还是初始值或者干脆越界读到了别的关节数据,那么第一步mj_step就会算出错误的加速度。我在某跨平台系统的动力学模块里曾经犯过这个错,现象是物体在空中原地打转,排查了半天才发现qpos的维度算错了,少赋了 4 个四元数分量。3.2 前向动力学最小示例:施加控制后读取 qacc前向动力学的本质是:给定当前状态(qpos、qvel)和控制力(ctrl或qfrc_applied),求广义加速度qacc,然后积分得到下一时刻的状态。MuJoCo 里mj_step是完整的一步仿真,它内部会调用约束求解器来解算接触力和关节力,最后更新qacc和状态。下面是一个加载单摆模型并跑一步的最小示例:#include <mujoco/mujoco.h> #include <cstdio> int main() { // 加载 MJCF 模型,xml 文件与可执行文件同目录 char error[1024] = {0}; mjModel* model = mj_loadXML("pendulum.xml", nullptr, error, 1024); if (!model) { std::printf("加载模型失败: %s\n", error); return 1; } mjData* data = mj_makeData(model); // 把摆锤放到与竖直方向成 30 度的位置 >// 先做一次 forward,让约束信息合法化 mj_forward(model, data); // 清零所有外力和控制,排除干扰项 std::memset(data->qfrc_applied, 0, sizeof(mjtNum) * model->nv); std::memset(data->ctrl, 0, sizeof(mjtNum) * model->nu); // 计算逆动力学 mj_inverse(model, data); std::printf("逆动力学广义力 qfrc_inverse[0] = %f\n",><mujoco model="simple_pendulum"> <option gravity="0 0 -9.81" integrator="Euler" /> <worldbody> <light pos="0 0 1.5" /> <body name="pendulum" pos="0 0 0"> <joint name="hinge_joint" type="hinge" axis="0 1 0" pos="0 0 0" /> <geom name="rod" type="capsule" fromto="0 0 0 0 0 -1" size="0.02" /> <geom name="mass" type="sphere" pos="0 0 -1" size="0.1" mass="0.5" /> </body> </worldbody> </mujoco>参数说明:重力加速度方向和数值都显式写了,避免 MuJoCo 默认设置和实际环境不一致。integrator="Euler"是为了和入门教材里的显式欧拉积分做对齐,实际仿真建议用默认的半隐式欧拉,数值稳定性更好。hinge_joint的旋转轴是 Y 轴,摆锤绕着 Y 轴在 XZ 平面内摆动。geom里的fromto定义了几何体的骨架线段,capsule类型会把这段线段变成胶囊体。质心集中在pos="0 0 -1"处的球体上,质量 0.5kg,这样单摆的有效摆长就是 1 米。编译这个示例时,把上面的 XML 文件和main.cpp放在同一目录,用第 2 章的CMakeLists.txt构建即可。注意 XML 文件的加载路径:mj_loadXML的第二个参数可以传nullptr,表示从当前工作目录的相对路径加载,但如果你在 IDE 里直接跑,工作目录可能是工程根目录而不会是src目录,所以要么把 XML 放到CMAKE_RUNTIME_OUTPUT_DIRECTORY下,要么在代码里把路径拼接正确。4.2 数值对比:能量守恒验证与时间步选择单摆是个保守系统,没有阻尼时机械能应该守恒。我把 MuJoCo 跑出来的动能和势能总和打印出来,观察不同时间步长下的能量漂移情况。下面这个脚本逻辑加到main.cpp里循环 2000 步:double energy_total = 0.0; for (int t = 0; t < 2000; ++t) { mj_step(model, data); // 动能:0.5 * qvel^T * M * qvel // M 是质量矩阵,直接用 qfrc_inverse 的线性项说明省去内部计算 double ke = 0.5 *>std::mutex data_mutex; void simulation_loop(mjModel* model, mjData* data) { double sim_time = 0.0; double sim_step = 0.002; // 固定 500Hz 仿真步长 double start_wall = 0.0; while (running) { start_wall = now(); { std::lock_guard<std::mutex> lock(data_mutex); mj_step(model, data); sim_time += sim_step; } double elapsed = now() - start_wall; // 按剩余时间睡眠,保持仿真节奏 if (elapsed < sim_step) { sleep_for(sim_step - elapsed); } } }这个循环里我做了两件防止追帧的事:一是锁保护,二是睡眠补偿。如果你不加睡眠补偿,仿真会跑得比真实时间快,控制频率就会漂移,这在力矩控制里会让执行器发烫。验证封装是否可靠的一个技巧是:让仿真跑一个已知周期的单摆,持续 60 秒,统计摆动的周期误差。误差小于 1% 说明时间管理合格,否则就要检查睡眠精度和数据竞争问题。关于这个工程整体,我最后想分享的一点是:MuJoCo 的动力学计算本身并不复杂,复杂的是工程组织和对mjData字段语义的理解。我的习惯是把qpos、qvel、qfrc这些数组的索引和维度映射单独写一个函数集中管理,每次加载新模型时先打印所有关节的名称、类型和索引范围,确认无误后再写业务逻辑。从那以后我每次换模型,都会强制走一遍这个打印流程,再也没出现过维度错位的低级错误。希望帮到你。本文还有配套的精品资源,点击获取
简介:面向机器人、运动控制与强化学习领域的 C++ 开发者,这是一套以 CMake 搭建 MuJoCo 工程并执行基础动力学计算的完整示例包。包内展示了从 CMakeLists 配置、XML/URDF 模型定义到加载模型、初始化数据并反复调用 mj_step 推进仿真的关键流程,便于快速建立起多体仿真项目的编译与运行骨架。压缩包共 217 个文件,约 26.29MB,包含 55 个 txt 说明、15 份 CMake 配置、14 个头文件、8 个 cpp 源码,以及 stl/urdf/xml 模型文件和编译生成的 so/a 库与可执行程序,结构完整、可直接参考。目前已有 311 人学习下载。对于需要将 MuJoCo 集成进 CMake 体系、希望了解库文件与构建产物组织的初学者和中级工程师,这套资料能省去大量摸索时间。1. 用 CMake 调 MuJoCo 做动力学计算:一份能跑通的最小工程标题写的是「简单动力学计算」,但真正卡住大多数人的不是动力学公式,而是 C++ 工程怎么把 MuJoCo 正确链接进来,以及链接之后面对mjData里几十个数组时,到底该读写哪几个字段。这份资源解决的就是这件事:用 CMake 组织一个最小 C++ 工程,加载 MJCF 模型,跑通前向动力学和逆动力学的数值计算,并把结果落成可验证的数据。适合两类人:一类是刚接触 MuJoCo、想在 C++ 里复现书本上牛顿-欧拉公式的初学者;另一类是已经在用 Python 封装做仿真、想换成 C++ 写实时控制协议的熟手。前提是你对 CMake 的基本语法不至于陌生到连target_link_libraries都要查半天。2. CMake 集成 MuJoCo:两种落地方式与版本陷阱2.1 为什么要优先考虑源码编译而不是预编译包MuJoCo 从开源以后,官方维护的 C API 头文件和库文件是分离的。Windows 上直接下预编译包,解压后bin里有mujoco.dll,include里有mujoco.h,看起来很方便,但实际用 CMake 对接时会有两个隐患:一是预编译包使用 MSVC 工具链,如果你用 MinGW 或 Clang 编译自己的工程,链接阶段大概率报 ABI 不兼容;二是不同小版本的mjData结构体字段有增删,预编译包的头文件如果和自己的代码版本不一致,运行时踩内存的概率极高。我一般推荐的做法是把 MuJoCo 作为源码子目录直接编进自己的工程。MuJoCo 官方仓库里自带了一个mujoco.h和mujoco.cpp的单文件编译入口,这两个文件在include目录下,CMake 里只要把它们加进源文件列表,再链接系统的dl和m库,就完成了集成。这个做法的好处是:你的可执行文件和 MuJoCo 源码用同一套编译器、同一套标准库,不会出现 ABI 错位;坏处是首次编译时间稍微长一点,但相比排查链接错误的成本,完全值得。2.2 FetchContent 方式:CMakeLists 从零到可运行的完整写法用 FetchContent 把 MuJoCo 源码拉下来编译,是当前社区里最常见的做法,原因很简单:find_package依赖系统里预先装好的包,而FetchContent可以在 CMake 配置阶段自动下载并编译,换一台机器也能复现。下面这个CMakeLists.txt是我在模拟项目X里实际用过的结构,去掉了和具体业务相关的部分:cmake_minimum_required(VERSION 3.16) project(mujoco_dynamics_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关闭 MuJoCo 自带的测试和示例,减少编译负担 set(MUJOCO_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(MUJOCO_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) include(FetchContent) FetchContent_Declare( mujoco GIT_REPOSITORY https://github.com/google-deepmind/mujoco.git GIT_TAG 3.1.4 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(mujoco) add_executable(dynamics_demo src/main.cpp ) target_link_libraries(dynamics_demo PRIVATE mujoco::mujoco)这里有几个参数需要说明。MUJOCO_BUILD_TESTS和MUJOCO_BUILD_EXAMPLES必须设为OFF,否则会多编译出大量测试文件,耗时至少增加三分之一。GIT_SHALLOW TRUE表示只拉取指定 tag 的浅克隆,既能减少下载体积,也能避免拉到未发布的开发分支。GIT_TAG建议锁定一个你验证过的版本,MuJoCo 的小版本迭代里,传感器接口和mjData的字段排列都变过,锁版本后换机器才能稳定复现结果。mujoco::mujoco是 MuJoCo 官方 CMake 导出的 target 名称,链接它之后,头文件路径和编译选项都已经传递过来了,不需要再手动include_directories。2.3 系统安装版配合 find_package:使用场景与路径匹配如果公司内部已经有统一的 MuJoCo 安装路径,或者你想快速验证一个改动而不想每次重新编译 MuJoCo,也可以走find_package路线。前提是安装时确实把 CMake 配置文件写进了系统路径。官方在发布包中带了mujoco-config.cmake,安装后可以通过下面的方式查找:find_package(mujoco REQUIRED) if(NOT mujoco_FOUND) message(FATAL_ERROR "MuJoCo 未找到,请检查安装路径或设置 CMAKE_PREFIX_PATH") endif() add_executable(dynamics_demo src/main.cpp) target_link_libraries(dynamics_demo PRIVATE mujoco::mujoco)如果 CMake 提示找不到包,最常见的原因是安装路径不在搜索范围内。此时不要急着改系统环境变量,先在命令行里用cmake -DCMAKE_PREFIX_PATH=/your/mujoco/install/path ..指定一次,能通就把该路径写进工程的CMakeLists作为 fallback。需要特别注意的是,某些旧版本的 MuJoCo 导出的 target 名称是mujoco而不是mujoco::mujoco,如果链接报 target 不存在,去安装目录下的*.cmake文件里搜一下实际导出的名称。系统安装版的另一个坑是版本不匹配:find_package找到的版本和你代码里#include <mujoco/mujoco.h>对应的头文件可能是不同步的,编译期不出错,运行期mjData的字段偏移对不上就会产生脏数据。所以凡是走find_package,我都会在代码里打印mj_version()来核对实际链接的版本。3. 动力学计算的核心:mjData 字段结构与正逆动力学3.1 qpos、qvel、qfrc 系列数组的索引对应关系MuJoCo 的动力学计算不直接操作刚体的速度和加速度,而是统一走广义坐标和广义速度。模型里每个关节按类型被分配一段索引,qpos是广义位置,qvel是广义速度。最容易出错的地方就在这里:不同类型的关节占用的维度不一样,而且qpos和qvel的维度并不相等。关节类型qpos 维度qvel 维度含义free(自由飞行)76四元数+位置 / 角速度+线速度ball(球铰)43四元数 / 角速度slide(滑动铰)11位移 / 线速度hinge(转动铰)11角度 / 角速度拿free关节举例,它的qpos是 7 维,前 4 个是表示姿态的四元数,后 3 个是全局位置;而qvel是 6 维,前 3 个是全局坐标系下的角速度,后 3 个是线速度。如果你手动给一个自由飞行体的qpos赋值,只塞了 3 个位置坐标进去,后面的四元数还是初始值或者干脆越界读到了别的关节数据,那么第一步mj_step就会算出错误的加速度。我在某跨平台系统的动力学模块里曾经犯过这个错,现象是物体在空中原地打转,排查了半天才发现qpos的维度算错了,少赋了 4 个四元数分量。3.2 前向动力学最小示例:施加控制后读取 qacc前向动力学的本质是:给定当前状态(qpos、qvel)和控制力(ctrl或qfrc_applied),求广义加速度qacc,然后积分得到下一时刻的状态。MuJoCo 里mj_step是完整的一步仿真,它内部会调用约束求解器来解算接触力和关节力,最后更新qacc和状态。下面是一个加载单摆模型并跑一步的最小示例:#include <mujoco/mujoco.h> #include <cstdio> int main() { // 加载 MJCF 模型,xml 文件与可执行文件同目录 char error[1024] = {0}; mjModel* model = mj_loadXML("pendulum.xml", nullptr, error, 1024); if (!model) { std::printf("加载模型失败: %s\n", error); return 1; } mjData* data = mj_makeData(model); // 把摆锤放到与竖直方向成 30 度的位置 >// 先做一次 forward,让约束信息合法化 mj_forward(model, data); // 清零所有外力和控制,排除干扰项 std::memset(data->qfrc_applied, 0, sizeof(mjtNum) * model->nv); std::memset(data->ctrl, 0, sizeof(mjtNum) * model->nu); // 计算逆动力学 mj_inverse(model, data); std::printf("逆动力学广义力 qfrc_inverse[0] = %f\n",><mujoco model="simple_pendulum"> <option gravity="0 0 -9.81" integrator="Euler" /> <worldbody> <light pos="0 0 1.5" /> <body name="pendulum" pos="0 0 0"> <joint name="hinge_joint" type="hinge" axis="0 1 0" pos="0 0 0" /> <geom name="rod" type="capsule" fromto="0 0 0 0 0 -1" size="0.02" /> <geom name="mass" type="sphere" pos="0 0 -1" size="0.1" mass="0.5" /> </body> </worldbody> </mujoco>参数说明:重力加速度方向和数值都显式写了,避免 MuJoCo 默认设置和实际环境不一致。integrator="Euler"是为了和入门教材里的显式欧拉积分做对齐,实际仿真建议用默认的半隐式欧拉,数值稳定性更好。hinge_joint的旋转轴是 Y 轴,摆锤绕着 Y 轴在 XZ 平面内摆动。geom里的fromto定义了几何体的骨架线段,capsule类型会把这段线段变成胶囊体。质心集中在pos="0 0 -1"处的球体上,质量 0.5kg,这样单摆的有效摆长就是 1 米。编译这个示例时,把上面的 XML 文件和main.cpp放在同一目录,用第 2 章的CMakeLists.txt构建即可。注意 XML 文件的加载路径:mj_loadXML的第二个参数可以传nullptr,表示从当前工作目录的相对路径加载,但如果你在 IDE 里直接跑,工作目录可能是工程根目录而不会是src目录,所以要么把 XML 放到CMAKE_RUNTIME_OUTPUT_DIRECTORY下,要么在代码里把路径拼接正确。4.2 数值对比:能量守恒验证与时间步选择单摆是个保守系统,没有阻尼时机械能应该守恒。我把 MuJoCo 跑出来的动能和势能总和打印出来,观察不同时间步长下的能量漂移情况。下面这个脚本逻辑加到main.cpp里循环 2000 步:double energy_total = 0.0; for (int t = 0; t < 2000; ++t) { mj_step(model, data); // 动能:0.5 * qvel^T * M * qvel // M 是质量矩阵,直接用 qfrc_inverse 的线性项说明省去内部计算 double ke = 0.5 *>std::mutex data_mutex; void simulation_loop(mjModel* model, mjData* data) { double sim_time = 0.0; double sim_step = 0.002; // 固定 500Hz 仿真步长 double start_wall = 0.0; while (running) { start_wall = now(); { std::lock_guard<std::mutex> lock(data_mutex); mj_step(model, data); sim_time += sim_step; } double elapsed = now() - start_wall; // 按剩余时间睡眠,保持仿真节奏 if (elapsed < sim_step) { sleep_for(sim_step - elapsed); } } }这个循环里我做了两件防止追帧的事:一是锁保护,二是睡眠补偿。如果你不加睡眠补偿,仿真会跑得比真实时间快,控制频率就会漂移,这在力矩控制里会让执行器发烫。验证封装是否可靠的一个技巧是:让仿真跑一个已知周期的单摆,持续 60 秒,统计摆动的周期误差。误差小于 1% 说明时间管理合格,否则就要检查睡眠精度和数据竞争问题。关于这个工程整体,我最后想分享的一点是:MuJoCo 的动力学计算本身并不复杂,复杂的是工程组织和对mjData字段语义的理解。我的习惯是把qpos、qvel、qfrc这些数组的索引和维度映射单独写一个函数集中管理,每次加载新模型时先打印所有关节的名称、类型和索引范围,确认无误后再写业务逻辑。从那以后我每次换模型,都会强制走一遍这个打印流程,再也没出现过维度错位的低级错误。希望帮到你。本文还有配套的精品资源,点击获取