引言:

  • 本笔记为各方资料整理,如有侵权请联系

UnityMesh网格编程

一、创建一个简单的网格

1.渲染事物

在 Unity 中,如果你想将某些内容可视化,可以使用网格。它可以是从其他程序导出的 3D 模型,也可以是程序生成的网格,还可以是精灵图、UI 元素或粒子系统,这些 Unity 也使用网格来实现。甚至屏幕效果也使用网格进行渲染。

那么,什么是网格??

一组3D空间点+一组连接点的三角形(这些三角形构成了网格所代表内容的表面。)

由于三角形是平面的,且边缘笔直,因此它们可以用来完美地呈现平面和直线的物体,例如立方体的面。曲面或圆形表面只能通过使用许多小三角形来近似。如果三角形看起来足够小(不大于一个像素),那么您将无法察觉到这种近似。通常,这对于实时性能来说是不可行的,因此表面总会呈现出某种程度的锯齿状。

image-20250616150715588

​ 图1 Unity默认立方体以及线宽(能看到锯齿)

如何显示线框?

您可以在场景视图的左侧工具栏中选择其显示模式。前三个选项分别是“Shaded”(阴影)、“Wireframe”(线框)和“Shaded Wireframe”(阴影线框)。

如何显示3D模型?

它需要两个组件。

  • 第一个是网格过滤器。该组件保存对要显示的网格的引用。
  • 第二个是网格渲染器。使用它来配置网格的渲染方式,例如使用哪种材质、是否投射或接收阴影等等。
image-20250616151129233

​ 图2 网格过滤器和渲染器

为什么网格渲染器里面可以添加一系列的材质???
网格渲染器可以拥有多种材质。这主要用于渲染包含多个独立三角形集(称为子网格)的网格。

您可以通过调整网格的材质来彻底改变其外观。Unity 的默认材质是纯白色。您可以通过*“Assets / Create / Material”*创建新的材质资源并将其拖到游戏对象上,将其替换为您自己的材质。新材质默认使用 Unity 的标准着色器,该着色器为您提供了一组控件来调整表面的视觉效果。

为网格添加大量细节的一种快速方法是提供反照率贴图。反照率贴图是一张表示材质基本颜色的纹理。当然,我们需要知道如何将此纹理投影到网格的三角形上。这可以通过向顶点添加二维纹理坐标来实现。纹理空间的两个维度称为UV,这就是它们被称为 UV 坐标的原因。这些坐标通常位于**(0, 0)(1, 1)**之间,覆盖整个纹理。根据纹理设置,超出该范围的坐标要么被限制,要么导致平铺。

image-20250616152944332 image-20250616152958126

​ 图3 Unity的UV测试网格

2.创建顶点网格

让我们先来了解一下,先生成一个简单的矩形网格。该网格由单位长度的正方形图块(四边形)组成。创建一个新的 C# 脚本,并将其转换为具有水平和垂直尺寸的网格组件。

1
2
3
4
5
6
7
8
9
10
11
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Grid : MonoBehaviour
{
[Header("网格水平尺寸")]
public int xSize;
[Header("网格垂直尺寸")]
public int ySize;
}

当我们将此组件添加到游戏对象时,我们还需要为其添加网格过滤器和网格渲染器。我们可以在类中添加一个属性,让 Unity 自动添加它们。

1
2
3
4
5
6
7
8
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Grid : MonoBehaviour
{
[Header("网格水平尺寸")]
public int xSize;
[Header("网格垂直尺寸")]
public int ySize;
}

现在,您可以创建一个新的空游戏对象,并向其中添加网格组件,这样它就包含另外两个组件了。设置渲染器的材质,并保留滤镜的网格。我将网格的尺寸设置为 10 x 5。

image-20250616154403486

​ 图4 网格对象

一旦对象被唤醒,我们就会生成实际的网格.

1
2
3
4
private void Awake()
{
Generate();
}

我们先关注顶点位置,三角形留到以后再说。

我们需要一个 3D 矢量数组来存储这些点。

顶点的数量取决于网格的大小。

每个四边形的角上都需要一个顶点,但相邻的四边形可以共享同一个顶点

因此,我们需要的顶点数量比每个维度上的图块数量多一个

水平顶点数(x+1) 垂直顶点数(y+1)

image-20250616154749059

