☰
Unity运行时Mesh切割:原理、脚本与避坑指南
2026/10/5 9:25:10 网站建设 项目流程

简介:面向 Unity 初学者的模型切割案例,演示通过鼠标左键蓄力、右键触发切割的基础交互,结合碰撞检测与网格数据处理实现模型分割,适合学习物理模拟与网格编程的开发者。压缩包共 595 个文件,约 22.05MB,包含 30 个 C# 脚本、46 个材质、15 个 FBX 模型、15 段动画、纹理贴图以及 Unity 场景、预制体等,目录组织便于直接导入工程对照学习。案例围绕切割工具与目标模型展开,涉及网格碰撞体检测、刚体物理属性、切割路径计算、顶点与三角形拆分等关键步骤,从输入监听、蓄力变量到模型实时刷新均有对应实现;同时附带着色器图形、预设与若干贴图,可辅助理解渲染表现。已有 1310 人学习下载。通过阅读其中的 C# 脚本和场景布局,能完整掌握模型切割在物理交互与网格修改上的实现思路,并迁移到自己的游戏原型中。

1. 做 unity切割模型案例,难的不是切,而是切完那一瞬间模型碎成什么样

在 Unity 里做切割模型案例,最劝退的不是算法本身,而是切完以后模型直接变成一根烂面条,或者干脆从场景里消失。我见过不少刚开始做物理破坏、水果忍者这类玩法的团队,第一版切完以后所有三角形全部绕在一起,画面跟抽象画一样,只能靠关掉 MeshRenderer 逃避问题。这个标题背后真正要解决的事,是把一个静态 Mesh 在运行时拆成两个完整、封闭、还能继续被物理系统处理的子网格,同时外观不出洞、UV 不拉伸、法线不反。它适合三类人:做破坏效果的玩法开发、做训练模拟器的工具链工程师、以及想在编辑器里做自定义切分功能的扩展党。顺着这个标题往下走,你会把原理、脚本、参数和坑一次踩明白。

2. 切割模型的原理与选型:先懂 Mesh 再写代码,不然切的是玄学

2.1 切割的本质是三个数组的重新分配

Unity 的 Mesh 在内存里其实就是一个顶点数组、一个三角形索引数组和一套 UV / 法线数组。切割模型的本质不是“用刀切”,而是“根据一个切割平面,把现有三角形拆成两部分,再为切面生成新的三角形把洞补上”。这段话你可以记一辈子:切割就是算点、分三角形、补洞,最后重新生成 Mesh。

先说算点。你需要把每个三角形的三个顶点分别取出来,逐个和切割平面做距离判断。如果三个到切割平面的符号一致,说明整个三角形在平面一侧,直接归到对应半边;如果符号不一致,说明三角形被平面穿过了,需要在三角形边上算出交点,把原来的三角形拆成两个甚至三个三角形。这一步是全部逻辑的核心。

需要注意的是,Unity 的 Mesh.vertices 和 Mesh.triangles 是两条平行的数组。顶点数组记录的是位置和跟随位置的一堆属性,三角形索引数组只是“哪些顶点组成一个三角形”的编号。你在切割时对顶点做的任何插值,都要同步处理 UV、法线和切线,不能只算坐标。否则切完以后光照和贴图就会立刻翻车。

2.2 运行时 Mesh 切和美术预切:两条路难度差十倍

做切割模型案例时,你首先要想清楚用哪种方式。运行时 Mesh 切是动态的,任何角度、任意位置都能切,适合玩家自由发挥的水果忍者、建造破坏类游戏。美术预切则是模型师提前把物体切成几十块,运行时只做“隐藏一块,显示两块”的操作,优势是效果完美、性能极低,代价是只能按预设的路线裂开,自由度和真实破坏感都大打折扣。

这里给一张对比表,方便你根据项目规模选型。

对比项运行时 Mesh 切美术预切
自由度任意平面切割固定切面
性能切完需要重算 Mesh几乎零开销
开发成本算法复杂,边界多建模工作量大
内存占用顶点数量翻倍多份预制体
适用场景破坏玩法、实时模拟剧情演出、移动端轻量项目

我作为一线工程师的建议是:如果你做的是需要“玩家随意砍”的玩法,直接走运行时 Mesh 切。如果只是让石头按固定几条裂缝碎掉,老老实实用美术预切配合临时显隐,别为了炫技给自己挖坑。

2.3 切割平面的数学基础:点、符号、插值三点定全局

所有运行时切的基础是平面方程。Unity 的 Plane 类封装了最常用的功能:Plane.GetSide(Vector3) 返回点在平面哪一侧,Plane.GetDistanceToPoint(Vector3) 返回带符号的距离。切割时,你只要把每个顶点代入距离计算,就能判断它在哪一侧,然后把三角形按顶点分布分到两个组。

