简介面向机器人、运动控制与强化学习领域的 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.dllinclude里有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 modelsimple_pendulum option gravity0 0 -9.81 integratorEuler / worldbody light pos0 0 1.5 / body namependulum pos0 0 0 joint namehinge_joint typehinge axis0 1 0 pos0 0 0 / geom namerod typecapsule fromto0 0 0 0 0 -1 size0.02 / geom namemass typesphere pos0 0 -1 size0.1 mass0.5 / /body /worldbody /mujoco参数说明重力加速度方向和数值都显式写了避免 MuJoCo 默认设置和实际环境不一致。integratorEuler是为了和入门教材里的显式欧拉积分做对齐实际仿真建议用默认的半隐式欧拉数值稳定性更好。hinge_joint的旋转轴是 Y 轴摆锤绕着 Y 轴在 XZ 平面内摆动。geom里的fromto定义了几何体的骨架线段capsule类型会把这段线段变成胶囊体。质心集中在pos0 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_guardstd::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这些数组的索引和维度映射单独写一个函数集中管理每次加载新模型时先打印所有关节的名称、类型和索引范围确认无误后再写业务逻辑。从那以后我每次换模型都会强制走一遍这个打印流程再也没出现过维度错位的低级错误。希望帮到你。本文还有配套的精品资源点击获取