three.js ConvexObjectBreaker 详解:凸体对象实时破碎的 API 原理与实战
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
ConvexObjectBreaker 是 three.js 官方 examples/jsm 附加载具(Addon)中用于把凸多面体 Mesh 实时切割成碎块的几何分解类,典型场景是"炮弹击中石塔后碎裂飞溅"的物理破碎效果。本文基于 官方 API 文档 及其完整源码 examples/jsm/misc/ConvexObjectBreaker.js,系统讲解prepareBreakableObject、subdivideByImpact、cutByPlane三大核心方法的工作流程、参数细节,并结合官方示例 physics_ammo_break.html 给出从"准备可破碎物体"到"碰撞触发碎块生成"的完整接入方案,读完后可独立在项目中实现凸体对象的分形破碎。
一、它解决什么问题:凸体对象的递归切割
在三维物理场景中,"破碎"效果需要三步能力:几何上把模型切成两块、把两块继续递归切小、为每个碎块保留质量/速度等物理信息。ConvexObjectBreaker 把前两步封装为一个类,第三步所需的物理量则以元数据形式挂在对象的userData上。
类头部的 JSDoc 注释(examples/jsm/misc/ConvexObjectBreaker.js)给出了完整使用约定:
- 先用
prepareBreakableObject把一个 Mesh 注册为"可破碎对象"; - 之后调用
subdivideByImpact(按撞击点分裂)或cutByPlane(按平面切割)做实际分裂; - 分裂产生的子对象无需再次调用
prepareBreakableObject——这一点很重要,源码中cutByPlane在生成碎块时会自动为新碎块调用prepareBreakableObject(见下文第四节)。
文档同时明确了三条对象前提约束:
- Mesh 必须拥有 BufferGeometry 和一个 material(material 会被传播给所有子碎块);
- 顶点法线必须是平面的(planar, not smoothed)——即同一面内的三个顶点共享同一个法线。这是算法的核心假设,后文会看到源码直接依赖这一假设;
- 几何体必须是凸的(库不做凸性校验)。可用 ConvexGeometry(底层基于 examples/jsm/math/ConvexHull.js 的凸包算法,平均时间复杂度 O(n log n))构造凸几何,
BoxGeometry、SphereGeometry等凸图元也可直接使用。
还有一条文档特别强调的注意事项:该类会向对象的userData中写入mass、velocity、angularVelocity、breakable等成员变量,与其他会操作userData的库混用时需谨慎阅读代码,避免键名冲突。
二、导入与构造:两个参数的实际含义
ConvexObjectBreaker 属于 addon,必须显式导入(参考 Installation#Addons):
import { ConvexObjectBreaker } from 'three/addons/misc/ConvexObjectBreaker.js';new ConvexObjectBreaker( minSizeForBreak = 1.4, smallDelta = 0.0001 )
两个构造参数在 构造函数实现 中直接存入实例属性,各自控制一类数值行为:
minSizeForBreak(默认 1.4)——碎块继续破碎的最小尺寸阈值。源码中它只在一处生效:cutByPlane生成碎块时计算碎块外接"半径"radius,并用2 * radius > this.minSizeForBreak作为新碎块的breakable标志(examples/jsm/misc/ConvexObjectBreaker.js#L439-L461)。也就是说:当碎块的近似直径(2 × 到质心最远顶点的距离)小于该阈值时,它会被标记为"不可再碎",递归切割就此自然终止——这是控制碎块最终数量和整体粒度的关键参数。值越大碎块越少,值越小碎得越细(也更耗性能)。
smallDelta(默认 0.0001)——判定"点是否落在切割平面上"的最大距离容差。它有两处用途:
cutByPlane中对每个顶点计算localPlane.distanceToPoint(p),只有d > delta或d < -delta时才判定顶点严格位于平面正侧或负侧,落在 ±delta 带内的顶点被标记为 coplanar(标记 3),同时推入两侧的顶点集合(examples/jsm/misc/ConvexObjectBreaker.js#L311-L358),从而保证切割面两侧的碎块共享切割缝上的顶点,几何上严丝合缝;- 同一 delta 还用于共面三角面判定
1 - n0.dot(n1) < delta,用以标记被两个共面面共享的边,避免一条边被处理两次(见下文第三节的 segments 机制)。
构造函数另外预分配了一批临时对象(tempPlane1/2、tempLine1、若干Vector3以及一个numPoints × numPoints的segments布尔标记池),供切割时复用以减少 GC 压力——这也意味着一个 ConvexObjectBreaker 实例的segments池大小在构造时按30 × 30顶点上限初始化,超大模型需要留意。
三、核心 API 逐一解析
3.1 prepareBreakableObject(object, mass, velocity, angularVelocity, breakable)
文档要求:所有期望可破碎的对象都必须调用它。源码实现非常简洁(examples/jsm/misc/ConvexObjectBreaker.js#L76-L88):
prepareBreakableObject( object, mass, velocity, angularVelocity, breakable ) { const userData = object.userData; userData.mass = mass; // 质量(kg),必须 > 0 userData.velocity = velocity.clone(); userData.angularVelocity = angularVelocity.clone(); userData.breakable = breakable; }五个参数的角色:
| 参数 | 类型 | 说明 |
|---|---|---|
| object | Object3D | 待破碎的 Mesh,几何体必须凸 |
| mass | number | 质量(kg),必须大于 0 |
| velocity | Vector3 | 线速度 |
| angularVelocity | Vector3 | 角速度 |
| breakable | boolean | 是否可破碎 |
注意velocity/angularVelocity会被clone存储,而cutByPlane生成碎块时是直接引用父对象的这两个向量(不 clone),所以物理引擎每帧更新刚体速度后,碎块继承到的就是最新速度——这是与物理库衔接时的关键行为。
3.2 subdivideByImpact(object, pointOfImpact, normal, maxRadialIterations, maxRandomIterations)
按"撞击"语义把对象切成多块:想象另一物体以法线normal撞击表面点pointOfImpact,碎片沿撞击点周围呈放射状分布。参数含义(见 方法实现):
pointOfImpact:撞击点(世界空间);normal:撞击法线;maxRadialIterations:径向切割的最大迭代数——围绕撞击点、沿法线轴旋转的"扇形切刀"次数;maxRandomIterations:非径向随机切割的最大迭代数。
算法流程(递归扇形切割):
- 先用撞击点、对象位置、
pointOfImpact + normal三点确定初始切割平面tempPlane1; - 内部递归函数
subdivideRadial(subObject, startAngle, endAngle, numIterations)在[startAngle, endAngle]角度区间内随机取一个角度angle(0.2 + 0.6 × Math.random()的插值),绕法线轴旋转得到新的切割平面,然后调用cutByPlane切出两块,对每一块带着更新后的角度区间进入下一轮递归; - 递归的终止条件(碎片直接收入
debris数组、不再切)有两个:- 概率性停止:
Math.random() < numIterations * 0.05——迭代越深,越早停止的概率越大,使碎块大小呈现自然的随机分布; - 深度上限:
numIterations > maxRadialIterations + maxRandomIterations;
- 概率性停止:
- 最终返回碎块数组。
一个值得注意的实现细节:迭代次数≤ maxRadialIterations时,切割平面绕撞击点旋转(真正的"放射"切割);超过该次数后,切割平面改为绕当前子对象的位置旋转,且角度公式切换到基于(numIterations & 1)与随机数的组合——从源码结构看,这是为了让深层碎片在子对象自身坐标系内继续随机细分,避免放射刀全部汇聚到原撞击点。
3.3 cutByPlane(object, plane, output) : number
最底层的二元切割:用世界空间的plane把对象切成两半。文档签名之外,源码(examples/jsm/misc/ConvexObjectBreaker.js#L189-L468)揭示了几个文档未展开的返回契约:
- 结果写入
output.object1与output.object2两个成员; - 若平面未真正切割对象,
object2为null;object1仅在内错时才会为null; - 返回值为碎块数量(0/1/2),返回 0 表示内部错误(如"线段与平面不相交"这种理论上不会发生的分支,会
console.error并置空输出)。
切割的完整步骤:
- 共面边标记:双重遍历所有三角面对
(i, j),若两面法线点积满足1 - n0.dot(n1) < smallDelta判为共面,再按顶点索引重叠找出共享边,把segments[a × numPoints + b](双向)置 true。这一步利用了"顶点法线平面化"的前提——面的三个顶点法线相同,取第一个顶点即可代表整面法线(examples/jsm/misc/ConvexObjectBreaker.js#L236-L284)。 - 平面变换到对象局部空间:几何坐标是局部空间的,而传入的切割平面是世界的,因此调用静态方法
transformPlaneToLocalSpace(examples/jsm/misc/ConvexObjectBreaker.js#L523-L535)——取平面上的一个参考点做逆仿射变换、法线做转置逆变换,再重算平面常数。 - 逐边分类:对每条未处理的边,两端点按
distanceToPoint与 ±delta 的比较标记为负侧(1)/正侧(2)/共面(3),共面顶点双侧各推一份;若端点异侧,用Plane.intersectLine求出交点,交点同时加入两侧——保证切割缝顶点共享。 - 碎块重建:当某侧顶点数
> 4时,用new ConvexGeometry( points )直接由顶点集重建凸包网格,共享原material,位置设为顶点集近似质心(顶点算术平均,再平移到object.position),四元数继承父对象。源码注释坦承质量与质心都是"very fast and imprecise"的近似:质量恒取父对象的一半(userData.mass × 0.5),质心取顶点平均。 - 递归标记:新碎块立即调用
prepareBreakableObject(object, newMass, 原velocity, 原文角velocity, 2 × radius > minSizeForBreak)——即第一节提到的"子对象无需手动 prepare"的实现所在,同时breakable由minSizeForBreak阈值自动决定。
cutByPlane的三个静态向量变换助手(transformFreeVector/transformFreeVectorInverse/transformTiedVectorInverse)要求输入矩阵为正交矩阵(不含缩放),这是使用时的隐含前提:不要对可破碎对象施加非等比缩放。
四、官方示例:与 Ammo 物理引擎联动的实时破碎
官方示例 examples/physics_ammo_break.html 演示了"鼠标发射球体击中塔楼/石桥/石块/山体,对象实时碎裂"的完整链路,是理解三个 API 如何协作的最佳范本。核心接入点如下:
1. 准备可破碎对象(塔楼、石桥、石块用 BoxGeometry,山体用 5 个顶点构造的 ConvexGeometry):
const convexBreaker = new ConvexObjectBreaker(); // 使用默认 minSizeForBreak / smallDelta const object = new THREE.Mesh( new THREE.BoxGeometry( halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2 ), material ); object.position.copy( pos ); object.quaternion.copy( quat ); // 注册为可破碎对象:质量 1000 kg,初速度/角速度为零,允许破碎 convexBreaker.prepareBreakableObject( object, mass, new THREE.Vector3(), new THREE.Vector3(), true );2. 为碎块创建物理刚体:示例用btConvexHullShape从碎块顶点坐标建凸包碰撞体(createConvexHullPhysicsShape),并通过body.setUserPointer( btVecUserData )(其中btVecUserData.threeObject = object)把 three.js 对象回指到刚体上,以便碰撞回调时找回原始 Mesh。
3. 碰撞检测中触发破碎:每帧遍历dispatcher的 contact manifold,取施加冲量最大的接触点,若冲量超过破碎阈值fractureImpulse = 250且对象breakable && !collided,则调用:
const debris = convexBreaker.subdivideByImpact( threeObject, // 被击中的可破碎 Mesh impactPoint, // 接触点世界坐标 impactNormal, // 接触法线 1, 1 // 1 次径向迭代 + 1 次随机迭代(碎片较少、性能友好) ); for ( const fragment of debris ) { const vel = rigidBody.getLinearVelocity(); const angVel = rigidBody.getAngularVelocity(); // 碎块自动继承了父级速度向量引用,这里覆盖为刚体当前速度 fragment.userData.velocity.set( vel.x(), vel.y(), vel.z() ); fragment.userData.angularVelocity.set( angVel.x(), angVel.y(), angVel.z() ); createDebrisFromBreakableObject( fragment ); // 为碎块建物理刚体并加入世界 } objectsToRemove[ numObjectsToRemove ++ ] = threeObject; // 帧末移除原对象4. 防止同帧重复破碎:示例给每个对象维护userData.collided标志,破碎后置 true,并在updatePhysics开头清零——因为碎块刚进入物理世界就可能与撞击球再次接触,不加保护会同一帧连续碎裂。
这个示例同时展示了破碎链的自终止机制:subdivideByImpact产生的碎片若小于minSizeForBreak,其userData.breakable即为 false,后续碰撞冲量再大也不会再切。
五、使用要点与工程建议
综合文档约束与源码实现,实际接入时有以下几点值得核对:
- 几何必须凸且法线平面化。
BoxGeometry、SphereGeometry、ConvexGeometry满足要求;对BufferGeometry使用平滑法线的模型会破坏共面边判定与法线假设,切割结果不可靠。库不校验凸性,非凸输入属于未定义行为。 - 不要对可破碎对象施加非等比缩放:静态变换助手假设矩阵正交(无 scale)。
minSizeForBreak决定碎片粒度与性能上限:它同时是递归终止条件之一。破碎效果过碎时优先调大该值或减小maxRadialIterations/maxRandomIterations,而不是靠物理侧优化。- 与物理引擎配合时注意速度向量共享:碎块的
userData.velocity引用父对象的同一Vector3实例,物理引擎侧每帧同步速度后碎块自动"看到"新速度;但若你手动改父对象速度,会影响所有未脱离共享的碎块。 userData键名占用:该类固定写入mass、velocity、angularVelocity、breakable四个键,与其他操作userData的库(如物理引擎写physicsBody)共存时避免冲突。- 切割平面是局部/世界空间敏感点:
cutByPlane内部已把平面变换到对象局部空间,调用方只需传入世界空间Plane;但要求对象矩阵已更新(内部会调用object.updateMatrix())。
六、小结与相关资源
ConvexObjectBreaker 用"共面边标记 + 平面双侧顶点分类 + ConvexGeometry 凸包重建"三步完成凸体二切,再以概率化的递归扇形切割叠出自然碎块分布,质量与尺寸阈值提供递归自终止。它与 any 刚体物理引擎的组合模式(注册 → 碰撞冲量判定 → 碎块继承速度 → 重建碰撞体)是 three.js 生态中实现破坏效果的标准范式。
进一步阅读:
- API 原文:docs/pages/ConvexObjectBreaker.html.md
- 完整源码:examples/jsm/misc/ConvexObjectBreaker.js
- 凸几何构造:docs/pages/ConvexGeometry.html.md、examples/jsm/geometries/ConvexGeometry.js、凸包核心 examples/jsm/math/ConvexHull.js
- 物理破碎示例:examples/physics_ammo_break.html(运行效果见 examples/screenshots/physics_ammo_break.jpg)
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考