three.js AmmoPhysics 组件:用 Ammo.js 物理引擎为 three.js 应用添加刚体模拟
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
AmmoPhysics是 three.js 官方提供的一个物理引擎 Addon,它把基于 Bullet 物理引擎移植而来的 Ammo.js(WASM 版本)包装进three.js应用中,让开发者用几行代码即可为场景中的Mesh/InstancedMesh添加刚体碰撞、重力下落与回弹模拟。读完本文,你将掌握该组件的初始化方式、三个核心 API(.addMesh、.addScene、.setMeshPosition)的完整参数与默认值,以及它在源码层面的碰撞管线构建、运动状态同步机制和适用限制。
组件定位:官方 Addon 之一
AmmoPhysics是一个 addon(附加组件),不属于three核心包,必须显式导入。仓库中它位于 examples/jsm/physics/AmmoPhysics.js,与同为 WASM 物理封装的JoltPhysics、RapierPhysics并列存放在examples/jsm/physics/目录下。官方手册 manual/pages/physics.html 中也对三者做了区分:
- AmmoPhysics:Ammo.js(Bullet Physics)的封装;
- JoltPhysics:Jolt Physics 的封装;
- RapierPhysics:Rapier 的封装。
手册同时指出,像 Ammo.js 这类 C++ 物理引擎编译到 WebAssembly 的方案,“在性能、稳定性和精度上表现最好,尤其适合复杂模拟”,但往往需要更多搭建代码——而AmmoPhysics正是为了降低这套搭建成本而存在的薄封装层。需要注意手册中的一句提示:Ammo.js 目前“不再活跃维护”,选型时建议同时对比RapierPhysics等仍在维护的方案。
import { AmmoPhysics } from 'three/addons/physics/AmmoPhysics.js';初始化:await AmmoPhysics()的加载机制
组件通过一个异步工厂函数初始化:
const physics = await AmmoPhysics();文档明确强调:组件会自动从 CDN 导入 Ammo.js,因此必须保证运行时处于联网状态。这一点在源码中可以直接印证——examples/jsm/physics/AmmoPhysics.js 的开头硬编码了 Ammo.js 的 CDN 地址(固定到 Ammo.js 的一个具体 commit):
const AMMO_PATH = 'https://cdn.jsdelivr.net/gh/kripken/ammo.js@79190a1f03845794b1bba1777f30037349967658/builds/ammo.wasm.js';AmmoPhysics()内部的加载逻辑是:
- 检查全局
Ammo是否已存在(例如页面已经手动引入过ammo.wasm.js脚本,则可跳过这一步); - 若不存在,则动态创建
<script>标签注入document.head,onload时 resolve、onerror时 reject; - 加载完成后执行
await Ammo()初始化 WASM 模块,拿到AmmoLib。
拿到AmmoLib后,组件立即构建一套完整的 Bullet 碰撞管线(源码 L35-L42):
const frameRate = 60; const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration(); const dispatcher = new AmmoLib.btCollisionDispatcher( collisionConfiguration ); const broadphase = new AmmoLib.btDbvtBroadphase(); const solver = new AmmoLib.btSequentialImpulseConstraintSolver(); const world = new AmmoLib.btDiscreteDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration ); world.setGravity( new AmmoLib.btVector3( 0, - 9.8, 0 ) );从源码结构看,这套配置是写死的:重力固定为(0, -9.8, 0),帧率固定为 60,均不对外暴露修改接口。物理世界通过setInterval( step, 1000 / frameRate )(源码 L292)以约 60fps 的固定节奏驱动,每帧计算真实流逝的 delta 后调用world.stepSimulation( delta, 10 ),其中第二个参数 10 表示每帧最多允许的固定时间步数。
AmmoPhysics()最终返回一个包含三个方法的对象:addScene、addMesh、setMeshPosition。下面逐一说明。
API 详解
.addMesh( mesh, mass, restitution )
把单个网格加入物理模拟。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mesh | Mesh | 必填 | 要加入模拟的网格 |
mass | number | 0 | 质量(单位:kg)。传0表示静态刚体(不随模拟运动,但仍参与碰撞) |
restitution | number | 0 | 恢复系数,通常取 0~1,表示碰撞时物体有多“弹” |
源码实现(handleMesh,L125-L155)揭示了两个关键行为:
- mass 决定刚体类型:只有
mass > 0的网格才会被推进内部meshes更新列表并建立mesh → body的WeakMap映射;mass === 0的刚体虽然也加入世界,但属于静态体,不参与每帧的位置回写。 - 惯性自动计算:组件调用
shape.calculateLocalInertia( mass, localInertia )由碰撞形状自动计算转动惯量,无需手动提供。
同时,addMesh对几何体类型有硬性限制。内部getShape()(源码 L48-L80)只支持两种映射:
| three.js 几何体 | Bullet 碰撞形状 | 尺寸取值 |
|---|---|---|
BoxGeometry | btBoxShape | width / height / depth的一半(未提供时各半轴默认 0.5) |
SphereGeometry/IcosahedronGeometry | btSphereShape | parameters.radius(未提供时默认 1) |
两种形状都会setMargin( 0.05 )给碰撞形状加上 0.05 的外扩余量。传入其他几何体(如CylinderGeometry、PlaneGeometry、加载出的 GLTF 几何体)时,组件只会console.error( 'AmmoPhysics: Unsupported geometry type:', ... )并静默跳过,不会抛出异常——这是一个容易踩的坑。若需要处理任意凸体,需绕过封装直接使用 Ammo API(后文示例会展示)。
const physics = await AmmoPhysics(); // 动态刚体:1kg 的箱子,恢复系数 0.3 physics.addMesh( box, 1, 0.3 ); // 静态刚体:地面 physics.addMesh( ground, 0 );.addScene( scene )
把整个场景(或任意Object3D子树)加入模拟。它会scene.traverse()遍历所有子节点,仅处理isMesh且userData.physics字段非空的网格(源码 L85-L103):
function addScene( scene ) { scene.traverse( function ( child ) { if ( child.isMesh ) { const physics = child.userData.physics; if ( physics ) { addMesh( child, physics.mass, physics.restitution ); } } } ); }也就是说,userData.physics是网格与物理世界的“契约”,可以携带mass和restitution两个字段:
box.userData.physics = { mass: 1 }; sphere.userData.physics = { mass: 0.5, restitution: 0.6 }; floor.userData.physics = { mass: 0 }; // 静态碰撞体这个设计很适合“批量声明式”地为场景装配物理属性:先搭好整个场景图,再一次性physics.addScene( scene )。没有userData.physics的网格会被自动忽略,所以它天然与“普通装饰物体”共存。
.setMeshPosition( mesh, position, index )
设置已加入模拟的网格的位置。文档特别指出:调用该方法会重置该网格当前模拟的线速度与角速度——源码中它对对应刚体先执行setAngularVelocity( 0,0,0 )和setLinearVelocity( 0,0,0 ),再用body.setWorldTransform()写入新位置(源码 L197-L224)。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mesh | Mesh | 必填 | 要更新位置的网格 |
position | Vector3 | 必填 | 新位置 |
index | number | 0 | 若网格是InstancedMesh,index表示实例 ID;普通Mesh忽略该参数 |
index参数正是该组件支持实例化物理的体现。
内部机制:运动状态如何回写到渲染网格
每帧的step()函数(源码 L230-L288)是数据同步的核心,流程为:
world.stepSimulation( delta, 10 )推进物理世界;- 遍历
mass > 0的网格,从对应btRigidBody的btDefaultMotionState中取回世界变换; - 普通
Mesh:直接mesh.position.set(...)/mesh.quaternion.set(...)回写; InstancedMesh:调用文件末尾的compose()工具函数(源码 L336-L364),把四元数与位置手工展开为列主序 4x4 矩阵,直接写入instanceMatrix.array对应实例的 16 个元素,然后设置mesh.instanceMatrix.needsUpdate = true并computeBoundingSphere()。
对InstancedMesh的建立过程也值得一看(handleInstancedMesh,L157-L193):它读取mesh.instanceMatrix.array,每 16 个元素用btTransform.setFromOpenGLMatrix()取出一个实例的初始变换,为每个实例创建独立的刚体,并把“一个 Mesh 对应一个 body 数组”存入meshMap。这就是setMeshPosition中index参数的用武之地——它选择bodies[index]来单独操作某一个实例。
从源码结构看,组件还留有扩展占位:返回对象附近有一行注释掉的// addCompoundMesh,可以推断组合刚体(compound shape)封装在规划中但尚未实现。
实战示例一:400+400 实例的批量刚体
官方示例 examples/physics_ammo_instancing.html 是该组件最完整的用例,完整代码可直接阅读该文件,关键片段如下:
import { AmmoPhysics } from 'three/addons/physics/AmmoPhysics.js'; physics = await AmmoPhysics(); // 静态地面:mass 为 0,不参与运动 const floorCollider = new THREE.Mesh( new THREE.BoxGeometry( 10, 5, 10 ), new THREE.MeshBasicMaterial( { color: 0x666666 } ) ); floorCollider.position.y = - 2.5; floorCollider.userData.physics = { mass: 0 }; floorCollider.visible = false; scene.add( floorCollider ); // 400 个实例的盒子 const geometryBox = new THREE.BoxGeometry( 0.075, 0.075, 0.075 ); boxes = new THREE.InstancedMesh( geometryBox, material, 400 ); boxes.instanceMatrix.setUsage( THREE.DynamicDrawUsage ); // 每帧更新 boxes.userData.physics = { mass: 1 }; scene.add( boxes ); for ( let i = 0; i < boxes.count; i ++ ) { matrix.setPosition( Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5 ); boxes.setMatrixAt( i, matrix ); } // 400 个实例的二十面体(IcosahedronGeometry 会被映射为球体碰撞形状) const geometrySphere = new THREE.IcosahedronGeometry( 0.05, 4 ); spheres = new THREE.InstancedMesh( geometrySphere, material, 400 ); spheres.instanceMatrix.setUsage( THREE.DynamicDrawUsage ); spheres.userData.physics = { mass: 1 }; scene.add( spheres ); // 一次遍历,装配整个场景 physics.addScene( scene ); // 每帧随机重置一个实例的位置,观察其重新坠落 setInterval( () => { let index = Math.floor( Math.random() * boxes.count ); position.set( 0, Math.random() + 1, 0 ); physics.setMeshPosition( boxes, position, index ); }, 1000 / 60 );这个示例把组件的三个要点全部覆盖了:userData.physics声明式装配、InstancedMesh批量刚体、setMeshPosition( mesh, position, index )按实例 ID 重置位置。DynamicDrawUsage的提示也很重要——实例矩阵每帧都会被物理结果改写,应告知 GPU 该缓冲是高频写入的。
实战示例二:封装之外的底层 Ammo.js 用法
examples/physics_ammo_break.html 演示的是“凸物体实时破碎”:页面用ConvexObjectBreaker把塔楼、桥梁、山体拆成凸碎片,当接触冲量超过fractureImpulse = 250时对受冲击的凸体递归细分。它没有使用AmmoPhysics封装,而是直接操作 Ammo API,是理解封装内部机制、也是突破封装限制(如仅支持 Box/Sphere 形状)的参考实现:
- 手动构建世界:
btDefaultCollisionConfiguration→btCollisionDispatcher→btDbvtBroadphase→btSequentialImpulseConstraintSolver→btDiscreteDynamicsWorld,与封装内完全一致; - 任意凸体形状:
btConvexHullShape逐点addPoint(),这是addMesh所不支持的; - 接触点与冲量读取:通过
dispatcher.getNumManifolds()/getManifoldByIndexInternal( i )遍历接触流形,再用contactPoint.getAppliedImpulse()判断是否触发破碎; - 用
body.setUserPointer()把 three.js 对象挂到刚体上,实现物理回调中反查渲染对象。
如果你的需求(凹面碰撞、凸包刚体、接触事件回调、约束关节等)超出AmmoPhysics三个方法的覆盖范围,这个示例展示了在同一套 Ammo API 下自行搭建设计的路径。同一目录下还有physics_ammo_cloth、physics_ammo_rope、physics_ammo_terrain、physics_ammo_volume等示例,分别覆盖布料、绳索、地形、体积雾等场景。
限制与注意事项
结合文档声明与源码实现,使用该组件前需要明确以下边界:
- 强依赖网络:默认从 CDN 拉取固定 commit 的
ammo.wasm.js,离线环境会加载失败。规避方式是页面预先加载并暴露全局Ammo,组件检测到typeof Ammo === 'undefined'为假时会跳过注入逻辑。 - 几何体支持有限:
addMesh/addScene仅支持BoxGeometry与SphereGeometry/IcosahedronGeometry,且只读取geometry.parameters;不满足条件的几何体仅打印错误日志而不生效。 - 物理参数不可调:重力
(-9.8)、帧率 60、形状 margin 0.05、每帧最多 10 个固定步均写死在源码中;摩擦系数行body.setFriction( 4 )处于注释状态,即未显式设置摩擦。 setMeshPosition会清零速度:用它重置位置时,刚体不会保留原来的运动状态,而是从静止开始,这一点在需要“接住再抛出”的交互设计时要注意。- 无销毁接口:组件返回对象只有三个方法,模拟由
setInterval常驻驱动,页面卸载前需要自行管理InstancedMesh等资源的释放,可参考官方手册 manual/pages/how-to-dispose-of-objects.html 的释放流程。 - 引擎维护状态:如 manual/pages/physics.html 所述,Ammo.js 本身已不再活跃维护;新项目若需要长期演进,可对比同目录的 examples/jsm/physics/RapierPhysics.js 与 examples/jsm/physics/JoltPhysics.js。
参考文件
- 组件文档:docs/pages/AmmoPhysics.html.md
- 组件源码:examples/jsm/physics/AmmoPhysics.js
- 实例化示例:examples/physics_ammo_instancing.html
- 凸体破碎示例(底层用法):examples/physics_ammo_break.html
- 物理引擎选型说明:manual/pages/physics.html
userData字段说明:docs/pages/Object3D.html
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考