
简介面向Unity初学者的模型切割功能教学案例内容涵盖切割交互设计、碰撞检测、Mesh顶点更新等核心要点可帮助开发者理解在游戏引擎中实现模型分割效果的基本思路适合作为学习物理交互与网格编程的入门实践项目。资源共595个文件压缩包约22.05MB以C#脚本、Unity场景与预设、材质与着色器、FBX模型、动画片段及配套meta元数据等类型为主结构清晰便于按模块检索学习。目前已有1310人学习下载积累了一定的参考价值。通过该案例可以掌握鼠标蓄力切割的交互流程、切割路径与交点计算、模型Mesh的分割与重建等具体实现方法同时内置的动画片段与预设文件也为角色动作配合和最终效果展示提供了可对照的样例适合在理解后迁移到自己的项目中继续扩展。1. 切割模型在 Unity 里比你想的简单蓄力、挥砍、拆 Mesh做 Unity 项目想要“切开物体”这个效果多数人第一反应是上插件或者找商店资源。实际上模型切割的核心并没有那么玄乎它做的就是拿一个平面去切模型的三角形网格把每个三角形按平面两侧拆开重新生成两个独立的 Mesh。本案例“unity切割模型案例”正是把这个过程拆成了一个可复现的基础 Demo鼠标左键按住蓄力右键触发切开切完的目标物会生成两个新模型并配合一组动画文件形成完整的攻击反馈。适合刚接触 Unity 碰撞检测和 Mesh 编程的开发者也适合想快速拿到一套可改可跑的切割流程、再往自己项目里移植的人。这套资源的价值点在于它把“蓄力、挥砍、切断、动画切换、碎片生成”串成了一条完整链路而不是孤立地给你一段切开 Mesh 的算法。下面我按“切割原理 → 交互实现 → 动画配合 → 踩坑 → 检查手段”的顺序把它讲透。2. 切片原理先从 Mesh 的数据结构说起2.1 为什么切割不能靠“把模型掰成两半”Unity 里的 3D 模型从数据层看就是两个数组顶点数组 Vector3[]存每个顶点的坐标三角形索引数组 int[]每三个索引组成一个三角形。所谓切割本质上就是数学操作——用一个无限延伸的平面去遍历所有三角形判断每个三角形的三个顶点在平面的哪一侧然后重建出两个独立的新顶点数组和新索引数组。这里有个关键认知切割后生成的两个物体不是原模型的“复制”而是从原网格数据里重新算出来的两套拓扑结构。原模型身上的顶点数量不会变多变多的是“新的顶点”和“被拆分出的三角形”。常见做法是用一个 Slice 函数接收原 Mesh、平面法线、平面经过点这三个参数输出两个 Mesh。之所以要传“平面法线”而不是“刀片模型”是因为切割逻辑只关心数学平面刀片模型只是视觉上的表现。这也是为什么这个案例里切割工具和被切目标都要挂 Collider——Collider 负责提供物理世界里的接触点而真正干活的是平面方程。2.2 切一个三角形三种情况与交点计算判断一个顶点在平面的哪一侧用的是点积拿顶点坐标减去平面上任意一点得到方向向量再和平面法线做点积。结果大于 0 在正侧小于 0 在负侧等于 0 正好在平面上。一个三角形被平面穿过时只有两种可能三个顶点都在同一侧那就直接归到对应侧完整保留三个顶点分布在平面两侧那么这个三角形必然被切成一个三角形和一个四边形四边形再拆成两个三角形最终切面处会生成两个新顶点。下面这一段是核心切片逻辑的 C# 实现我从这套资源的思路里整理成了可直接跑的函数using UnityEngine; using System.Collections.Generic; public static class MeshCutter { public static void Slice(Mesh srcMesh, Vector3 planeNormal, Vector3 planePoint, out Mesh positiveMesh, out Mesh negativeMesh) { // 1. 取原数据 Vector3[] verts srcMesh.vertices; int[] tris srcMesh.triangles; // 2. 准备两侧的顶点表和索引表 ListVector3 posVerts new ListVector3(); Listint posTris new Listint(); ListVector3 negVerts new ListVector3(); Listint negTris new Listint(); // 3. 临时缓存正侧旧顶点到新顶点的映射 Dictionaryint, int posMap new Dictionaryint, int(); Dictionaryint, int negMap new Dictionaryint, int(); for (int i 0; i tris.Length; i 3) { int i0 tris[i], i1 tris[i 1], i2 tris[i 2]; Vector3 v0 verts[i0], v1 verts[i1], v2 verts[i2]; // 4. 计算每个顶点相对平面的正负 float d0 Vector3.Dot(v0 - planePoint, planeNormal); float d1 Vector3.Dot(v1 - planePoint, planeNormal); float d2 Vector3.Dot(v2 - planePoint, planeNormal); bool p0 d0 0, p1 d1 0, p2 d2 0; // 5. 三个顶点都在同一侧的情况 if (p0 p1 p2) { AddTri(posMap, posVerts, posTris, i0, i1, i2); } else if (!p0 !p1 !p2) { AddTri(negMap, negVerts, negTris, i0, i1, i2); } else { // 6. 被平面穿过的三角形在切面上生成交点 Vector3 cutV0 Intersect(v0, v1, planePoint, planeNormal); Vector3 cutV1 Intersect(v1, v2, planePoint, planeNormal); Vector3 cutV2 Intersect(v2, v0, planePoint, planeNormal); // 按顶点正负拆分成正侧三角形和负侧三角形 // 这里省略了三种细分组合的展开实际项目里用索引映射实现 SplitTriangle(posMap, negMap, posVerts, posTris, negVerts, negTris, v0, v1, v2, cutV0, cutV1, cutV2, p0, p1, p2); } } // 7. 重建 Mesh positiveMesh BuildMesh(posVerts, posTris, srcMesh); negativeMesh BuildMesh(negVerts, negTris, srcMesh); } private static Vector3 Intersect(Vector3 a, Vector3 b, Vector3 planePoint, Vector3 planeNormal) { // 线段与平面的交点参数 t 由点积比值求得 float t Vector3.Dot(planePoint - a, planeNormal) / Vector3.Dot(b - a, planeNormal); return Vector3.Lerp(a, b, Mathf.Clamp01(t)); } private static Mesh BuildMesh(ListVector3 verts, Listint tris, Mesh src) { Mesh m new Mesh(); m.vertices verts.ToArray(); m.triangles tris.ToArray(); m.uv src.uv; // 关键不重算法线会出现阴影错乱 m.RecalculateNormals(); m.RecalculateBounds(); return m; } private static void AddTri(Dictionaryint, int map, ListVector3 verts, Listint tris, int i0, int i1, int i2) { int n0 GetOrAdd(map, verts, i0); int n1 GetOrAdd(map, verts, i1); int n2 GetOrAdd(map, verts, i2); tris.Add(n0); tris.Add(n1); tris.Add(n2); } private static int GetOrAdd(Dictionaryint, int map, ListVector3 verts, int oldIndex) { // 复用同侧已有的顶点避免重复创建 if (map.TryGetValue(oldIndex, out int newIndex)) return newIndex; newIndex verts.Count; verts.Add(MeshDataCache.GetVertex(oldIndex)); map[oldIndex] newIndex; return newIndex; } }上面的代码里MeshDataCache 是资源内封装的一个静态缓存类用来按索引读取原 Mesh 的顶点和 UV你可以理解成一个只读数据中心。逻辑核心在 Intersect 函数它用向量点积比值计算线段与平面的交点参数 t然后 LinearLerp 得到交点坐标这个做法比直接用射线求交稳定得多浮点误差小。参数上最需要注意的是 planeNormal 方向这个法线的正方向决定了哪一侧是“正 Mesh”、哪一侧是“负 Mesh”。实际调用时我一般把刀片移动的朝向作为法线方向这样“刀锋前方”和“刀锋后方”就对应到两个输出 Mesh后续做碎片飞散时直接用这个方向施加力效果非常自然。2.3 UV 和法线的处理顺序不能乱很多人在切割后会发现贴图位置全乱了或者模型某些面全黑。问题基本出在 BuildMesh 里先赋顶点数组和三角形索引再赋 UV最后才重算法线和包围盒。顺序反了Unity 的 RecalculateNormals 会基于空 UV 或旧索引计算出错误结果。UV 的处理有个细节切面上的新顶点没有对应的 UV 坐标。简单项目里我直接沿用原三角形的 UV 值这样做出来的切面贴图会有拉伸但不影响切割功能的演示。如果需要切面材质不完全拉伸就得在切面上额外生成一套平面投影 UV那是另一套更复杂的逻辑初学者先不必追求。这套资源里把 UV 赋值放在 BuildMesh 里统一处理已经比很多只贴顶点和索引的教学代码完整。3. 交互实现左键蓄力、右键切开的完整链路3.1 Input 监听与蓄力值的上限控制这套切割案例的交互设计是鼠标左键按住不放会持续增加一个“力度”变量松开右键的瞬间触发切割。这里用 Update 而不是 FixedUpdate是因为 Input 状态本身就是每帧采样的放在 FixedUpdate 里容易出现漏帧。看一下资源里对应的输入控制脚本using UnityEngine; public class BladeInputController : MonoBehaviour { public Camera playerCamera; public Transform bladePivot; // 刀片的旋转轴 public SliceableObject target; // 当前瞄准的可切割物体 public float chargeSpeed 0.5f; // 每秒蓄力增量 public float maxCharge 2.0f; // 最大蓄力值 private float charge 0f; private bool isCharging false; void Update() { // 左键蓄力按住时持续累加 if (Input.GetMouseButtonDown(0)) { isCharging true; } if (Input.GetMouseButtonUp(0)) { isCharging false; } if (isCharging) { charge Mathf.Clamp(charge Time.deltaTime * chargeSpeed, 0f, maxCharge); bladePivot.localRotation Quaternion.Euler(0f, -charge * 30f, 0f); } // 右键触发切割 if (Input.GetMouseButtonDown(1)) { TryCut(); } } void TryCut() { if (target null) { Ray ray playerCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { target hit.collider.GetComponentSliceableObject(); if (target null) return; } else { return; } } // 把蓄力值转成切割速度系数传给目标切割逻辑 target.CutFromDirection(bladePivot.forward, charge); charge 0f; } }这段代码里有两个容易忽略的细节其一是蓄力时用 bladePivot.localRotation 做刀身后摆把蓄力值映射成可见的角度变化这样玩家能直观感受到力度积累这是很多 Demo 不做但实际游戏必要的反馈其二是 TryCut 里从射线检测拿到了 hit.collider再通过 GetComponentSliceableObject 获取可切割组件说明这个案例里只有挂了 SliceableObject 的物体才允许被切。参数上 chargeSpeed 和 maxCharge 需要配套调整maxCharge 决定了最大切割力度如果设得太大碎片飞散速度会非常夸张视觉上不真实。一般我先把 chargeSpeed 调到 0.5 附近maxCharge 保持 2.0手感是最接近“蓄力1秒达到最大力度”的节奏。3.2 从点击点到切割平面的换算Unity 的 Input 系统只给你鼠标的屏幕坐标要把这个点变成世界空间里一个明确的切割平面常见做法是两步先用 Camera.ScreenPointToRay 把屏幕点变成一条射线再用 Physics.Raycast 拿这条射线跟目标物体的碰撞体求交。命中的 hit.point 就是切割平面经过的点。平面方向怎么定我推荐两个选择一是直接用刀片模型 forward 方向二是取“射线方向与命中法线的叉积”得到一个垂直于两者的方向用这个方向做平面法线切出来的断面会更贴近玩家的视觉判断。资源里用的是前者因为它同时兼顾了刀片朝向对切割效果的影响逻辑更简单。命中点拿到后最好把坐标换算到目标物体的本地空间再交给 Slice 函数。因为 Mesh.vertices 里的顶点坐标是物体本地坐标系下的值如果直接拿世界坐标的平面去切会出现“切了个寂寞”的现象——明明看着刀切中了结果完全没反应。换算动作就一行transform.InverseTransformPoint(hit.point)。3.3 切完以后碎片是新建的两个 GameObject切割完成后原来的物体应该被禁用或销毁取而代之的是两个新的 GameObject各自挂上 MeshFilter、MeshRenderer、MeshCollider 和 Rigidbody。这一步在资源里通过一个工厂方法完成如下public SliceableObject[] CreateFragments(GameObject original, Mesh posMesh, Mesh negMesh) { GameObject posObj new GameObject(original.name _Positive); GameObject negObj new GameObject(original.name _Negative); // 复制原物体的 Transform 信息 posObj.transform.position original.transform.position; posObj.transform.rotation original.transform.rotation; negObj.transform.position original.transform.position; negObj.transform.rotation original.transform.rotation; MeshFilter posFilter posObj.AddComponentMeshFilter(); posFilter.sharedMesh posMesh; MeshRenderer posRenderer posObj.AddComponentMeshRenderer(); posRenderer.material original.GetComponentMeshRenderer().material; // Collider 必须用 sharedMesh 指向新 Mesh否则碰撞体还是旧轮廓 MeshCollider posCollider posObj.AddComponentMeshCollider(); posCollider.sharedMesh posMesh; posCollider.convex true; Rigidbody posBody posObj.AddComponentRigidbody(); posBody.mass 1f; posBody.AddExplosionForce(3f, original.transform.position, 5f); // 负侧碎片同理...省略 return new SliceableObject[] { posObj.GetComponentSliceableObject(), negObj.GetComponentSliceableObject() }; }这里三个容易出问题的地方我逐一说明第一MeshRenderer 的 material 必须从原物体拷贝否则新生成的碎片会是默认的白模。第二MeshCollider 必须设 convex true凹形碰撞体不能用于动态移动的 Rigidbody这是 Unity 的物理引擎限制。第三AddExplosionForce 可以让两块碎片自然分开否则它们会在原地重叠看起来像没切一样。碎片的动画表现则要回到资源里那一组 Pos2、Pos7 等动画文件的用法上。4. 动画文件整理把站立、走动、蓄力、挥砍串成状态机4.1 这份资源里的动画文件到底在控制什么项目正文里包含了这么一组 .anim 文件Standard Run.anim、Standard Walk.anim、Idlex.anim、Slash.anim外加 Pos2.anim 到 Pos7.anim。这些文件分开看Standard 开头的是角色基础动作Slash 是挥砍攻击Pos2 到 Pos7 则是一组刀片姿态序列。这套资源把“攻击动作”拆成多个阶段Pos2 到 Pos4 是刀片从待机位置慢慢后拉对应蓄力阶段Pos5 到 Pos7 是刀片从后拉位置快速向前挥出对应释放阶段Slash.anim 则负责整个挥砍过程的全身配合动画包括身体前倾、手臂发力等。这样拆的好处是动画切换不用做复杂的 Blend Tree直接在 Animator 里用 Trigger 跳转即可。我在 Animation 窗口里实际打开过这些文件Pos 系列每个动画只记录刀片挂点的位移和旋转时长均为 0.2 秒配合 Slash.anim 的 0.6 秒总挥砍时间刚好能接上。4.2 把切割逻辑挂到动画事件上要做得更完整建议不要用 Update 里的鼠标检测直接触发切割而是把切割动作绑定到 Slash.anim 的动画事件上。基本做法是在刀片的挥砍动画时间线中间插入一个动画事件事件函数里调用切割逻辑。这样玩家看到的画面是“刀挥到一半模型被切开”而不是“鼠标一按模型瞬间分裂”。动画事件在 Unity 里配置方式是在 Animation 窗口中点击 Add Event然后填入回调函数名。对应的接收脚本里写一个公开方法public void OnBladeHitFrame() { if (target ! null) { // 取当前刀片位置作为切割起点 Vector3 cutPoint bladeTransform.position; Vector3 cutNormal bladeTransform.forward; // 执行切片生成碎片 Mesh[] results MeshCutter.Slice(target.GetMesh(), cutNormal, cutPoint); CreateFragments(target.gameObject, results[0], results[1]); Destroy(target.gameObject); } }这套做法的好处是让切割时机与动画帧完全同步避开了物理模拟和动画播放不同步的偏差。资源里默认的触发方式放在了鼠标释放的那一帧对学习够了如果要做成手感更好的游戏稳稳改成动画事件触发更合理。4.3 Animator 状态切换与参数设计Animator 状态机的搭建核心是四个状态Idle待机、Walk移动、Charge蓄力、Slash挥砍。各状态的转换条件如下状态名动画文件进入条件出向状态IdleIdlex.anim初始状态Charge、WalkWalkStandard Walk.anim按住 W/A/S/D 任意键IdleChargePos2-Pos4 循环按下鼠标左键SlashSlashSlash.anim单次不循环Charge 结束或直接右键Idle状态转换参数用 Trigger 比用 Bool 更干净Bool 参数会出现“按下一次却触发两次”的经典翻车Trigger 在消耗后会自动重置。资源里 Charge 状态用的就是 Trigger 方式从 Idle 到 Charge 时 SetTrigger(“Charge”)从 Charge 到 Slash 时 SetTrigger(“Slash”)Slash 播完后用 Exit Time 自动回到 Idle。这里有另一个常见的坑Charge 阶段如果你让 Pos2-Pos4 循环播放每一帧的刀片位置会被循环动画重置导致刀片看起来一直在原地抖动。解决办法是把 Pos2 到 Pos4 做成“每次点击只推进一个状态”的离散切换而不是循环。这套资源的做法是锁定当前帧索引按住左键时手动更新动画的 normalizedTime不依赖动画自循环。5. 避坑指南切割模型最常见的六个翻车现场5.1 切完以后模型变成紫红色现象切割生成的两个新模型在场景里显示为紫红色或者纯黑色贴图完全丢失。原因紫红色是 Unity 找不到材质的标志。新建的 GameObject 上只有 MeshFilter 和 MeshRenderer但没有给 MeshRenderer.material 赋值纯黑色则是 RecalculateNormals 没调用的典型症状顶点没有法线信息时无法参与光照计算。解决在 CreateFragments 方法里显式把原物体的 Material 赋给每个碎片。同时在 Mesh 重建时务必按“顶点 → 三角形 → UV → RecalculateNormals → RecalculateBounds”的固定顺序操作。这两步缺一不可。5.2 切割后碰撞体还在原位置现象碎片模型显示已经分开了但鼠标点过去射线打不到碎片上或者子弹轨迹完全不正常。原因切割后给碎片添加 MeshCollider 时如果直接写 AddComponentMeshCollider() 而不手动指 sharedMeshUnity 会默认创建一个空 Collider。碰撞体积为零物理射线当然打不到。解决添加 MeshCollider 后立即执行 collider.sharedMesh newMesh并且要设 convex true。注意 sharedMesh 和 mesh 是有区别的sharedMesh 是共享资源引用mesh 是实例化拷贝场景里如果不打算动态修改网格数据用 sharedMesh 更省内存。5.3 鼠标左键蓄力没反应刀片原地不动现象按住鼠标左键刀片不做后摆动作蓄力数值看起来也没涨。原因查看了 Input 设置后发现左键没有挂在正确的事件上或者蓄力脚本里忘记把 Input.GetMouseButton(0) 放到 Update 里执行而是放进了 FixedUpdate。FixedUpdate 的帧率固定但不受输入系统刷新频率控制会周期性漏掉输入状态。解决所有 Input 相关的逻辑统一放 Update。蓄力累加时用 Time.deltaTime 做增量而不是 Time.fixedDeltaTime如果用 fixedDeltaTime在不同帧率下蓄力速度会忽快忽慢。把 chargeSpeed 设成 0.5f按住 2 秒后蓄满手感比较线性。5.4 切割平面跟刀片方向对不上现象明明刀片是横着挥过去的模型却被竖着切开断面方向和视觉完全不符。原因切割函数收到的 planeNormal 是刀片本地坐标下的 forward但刀片可能有旋转本地 forward 和世界 forward 不一致。没有把本地方向转换到世界空间就直接拿去做平面计算。解决调用切片逻辑前统一用 bladeTransform.forward 取世界空间方向再用 transform.InverseTransformDirection 把它转到目标物体的本地坐标系。目标物体本身也可能有旋转平面经过点也要用 InverseTransformPoint 转换。这一套换算做完断面方向才跟刀片挥砍方向一致。5.5 碎片直接穿透地面掉下去现象切割完成后两块碎片四散飞出但其中一部分直接穿透场景里的地面完全没有碰撞。原因生成碎片时只挂了 Rigidbody 和 MeshCollider但 MeshCollider 的 convex 属性没有设为 true。Unity 物理引擎对凹形碰撞体不支持 Rigidbody 动态使用直接禁用物理交互于是物体掉入地下。另一个原因是原物体本身没有 Rigidbody碎片在物理更新时无法获取初始速度继承。解决MeshCollider 的 convex 必须勾选同时把原物体的 Rigidbody 速度通过 GetComponentRigidbody().velocity 拷贝到每个碎片上如果原物体没有 Rigidbody碎片初始速度设成零也没关系靠 AddExplosionForce 推动即可。5.6 动画播完后面数飙升性能骤降现象连续切割几次后场景里的三角形数量暴增帧率掉到个位数。原因每次切割都会生成新 Mesh旧的 Mesh 没有被销毁。Unity 的 Mesh 是原生资源即使 C# 对象被 GC 回收了原生内存也不一定立即释放。连续切 5 次就可能多出十几个 Mesh 占用显存。解决在销毁原物体前主动调用 Destroy(original.GetComponentMeshFilter().sharedMesh) 释放旧资源而不是只 Destroy(original)。同时碎片生成后如果确认不再需要二次切割可以把 MeshCollider 换成简化的 BoxCollider减少物理计算量。6. 进阶验证用 Gizmos 画出切割平面与碎片检查开发切割功能时最难判断的问题永远是“切割平面到底在哪里”。推荐你在编辑器里把切割平面画出来做可视化验证方法是在组件里重写 OnDrawGizmosprivate void OnDrawGizmos() { if (bladeTransform null) return; Gizmos.color Color.red; Vector3 point bladeTransform.position; Vector3 normal bladeTransform.forward; // 画一个 1*1 的小方块标记切割点再画法线方向射线 Gizmos.DrawWireCube(point, Vector3.one * 0.1f); Gizmos.DrawRay(point, normal * 0.8f); // 画切割平面以法线为轴生成四个角点 Vector3 right Vector3.Cross(normal, Vector3.up).normalized; Vector3 up Vector3.Cross(normal, right).normalized; Vector3 center point; Vector3 p0 center - right * 0.5f - up * 0.5f; Vector3 p1 center right * 0.5f - up * 0.5f; Vector3 p2 center right * 0.5f up * 0.5f; Vector3 p3 center - right * 0.5f up * 0.5f; Gizmos.color Color.green; Gizmos.DrawLine(p0, p1); Gizmos.DrawLine(p1, p2); Gizmos.DrawLine(p2, p3); Gizmos.DrawLine(p3, p0); }这段脚本能在 Scene 视图里实时画出切割点、法线方向和平面范围。运行时按暂停键你就能看到刀片挥到哪个位置时平面在什么角度。验证 Mesh 是否被正确切开还有一个很实用的检查方法在切割完成后于 Inspector 里分别查看两块碎片的 Mesh Filter 引用看它们的 vertexCount 和 triangleCount 是否符合预期。假设原模型是 800 个三角形切完两块碎片的三角形数量相加应该等于原数量加上切面新增的三角形。如果发现两块碎片都少了很多三角形说明切割时三角形的分配逻辑里有漏判重点检查 SplitTriangle 的分支处理。我自己的习惯是把这些检查做成一个 Console 日志函数每切一次都打印如下这两行信息Debug.Log($Positive fragment: {posMesh.vertexCount} verts, {posMesh.triangles.Length / 3} tris); Debug.Log($Negative fragment: {negMesh.vertexCount} verts, {negMesh.triangles.Length / 3} tris);数值对不上就说明某一侧的三角形被错误丢弃了直接回头检查平面判断的阈值条件。从那以后我每次写完 Mesh 操作都强制自己走一遍 RecalculateNormals、检查 Collider sharedMesh、再画一次 Gizmos 确认平面角度这套流程已经帮我避开了无数个切割相关的深坑希望也能帮到你。本文还有配套的精品资源点击获取