​ 图5 4X2网格示例和四边形索引

让我们将这些顶点可视化,以便检查它们的位置是否正确。我们可以添加一个OnDrawGizmos方法,并在场景视图中为每个顶点绘制一个小黑球。

什么是Gizmo
Gizmo 是您可以在编辑器中使用的视觉提示。默认情况下,它们在场景视图中可见,在游戏视图中不可见,但您可以通过其工具栏进行调整。Gizmos工具类允许您绘制图标、线条和其他一些内容。

当我们不在运行模式时,这会产生错误,因为OnDrawGizmos当 Unity 处于编辑模式时,即使我们没有任何顶点,也会调用方法。为了避免此错误,请检查数组是否存在,如果不存在,则跳出该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void OnDrawGizmos()
{
//后插入
if (vertices == null)
{
return;
}
//
Gizmos.color = Color.black;
for (int i = 0; i < vertices.Length; i++)
{
Gizmos.DrawSphere(vertices[i], 0.1f);
}
}

image-20250616160503013

​ 图6 绘制的顶点

此时所有的顶点默认都是原点位置,所以我们要在 Generate();函数中将位置重新赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
private void Generate()
{
vertices = new Vector3[(xSize + 1) * (ySize + 1)];
//双循环赋值
for (int i = 0, y = 0; y <= ySize; y++)
{
for (int x = 0; x <= xSize; x++, i++)
{
vertices[i] = new Vector3(x, y);
}
}

}
image-20250616160804360

​ 图7 网格顶点绘制完毕

为什么小黑点不会随着物体移动????
Gizmo直接在世界空间中绘制,而不是在对象的局部坐标空间中绘制。如果希望它们遵循对象的变换,则必须使用transform.TransformPoint(vertices[i])。

现在我们可以看到顶点了,但它们的放置顺序却不可见。我们可以用颜色来显示,也可以使用协程来减慢这个过程。这就是我把它添加using System.Collections到脚本中的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void Awake()
{
StartCoroutine(Generate());
}

private IEnumerator Generate()
{
WaitForSeconds wait = new WaitForSeconds(0.05f);
vertices = new Vector3[(xSize + 1) * (ySize + 1)];
for (int i = 0, y = 0; y <= ySize; y++)
{
for (int x = 0; x <= xSize; x++, i++)
{
vertices[i] = new Vector3(x, y);
yield return wait;
}
}

}
498e2fe5-8a6c-4e7e-987a-a7fd063678cc

​ 图8 顶点顺序绘制

3.创建网格

现在我们知道顶点的位置正确了,我们就可以处理实际的网格了。除了在我们自己的组件中保存对它的引用之外,我们还必须将其赋值给网格过滤器。处理完顶点后,我们就可以将它们赋值给网格了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Mesh mesh;
private IEnumerator Generate()
{
WaitForSeconds wait = new WaitForSeconds(0.05f);
//创建一个网格
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Grid";
vertices = new Vector3[(xSize + 1) * (ySize + 1)];
for (int i = 0, y = 0; y <= ySize; y++)
{
for (int x = 0; x <= xSize; x++, i++)
{
vertices[i] = new Vector3(x, y);
yield return wait;
}
}
//把创建好的网格顶点索引传给mesh里面的网格
mesh.vertices = vertices;
}
image-20250616163914574

​ 图8 运行时出现网格

现在,我们在播放模式下已经有了一个网格,但它还没有显示出来,因为我们还没有给它分配任何三角形。

三角形是通过顶点索引数组定义的。

由于每个三角形有三个点,所以三个连续的索引可以描述一个三角形。让我们先从一个三角形开始。

1
2
3
4
5
6
7
8
9
private IEnumerator Generate () {


int[] triangles = new int[3];
triangles[0] = 0;
triangles[1] = 1;
triangles[2] = 2;
mesh.triangles = triangles;
}

我们现在有一个三角形,但我们使用的三个点都在一条直线上。这会产生一个退化的三角形,不可见。前两个顶点没有问题,但我们应该跳到下一行的第一个顶点。

image-20250616190348686

​ 图9 绘制三角形(反)

1
2
3
triangles[0] = 0;
triangles[1] = 1;
triangles[2] = xSize + 1;

这确实给出了一个三角形,但它只能从一个方向可见。在这种情况下,只有从 Z 轴的反方向看时才可见。