跨越切割平面的三角形需要生成新顶点。常见做法是用线性插值:设 A 和 B 是两个原始顶点,dA 和 dB 是它们到平面的带符号距离,当 dA 和 dB 异号时,交点 T = A + (B - A) * (dA / (dA - dB))。这个公式对位置、UV、法线同样适用,千万别只插值坐标。

Vector3 vertexA = vertices[triA]; Vector3 vertexB = vertices[triB]; float dA = plane.GetDistanceToPoint(vertexA); float dB = plane.GetDistanceToPoint(vertexB); float t = dA / (dA - dB); Vector3 newPos = Vector3.Lerp(vertexA, vertexB, t); Vector2 newUV = Vector2.Lerp(uvA, uvB, t); Vector3 newNormal = Vector3.Lerp(normalA, normalB, t).normalized;

逻辑说明:t 是由距离比例算出的插值权重,它同时作用于位置、UV 和法线。用 normalized 归一化法线是为了让光照方向正确,否则切面上的高光和阴影会显得脏。这里的核心参数是 dA 和 dB 的符号判定:当 dA 和 dB 非常接近 0 时,插值权重会跳到极值,所以后续代码里要加一个容差判断。

3. 用 C# 复现 Unity 切割模型的最小脚本:核心函数与参数拆解

3.1 搭建切割脚本骨架:一个入口函数,两个输出对象

我一般会把切割做成一个静态类,传入 Mesh 和一个 Plane,返回两个 Mesh,这样不管是编辑器工具还是运行时都能复用。下面是最小的入口结构:

