mjswan + MicroDuck:把策略搬到浏览器里,并用真实物理外力把鸭子甩倒
这一节复现一个很适合公开展示的具身智能小实验:使用官方 mjswan_playground 构建 MicroDuck 的浏览器端 MuJoCo 场景,在浏览器里运行 ONNX 策略控制鸭子,再用鼠标拖拽鸭子的身体,给它施加真实的 MuJoCo 外力。
这里要先把边界说清楚:
- 这不是重新训练 MicroDuck,也不是把一个视频做成可交互的假动画;
- 浏览器端仍然会加载场景、物理引擎、策略模型和控制面板;
- 拖拽时,mjswan 会把指针位移转换成施加在刚体上的力,释放鼠标后由物理引擎继续计算;
- 本节验证的是“策略回放 + 浏览器物理交互 + 静态部署”这条链路,不等于 MicroDuck 真机已经可以安全执行这些动作。
1. 这次要跑通什么
最终会得到一个可以通过静态 HTTP 服务打开的网页应用:
- MuJoCo 模型被压缩成浏览器可加载的场景文件;
- MicroDuck 的策略被导出为 ONNX;
- MuJoCo WASM 在浏览器中负责动力学计算;
- ONNX Runtime Web 负责策略推理;
- Three.js 负责渲染;
- 控制面板负责切换场景、策略和速度指令;
- 鼠标拖拽负责给动态刚体施加外力。
本地复核时,普通回放可以正常显示 MicroDuck;拖拽鸭子以后,画面会出现力箭头,鸭子会因为外力和接触动力学倒下,而不是沿着预先录好的轨迹播放。

图 1:本地构建的 mjswan MicroDuck 浏览器应用,策略控制面板和 MuJoCo 场景均已加载。

图 2:按住鼠标拖拽鸭子时的活动状态。红色箭头表示当前施加的外力,鸭子的姿态变化来自物理仿真。
下面这张动图来自官方 mjswan_playground 项目,用来展示项目原本的浏览器交互效果;上面两张 PNG 则是本次在本地构建并验证得到的截图。

图 3:官方 MicroDuck 浏览器示例。来源:ttktjmt/mjswan_playground。