所以你可能需要旋转视图才能看到它。

三角形从哪一侧可见取决于其顶点索引的方向。

默认情况下,如果它们按顺时针方向排列,则该三角形被视为朝前且可见。

逆时针方向的三角形会被丢弃,这样我们就不需要花时间渲染物体的内部,因为通常情况下,物体的内部是不应该被看到的。

img

​ 图10 三角形索引顺序

因此,为了让三角形在 Z 轴方向上可见,我们必须改变顶点的遍历顺序。我们可以通过交换最后两个索引来实现。

1
2
3
triangles[0] = 0;
triangles[1] = xSize + 1;
triangles[2] = 1;
image-20250616190712982

​ 图11 第一个三角形

现在,我们已经有一个三角形覆盖了网格第一个图块的一半。要覆盖整个图块,我们只需要第二个三角形。

1
2
3
4
5
6
7
int[] triangles = new int[6];
triangles[0] = 0;
triangles[1] = xSize + 1;
triangles[2] = 1;
triangles[3] = 1;
triangles[4] = xSize + 1;
triangles[5] = xSize + 2;
image-20250616191103351

​ 图11 由两个三角形组成的四边形

由于这些三角形共享两个顶点,我们可以将其减少到四行代码,每个顶点索引仅明确提及一次。

1
2
3
4
   triangles[0] = 0;
triangles[3] = triangles[2] = 1;
triangles[4] = triangles[1] = xSize + 1;
triangles[5] = xSize + 2;
image-20250616191614332

​ 图12 第一块正方形面片

我们可以通过将其转换为循环来创建完整的第一行图块。由于我们同时迭代顶点和三角形索引,因此必须同时跟踪它们。我们还可以将 yield 语句移到这个循环中.

1
2
3
4
5
6
7
8
9
triangles = new int[xSize * 6];//6个三角形顶点为一个小正方形循环,所以有xSize* 6 个三角形顶点
for (int ti = 0, vi = 0, x = 0; x < xSize; x++, ti += 6, vi++) {
triangles[ti] = vi;
triangles[ti + 3] = triangles[ti + 2] = vi + 1;
triangles[ti + 4] = triangles[ti + 1] = vi + xSize + 1;
triangles[ti + 5] = vi + xSize + 2;
mesh.triangles = triangles;
yield return wait;
}
image-20250616201319784

​ 图13 第一行网格

现在将单循环改为双循环,填充整个网格。注意,移动到下一行需要将顶点索引加一,因为每行的顶点数比图块数多一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
triangles = new int[xSize * ySize * 6];
for (int ti = 0, vi = 0, y = 0; y < ySize; y++, vi++)
{
//比如第一次外循环结束vi是10,但是第二行第一个是11,所以要在外循环也++
for (int x = 0; x < xSize; x++, ti += 6, vi++)
{
triangles[ti] = vi;
triangles[ti + 3] = triangles[ti + 2] = vi + 1;
triangles[ti + 4] = triangles[ti + 1] = vi + xSize + 1;
triangles[ti + 5] = vi + xSize + 2;
yield return wait;
mesh.triangles = triangles;
}
}
image-20250616202016661

​ 图13 10 * 5网格渲染完成

4.生成附加顶点数据

我们的网格目前以一种奇怪的方式被照亮。这是因为我们还没有给网格指定任何法线。默认的法线方向是**(0, 0, 1)**,这与我们需要的方向完全相反。

法线如何运作?
法线是垂直于表面的向量。我们通常使用单位长度的法线,它们指向表面的外部,而不是内部
法线可以用来确定光线照射到表面的角度(如果有的话)。具体如何使用法线取决于着色器
由于三角形始终是平面的,因此无需单独提供法线信息。然而,这样做可以起到一定的欺骗作用。
实际上,顶点没有法线,而三角形有。通过将自定义法线附加到顶点,并在三角形之间进行插值,我们可以假装拥有一个平滑弯曲的表面,而不是一堆平面三角形。只要你不注意网格的锐利轮廓,这种视觉效果就很逼真。

法线是每个顶点定义的,所以我们必须填充另一个向量数组。

或者,我们可以让网格根据其三角形自行计算法线。这次我们偷懒一下,直接这样做。

