Unity网格切割实战:从Mesh数据到碰撞检测的完整实现

Unity网格切割实战:从Mesh数据到碰撞检测的完整实现 简介这是一套面向Unity初学者的模型切割功能学习案例源自MGR-BladeMode-master项目重点演示如何通过碰撞检测、Mesh Collider和C#脚本实现“鼠标左键蓄力、右键切割”的交互流程并涵盖切割路径计算、顶点/三角形拆分、Mesh实时刷新等关键环节。资源共595个文件压缩包约22.05MB包含282个meta配置、30个cs脚本、46个asset、15个fbx模型、15个anim动画、46个mat材质以及shadergraph、prefab等类型可满足从场景搭建到切割逻辑调试的完整学习需要。目前已有1308人学习下载。通过研究其中预设的基础几何体切割示例读者能掌握碰撞检测与模型数据修改的配合方式理解蓄力变量与切割动作的映射关系并学习如何将切割结果实时反馈到渲染层为后续开发更复杂的可破坏场景或武器切割玩法打下基础。1. 切割不只是把模型劈开从BladeMode看Unity网格切割的边界在Unity里做“切割”常常会给你一个错觉把刀撞上去模型就裂开了。实际上Unity根本没有提供任何“切开Mesh”的内置接口所谓切割效果是用代码对Mesh的顶点和三角形索引做一次原位拆分再生成两个新Mesh替换显示。MGR-BladeMode-master这套案例把这件事简化到了一个很适合上手的状态鼠标左键蓄力右键一刀切开动画文件里准备了Standard Run、Walk、Slash等作为演示角色动作但切割逻辑和动画其实互相独立。适合正在学Unity网格编程、想搞明白碰撞检测和Mesh到底怎么协作的人。看完你会知道为什么这类实现往往只能用自带基础模型以及从哪里改才能接进自己的角色和武器。2. 切割的底层逻辑Mesh数据结构与碰撞检测选型2.1 模型切割的本质修改顶点数组与三角形索引Unity的一个Mesh最核心的两个数组是vertices和triangles。vertices存所有顶点坐标triangles存三角形索引每三个int组成一个三角面。渲染时GPU按索引取顶点画三角形。所以切割某个模型本质上不是“把模型切成两半”而是“把原Mesh的顶点分成两组两组共享切割线上的交点并各自重新组合三角形索引”。这里有个关键点切割线处的顶点是原Mesh里不存在的。比如一个立方体有8个顶点、12个三角形如果一刀从中间切开横截面上会出现新的4个顶点。算法必须计算切割平面与每条相交边的交点插入新顶点再把原三角形拆分成多个小三角形才能保证切开后两个半块都有封闭的截面不漏面。对于MGR-BladeMode这种案例它只处理“自身”的网格数据不从外部读取复杂模型所以可以在内存中直接修改原始数组。你在写这类逻辑时第一件事永远是拿到MeshFilter.mesh的副本而不是引用因为sharedMesh是全局共享的直接改会导致所有开发者共用同一个资源运行时改完恢复不了。2.2 为什么案例只能切开自带基础模型Mesh Collider的隐藏约束查看案例项目时你会发现被切割的目标基本都是Unity基础几何体比如Cube、Sphere、Cylinder而不是导入的高模角色。这背后的直接原因是Mesh Collider的行为约束。Mesh Collider的碰撞网格默认是在编辑阶段烘焙的一旦运行时修改Mesh碰撞体不会自动跟着变。哪怕你调用了GetComponentCollider().enabled false再开启或者重新赋值sharedMesh碰撞体的创建依然很耗时而且对凸包要求严格。更现实的约束是高模顶点几千上万个切割时每帧遍历所有三角形做相交测试在纯C#层面很容易把帧率拉到个位数。基础模型三角形数量只有12到几百个可以在Update()里完成计算而不产生明显卡顿。所以初学者看到这个项目有“只能切开自带基础模型”的限制不要觉得是Bug这其实是教学案例主动划定的安全边界。等你把算法吃透再优化成只处理刀片扫过的局部三角形区域才有能力碰高模。2.3 蓄力值的动机把切割力度变成参数项目要求“鼠标左键蓄力右键切开”这看起来像交互设计其实是在为切割算法提供输入参数。蓄力值本质上是一个浮点数左键按住期间持续累加右键触发时读取它。这个值典型的用途有两个一是控制切割平面的延伸长度决定刀光扫过的范围二是决定切开后两块模型分离的初速度力度越大飞得越远。这个设计对新手很友好因为它把“切割算法”和“切割效果”解耦了。算法只需要一个切割平面开始点和一个方向而力度值在调用算法前被转成一个带长度的切割平面或转成一个冲量。这样你替换输入方式比如换成手柄扳机时完全不用改动网格分割的核心代码。下面就从输入监听开始实现这个交互链路。3. 从输入到切割Input Manager与C#脚本骨架3.1 左键蓄力、右键切开Input类的标准写法MGR-BladeMode属于偏老的项目用的还是旧版Input Manager而不是新版Input System。在Unity 2021之后新建项目默认是Input System你需要把Project Settings里的Active Input Handling切换成Both或Input Manager (Old)否则Input.GetMouseButton相关代码会直接失效。看一下案例中的关键输入逻辑通常长这样public class BladeCutInput : MonoBehaviour { public float chargePower 0f; // 当前蓄力值 public float maxCharge 3f; // 最大蓄力限制 public float chargeSpeed 1.2f; // 每秒增加的量 void Update() { // 左键按住蓄力值朝上限累加 if (Input.GetMouseButton(0)) { chargePower Mathf.Clamp(chargePower chargeSpeed * Time.deltaTime, 0f, maxCharge); } // 右键按下触发切割并重置蓄力 if (Input.GetMouseButtonUp(1)) { if (chargePower 0.1f) return; // 没有蓄力不执行 DoCut(chargePower); chargePower 0f; } } }这里把蓄力和施放放在Update()里因为输入事件是按帧轮询的。GetMouseButton(0)返回的是按住状态不是按下瞬间所以用GetMouseButtonUp(1)检测右键的抬起动作这样玩家按住右键到松开再触发更符合蓄力切开的操作直觉。Mathf.Clamp把蓄力值限制在0到3之间避免浮点值无限增长。注意切换Active Input Handling后需要重启Unity Editor不然后续编译会报找不到Input类。如果你在项目里切换到了新输入系统对应的写法是把GetMouseButton换成绑定到InputAction的started和canceled回调但蓄力累加放在Update里的思路不变。3.2 切割脚本的完整工作流把输入逻辑和网格切割放在一起会得到一套更完整的流程。我从几个类似项目的共性中整理一下通常可以按下面几步走从鼠标位置发射一条射线获取刀片碰到的模型。根据刀片的前进方向构造一个垂直于刀面的切割平面。调用网格切割函数传入目标MeshFilter和切割平面。用切割函数返回的两个Mesh创建新的GameObject。销毁或隐藏原始物体。核心调用代码大致如下void DoCut(float power) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit)) return; MeshFilter targetMeshFilter hit.collider.GetComponentMeshFilter(); if (targetMeshFilter null) return; // 以刀片Transform的right向量作为切割方向hit.point作为平面经过点 Plane cutPlane new Plane(bladeTransform.right, hit.point); // power只决定切开后初始分离速度不改变平面位置 Mesh[] halfMeshes MeshCutter.Cut(targetMeshFilter.mesh, cutPlane); // 构造两个模型 CreateHalfModel(halfMeshes[0], hit.collider, power); CreateHalfModel(halfMeshes[1], hit.collider, power); Destroy(hit.collider.gameObject); }这段代码里的MeshCutter.Cut是核心后面会展开。注意切割平面用了bladeTransform.right也就是刀片模型的X轴方向。实际项目中你更该使用刀片扫过的两个采样点来构造平面否则固定轴方向会导致斜切不准。但案例为了操作简单直接拿刀片局部坐标轴当切割方向代价是不支持任意角度的斜切。3.3 生成两个新模型切割后的左右部分MeshCutter.Cut返回两个Mesh数组接下来要把它们摆在场景里。一个常见错误是直接沿用原GameObject的Mesh Collider重新签一个Mesh就完事结果物理上还是原来一整块的碰撞。合理的做法是给两个半块分别添加独立的MeshFilter、MeshRenderer和MeshCollider再根据原模型的位置同步Transform。GameObject CreateHalfModel(Mesh mesh, Collider originalCollider, float power) { GameObject half new GameObject(originalCollider.name Half); half.transform.position originalCollider.transform.position; half.transform.rotation originalCollider.transform.rotation; half.AddComponentMeshFilter().mesh mesh; half.AddComponentMeshRenderer().material originalCollider.GetComponentMeshRenderer().material; MeshCollider mc half.AddComponentMeshCollider(); mc.sharedMesh mesh; // 碰撞网格使用当前切割结果 Rigidbody rb half.AddComponentRigidbody(); // 根据蓄力值给一个向外推的力 rb.AddForce(originalCollider.transform.up * (1f power * 2f), ForceMode.Impulse); return half; }这里sharedMesh因为是从原mesh切割出来的新实例所以直接赋值碰撞体没有问题。如果你直接给sharedMesh赋原mesh改其中一个会影响到另一个。蓄力值作用在分离初速度上力度越大两半飞得越远。注意我加了ForceMode.Impulse和直接赋velocity的区别在于它更符合物理直觉且不会因为质量不同导致速度不一致。4. 切割算法的关键步骤从碰撞检测到三角形切分4.1 用Plane类定义切割平面Unity自带的Plane结构体是做这种平面切割的好帮手。它包含一个法向量和一个到原点的距离。通过Plane.GetSide(Vector3)可以判断某个点在平面的哪一侧Plane.SameSide(a, b)判断两个点是否在同一侧Plane.Raycast则可以用来求射线交点。在网格切割里最常用的组合是遍历所有三角形顶点判断它们相对切割平面是正侧、负侧还是相交。构造一个正确的平面是第一步。假设你有一个刀片对象想要从它的刀锋方向切过去平面法线应该垂直于刀锋所在面而平面穿过刀锋尖端的一个点。这里我沿用上一章的bladeTransform.right并强调一下Plane构造函数的第一个参数是法线第二个参数是平面上的一点。如果法线方向反了切割结果的两个半块会左右互换甚至全部落在同一侧导致切割失败。4.2 遍历三角形侧向判断与顶点插值这是切割算法的核心也是MGR-BladeMode这类小项目最值得看的一段。思路是这样的对于每一个原三角形看它的三个顶点分布在切割平面的哪一侧。可能出现三种情况三个顶点都正侧三角形保留到正侧网格三个顶点都负侧保留到负侧网格顶点跨过平面这时需要计算切割边与平面交点把原三角形拆分成两个三角形分别归到两侧。计算交点的标准方法是线性插值已知一条边的两端点A和B且A在正侧、B在负侧那么切割点C满足C A t * (B - A)其中t等于A到平面的符号距离除以两个符号距离之差的绝对值。Unity的Plane结构体没有直接暴露符号距离但可以用plane.GetDistanceToPoint(p)得到有符号距离正好拿来算t。下面是核心遍历代码public static Mesh[] Cut(Mesh sourceMesh, Plane cutPlane) { Vector3[] verts sourceMesh.vertices; int[] tris sourceMesh.triangles; // 为两个半块准备顶点列表和三角形列表 ListVector3 positiveVerts new ListVector3(); ListVector3 negativeVerts new ListVector3(); Listint positiveTris new Listint(); Listint negativeTris new Listint(); // 原Mesh的三角形索引每三个组成一个面 for (int i 0; i tris.Length; i 3) { Vector3 a verts[tris[i]]; Vector3 b verts[tris[i 1]]; Vector3 c verts[tris[i 2]]; // 分别记录三个点到平面的有符号距离 float da cutPlane.GetDistanceToPoint(a); float db cutPlane.GetDistanceToPoint(b); float dc cutPlane.GetDistanceToPoint(c); // 全正侧三角形整体保留到正侧 if (da 0 db 0 dc 0) { int idx positiveVerts.Count; positiveVerts.Add(a); positiveVerts.Add(b); positiveVerts.Add(c); positiveTris.Add(idx); positiveTris.Add(idx1); positiveTris.Add(idx2); } // 全负侧整体保留到负侧 else if (da 0 db 0 dc 0) { int idx negativeVerts.Count; negativeVerts.Add(a); negativeVerts.Add(b); negativeVerts.Add(c); negativeTris.Add(idx); negativeTris.Add(idx1); negativeTris.Add(idx2); } else { // 跨平面需要插值生成新顶点并把原三角形拆分成两个 CutTriangle(a,b,c, da,db,dc, cutPlane, positiveVerts, negativeVerts, positiveTris, negativeTris); } } Mesh positiveMesh BuildMesh(positiveVerts, positiveTris); Mesh negativeMesh BuildMesh(negativeVerts, negativeTris); return new Mesh[] { positiveMesh, negativeMesh }; }GetDistanceToPoint返回的距离带正负号所以用0作为正侧判断。这里的阈值零是切割平面的数学定义不需要额外处理浮点精度因为插值计算本来兼容微小误差。关键是跨平面情况CutTriangle里要分别计算三条边与平面的交点再把原来的一个三角形补成两个三角形。由于我们的半块顶点列表是独立累积的新顶点的索引会在原有顶点追加后算出不会与原始三角形索引冲突。4.3 避免断边和破洞共享顶点、法线重算与Mesh重载在切割算法里最常见的视觉Bug是切口处出现锯齿状破洞。原因通常是切割时只对每个三角形独立插入交点没有处理相邻三角形共享同一条边的情况。比如两个相邻三角形共享一条跨切割平面的边它们各自插入的交点坐落在同一位置但因为是分别用插值算出来的浮点数可能略有差异导致网格缝合不了。解决办法是建立一个词典以边的两个端点索引为key缓存切割点。如果一条边已经被处理过直接复用之前算出的交点。代码里可以用DictionaryVector2Int, int来做其中Vector2Int存边两端点的原索引int是新增顶点的索引。另外切割完成后一定要调用一次mesh.RecalculateBounds()和mesh.RecalculateNormals()否则模型显示会黑掉或者拾取碰撞时判定范围错误。下面把经验整理成一张排查表现象原因处理方法切出来两面各缺一部分切割平面法线方向反了反转法线向量或交换正负侧结果截面是黑色或法线异常没有重算法线调用RecalculateNormals()模型边缘有裂纹共享边顶点未复用用字典缓存切割点索引碰撞和视觉错位碰撞网格和渲染网格引用了不同实例给MeshCollider赋值和MeshFilter同一份mesh这些条目不是推导出来的是实际运行这类切割案例最容易撞上的坑。MGR-BladeMode里很多初学者反馈“切开后模型消失了”其实多半不是算法错而是Destroy原物体后两个半块没有正确设置碰撞体或渲染材质被相机剔除或者掉出世界边界。4.4 物理塌陷刚体与碰撞器的重建切割完成后的两个新模型如果只是换了Mesh物理上依然可能互相穿透。因为Mesh Collider在运行时创建很慢有些实现会直接用Box Collider近似替代导致两块模型还没分开就互相卡住。更好的做法是让每半块都用拆分后的Mesh生成一个MeshCollider并开启convex true这样不会因为非凸碰撞体导致Rigidbody警告。在性能敏感的移动端这两种做法实际差别很大。用凸包碰撞体物理引擎只需要对一个凸包做碰撞计算速度远快于精确三角形网格碰撞。代价是凸包近似可能会让两半模型碰撞时感觉“棱角膨胀”但对教学案例来说完全够用。如果你最终做的是Apple Vision Pro或PICO混合现实项目这类切割效果更建议用GPU计算和碰撞体简化方案绕开CPU端的物理瓶颈——那属于另一套优化路径了先把这里的算法跑通再说。5. 实操验证与进阶让切割更通用的三个小改造5.1 在Unity中跑通MGR-BladeMode参数与步骤拿到项目后建议按这个顺序做一遍验证能少走很多弯路打开项目检查Package Manager里有没有导入旧版Input Manager相关设置没有就在Project Settings中切换Active Input Handling为Both。找到带有动画控制器的角色把Standard Run、Standard Walk、Idle和Slash动画拖进Animator Controller确认角色能跑起来。这一步和切割没有直接关系但能让你确认动画资源可用。把切割脚本挂到刀片模型上刀片需要有一个带Trigger的Collider目标模型需要是带MeshFilter和Mesh Collider的基础几何体。运行后先按住鼠标左键看到蓄力UI或数值变化再点右键观察目标模型是否被切成两块。如果右键没反应优先检查鼠标事件是否被UI的RaycastBlock挡住或者是否用的是GetMouseButtonUp而不是GetMouseButtonDown。这里有一个容易忽略的参数蓄力最大值。案例里默认把maxCharge设得比较低导致你在编辑器里非常快地按下右键可能蓄力值还没超过阈值就触发了切割看起来像没蓄满也能切。实际的判定条件是chargePower 0.1f才不执行也就是说哪怕你只按了0.1秒也能满足条件。想做严格的门槛就把这个阈值提高到一半或者要求蓄力时间达到0.5秒。5.2 改造点从只能切开基础模型到任意Mesh想要切割非基础模型第一个要换掉的是碰撞检测方式不要用Mesh Collider做射线判定而用刀片的Trigger来获取进入范围的目标。然后在刀片扫过时记录前后两帧刀锋世界坐标用这两个点构造切割平面而不是直接拿刀片的局部轴。这样可以支持任意角度的斜切不受基础模型顶点布局影响。第二个改造是性能。遍历整个Mesh的三角形在顶点数超过5000时就开始吃力常见做法是把目标模型按空间划分到Unity的Bounds或者八叉树里只遍历刀锋所在的局部格子。还有一个更粗暴但有效的办法把切割过程移出Update()放到协程里分几帧完成每帧只处理一部分三角形并在切割完成后一次性生成两个新Mesh。这样帧率不会掉得太难看。void CutOverFrames(MeshFilter target, Plane plane, int trianglesPerFrame 500) { StartCoroutine(CuttingCoroutine(target, plane, trianglesPerFrame)); }用协程切片执行比一次性切割更适合真实游戏项目。注意切割期间需要锁定目标对象不能用户再切第二刀否则两段协程会同时修改同一个顶点数组造成数据错乱。这个在MGR-BladeMode这种教学资源里没有涉及但接进自己的战斗系统时一定要处理。5.3 用Gizmos可视化切割平面调试效率翻倍最后分享一个调试技巧每次执行切割前在场景里画出切割平面的位置和法线能立刻看出思路对不对。void OnDrawGizmos() { if (cutPlaneReady) { Gizmos.color Color.red; Gizmos.DrawWireCube(cutPlaneCenter, new Vector3(1f, 0.01f, 1f)); Gizmos.DrawRay(cutPlaneCenter, cutPlaneNormal * 2f); } }OnDrawGizmos在Scene视图里每帧绘制编辑器下可以直接看到平面是否贴合刀锋法线有没有反。调试完成后把cutPlaneReady置为false或者删掉这段代码避免最终打包时留在游戏逻辑里。用这招排查切割方向问题比盯着Console看坐标日志效率高得多。如果现在再看MGR-BladeMode的源码你应该能直接指出它在哪里漏掉了凸包碰撞体的重建然后把这段补上。本文还有配套的精品资源点击获取