three.js AmmoPhysics 组件:用 Ammo.js 物理引擎为 three.js 应用添加刚体模拟
2026/9/7 19:22:54 网站建设 项目流程

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 物理封装的JoltPhysicsRapierPhysics并列存放在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()内部的加载逻辑是:

  1. 检查全局Ammo是否已存在(例如页面已经手动引入过ammo.wasm.js脚本,则可跳过这一步);
  2. 若不存在,则动态创建<script>标签注入document.headonload时 resolve、onerror时 reject;
  3. 加载完成后执行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()最终返回一个包含三个方法的对象:addSceneaddMeshsetMeshPosition。下面逐一说明。

API 详解

.addMesh( mesh, mass, restitution )

把单个网格加入物理模拟。

参数类型默认值说明
meshMesh必填要加入模拟的网格
massnumber0质量(单位:kg)。传0表示静态刚体(不随模拟运动,但仍参与碰撞)
restitutionnumber0恢复系数,通常取 0~1,表示碰撞时物体有多“弹”

源码实现(handleMesh,L125-L155)揭示了两个关键行为:

  • mass 决定刚体类型:只有mass > 0的网格才会被推进内部meshes更新列表并建立mesh → bodyWeakMap映射;mass === 0的刚体虽然也加入世界,但属于静态体,不参与每帧的位置回写。
  • 惯性自动计算:组件调用shape.calculateLocalInertia( mass, localInertia )由碰撞形状自动计算转动惯量,无需手动提供。

同时,addMesh对几何体类型有硬性限制。内部getShape()(源码 L48-L80)只支持两种映射:

three.js 几何体Bullet 碰撞形状尺寸取值
BoxGeometrybtBoxShapewidth / height / depth的一半(未提供时各半轴默认 0.5)
SphereGeometry/IcosahedronGeometrybtSphereShapeparameters.radius(未提供时默认 1)

两种形状都会setMargin( 0.05 )给碰撞形状加上 0.05 的外扩余量。传入其他几何体(如CylinderGeometryPlaneGeometry、加载出的 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()遍历所有子节点,仅处理isMeshuserData.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是网格与物理世界的“契约”,可以携带massrestitution两个字段:

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)。

参数类型默认值说明
meshMesh必填要更新位置的网格
positionVector3必填新位置
indexnumber0若网格是InstancedMeshindex表示实例 ID;普通Mesh忽略该参数

index参数正是该组件支持实例化物理的体现。

内部机制:运动状态如何回写到渲染网格

每帧的step()函数(源码 L230-L288)是数据同步的核心,流程为:

  1. world.stepSimulation( delta, 10 )推进物理世界;
  2. 遍历mass > 0的网格,从对应btRigidBodybtDefaultMotionState中取回世界变换;
  3. 普通Mesh:直接mesh.position.set(...)/mesh.quaternion.set(...)回写;
  4. InstancedMesh:调用文件末尾的compose()工具函数(源码 L336-L364),把四元数与位置手工展开为列主序 4x4 矩阵,直接写入instanceMatrix.array对应实例的 16 个元素,然后设置mesh.instanceMatrix.needsUpdate = truecomputeBoundingSphere()

InstancedMesh的建立过程也值得一看(handleInstancedMesh,L157-L193):它读取mesh.instanceMatrix.array,每 16 个元素用btTransform.setFromOpenGLMatrix()取出一个实例的初始变换,为每个实例创建独立的刚体,并把“一个 Mesh 对应一个 body 数组”存入meshMap。这就是setMeshPositionindex参数的用武之地——它选择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 形状)的参考实现:

  • 手动构建世界:btDefaultCollisionConfigurationbtCollisionDispatcherbtDbvtBroadphasebtSequentialImpulseConstraintSolverbtDiscreteDynamicsWorld,与封装内完全一致;
  • 任意凸体形状:btConvexHullShape逐点addPoint(),这是addMesh所不支持的;
  • 接触点与冲量读取:通过dispatcher.getNumManifolds()/getManifoldByIndexInternal( i )遍历接触流形,再用contactPoint.getAppliedImpulse()判断是否触发破碎;
  • body.setUserPointer()把 three.js 对象挂到刚体上,实现物理回调中反查渲染对象。

如果你的需求(凹面碰撞、凸包刚体、接触事件回调、约束关节等)超出AmmoPhysics三个方法的覆盖范围,这个示例展示了在同一套 Ammo API 下自行搭建设计的路径。同一目录下还有physics_ammo_clothphysics_ammo_ropephysics_ammo_terrainphysics_ammo_volume等示例,分别覆盖布料、绳索、地形、体积雾等场景。

限制与注意事项

结合文档声明与源码实现,使用该组件前需要明确以下边界:

  1. 强依赖网络:默认从 CDN 拉取固定 commit 的ammo.wasm.js,离线环境会加载失败。规避方式是页面预先加载并暴露全局Ammo,组件检测到typeof Ammo === 'undefined'为假时会跳过注入逻辑。
  2. 几何体支持有限addMesh/addScene仅支持BoxGeometrySphereGeometry/IcosahedronGeometry,且只读取geometry.parameters;不满足条件的几何体仅打印错误日志而不生效。
  3. 物理参数不可调:重力(-9.8)、帧率 60、形状 margin 0.05、每帧最多 10 个固定步均写死在源码中;摩擦系数行body.setFriction( 4 )处于注释状态,即未显式设置摩擦。
  4. setMeshPosition会清零速度:用它重置位置时,刚体不会保留原来的运动状态,而是从静止开始,这一点在需要“接住再抛出”的交互设计时要注意。
  5. 无销毁接口:组件返回对象只有三个方法,模拟由setInterval常驻驱动,页面卸载前需要自行管理InstancedMesh等资源的释放,可参考官方手册 manual/pages/how-to-dispose-of-objects.html 的释放流程。
  6. 引擎维护状态:如 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询