图 4:本地录制的多次人工拖拽示范。视频依次展示右上提拉、左侧推拽和斜向摆动;每次拖拽之间都重置场景,便于比较不同外力方向对姿态和倒地过程的影响。
2. 项目和代码入口
| 资源 | 地址 | 作用 |
|---|---|---|
| mjswan 引擎 | ttktjmt/mjswan | 将 MuJoCo 模型、控制逻辑和浏览器渲染整合起来 |
| mjswan 文档 | muwanx.readthedocs.io | 安装、示例、构建和浏览器运行说明 |
| mjswan Playground | ttktjmt/mjswan_playground | 官方示例项目和构建入口 |
| MicroDuck 示例源码 | mjswan_playground/microduck | MicroDuck 场景、策略和 Builder 配置 |
| MicroDuck 机器人项目 | pollen-robotics/microduck | 机器人模型、控制接口和硬件背景 |
| MicroDuck 强化学习环境 | pollen-robotics/microduck_rl | 训练环境、策略和部署相关资源 |
| 官方在线预览 | mjswan.com/s/DOGILsh | 不安装环境时直接观察官方示例 |
官方 Playground 的 MicroDuck 示例不是一个单独的“视频播放页面”,而是把场景和策略打包成静态网页。构建后的网页不需要 Python 后端来执行每一步仿真,浏览器内的 WASM、ONNX Runtime Web 和 WebGL 会共同完成运行。
3. 整体架构
mermaidgraph LR
A[MicroDuck MJCF / XML 场景] --> B[mjswan_playground Builder]
C[MicroDuck ONNX policies] --> B
D[MuJoCo scene assets] --> B
B --> E[manifest.json]
B --> F[scene.mjz / model assets]
B --> G[policy.onnx]
E --> H[静态 HTTP 服务]
F --> H
G --> H
H --> I[浏览器端 MuJoCo WASM]
H --> J[ONNX Runtime Web]
H --> K[Three.js / WebGL]
I --> L[状态与接触动力学]
J --> M[14 维动作]
M --> I
N[鼠标拖拽] --> O[mjData.xfrc_applied]
O --> I
I --> P[下一帧渲染与策略状态]
3.1 构建阶段
构建阶段发生在 Python 环境中。Builder 会收集项目的场景文件、策略文件、资源清单和网页运行时,把它们整理成浏览器可以直接读取的静态目录。
这里的关键产物不是一个新的训练 checkpoint,而是一组运行时文件:
- manifest.json:项目、场景、策略和资源的索引;
- .mjz 或其他场景资源:压缩后的 MuJoCo 模型;
- .onnx:浏览器端策略推理文件;
- .wasm 和 JavaScript:MuJoCo 浏览器运行时;
- index.html、CSS 和前端 bundle:控制面板与渲染入口。
3.2 浏览器运行阶段
浏览器打开页面以后,前端读取 manifest.json,选择当前场景和策略,然后加载对应的模型和 ONNX 文件。每一个控制周期大致经过以下流程:
text读取 MuJoCo 状态
-> 整理 61 维 observation
-> ONNX Runtime Web 推理
-> 得到 14 维 action
-> 写入 MuJoCo actuator
-> 计算接触、重力、摩擦和外力
-> Three.js 渲染下一帧
鼠标拖拽会插入到物理计算阶段:指针命中一个动态刚体后,拖拽位移会换算为力和力矩,写入 MuJoCo 的外力数组。松开鼠标后,外力被清除,鸭子继续按照当前速度、姿态、接触和策略输出运动。
4. 场景、策略和动作接口
4.1 官方 MicroDuck 示例包含什么
官方 Playground 的 MicroDuck 入口把多个场景和策略放在同一个浏览器应用里,常见组合如下:
| 场景 | 可观察的策略或动作 | 适合观察什么 |
|---|---|---|
| Duck | Walk、Stand & Pose、Sit / Stand、Ground Pick、Roulade | 行走、站立、坐下、拾取和翻滚 |
| Ball (right foot) | Kick | 右脚踢球 |
| Ball (left foot) | Kick | 左脚踢球 |
| Rollers | Roller Skate、Roller Crouch | 滑轮状态下的姿态和动作 |
不同版本的前端菜单文字可能略有变化。以页面实际列出的场景和策略为准,不要把菜单里的 Scene 和 Policy 当成两个完全独立的机器人:有些策略只在对应场景里有意义。
4.2 61 维输入和 14 维动作
MicroDuck 的策略接口延续了官方机器人控制约定。浏览器端示例中的策略通常读取 61 维 observation,并输出 14 维关节动作:
| observation 内容 | 维度 |
|---|---|
| 身体角速度或陀螺仪信息 | 3 |
| 身体坐标系中的重力投影 | 3 |
| 14 个关节位置 | 14 |
| 14 个关节速度 | 14 |
| 上一时刻动作 | 14 |
| 前向、横向和偏航速度指令 | 3 |
| 其余 embodiment command / padding | 10 |
| 合计 | 61 |
这里最容易被忽略的是动作接口。输出的 14 维向量并不是“直接给网页模型的 XYZ 位置”,而是和 MicroDuck 的关节顺序、动作缩放和 position actuator 定义绑定。换机器人、换关节顺序或换动作范围以后,不能只保留一个 ONNX 文件就认为策略仍然兼容。
本地检查的策略文件均能被 ONNX checker 正常读取,策略的 observation/action 形状符合 61 -> 14 契约。这个检查只能说明文件格式和接口正确,不能说明策略在新的机器人或新的物理参数上仍然有效。
4.3 浏览器示例与训练环境的区别
MicroDuck 的公开训练资源和浏览器部署资源不是完全同一层:
- 训练时可以使用更复杂的 actuator、随机化、课程学习和训练专用观测;
- 浏览器端需要把模型转换为 WASM 和 ONNX 能够执行的静态资源;
- 浏览器里看到的是导出后的 actor 和运行时场景;
- privileged critic、训练日志、并行环境和 PPO 更新不会在这个页面中运行。
因此,mjswan 适合验证“训练好的策略能否被打包并交互式展示”,不适合替代原始 RL 环境做大规模训练。
5. 物理拖拽是怎么工作的
5.1 它不是动作策略学出来的甩动
浏览器中拖拽鸭子时,红色箭头和倒地效果很容易让人误以为策略学会了“被人甩倒后恢复”。实际链路是:
- 鼠标射线命中一个可动态运动的刚体;
- 前端记录命中点相对于刚体的位置;
- 拖拽过程中的指针位移被换算成力和力矩;
- 力写入 MuJoCo 的 mjData.xfrc_applied;
- MuJoCo 根据质量、惯量、重力、接触和摩擦计算下一步;
- 页面把当前受力和身体姿态渲染出来。
这是一种 engine-level perturbation,也就是引擎级物理扰动,不是神经网络动作头生成的动作。这个区别很重要:
- 它可以用来测试策略受到外界冲击后的行为;
- 它可以用来制作很直观的浏览器教学演示;
- 它不能单独证明策略具有摔倒恢复能力;
- 真正研究恢复动作,还需要在训练环境中加入外力扰动、恢复奖励和独立测试集。
5.2 和我们已有 MicroDuck 任务的关系
我们之前整理的 MicroDuck 篮球平衡任务关注的是自由滚动篮球上的平衡和运动控制;本节的 mjswan 示例关注的是浏览器部署和物理交互。两者可以互相补充,但不要混写:
- 篮球平衡:重点是 RL 任务、接触奖励、LSTM actor、PPO 和 sim-to-real 边界;
- mjswan:重点是 MuJoCo 模型如何进入浏览器、ONNX 如何执行、鼠标外力如何进入物理引擎;
- mjswan 的右脚/左脚踢球场景可以作为网页端踢球展示的参考;
- 要实现我们自己的球门、连续追球、3v3 阵型和符合电机约束的真机策略,仍然需要单独修改场景、策略和部署接口。
6. 环境准备
6.1 推荐环境
官方仓库的主开发环境是 macOS Apple Silicon 或 Linux x86-64。Windows 用户建议使用 WSL2;如果只想观察已经构建好的网页,浏览器本身不要求本机安装 MuJoCo。
需要准备:
- Git;
- uv;
- 支持 WebGL、WebAssembly 和 Web Workers 的现代浏览器;
- 能够访问 GitHub 的网络环境;
- 足够的磁盘空间存放 Python 环境、前端 bundle 和场景资源。
6.2 拉取官方 Playground
在终端中执行:
bashgit clone https://github.com/ttktjmt/mjswan_playground.git
cd mjswan_playground
uv sync --all-extras
安装完成后,先列出官方示例:
bashuv run msp list
正常情况下可以看到 microduck、husky、pacman 和 wbc 等示例。
7. 构建和启动 MicroDuck
7.1 直接使用官方命令
在 mjswan_playground 根目录中构建 MicroDuck:
bashuv run msp build microduck --output-dir dist/microduck
构建成功后,dist/microduck 是一个静态网站目录。不要直接双击 index.html:浏览器会限制模块脚本、WASM 和 worker 的加载,页面可能看起来是黑屏或只显示 Loading。
官方 Playground 也提供直接运行示例的入口:
bashuv run msp run microduck --port 8092
如果命令自动获取上游仓库或 GitHub 资产时长时间没有进展,先检查网络和 Git 凭据。也可以按照 src/mjswan_playground/microduck/main.py 中锁定的版本手动下载源码归档,再使用同一套 Builder;不要把整个 Python 缓存目录复制进教程仓库。
7.2 使用本节提供的静态服务器
Windows 环境里,某些 Python 版本会把 .js 当作 text/plain 返回,浏览器会报:
textExpected a JavaScript-or-Wasm module script but the server responded with a MIME type of text/plain
本节提供的 serve_mjswan.py 会补齐常见 JavaScript、WASM、JSON、ONNX 和 MuJoCo 资源的 MIME 类型,并设置浏览器多线程 WASM 常用的响应头。构建完成后,在本教程目录执行:
bashpython serve_mjswan.py --directory PATH/TO/dist/microduck --port 8092
然后访问:
texthttp://127.0.0.1:8092/
如果路径包含空格,请给 --directory 的值加引号。服务器只用于本地教学验证,不要把它直接暴露到公网。
7.3 自动录制多次拖拽
本节的 record_mjswan_drag_demo.py 使用 Playwright 打开已经运行的网页,自动执行三种拖拽方向,并把原始录制保存为 WebM。它适合重新生成图 4,也方便后续增加新的拖拽动作:
bashuv run python record_mjswan_drag_demo.py \
--url http://127.0.0.1:8092/ \
--output-dir recordings
录制完成后,可以用 FFmpeg 转成网页和论坛更容易播放的 MP4:
bashffmpeg -i recordings/mjswan_microduck_manual_drag_demo.webm \
-c:v libx264 -pix_fmt yuv420p -movflags +faststart \
recordings/mjswan_microduck_manual_drag_demo.mp4
脚本只录制网页视口,不会录入本机地址栏、桌面或其他窗口;视频中的箭头和倒地来自浏览器端 MuJoCo 物理交互。
8. 浏览器操作步骤
Checkpoint 1:基础场景加载
打开页面后,先选择 Duck 场景和 Walk 策略。第一次加载通常需要等待浏览器下载 WASM、场景和 ONNX 文件,页面上的 Loading scene… 消失后再操作控制面板。
此时应该能看到:
- MicroDuck 模型;
- MuJoCo 网格和碰撞场景;
- Scene / Policy / Commands 控制区;
- 速度指令或动作控制滑块;
- 浏览器控制台没有资源 404。
Checkpoint 1 只能说明静态资源、WASM 和渲染链路已经启动,不能说明每个策略都已评测。
Checkpoint 2:策略回放
提高 Forward 指令,观察鸭子的行走动作。点击 Reset 后再次运行,用来确认 episode reset 会恢复初始状态。
建议依次观察:
- Duck + Walk:基础行走;
- Duck + Stand & Pose:站立和姿态动作;
- Duck + Sit / Stand:坐下和起立;
- Ball (right foot) + Kick:右脚踢球;
- Ball (left foot) + Kick:左脚踢球;
- Rollers:滑轮场景。
不同浏览器、设备和帧率下,策略动作速度可能不同。网页端的运行效果主要用于理解调用链和交互方式,不应直接拿来做定量控制频率结论。
Checkpoint 3:物理拖拽
切回 Duck 场景,按住鸭子身体并拖动。正常现象包括:
- 拖拽过程中出现力箭头;
- 鸭子的身体姿态发生变化;
- 脚、地面和身体之间仍然按照碰撞和摩擦计算;
- 松开鼠标后,拖拽外力消失;
- 点击 Reset 可以恢复干净的初始状态。
如果拖到了地面网格却没有反应,先把鼠标移动到鸭子身体或腿部的实体模型上。只有命中动态刚体,拖拽才会形成有效的外力。
9. 本次本地复核记录
本次没有只停留在阅读项目页,而是按官方源码和 Builder 实际构建了 MicroDuck 静态应用,并使用浏览器做了 smoke test。
| 检查项 | 结果 |
|---|---|
| 官方 Playground 环境同步 | 通过,mjswan 0.9.4 |
| 示例列表 | 通过,msp list 可列出 MicroDuck |
| MicroDuck Builder | 通过,生成 manifest.json、场景资源和策略资源 |
| ONNX 文件检查 | 通过,构建目录中的 26 个 ONNX 文件可被 checker 读取 |
| 浏览器 HTTP 服务 | 通过,静态资源返回状态 200 |
| WebGL canvas | 通过,页面检测到 1 个 canvas |
| 页面错误 | 0 个 JavaScript page error |
| 拖拽物理测试 | 通过,出现力箭头并观察到鸭子受力倒地 |
这份记录证明的是构建和交互链路可用,不是训练收敛指标,也不是官方 benchmark。为了让读者能够分辨证据来源,本教程的两张 PNG 都是本地截图,官方动图则单独标注为官方素材。
10. 常见问题
10.1 页面全黑或一直 Loading
先打开浏览器开发者工具的 Network 面板,检查 .js、.wasm、.mjz 和 .onnx 是否返回 200。然后确认:
- 页面是通过 HTTP 服务打开,而不是 file://;
- 静态服务器没有把 .js 返回为 text/plain;
- 服务器没有把大文件截断;
- 设备浏览器允许 WebGL 和 WebAssembly;
- 首次加载时耐心等待完整资源下载。
10.2 只有模型,没有动作
检查 manifest.json 中的策略路径,以及浏览器控制台中的 ONNX Runtime 报错。最常见原因是模型文件路径错误、跨域响应头缺失,或者页面加载的是另一套场景的 policy。
10.3 拖拽没有力箭头
拖拽点可能命中了静态地面,也可能当前场景没有可拖拽的动态 body。切换到 Duck 场景,把指针放到鸭子的身体、头部或腿部实体上,再按住并移动。
10.4 Windows 上 uv run pytest 出现编码错误
上游部分测试会直接用默认编码读取源文件。Windows 中文代码页下如果出现 UnicodeDecodeError,先设置:
powershell$env:PYTHONUTF8 = "1"
uv run pytest -q
如果仍然失败,要区分“测试脚本的默认编码问题”和“浏览器运行失败”。本次浏览器 HTTP、WebGL、ONNX 加载和物理拖拽均已单独验证,不把 Windows 测试编码问题误判成 mjswan 引擎问题。
10.5 GitHub 下载很慢
官方 Builder 需要获取依赖仓库或模型资产时,网络质量会直接影响首次构建。建议:
- 先执行 uv sync --all-extras,确认 Python 依赖完成;
- 使用官方仓库的固定 commit 下载源码归档;
- 将大缓存放在仓库外;
- 不要把 .cache、虚拟环境或完整构建目录提交到 Every Embodied。
11. 复现边界和后续改造
11.1 这次确实跑通了什么
- MicroDuck 的浏览器端静态应用可以被构建;
- 场景和 ONNX 策略可以由浏览器读取;
- MuJoCo WASM 能够执行动力学;
- 策略回放和网页控制面板可以工作;
- 鼠标外力会进入物理计算,并产生可观察的姿态变化。
11.2 这次没有证明什么
- 没有重新训练 MicroDuck policy;
- 没有证明浏览器策略可以直接部署到 RDK X5;
- 没有证明网页端的动作频率等于真实舵机控制频率;
- 没有证明右脚/左脚踢球策略能自动追球、连续踢球或精准射门;
- 没有证明被拖倒以后策略具备学习到的恢复能力;
- 没有把官方 3D 模型和完整训练资产重新打包发布。
如果后续要把这个例子接到我们的鸭子足球线,合理的改造顺序是:
- 先保留 mjswan 的浏览器物理和控制面板;
- 加入自定义球门、球场边界和球的碰撞材质;
- 将球检测、接近、对准和踢球拆成可观察的状态机;
- 再把训练得到的 policy 导出为浏览器可用的 ONNX;
- 通过同一套动作接口接到 RDK X5 控制端;
- 最后再做多鸭子、3v3 和真实电机约束验证。
这样可以把“浏览器展示”和“机器人控制策略”分成两个清楚的层次:网页先负责可视化和交互验证,RDK 再负责真实执行与安全限幅。
12. 许可证和素材使用
mjswan 和 mjswan Playground 的代码入口以各自仓库中的许可证为准;MicroDuck 代码和策略资源也应按照上游仓库的许可证使用。MicroDuck 的 3D 模型资产存在单独的非商业署名许可限制,公开教程时应保留来源和许可证说明,不要把本地构建目录中的全部资源直接当作 Every Embodied 自有资产重新发布。
本节只提交:
- 学习者可以阅读的教程文字;
- 小尺寸的本地验证截图;
- 官方公开示例的本地归档副本和来源说明;
- 可复用的静态服务器脚本。
不提交浏览器构建产生的完整 WASM、ONNX、场景包、缓存和虚拟环境。需要完整运行包时,请按照上游仓库和官方构建命令重新生成。