news 2026/10/27 15:06:28

Unity3D代码创建三角面:Mesh顶点索引、绕序、法线与包围盒实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity3D代码创建三角面:Mesh顶点索引、绕序、法线与包围盒实战

场景里拖一个 Cube,转两下就能看到六个面,这件事太顺手了,顺手到不少人做了很久项目,都没认真翻开过 Unity3D 里 Mesh 组件到底存了什么。可一旦需求变成“动态生成一块地形”、“做一根会扭动的绳子”、“把一堆测点数据变成曲面”,内置几何体立马就不够用,这时候绕不开的就是从最原始的三角面开始,自己往 Mesh 里填数据。这篇就只干一件事:在 Unity3D 里用代码创建一个最简单的三角面,并把它渲染出来。听起来像个入门 Demo,但里面藏着绕序、法线、包围盒、索引格式这一连串会被反复绊倒的坑,后面做程序化建模、网格变形、运行时切割,全都建立在对这几个概念的正确理解上。适合刚接触 Mesh 的同学打地基,也适合写过一些生成器代码但总是“莫名其妙看不见东西”的同学回头补漏。

1. 三角面虽小,却是 Mesh 体系的地基

1.1 引擎只认三角形

显卡那套光栅化管线,最终处理的图元只有三种:点、线、三角形。所谓四边形、多边形、曲面,到了硬件这一层,统统被拆成一个个三角形。你在编辑器里看到的 Cube,展开看其实是 12 个三角面拼起来的;一个高模角色几十万面,本质也是几十万个三角形按顶点索引排列在一起。

这个事实带来一个很直接的推论:只要你能正确地把顶点位置和它们的连接关系喂给引擎,你就能造出任何形状。模型师在 DCC 软件里雕刻、拓扑、导出 FBX,本质是在产出一张三角形列表;我们在代码里手写顶点数组,本质是在做同一件事,只是没可视化的雕刻笔刷,全靠数学计算。

理解了这一点,再看 Unity3D 的 Mesh 类,就不会觉得它神秘。它就是一个容器,里面装着若干数组:顶点在哪、谁和谁连成面、面朝哪边、贴图怎么贴。剩下的工作由渲染管线和 Shader 完成。

从三角面开始练,最大的好处是变量最少。三个顶点、三个索引,任何一处写错,现象都很直观,排查路径也短。等你把这套东西彻底理顺,再去做几千个顶点的网格,出问题时至少知道该从哪几个方向去怀疑。

1.2 一张 Mesh 到底装了什么

新手容易把 Mesh 想成“模型文件”,其实它更接近一张结构化表格。核心的就下面这几项,我按重要程度排一下:

数据项作用缺失后的现象
vertices(顶点数组)定义所有顶点的局部坐标物体什么都不显示
triangles(索引数组)三个一组,声明哪些顶点连成面有顶点但无面,同样看不见
normals(法线)决定光照计算朝向面是黑的、光照错乱
uv决定贴图采样位置贴图糊成一片纯色或拉伸
bounds(包围盒)参与视锥剔除和射线检测物体在视野边缘突然消失
colors、tangents顶点色、切线空间一般不影响可见性

有一件事得说清楚:只有 vertices 和 triangles 是必须的,其它都能省,但省了就要承担对应后果。法线不写,Unity 不会自动补,除非你主动调用重算接口;包围盒不写,Unity 会用初始值,位置一偏移就会出问题。

我见过太多“代码看起来没问题但就是不显示”的情况,八成不是法线就是绕序,这两块后面会重点拆。

1.3 什么时候必须自己造 Mesh

不是所有需求都值得手写网格。判断标准很简单:如果内置几何体加缩放旋转就能表达,那就别造。用代码拼一个立方体除了练手没有实际收益。真正需要动手的场合,通常是这几类:

  • 数据驱动形状:从传感器、表格、接口拿到一批数值,要转成可视化的面或体;
  • 运行时变化:网格顶点位置每帧都在动,比如波浪、融化、布料感;
  • 批量差异化:成百上千个物体形状各不相同,用预制体加缩放做不出来;
  • 结构可见:调试用的线框、法线可视化、碰撞体轮廓;
  • 超低面数优化:只需要一个面片做投影、贴花、UI 特效。

这几类的共同点是“形状本身是逻辑的一部分”,而不是美术资产的一部分。把形状交给代码生成,好处是能参数化、能联动数据、能节省包体,坏处是调试成本高,看不到的东西全靠推。所以第一张网格越简单越好,一个三角面刚刚好。

