news 2026/9/7 6:28:44

three.js ConvexObjectBreaker 详解:凸体对象实时破碎的 API 原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
three.js ConvexObjectBreaker 详解:凸体对象实时破碎的 API 原理与实战

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,系统讲解prepareBreakableObjectsubdivideByImpactcutByPlane三大核心方法的工作流程、参数细节,并结合官方示例 physics_ammo_break.html 给出从"准备可破碎物体"到"碰撞触发碎块生成"的完整接入方案,读完后可独立在项目中实现凸体对象的分形破碎。

一、它解决什么问题:凸体对象的递归切割

在三维物理场景中,"破碎"效果需要三步能力:几何上把模型切成两块、把两块继续递归切小、为每个碎块保留质量/速度等物理信息。ConvexObjectBreaker 把前两步封装为一个类,第三步所需的物理量则以元数据形式挂在对象的userData上。

类头部的 JSDoc 注释(examples/jsm/misc/ConvexObjectBreaker.js)给出了完整使用约定:

  • 先用prepareBreakableObject把一个 Mesh 注册为"可破碎对象";
  • 之后调用subdivideByImpact(按撞击点分裂)或cutByPlane(按平面切割)做实际分裂;
  • 分裂产生的子对象无需再次调用prepareBreakableObject——这一点很重要,源码中cutByPlane在生成碎块时会自动为新碎块调用prepareBreakableObject(见下文第四节)。

文档同时明确了三条对象前提约束:

  1. Mesh 必须拥有 BufferGeometry 和一个 material(material 会被传播给所有子碎块);
  2. 顶点法线必须是平面的(planar, not smoothed)——即同一面内的三个顶点共享同一个法线。这是算法的核心假设,后文会看到源码直接依赖这一假设;
  3. 几何体必须是凸的(库不做凸性校验)。可用 ConvexGeometry(底层基于 examples/jsm/math/ConvexHull.js 的凸包算法,平均时间复杂度 O(n log n))构造凸几何,BoxGeometrySphereGeometry等凸图元也可直接使用。

还有一条文档特别强调的注意事项:该类会向对象的userData中写入massvelocityangularVelocitybreakable等成员变量,与其他会操作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 > deltad < -delta时才判定顶点严格位于平面正侧或负侧,落在 ±delta 带内的顶点被标记为 coplanar(标记 3),同时推入两侧的顶点集合(examples/jsm/misc/ConvexObjectBreaker.js#L311-L358),从而保证切割面两侧的碎块共享切割缝上的顶点,几何上严丝合缝;
  • 同一 delta 还用于共面三角面判定1 - n0.dot(n1) < delta,用以标记被两个共面面共享的边,避免一条边被处理两次(见下文第三节的 segments 机制)。

构造函数另外预分配了一批临时对象(tempPlane1/2tempLine1、若干Vector3以及一个numPoints × numPointssegments布尔标记池),供切割时复用以减少 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; }

五个参数的角色:

参数类型说明
objectObject3D待破碎的 Mesh,几何体必须凸
massnumber质量(kg),必须大于 0
velocityVector3线速度
angularVelocityVector3角速度
breakableboolean是否可破碎

注意velocity/angularVelocity会被clone存储,而cutByPlane生成碎块时是直接引用父对象的这两个向量(不 clone),所以物理引擎每帧更新刚体速度后,碎块继承到的就是最新速度——这是与物理库衔接时的关键行为。

3.2 subdivideByImpact(object, pointOfImpact, normal, maxRadialIterations, maxRandomIterations)

按"撞击"语义把对象切成多块:想象另一物体以法线normal撞击表面点pointOfImpact,碎片沿撞击点周围呈放射状分布。参数含义(见 方法实现):

  • pointOfImpact:撞击点(世界空间);
  • normal:撞击法线;
  • maxRadialIterations:径向切割的最大迭代数——围绕撞击点、沿法线轴旋转的"扇形切刀"次数;
  • maxRandomIterations:非径向随机切割的最大迭代数。

算法流程(递归扇形切割)

  1. 先用撞击点、对象位置、pointOfImpact + normal三点确定初始切割平面tempPlane1
  2. 内部递归函数subdivideRadial(subObject, startAngle, endAngle, numIterations)[startAngle, endAngle]角度区间内随机取一个角度angle0.2 + 0.6 × Math.random()的插值),绕法线轴旋转得到新的切割平面,然后调用cutByPlane切出两块,对每一块带着更新后的角度区间进入下一轮递归;
  3. 递归的终止条件(碎片直接收入debris数组、不再切)有两个:
    • 概率性停止Math.random() < numIterations * 0.05——迭代越深,越早停止的概率越大,使碎块大小呈现自然的随机分布;
    • 深度上限numIterations > maxRadialIterations + maxRandomIterations
  4. 最终返回碎块数组。