1
2
3
4
5
private void Generate () {

mesh.triangles = triangles;
mesh.RecalculateNormals();
}
如何重新计算法线?
Mesh.RecalculateNormals方法通过找出哪些三角形与该顶点相连、确定这些平面三角形的法线、对它们进行平均以及对结果进行标准化来计算每个顶点的法线。
image-20250616203338474

​ 图14 添加法线后

接下来是 UV 坐标。你可能已经注意到,即使网格使用了带有反照率纹理的材质,它目前的颜色也是统一的。这是有道理的,因为如果我们不自己提供 UV 坐标,那么它们就全为零

为了使纹理适合我们的整个网格,只需将顶点的位置除以网格尺寸。归一化即可

1
2
3
4
5
6
7
8
9
10
Vector2[] uv = new Vector2[vertices.Length];
for (int i = 0, y = 0; y <= ySize; y++)
{
for (int x = 0; x <= xSize; x++, i++)
{
vertices[i] = new Vector3(x, y);
uv[i] = new Vector2(((float)x) / xSize, ((float)y) / ySize);
}
}
mesh.uv = uv;
image-20250616210113460

​ 图15 uv归一化后正确显示材质

另一种为表面添加更多细节的方法是使用法线贴图。

这些贴图包含以颜色编码的法线向量。将它们应用于表面,可以产生比单独使用顶点法线更细致的光照效果

image-20250616210741792 image-20250616210804900

​ 图16 法线贴图材质

将此材质应用于我们的网格会产生凹凸效果,但这是不正确的。我们需要在网格中添加切向量来正确调整它们的方向。

切线如何起作用?
法线贴图定义在切线空间中。切线空间是一个围绕物体表面流动的3D空间。这种方法允许我们在不同的位置和方向上应用相同的法线贴图。
表面法线代表该空间的向上方向,但哪个方向是正确的呢?这由切线定义。理想情况下,这两个向量之间的夹角为 90°。它们的叉积给出了定义 3D 空间所需的第三个方向。实际上,这个角度通常不是 90°,但结果仍然足够好。
因此,切线是一个 3D 向量,但 Unity 实际上使用的是 4D 向量。它的第四个分量始终为 -1 或 1,用于控制切线空间的第三个维度的方向——向前或向后。这有利于法线贴图的镜像,法线贴图通常用于具有左右对称性的物体(例如人体)的 3D 模型。Unity 着色器执行此计算的方式要求我们使用 -1。

由于我们有一个平面,所以所有切线都指向同一个方向,即向右。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vertices = new Vector3[(xSize + 1) * (ySize + 1)];
Vector2[] uv = new Vector2[vertices.Length];
//Vector4[] tangents = new Vector4[vertices.Length];
//Vector4 tangent = new Vector4(1f, 0f, 0f, -1f);切线内容
for (int i = 0, y = 0; y <= ySize; y++) {
for (int x = 0; x <= xSize; x++, i++) {
vertices[i] = new Vector3(x, y);
uv[i] = new Vector2((float)x / xSize, (float)y / ySize);
//tangents[i] = tangent;
}
}
mesh.vertices = vertices;
mesh.uv = uv;
//mesh.tangents = tangents;
image-20250617130608416

​ 图17 未用切线坐标(凹凸效果与法线贴图不一致)

image-20250617130426089

​ 图18 使用切线坐标(两次凹凸效果是不一致的)

关于切线后期会补充内容。

二、绘制圆角立方体

  • 创建一个具有无缝网格的立方体。
  • 为立方体添加圆边。
  • 定义法线。
  • 使用子网格。
  • 创建自定义着色器。
  • 组合原始碰撞器。

1.合成立方体

处理完二维网格后,下一步就是程序化地生成三维结构。我们来看一下立方体。

从概念上讲,它由六个二维面组成,这些面经过定位和旋转,最终形成一个三维体。我们可以用六个网格实例来实现这一点。

我们的网格位于 XY 平面,朝向负 Z 轴方向。这是立方体的负 Z 轴面。你可以复制它,将其 Y 轴旋转设置为 180°,然后重新定位,使两个面对齐,从而创建正 Z 轴面。

-X 和 +X 面的创建方式相同,但 Y 轴旋转 90° 和 270°。如果需要,您也可以为这些面指定与 Z 面不同的xSize ,但它们的**ySize必须匹配。四个面必须对齐,形成一个闭合的环。

