
简介这是一份面向Unity初中级开发者的模型切割功能教学案例围绕“鼠标左键蓄力、右键切开”的交互方式演示如何通过碰撞检测、Mesh顶点与三角形分割实现刀片切割效果适合想掌握物理模拟与图形编程基础的学习者动手实践。压缩包共595个文件约22MB包含C#脚本、Unity场景、材质、FBX模型、动画片段与Shader Graph等资源目录结构完整可直接打开工程对照学习。资源已有1308人学习口碑和实用性经过了初步验证。通过研读该案例开发者可以理解切割路径计算、Mesh更新与碰撞器配置等关键步骤并能在此基础上延伸出自己的切割玩法。1. unity 切割模型案例先从几何边界说起运行时把一个方块或石头切成两半很多团队的第一反应是播放一段预制的碎裂动画顶多再隐藏原物体、生成两个碎片预制体。真正可交互的 unity 切割模型案例不是预演而是当场修改 Mesh 数据先定义一个无限延伸的切割平面再把平面上方和下方两组三角形重新组织成两个独立网格。一次完整切割通常要处理顶点求交、法线与 UV 保留、剖面补洞、碰撞体和刚体挂接这几件事。这个方向在数字孪生拆解、技能养成、物理破坏等玩法里都很常用。已经熟悉 MonoBehaviour 生命周期、但没碰过 Mesh 顶点数组的开发者可以从这里入手如果已经在用第三方切割插件只是被不规则网格的破洞、法线翻转和碰撞体抖动问题卡住下面这套最小实现也能帮你定位边界条件。下文不依赖付费插件只基于 Unity 自带的 Mesh、Plane 和 Physics 接口。2. 切割模型的原理与前置Mesh 数据、切割平面与三个预处理2.1 Unity Mesh 的结构决定切割模型要从三角形索引下手Mesh 里最常见的是vertices、triangles、uv、normals四个数组。vertices存放顶点坐标triangles按每三个整数一个三角形列出索引uv和normals分别用于贴图和光照。切割模型案例中所谓“把模型切开”本质上是重排这套数组每个三角形要么完整留在切割平面一侧要么被平面切成两个三角形再分给两侧。默认情况下很多美术导出的模型并不封闭可能有开口边、翻转面或者重复顶点。切割前如果不检查切出来的碎片会出现“漏风”、背面缺失或阴影异常。针对“unity切割模型案例”这类需求我习惯先打开Frame Debugger看一遍网格的三角形绘制顺序确认不是所有面都靠双面渲染兜底。2.2 用 Plane 结构表达切割平面Unity 自带的Plane足够用来做平面求交。new Plane(normal, point)会生成一个经过 point、法向为 normal 的平面GetDistanceToPoint返回带符号距离大于 0 表示在法线一侧小于 0 表示另一侧。这个正负号就是后续把三角形分到两个新 Mesh 的依据。Plane cutPlane new Plane(Vector3.up, Vector3.zero); float d cutPlane.GetDistanceToPoint(new Vector3(0f, 1f, 0f)); // d 1说明该顶点在法线正侧Plane的三个公开参数里normal是单位法向distance是平面到原点的常量很多资料把它写成“平面距离”但不要误解成顶点到平面的距离后者要用GetDistanceToPoint。切平面往往是从玩家点击位置生成的可以用射线拾取表面点做原点Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { Plane plane new Plane(hit.normal, hit.point); }这里hit.normal是表面法线用它做切面法线适合“在表面砍一刀”的效果如果想让刀路始终水平改成new Plane(Vector3.up, hit.point)更直观。2.3 切割前的三个预处理法线、UV 与封闭体开始写算法前我会固定三件事。第一把被切物体放到自身局部坐标系里计算避免世界坐标下出现非均匀缩放如果Transform有负数缩放hit.normal与局部法线会不一致切割结果会很怪。第二确认模型封闭法线整体朝外否则剖面补洞后可能会产生反转的可见面。第三备份原 Mesh 的uv和normals因为纯几何重建后这些属性会丢失光照会立刻变暗或出现硬边。下面是常用的属性检查表属性切割模型中的作用预处理建议vertices几何位置和求交输入检查 NaN、无穷值triangles三角形拓扑索引不越界最好统一起始点normals光照方向切割后需要RecalculateNormalsuv纹理坐标原面可插值剖面要单独生成mesh.bounds包围盒用于剔除不在切面附近的三角形这组检查做下来剩下的工作就是纯几何拿Plane去切三角形再把切出来的两堆三角形和补洞面写成两个新 Mesh。3. 三角形求交与两片网格重建的实现3.1 遍历三角形并按平面分类三种情况遍历原triangles数组时每个三角形三个顶点分别计算带符号距离da、db、dc。如果三个值都大于等于 0三角形归入正面都小于等于 0归入背面有正有负说明这条三角形被平面穿过。等于 0 的顶点我统一算作正面可以减少后续退化和重复交点。bool af da 0f, bf db 0f, cf dc 0f; if (af bf cf) { // 全部正面原样加入正面网格 } else if (!af !bf !cf) { // 全部背面原样加入背面网格 } else { // 跨界需要求交并重建 }分类本身不改变顶点只是为后续重建指定去向。跨界三角形不能简单拆成两个因为切开后正面部分可能是个四边形背面部分也可能是个三角形或四边形必须用交点补成若干个三角形否则网格会出现空洞。3.2 顶点插值与切面点计算一条边与平面相交时交点可以用线性插值计算。设端点 a 和 b 到平面的带符号距离为 da、db交点参数t da / (da - db)它表示交点沿 ab 边的比例。这个公式成立的基础是带符号距离随位置线性变化适合绝大多数模型当 da 和 db 非常接近时分母接近 0先判断符号是否相反再插值更安全。private static Vector3 EdgeIntersection( Vector3 a, Vector3 b, float da, float db) { float t da / (da - db); return Vector3.Lerp(a, b, t); }把交点同时加入正面和背面的顶点序列是后续补洞的关键。每次跨界都会产生一两个交点把所有交点收集起来就得到了切割平面与整个 Mesh 的相交轮廓。3.3 用三角扇重建两个网格从顶点序列到三角形实际实现里我不会直接修改原triangles索引而是用两个ListVector3和两个Listint重新生成两个 Mesh。每个跨界三角形经过 3.2 的求交后会得到一组正面顶点和一组背面顶点然后对它们做三角扇剖分。private static void FanTriangulate( ListVector3 pts, ListVector3 verts, Listint tris, bool reverse) { if (pts.Count 3) return; int baseIndex verts.Count; verts.AddRange(pts); for (int i 1; i pts.Count - 1; i) { int a baseIndex; int b baseIndex i; int c baseIndex i 1; if (reverse) (a, c) (c, a); tris.Add(a); tris.Add(b); tris.Add(c); } }这里reverse参数用于背面网格。因为原三角形的绕序是固定的背面那一侧的三角形如果沿用相同绕序法线会朝内切割模型案例最典型的反馈就是“碎片看起来是黑的”。因此背面网格需要把三角形索引顺序反过来再配合RecalculateNormals修正法线。三角形情况是否求交正面网格背面网格全部正面否原样加入不处理全部背面否不处理原样加入跨界是交点正面顶点三角化交点背面顶点反向三角化这里要特别说明三角扇只适合凸多边形。如果切割平面把一个凹多边形切成两半成型后可能需要用约束三角剖分工具。对大多数 Unity 游戏里的小方块、圆柱、石头而言凸性假设已经够用。3.4 剖面补洞用切面上的交点轮廓生成封口原始网格没有横截面切完后两个碎片中间必须补一个平面。把 3.2 收集到的cutPoints放到切割平面上求一个质心center然后把点按绕质心的角度排序形成轮廓环再对这个环做三角扇就能生成封口面。Vector3 center Vector3.zero; foreach (Vector3 p in cutPoints) center p; center / cutPoints.Count; // 用切面自身的两个正交轴做二维投影 float dotUp Mathf.Abs(Vector3.Dot(plane.normal, Vector3.up)); Vector3 axisX Vector3.Cross( plane.normal, dotUp 0.99f ? Vector3.right : Vector3.up).normalized; Vector3 axisY Vector3.Cross(plane.normal, axisX).normalized; ListVector3 ring new ListVector3(cutPoints); ring.Sort((p, q) { float ap Mathf.Atan2( Vector3.Dot(p - center, axisY), Vector3.Dot(p - center, axisX)); float aq Mathf.Atan2( Vector3.Dot(q - center, axisY), Vector3.Dot(q - center, axisX)); return ap.CompareTo(aq); });这里axisX和axisY是切面上两个互相垂直的方向Mathf.Atan2把每个投影点转成角度排序后ring就是按绕序排列的交点轮廓。随后对ring做三角扇再加到两个 Mesh 的三角形列表里一个用正向绕序一个用反向绕序。注意cutPoints必须去重距离小于 0.001 的点视为同一个否则排序后的环会出现裂缝。这段代码是简化版生产环境里还要考虑平面斜切时轮廓是复杂凹多边形的情况但用在入门案例上足够。4. 在 Unity 场景里跑通一个完整的切割模型案例4.1 把切割流程封装成 MeshCut 静态类前面讲的是切和补的算法这里给出一个可放在场景里测试的最小案例。我会写一个MeshCut静态类暴露Cut(GameObject source, Plane plane)调用后生成两个新的 GameObject原物体进入隐藏状态。public static void Cut(GameObject source, Plane plane) { MeshFilter meshFilter source.GetComponentMeshFilter(); Renderer renderer source.GetComponentRenderer(); Mesh srcMesh meshFilter.sharedMesh; Mesh frontMesh new Mesh(); Mesh backMesh new Mesh(); SplitAndFill(srcMesh, plane, frontMesh, backMesh); frontMesh.RecalculateNormals(); backMesh.RecalculateNormals(); frontMesh.RecalculateBounds(); backMesh.RecalculateBounds(); CreateFragment(CutFront, source, frontMesh, renderer.sharedMaterial); CreateFragment(CutBack, source, backMesh, renderer.sharedMaterial); source.SetActive(false); }SplitAndFill就是第 3 章里全部三角形分类、求交、补洞逻辑的汇总它会返回两个新 Mesh。这里用RecalculateNormals重新计算法线是为了让切面在光照下看起来自然如果原模型有关键的平滑法线信息也可以根据原法线做插值再赋给新 Mesh视觉会更平滑。4.2 切割后生成新物体复制材质与碰撞体参数碎片物体必须拥有独立的MeshFilter、MeshRenderer以及用于物理反馈的MeshCollider和Rigidbody。下面是一个很实用的创建函数private static Renderer CreateFragment( string name, GameObject source, Mesh mesh, Material material) { GameObject go new GameObject(name); go.transform.SetPositionAndRotation( source.transform.position, source.transform.rotation); go.transform.localScale source.transform.localScale; go.AddComponentMeshFilter().sharedMesh mesh; go.AddComponentMeshRenderer().sharedMaterial material; MeshCollider collider go.AddComponentMeshCollider(); collider.sharedMesh mesh; collider.convex true; Rigidbody body go.AddComponentRigidbody(); body.mass 1f; body.interpolation RigidbodyInterpolation.Interpolate; return go.GetComponentMeshRenderer(); }这个函数把convex设置为 true意味着 PhysX 会把碎片网格当作凸体参与碰撞检测物理稳定性最好。但也要清楚凹面物体切出来的碎片如果设置 convextrue碰撞体形状会和视觉网格差异变大。你要做桥梁或 L 形墙体拆除时可以改成 false物理更准确但多个碎片同时运算的性能会明显下降。碎片类型convex 设置原因小方块、圆柱碎片true物理最稳性能最好L 形 / 凹形碎片false碰撞形状准确但物理开销大大量小碎片true避免 PhysX 卡顿Rigidbody.mass按需设置给 1f 只是提供一个基准实际项目里要根据碎片体积自动计算否则大块浮空小块沉重手感很怪。4.3 性能与稳定性碎片数量、Mesh 合并与碰撞体上限这个切割模型案例继续切几次后Mesh 顶点数翻倍增长一个三角形被切一次会产生一到两个新三角形连续切 3 次后碎片数量会到几十个。这里要提前设上限比如“同一物体最多被切 4 次”或“碎片数量超过 20 就停”。另外每次生成MeshCollider时引擎都会重新烘焙这是比 Mesh 重建更贵的操作所以碎片不要一出生就都挂MeshCollider可以延迟 0.1 秒再启用。对 Unity 游戏优化来说最直接的优化是用Mesh.CombineMeshes把相邻碎片合并成一个静态 Mesh再统一挂一个MeshCollider或者用一个BoxCollider近似较大碎片的包围盒。还要记得开启对象池重复使用GameObject和Mesh避免 GC 和 Mesh 泄漏。切割后的原 Mesh 如果不使用调用DestroyImmediate或放进池子复用别留在资源里。5. 进阶让切割模型结果更稳、更好看的几个验证技巧5.1 用局部坐标系做切割减小浮点误差如果源物体挂在一个缩放很大的父节点下直接在世界坐标求交会把缩放带进交点计算产生约 0.0001 级的偏差表现在碎片之间就是细微缝隙。常见做法是先把切面转换到局部坐标Plane localPlane new Plane( source.transform.InverseTransformDirection(plane.normal), source.transform.InverseTransformPoint(plane.origin));Plane类没有直接暴露 origin但可以用plane.normal * (-plane.distance)推出来。切割时所有顶点都用局部坐标切完再把碎片放到世界坐标能省掉不少粒级 bug。5.2 剖面材质与 UV让剖面纹理看起来自然默认生成的切面没有 UV采样材质时会退回 (0,0)导致剖面一片同色。想显示木纹横切面可以按切面的平面坐标生成 UV继续用 3.4 里算好的axisX和axisYVector2[] capUV new Vector2[capVerts.Count]; for (int i 0; i capVerts.Count; i) { Vector3 offset capVerts[i] - center; capUV[i] new Vector2( Vector3.Dot(offset, axisX), Vector3.Dot(offset, axisY)); }这样切面纹理不会因为物体旋转而变形。材质方面我会给剖面单独捏一个带主纹理的材质实例原物体表面材质继续用sharedMaterial避免两个材质互相污染。5.3 切割结果验证的三个检查点拿到碎片后别急着摆场景先做三个确认。第一用Debug.DrawRay在法线上画线段确认RecalculateNormals后的法线朝外不正确就把 3.3 里的reverse参数翻转。第二打开Frame Debugger看碎片 Mesh 是否出现背面剔除导致的空洞。第三把两个碎片原地放 1 秒观察刚体是否会互相推开。如果是把切面顶点沿法线方向收缩 0.001 单位让相邻碎片在初始帧就有一点间隙。if (Physics.Raycast(ray, out RaycastHit hit)) { Debug.DrawRay(hit.point, hit.normal * 0.2f, Color.green, 3f); }上面这行射线在点击位置绘制法线方向能直观看出表面法线是否已经翻转。把三个检查点跑完再看一眼切面接缝处有没有细缝有的话回到 5.1 的局部坐标方向查一遍。本文还有配套的精品资源点击获取