ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity动态Mesh生成:从三角形到圆柱体的代码构建指南

Unity动态Mesh生成:从三角形到圆柱体的代码构建指南 1. 项目概述从“拖拽模型”到“创造模型”的思维跃迁在Unity开发中我们绝大多数时候都在与现成的3D模型打交道——从Asset Store下载或者由美术同事提供FBX文件我们拖入场景调整材质编写交互逻辑。这就像在乐高商店里购买现成的套装进行拼装固然高效但创造力被限制在了已有的积木形状里。然而当你需要动态生成地形、创建可破坏的物体、实现酷炫的网格变形特效或者仅仅是为了深入理解3D图形渲染的底层逻辑时你就必须跨出舒适区直面最核心的构建单元Mesh网格。Mesh是Unity中一切3D可视物体的基石。无论是复杂的角色模型还是简单的一个立方体在GPU看来它们都是由无数个三角形Triangles拼接而成的网格。理解并掌握通过代码动态构建Mesh意味着你从“模型使用者”转变为“模型创造者”。这不仅能解决特定场景下的动态生成需求如程序化生成迷宫、河流、植被更是进行高级图形编程如自定义Shader、GPU实例化、曲面细分的必备前置知识。很多开发者觉得Mesh操作深奥难懂其实它的核心逻辑非常直观确定顶点位置连接成三角形告诉GPU如何渲染。本文将彻底拆解这个过程手把手带你用代码“捏”出三角形、立方体和圆柱体这三种基础几何体让你获得从零构建3D世界的底层能力。2. 核心原理拆解Mesh的“数据结构”与“渲染流水线”在动手写代码之前我们必须先搞清楚两个核心问题Mesh在内存中究竟是如何存储数据的以及这些数据是如何一步步被绘制到屏幕上的如果把构建Mesh比作造房子那么理解数据结构和渲染流水线就是看懂建筑图纸和施工流程。2.1 Mesh的四大核心数据结构一个最简单的Mesh至少需要定义以下四类数据。你可以把它们想象成建造一个石膏像所需的模具和说明书。2.1.1 顶点坐标Vertices这是最基础的数据定义了模型在三维空间中的骨架。它是一个Vector3数组每个Vector3代表一个顶点的(x, y, z)坐标。例如一个在原点边长为1的立方体它的8个顶点坐标就包括(0,0,0)、(1,0,0)、(1,1,0)、(0,1,0)等。关键点在于顶点坐标定义的是“点”的位置本身并不包含任何连接关系。多个三角形可以共享同一个顶点这能有效减少数据量是优化Mesh的重要手段。2.1.2 三角形序列Triangles这是将离散的顶点连接成“面”的指令集。它是一个int数组其内容不是坐标而是顶点数组的索引。Unity以及绝大多数图形API规定三角形以顺时针顺序定义其正面即法线朝向的方向。例如如果vertices[0] (0,0,0),vertices[1] (1,0,0),vertices[2] (0,1,0)那么triangles [0, 1, 2]就定义了一个以这三个点为顶点的三角形。Triangles数组的长度永远是3的倍数。一个极易出错的细节是如果你定义的三角形顶点顺序是逆时针的从正面看这个三角形将是不可见的背面剔除。2.1.3 法线Normals法线是一个Vector3数组每个元素对应一个顶点表示该顶点“朝向”的方向。它主要用于光照计算。对于平面来说所有顶点的法线方向通常是一致的都垂直于平面向外。例如一个躺在XZ平面上的三角形其法线可能是(0, 1, 0)。重要经验在代码中生成Mesh时如果你没有手动指定法线Unity可能会尝试自动计算但结果往往不正确导致模型看起来是全黑或光照怪异。手动计算并赋值法线是保证渲染正确的关键一步。2.1.4 纹理坐标UVUV是一个Vector2数组用于将2D纹理图像“映射”到3D网格上。U和V的取值范围通常是[0, 1]对应纹理的从左到右和从下到上。uv[0]对应vertices[0]以此类推。它告诉GPU“这个三维顶点对应纹理图片上的这个二维坐标”。对于简单的几何体UV映射需要一些技巧比如立方体需要将六个面“展开”到一张纹理上。注意以上四个数组的长度必须满足严格的对应关系。vertices,normals,uv这三个数组的长度必须相等因为它们都是一一对应每个顶点的属性。而triangles数组中的每个索引值都必须是在vertices数组的有效索引范围内即 0 到vertices.Length - 1。索引越界是运行时崩溃的常见原因。2.2 渲染流水线中的Mesh从数据到像素理解了Mesh的数据构成我们再来看Unity如何用它进行渲染。这个过程主要由两个组件协作完成Mesh Filter你可以把它看作一个“数据持有者”。它本身不负责渲染其唯一的工作就是存储一个Mesh对象的引用。这个Mesh对象包含了我们上面定义的所有顶点、三角形等数据。Mesh Renderer这是真正的“画家”。它从Mesh Filter中拿到Mesh数据再结合自身附带的Material材质球包含了使用的Shader和纹理将计算指令提交给GPU。GPU则根据这些数据进行顶点变换、光栅化、片段着色等一系列操作最终在屏幕上画出像素。所以一个可渲染物体的标准工作流是脚本创建/修改Mesh数据 - 赋值给Mesh Filter的mesh属性 - Mesh Renderer读取并渲染。当我们动态创建Mesh时通常需要同时为GameObject添加MeshFilter和MeshRenderer组件并为其分配合适的材质。3. 实战构建一从零绘制一个单面三角形理论铺垫完毕我们现在开始动手。构建一个单面三角形是最简单的入门练习它能让你清晰地看到所有核心数据是如何关联的。3.1 顶点与三角形的定义我们计划在XZ平面上创建一个直角三角形直角位于原点。using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class CreateTriangle : MonoBehaviour { void Start() { // 1. 获取或添加必要的组件 MeshFilter meshFilter GetComponentMeshFilter(); MeshRenderer meshRenderer GetComponentMeshRenderer(); // 如果是新创建的物体可能需要先为其设置一个默认材质 // meshRenderer.material new Material(Shader.Find(Standard)); // 2. 创建一个新的Mesh对象 Mesh mesh new Mesh(); mesh.name MyTriangle; // 给Mesh起个名字便于调试 // 3. 定义顶点坐标 (在XZ平面上) Vector3[] vertices new Vector3[3]; vertices[0] new Vector3(0, 0, 0); // 顶点A原点 vertices[1] new Vector3(1, 0, 0); // 顶点BX轴正方向1单位 vertices[2] new Vector3(0, 0, 1); // 顶点CZ轴正方向1单位 mesh.vertices vertices; // 4. 定义三角形 (注意顶点顺序为顺时针) int[] triangles new int[3]; triangles[0] 0; // 使用vertices[0] triangles[1] 1; // 使用vertices[1] triangles[2] 2; // 使用vertices[2] mesh.triangles triangles;这段代码创建了三个顶点和一个三角形。如果你此时运行物体可能还不可见或者显示为奇怪的粉色缺少材质甚至可能是全黑缺少法线。3.2 计算法线与UV坐标为了让三角形能被正确光照和贴图我们必须补充法线和UV数据。// 5. 计算并设置法线 (对于平面三角形所有顶点法线方向相同) // 法线方向可以通过三角形两条边的叉积求得方向遵循右手定则。 Vector3 edgeAB vertices[1] - vertices[0]; Vector3 edgeAC vertices[2] - vertices[0]; Vector3 triangleNormal Vector3.Cross(edgeAB, edgeAC).normalized; // 叉积并归一化 Vector3[] normals new Vector3[3]; normals[0] triangleNormal; normals[1] triangleNormal; normals[2] triangleNormal; mesh.normals normals; // 6. 设置简单的UV坐标 (将三角形映射到纹理的左下角) Vector2[] uvs new Vector2[3]; uvs[0] new Vector2(0, 0); // 对应vertices[0] uvs[1] new Vector2(1, 0); // 对应vertices[1] uvs[2] new Vector2(0, 1); // 对应vertices[2] mesh.uv uvs; // 7. 重要重新计算包围盒 mesh.RecalculateBounds(); // 8. 将构建好的Mesh赋值给MeshFilter meshFilter.mesh mesh; } }关键操作解析与避坑指南法线计算我们通过计算边向量edgeAB和edgeAC的叉积来得到垂直于三角形平面的法线向量。Vector3.Cross(edgeAB, edgeAC)的顺序决定了法线方向。如果顺序反过来法线会指向相反方向导致三角形从另一面看才是亮的。.normalized是为了将法线向量长度变为1这是光照计算的要求。RecalculateBounds()这是一个极其重要但容易被忽略的步骤。Mesh的bounds属性是一个包围盒Bounding Box用于快速判断物体是否在相机视锥体内视锥体剔除。如果我们不手动调用此方法Unity可能会使用默认的或未更新的包围盒导致物体明明在屏幕内却被错误地剔除从而不显示。在动态修改Mesh顶点后必须调用mesh.RecalculateBounds()。材质如果运行后物体是粉色Missing Material记得在Inspector窗口中为Mesh Renderer组件分配一个材质或者像注释中那样在代码里动态创建。粉色是Unity表示“缺少可用Shader或材质”的默认颜色。完成以上步骤后你将得到一个白色的三角形面片。在Scene视图中选中它并在右上角的Gizmos下拉菜单中开启“Wireframe”显示就能清晰地看到它的三角面结构。4. 实战构建二构建一个完整的立方体立方体比三角形复杂因为它有6个面每个面由2个三角形组成共12个三角形。这里有一个核心技巧虽然立方体只有8个独特的顶点但为了给每个面独立的法线和UV我们通常需要定义24个顶点6个面 * 每个面4个顶点。这是因为一个顶点在属于不同面时其法线方向是不同的。4.1 定义24个顶点与法线我们采用“每个面独立定义顶点”的策略虽然数据量略有冗余但逻辑清晰且能保证每个面的法线和UV正确。using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class CreateCube : MonoBehaviour { public float size 1f; // 立方体尺寸 void Start() { MeshFilter meshFilter GetComponentMeshFilter(); Mesh mesh new Mesh(); mesh.name ProceduralCube; // 以原点为中心创建边长为size的立方体 float halfSize size / 2f; // 定义24个顶点 (6个面 * 4个顶点/面) Vector3[] vertices new Vector3[24]; // 前面 (Forward, Z) 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); // 后面 (Back, -Z) 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); // 上面 (Up, Y) vertices[8] new Vector3(-halfSize, halfSize, halfSize); vertices[9] new Vector3(halfSize, halfSize, halfSize); vertices[10] new Vector3(-halfSize, halfSize, -halfSize); vertices[11] new Vector3(halfSize, halfSize, -halfSize); // 下面 (Down, -Y) vertices[12] new Vector3(-halfSize, -halfSize, -halfSize); vertices[13] new Vector3(halfSize, -halfSize, -halfSize); vertices[14] new Vector3(-halfSize, -halfSize, halfSize); vertices[15] new Vector3(halfSize, -halfSize, halfSize); // 左面 (Left, -X) vertices[16] new Vector3(-halfSize, -halfSize, -halfSize); vertices[17] new Vector3(-halfSize, -halfSize, halfSize); vertices[18] new Vector3(-halfSize, halfSize, -halfSize); vertices[19] new Vector3(-halfSize, halfSize, halfSize); // 右面 (Right, X) vertices[20] new Vector3(halfSize, -halfSize, halfSize); vertices[21] new Vector3(halfSize, -halfSize, -halfSize); vertices[22] new Vector3(halfSize, halfSize, halfSize); vertices[23] new Vector3(halfSize, halfSize, -halfSize); mesh.vertices vertices; // 定义法线每个面的四个顶点法线方向相同垂直于面向外 Vector3[] normals new Vector3[24]; for (int i 0; i 4; i) normals[i] Vector3.forward; // 前面 for (int i 4; i 8; i) normals[i] Vector3.back; // 后面 for (int i 8; i 12; i) normals[i] Vector3.up; // 上面 for (int i 12; i 16; i) normals[i] Vector3.down; // 下面 for (int i 16; i 20; i) normals[i] Vector3.left; // 左面 for (int i 20; i 24; i) normals[i] Vector3.right; // 右面 mesh.normals normals;4.2 构建三角形序列与UV映射接下来我们需要将每个面的四个顶点拆分成两个三角形并设置UV坐标。// 定义三角形序列 (12个三角形 * 3个索引 36个索引) int[] triangles new int[36]; // 每个面的三角形索引模式 (0,1,2) 和 (2,1,3) int triIndex 0; for (int face 0; face 6; face) { int vertexIndex face * 4; // 当前面起始顶点索引 // 第一个三角形 triangles[triIndex] vertexIndex; triangles[triIndex] vertexIndex 1; triangles[triIndex] vertexIndex 2; // 第二个三角形 triangles[triIndex] vertexIndex 2; triangles[triIndex] vertexIndex 1; triangles[triIndex] vertexIndex 3; } mesh.triangles triangles; // 定义UV为简单起见每个面的UV都映射到纹理的整个(0,0)-(1,1)区域 // 更复杂的映射需要“纹理展开”(UV Unwrapping)知识 Vector2[] uvs new Vector2[24]; Vector2 uv00 new Vector2(0, 0); Vector2 uv10 new Vector2(1, 0); Vector2 uv01 new Vector2(0, 1); Vector2 uv11 new Vector2(1, 1); for (int face 0; face 6; face) { int uvIndex face * 4; uvs[uvIndex] uv00; uvs[uvIndex 1] uv10; uvs[uvIndex 2] uv01; uvs[uvIndex 3] uv11; } mesh.uv uvs; mesh.RecalculateBounds(); // 重新计算包围盒 meshFilter.mesh mesh; } }实操心得顶点顺序的重要性在定义每个面的四个顶点时我采用了一致的环绕顺序通常是顺时针或逆时针。这能保证所有面的正面法线指向都朝外。你可以用手比划一下从面的左下角开始逆时针或顺时针依次定义四个角点。三角形拆分模式将一个四边形拆分为两个三角形有两种常见方式(0,1,2)(2,1,3)或(0,1,3)(0,3,2)。我选择了第一种因为它形成的对角线拆分在大多数情况下视觉上更自然。无论选择哪种必须确保两个三角形的顶点顺序一致同为顺时针或逆时针否则会导致其中一个三角形背面剔除。UV的简单处理这里采用了最简单的“每个面完整贴图”策略。在实际项目中为了纹理连续比如让木纹环绕整个立方体你需要精心设计UV坐标这涉及到“UV展开”技术是一个专门的领域。运行脚本后你将得到一个标准的立方体。为其添加一个带有纹理的材质就能看到贴图效果。5. 实战构建三生成平滑的圆柱体圆柱体是曲面几何体它的构建思路是用多个分段Segment的矩形侧面来近似一个平滑的曲面再加上顶面和底面。分段数越多圆柱看起来越圆滑。5.1 构建侧面网格侧面的构建是圆柱体的核心。我们可以把它想象成一个正多棱柱的侧面。using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class CreateCylinder : MonoBehaviour { public float radius 0.5f; public float height 2f; public int radialSegments 16; // 侧面分段数越高越圆滑 public int heightSegments 1; // 高度分段可用于弯曲变形 void Start() { MeshFilter meshFilter GetComponentMeshFilter(); Mesh mesh new Mesh(); mesh.name ProceduralCylinder; // --- 1. 计算顶点总数 --- // 侧面顶点: (radialSegments 1) * (heightSegments 1) // 顶面中心点: 1 // 顶面边缘点: radialSegments // 底面中心点: 1 // 底面边缘点: radialSegments int sideVertCount (radialSegments 1) * (heightSegments 1); int topVertCount 1 radialSegments; int bottomVertCount 1 radialSegments; int totalVertCount sideVertCount topVertCount bottomVertCount; Vector3[] vertices new Vector3[totalVertCount]; Vector3[] normals new Vector3[totalVertCount]; Vector2[] uvs new Vector2[totalVertCount]; int vertIndex 0; // --- 2. 生成侧面顶点 --- float heightStep height / heightSegments; float angleStep 2f * Mathf.PI / radialSegments; for (int y 0; y heightSegments; y) { float currentHeight -height / 2f y * heightStep; // 从底部开始 for (int x 0; x radialSegments; x) // 注意是 为了闭合侧面 { float angle x * angleStep; float cosA Mathf.Cos(angle); float sinA Mathf.Sin(angle); // 顶点位置 vertices[vertIndex] new Vector3(radius * cosA, currentHeight, radius * sinA); // 法线方向从圆心指向外侧即(cosA, 0, sinA)并归一化 normals[vertIndex] new Vector3(cosA, 0, sinA).normalized; // UV坐标U沿环绕方向V沿高度方向 uvs[vertIndex] new Vector2((float)x / radialSegments, (float)y / heightSegments); vertIndex; } }这段代码通过两层循环构建了侧面的顶点网格。外层循环沿高度方向内层循环沿圆周方向。radialSegments决定了圆柱的圆滑度值太小如6会看到明显的棱角。5.2 构建侧面三角形与端面侧面顶点构成了一个网格我们需要用三角形将其缝合起来。同时我们还需要创建顶面和底面。// --- 3. 生成顶面顶点 --- int topCenterIndex vertIndex; vertices[vertIndex] new Vector3(0, height / 2f, 0); // 顶面中心 normals[vertIndex] Vector3.up; uvs[vertIndex] new Vector2(0.5f, 0.5f); // 中心UV vertIndex; for (int x 0; x radialSegments; x) { float angle x * angleStep; float cosA Mathf.Cos(angle); float sinA Mathf.Sin(angle); vertices[vertIndex] new Vector3(radius * cosA, height / 2f, radius * sinA); normals[vertIndex] Vector3.up; // 将圆形边缘的UV映射到一个单位圆内 uvs[vertIndex] new Vector2(cosA * 0.5f 0.5f, sinA * 0.5f 0.5f); vertIndex; } // --- 4. 生成底面顶点 (与顶面对称但法线朝下) --- int bottomCenterIndex vertIndex; vertices[vertIndex] new Vector3(0, -height / 2f, 0); normals[vertIndex] Vector3.down; uvs[vertIndex] new Vector2(0.5f, 0.5f); vertIndex; for (int x 0; x radialSegments; x) { float angle x * angleStep; float cosA Mathf.Cos(angle); float sinA Mathf.Sin(angle); vertices[vertIndex] new Vector3(radius * cosA, -height / 2f, radius * sinA); normals[vertIndex] Vector3.down; uvs[vertIndex] new Vector2(cosA * 0.5f 0.5f, sinA * 0.5f 0.5f); vertIndex; } // --- 5. 构建三角形序列 --- // 计算三角形总数 int sideTriCount radialSegments * heightSegments * 2; int capTriCount radialSegments * 2; // 顶面和底面各 radialSegments 个三角形 int totalTriCount (sideTriCount capTriCount) * 3; // 总索引数 int[] triangles new int[totalTriCount]; int triIndex 0; // 5.1 侧面三角形 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 bl currentRow x; // 当前行左下标 int br currentRow x 1; // 当前行右下标 int tl nextRow x; // 下一行左下标 int tr nextRow x 1; // 下一行右下标 // 第一个三角形 (bl, br, tl) triangles[triIndex] bl; triangles[triIndex] br; triangles[triIndex] tl; // 第二个三角形 (tl, br, tr) triangles[triIndex] tl; triangles[triIndex] br; triangles[triIndex] tr; } } // 5.2 顶面三角形 (扇形) for (int x 0; x radialSegments; x) { int edgeIndex topCenterIndex 1 x; int nextEdgeIndex topCenterIndex 1 ((x 1) % radialSegments); triangles[triIndex] topCenterIndex; // 中心点 triangles[triIndex] edgeIndex; triangles[triIndex] nextEdgeIndex; } // 5.3 底面三角形 (扇形注意顶点顺序要反过来以保证法线朝下) for (int x 0; x radialSegments; x) { int edgeIndex bottomCenterIndex 1 x; int nextEdgeIndex bottomCenterIndex 1 ((x 1) % radialSegments); triangles[triIndex] bottomCenterIndex; // 中心点 triangles[triIndex] nextEdgeIndex; // 顺序与顶面相反 triangles[triIndex] edgeIndex; } // --- 6. 赋值并完成 --- mesh.vertices vertices; mesh.normals normals; mesh.uv uvs; mesh.triangles triangles; mesh.RecalculateBounds(); meshFilter.mesh mesh; } }构建难点与技巧侧面网格的索引计算这是最易出错的部分。关键在于理解顶点在数组中的排列顺序。我们将侧面顶点存储为一个(heightSegments1)行 * (radialSegments1)列的网格。radialSegments1中的“1”是为了让纹理UV在环绕一圈后能无缝衔接从0回到1。计算三角形索引时需要找到每个小矩形由四个顶点构成对应的四个索引然后拆分成两个三角形。端面的扇形构造顶面和底面都是将一个中心点与一圈边缘点连接成三角形扇。特别注意底面三角形的顺序因为底面法线朝下为了保证三角形正面顺时针朝向外部其顶点顺序需要与顶面相反。UV映射的优化侧面的UV是简单的平面映射。而端面的UV我采用了将圆形投影到单位正方形内的映射方式(cosA*0.50.5, sinA*0.50.5)这能避免纹理在中心点过度扭曲。对于更高质量的贴图可能需要使用更复杂的展开算法。6. 性能优化、常见问题与高级应用方向当你成功创建出基础几何体后可能会遇到一些实际问题和性能考量。同时掌握基础后Mesh的世界才刚向你打开大门。6.1 性能优化核心减少Draw Call与网格合并动态生成Mesh在CPU端有一定开销但更大的瓶颈在于渲染。每一个使用不同材质的Mesh Renderer都会产生至少一个Draw Call。合并网格Mesh Combining如果你需要动态生成大量相同的简单几何体如一片草地、一堆石块不要为每个物体单独创建GameObject和MeshRenderer。相反应该将所有这些几何体的顶点数据合并到一个大的Mesh中然后只用一个MeshRenderer来渲染。这可以将成千上万个Draw Call减少到1个。Unity提供了Mesh.CombineMeshes方法来实现此功能。使用GPU Instancing对于大量相同的物体使用同一材质启用GPU Instancing是更高效的方案。它通过一次Draw Call渲染多个实例由GPU处理位置、旋转等变换。这需要Shader支持。在自定义Mesh时确保你的材质球勾选了“Enable GPU Instancing”选项。减少顶点数量在满足视觉需求的前提下尽量使用低分段数。动态生成的Mesh也要遵循游戏建模的优化原则。6.2 常见问题排查速查表问题现象可能原因解决方案物体不显示/闪烁1. 未添加或未正确赋值MeshRenderer和材质。2. 三角形顶点顺序为逆时针导致背面剔除。3. 未调用mesh.RecalculateBounds()被视锥体错误剔除。4. 相机近裁剪面Near Clip Plane设置过大物体在近裁剪面之内。1. 检查Inspector中Mesh Renderer组件和材质赋值。2. 检查triangles数组的顶点顺序确保从正面看是顺时针。3. 在设置完Mesh所有属性后务必调用mesh.RecalculateBounds()。4. 调整相机的Near属性为一个更小的值如0.01。物体显示为纯黑或粉色1. 粉色缺少有效材质或Shader出错。2. 纯黑法线计算错误或未赋值。法线方向错误会导致光照计算异常。1. 为Mesh Renderer分配一个有效的材质如Standard。2. 检查并重新计算法线。确保法线数组长度与顶点数组一致且方向正确通常指向面外。对于平面所有顶点法线可设为相同值。纹理拉伸或错乱UV坐标设置错误。UV值超出了常规的[0,1]范围或者顶点与UV的对应关系错乱。仔细检查UV数组的赋值逻辑。确保uvs[i]对应vertices[i]。对于简单几何体可以先将UV全部设为Vector2.zero再逐步调试每个面的映射。运行时顶点索引越界错误triangles数组中包含的索引值超出了vertices数组的最大索引vertices.Length - 1。仔细检查生成三角形索引的循环逻辑。使用Debug.Log打印vertices.Length和triangles数组中的最大值进行比对。确保索引计算公式正确。网格在编辑器中可见运行时消失脚本可能被多次执行或者Mesh数据在游戏运行后被意外修改/清除。确保生成Mesh的代码如Start()只执行一次。避免在Update()中每帧创建新的Mesh除非是用于动态变形。检查是否有其他脚本在修改或清空MeshFilter的mesh。6.3 从基础到进阶Mesh的无限可能掌握了手动构建静态Mesh后你可以探索更激动人心的领域动态网格变形在Update()中修改mesh.vertices数组可以实现波浪、融化、膨胀等效果。记得修改后要重新赋值mesh.vertices newVertices并调用mesh.RecalculateNormals()和mesh.RecalculateBounds()。程序化生成将上述几何体生成逻辑模块化、参数化可以轻松创建程序化地形、建筑、植被、道路等。这是开放世界游戏和沙盒游戏的核心技术之一。Mesh切割与破碎通过计算平面与Mesh的相交生成新的切面和顶点可以实现物体被切割或破碎的效果。这需要更复杂的几何算法。与ShaderGraph/自定义Shader配合将顶点位置、法线、UV等信息传递给自定义Shader可以实现基于顶点颜色的着色、曲面细分、顶点动画等高级图形效果。手动构建Mesh就像掌握了3D世界的“原子操作”它赋予了你突破预制件限制的自由。起初可能会被索引和数组搞得头晕但一旦你成功画出第一个三角形并看着它按照你的代码指令组合成复杂的形体那种对图形底层控制的成就感是无与伦比的。从今天这个三角形、立方体、圆柱体开始你的创造力将不再受限于已有的模型库。
返回列表