-Y 面和 +Y 面分别由 X 轴旋转 270° 和 90° 构成。它们的x 轴大小应与 Z 面的 x 轴大小一致,y 轴大小应与X 面的x 轴大小一致。

image-20250617131451791

​ 图19 由六个独立网格构成的立方体

这样我们就得到了一个由六个独立网格组成的立方体。虽然看起来不错,可以作为参考,但实际操作起来不太方便。我们可以通过 来组合这些网格Mesh.CombineMeshes,但不如一次性创建整个立方体。

2.创建立方体顶点

要创建我们自己的立方体,我们需要创建一个新的组件脚本。让我们利用上一个教程中的一些代码来引导它。

目前唯一的新内容是第三维,所以我们必须添加zSize。我再次使用了协程和 Gizmos 来帮助可视化正在发生的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Cube : MonoBehaviour
{
[Header("水平尺寸")]
public int xSize;
[Header("垂直尺寸")]
public int ySize;
[Header("深度尺寸")]
public int zSize;

private Mesh mesh;
private Vector3[] vertices;
private void Awake()
{
StartCoroutine(Generate());
}

private IEnumerator Generate()
{
this.GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Cube";
WaitForSeconds wait = new WaitForSeconds(0.05f);
yield return wait;
}
//画点
private void OnDrawGizmos()
{
if (vertices == null)
{
return;
}
Gizmos.color = Color.black;
for (int i = 0; i < vertices.Length; i++)
{
Gizmos.DrawSphere(vertices[i], 0.1f);
}
}
}

在添加立方体的顶点之前,我们首先需要知道它有多少个顶点。

我们已经知道单个面所需的顶点数量。

