简介面向需要在C工程中集成MuJoCo并进行基础动力学计算的开发者这是一份CMake构建MuJoCo项目的实操资源包适用于机器人学、运动控制与强化学习仿真场景。压缩包共217个文件、约26.29MB包含cmake/make构建配置、cpp/h源码、xml/urdf模型描述、stl网格模型、js辅助资源以及编译生成的库文件与可执行产物从源代码、构建脚本到最终产出均可对照阅读便于理解整个仿真项目的组成与生成过程。已有310人学习。通过资源中的CMakeLists配置、MuJoCo API调用示例和配套模型文件读者可以掌握find_package查找库、链接Mujoco::mujoco、用mj_loadXML加载模型并循环执行mj_step完成动力学求解的完整流程同时还能利用打包好的运行库直接复现工程。对于希望快速搭起MuJoCoCMake项目骨架、排查构建链接问题、梳理物理仿真代码组织的学习者这份现成模板能显著缩短上手时间。1. 把 MuJoCo 装进 CMake 工程为什么“简单的动力学计算”要先过构建这关你想用 MuJoCo 算一个简单动力学数值比如单摆的角加速度却很可能先被 CMake 劝退源码包放进工程后 add_subdirectory头文件找不到链接符号对不上最后还得去翻库里的 CMakeLists 看导出目标名到底是什么。这几乎是每个新用户遇到的第一个坑。实际上只要把 MuJoCo 作为一个子目录集成进自己的 CMake 工程加载模型后用一次mj_forward调用就能拿到广义加速度qacc计算本身并不复杂。这篇文章从构建讲起给你一份能编译的最小 CMakeLists再逐步做到前向动力学计算、状态赋值和参数验证。适合已经会写 C/CMake、想做机器人或物理仿真动力学分析的人不需要图形渲染经验。2. 用 CMake 把 MuJoCo 编译进本地工程最小配置与关键开关2.1 源码与依赖落位 third_party 还是 find_package往自己工程里接 MuJoCo常见做法有两种一种是源码级依赖把 MuJoCo 源码包放进third_party/目录再用add_subdirectory编进来另一种是系统级安装后用find_package(mujoco REQUIRED)链接。对“简单动力学计算”这个目标我更推荐前者。原因很实际MuJoCo 版本迭代快不同版本对编译器版本和依赖库的要求有差异源码子目录方式能把版本锁死在你的仓库里换机器时重新拉依赖即可不会污染系统环境。前置条件比较简单一个支持 C17 的编译器GCC 9 以上或者 Clang 10 以上即可MSVC 2019 也可以CMake 版本建议 3.16 以上。MuJoCo 本身会带一些编译期依赖比如 OpenGL、SDL 这些跟渲染窗口相关的库。如果你只是在无显示环境里做动力学计算这些依赖完全可以通过 CMake 选项关掉后面我会给出具体写法。关掉渲染模块不会影响mj_forward、mj_step这些核心数值计算函数。我经常在服务器容器里只关 OpenGL 编一个纯动力学的可执行文件编译时间能缩短不少。需要注意的是一旦关闭 OpenGLMuJoCo 的mj_visualize相关接口就不可用但这跟我们要算的qacc没有任何关系。落地目录结构我一般这样摆project/ CMakeLists.txt main.cpp third_party/ mujoco/ # MuJoCo 源码包目录名自己定拿到源码包之后第一件事不是直接写 CMakeLists而是看一眼third_party/mujoco/CMakeLists.txt里的库目标名。不同版本的导出目标可能是mujoco::mujoco也可能是Mujoco::mujoco这决定了你后面target_link_libraries怎么写。如果没找到就去构建目录里搜索ALIAS关键字构建时 CMake 通常会把别名目标输出在cmake_install.cmake或生成的*.cmake文件里。2.2 最小 CMakeLists.txtadd_subdirectory、cache 变量与目标名这里给出一份可直接复制的最小 CMakeLists。它只做了一个事把 MuJoCo 编译进来然后链接给一个叫dynamics的可执行目标。cmake_minimum_required(VERSION 3.16) project(mj_dynamics LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关闭与动力无关的模块缩短编译时间 set(MUJOCO_BUILD_PYTHON OFF CACHE BOOL FORCE) set(MUJOCO_BUILD_EXAMPLES OFF CACHE BOOL FORCE) set(MUJOCO_BUILD_SIMULATE OFF CACHE BOOL FORCE) set(MUJOCO_ENABLE_OPENGL OFF CACHE BOOL FORCE) add_subdirectory(third_party/mujoco) add_executable(dynamics main.cpp) target_link_libraries(dynamics PRIVATE mujoco::mujoco)前四行set(... CACHE BOOL FORCE)是关键。这几个变量如果不显式设置MuJoCo 的默认配置会把 Python 绑定、示例程序、仿真器等全部编一遍既慢又容易出现依赖缺失。用FORCE是为了覆盖掉变量在缓存里可能存在的旧值保证每次重新配置都是同一套选项。最后一行target_link_libraries是整个集成的核心。链接隐藏目标mujoco::mujoco时CMake 会把 MuJoCo 的头文件目录、编译宏一并传递给dynamics这个目标不需要你再手动include_directories。所以第 2.1 节里确认过目标名后这行代码基本就是全部集成成本。有一点要提醒如果你看到的目标名不是mujoco::mujoco而是别的不要硬改命名空间直接去源码 CMakeLists 搜add_library和ALIAS把找到的目标名填进target_link_libraries即可。硬编码不存在的目标名CMake 会在配置阶段直接报错。2.3 首次构建与运行最小程序怎么验证可计算写一个最简main.cpp只加载模型并打印模型的自由度个数。这一步能快速区分“构建问题”和“动力学问题”先确认库本身能编译能链接再谈计算。#include mujoco/mujoco.h #include cstdio int main(int argc, char** argv) { if (argc 2) { std::fprintf(stderr, usage: %s model.xml\n, argv[0]); return 1; } char err[1024] {0}; mjModel* m mj_loadXML(argv[1], nullptr, err, sizeof(err)); if (!m) { std::fprintf(stderr, XML error: %s\n, err); return 1; } mjData* d mj_makeData(m); std::printf(model loaded: nq%d nv%d nu%d\n, m-nq, m-nv, m-nu); mj_deleteData(d); mj_deleteModel(m); return 0; }这里nq是广义坐标维度nv是广义速度维度nu是执行器数量。对于单摆这种一个铰链关节的模型nq1、nv1通常是预期值。nq和nv不相等也很正常比如有欧拉角表示旋转的关节时nq会比nv大。编译命令比较常规cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j4 ./build/dynamics pendulum.xml-DCMAKE_BUILD_TYPERelease建议每次都带因为 MuJoCo 的动力学求解涉及密集的矩阵运算Release 模式下的优化对计算耗时影响非常明显。如果看到nq1 nv1 nu0说明构建链路已经通了接下来可以进入真正的动力学计算。3. 前向动力学的计算闭环mjModel、mjData 与 mj_forward3.1 用 XML 描述一个单摆模型文件怎么选动力学计算先从模型文件开始。MuJoCo 的模型格式是 MJCF本质是 XML。最小可运行的单摆模型可以这样写mujoco modelsimple_pendulum option gravity0 0 -9.81/ worldbody body namelink pos0 0 0 joint namepin typehinge axis0 0 1/ geom namepoint typesphere size0.02 pos0.1 0 0 mass0.1/ /body /worldbody /mujoco这里body是运动链条上的一个刚体它的原点在(0,0,0)joint定义了一个铰链旋转轴是 z 轴geom是质量属性载体位置在局部坐标(0.1,0,0)也就是说质心距离旋转轴 0.1 米。gravity明确写成垂直向下避免依赖库里的默认值。把geom用成一个小球而不是细杆是为了验证方便球的半径很小自转惯量对整体转动惯量影响可以忽略这样解析解和数值解能对得上。如果换成胶囊体或者立方体虽然模型更接近真实摆但验证公式要额外算惯性张量对新手不太友好。3.2 mj_loadXML 与 mj_makeData静态模型与动态数据的分工加载模型时mjModel存的是所有“不随状态变化”的信息关节位置、几何尺寸、质量、惯性张量、执行器参数等。mjData存的是“每一帧都会变”的信息关节位置、速度、加速度、力、约束状态等。#include mujoco/mujoco.h #include cstdio int main(int argc, char** argv) { char err[1024] {0}; if (argc 2) { std::fprintf(stderr, usage: %s model.xml\n, argv[0]); return 1; } mjModel* m mj_loadXML(argv[1], nullptr, err, sizeof(err)); if (!m) { std::fprintf(stderr, load failed: %s\n, err); return 1; } mjData* d mj_makeData(m); // 通过名字拿到关节 id比硬编码索引稳妥 int jid mj_name2id(m, mjOBJ_JOINT, pin); if (jid 0) { std::fprintf(stderr, joint not found\n); return 1; } // jnt_qposadr 和 jnt_dofadr 给出这个关节在 qpos/qvel 中的起始索引 int qadr m-jnt_qposadr[jid]; int vadr m-jnt_dofadr[jid]; std::printf(joint axis index: qadr%d, vadr%d\n, qadr, vadr); mj_deleteData(d); mj_deleteModel(m); return 0; }mjLoadXML第二个参数传空指针表示不指定模型回调如果有需要可以在那里传一个mjvOption之类的结构但计算场景基本用不到。加载失败时错误信息会写入err最多 1024 字节务必检查返回值而不是假定成功。值得强调的是jnt_qposadr和jnt_dofadr。MuJoCo 内部在求解动力学前会对自由度重新排序关节声明的顺序不等于qpos里的顺序。即使这个模型只有一个关节硬编码索引 0 没问题但一旦模型有多个关节索引错位会直接导致计算结果毫无意义。所以养成通过mj_name2id查 id 的习惯后面写复杂模型就不会翻车。3.3 一次调用 mj_forward前向动力学到底做了什么mj_forward是 MuJoCo 里最核心的数值函数。它接受当前状态qpos、qvel以及外部输入ctrl、xfrc_applied等然后完成正向运动学、质量矩阵组装、约束求解最终把广义加速度写入d-qacc。用一句话概括给定这一时刻的位置、速度和输入力矩算出这一刻的加速度。这个加速度可以直接用来做积分也可以用来分析某个构型下的动力学特性。// 在加载完模型、创建完 data 之后 d-qpos[0] 0.5; // 让摆偏转 0.5 弧度 d-qvel[0] 0.0; // 初始角速度为 0 mj_forward(m, d); // 计算当前状态下的广义加速度 std::printf(qacc[0] %.6f\n, d-qacc[0]);这段代码的顺序很重要必须先给qpos、qvel赋值再调用mj_forward。如果你先调mj_forward再改状态qacc里存的就是修改前的计算结果。另外注意qpos的单位是弧度qvel的单位是弧度每秒不是角度。mj_forward没有返回值它把计算结果直接写进mjData。这样做的好处是调用栈简单不需要额外的输出参数坏处是很多人第一次用的时候容易到处找返回值找半天才发现数据都在d-qacc里。3.4 用解析解验证数值结果先对照再放心拿到qacc[0]只是第一步。怎么知道这个数算得对不对单摆模型有个现成的解析公式角加速度等于-g/l * sin(theta)。在上面这个模型里质心距离旋转轴0.1米重力加速度取9.81所以期望值约等于-98.1 * sin(theta)。#include cmath double theta 0.5; double g 9.81; double l 0.1; double alpha_expected -g / l * std::sin(theta); std::printf(mujoco : %.6f\n, d-qacc[0]); std::printf(analytic : %.6f\n, alpha_expected);运行结果会和解析解差 1% 左右原因是小球自身有转动惯量2/5 * m * r^2这一项在整体惯量里虽然小但不为零。如果这个差异让你不安可以把球的size改到0.002偏差会进一步缩小到 0.01% 量级。这种“自己动手验证”的习惯建议保留尤其是当后面换成多自由度模型时手算验证不太现实但至少可以先在小模型上确认你自己的调用方式正确。4. 动力学计算避坑五个高频问题与处理顺序4.1 现象mj_loadXML 失败错误信息只说 XML 解析有问题原因最常见是文件路径不对或者 XML 根节点写错。mj_loadXML对路径很敏感工作目录不同相对路径就失效。另一个隐蔽原因是 XML 文件编码MuJoCo 的解析器对 UTF-8 支持最好带 BOM 的文件容易时报无法识别。解决先在代码里打印argv[1]确认实际读到的是哪个路径再检查 XML 首行是不是mujoco开头。如果路径复杂建议把模型文件和可执行文件放在同一目录下用相对路径调试。遇到 BOM 问题用编辑器把文件另存为 UTF-8 without BOM。4.2 现象qacc 一直是 0怎么改变状态都没有反应原因有两个常见可能。第一mj_forward调用发生在qpos、qvel赋值之前计算时状态还未更新第二XML 里显式写了gravity0 0 0在没有任何外力的情况下单摆的加速度确实为 0这其实是正常结果。解决把赋值顺序调整为先写qpos、qvel再调mj_forward。同时打印m-opt.gravity的实值确认重力方向不是零向量。我习惯在模型加载后立刻输出一次m-opt.gravity避免用了默认配置还以为自己设了重力。4.3 现象多个关节时索引错位读出来的 qacc 值明显不对原因MuJoCo 的qpos和qvel顺序不是按模型 XML 中关节书写顺序排列的。内部会针对运动学树做自由度编号优化尤其在带有自由关节、球关节或全局坐标系时关节 id 和数组索引可能差得远。解决不要用“第几个关节就取第几个数”的方式。改用mj_name2id拿到关节 id再通过m-jnt_qposadr[jid]和m-jnt_dofadr[jid]获得真实索引。这个习惯虽然多写两行代码但换模型、加关节时几乎不用改逻辑。4.4 现象链接时找不到 mujoco target报 target not found原因不同版本的 MuJoCo 导出的 CMake 目标名不完全相同可能是mujoco::mujoco也可能是Mujoco::mujoco或者直接是mujoco。如果没确认就照抄别人的写法CMake 会在配置阶段直接失败。解决在add_subdirectory之后打开构建目录里的CMakeCache.txt搜mujoco相关的键值确认实际生成的库目标名。也可以看 MuJoCo 源码的CMakeLists.txt里add_library那几行的命名。确认后再回写target_link_libraries。4.5 现象数值结果和手算解析解差百分之几角度越大误差越大原因几何体自转惯量没有进解析式。你在推导时只算了m*l^2但 MuJoCo 计算惯性张量时包含了球体绕自身轴的转动惯量这部分随角度不变但会让实际角加速度偏小。解决把几何体半径改小比如从0.02改到0.002重新解析验证。如果差的是固定比例说明你漏了某项常数如果差值随角度非线性变化就要检查是不是把铰链轴向写错了。5. 从单步计算到可复现实验参数扫描与轨迹输出5.1 用命令行参数控制初始状态一次运行算多个工况单摆只有一个自由度手动改qpos就能算不同初始角度。但真实场景往往需要批量扫描不同位形或不同速度比如看一支机械臂在不同关节角组合下的加速度响应。这时候把初始状态参数化最方便。// run_dyn model.xml qpos qvel #include mujoco/mujoco.h #include cstdio #include cstdlib int main(int argc, char** argv) { if (argc 4) { std::fprintf(stderr, usage: %s model.xml qpos qvel\n, argv[0]); return 1; } char err[1024] {0}; mjModel* m mj_loadXML(argv[1], nullptr, err, sizeof(err)); if (!m) return 1; mjData* d mj_makeData(m); double q0 std::atof(argv[2]); double v0 std::atof(argv[3]); d-qpos[0] q0; d-qvel[0] v0; mj_forward(m, d); std::printf(theta%f v%f qacc%f\n, q0, v0, d-qacc[0]); mj_deleteData(d); mj_deleteModel(m); return 0; }这个程序把模型路径、初始角度、初始角速度都通过命令行传入。配合 shell 的 for 循环可以从0.1扫到1.5弧度一次输出多条结果for th in 0.1 0.5 1.0 1.57 3.0; do ./build/run_dyn pendulum.xml $th 0.0 doneqpos只传一个值是因为模型只有一个自由度。如果模型有多个自由度程序里就要按nq顺序读参数或者读一个 JSON 文件来传状态。命令行方式适合临时实验文件方式适合正式实验记录。5.2 把 mj_forward 接成时间步进循环得到一条完整轨迹单步mj_forward算的是某个冻结状态下的瞬时加速度而实际动力学计算通常需要随时间推进。MuJoCo 提供mj_step它在内部做半隐式欧拉积分每一步都会更新qpos、qvel和仿真时间。// 在 mj_makeData 之后 double dt m-opt.timestep; // 步长单位秒 for (int i 0; i 2000; i) { mj_step(m, d); std::printf(%.6f %.6f %.6f\n, i * dt, d-qpos[0], d-qvel[0]); }这里的m-opt.timestep是模型文件里配置的时间步长MuJoCo 默认通常是0.002秒也就是 2 毫秒。如果想改积分步长在 XML 的option标签里加timestep0.001即可。注意不要在一个循环里既调mj_forward又调mj_step。mj_step内部已经做了“计算加速度 积分更新”两步如果混用状态会被重复推进数据就不连续了。想观察每一步加速度可以在调用mj_step之前打印d-qacc或者直接在一个稳态构型下用mj_forward单独算。5.3 与解析解对照的三种验证方法做完时间步进之后你需要一个判断标准确认仿真结果没有偏离物理规律。这里给出三种验证手段按成本从低到高排列验证方式构造方法通过标准单步瞬态对比固定qpos、qvel调用mj_forward与解析公式比较数值差小于 2%能量守恒关闭摩擦和阻尼记录总能量随时间变化总能量波动小于初值的 0.5%周期对比单摆从同一初始角度释放记录角速度过零时间两次周期差异小于 1%能量守恒是最有效的大坑探测器。如果在关闭阻尼后机械能明显衰减说明你无意中开了摩擦或者执行器仍在输出力矩。检查d-ctrl和 XML 里是否没有damping属性。周期对比则能验证长时间积分是否稳定——如果步长过大周期会系统性偏移。6. 顺手把轨迹落盘成 CSV一套可回放的动力学实验习惯我现在的项目习惯是任何动力学计算程序都先把结果写成 CSV再考虑打印屏幕。原因很简单屏幕输出是易失的改一个参数重新编译就没了CSV 可以做曲线图、可以和其他工具比对、也可以交给同事复现。在时间步进循环里加上文件输出只需要几行#include cstdio FILE* fp std::fopen(trajectory.csv, w); std::fprintf(fp, time,qpos,qvel,qacc\n); double dt m-opt.timestep; for (int i 0; i 2000; i) { std::printf(time%.6f\n, m-data-time); double acc_before_step d-qacc[0]; std::fprintf(fp, %.6f,%.6f,%.6f,%.6f\n, i * dt, d-qpos[0], d-qvel[0], acc_before_step); mj_step(m, d); } std::fclose(fp);注意我在mj_step之前读qacc因为步进之后qacc会被下一次前向动力学覆盖。拿到 CSV 后用 Python 读取并画相位图qvel对qpos单摆的轨迹会呈现闭合环如果环逐渐缩小说明有能量损失。这个工作流的价值不在代码量而在于“可回放”。任何一次实验只要保留模型 XML 和 CSV别人就能精确复现你的结果。我希望你在自己的项目里也保留这个习惯哪怕只是临时调试。希望帮到你。本文还有配套的精品资源点击获取