news 2026/7/13 10:21:20

Unity Mesh编程入门:从三角形到圆柱体的程序化网格生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity Mesh编程入门:从三角形到圆柱体的程序化网格生成

1. 项目概述:为什么我们要从零开始绘制Mesh?

如果你在Unity里做过3D项目,大概率是从导入一个FBX或者OBJ模型开始的。拖拽进来,挂上材质,一个精致的角色或场景就出现了。这很方便,但久而久之,你可能会产生一种“黑盒”感:这个模型到底是怎么被GPU画出来的?它的顶点数据长什么样?法线、UV这些玩意儿究竟是如何组织,最终变成屏幕上那个立体的图形的?

这就是我们今天要聊的Mesh(网格)。你可以把它理解为一个3D模型的“骨架”或“蓝图”。所有你看到的3D物体,无论多复杂,本质上都是由无数个三角形(或四边形)拼接而成的表面,而Mesh就是定义这些三角形顶点位置、连接关系以及表面属性(如法线、UV)的数据结构。Unity的MeshFilter组件持有这个蓝图,MeshRenderer组件则负责根据蓝图和材质(油漆)把它画出来。

那么,自己动手用代码生成Mesh的意义何在?我总结了几点:

  1. 彻底理解3D渲染的底层原理:这是从“使用者”到“创造者”的关键一步。理解了Mesh,你就理解了3D图形的基石。
  2. 实现程序化生成内容:比如随机生成的地形、动态变化的河流、被破坏的墙体碎片、科技感的数据可视化图表。这些都无法通过预制的静态模型完美实现。
  3. 极致优化:当你完全掌控顶点和三角形时,你可以用最精简的数据来表达模型,合并批次,减少Draw Call,这对于移动端或大型场景性能优化至关重要。
  4. 解决特定问题:比如修复导入模型时奇怪的接缝、动态修改模型顶点实现形变动画(如旗帜飘动、水面波纹),或者创建一些标准建模工具难以制作的特殊形状。

网上很多教程一上来就丢给你一大段生成立方体的代码,让人看得云里雾里。今天,我们就从最简单的一个三角形开始,逐步构建正方体圆柱体,我会把每一步“为什么这么做”都掰开揉碎讲清楚。你会发现,一旦理解了核心逻辑,Mesh其实并不神秘。

2. 核心概念与数据结构拆解:Mesh的“五脏六腑”

在动手写代码之前,我们必须先搞清楚一个Mesh对象里到底包含了哪些关键数据。你可以把它想象成一个需要填写的表格。

2.1 顶点的多重属性:不止是位置

首先是最核心的顶点(Vertices)。一个顶点在3D空间里不仅仅是一个坐标点(x, y, z),它还可能携带了多种描述其“状态”的信息。

  • 位置(Position):Vector3类型,定义顶点在模型局部空间中的坐标。这是必需项。
  • 法线(Normal):Vector3类型,定义顶点指向的方向,垂直于模型表面。主要用于光照计算。如果法线信息错误,光照就会看起来非常奇怪,像破了洞一样。
  • UV坐标(UV):Vector2类型,定义顶点在纹理贴图上的采样位置。UV决定了贴图如何“包裹”在模型表面。UV的U和V轴范围通常是0到1。
  • 切线(Tangent):Vector4类型,主要用于法线贴图(Normal Map)的计算,它定义了表面的朝向,与法线和副切线(Binormal)共同构成一个坐标系。
  • 顶点色(Vertex Color):Color类型,可以给顶点赋予颜色,常用于简单的着色或渐变效果。

对于我们入门练习,位置、法线和UV是三个最需要关注的基本属性。

2.2 三角形的组织方式:索引的力量