(#x+1)(#y+1)

所以我们只需将这六个面加在一起就可以得到总数。

2((#x+1)(#y+1)+(#x+1)(#z+1)+(#y+1)(#z+1))

然而,由于面的边缘相互接触,它们的顶点会重叠,从而导致重复顶点。立方体的每个角顶点数增加了三倍,而其边缘上的所有顶点数增加了一倍。

image-20250617133258311

​ 图20 重叠的面顶点

那么我们需要多少个顶点呢?我们按类型来分解一下。首先,我们有八个角顶点,这很容易理解。然后,我们有十二条边,每个方向四条。由于不包含角,每条边的顶点数等于其对应大小减一。或者,我们可以将其视为四组 X、Y 和 Z 边。

不包含角的十二条边上的顶点:4(#x+#y+#z−3)

其余顶点位于面内。这相当于一个顶点重复的立方体,其尺寸缩小了一半。

中间顶点:2((#x−1)(#y−1)+(#x−1)(#z−1)+(#y−1)(#z−1))

image-20250617134934155

​ 图21 中间顶点

计算顶点个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 角顶点
/// </summary>
int cornerVertices;

/// <summary>
/// 边顶点
/// </summary>
int edgeVertices;

/// <summary>
/// 面顶点
/// </summary>
int faceVertices;

cornerVertices = 8;
edgeVertices = 4 * (xSize + ySize + zSize - 3);//其实为+3-6
faceVertices = (
(xSize - 1) * (ySize - 1) +
(xSize - 1) * (zSize - 1) +
(ySize - 1) * (zSize - 1) )* 2;
vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];

定位第一面行的顶点与定位网格的第一行完全相同。

1
2
3
4
5
6
7
vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];

int v = 0;
for (int x = 0; x <= xSize; x++) {
vertices[v++] = new Vector3(x, 0, 0);
yield return wait;
}

接下来会变得更有趣。我们继续从第二个面的第一行开始,依此类推,创建一个由顶点组成的方形环。

这需要循环四次,使用不同的范围和位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (int x = 0; x <= xSize; x++) {
vertices[v++] = new Vector3(x, 0, 0);
yield return wait;
}
for (int z = 1; z <= zSize; z++) {
vertices[v++] = new Vector3(xSize, 0, z);
yield return wait;
}
for (int x = xSize - 1; x >= 0; x--) {
vertices[v++] = new Vector3(x, 0, zSize);
yield return wait;
}
for (int z = zSize - 1; z > 0; z--) {
vertices[v++] = new Vector3(0, 0, z);
yield return wait;
}

e7af6ca4-91c7-4c83-9239-78c220947fc2

​ 图21 出现底层顶点

通过沿 Y 轴重复放置环,我们可以将其变成立方体高度的完整环绕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	int v = 0;
//for (int y = 0; y <= ySize; y++) {
for (int x = 0; x <= xSize; x++) {
vertices[v++] = new Vector3(x, y, 0);
yield return wait;
}
for (int z = 1; z <= zSize; z++) {
vertices[v++] = new Vector3(xSize, y, z);
yield return wait;
}
for (int x = xSize - 1; x >= 0; x--) {
vertices[v++] = new Vector3(x, y, zSize);
yield return wait;
}
for (int z = zSize - 1; z > 0; z--) {
vertices[v++] = new Vector3(0, y, z);//坐标中有y
yield return wait;
}
// }

1dcdfc75-e486-47db-a3ba-e6fd6c07841f

​ 图21 外层顶点绘制

之后,我们需要封住顶部和底部。我像普通网格一样填充孔洞。

1
2
3
4
5
6
7
8
9
10
11
12
for (int z = 1; z < zSize; z++) {
for (int x = 1; x < xSize; x++) {
vertices[v++] = new Vector3(x, ySize, z);
yield return wait;
}
}
for (int z = 1; z < zSize; z++) {
for (int x = 1; x < xSize; x++) {
vertices[v++] = new Vector3(x, 0, z);
yield return wait;
}
}
image-20250617143259775

​ 图22 所有顶点位置(无重复)

3.添加三角形

现在顶点的位置已经正确,我们也熟悉了它们的放置顺序,接下来可以开始处理三角形了。为了做好准备,我移除了协程相关的部分,并添加了分别用于创建顶点和三角形的方法。当然,顶点需要被赋值给网格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void Awake () {
Generate();
}

private void Generate () {
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Cube";
CreateVertices();
CreateTriangles();
}

private void CreateVertices () {


mesh.vertices = vertices;
}

private void CreateTriangles () {
}

单个四边形的创建与网格完全相同。由于我们最终会在多个位置创建四边形,因此最好为其创建一个方法。

1
2
3
4
5
6
7
8
private static int
SetQuad (int[] triangles, int i, int v00, int v10, int v01, int v11) {
triangles[i] = v00;
triangles[i + 1] = triangles[i + 4] = v01;
triangles[i + 2] = triangles[i + 3] = v10;
triangles[i + 5] = v11;
return i + 6;
}
image-20250617154706619
																							   图23  四边形解剖图

与顶点不同,三角形的数量等于六个面的总和。它们是否使用共享顶点并不重要

1
2
3
4
5
private void CreateTriangles () {
int quads = (xSize * ySize + xSize * zSize + ySize * zSize) * 2;
int[] triangles = new int[quads * 6];
mesh.triangles = triangles;
}

创建第一个三角形行与创建网格相同。目前唯一的区别是下一行顶点的偏移量等于整个顶点环的偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void CreateTriangles()
{
//三角形数量
int quads = (xSize * ySize + xSize * zSize + ySize * zSize) * 2;
int[] triangles = new int[quads * 6];

//一圈的顶点
int ring = (xSize + zSize) * 2;
int t = 0, v = 0;
for (int q = 0; q < ring - 1; q++, v++)
{
t = SetQuad(triangles, t, v, v + 1, v + ring, v + ring + 1);
}
//它的第二个和第四个顶点需要回溯到环的起始位置
t = SetQuad(triangles, t, v, v - ring + 1, v + ring, v + 1);
mesh.triangles = triangles;
}

image-20250617161526556

​ 图24 一周面片

要对所有环进行三角剖分,只需再次沿 Y 重复该过程。请注意,每个环之后都需要增加顶点索引,因为我们的环路短了一步。

1
2
3
4
5
6
7
8
9
for (int y = 0; y < ySize; y++, v++)
{
for (int q = 0; q < ring - 1; q++, v++)
{
t = SetQuad(triangles, t, v, v + 1, v + ring, v + ring + 1);
}
//它的第二个和第四个顶点需要回溯到环的起始位置
t = SetQuad(triangles, t, v, v - ring + 1, v + ring, v + 1);
}
image-20250617161759475

​ 图25 外围所有面片