Unity运行时切割模型实现:Mesh数据处理与剖面补洞
2026/9/14 9:50:05 网站建设 项目流程

简介:这是一份面向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 里最常见的是verticestrianglesuvnormals四个数组。vertices存放顶点坐标,triangles按每三个整数一个三角形列出索引,uvnormals分别用于贴图和光照。切割模型案例中,所谓“把模型切开”,本质上是重排这套数组:每个三角形要么完整留在切割平面一侧,要么被平面切成两个三角形,再分给两侧。

默认情况下很多美术导出的模型并不封闭,可能有开口边、翻转面或者重复顶点。切割前如果不检查,切出来的碎片会出现“漏风”、背面缺失或阴影异常。针对“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 的uvnormals,因为纯几何重建后这些属性会丢失,光照会立刻变暗或出现硬边。

下面是常用的属性检查表:

属性切割模型中的作用预处理建议
vertices几何位置和求交输入检查 NaN、无穷值
triangles三角形拓扑索引不越界,最好统一起始点
normals光照方向切割后需要RecalculateNormals
uv纹理坐标原面可插值,剖面要单独生成
mesh.bounds包围盒用于剔除不在切面附近的三角形

这组检查做下来,剩下的工作就是纯几何:拿Plane去切三角形,再把切出来的两堆三角形和补洞面写成两个新 Mesh。

3. 三角形求交与两片网格重建的实现

3.1 遍历三角形并按平面分类:三种情况

遍历原triangles数组时,每个三角形三个顶点分别计算带符号距离dadbdc。如果三个值都大于等于 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索引,而是用两个List<Vector3>和两个List<int>重新生成两个 Mesh。每个跨界三角形经过 3.2 的求交后,会得到一组正面顶点和一组背面顶点,然后对它们做三角扇剖分。

private static void FanTriangulate( List<Vector3> pts, List<Vector3> verts, List<int> 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; List<Vector3> ring = new List<Vector3>(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); });

这里axisXaxisY是切面上两个互相垂直的方向,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.GetComponent<MeshFilter>(); Renderer renderer = source.GetComponent<Renderer>(); 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 切割后生成新物体,复制材质与碰撞体参数

碎片物体必须拥有独立的MeshFilterMeshRenderer,以及用于物理反馈的MeshColliderRigidbody。下面是一个很实用的创建函数:

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.AddComponent<MeshFilter>().sharedMesh = mesh; go.AddComponent<MeshRenderer>().sharedMaterial = material; MeshCollider collider = go.AddComponent<MeshCollider>(); collider.sharedMesh = mesh; collider.convex = true; Rigidbody body = go.AddComponent<Rigidbody>(); body.mass = 1f; body.interpolation = RigidbodyInterpolation.Interpolate; return go.GetComponent<MeshRenderer>(); }

这个函数把convex设置为 true,意味着 PhysX 会把碎片网格当作凸体参与碰撞检测,物理稳定性最好。但也要清楚:凹面物体切出来的碎片如果设置 convex=true,碰撞体形状会和视觉网格差异变大。你要做桥梁或 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近似较大碎片的包围盒。还要记得开启对象池,重复使用GameObjectMesh,避免 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 里算好的axisXaxisY

Vector2[] 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 的局部坐标方向查一遍。

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

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

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

立即咨询