我最早在 Unity 里做破碎效果时第一反应是去商店找插件装上、拖一下、运行看起来挺唬人。真到项目里才发现问题一堆URP 下 Shader 不支持、移动端碎片一多直接卡成 PPT、MeshCollider 在非凸网格上报错、碎片复活动画生硬。后来我花了一周时间把切片的底层逻辑啃了一遍才意识到破碎效果从来不是把模型拆开这么简单它背后是 Mesh 数据结构、几何切割、物理碰撞和渲染性能四条线的交叉问题。这篇我就把实现模型破碎效果的关键链路拆开讲从 Mesh 基础到 Voronoi 预切片再到物理表现和优化给出一套可以直接落地的思路适合想绕开插件、把破碎效果做得可控的 Unity 开发者参考。1. 破碎效果的本质切片、冲量与层级感缺一不可1.1 一个看起来简单的需求底层要处理三件事很多人第一次做破碎效果脑子里浮现的画面是物体受到攻击然后啪地变成一堆零件飞出去。这个画面拆开来看其实涉及三件完全不同的事几何上要把一个完整网格切成多个独立网格物理上要让这些网格各自拥有刚体和碰撞体并产生飞散运动视觉上还要让裂痕、剖面和飞散速度看起来符合直觉。这三件事不是顺序执行一次就完了。比如几何切完碎片网格如果直接丢给物理引擎性能可能很难看物理算完碎片如果长时间在地面滑动又会暴露碰撞体和地面摩擦参数的调校问题视觉上裂缝必须提前烘焙或者用 Shader 处理不然后切出来的剖面就是一片单一颜色一眼假。所以做破碎效果本质上是在做一次模型级的数据重构。之前一个 GameObject、一个 Mesh之后是几十个 GameObject、几十个 Mesh每个 Mesh 还要保证顶点、法线、UV、三角形索引都正确才能被物理引擎和渲染管线正常使用。1.2 为什么炸成很多块不等于破碎有朋友会问我提前准备好几个碎块模型运行时直接替换显示效果不是一样吗这种思路在美术资源量允许的情况下确实可行但很容易看出破绽。人工拆分的碎块裂缝是模型师画的碎块之间的贴合精度取决于手工建模一旦碎块大小、形状太规律玩家一眼就觉得这是动画不是实时破碎。更关键的问题是真实破碎的碎片形状是不规则的。自然界里石头砸碎、玻璃碎裂、混凝土崩落断面都是随机多边形拼合的结果碎块之间的边界不是简单的一刀切而是呈现出类似 Voronoi 图的多边形结构。Voronoi 听上去很高端本质却很好理解给空间里撒一批种子点每个区域归离它最近的那个种子点管区域边界就是相邻种子点的垂直平分面。撒点不规则分出来的区域自然也不规则这正好就是碎裂碎块的几何基础。如果把模型预先沿着这些区域边界切割再把每个区域单独导出成一个网格运行时根据需要激活对应的碎片对象破碎效果就能做到每一次看起来都不同同时又不会在关键时刻产生大量实时计算。1.3 破碎效果的分层设计视觉层、物理层、反馈层我习惯把破碎效果拆成三个层去设计。视觉层负责的是破之前、破的一瞬间、破之后三个阶段。破之前模型完好破的一瞬间需要有速度感碎块向外飞散裂缝处最好能看到剖面材质或者裂纹贴图破之后碎块逐渐停止、堆叠在地面。视觉层的核心是把时间的流逝翻译成可观察的变化。物理层负责的是碰撞和运动。每个碎块要能落地、能堆叠、能受力推动这需要刚体、碰撞体、质量参数和阻尼配合。物理层最容易被低估因为碎片一旦超过几十个Unity 物理引擎的压力会直线上升。反馈层是很多人忽略的。破碎的一瞬间要不要震屏要不要停顿几帧让玩家看清碎片飞散方向要不要给攻击者一个短暂的慢动作这些反馈设计决定了破碎效果是有冲击力还是只是撒了一地零件。反馈层不是特效而是节奏控制它直接影响玩家对打击感的判断。这三层各管各的但在实现里又互相牵扯。比如物理层碎片多了视觉层的相机就容易被碎片遮挡反馈层想慢动作物理层的 Time.timeScale 又会影响刚体模拟。所以设计时要先定清楚优先级我自己一般是视觉表现优先物理效果够用就好这样优化空间会大很多。2. 方案选型预计算切片与运行时切割差的不只是性能2.1 三条技术路线对比Unity 里做破碎主流路线大致有三条美术手工预拆、运行时实时切割、预计算切片加运行时激活。三条路线我不是只比性能还会看接入成本、可控性和最终效果上限。方案制作流程运行时开销碎片随机性典型适用场景美术手工预拆模型师提前做多套破碎状态低只是切换显示随机性弱依赖美术量过场动画、固定演出运行时实时切割运行时用平面/刀具切割网格高切割瞬间计算量大随机性强切割位置灵活可破坏墙面、切水果类玩法预计算切片启动时或离线切好碎片网格较低运行只做激活和物理随机性强碎片模型完全可控大多数实时战斗破碎运行时切割听起来很酷因为玩家可以对着任意位置砍下去网格当场切开。但代价是每切一刀都要重新计算网格拓扑还要即时生成剖面封口和碰撞体。资源管理稍不注意就会出现切出来的碎片没有正确法线、碰撞体穿模、内存碎片暴涨这些问题。移动端尤其明显一次切割如果造成几百个三角形重建帧率会瞬间掉下去。预计算切片则是把计算量前置。启动阶段或者编辑器扩展里就把模型切成几十个碎块保存成 Asset运行时直接加载。这样切割算法再复杂也不影响主线程关键时刻只要把碎片对象 SetActive(true) 并加上初速度就行。风险在于预先切好的碎片不能响应任意切割位置切点位置在设计破碎时就定死了。2.2 预计算切片的适用范围与前提预计算切片最适合的场景是爆炸物、陶罐、雕像、墙体这类对破碎位置有明确预期的对象。比如玩家攻击一个陶罐陶罐碎了碎块形状已经提前切好玩家不会在意碎裂位置是否正好命中攻击点只要碎片飞散方向和攻击方向一致就行。做预计算切片之前我得确认两个前提。第一个是原模型不能太复杂。我说复杂不是指顶点数而是指拓扑。如果一个模型内部有倒角、空洞、多层嵌套切割出来的碎片很容易出现非流形网格这种网格丢给 MeshCollider 会直接报错。第二个是模型最好封闭。封闭模型切割后能自然生成剖面封口半开放模型比如一面薄墙切完还需要手动处理背面否则从侧面看会看到空洞。2.3 运行时切割的典型诉求如果你的玩法真的需要切哪里碎哪里比如墙面可以被子弹打出一个洞、绳子能被刀砍断那只能选运行时切割。运行时切割对数据结构的依赖比预计算更强通常要把网格转成半边结构或者至少是标准的顶点索引结构才能高效地处理三角形相交。这里我想给一个实用建议就算最后选择了运行时切割也建议先用预计算切片把核心对象做出来跑通一遍破碎的整体流程。因为两种方案在碎片生成之后的物理、渲染、回收逻辑几乎完全一样先做预计算能让你把最容易出问题的部分稳定下来再回头做实时切割时只需要替换碎片来源模块这一类逻辑。2.4 我的选择预计算加按需激活在多数项目里我实际选的是预计算切片运行时按需激活的折中路线。离线或者启动阶段先对模型做 Voronoi 切割生成碎片 Asset 列表运行时根据攻击方向、伤害类型决定要激活多少碎片以及对哪些碎片施加冲量。这个方案的好处有三点。第一切割过程不在战斗关键帧上发生帧率稳定第二碎片网格可以在切割时就把 UV、法线、剖面封口都处理干净渲染正确率高第三因为碎块是预先存在的对象可以做对象池减少运行时实例化开销。接下来我讲的实现也主要围绕这条路线展开。3. 动手前必须吃透的 Mesh 基础顶点、三角形与切割面3.1 网格的存储方式与最小单元Unity 的 Mesh 本质上就是一个顶点数组加一个三角形索引数组。顶点数组里存了 position、normal、uv 等数据三角形索引数组则是三个一组表示一个三角形。渲染时 GPU 会按照索引去取顶点然后把三个顶点连成一个三角形。我把三角形当成整个网格的最小操作单元。切割一个网格实际就是在处理三角形和切割平面的相交关系。一个平面把一个三角形切过去结果只有三种三角形整体在平面一侧、三角形整体在另一侧、三角形被平面切成两部分。前两种好处理直接把三角形放进对应的碎片列表就行。第三种才是关键它要求我计算三角形三条边和平面相交出来的交点然后用交点把原三角形拆分成两个甚至三个新三角形。这个过程中有个反直觉的点顶点数并没有变多变多的是三角形索引。比如一个三角形被平面切成一个四边形加一个三角形四边形还要再拆成两个三角形最终三角形数量会从 1 变成 3。如果原模型有上万三角形切割一次可能增加几千个三角形所以切割算法要尽量保证不产生重复顶点否则内存和渲染开销都会失控。3.2 切割面的生成原理从一个平面到一堆碎片实现一条切割线要解决两个数学问题。第一个是判断顶点在平面哪一侧第二个是求线段和平面的交点。判断顶点在平面哪一侧用的是平面方程。一个平面可以用一个法线向量 n 和一个距离 d 表示顶点 p 到平面的带符号距离就是 dot(n, p) d。结果大于 0 在平面一侧小于 0 在另一侧等于 0 就在平面上。Unity 的 Plane 结构体已经封装了 GetSide 和 DistanceToPoint 方法直接用就行。求交点相对麻烦一点。线段 AB 与平面相交交点 t 可以用公式 t -dot(n, A) - d / dot(n, B - A) 算出来前提是分母不为 0也就是线段不能和平面平行。算出 t 之后交点坐标就是 A t * (B - A)。这里我要强调一个细节交点的法线、UV 不能直接取 A 或 B 的值要做线性插值。否则切割出来的剖面边缘会出现 UV 拉伸和法线断裂。把所有三角形按照上述规则切完再把同属一个区域的三角形收集起来就得到了一个碎片网格的雏形。这个雏形还缺剖面封口也就是切割面本身那一层新露出来的面没有它模型会漏水。3.3 法线、UV 和顶点的重算细节很多教程只教你怎么切三角形不讲法线和 UV结果一运行发现碎片侧面和剖面黑一块亮一块或者贴图在裂口处拉丝。我在这里把细节补完整。法线方面切割产生的新顶点法线要按插值算。对于原表面上的新顶点法线应该沿着原三角形的法线做插值对于剖面封口上的顶点法线直接取切割平面的法线方向就行。但剖面往往不是一个平面而是多个多边形拼接严格做法是逐三角形用叉积计算法线再按顶点相邻三角形面积加权平均。工程上我建议先按三角形叉积算出三角形法线然后叠加到顶点上最后归一化。UV 方面原表面新顶点的 UV 需要沿用顶点所在边的 UV 插值不能随机给一个值。剖面封口的 UV 则可以映射到平面局部坐标系常见做法是取平面上的两个正交向量作为 U 轴和 V 轴把顶点投影到这个坐标系里得到 UV。这样剖面贴裂痕贴图时纹理方向是稳定的。顶点去重也是不可跳过的一步。同一位置可能被多个三角形引用如果不做焊接网格会出现看起来是封闭的但实际上顶点没有共享的情况。这种网格在物理碰撞检测时容易出问题。我一般用一个以位置为 key 的字典来合并顶点位置差值小于某个阈值就认为是同一点合并后还要重建三角形索引。4. 实现一套 Voronoi 预切片从随机种子到碎片网格4.1 生成 Voronoi 单元简化的 CPU 方案要做出自然的破碎第一步是生成 Voronoi 单元。3D Voronoi 有专门的库和算法但我们在 Unity 里做一个简化版完全够用先在一个模型包围盒内随机撒 N 个种子点再对模型空间做最近点归属每个种子点管辖一个单元。撒种子的数量决定了碎块数量。碎块太多物理扛不住太少破碎效果不明显。我的经验是一个中等尺寸物体控制在 15 到 40 块之间具体看物体大小和玩家视角距离。种子点之间要有最小距离限制否则两个种子离太近生成的小碎片可能只有几个三角形既不美观又浪费性能。实现上就是循环撒点新点离已有任意种子太近就重撒。拿到种子点集合后要生成每个 Voronoi 单元的多边形区域。3D 情况比较复杂工程上常用的技巧是从一个大包围盒开始对这个包围盒用所有其他种子点的垂直平分面依次做切割最后剩下的凸多面体就是一个 Voronoi 单元。这一步计算量不小但做预计算时可以放到编辑器扩展里玩家运行时不需要感知。这里我贴一段能表达核心思路的伪代码// 用一个凸多面体表示 cell初始为包围盒 ConvexVolume cell GetBoundingBoxVolume(bounds); for (int i 0; i seeds.Count; i) { if (seeds[i] currentSeed) continue; Plane cutPlane GetBisectorPlane(currentSeed, seeds[i]); cell CutVolume(cell, cutPlane); }GetBisectorPlane 做的就是把两个种子点的中垂面作为切割面。CutVolume 则是用一个平面对凸多面体的每个面进行裁剪。这套逻辑跑通后每个 cell 都是一个凸多面体随后就可以拿 cell 的面对原模型做精确切割。4.2 用切割平面切出每一个碎片的网格Voronoi 单元只是空间区域真正要得到碎片网格还得用单元的表面去切原模型。这一步我在实现里是逐 cell 处理的遍历所有三角形判断三角形是在 cell 内部还是外部跨越边界的三角形要和 cell 的每个面做切割。由于 cell 是凸多面体判断一个点是否在 cell 内部很简单对 cell 的每个面点都必须位于面内侧。若三角形三个顶点都在内部整个三角形归入这个 cell若部分顶点在外面就用面逐个裁剪得到内部的子三角形。这里要特别强调一个三角形可能同时属于多个 cell 吗不会被裁剪的话不会但跨越多个 cell 边界时会被切到多个 cell 中这是正常的因为相邻碎片是共边的。这套逻辑真正难调的是边界稳定性。点在平面上的误差判断、浮点精度、共面三角形都会导致同一顶点在不同 cell 里位置略有偏移最终碎片间出现微小缝隙或重叠。规避方法是切割前先对模型做一次顶点统一化处理切割过程中所有平面都用 double 精度计算生成最后的 float 顶点之前再统一转换。4.3 生成碎片内部的剖面封口不生成剖面封口的话一个花瓶碎了每个碎片靠近断裂面那一侧都是空的从镜头里能看到内部是黑洞。生成封口的第一步是收集 cell 表面上落在模型内部的边界多边形。这些边界多边形是从原模型三角形和 cell 面相交产生的线段组织出来的。收集到边界线段后要把它们连成闭合多边形。这是个典型的边拼接问题两条边共用一个端点就拼起来最终得到一个或多个多边形环。环可能不是严格平面但由于 cell 的面是凸的边界环基本趋近于平面。随后把这个环三角化。最稳妥的办法是投影到 cell 面的局部坐标系里用耳切法做 2D 三角剖分再映射回 3D。这部分如果自己实现工作量不小。好在非常多优秀的开源实现都把这套逻辑封装好了项目中我推荐直接去读成熟插件的源码重点看它如何处理边界环的缝合和顶点去重比自己从零造轮子效率高得多。4.4 碎片编号与邻接关系记录预计算切片不只是一个几何问题数据管理同样要紧。每个碎片生成后我会给它一个编号并记录两个信息它的种子点位置以及和它相邻的碎片列表。种子点位置有什么用运行时施加爆炸力时碎片初始速度的方向可以直接用碎片自身种子点位置减去模型中心得到这样所有碎片是沿径向飞出的效果很自然。邻接关系有用在更进阶的玩法上当前碎片被击碎时可以按邻接关系决定哪些邻居碎片产生松动效果做连锁坍塌。这条数据在编辑器扩展里最好直接写进脚本对象的字段运行时加载后直接序列化好不要现场分析网格去算邻接关系那又会拖慢首帧。5. 碎片的物理与表现碰撞体、刚体、复活机制5.1 动态生成碎片的 GameObject 配置切片算法输出的是一个个 Mesh接下来要把它转成场景里可交互的碎片。我给每个碎片创建一个 GameObject挂上 MeshFilter、MeshRenderer 和刚体组件并按需要添加碰撞体。这里要注意碎片 Mesh 在运行时创建后不能直接不管。必须调用 mesh.RecalculateNormals() 和 mesh.RecalculateBounds()。RecalculateBounds 尤其关键因为很多碰撞体和剔除逻辑依赖正确的包围盒Unity 默认创建的 Mesh bounds 可能是一个原始包围盒不重算的话物体会因为显示范围错误被莫名其妙裁掉。MeshRenderer 的材质建议尽量复用同一份 Material。如果碎片数量多实例化多个材质会打断合批Draw Call 会跟着碎片数量一起翻倍。你会看到 frame 时间大部分花在渲染而非物理上那时优化就很被动。5.2 碰撞体不能无脑套 MeshCollider先考虑 Convex碎片网格通常是凹多面体。直接用 MeshCollider 并勾选 Convex 有时候能工作但一旦网格的三角形数量过高或者不是有效凸包Unity 会因为 Convex 化失败报错。我见过不少项目在这里直接崩溃。我的做法分三层。第一层碎片量少或者体积大时用 MeshCollider Convex因为它的碰撞形状最准。第二层碎片碎而多时用凸近似的包围盒碰撞体代替碰撞精度下降一些但性能提升明显。第三层对特别小的碎片干脆不加碰撞体只做视觉碎片因为它们对玩法影响很小却能省掉大量物理计算。刚体参数上我建议把 mass 按碎片体积分配而不是全部设成 1。同样大小的两块体积小的质量小飞散速度更快看起来更真实。drag 和 angularDrag 不要给太大否则碎片飞出不到一米就定住了没有滑落、翻滚的感觉。5.3 复活机制从动画到状态的切换技巧破碎效果做完不能只碎一次往往要支持一段时间后复原或者下次再碎。很多项目在这里会直接把原物体 SetActive(true)结果视觉上像是凭空修复非常生硬。我常用一个简单的层级状态机broken 状态显示碎片对象集合intact 状态显示完整模型。从 broken 回 intact 时先把碎片集合整体移动到原模型位置再播放一两帧淡入动画。淡入可以通过 Shader 的透明度参数控制不用真的做碎片飞回去的动画成本和观感之间性价比很高。另一种玩法是永远不复活但它要求碎片有清理机制。我的策略是记录所有碎片的速度和角速度当碎片长时间处于低速状态就每隔几秒尝试合并它们。合并后只保留一个静态 GameObject把子碎片变成不可见节点这样能大幅降低物理引擎的负担。5.4 影响破碎手感的关键参数手感这个东西听起来虚其实可以参数化。我总结了四个对最终体验影响最大的参数建议放到脚本的 Inspector 里方便调碎片数量数量太少像断成两截太多像灰烬一般 15 到 40 之间比较舒服。爆炸强度施加在刚体上的冲量大小太强碎片飞出屏幕太弱碎片原地掉落。碎片阻尼linearDamping 和 angularDamping 控制停止速度影响重还是轻的感觉。复活延迟从破碎到复原的时间太短玩家没看清碎了太长影响后续可玩性。我经常用石头 vs 玻璃来测参数。石头希望碎片沉重、掉落感强阻尼大爆炸强度小玻璃希望碎片轻快、飞得远阻尼小爆炸强度大。参数不是拍脑袋定的先明白自己要什么质感再去调对应项。6. 优化与细节移动端能跑效果才叫落地6.1 顶点数与 Draw Call碎片越多越容易翻车预计算切片方案最容易被忽略的黑洞是它会在运行时产生大量独立的碎片 GameObject每一个都对应一次 Draw Call。如果你不做合批一个陶罐碎成 30 块就是 30 个 Draw Call再叠加场景里的其他物体移动端很快扛不住。优化的一个大方向是碎片合批。如果碎片本身不会再被切割只是做飞散和堆叠可以在切割完成后把多个碎片的 Mesh 合并成一个大的网格每个碎片用 MeshRenderer 的 materialPropertyBlock 来控制位置偏移和颜色变化。另一种更省事的思路是让碎片共用 Material 且使用同一图集这样引擎能自动合批一部分但 Transform 矩阵不同静态合批又失效。最实用的做法还是控制同时存在的碎片数量。查询范围内的碎片可以激活范围外的直接隐藏。碎片落地且速度低于阈值后就不再参与物理模拟只保留为静态场景物体。6.2 裂痕与剖面的视觉补救几何切割切出来的剖面如果没有额外处理往往是平整的看起来不像真实断裂。两个常用补救手段一是给剖面材质叠加一张裂缝贴图利用 UV 映射让割裂的边缘有颜色变化和粗糙度变化二是在预计算阶段用噪声扰动剖面的顶点位置让断裂面不是严格的平面而是带一些起伏效果立刻真实不少。噪声扰动不能太夸张否则会造成碎片边界互相穿插。我的做法是只偏移剖面内部顶点而且偏移方向平行于切割平面垂直于平面方向的扰动钳制在很小的范围。这样碎片拼在一起还能大致对得上裂开之后又有自然的不规则感。如果项目里用 Shader Graph 或者较新的 URP还可以在碎片材质里加上基于顶点色的裂缝发光效果或者用 alpha 遮罩动态显示裂缝随时间的扩展适合做持续碎裂演出。6.3 实测中的常见翻车点我做完这套流程后在三个平台踩过的坑值得拿出来说。第一个坑是 MeshCollider 的 Convex 限制。碎片网格三角形一多Convex 化失败是家常便饭。规避方式是切割后执行一次三角形数量削减比如把共面的三角形合并或者直接用盒碰撞体替代。第二个坑是深度穿透。碎片飞散速度太快或者地面碰撞体太薄时碎片会直接穿过去。刚体默认的碰撞检测是离散模式高速物体建议临时切换为 Continuous Speculative。注意别把所有碎片都设成连续碰撞开销会变高只对关键几个碎片设置就行。第三个坑是内存峰值。预计算数据虽然避开了运行时切割的计算峰值但运行时要为每个碎片创建 Mesh、碰撞体对象。如果一帧内创建太多碎片还是会出现一瞬间的卡顿。解决方法是分批创建每帧最多创建 5 到 10 个碎片配合对象池复用基本可以做到平滑。我在一个 ARPG 项目里实际调这套效果时还发现了一个反直觉的点碎片数量并不是越多越有冲击力。把数量从几十个降到 15 个左右同时每块碎片加大一点物理碰撞声和镜头震动配合好反而是玩家反馈打击感最强的版本。开发者容易陷入效果越复杂越好的误区破碎效果真正的优先级是视觉清楚、物理自然、性能稳定。最后分享一个提升效率的习惯我会在编辑器里写一个菜单项让策划或美术能一键把任意模型切成碎片并导出成 prefab。这样调试参数时不用重新编译游戏直接在编辑器里改完刷新即可。配合一个简单的场景脚本显示碎片数量、总三角形数、预计 Draw Call优化起来会快很多。
阅读完成 · 觉得有帮助?