一个值得注意的实现细节:迭代次数≤ maxRadialIterations时,切割平面绕撞击点旋转(真正的"放射"切割);超过该次数后,切割平面改为绕当前子对象的位置旋转,且角度公式切换到基于(numIterations & 1)与随机数的组合——从源码结构看,这是为了让深层碎片在子对象自身坐标系内继续随机细分,避免放射刀全部汇聚到原撞击点。

3.3 cutByPlane(object, plane, output) : number

最底层的二元切割:用世界空间的plane把对象切成两半。文档签名之外,源码(examples/jsm/misc/ConvexObjectBreaker.js#L189-L468)揭示了几个文档未展开的返回契约:

  • 结果写入output.object1output.object2两个成员;
  • 若平面未真正切割对象,object2nullobject1仅在内错时才会为null
  • 返回值为碎块数量(0/1/2),返回 0 表示内部错误(如"线段与平面不相交"这种理论上不会发生的分支,会console.error并置空输出)。

切割的完整步骤

  1. 共面边标记:双重遍历所有三角面对(i, j),若两面法线点积满足1 - n0.dot(n1) < smallDelta判为共面,再按顶点索引重叠找出共享边,把segments[a × numPoints + b](双向)置 true。这一步利用了"顶点法线平面化"的前提——面的三个顶点法线相同,取第一个顶点即可代表整面法线(examples/jsm/misc/ConvexObjectBreaker.js#L236-L284)。
  2. 平面变换到对象局部空间:几何坐标是局部空间的,而传入的切割平面是世界的,因此调用静态方法transformPlaneToLocalSpace(examples/jsm/misc/ConvexObjectBreaker.js#L523-L535)——取平面上的一个参考点做逆仿射变换、法线做转置逆变换,再重算平面常数。
  3. 逐边分类:对每条未处理的边,两端点按distanceToPoint与 ±delta 的比较标记为负侧(1)/正侧(2)/共面(3),共面顶点双侧各推一份;若端点异侧,用Plane.intersectLine求出交点,交点同时加入两侧——保证切割缝顶点共享。
  4. 碎块重建:当某侧顶点数> 4时,用new ConvexGeometry( points )直接由顶点集重建凸包网格,共享原material,位置设为顶点集近似质心(顶点算术平均,再平移到object.position),四元数继承父对象。源码注释坦承质量与质心都是"very fast and imprecise"的近似:质量恒取父对象的一半(userData.mass × 0.5),质心取顶点平均。
  5. 递归标记:新碎块立即调用prepareBreakableObject(object, newMass, 原velocity, 原文角velocity, 2 × radius > minSizeForBreak)——即第一节提到的"子对象无需手动 prepare"的实现所在,同时breakableminSizeForBreak阈值自动决定。

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,后续碰撞冲量再大也不会再切。

五、使用要点与工程建议

综合文档约束与源码实现,实际接入时有以下几点值得核对:

  • 几何必须凸且法线平面化BoxGeometrySphereGeometryConvexGeometry满足要求;对BufferGeometry使用平滑法线的模型会破坏共面边判定与法线假设,切割结果不可靠。库不校验凸性,非凸输入属于未定义行为。
  • 不要对可破碎对象施加非等比缩放:静态变换助手假设矩阵正交(无 scale)。
  • minSizeForBreak决定碎片粒度与性能上限:它同时是递归终止条件之一。破碎效果过碎时优先调大该值或减小maxRadialIterations/maxRandomIterations,而不是靠物理侧优化。
  • 与物理引擎配合时注意速度向量共享:碎块的userData.velocity引用父对象的同一Vector3实例,物理引擎侧每帧同步速度后碎块自动"看到"新速度;但若你手动改父对象速度,会影响所有未脱离共享的碎块。
  • userData键名占用:该类固定写入massvelocityangularVelocitybreakable四个键,与其他操作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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 6:22:51

XMLViewer:一站式解决XML格式化、树形查看与校验问题

简介&#xff1a;这是一款轻量级的 XML 查看器工具&#xff0c;主要面向开发人员、测试人员以及需要频繁检查 XML 数据格式的技术用户。安装后&#xff0c;用户可直接在系统右键菜单中调用 View 功能&#xff0c;快速打开 XML 文件并查看其层级与缩进结构&#xff0c;便于第一时…

作者头像 李华
网站建设 2026/9/7 6:19:45

读懂CANOpen源码:核心机制、协议栈选型与STM32移植实战

简介&#xff1a;CANOpen协议源码是基于CiA DS301规范的CAN高层通信协议实现&#xff0c;面向工业自动化、汽车电子、医疗设备等领域的嵌入式开发者&#xff0c;可用于在CAN网络上快速搭建对象字典、PDO、SDO、NMT、心跳、LSS与紧急报文等核心机制。压缩包共437个文件&#xff…

作者头像 李华