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

发布时间:2026/7/13 10:21:23

Unity Mesh编程入门:从三角形到圆柱体的程序化网格生成 1. 项目概述为什么我们要从零开始绘制Mesh如果你在Unity里做过3D项目大概率是从导入一个FBX或者OBJ模型开始的。拖拽进来挂上材质一个精致的角色或场景就出现了。这很方便但久而久之你可能会产生一种“黑盒”感这个模型到底是怎么被GPU画出来的它的顶点数据长什么样法线、UV这些玩意儿究竟是如何组织最终变成屏幕上那个立体的图形的这就是我们今天要聊的Mesh网格。你可以把它理解为一个3D模型的“骨架”或“蓝图”。所有你看到的3D物体无论多复杂本质上都是由无数个三角形或四边形拼接而成的表面而Mesh就是定义这些三角形顶点位置、连接关系以及表面属性如法线、UV的数据结构。Unity的MeshFilter组件持有这个蓝图MeshRenderer组件则负责根据蓝图和材质油漆把它画出来。那么自己动手用代码生成Mesh的意义何在我总结了几点彻底理解3D渲染的底层原理这是从“使用者”到“创造者”的关键一步。理解了Mesh你就理解了3D图形的基石。实现程序化生成内容比如随机生成的地形、动态变化的河流、被破坏的墙体碎片、科技感的数据可视化图表。这些都无法通过预制的静态模型完美实现。极致优化当你完全掌控顶点和三角形时你可以用最精简的数据来表达模型合并批次减少Draw Call这对于移动端或大型场景性能优化至关重要。解决特定问题比如修复导入模型时奇怪的接缝、动态修改模型顶点实现形变动画如旗帜飘动、水面波纹或者创建一些标准建模工具难以制作的特殊形状。网上很多教程一上来就丢给你一大段生成立方体的代码让人看得云里雾里。今天我们就从最简单的一个三角形开始逐步构建正方体和圆柱体我会把每一步“为什么这么做”都掰开揉碎讲清楚。你会发现一旦理解了核心逻辑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如何用这些顶点来组装成三角形面片。这里有两种方式三角形列表Triangle List: 直接列出每个三角形的三个顶点。例如一个四边形两个三角形需要6个顶点数据。这种方式简单直观但存在大量顶点重复一个顶点被多个三角形共享时在数据中会重复存储多次效率较低。索引绘制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的基本流程如下创建一个新的Mesh对象。准备好Vector3[] vertices数组并赋值给mesh.vertices。准备好int[] triangles数组并赋值给mesh.triangles。注意这一步必须在顶点数据赋值之后。可选但推荐计算并赋值法线mesh.normals和UVmesh.uv。调用mesh.RecalculateBounds()重新计算包围盒这对于裁剪和碰撞检测很重要。将创建好的Mesh赋值给MeshFilter组件的mesh属性。注意mesh.vertices等属性的setter方法内部会创建数据的副本。频繁修改Mesh时如每帧变形的顶点动画应考虑使用Mesh.SetVertices、Mesh.SetTriangles等List版本的方法或者直接操作Mesh.vertices返回的数组后再赋值以避免产生过多的GC垃圾回收开销。对于静态Mesh初始化时赋值一次即可。3. 实战演练一绘制第一个三角形理论说再多不如动手试一下。我们从一个在3D空间中的单面三角形开始。3.1 创建基础场景与脚本在Unity中创建一个空场景。在Hierarchy中创建一个空GameObject命名为“MeshGenerator”。为这个GameObject添加两个组件Mesh Filter和Mesh Renderer。创建一个新的C#脚本命名为GenerateTriangle挂载到“MeshGenerator”上。在Mesh Renderer组件上给它赋一个默认的材质比如Standard材质。3.2 代码实现与逐行解析打开GenerateTriangle.cs脚本我们编写以下代码using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class GenerateTriangle : MonoBehaviour { void Start() { // 1. 获取或创建MeshFilter组件 MeshFilter meshFilter GetComponentMeshFilter(); 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); // 顶点BX轴正方向1单位 vertices[2] new Vector3(0, 1, 0); // 顶点CY轴正方向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 GetComponentMeshFilter(); 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边形Nsegments来近似。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 GetComponentMeshFilter(); Mesh mesh new Mesh(); mesh.name ProceduralCylinder; ListVector3 vertices new ListVector3(); Listint triangles new Listint(); ListVector3 normals new ListVector3(); ListVector2 uvs new ListVector2(); // 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行以后顶部和底部使用“三角形扇”来构建。即一个中心顶点连接一圈边缘顶点形成多个三角形。底部三角形的顺序需要反转以确保从外部看下方时三角形是逆时针的。使用List和Set方法由于顶点和三角形数量是动态的使用ListT比数组更方便。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到1V对应高度0到1。对于平面投影可以使用顶点位置进行简单线性映射。使用Unity的Scene视图的UV Checker纹理进行可视化调试非常有效。模型在移动/旋转时闪烁或撕裂每帧都在创建新的Mesh对象产生大量GC垃圾回收。对于动态Mesh在Start或Awake中创建一次Mesh对象并缓存引用。在Update中直接修改其vertices等数组然后重新赋值mesh.vertices myVertices并调用mesh.RecalculateBounds()和mesh.RecalculateNormals()如需。性能低下1. 顶点数过多特别是radialSegments设置过高。2. 每帧更新整个Mesh且顶点数很多。1. 遵循“够用就好”原则在视觉可接受的范围内减少分段数。2. 对于静态Mesh确保其标记为Static以便Unity进行静态合批优化。3. 对于动态Mesh考虑使用Job System和Burst Compiler进行多线程计算或者使用Compute Shader在GPU上计算。6.2 性能优化与高级技巧重用与合并如果场景中有大量相同的程序化Mesh比如一片草地不要每个都单独生成一个Mesh。应该生成一个然后通过Graphics.DrawMesh或MaterialPropertyBlock进行批量绘制或者使用GPU Instancing。使用MeshDataAPI (Unity 2020.2): 对于高性能、多线程的Mesh生成和修改新的MeshData和Mesh.ApplyAndDisposeWritableMeshDataAPI是更好的选择它避免了主线程的阻塞和托管数组的拷贝。法线计算优化mesh.RecalculateNormals()很方便但它是基于三角形平均的可能不是你想要的效果特别是硬边。对于复杂Mesh手动计算法线比如根据顶点相邻面的加权平均能获得更好的控制。UV通道与切线除了主UV (mesh.uv)还有第二套UV (mesh.uv2)常用于光照贴图。如果你的材质需要法线贴图还必须正确计算并赋值mesh.tangents切线这是一个Vector4数组其w分量通常为1或-1用于决定副切线的方向。6.3 从原理到扩展你的Mesh之旅才刚刚开始通过三角形、立方体、圆柱体这三个例子我们已经走完了从理解概念到实现基础几何体的完整路径。但这仅仅是开始。Mesh的世界无比广阔复杂模型你可以用算法生成山脉使用噪声函数、树木、云朵。动态变形修改vertices数组的Y值就能做出水面波浪根据距离修改顶点颜色就能实现溶解效果。布尔运算通过算法求两个Mesh的交、并、差集可以实现程序化的建模。从数据到可视化将任何三维数据如科学数据、点云转换为Mesh进行立体可视化。我个人的体会是手动创建Mesh就像在给GPU直接“喂数据”。一开始会觉得繁琐但当你能够随心所欲地生成和操控这些顶点与三角形时你对3D图形渲染的理解会达到一个新的层次。它打破了预制模型的黑盒让你获得了在运行时创造无限可能性的能力。下次当你遇到一个需要特殊形状或动态效果的需求而资源商店里又找不到合适的东西时不妨想想我能不能用代码把它“画”出来

相关新闻