顶点定义好了,接下来需要告诉GPU如何用这些顶点来组装成三角形面片。这里有两种方式:

  1. 三角形列表(Triangle List): 直接列出每个三角形的三个顶点。例如,一个四边形(两个三角形)需要6个顶点数据。这种方式简单直观,但存在大量顶点重复(一个顶点被多个三角形共享时,在数据中会重复存储多次),效率较低。
  2. 索引绘制(Indexed Drawing): Unity的Mesh采用这种方式。我们提供一份顶点数组(vertices)和一份三角形索引数组(triangles)
    • vertices数组:存储所有不重复的顶点数据。
    • triangles数组:存储的是顶点在vertices数组中的索引(下标),每三个索引构成一个三角形。

为什么用索引?假设一个立方体有8个顶点,但需要12个三角形面(每个面2个三角)。如果用三角形列表,需要12 * 3 = 36个顶点数据,其中大量重复。而用索引,vertices数组只有8个元素,triangles数组有36个元素,但每个元素只是一个整数索引。这极大地节省了内存和GPU带宽,是现代图形API的通用做法。

顶点顺序与背面剔除在设置triangles数组时,顶点的顺序(顺时针CW或逆时针CCW)决定了三角形的“正面”。默认情况下,Unity会进行背面剔除(Backface Culling),即只渲染正面朝向摄像机的三角形。Unity的约定是逆时针(CCW)顺序的顶点构成三角形的正面。弄反了会导致该面在渲染时不可见。

2.3 Mesh数据的赋值流程

在Unity C#脚本中,操作一个Mesh的基本流程如下:

  1. 创建一个新的Mesh对象。
  2. 准备好Vector3[] vertices数组并赋值给mesh.vertices
  3. 准备好int[] triangles数组并赋值给mesh.triangles注意:这一步必须在顶点数据赋值之后。
  4. (可选但推荐)计算并赋值法线mesh.normals和UVmesh.uv
  5. 调用mesh.RecalculateBounds()重新计算包围盒,这对于裁剪和碰撞检测很重要。
  6. 将创建好的Mesh赋值给MeshFilter组件的mesh属性。

注意:mesh.vertices等属性的setter方法内部会创建数据的副本。频繁修改Mesh时(如每帧变形的顶点动画),应考虑使用Mesh.SetVerticesMesh.SetTriangles等List版本的方法,或者直接操作Mesh.vertices返回的数组后再赋值,以避免产生过多的GC(垃圾回收)开销。对于静态Mesh,初始化时赋值一次即可。

3. 实战演练一:绘制第一个三角形

理论说再多不如动手试一下。我们从一个在3D空间中的单面三角形开始。

3.1 创建基础场景与脚本

  1. 在Unity中创建一个空场景。
  2. 在Hierarchy中创建一个空GameObject,命名为“MeshGenerator”。
  3. 为这个GameObject添加两个组件:Mesh FilterMesh Renderer
  4. 创建一个新的C#脚本,命名为GenerateTriangle,挂载到“MeshGenerator”上。
  5. Mesh Renderer组件上,给它赋一个默认的材质(比如Standard材质)。

3.2 代码实现与逐行解析

打开GenerateTriangle.cs脚本,我们编写以下代码:

