WAMR3D:Rust+WASM 内核的 WebGPU 引擎,比 three.js 快多少
一句话结论:three.js 场景一旦上到几千个动态对象,帧预算常常花在 JavaScript 的对象更新、矩阵运算和把 draw call 交给 GPU 上。2026 年 10 月出现的开源引擎 WAMR3D 换了个分工:把场景图、世界矩阵、视锥剔除、绘制排序、射线检测放进用 Rust 编译的 WebAssembly 内核,再配一套从零写的 WebGPU 渲染器,外层保留一套读起来像 three.js 的 API。作者公布的基准里,10,000 个独立动画网格 @1920×1080:WAMR3D 两种 API 都跑满 60 fps、CPU 提交约 12.2–12.3 ms;three.js WebGPU 9.5 fps / 100.5 ms,three.js WebGL 33.7 fps / 23.7 ms。同一份基准也写了反面:细粒度小调用比 three.js 慢 1.3–3.8 倍。
披露:本文编译自 WAMR3D 作者 Aymen 的发布文(dev.to,2026-10-14)。作者是引擎维护者;基准来自其仓库记录(单机、单次、中位数),文中的所有性能数字都应按你自己的场景复测。
一、它是什么:不是 three.js 的包装
WAMR3D 是一个 WebAssembly 3D 渲染引擎:Rust 内核 + 独立的 WebGPU 渲染器。它不是 three.js 的封装,也不是成熟的替代品——项目很年轻,README 自己就这么写。
三个组成部分:
| 部分 | 内容 |
|---|---|
| WASM 原生内核 | 场景图、世界矩阵、视锥剔除、绘制排序、射线检测都在 Wasm 模块里 |
| 自研 WebGPU 渲染器 | 网格、线、点、精灵、实例化网格;Basic / Lambert / Standard 材质;颜色贴图、雾、最多 16 盏灯、PCF 阴影、色调映射、渲染目标、4× MSAA |
| 两套 API | wamr3d/compat 下是 three.js 风格对象 API;另有面向大场景的低层批量 API |
许可 MIT,无运行时依赖,Wasm 模块约 488 KiB。
二、为什么把内核放进 Wasm
在大多数 three.js 应用里,场景状态存在于 JavaScript 对象上。每一次 position.set()、rotation.y +=、矩阵更新都是 JS 的工作,垃圾回收最终会介入。
WAMR3D 的做法不同:
- 场景存在引擎内存里。 对象在 WebAssembly 里显式创建和销毁,引擎本身不引入 GC 暂停。
- 分代句柄(generational handles)。 指向已销毁对象的句柄,不会悄悄指向另一个对象。
- 零拷贝几何。 通过 typed array 视图直接读写引擎内存中的顶点属性。
- 批量同步。 对象 API 里 position/rotation/scale 仍留在 JS,但每帧把改动一次性发送给引擎。
结果是一个明确的取舍:批量工作时 WAMR3D 赢,大量细碎调用时它输。项目把两面都写进了文档。
三、基准:先看清条件
数字来自仓库的基准记录,2026-10-14,headless Chrome、Windows x64、RTX 2070 SUPER,每种分辨率单次运行的中位数。作者明确说:这只是起点,请在自己的场景里测。
10,000 个独立动画网格 @1920×1080
| 引擎 | fps | Draw 提交(CPU) |
|---|---|---|
| WAMR3D(批量 API) | 60.0 | 12.2 ms |
| WAMR3D(对象 API) | 60.0 | 12.3 ms |
| three.js WebGPU | 9.5 | 100.5 ms |
| three.js WebGL | 33.7 | 23.7 ms |
50,000 个实例化物体,场景在 CPU 上更新
| 引擎 | 场景更新(CPU) |
|---|---|
| WAMR3D 批量 API | 12.0 ms |
| three.js WebGPU | 39.1 ms |
| three.js WebGL | 39.7 ms |
换成作者的话:在这个 showcase 里,WAMR3D 的 draw 提交 CPU 时间大约是 three.js WebGPU 的 1/8、three.js WebGL 的 1/1.9。帧率封顶 60 是 showcase 的显示限制。
反面:小调用更慢
项目也公布了另一面:做很少工作的调用(例如单次向量运算、一次改一个属性),比 three.js 慢 1.3 到 3.8 倍,因为进入 WebAssembly 有固定开销。GPU 时间没有单独测,只体现在帧率里。
读法:这不是"全面更快",而是"批量提交快、单点调用慢"。你的场景越像"几万个对象、每帧整体更新",越适合;越像"每帧对几十个对象做几百次细粒度 getter/setter",越不适合。
四、正确性:7,600 个对照用例
速度只有在输出正确时才有意义。WAMR3D 用 three.js r186 源码生成的 7,600 多个参照用例回归它的 Rust 实现:
- 单个操作的结果一致到约百万分之一;
- 23 个渲染器测试用例与保存的 three.js 图像逐像素比对;
- 643 个 Rust 测试覆盖单元、行为一致性(parity)与内存分配;
- 仓库还有一个视觉一致性页面,把 WAMR3D 渲染结果、three.js 图像和像素差异并排显示。
这套做法的价值在于:"像 three.js"不是口头承诺,而是可回归的对照。它也划出了边界——对照的是 three.js r186 的语义,不是所有 three.js 生态插件。
五、两分钟上手
npm install wamr3d
改 import、初始化一次、切到 WebGPU 渲染器:
- import * as THREE from 'three';
+ import * as THREE from 'wamr3d/compat';
+ await THREE.init();
- const renderer = new THREE.WebGLRenderer({ antialias: true });
+ const renderer = new THREE.WebGPURenderer({ antialias: true });
+ await renderer.init();
一个完整例子:
import * as WAMR3D from 'wamr3d/compat';
await WAMR3D.init();
const scene = new WAMR3D.Scene();
const camera = new WAMR3D.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(3, 2, 5);
camera.lookAt(0, 0, 0);
const cube = new WAMR3D.Mesh(
new WAMR3D.BoxGeometry(),
new WAMR3D.MeshLambertMaterial({ color: 0x4f8cff })
);
scene.add(cube, new WAMR3D.DirectionalLight(0xffffff, 2));
const renderer = new WAMR3D.WebGPURenderer({ antialias: true });
await renderer.init();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
renderer.setAnimationLoop((time) => {
cube.rotation.y = time / 1000;
renderer.render(scene, camera);
});
对 WAMR3D 支持的类,scene.add、position.set、rotation.y +=、lookAt、射线检测都按原样工作。迁移指南列出了具体有哪些类、要注意什么。
要求:支持 WebGPU 的浏览器(当前 Chrome 或 Edge),通过 HTTP 而不是 file:// 提供页面;Node.js 18+ 可以在没有渲染器的情况下跑数学与场景图。
六、大场景用批量 API
对象超过几百个之后,收益主要来自批量 API:句柄与 setRotations 之类的调用一次跨进 WebAssembly,更新成千上万个对象;世界矩阵通过 typed array 视图读取,不复制。
低层 API 直接从 wamr3d 导出:
import { init, World } from 'wamr3d';
await init();
const world = new World();
const scene = world.createObject();
const arm = world.createObject();
const hand = world.createObject();
world.add(scene, arm);
world.add(arm, hand);
world.setPosition(arm, 10, 0, 0);
world.setRotation(arm, 0, 0, Math.PI / 2, 'XYZ');
world.setPosition(hand, 1, 0, 0);
world.updateMatrixWorld(scene, false);
console.log(world.matrixWorld(hand)); // Float32Array(16), column-major
七、什么时候选它,什么时候别选
| 你的场景 | 建议 | 理由 |
|---|---|---|
| 数万实例化物体、批量动画 | 值得试 | 批量 API 的收益最大 |
| 数字孪生/城市/点云式大场景 | 值得试 | 剔除、矩阵、排序在 Wasm 内核 |
| 需要大量 three.js 生态插件 | 暂缓 | compat 覆盖的是核心类,不是整个生态 |
| 每帧大量细粒度属性读写 | 暂缓 | 跨 Wasm 调用有固定成本,慢 1.3–3.8× |
| 必须支持 Safari/旧设备 | 暂缓 | 依赖 WebGPU;且项目年轻 |
| 需要成熟的光照/后处理全家桶 | 暂缓 | 材质与后处理覆盖有限,需自行核对 |
八、一个可复现的评估流程
- 先跑官方 showcase(
wamr3d.org/examples/showcase),四种渲染器同页对比,确认在你的机器上确实有差距。 - 换成你自己的模型:GLB 走一遍加载、材质、阴影、实例化。可以先用 glTF 查看器 确认模型本身没有材质/贴图问题,再进引擎。
- 记录三个数:draw 提交 CPU 时间、场景更新 CPU 时间、帧率;同时记分辨率、对象数、是否实例化。
- 测最差路径:每帧对单个对象做几十次属性读写,看是否掉到 three.js 以下。
- 验收视觉:把同一相机角度的渲染结果与 three.js 对比;必要时用 3D 查看器 打开源模型作为参照。
- 确认部署条件:WebGPU 可用性、页面必须走 HTTP(S)、Wasm 488 KiB 的加载预算。
九、结论
WAMR3D 的价值不在于"打败 three.js",而在于把 Web 3D 的性能瓶颈换个位置:把每帧的 JavaScript 场景管理搬进 Wasm,把绘制交给自研 WebGPU 渲染器,用 60 fps vs 9.5 fps 这类数字证明路线可行;同时用 小调用慢 1.3–3.8 倍 提醒你它不是万能。
如果你的项目是"大场景 + 批量更新 + 愿意接受年轻引擎",它值得一个下午的实测;如果你要的是"成熟生态 + 细粒度控制 + 全平台兼容",three.js 仍然是更稳的默认选项。
Sources
- Aymen, WAMR3D: A Native WebAssembly 3D Engine with a three.js-Style API and Its Own WebGPU Renderer, dev.to, 2026-10-14:https://dev.to/aymen94/wamr3d-a-native-webassembly-3d-engine-with-a-threejs-style-api-and-its-own-webgpu-renderer-9ok
- 官网 / 基准展示:https://wamr3d.org/ | 仓库:https://github.com/aymen94/wamr3d
同主题阅读
本文属于「Web3D / 游戏引擎」专题。BimAnt 编译整理,转载请注明出处。基准为单机单次中位数,实际性能请以你的场景复测为准。