public static class MeshCutter { public static void Slice(Mesh source, Plane plane, out Mesh partA, out Mesh partB) { Vector3[] vertices = source.vertices; int[] triangles = source.triangles; Vector2[] uvs = source.uv; Vector3[] normals = source.normals; List<Vector3> vertsA = new List<Vector3>(); List<Vector3> vertsB = new List<Vector3>(); List<Vector2> uvsA = new List<Vector2>(); List<Vector2> uvsB = new List<Vector2>(); List<Vector3> normalsA = new List<Vector3>(); List<Vector3> normalsB = new List<Vector3>(); List<int> trisA = new List<int>(); List<int> trisB = new List<int>(); // 核心处理:逐三角形拆分 ProcessTriangles(source, plane, vertices, triangles, uvs, normals, vertsA, vertsB, uvsA, uvsB, normalsA, normalsB, trisA, trisB); partA = BuildMesh(vertsA, trisA, uvsA, normalsA); partB = BuildMesh(vertsB, trisB, uvsB, normalsB); } }

逻辑说明:这边把源 Mesh 的所有属性全部展开为 List,方便在切割过程中动态添加。out 参数 partA 和 partB 返回两个独立的新 Mesh,源 Mesh 保持不变,这样调用方可以决定是替换原物体还是生成两个新物体。这一步里头没有多余的高深算法,就是一个数据搬运和重新组织的过程。

3.2 核心三角拆分:一个三角形变成零到两个三角形

接下来是核心函数 ProcessTriangles 的细节。对每个三角形,取三个顶点到切割平面的距离,然后根据符号分布做分支处理。常见做法是把距离转成 1 或 -1,用加法判断:

for (int i = 0; i < triangles.Length; i += 3) { Vector3 v0 = source.vertices[triangles[i]]; Vector3 v1 = source.vertices[triangles[i + 1]]; Vector3 v2 = source.vertices[triangles[i + 2]]; float d0 = plane.GetDistanceToPoint(v0); float d1 = plane.GetDistanceToPoint(v1); float d2 = plane.GetDistanceToPoint(v2); int side0 = d0 >= 0 ? 1 : -1; int side1 = d1 >= 0 ? 1 : -1; int side2 = d2 >= 0 ? 1 : -1; int sideSum = side0 + side1 + side2; if (sideSum == 3 || sideSum == -3) { // 整个三角形在平面一侧,直接归入对应列表 } else if (sideSum == 1 || sideSum == -1) { // 三角形被平面穿过,拆成一个三角形 } else if (sideSum == 0) { // 三角形被平面穿过,拆成两个三角形 } }

逻辑说明:sideSum 的阈值是判断的关键。全在平面同一侧时 sideSum 为 3 或 -3,直接搬走。sideSum 为 0 时,一个顶点在一侧,另外两个在另一侧,需要拆成两个三角形。sideSum 为 1 或 -1 时,两个顶点在一个方向、一个顶点在另一个方向,只需拆成一个三角形。这里最难的是“把三角形拆成两个”的分配细节:你必须保证新三角形的顶点顺序是顺时针的,否则法线会反向,切出来的模型会黑一半。我通常用向量叉积验证朝向,遇到反向就交换三角形索引的前两位。

3.3 填补切面空洞:收集交点,按角度排序,扇形三角剖分

三角形拆完以后,切面上会留下一个多边形缺口。如果不补洞,Mesh 就是镂空的,看起来像一张被撕开的纸。补洞的常见做法是:把所有新生成的交点收集起来,按它们在切平面上的 2D 坐标做角度排序,然后以第一个点为基准做扇形三角剖分。

List<Vector3> borderPoints = new List<Vector3>(); // borderPoints 已经收集了所有新交点 Vector3 center = Vector3.zero; foreach (Vector3 p in borderPoints) center += p; center /= borderPoints.Count; borderPoints.Sort((a, b) => { Vector2 da = new Vector2(a.x - center.x, a.z - center.z); Vector2 db = new Vector2(b.x - center.x, b.z - center.z); return Mathf.Atan2(da.y, da.x).CompareTo(Mathf.Atan2(db.y, db.x)); }); for (int i = 1; i < borderPoints.Count - 1; i++) { trisA.Add(borderPoints[0].vertexIndex); trisA.Add(borderPoints[i].vertexIndex); trisA.Add(borderPoints[i + 1].vertexIndex); }

逻辑说明:排序时用投影到切割平面上的 2D 坐标计算 atan2,得到的角度从小到大排列,这样三角剖分不会出现交叉缠绕。代码里取的是 x 和 z,但具体取哪两个轴取决于你的切割平面方向。比如切割平面是水平向上的,就应该用 x 和 z;竖直切割则用 x 和 y。这个细节错了,剖分照样会绕成一团。

3.4 三个必调参数:容差、法线重算和内部材质

切割脚本写完后,真正影响效果的只有几个参数。你不需要过度调参,但下面三个必须是可控的。

第一是顶点容差。切割计算交点时,浮点误差会带来微小的缝隙,所以通常用一个容差值把距离极近的顶点焊接起来。常见设 0.001f,太小在移动端会有缝隙,太大则会把相邻三角面也焊在一起。第二是重新计算法线。源 Mesh 的法线经过插值以后只能保证平滑过渡,但切面方向完全变了,必须调用 mesh.RecalculateNormals() 才能让切开的边缘获得正确的光照。第三是内部材质。补出来的那部分三角形通常没有贴图,需要给 MeshRenderer 设置多子材质,把切面所在的 submesh 指向一个单独的“剖面材质”,常见做法是给切面生成一个基础色贴图或者直接用半透明材质。

Mesh newMesh = new Mesh(); newMesh.vertices = vertexList.ToArray(); newMesh.triangles = triangleList.ToArray(); newMesh.uv = uvList.ToArray(); newMesh.normals = normalList.ToArray(); newMesh.RecalculateNormals(); newMesh.RecalculateBounds();

逻辑说明:RecalculateBounds 同样重要,因为切割后 Mesh 的包围盒如果没有更新,物体可能会被视锥裁剪掉,表现就是切完以后一块消失。写完代码后,先把 RecaluclateNormals 和 RecalculateBounds 都调用,确认物体能正常显示再谈别的。

4. Unity 切割模型避坑指南:五处最容易翻车的 Mesh 细节

4.1 切完模型直接消失:三角形索引越界与包围盒未更新

现象:切割脚本执行完,原物体不见,两块新的也都不渲染,场景里什么都没有。

原因:最常见的两个,一是三角形索引数组里出现了来源不明的编号,导致顶点引用越界,Mesh 被 Unity 判定为非法数据;二是新 Mesh 生成后没有调用 RecalculateBounds,包围盒是空的,渲染系统直接把物体淘汰掉。

解决:在 BuildMesh 函数里强制调用 newMesh.RecalculateBounds(),同时打印切完后的顶点数量和三角形数量,排查triangles[tri]是否始终小于 vertices.Length。我习惯在每个三角形索引写入时做一个 Validate 函数,一旦发现越界就中断并输出调试信息,能节省很多排查时间。

4.2 切出来的边缘又黑又脏:法线和 UV 没有同步插值

现象:切割面显示为黑色,或切开处贴图被拉伸得像泼了油漆。

原因:新生成的交点只有位置被正确计算,UV 和法线还是默认值,导致光照明暗错乱,贴图坐标出错。

解决:在生成新交点时同步对 float3 类型的 UV 和法线做线性插值。检查方法很简单,在 Scene 视图开启 Mesh 显示,看切面法线是否垂直于切割平面。做水果忍者效果时,切面通常要朝向玩家,这时可以把切面三角形的法线全部强制指向某个固定方向,而不是用插值结果,效果会更可控。

4.3 物理碰撞体没有跟着切:只有 MeshFilter 动了,Collider 还停在原处

现象:切割后,两个新的可视模型出现在场景里,但玩家走过去发现仍被一个看不到的边界挡住,甚至子弹打在空气上。

原因:切的是 MeshFilter,但 MeshCollider 挂的还是旧的那份,Unity 不会自动帮你把碰撞体积拆开。

解决:切完以后,用生成的 partA 和 partB 各创建一个新的 MeshCollider,把 sharedMesh 赋给对应 collider。注意,切完后两个物体的 MeshCollider 需要设置 convex = true,否则 Unity 无法为拆分后的复杂形状生成正确的物理碰撞。整个过程是这样的:partA.gameObject.AddComponent<MeshCollider>().sharedMesh = partA,然后跑一遍物理模拟,确认没有穿透。

4.4 顶点数超过 65535 后模型直接花掉:16 位索引溢出的边界

现象:切一个复杂的场景模型,比如雕像,切到后面模型开始出现诡异的三角形乱飞,顶点位置完全不对。

原因:旧版本 Unity 的 Mesh 默认使用 16 位索引,顶点数超过 65535 以后索引不再生效,三角形会引用到错误的位置。

解决:在生成 Mesh 前先判断顶点数量,如果超过 65000,则设置newMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;。这个改动对移动端性能有一定影响,所以只在大模型或多次切割后顶点数必然会超过边界时启用。

4.5 切一次卡一下,GC 压力巨大:临时 List 不知道该释放

现象:切割函数执行时 FPS 掉到个位数,执行完以后内存占用不断上涨。

原因:切割过程中创建的 List、Vector3、Mesh 对象没有被及时释放,部分旧 Mesh 还挂在场景里没有被销毁。

解决:用对象池管理临时 List,切割完成后清空容量并缓存;旧 Mesh 用 Destroy 销毁,别等着 GC 自动回收。用 Profiler 看 GC Alloc,每个顶点插值至少会产生几个临时对象,每帧切 100 个顶点就是几百次 Alloc,能压多少是多少。

5. 进阶技巧:让切割效果从“能跑”变成“经得起玩”——验证与内部质感

5.1 给切面加上“果肉”材质,用第二副贴图映射剖面纹理

切面默认是纯色的,玩起来不真实。常见做法是给 Mesh 生成第二个 submesh,把所有切面三角形单独存到一个索引数组里,然后给 MeshRenderer 的 materials 数组设置两个材质,index 0 是原模型材质,index 1 是剖面材质。剖面的 UV 可以直接用切割平面的投影坐标计算,也可以直接用世界坐标映射到一张程序化生成的纹理贴图上。

MeshRenderer renderer = partObject.GetComponent<MeshRenderer>(); Material originalMat = renderer.material; Material cutMat = new Material(Shader.Find("Standard")); cutMat.color = new Color(1f, 0.85f, 0.7f); // 类似木头或石头的剖面偏色 renderer.materials = new Material[] { originalMat, cutMat };

逻辑说明:剖面材质的颜色偏暖通常更有“破坏感”,同时把切面的三角形索引放到 subMesh 的第二个位置。这个过程相当于让 Unity 在渲染时把同一物体的两部分套用不同材质,和 UI 分离是两回事,注意 subMesh 的索引顺序不能乱。

5.2 三种必做的压力测试:转角度、反复切、重复合并

切割功能写完之后一定要压测,不然上线你就等着翻车。第一个压测是同一物体连续切 100 次,每次换一个切割平面角度,看最终 Mesh 的三角形数量是否爆掉,FPS 是否维持在可接受范围。第二个压测是极端角度:切割平面和物体表面近乎平行的时候,交点插值会变得极度敏感,很容易出现退化三角形。第三个压测是验证切完的两块能不能拼回原物体的包围盒,用来避免出现顶点漂移或坐标偏移。

5.3 我的最终建议:先做平面切,别一上来就做“完全随机破坏”

如果你是从零开始做 unity切割模型案例,我会强烈建议先把固定平面切割跑通,无论什么角度割下去都不会出问题,再去想随机破坏、多段切割和碎片飞溅。我自己的项目里,前期贪快直接跳到了随机切割,结果后期百分之八十的时间都在处理因为前期地基没打牢导致的 Mesh 索引错乱和数据泄漏。一次把一个基础行为做稳,后面所有效果都是在同一个框架上加料,而不是无限返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询