# MuJoCo JavaScript 绑定 这是 [MuJoCo 物理引擎](https://github.com/google-deepmind/mujoco) 的官方规范 JavaScript 和 TypeScript 绑定。 本软件包提供了一个高级 API,使您能够与编译为高性能 WebAssembly (WASM) 模块的 MuJoCo 核心引擎进行交互。这些绑定由 Google DeepMind 开发和维护,并始终与 MuJoCo 的最新进展保持同步。为简明起见,下文中的文档通常称为“JavaScript”,但相关概念同样完全适用于 TypeScript。 > [!IMPORTANT] > _这些绑定仍处于开发阶段(WIP)。详情请参阅[未来工作](#未来工作)部分。另请注意,开发主要是在 Linux 上使用 Google Chrome 进行的。如果您在其他操作系统或浏览器上开发,可能会遇到一些粗糙边缘。我们已在 CI 中成功在 macOS 上测试了绑定,但截至 2025 年 11 月 13 日,Windows 支持仍处于实验性阶段(在某台 Windows 11 设备上安装成功,但在其他设备上失败)。_ ## 安装 使用 MuJoCo JavaScript 绑定最简单的方法是从 npm 安装 `@mujoco/mujoco` 软件包: ```sh npm install @mujoco/mujoco ``` 该软件包采用 ESM 模块规范(`type: module`),包含预编译的 WebAssembly 模块、JavaScript 绑定和 TypeScript 类型声明。请确保您的打包工具(bundler)或开发服务器在运行时能够正确提供 `.wasm` 静态资源服务。 ### 线程模型(Threading Models) `@mujoco/mujoco` 软件包包含两个不同的引擎构建版本,以支持不同的浏览器环境和性能需求。 #### 1. 单线程(默认) 标准的单线程版本位于软件包的根目录。它兼容所有现代浏览器,无需特殊的安全响应头配置。 ```typescript import loadMujoco from '@mujoco/mujoco'; ``` #### 2. 多线程(MT) 多线程版本位于 `/mt` 子目录中。它利用 Web Workers 和 `SharedArrayBuffer` 来并行化物理计算。 ```typescript import loadMujoco from '@mujoco/mujoco/mt'; ``` > [!NOTE] > 由于使用了 `SharedArrayBuffer`,浏览器需要配置跨域隔离(Cross-Origin Isolation)才能启用多线程。您的 Web 服务器必须发送以下 HTTP 响应头: > - `Cross-Origin-Opener-Policy: same-origin` > - `Cross-Origin-Embedder-Policy: require-corp` > > 如果缺少这些响应头,模块将无法完成初始化。 ## 从源码构建 ### 前置依赖 > [!NOTE] > 请在项目顶级根目录下运行本 README 中的所有命令。 - 要编译 [`bindings.cc`](codegen/generated/bindings.cc) 文件(用于生成 `.wasm` WebAssembly 文件、`.js` JavaScript 导入文件和 `.d.ts` TypeScript 声明文件),您需要安装 Emscripten SDK `4.0.10` 版本。更新的版本可能可用但未经充分测试。要配置 SDK,请执行以下命令(您可以在任何位置运行安装,但本 README 中的后续命令仅在执行了 `source ./emsdk/emsdk_env.sh` 的 Shell 中生效): ```sh git clone https://github.com/emscripten-core/emsdk.git ./emsdk/emsdk install 4.0.10 ./emsdk/emsdk activate 4.0.10 source ./emsdk/emsdk_env.sh ``` - 要轻松运行 JavaScript 测试和演示应用,需要安装 `node` 和 `npm`。我们建议使用 [nvm](https://github.com/nvm-sh/nvm) 进行版本管理。此外,测试、演示和绑定构建过程还需要一些 JavaScript 依赖项。这些依赖位于 `wasm` 文件夹中。要安装它们并确保后续命令可以找到相关工具,请运行: ```sh npm install --prefix ./wasm export PATH="$(pwd)/wasm/node_modules/.bin:$PATH" ``` - 要修改绑定,需要安装 `python3`,因为 [`bindings.cc`](codegen/generated/bindings.cc) 文件是由 Python 脚本生成的。要运行绑定生成器测试,需要 `absl`,同时 `pytest` 也很有帮助。配置包含这些依赖的 Python 虚拟环境: ```sh python3 -m venv .venv source .venv/bin/activate pip install -r python/build_requirements.txt ``` > [!TIP] > _Emscripten 文档非常详尽。建议阅读涵盖 [Emscripten 编译器设置](https://emscripten.org/docs/tools_reference/settings_reference.html)、[Emscripten SDK](https://emscripten.org/docs/tools_reference/emsdk.html) 以及 [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html) 库的章节。若要了解将浏览器作为运行平台的相关限制和注意事项,请参阅 [Porting(移植)](https://emscripten.org/docs/porting/index.html#porting) 章节。_ 编译 [`bindings.cc`](codegen/generated/bindings.cc) 文件将生成 `.wasm` WebAssembly 文件、`.js` JavaScript 导入文件以及 `.d.ts` TypeScript 声明文件。这些文件将用于在 JavaScript 中调用 MuJoCo。要生成它们,请确保已配置好 npm 和 Emscripten SDK 前置环境,然后运行以下命令: ```sh emcmake cmake -B build && cmake --build build ``` 该命令将在项目根目录下生成以下文件夹: - `build`:包含使用 Emscripten 编译的 MuJoCo。 - `wasm/dist`:包含 WebAssembly 模块、`.js` 和 `.d.ts` 文件。 这些文件夹内的资源默认编译并配置为单线程运行。如果需要使用多线程版本的模块,请传递 `-DMUJOCO_WASM_THREADS=ON` 标志: ```sh emcmake cmake -B build -DMUJOCO_WASM_THREADS=ON && cmake --build build ``` ### 示例应用程序 生成绑定后,您就可以开始编写使用 MuJoCo 的 Web 应用程序了。我们提供了一个使用 Three.js 渲染简单仿真的基础 Web 应用,运行以下命令体验: ```sh npm run dev:demo --prefix ./wasm ``` 您也可以选择完全用 C++ 编写整个应用并使用 Emscripten 编译。如果采用这种方式,您将不需要使用这些绑定,因为您只需编写极少量的 JavaScript,而且这些绑定的粒度可能不太合适(例如,您可能希望在 `requestAnimationFrame` 触发的 C++ 回调中调用多个 MuJoCo 函数)。 我们还发现混合架构非常实用,因为直接在 JavaScript 中操作浏览器 API 通常更方便。如果您选择用 C++ 编写应用程序并使用 Emscripten 编译,您可以将 `bindings.cc` 中 `EMSCRIPTEN_BINDINGS` 的子集复制到您的应用程序源文件中。 ## 用户指南 ### 按名称访问(Named Access) 绑定支持按名称访问方法(与 Python 绑定类似),允许通过名称或索引便捷地访问模型和数据元素。例如,可以通过 `model.geom('mygeom')` 按名称访问几何体,或通过 `data.jnt('myjoint')` 访问关节。 有关如何使用按名称访问的更多详细信息和示例,请参阅[按名称访问测试](tests/bindings_test.ts#L1876-L2378)以及[官方文档](https://mujoco.readthedocs.io/en/stable/python.html#named-access)。 ### 内存管理 通过 Embind 封装创建或返回到 JavaScript 中的 C++ 对象句柄保存在 WebAssembly 堆内存中,**不会**被 JS 运行时的垃圾回收器自动回收。 任何暴露给 JS 的堆分配 C++ 对象(例如通过 `new Module.MyClass(...)` 创建,或作为绑定的指针/引用返回),在不再需要时必须显式释放,以避免内存泄漏。 在封装实例上调用生成的 `.delete()` 方法来销毁底层 C++ 对象: ```typescript const obj = new Module.MyClass(...); // ... 使用 obj ... obj.delete(); // 释放 C++ 内存 ``` 请注意,每个创建的对象只能调用 `.delete()` **一次**(重复释放属于错误)。在可能抛出异常或提前返回的 JS 代码路径中,请确保在 `finally` 块中执行删除操作,或对生命周期管理进行封装以避免内存泄漏。 > [!IMPORTANT] > _Embind 文档强烈建议 JavaScript 代码显式释放其接收到的所有 C++ 对象句柄。_ ### 拷贝 vs. 引用(Copy vs. Reference) 通过 WASM 绑定与 MuJoCo 对象交互时,理解数据的访问方式非常重要。`MjModel` 和 `MjData` 等对象上的属性可以通过两种方式暴露数据:按值拷贝(Copy)或按引用(Reference)。 #### 1. 按值拷贝(基于值的访问) 某些属性在访问时会返回数据的副本。对于需要从 C++ 封送到 JavaScript 的复杂数据结构,这是常见做法。 一个典型的例子是 `MjData.contact`。当访问 `data.contact` 时,您会获得一个包含该仿真时刻接触信息副本的对象。 如果推进仿真步进,这些副本将不会自动更新。您必须重新访问 `data.contact` 才能获取最新的接触信息。 您获得的对象是由 [Emscripten Embind 库](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#built-in-type-conversions)在使用 `register_vector` 暴露 `std::vector` 时生成的 JavaScript 代理接口。它本质上是一个“桥接”对象。 ```typescript export interface MjContactVec extends ClassHandle { /** 向向量末尾追加一个新元素,长度加 1。 */ push_back(_0: MjContact): void; /** 调整向量大小以包含指定数量的元素,新位置填充提供的值。 */ resize(_0: number, _1: MjContact): void; /** 返回向量中当前存储的元素总数。 */ size(): number; /** 检索指定索引处的元素;如果索引越界则返回 undefined。 */ get(_0: number): MjContact | undefined; /** 覆盖指定索引处的元素;成功返回 true,索引无效返回 false。 */ set(_0: number, _1: MjContact): boolean; } ``` 示例: ```typescript // 获取当前时刻的接触信息 const contacts = data.contact; // 推进仿真单步 mujoco.mj_step(model, data); // `contacts` 现在已过时。要获取新的接触信息,必须重新访问该属性: const newContacts = data.contact; // 记得在不再需要时删除所有创建的对象 contacts.delete(); newContacts.delete(); ``` #### 2. 按引用(基于视图的访问) 许多属性(特别是大型数值数组)会直接返回指向 WebAssembly 内存的实时视图(Live View)。这种方式效率极高,避免了大量数据的拷贝。 一个典型的例子是 `MjData.qpos`(关节位置)。当您获取对该数组的引用时,它直接指向仿真的状态数据。仿真中的任何更改(例如调用 `mj_step` 之后)都将立即反映在该数组中。 ```typescript // `qpos` 是指向仿真状态的实时视图 const qpos = data.qpos; console.log(qpos[0]); // 输出初始位置 // 推进仿真单步 mujoco.mj_step(model, data); // `qpos` 自动更新 console.log(qpos[0]); // 输出新位置 // 记得在不再需要时释放对象 data.delete(); ``` ### 数据排布:行主序矩阵(Row-Major Matrices) 当 MuJoCo C API 函数返回矩阵(或需要矩阵作为输入)时,在 JavaScript 绑定中表示为扁平的一维 `TypedArray`。元素按行主序(Row-Major)存储。 例如,一个 3x10 的矩阵将作为包含 30 个元素的扁平数组返回。前 10 个元素表示第一行,接下来的 10 个元素表示第二行,依此类推。 示例:访问 `(row, col)` 位置的元素 ```typescript // 存储为扁平数组的 3x10 矩阵 const matrix: Float64Array = ...; const nRows = 3; const nCols = 10; // 要访问第 `i` 行、第 `j` 列的元素: const element = matrix[i * nCols + j]; ``` ### 处理输出参数(Out Parameters) MuJoCo C API 中的许多函数使用“输出参数”(Out Parameters)来返回数据。这意味着它们不是直接返回值,而是将结果写入通过引用(指针)传递给它们的参数中。在 JavaScript 绑定中,您需要针对这些情况做特别处理。 通常会遇到两种主要场景: #### 1. 类数组输出参数 当函数期望接收指向基本类型(如 `mjtNum*` 或 `int*`)的指针来写入值数组时,您需要在 JavaScript 端预先分配结果内存。我们为此提供了辅助类:`mujoco.Uint8Buffer`、`mujoco.DoubleBuffer`、`mujoco.FloatBuffer` 和 `mujoco.IntBuffer`。 使用步骤如下: 1. **创建缓冲区**:使用正确大小的初始数组(例如全零数组)实例化相应的缓冲区类。 2. **调用函数**:将缓冲区实例作为输出参数传递给函数。 3. **获取结果**:在缓冲区上调用 `.getView()` 方法,获取由 C++ 函数写入的数据的 `TypedArray` 视图。 4. **释放内存**:使用完缓冲区后,必须调用 `.delete()` 方法释放底层内存,以防止内存泄漏。 示例:旋转向量 函数 `mju_rotVecQuat` 使用四元数 `quat` 旋转向量 `vec`,并将结果存储在 `res` 输出参数中。 ```typescript // 创建缓冲区以容纳 3D 向量结果 const res = new mujoco.DoubleBuffer([0, 0, 0]); const vec = [1, 0, 0]; const quat = [0.707, 0, 0, 0.707]; // 绕 z 轴旋转 90 度 try { // 调用函数,将缓冲区作为输出参数 mujoco.mju_rotVecQuat(res, vec, quat); // 获取 Float64Array 格式的结果视图 const resultView = res.getView(); console.log(resultView); // 期望输出:约 [0, 1, 0] } finally { // 重要:释放为缓冲区分配的内存 res.delete(); } ``` #### 2. 结构体输出参数(例如 mjvCamera*、mjvScene*) 当函数修改通过指针传递的结构体时,应传递相应 JavaScript 包装类的实例。底层 C++ 结构体将被就地(in-place)修改。 示例:更新场景 函数 `mjv_updateScene` 使用来自 `mjModel` 和 `mjData` 的信息填充 `mjvScene` 对象。 ```typescript // 创建必要结构体的实例 const model = mujoco.MjModel.from_xml_string(xmlContent); const data = new mujoco.MjData(model); const scene = new mujoco.MjvScene(model, 1000); const option = new mujoco.MjvOption(); const perturb = new mujoco.MjvPerturb(); const camera = new mujoco.MjvCamera(); // ... (单步仿真等) // 更新场景。'scene' 对象被函数就地修改。 mujoco.mjv_updateScene( model, data, option, perturb, camera, mujoco.mjtCatBit.mjCAT_ALL.value, scene ); console.log('场景中的 geom 数量:', scene.ngeom); // 记得在不再需要时删除所有创建的对象 scene.delete(); camera.delete(); perturb.delete(); option.delete(); data.delete(); model.delete(); ``` 与缓冲区一样,您负责管理这些结构体实例的内存,并在使用完毕后调用 `.delete()`。 ### 枚举类型(Enums) 通过 `.value` 访问枚举值: ```javascript mujoco.mjtDisableBit.mjDSBL_CLAMPCTRL.value ``` ### 常量(Constants) 标量常量可以直接作为属性访问: ```javascript mujoco.mjNEQDATA ``` 非标量常量(如 `mjFRAMESTRING`)也可以作为属性访问,并返回 JavaScript 数组: ```javascript mujoco.mjFRAMESTRING ``` 这将返回 MuJoCo 中 `mjFRAMESTRING` 包含的值对应的 JavaScript 数组。 > [!NOTE] > 您会发现像 `mjFRAMESTRING` 这样的常量被类型化为 `any`。这是因为它们在 C++ 中使用 `emscripten::val::array()` 进行绑定,而 Embind 在 TypeScript 定义文件中将 `emscripten::val` 映射为 `any`。虽然可以使用 `EMSCRIPTEN_DECLARE_VAL_TYPE(StringArray)` 将 `StringArray` 定义为 `emscripten::val` 的别名,并提示 Embind 如何在函数签名或使用 `.as()` 时处理类型转换,但这不会改变 `emscripten::constant` 推断属性类型的方式。 ## 开发指南 若要修改绑定,需要修改 [`bindings.cc`](codegen/generated/bindings.cc) 文件,但不应手动编辑。该文件是使用 [`codegen`](codegen) 文件夹中的 Python 脚本和模板文件生成的。要编辑绑定,需要修改这些文件,并使用以下命令重新生成 [`bindings.cc`](codegen/generated/bindings.cc): ```sh PYTHONPATH=python/mujoco python3 -m wasm.codegen.update ``` 代码生成脚本使用 MuJoCo 的 Python 内省(introspect)库来生成将 C++ 函数和类绑定到 JavaScript 的 Embind `EMSCRIPTEN_BINDINGS` 代码块。被绑定的函数和类是 MuJoCo C API 的封装层。这些封装层便于添加边界检查和完善的错误报告等功能。 ### 测试 1. **JavaScript API 测试。** 验证从 JavaScript 调用各种 MuJoCo 函数和类时能否正确工作。运行测试命令: ```sh npm run test --prefix ./wasm ``` 2. **JavaScript API 基准性能测试。** 目前的基准测试检查 JavaScript/C++ 共享内存缓冲区的性能。随着时间推移,我们将提高基准测试的覆盖率。运行基准测试: ```sh npm run benchmark --prefix ./wasm ``` 3. **绑定生成器测试。** 在开发或扩展绑定时相关。以下命令会查找并运行 `wasm` 文件夹中的所有 `test_*.py` 或 `*_test.py` 文件: ```sh PYTHONPATH=python/mujoco python3 -m pytest ./wasm ``` ### 调试 我们提供了一个“沙盒(sandbox)”应用,您可以在其中快速编写要在浏览器中运行的代码。在 [`main.ts`](tests/sandbox/main.ts) 文件中编写代码,并使用以下命令在浏览器中执行: ```sh npm run dev:sandbox --prefix ./wasm ``` 页面将是空白的,因为脚本仅在控制台(console)输出日志。您可以在指定的占位符处添加代码,并使用 Chrome 开发者工具进行调试。可以设置支持跨语言边界正确调用栈追踪和单步调试的工作流。我们目前实现这一点的方法仅在 Google 内部有效,但应该可以使用开源工具链复制相同的体验——欢迎社区提出建议! ## 版本规范 软件包版本遵循 MuJoCo 官方发布版本。 例如: | npm 版本 | MuJoCo 版本 | |-------------|----------------| | 3.5.0 | 3.5.0 | ## 未来工作 1. **绑定所有实用 API。** 这些绑定尚未完全覆盖所有接口。虽然核心 MuJoCo API(`mj_step`、`mj_loadXML` 等)已经过良好测试,但其他 API(例如来自 `mjspec.h` 的函数)在真实的 Web 应用程序中仍未经充分测试(尽管 `mjspec` 绑定的测试代码已存在)。 2. **改善开发者体验。** 在开发 WASM 绑定本身的开发者体验方面仍有改进空间。最明显的问题是绑定生成尚未完全自动化。因此,当前识别和应用更新绑定所需的更改还不够便捷。目标是最终将所有绑定代码生成自动化,并清晰提示由于 C++ 更新而在 WASM 绑定中需要进行的更改。此问题仅影响从事 MuJoCo C++ 引擎开发的开发者,不影响编写 JavaScript 的最终用户。 3. **完善文档。** 一旦绑定完成且按名称访问完全实现,本 README 中的文档最终将合并到 MuJoCo 主文档中。我们还计划审查绑定 API 并进行调整,以在遵循语言惯用法的同时最大程度减少与 Python 绑定的差异,从而减少所需的额外文档量。 4. **完善[示例应用](#示例应用程序)。** 我们的目标是提供一个可以轻松修改并嵌入到学术论文项目主页中的示例应用程序(参见[此示例](https://kzakka.com/robopianist/))。这可以通过扩展 Three.js 示例或使用 Emscripten 工具链编译 MuJoCo 平台 C++ 代码来实现。欢迎社区提出建议并参与贡献!