注意:程序化生成的网格默认不会存进工程文件,运行时创建的对象在退出播放后就会消失。如果需要持久化,得走编辑器脚本把它存成资源,这一点在第 5 节会讲。

2. 动手前的三个关键概念

2.1 顶点数组与索引数组的分工

很多人第一次看到 triangles 数组时是懵的:明明只有三个顶点,为什么还要再写一个{0, 1, 2}?直接用顶点顺序连面不行吗?

不行的原因在于顶点复用。先看一个极端的例子:一个立方体有 8 个角,但它有 12 个三角面、总计 36 个“顶点位置”。如果按面依次写顶点,顶点数组就得有 36 项,其中大量是重复的同一个角。而用索引,顶点数组只要 8 项,索引数组写 36 个下标即可。显存占用差了好几倍,GPU 的顶点着色器调用次数也差好几倍。

所以这套设计的意图非常明确:顶点数组负责“有哪些点”,索引数组负责“怎么连”。点和连接关系解耦之后,同一批顶点能被多个面共享,也能在某些场合刻意拆开(比如需要硬边法线或不同 UV 时,同一个位置在数组里出现两次)。

索引数组还有两条硬约束需要记住:

  1. 长度必须是 3 的倍数,否则 Unity 会报错或直接忽略多余的部分;
  2. 每个元素必须落在顶点数组的有效下标范围内,越界会导致网格损坏,表现是渲染错乱甚至编辑器崩溃。

关于第二点我踩过一次:用循环生成网格时行列算错了一格,下标刚好超出顶点数一位,Unity 没有立刻报错,只是那个面变成一堆乱飞的三角形,排查了半天才定位到循环边界。

2.2 绕序决定正反面

这是最容易翻车的地方。同一个三角形,顶点顺序不同,正面朝向就完全不同。

Unity3D 使用左手坐标系,规则是:从正面看过去,三个顶点按顺时针排列时,这一面为正面;逆时针则为背面。默认材质下背面会被剔除(Cull Back),也就是不渲染,所以你看到的现象就是“三角面凭空消失”。

举个具体的例子。三个顶点分别放在(0,0,0)、(1,0,0)、(0,1,0),它们都在 XY 平面上。从 +Z 方向看向原点,这三个点的排列是逆时针。按上面的规则,这个三角面的正面朝向 -Z,从 +Z 看过去看到的是背面,会被剔除。

想让正面朝 +Z,索引顺序要写成{0, 2, 1}。这就是为什么很多人照着“0,1,2”写完发现什么都看不到——代码没毛病,就是顺序反了。

实操心得:判断绕序别靠脑补。我的做法是先写{0,1,2},看不见就把索引改成{0,2,1},一秒验证。虽然粗暴,但比盯着坐标算叉乘快得多。真要算,可以取两个边向量做叉乘,结果方向就是正面朝向,右手定则反过来推。

另外,绕序不仅影响可见性,也影响法线方向。RecalculateNormals是根据三角形的绕序来推算法线的,绕序反了,法线也就朝里了,即便你关掉背面剔除把面显示出来,光照也会是黑的。

2.3 法线与包围盒的隐性作用

法线是垂直于表面的单位向量,Shader 用它和光线方向做点积,算出这个面有多亮。你写RecalculateNormals(),Unity 会遍历所有三角形,算出每个面的法线,再按共享顶点做平均,得到平滑效果。

对单个三角面来说,法线就是它所在平面的垂线,方向由绕序决定。这里有个细节:对共享顶点做平均,会让相邻面看起来是圆滑过渡的。如果你的模型需要硬边(比如一个立方体),就不能让相邻面共享顶点,得把顶点拆开,让每个面拥有独立顶点,这样才能得到明确的棱角。这个技巧在后面做箱体、做切面时非常关键。

包围盒(Bounds)容易被忽略,它的作用是告诉引擎“这个网格占据的空间范围”。主相机做视锥剔除时,拿包围盒来判断要不要渲染;物理射线检测、Renderer.bounds也都依赖它。

关键点在于:修改顶点后必须重算包围盒。如果你先创建网格、调用RecalculateBounds,然后在运行时移动顶点位置却没有再次调用,包围盒还停留在旧位置。此时物体可能因为“包围盒不在视锥内”而被剔除,现象是走到某个角度突然消失,或者从摄像机边缘离开时整块不见了。我第一次遇到这个现象时以为是 Shader 的问题,查了很久。