using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class GenerateTriangle : MonoBehaviour { void Start() { // 1. 获取或创建MeshFilter组件 MeshFilter meshFilter = GetComponent<MeshFilter>(); Mesh mesh = new Mesh(); mesh.name = "MyTriangle"; // 给Mesh起个名字,方便在Profiler中识别 // 2. 定义三个顶点的位置 (在模型局部空间) Vector3[] vertices = new Vector3[3]; vertices[0] = new Vector3(0, 0, 0); // 顶点A:原点 vertices[1] = new Vector3(1, 0, 0); // 顶点B:X轴正方向1单位 vertices[2] = new Vector3(0, 1, 0); // 顶点C:Y轴正方向1单位 mesh.vertices = vertices; // 3. 定义三角形(使用顶点索引) // 顺序:A(0) -> B(1) -> C(2) 为逆时针,正面朝上(假设Z轴正方向为观察方向) int[] triangles = new int[3] { 0, 1, 2 }; mesh.triangles = triangles; // 4. 计算法线(对于简单平面,我们可以手动计算) // 一个三角形只有一个面,所有顶点法线方向相同,垂直于三角形平面。 Vector3 normal = Vector3.Cross(vertices[1] - vertices[0], vertices[2] - vertices[0]).normalized; Vector3[] normals = new Vector3[3]; normals[0] = normal; normals[1] = normal; normals[2] = normal; mesh.normals = normals; // 5. 定义UV坐标(简单映射) Vector2[] uvs = new Vector2[3]; uvs[0] = new Vector2(0, 0); // 顶点A对应贴图左下角 uvs[1] = new Vector2(1, 0); // 顶点B对应贴图右下角 uvs[2] = new Vector2(0, 1); // 顶点C对应贴图左上角 mesh.uv = uvs; // 6. 重新计算包围盒 mesh.RecalculateBounds(); // 7. 将Mesh赋值给MeshFilter meshFilter.mesh = mesh; } }

关键点解析:

  • 顶点顺序(第15行){0, 1, 2}意味着用第0、1、2号顶点按顺序构成一个三角形。从Z轴正方向看(Unity场景视图的默认视角),这个顺序是逆时针,所以三角形的正面朝向屏幕外(即Z轴正方向)。如果你写成{0, 2, 1},三角形正面就会朝内(Z轴负方向),在场景中你可能就看不到了(因为背面被剔除了)。
  • 法线计算(第18-24行)Vector3.Cross是叉乘运算。对于三角形ABC,向量AB和向量AC的叉乘结果是一个垂直于三角形平面的向量。我们将其归一化(.normalized)后作为三个顶点的法线。对于平面,顶点法线可以相同。对于平滑曲面(如球体),一个顶点可能被多个面共享,其法线是这些面法线的平均值。
  • UV映射(第27-31行):这里我们做了一个简单的线性映射,将三角形的三个角点映射到纹理贴图(一个单位正方形)的三个角上。这样,如果材质有贴图,就会完整地铺满这个三角形。

运行游戏,你应该能在场景中心看到一个白色的三角形面片。给它一个带颜色的材质,效果会更明显。尝试在代码中修改顶点坐标,观察三角形的变化。

4. 实战演练二:构建一个完整的立方体(正方体)

三角形是基础,立方体则是理解3D网格构造的经典案例。一个立方体有6个面,每个面由2个三角形组成,共12个三角形,36个索引。但立方体只有8个独特的顶点。我们的目标是:用8个顶点和12个三角形的索引,构建出一个完整的立方体。

4.1 设计顶点与面

首先,我们定义立方体在局部空间中的8个顶点。假设立方体中心在原点,边长为2(即从-1到1)。

// 立方体的8个顶点 Vector3[] vertices = new Vector3[8]; vertices[0] = new Vector3(-1, -1, -1); // 左-下-后 vertices[1] = new Vector3( 1, -1, -1); // 右-下-后 vertices[2] = new Vector3( 1, 1, -1); // 右-上-后 vertices[3] = new Vector3(-1, 1, -1); // 左-上-后 vertices[4] = new Vector3(-1, -1, 1); // 左-下-前 vertices[5] = new Vector3( 1, -1, 1); // 右-下-前 vertices[6] = new Vector3( 1, 1, 1); // 右-上-前 vertices[7] = new Vector3(-1, 1, 1); // 左-上-前

接下来是最关键的步骤:定义每个面的两个三角形。我们必须为每个面指定正确的、逆时针的顶点顺序(从该面的外侧看向立方体内部)。

以“前面”(Z轴正方向的面,由顶点4,5,6,7构成)为例:

  • 三角形1:左下(4) -> 右下(5) -> 右上(6) 【逆时针】
  • 三角形2:左下(4) -> 右上(6) -> 左上(7) 【逆时针】

按照这个逻辑,我们可以定义出所有6个面(前、后、左、右、上、下)的三角形索引。

4.2 代码实现:立方体Mesh生成器

创建一个新脚本GenerateCube

using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class GenerateCube : MonoBehaviour { public float size = 1f; // 立方体大小,中心到面的距离 void Start() { MeshFilter meshFilter = GetComponent<MeshFilter>(); Mesh mesh = new Mesh(); mesh.name = "ProceduralCube"; // 1. 定义8个顶点 Vector3[] vertices = new Vector3[8]; float halfSize = size * 0.5f; vertices[0] = new Vector3(-halfSize, -halfSize, -halfSize); vertices[1] = new Vector3( halfSize, -halfSize, -halfSize); vertices[2] = new Vector3( halfSize, halfSize, -halfSize); vertices[3] = new Vector3(-halfSize, halfSize, -halfSize); vertices[4] = new Vector3(-halfSize, -halfSize, halfSize); vertices[5] = new Vector3( halfSize, -halfSize, halfSize); vertices[6] = new Vector3( halfSize, halfSize, halfSize); vertices[7] = new Vector3(-halfSize, halfSize, halfSize); mesh.vertices = vertices; // 2. 定义12个三角形(6个面 * 2个三角/面) int[] triangles = new int[36]; // 前面 (Z+) triangles[0] = 4; triangles[1] = 5; triangles[2] = 6; triangles[3] = 4; triangles[4] = 6; triangles[5] = 7; // 后面 (Z-) triangles[6] = 1; triangles[7] = 0; triangles[8] = 3; triangles[9] = 1; triangles[10] = 3; triangles[11] = 2; // 左面 (X-) triangles[12] = 0; triangles[13] = 4; triangles[14] = 7; triangles[15] = 0; triangles[16] = 7; triangles[17] = 3; // 右面 (X+) triangles[18] = 5; triangles[19] = 1; triangles[20] = 2; triangles[21] = 5; triangles[22] = 2; triangles[23] = 6; // 上面 (Y+) triangles[24] = 3; triangles[25] = 7; triangles[26] = 6; triangles[27] = 3; triangles[28] = 6; triangles[29] = 2; // 下面 (Y-) triangles[30] = 1; triangles[31] = 5; triangles[32] = 4; triangles[33] = 1; triangles[34] = 4; triangles[35] = 0; mesh.triangles = triangles; // 3. 计算法线 - 使用Unity的便捷方法 mesh.RecalculateNormals(); // 4. 定义UV (简单立方体贴图映射) Vector2[] uvs = new Vector2[8]; // 这里是一个简单的映射,每个顶点分配一个UV。 // 更复杂的做法是为每个面的4个顶点单独计算UV,因为一个顶点在3个面上共享,UV却不同。 // 这就引出了“顶点拆分”的概念。 for (int i = 0; i < uvs.Length; i++) { // 这只是个示例,实际立方体贴图需要更精细的UV展开 uvs[i] = new Vector2(vertices[i].x + 0.5f, vertices[i].y + 0.5f); } mesh.uv = uvs; // 5. 重新计算包围盒 mesh.RecalculateBounds(); meshFilter.mesh = mesh; } }

运行脚本,你应该能看到一个标准的立方体。给它一个带贴图的材质,你会发现贴图拉伸得很奇怪。这是因为我们犯了一个常见的错误。

4.3 核心难点:顶点属性共享与拆分

仔细观察我们的立方体:它有8个顶点,但每个顶点实际上被3个相邻的面共享。问题来了:这3个面的法线方向是不同的(分别指向X、Y、Z轴的正/负方向)。如果我们像上面那样,使用mesh.RecalculateNormals(),Unity会为每个顶点计算一个平均法线。对于立方体这种有清晰棱角的模型,这会导致光照在棱角处变得圆滑,失去硬朗的感觉。

解决方案是:顶点数据拆分。

我们不应该只定义8个顶点。为了拥有独立的法线(以及独立的UV),我们需要为立方体的每个面的每个角都定义一个独立的顶点。一个立方体有6个面,每个面有4个角,所以总共需要6 * 4 = 24 个顶点。这样,每个顶点只属于一个面,可以拥有完全正确的、垂直于该面的法线。

同样,triangles数组的索引也需要对应这24个顶点重新编排。虽然顶点数增加了,但每个顶点的属性(法线、UV)是正确的,渲染效果才准确。修改后的顶点和三角形定义逻辑会更复杂,但结构更清晰。这是手工创建复杂Mesh时必须掌握的核心思想。

5. 实战演练三:生成平滑的圆柱体

圆柱体比立方体更进一步,它引入了曲面顶点环绕的概念。圆柱体由三部分组成:侧面(一个卷曲的矩形)、顶部圆面和底部圆面。

5.1 设计思路:分段与环绕

我们无法用有限的三角形完美表示一个平滑的圆,但可以用一个正多边形来无限逼近。这就是分段(Segments)的概念。我们将圆柱体的高度方向和圆周方向进行分段。

  • 高度分段:沿着Y轴方向将圆柱体切成若干层。对于标准圆柱体,通常1段就够了(只有顶部和底部边缘),但如果你想让圆柱体在高度方向上也变形(如弯曲),就需要更多分段。
  • 圆周分段:将顶圆和底圆用正N边形(N=segments)来近似。N越大,圆柱体侧面越平滑。

我们将分别生成侧面、顶面、底面的顶点,然后组合它们的三角形索引。

5.2 代码实现:参数化圆柱体生成

创建一个新脚本GenerateCylinder

using UnityEngine; using System.Collections.Generic; // 使用List更方便 [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class GenerateCylinder : MonoBehaviour { public float height = 2f; public float radius = 0.5f; public int radialSegments = 16; // 圆周分段数 public int heightSegments = 1; // 高度分段数 void Start() { MeshFilter meshFilter = GetComponent<MeshFilter>(); Mesh mesh = new Mesh(); mesh.name = "ProceduralCylinder"; List<Vector3> vertices = new List<Vector3>(); List<int> triangles = new List<int>(); List<Vector3> normals = new List<Vector3>(); List<Vector2> uvs = new List<Vector2>(); // 1. 生成侧面顶点 for (int y = 0; y <= heightSegments; y++) { float v = (float)y / heightSegments; // V坐标 (0到1) float yPos = (v - 0.5f) * height; // 将高度中心对齐到原点 for (int x = 0; x <= radialSegments; x++) { float u = (float)x / radialSegments; // U坐标 (0到1) float angle = u * Mathf.PI * 2.0f; // 将U映射到0~2π弧度 float xPos = Mathf.Cos(angle) * radius; float zPos = Mathf.Sin(angle) * radius; vertices.Add(new Vector3(xPos, yPos, zPos)); // 侧面法线是顶点位置在XZ平面上的归一化向量 normals.Add(new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle))); // UV: u沿环绕方向,v沿高度方向 uvs.Add(new Vector2(u, v)); } } // 2. 生成侧面三角形 for (int y = 0; y < heightSegments; y++) { for (int x = 0; x < radialSegments; x++) { int currentRow = y * (radialSegments + 1); int nextRow = (y + 1) * (radialSegments + 1); int topLeft = currentRow + x; int topRight = currentRow + x + 1; int bottomLeft = nextRow + x; int bottomRight = nextRow + x + 1; // 第一个三角形 (topLeft -> bottomLeft -> topRight) triangles.Add(topLeft); triangles.Add(bottomLeft); triangles.Add(topRight); // 第二个三角形 (topRight -> bottomLeft -> bottomRight) triangles.Add(topRight); triangles.Add(bottomLeft); triangles.Add(bottomRight); } } // 3. 生成顶部和底部圆面中心顶点 int topCenterIndex = vertices.Count; vertices.Add(new Vector3(0, height * 0.5f, 0)); normals.Add(Vector3.up); // 法线朝上 uvs.Add(new Vector2(0.5f, 0.5f)); int bottomCenterIndex = vertices.Count; vertices.Add(new Vector3(0, -height * 0.5f, 0)); normals.Add(Vector3.down); // 法线朝下 uvs.Add(new Vector2(0.5f, 0.5f)); // 4. 生成顶部和底部圆面边缘顶点及三角形 // 顶部圆面 int topEdgeStartIndex = vertices.Count; for (int x = 0; x <= radialSegments; x++) { float u = (float)x / radialSegments; float angle = u * Mathf.PI * 2.0f; float xPos = Mathf.Cos(angle) * radius; float zPos = Mathf.Sin(angle) * radius; vertices.Add(new Vector3(xPos, height * 0.5f, zPos)); normals.Add(Vector3.up); // UV从中心(0.5,0.5)向外辐射 uvs.Add(new Vector2(Mathf.Cos(angle) * 0.5f + 0.5f, Mathf.Sin(angle) * 0.5f + 0.5f)); } // 顶部三角形扇 for (int x = 0; x < radialSegments; x++) { triangles.Add(topCenterIndex); triangles.Add(topEdgeStartIndex + x + 1); triangles.Add(topEdgeStartIndex + x); } // 底部圆面 (法线朝下,顶点顺序要反过来以保证正面朝外) int bottomEdgeStartIndex = vertices.Count; for (int x = 0; x <= radialSegments; x++) { float u = (float)x / radialSegments; float angle = u * Mathf.PI * 2.0f; float xPos = Mathf.Cos(angle) * radius; float zPos = Mathf.Sin(angle) * radius; vertices.Add(new Vector3(xPos, -height * 0.5f, zPos)); normals.Add(Vector3.down); uvs.Add(new Vector2(Mathf.Cos(angle) * 0.5f + 0.5f, Mathf.Sin(angle) * 0.5f + 0.5f)); } // 底部三角形扇 (注意逆时针顺序) for (int x = 0; x < radialSegments; x++) { triangles.Add(bottomCenterIndex); triangles.Add(bottomEdgeStartIndex + x); triangles.Add(bottomEdgeStartIndex + x + 1); } // 5. 赋值给Mesh mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.SetNormals(normals); mesh.SetUVs(0, uvs); // 第一个UV通道 mesh.RecalculateBounds(); meshFilter.mesh = mesh; } }

代码要点解析:

  • 侧面生成(第20-55行):使用双层循环。外层循环高度(Y),内层循环圆周(X)。radialSegments + 1是因为首尾顶点UV不同(0和1),但位置在3D空间中是重合的,这对于正确的UV映射是必要的。
  • 三角形索引计算(第44-55行):这是将四边形(由四个顶点构成)拆分为两个三角形的通用模式。注意索引的顺序保证了所有三角形正面朝外。
  • 圆面生成(第58行以后):顶部和底部使用“三角形扇”来构建。即一个中心顶点,连接一圈边缘顶点,形成多个三角形。底部三角形的顺序需要反转,以确保从外部看(下方)时,三角形是逆时针的。
  • 使用ListSet方法:由于顶点和三角形数量是动态的,使用List<T>比数组更方便。mesh.SetVertices()等方法直接接受List,避免了创建中间数组。

将脚本挂载到物体上,调整radialSegments参数,你可以看到圆柱体从棱柱变得逐渐平滑的过程。这就是分段的意义。

6. 常见问题、优化技巧与实战心得

自己动手写Mesh生成代码,肯定会遇到各种坑。这里我分享一些积累下来的经验和常见问题的解法。

6.1 高频问题排查清单

问题现象可能原因解决方案
模型完全不可见(渲染不出来)1.MeshRenderer没有材质。
2. 三角形顶点顺序全是顺时针(背面剔除)。
3. 相机裁剪面设置不当,模型在视锥体外。
4. Mesh的包围盒(Bounds)计算错误。
1. 检查MeshRenderer的Material属性。
2. 检查triangles数组的顶点索引顺序,确保从面外看是逆时针。可以暂时关闭背面剔除(材质上设置Cull Off)调试。
3. 检查相机位置和模型的缩放/位置。
4. 生成Mesh后调用mesh.RecalculateBounds()
模型光照异常(全黑、全亮或部分发黑)1. 法线(Normals)数据错误或未计算。
2. 顶点法线没有归一化(长度不为1)。
3. 平滑组问题(如立方体边角圆滑)。
1. 确保赋值了mesh.normals或调用了mesh.RecalculateNormals()
2. 手动计算法线时确保使用.normalized
3. 对于需要硬边的模型(如立方体),必须进行顶点拆分,确保每个面的顶点有独立的法线。
纹理贴图拉伸、错乱UV坐标计算错误。UV坐标通常应在[0,1]范围内,且映射关系不符合预期。仔细检查UV生成逻辑。对于圆柱体侧面,U通常对应环绕角度(0到1),V对应高度(0到1)。对于平面投影,可以使用顶点位置进行简单线性映射。使用Unity的Scene视图的UV Checker纹理进行可视化调试非常有效。
模型在移动/旋转时闪烁或撕裂每帧都在创建新的Mesh对象,产生大量GC(垃圾回收)。对于动态Mesh,在StartAwake中创建一次Mesh对象并缓存引用。在Update中直接修改其vertices等数组,然后重新赋值(mesh.vertices = myVertices)并调用mesh.RecalculateBounds()mesh.RecalculateNormals()(如需)。
性能低下1. 顶点数过多(特别是radialSegments设置过高)。
2. 每帧更新整个Mesh,且顶点数很多。
1. 遵循“够用就好”原则,在视觉可接受的范围内减少分段数。
2. 对于静态Mesh,确保其标记为Static,以便Unity进行静态合批优化。
3. 对于动态Mesh,考虑使用Job SystemBurst Compiler进行多线程计算,或者使用Compute Shader在GPU上计算。

6.2 性能优化与高级技巧

  1. 重用与合并:如果场景中有大量相同的程序化Mesh(比如一片草地),不要每个都单独生成一个Mesh。应该生成一个,然后通过Graphics.DrawMeshMaterialPropertyBlock进行批量绘制,或者使用GPU Instancing。
  2. 使用MeshDataAPI (Unity 2020.2+): 对于高性能、多线程的Mesh生成和修改,新的MeshDataMesh.ApplyAndDisposeWritableMeshDataAPI是更好的选择,它避免了主线程的阻塞和托管数组的拷贝。
  3. 法线计算优化mesh.RecalculateNormals()很方便,但它是基于三角形平均的,可能不是你想要的效果(特别是硬边)。对于复杂Mesh,手动计算法线(比如根据顶点相邻面的加权平均)能获得更好的控制。
  4. UV通道与切线:除了主UV (mesh.uv),还有第二套UV (mesh.uv2),常用于光照贴图。如果你的材质需要法线贴图,还必须正确计算并赋值mesh.tangents(切线),这是一个Vector4数组,其w分量通常为1或-1,用于决定副切线的方向。

6.3 从原理到扩展:你的Mesh之旅才刚刚开始

通过三角形、立方体、圆柱体这三个例子,我们已经走完了从理解概念到实现基础几何体的完整路径。但这仅仅是开始。Mesh的世界无比广阔:

  • 复杂模型:你可以用算法生成山脉(使用噪声函数)、树木、云朵。
  • 动态变形:修改vertices数组的Y值,就能做出水面波浪;根据距离修改顶点颜色,就能实现溶解效果。
  • 布尔运算:通过算法求两个Mesh的交、并、差集,可以实现程序化的建模。
  • 从数据到可视化:将任何三维数据(如科学数据、点云)转换为Mesh,进行立体可视化。

我个人的体会是,手动创建Mesh就像在给GPU直接“喂数据”。一开始会觉得繁琐,但当你能够随心所欲地生成和操控这些顶点与三角形时,你对3D图形渲染的理解会达到一个新的层次。它打破了预制模型的黑盒,让你获得了在运行时创造无限可能性的能力。下次当你遇到一个需要特殊形状或动态效果的需求,而资源商店里又找不到合适的东西时,不妨想想:我能不能用代码把它“画”出来?

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

Tmux 3.7b 实战:5个高效工作流配置,提升远程开发效率 300%

Tmux 3.7b 实战&#xff1a;5个高效工作流配置&#xff0c;提升远程开发效率 300% 在远程开发场景中&#xff0c;终端管理工具的效率直接决定了生产力水平。作为终端复用器的标杆&#xff0c;Tmux 3.7b 版本带来了更稳定的会话管理和更灵活的面板控制能力。本文将分享五个经过实…

作者头像 李华
网站建设 2026/7/13 10:19:55

UE5多设备协同控制:基于Python与UDP组播的轻量级解决方案

1. 项目概述&#xff1a;为什么要在UE5里折腾Python和UDP组播&#xff1f;如果你正在用UE5做数字孪生、多屏互动演示&#xff0c;或者管理一个由多台渲染工作站组成的集群&#xff0c;那你肯定遇到过这个痛点&#xff1a;如何同时向所有设备发送一条指令&#xff1f;比如&#…

作者头像 李华
网站建设 2026/7/13 10:19:15

Unity Runtime Inspector:移动端调试利器,提升开发效率的实战指南

1. 项目概述&#xff1a;为什么我们需要Runtime Inspector&#xff1f;在Unity游戏开发中&#xff0c;调试是一个永恒的话题。无论是客户端逻辑错误、UI显示异常&#xff0c;还是性能瓶颈&#xff0c;找到问题的根源往往比解决问题本身更耗时。对于PC端的开发者来说&#xff0c…

作者头像 李华
网站建设 2026/7/13 10:18:27

锂离子电池主动平衡技术与BQ25887充电管理实践

1. 项目背景与核心需求解析在便携式电子设备设计中&#xff0c;两节锂离子电池串联&#xff08;2S&#xff09;架构因其更高的输出电压&#xff08;7.4V标称&#xff09;而备受青睐&#xff0c;但随之而来的电池单元间电压不平衡问题却成为设计难点。当两个电池单元的容量、内阻…

作者头像 李华
网站建设 2026/7/13 10:18:25

AD5593R与PIC18F4610硬件协同设计与优化实践

1. AD5593R与PIC18F4610的硬件协同设计1.1 AD5593R的核心特性解析AD5593R这颗芯片最吸引人的地方在于它的多功能引脚配置。每个引脚都可以通过软件配置为四种工作模式&#xff1a;12位DAC输出、12位ADC输入、数字输出或数字输入。这种灵活性意味着我们可以在不改变硬件连接的情…

作者头像 李华
网站建设 2026/7/13 10:17:16

如何用qmcdump免费解密QQ音乐加密文件:3分钟掌握终极技巧

如何用qmcdump免费解密QQ音乐加密文件&#xff1a;3分钟掌握终极技巧 【免费下载链接】qmcdump 一个简单的QQ音乐解码&#xff08;qmcflac/qmc0/qmc3 转 flac/mp3&#xff09;&#xff0c;仅为个人学习参考用。 项目地址: https://gitcode.com/gh_mirrors/qm/qmcdump 还…

作者头像 李华