3. 从零构建一个三角面的完整实操

3.1 场景与脚本准备

先做减法,把环境清干净:

  1. 新建一个 3D 项目,删掉默认场景里多余的对象,只留一个主相机;
  2. 把主相机位置设成(0, 0, -5),朝向(0, 0, 0),保证它从 +Z 方向看向原点;
  3. 创建一个空物体,命名随意,比如TriangleDemo;
  4. 给这个空物体挂一个脚本。

脚本里需要两个组件:MeshFilter负责持有网格数据,MeshRenderer负责用材质把它画出来。用[RequireComponent]特性自动挂上,避免手动遗漏。

这里顺带说一个常见误区:MeshFilter才是装数据的地方,MeshRenderer只管渲染。很多人删了 Renderer 发现物体不显示,第一反应是网格没了,其实是没渲染器。反过来删了 Filter,Renderer 还在但没有数据,也不显示。两者缺一不可。

注意:Unity 会为每个渲染物体自动创建MeshRenderer,但不会自动创建MeshFilter。如果只挂 Renderer,Profiler 里能看到渲染批次,屏幕上却是空的。

3.2 基础版本代码与逐段拆解

先上能跑的最小实现,代码不长,但每一行都有它的位置:

using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class SimpleTriangle : MonoBehaviour { void Start() { Mesh mesh = new Mesh(); mesh.name = "SimpleTriangle"; // 三个顶点,放在 XY 平面上 Vector3[] vertices = new Vector3[3]; vertices[0] = new Vector3(0f, 0f, 0f); vertices[1] = new Vector3(1f, 0f, 0f); vertices[2] = new Vector3(0f, 1f, 0f); // 索引顺序决定正反面,这里用顺时针让正面朝 +Z int[] triangles = new int[3] { 0, 2, 1 }; // UV,让贴图能正确采样 Vector2[] uvs = new Vector2[3]; uvs[0] = new Vector2(0f, 0f); uvs[1] = new Vector2(1f, 0f); uvs[2] = new Vector2(0f, 1f); mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uvs; mesh.RecalculateNormals(); mesh.RecalculateBounds(); GetComponent<MeshFilter>().mesh = mesh; } }

逐段解释一下。

创建 Mesh 实例:new Mesh()每次运行都会创建一个新的对象。在Start里创建没问题,但要是在Update里反复new,GC 压力会很明显。生产代码里更稳的做法是复用同一份网格,每次更新时调用mesh.Clear()再重新填数据。

顶点坐标:三个点放在单位尺度上,是为了方便观察。Unity 默认 1 单位约等于 1 米,这个三角面在场景里就是 1 米见方,相机在 5 米外看刚合适。

索引顺序:{0, 2, 1}而不是{0, 1, 2},理由在第 2 节讲过了,让正面朝向相机。这个顺序看着别扭,但写多了就习惯了。我一般在旁边留一行注释写明“顺时针面向 +Z”,下次打开不至于怀疑人生。

UV 数组:UV 是二维坐标,取值 0 到 1 表示贴图上的相对位置。(0,0)是左下角,(1,1)是右上角。三个顶点的 UV 和它们的空间位置一一对应,贴一张三角形状的图就能完整覆盖。如果 UV 数组长度和顶点数组长度不一致,Unity 会报错丢数据,这是硬性要求。

设置顺序:vertices必须最先赋值,因为triangles的合法性依赖于顶点数量。反过来先设triangles会直接报错。UV 和法线没有强制顺序,但建议统一成 vertices → triangles → uv → normals → bounds 这个流程,形成肌肉记忆。

法线与包围盒重算:RecalculateNormals必须在 triangles 设置之后调用,否则它算不出任何东西。RecalculateBounds同理,顶点改动后都要重算一次。

赋值给 Filter:GetComponent<MeshFilter>().mesh = mesh。这里的.mesh属性有个细节,读的时候会返回一份实例,写的时候会直接替换。如果想改共享资源,得用.sharedMesh。运行时动态生成的网格用.mesh是对的,避免污染工程里的资源文件。

3.3 材质配置与渲染验证

代码写完,点运行,如果材质没配,屏幕上大概率还是空的。

内置管线的默认材质是Default-Material,它接受光照,你需要有光源才能看清。快速验证的话有两种做法:

做法一:把材质换成 Unlit 类 Shader。URP 项目里新建材质,Shader 选Universal Render Pipeline/Unlit,颜色随便设个亮色。Unlit 不受光照影响,能立刻确认几何体是否存在。

做法二:保留默认材质,加一个方向光,角度调到正对三角面。这样还能顺便验证法线方向对不对——如果整块是黑的,说明法线朝反了。

验证渲染结果时,我通常按这个顺序检查:

  1. Play 模式下看 Hierarchy,对象是否存在,有没有被误删;
  2. 选中对象看 Inspector,MeshFilter 上是不是显示SimpleTriangle,顶点数是不是 3;
  3. Scene 视图切到线框模式,能不能看到那条三角边;
  4. Game 视图换角度,从 +Z 和 -Z 各看一次,确认只有一面可见。

第 3 步特别有用。Scene 视图里即使剔除掉了背面,也能看到网格的线框轮廓,这是判断“数据存在但渲染被剔除”最直接的手段。

提示:Scene 视图右上角有个小图标可以切换 Shaded / Wireframe,快捷键在 Windows 上是Ctrl + 数字系列。养成用手势切换的习惯,排查网格问题的效率会高很多。

3.4 改用 List 与 SetVertices 的进阶写法

数组版本够用,但每次生成都要申请新数组,反复创建会有分配开销。Unity 后来加了一批SetVertices、SetTriangles接口,接受List<T>,能复用内部缓冲区,写法也更灵活。

using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class SimpleTriangleList : MonoBehaviour { private Mesh _mesh; private readonly List<Vector3> _vertices = new List<Vector3>(3); private readonly List<int> _triangles = new List<int>(3); private readonly List<Vector2> _uvs = new List<Vector2>(3); void Start() { _mesh = new Mesh { name = "SimpleTriangleList" }; GetComponent<MeshFilter>().mesh = _mesh; Rebuild(); } private void Rebuild() { _mesh.Clear(); _vertices.Clear(); _triangles.Clear(); _uvs.Clear(); _vertices.Add(new Vector3(0f, 0f, 0f)); _vertices.Add(new Vector3(1f, 0f, 0f)); _vertices.Add(new Vector3(0f, 1f, 0f)); _triangles.Add(0); _triangles.Add(2); _triangles.Add(1); _uvs.Add(new Vector2(0f, 0f)); _uvs.Add(new Vector2(1f, 0f)); _uvs.Add(new Vector2(0f, 1f)); _mesh.SetVertices(_vertices); _mesh.SetTriangles(_triangles, 0); _mesh.SetUVs(0, _uvs); _mesh.RecalculateNormals(); _mesh.RecalculateBounds(); } }

几个值得说明的点:

List声明为只读字段,并在构造时预分配容量。三角面只有三个点,容量给 3 就够,避免扩容时的数组拷贝。这个习惯在做大网格(几万顶点)时能省下可观的分配。

_mesh.Clear()放在最前面。这个方法会清空所有顶点数据,但保留 Mesh 对象本身和它的材质引用。如果不调用 Clear 而直接重新SetVertices,旧数据会被覆盖,但如果新数据的顶点数比旧的少,残留的三角形索引可能会引用到不存在的顶点,导致报错。Clear 是最省心的做法。

SetTriangles的第二个参数是子网格索引,从 0 开始。单材质的网格永远传 0。多材质网格才需要拆成多个子网格,每个子网格对应一个材质槽。

SetUVs的第一个参数是通道号,也是 0 起。复杂效果可能用到通道 1、2,但基础贴图用 0 就够了。

这套写法好在结构清晰:数据的准备和提交分开,以后想做动态更新,只要改数据源再调Rebuild就行,不用重建整个 Mesh。

4. 排查实录:三角面不显示怎么办

4.1 先确认物体真的生成了

排查要从最外层往里收。很多“网格不显示”的问题,其实是脚本根本没执行。

第一步看 Hierarchy。Play 模式下,如果对象不在列表里,说明脚本没挂上、或者被别处销毁了、或者整个 GameObject 被 SetActive(false) 了。这种情况跟 Mesh 一点关系都没有。

第二步看 Console。Unity 里网格相关的问题有时会以警告形式出现,比如“Mesh.triangles is invalid”或者“Index buffer is too small”。这类提示要优先处理,别急着改逻辑。

第三步看 Inspector 上 MeshFilter 的显示。正常情况会显示网格名字和顶点数。如果显示None (Mesh),说明赋值那行没执行到,或者赋值给错对象了。如果显示的名字是Mesh这种默认名,说明赋值的是个临时对象,可能是写成了new Mesh()之后忘了保存引用。

我见过最隐蔽的一种情况:脚本挂在了子物体上,但相机对着的是父物体,看起来“什么都没生成”。实际是生成在别的地方了。这种情况用 Scene 视图拉远一点看全景,通常一眼就能发现。

4.2 逐层排查渲染链路

确认物体存在、网格有数据之后,问题基本集中在渲染链路上。按这个顺序过一遍,绝大多数问题能定位:

第一层:包围盒是否合理。选中对象,Scene 视图里有没有看到那个橙色的线框盒子?如果盒子在很远的地方,说明包围盒没重算。这时相机可能判定物体不在视锥内,直接不渲染。解决方法是在改顶点之后补一句RecalculateBounds()。

第二层:面是否被剔除。把相机绕到物体背面再看。如果一面有一面没有,那就是绕序问题,把索引顺序反转即可。也可以在材质里临时把 Cull 改成 Off 验证,如果两面都出来了,确诊无误。

第三层:材质是否正常。把材质换成 Unlit 纯色。如果出来了,说明是光照问题;如果还是不出来,往数据层查。

第四层:法线方向。如果换成受光材质后整块发黑,但轮廓能看到,那是法线朝里了。要么调绕序,要么在 Shader 里做双面法线处理。快速验证可以在临时材质上用 Unlit 对比。

第五层:缩放与层级。父物体如果被缩放成 0,子物体的网格也会消失。检查 Transform 的 Scale 三项有没有 0。这个坑非常经典,尤其是从代码里localScale = Vector3.zero之后忘了恢复。

第六层:渲染层级与相机。检查相机的 Culling Mask 有没有把这一层排除,检查对象的 Layer 设置。UI 相机和场景相机混用的时候特别容易出这种事。

4.3 高频问题速查表

把上面这些整理成表,出问题时对着过一遍,比漫无目的改代码快得多:

现象最可能的原因快速验证处理办法
Hierarchy 里没有对象脚本未执行或被销毁看 Console 有无报错检查挂载与执行顺序
有对象但 Mesh 显示 None赋值语句未执行断点或加日志检查 Filter 引用
Mesh 有名字但画面空白背面剔除从反方向观察反转索引顺序
只在特定角度消失包围盒未重算Scene 视图看橙色框补 RecalculateBounds
面可见但全黑法线朝向错误换 Unlit 材质对比调绕序或重算法线
贴图显示为纯色UV 缺失或全为 0Inspector 看 UV 通道补 SetUVs
大网格报索引越界顶点数超 65535看顶点计数切 UInt32 索引格式
编辑器报索引非法索引值超出顶点范围打印数组长度修正循环边界

实操心得:我给自己定了个规则,凡是生成网格的脚本,都在Rebuild末尾加一行日志,打印顶点数和三角形数。跑起来先看这两个数字对不对,能省掉一半的排查时间。数字对不上,问题在数据层;数字对得上但看不见,问题在渲染层。

5. 从单三角面延伸到实用网格

5.1 双面三角面的三种实现

一个三角面只有一面可见,这在很多场景下不够用:飘动的旗帜、双面显示的卡片、叶片、需要从任意角度观察的调试面板。解决方案有三种,各有取舍。

第一种:索引翻倍。在 triangles 数组里把同样的三个顶点再写一遍,顺序反过来:

int[] triangles = new int[6] { 0, 2, 1, // 正面朝 +Z 0, 1, 2 // 反面朝 -Z };

这种做法最省事,顶点数组不用动,只多了三个索引。缺点是两面共享同一批顶点,法线只能有一套,RecalculateNormals平均之后法线会互相抵消,导致光照发灰。如果用的是 Unlit 材质,这个问题不存在;用受光材质就需要注意。

第二种:Shader 关闭剔除。在材质对应的 Shader 里设置Cull Off,两个面都会被光栅化。这种做法性能开销略高(多画一遍),而且背面光照计算用的是正面法线,看起来会有点怪。适合快速验证、调试可视化。

第三种:顶点复制。把三个顶点在数组里再存一份,让正反两面各用独立的顶点集合,各自计算法线。顶点数从 3 变成 6,索引也是 6 个,但光照表现最正确。

Vector3[] vertices = new Vector3[6]; vertices[0] = new Vector3(0f, 0f, 0f); vertices[1] = new Vector3(1f, 0f, 0f); vertices[2] = new Vector3(0f, 1f, 0f); vertices[3] = vertices[0]; vertices[4] = vertices[1]; vertices[5] = vertices[2]; int[] triangles = new int[6] { 0, 2, 1, // 正面 3, 4, 5 // 反面,顶点独立 };

三种方案怎么选?我的经验是:Unlit 场景用第一种,最经济;需要正确光照的双面物体用第三种;临时调试、写工具用第二种,改一行就完事。

5.2 由三角面拼出四边形与网格条带

一个三角面是练手,两个拼起来就是一个四边形。四个顶点,六个索引,规律很直白:

Vector3[] vertices = new Vector3[4]; vertices[0] = new Vector3(0f, 0f, 0f); // 左下 vertices[1] = new Vector3(1f, 0f, 0f); // 右下 vertices[2] = new Vector3(0f, 1f, 0f); // 左上 vertices[3] = new Vector3(1f, 1f, 0f); // 右上 int[] triangles = new int[6] { 0, 2, 1, // 左下 - 左上 - 右下 2, 3, 1 // 左上 - 右上 - 右下 };

注意这里用了“左下-左上-右下”这种拆法,而不是“左下-右下-右上”再加一个。两种拆法都能成四边形,区别在于对角线方向。做平面时看不出差别,但如果顶点高度不同(比如做起伏地形),对角线方向会影响表面的折痕走向,进而影响光照和形变。做地形时我倾向统一一个方向的对角线,视觉上更整齐。

把四边形按行列复制,就是一个规则的网格面片:

private void BuildGrid(int rowCount, int colCount, float cellSize) { _vertices.Clear(); _triangles.Clear(); for (int r = 0; r <= rowCount; r++) { for (int c = 0; c <= colCount; c++) { _vertices.Add(new Vector3(c * cellSize, r * cellSize, 0f)); } } int stride = colCount + 1; for (int r = 0; r < rowCount; r++) { for (int c = 0; c < colCount; c++) { int bottomLeft = r * stride + c; int bottomRight = bottomLeft + 1; int topLeft = bottomLeft + stride; int topRight = topLeft + 1; _triangles.Add(bottomLeft); _triangles.Add(topLeft); _triangles.Add(bottomRight); _triangles.Add(topLeft); _triangles.Add(topRight); _triangles.Add(bottomRight); } } _mesh.Clear(); _mesh.SetVertices(_vertices); _mesh.SetTriangles(_triangles, 0); _mesh.RecalculateNormals(); _mesh.RecalculateBounds(); }

这段代码是后面做地形、水波、布料的基础骨架。顶点数按(rowCount+1) * (colCount+1)算,比如 100×100 的网格就是 10201 个顶点、20000 个三角形。这个量级完全没问题,但如果做到 300×300 以上,顶点数逼近 9 万,就触到 16 位索引的上限了。

索引格式这个点值得单独说。Unity 默认的网格索引用 16 位存储,最大支持 65535 个顶点。超过这个数,必须显式切换:

_mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;

这行要放在设置顶点之前。切换成 32 位索引后,显存占用会翻倍,所以在不需要的时候别乱开。我在做一块高密度地形时忘了加这句,结果网格只显示了一部分,剩下的全是乱的,加了一行就正常了。

5.3 把 Mesh 存成资源文件

运行时生成的网格关掉播放就没了。如果想让它在编辑状态下也保留,得用编辑器脚本存成资源文件。

#if UNITY_EDITOR using UnityEditor; using UnityEngine; public static class MeshSaver { [MenuItem("Tools/Save Mesh As Asset")] public static void Save() { MeshFilter filter = Selection.activeGameObject != null ? Selection.activeGameObject.GetComponent<MeshFilter>() : null; if (filter == null || filter.sharedMesh == null) { Debug.LogWarning("请先选中一个带有 MeshFilter 且已赋值网格的对象"); return; } string path = "Assets/GeneratedMesh.asset"; Mesh clone = Object.Instantiate(filter.sharedMesh); clone.name = "GeneratedMesh"; AssetDatabase.CreateAsset(clone, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } } #endif

几个细节值得注意。

必须克隆。直接把运行时网格存成资源,会把它和当前场景对象绑死,之后改场景可能连带改坏资源文件。Instantiate一份再存,能彻底断开关联。

路径必须在 Assets 目录下。放到 Assets 外面 Unity 不认,会报错。这个路径我习惯留一个可配置的字段,但演示用写死也无妨。

MenuItem 的位置。Tools/下面比较稳妥,不会和内置菜单冲突。写编辑器脚本时,#if UNITY_EDITOR包裹是必须的,否则打包时会因为找不到UnityEditor命名空间而编译失败。这个坑几乎所有写编辑器工具的人都踩过一次。

存完之后别忘了给 MeshFilter 换成新资源。资源文件是一个独立的资产,如果没有被引用,下一次工程刷新时可能被判定为无引用的孤立资源清理掉。做法很简单,存完之后把filter.sharedMesh指过去即可。

还有一点,存下来的网格只包含数据,不包含材质。材质是独立的,需要在导入之后手动挂上,或者用脚本一并处理。这一点在批量生成资产时需要特别注意,否则会得到一堆没有材质的网格,看着像个半成品。

写完这套东西再回头看,一个三角面确实简单到有点寒酸。但把它彻底跑通之后,绕序、法线、包围盒、索引格式这四件事就算真正上手了。后面无论做地形、做变形、做网格切割,都是在同一套数据结构上做加减法,出问题时排查的路径也是完全一样的:先看数据,再看可见性,最后看光照。这个顺序,比任何工具都可靠。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 15:06:16

动态规划解香槟塔问题:LeetCode 799题详解

1. 问题背景与题目解析香槟塔问题源自LeetCode第799题&#xff0c;属于中等难度题型。这道题目模拟了香槟塔中液体流动的物理过程&#xff0c;考察对动态规划或模拟算法的理解和应用能力。题目描述如下&#xff1a;我们有一个香槟塔&#xff0c;由若干层玻璃杯组成。第一层有1个…

作者头像 李华
网站建设 2026/10/27 15:06:14

ponytail插件:浏览器信息收束与线索管理实战指南

1. 从“ponytail”这个标题说起&#xff1a;它到底是什么 第一次看到“ponytail”这个词&#xff0c;大多数人脑子里蹦出来的画面是扎在脑后的马尾辫。但在技术圈和工具链语境里&#xff0c;它指的是一款轻量级的浏览器插件&#xff0c;核心定位是“把当前页面里散落的信息快速…

作者头像 李华
网站建设 2026/10/27 15:05:29

OpenMontage:模块化视频蒙太奇编排与自动化剪辑实践

1. 项目缘起与核心定位第一次看到 OpenMontage 这个名字&#xff0c;我脑子里蹦出来的画面是剪辑软件里那条永远对不齐的时间线。Montage 这个词在影视行业里就是“蒙太奇”&#xff0c;说白了就是把一堆零散的镜头素材&#xff0c;按照某种逻辑重新拼接成一段有节奏、有情绪的…

作者头像 李华
网站建设 2026/10/27 15:05:24

大专生如何在大数据领域实现职业突破

1. 大专生如何在大数据领域找到出路&#xff1f;大数据行业近年来发展迅猛&#xff0c;2023年全球大数据市场规模已达到2500亿美元。对于大专学历的从业者来说&#xff0c;这个领域既充满挑战也蕴含巨大机遇。我作为从业8年的数据工程师&#xff0c;见过太多不同学历背景的同行…

作者头像 李华
网站建设 2026/10/27 15:04:32

MUSIC测向性能分析:MATLAB实战中的噪声容限、角度分辨与快拍数约束

简介&#xff1a;本资源是一份面向信号处理方向初学者与工程实践者的MATLAB实操资料包&#xff0c;聚焦MUSIC算法在阵列测向中的原理理解、代码实现与性能评估。内容涵盖DOA估计核心理论、算法优缺点深度解析&#xff0c;以及多组不同信噪比、信号源数条件下的仿真实验设计&…

作者头像 李华
网站建设 2026/10/27 15:04:32

OpenClaw与AgenticHub:企业级AI智能体架构与落地实践

1. OpenClaw与AgenticHub&#xff1a;企业级智能体生态全景透视 当企业开始规模化部署AI智能体时&#xff0c;单点工具已无法满足复杂业务需求。OpenClaw作为模块化技能库与AgenticHub的智能体管理平台组合&#xff0c;正在重新定义企业AI基础设施的构建方式。这套生态的核心价…

作者头像 李华