# 1. 基本概念

  • MeshFilter:负责存储 Mesh 数据。它将 Mesh(包含顶点、边、面等几何数据)赋予对象,使其具有形状。
  • MeshRenderer:负责渲染 Mesh。它使用材质(Material)对 Mesh 进行渲染,使其在场景中可见。
  • MeshCollider:一种碰撞器组件,允许物体根据 Mesh 形状进行碰撞检测。

# 2. Mesh 的基本结构

Mesh 通常由以下元素组成:

  • 顶点(Vertices):Mesh 的基本组成单位,一个顶点就是一个 3D 空间中的点。Mesh 的形状通过连接这些顶点来定义。
  • 三角形(Triangles):由顶点组成的面,用于定义 Mesh 的表面。
  • 法线(Normals):用于定义每个顶点的方向,影响光照效果。
  • UV 坐标(UV Coordinates):用于将纹理贴图应用到 Mesh 上,定义了每个顶点在纹理图上的位置。

# 3. 创建 Mesh

# 1. 顶点 (Vertices)

顶点是 3D 空间中的一个点,是构成 3D 模型的基本单元。一个 3D 模型的形状是由多个顶点和顶点之间的连接方式决定的。

  • 表示方式:在 Unity 中,顶点通常用 Vector3 表示,包括 xyz 坐标。
  • 用途:顶点的集合决定了物体的形状,通过连接顶点可以形成边和面。顶点位置的改变会直接影响模型的形状。
  • 示例:一个简单的正方形平面可以由 4 个顶点定义,如 [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]

# 2. 三角形 (Triangles)

三角形是由三个顶点构成的一个平面,是 3D 模型的基本面。所有复杂的 3D 模型都是由大量三角形组合而成的,因为三角形是最简单的平面,可以稳定地定义在 3D 空间中。

  • 表示方式:在 Unity 中,三角形通过一个顶点索引数组来定义。例如, [0, 1, 2] 表示使用第 0、1、2 个顶点构成一个三角形。
  • 用途:三角形是 Mesh 的渲染基础。Unity 会根据三角形数据来生成物体的表面,并在这些表面上应用纹理和光照。
  • 示例:一个由 4 个顶点组成的正方形平面需要用 2 个三角形表示,如 [(0, 1, 2), (0, 2, 3)]

# 3. 法线 (Normals)

法线是一个垂直于三角形或顶点表面的向量,用于定义表面朝向。它影响模型如何与光源交互,从而影响视觉效果,如阴影和高光。

  • 表示方式:在 Unity 中,法线通常用 Vector3 表示,包括 xyz 坐标。
  • 用途:法线决定了光照如何应用在模型表面。正确的法线可以让光照看起来更自然,模型更有立体感。
  • 示例:如果一个平面在 X-Z 平面上,法线通常会指向 Y 轴正方向 (0, 1, 0) ,表示平面朝上。

# 4. UV 坐标 (UV Coordinates)

UV 坐标用于将 2D 纹理贴图映射到 3D 模型的表面。UV 坐标定义了模型表面上每个顶点在纹理图上的位置。

  • 表示方式:UV 坐标通常用 Vector2 表示,包括 UV 分量,范围为 [0, 1]
    • U 表示纹理的水平坐标。
    • V 表示纹理的垂直坐标。
  • 用途:UV 坐标控制了纹理在模型表面的显示方式。通过调整 UV 坐标,可以改变纹理的平铺、缩放和对齐方式。
  • 示例:一个简单的正方形平面的 UV 坐标可能是 [(0, 0), (1, 0), (1, 1), (0, 1)] ,表示将整个纹理贴图到平面上。

# 示例:在 Unity 中创建一个简单的 Mesh

假设我们要创建一个 1x1 的正方形平面,以下是顶点、三角形、法线和 UV 的示例:

row
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
Mesh mesh = new Mesh();

// 定义 4 个顶点,组成正方形的四个角
Vector3[] vertices = new Vector3[]
{
new Vector3(0, 0, 0), // 顶点 0
new Vector3(1, 0, 0), // 顶点 1
new Vector3(1, 1, 0), // 顶点 2
new Vector3(0, 1, 0) // 顶点 3
};

// 定义 2 个三角形,使 4 个顶点形成一个平面
int[] triangles = new int[]
{
0, 1, 2, // 三角形 1
0, 2, 3 // 三角形 2
};

// 定义每个顶点的法线,表示该平面朝向 Z 轴正方向
Vector3[] normals = new Vector3[]
{
new Vector3(0, 0, 1), // 顶点 0 的法线
new Vector3(0, 0, 1), // 顶点 1 的法线
new Vector3(0, 0, 1), // 顶点 2 的法线
new Vector3(0, 0, 1) // 顶点 3 的法线
};

// 定义每个顶点的 UV 坐标,使纹理完全覆盖整个平面
Vector2[] uv = new Vector2[]
{
new Vector2(0, 0), // 顶点 0 的 UV
new Vector2(1, 0), // 顶点 1 的 UV
new Vector2(1, 1), // 顶点 2 的 UV
new Vector2(0, 1) // 顶点 3 的 UV
};

// 设置 Mesh 的顶点、三角形、法线和 UV
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.normals = normals;
mesh.uv = uv;

# 4. 动态生成和修改 Mesh

在游戏过程中,可以动态修改 Mesh 数据。例如,改变顶点的位置可以让 Mesh 变形。以下是如何动态修改 Mesh 顶点位置的示例:

row
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Update()
{
Mesh mesh = GetComponent<MeshFilter>().mesh;
Vector3[] vertices = mesh.vertices;

// 修改每个顶点的位置
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].y = Mathf.Sin(Time.time + i); // 根据时间生成动态效果
}

mesh.vertices = vertices;
mesh.RecalculateNormals(); // 更新法线以影响光照
}

# 5. Mesh 的应用

  • 地形生成:可以通过 Perlin Noise 或其他算法生成地形。
  • 角色建模和动画:Mesh 可以应用于角色建模,还可以通过骨骼动画(Skinned Mesh)进行变形。
  • 破碎效果:通过分割 Mesh,可以实现物体破碎的视觉效果。
  • 动态水面:修改 Mesh 顶点可以实现水面波动的效果。

# 6. 注意事项

  • 性能:修改 Mesh 可能影响性能,尤其是复杂的 Mesh 或频繁更新的情况。
  • 法线和 UV:需要正确设置法线和 UV,确保光照和纹理显示正常。
  • Recalculate:修改 Mesh 后需要调用 RecalculateNormalsRecalculateBounds 等方法来更新法线和边界框。

# 完整示例代码

row
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
using System.Collections;

using System.Collections.Generic;

using UnityEngine;



public class WorldGenerator : MonoBehaviour

{



    public Vector2 dimesions;

    public Material meshMaterial;



    public float perlinScale;

    public float waveHeight;

    public float offset;



    public float scale;

    // Start is called before the first frame update

    void Start()

    {

        CreateCylinder();

    }



    // Update is called once per frame

    void Update()

    {

    }

    public void CreateCylinder(){

        //Mesh通过网格绘制的,MeshFiltrer 持有的Mesh的引用,MeshRenderer持有的材质的引用

        //创建 gameobject并且命名

        GameObject newCylinder = new GameObject();

        newCylinder.name = "World piece";

        //添加MeshFilter组件 和 MeshRenderer组件

        MeshFilter meshFilter = newCylinder.AddComponent<MeshFilter>();

        MeshRenderer meshRenderer = newCylinder.AddComponent<MeshRenderer>();

        //添加材质 包含纹理,shader等

        meshRenderer.material = meshMaterial;

        //创建一个网格

        meshFilter.mesh =Generate();



        //创建网格以后,添加碰撞,适配新的mesh

        newCylinder.AddComponent<MeshCollider>();

    }



    Mesh Generate(){

        Mesh mesh = new Mesh();

        mesh.name = "MESH";

        //顶点

        Vector3[] vertices = new Vector3[4];

        //三角形

        int[] triangles = new int[6];

        //UV

        Vector2[] uv = new Vector2[4];

        CreateShape(ref vertices, ref uv, ref triangles);



        //再去赋值

        mesh.vertices = vertices;

        mesh.triangles = triangles;

        mesh.uv = uv;

        return mesh;

    }



    void CreateShape(ref Vector3[] vertices, ref Vector2[] uvs, ref int[] triangles)

    {

        int xCount = (int)dimesions.x;

        int zCount = (int)dimesions.y;



        vertices = new Vector3[(xCount + 1) * (zCount + 1)];

        uvs = new Vector2[(xCount + 1) * (zCount + 1)];

        triangles = new int[xCount * zCount * 6];



        int index = 0;

        // 将半径设置为固定值,避免受到分段数和scale的影响

        float radius = 5.0f;



        for (int x = 0; x <= xCount; x++)

        {

            for (int z = 0; z <= zCount; z++)

            {

                float angle = x * Mathf.PI * 2f / xCount;

                vertices[index] = new Vector3(

                    Mathf.Cos(angle) * radius,

                    Mathf.Sin(angle) * radius,

                    z * scale

                );



                uvs[index] = new Vector2(x * scale, z * scale);



                float pX = (vertices[index].x * perlinScale) + offset;

                float pY = (vertices[index].y * perlinScale) + offset;

                Vector3 center = new Vector3(0, 0, vertices[index].z);

                vertices[index] += (center - vertices[index]).normalized * Mathf.PerlinNoise(pX, pY) * waveHeight;



                index++;

            }

        }



        int triIndex = 0;

        for (int x = 0; x < xCount; x++)

        {

            for (int z = 0; z < zCount; z++)

            {

                int topLeft = x * (zCount + 1) + z;

                int bottomLeft = (x + 1) * (zCount + 1) + z;



                triangles[triIndex++] = topLeft;

                triangles[triIndex++] = topLeft + 1;

                triangles[triIndex++] = bottomLeft;



                triangles[triIndex++] = topLeft + 1;

                triangles[triIndex++] = bottomLeft + 1;

                triangles[triIndex++] = bottomLeft;

            }

        }

    }



}

# 1. 顶点创建代码解析

row
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取横向(圆周)和纵向(高度)方向的分段数

        int xCount = (int)dimesions.x;

        int zCount = (int)dimesions.y;



        // 初始化顶点和 UV 数组,数组大小基于网格的尺寸

        vertices = new Vector3[(xCount + 1) * (zCount + 1)];

        uvs = new Vector2[(xCount + 1) * (zCount + 1)];

        triangles = new int[xCount * zCount * 6];  // 每个矩形需要两个三角形,每个三角形有 3 个顶点

为什么数组的下标初始化为 (xCount + 1) * (zCount + 1) 呢?
考虑一个简单的例子:一个 2×2 的网格。这个网格包含 4 个单元格(每个单元格可以被两个三角形定义)。但要定义这 4 个单元格的顶点,实际需要 9 个顶点。

row
1
2
3
4
5
6
7
8
顶点布局 (xCount = 2, zCount = 2)
O---O---O
| | |
O---O---O
| | |
O---O---O

O 表示顶点,网格有 4 个单元格,但需要 9 个顶点

# 解释为什么需要额外的顶点

  1. 边界共享:每个单元格的边界与相邻单元格共享顶点,因此在 xz 方向上都需要比单元格数量多 1 个顶点来包围网格的边界。

  2. 顶点数量计算:这样,每一行有 xCount + 1 个顶点,每一列有 zCount + 1 个顶点。总顶点数为 (xCount+1)×(zCount+1)。

# 2. 顶点循环初始化

row
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int index = 0; // 用于遍历顶点和 UV 数组的索引

        float radius = 5.0f; // 固定半径,避免受到分段数和 scale 的影响



        // 遍历 x 和 z 方向,为每个顶点计算位置和 UV

        for (int x = 0; x <= xCount; x++)

        {

            for (int z = 0; z <= zCount; z++)

            {

                // 计算当前 x 的角度(沿圆周分布)

                float angle = x * Mathf.PI * 2f / xCount;



                // 使用三角函数计算圆周上的 x 和 y 坐标,z 方向拉伸

                vertices[index] = new Vector3(

                    Mathf.Cos(angle) * radius,    // x 位置

                    Mathf.Sin(angle) * radius,    // y 位置

                    z * scale                     // z 位置

                );



                // 设置 UV 坐标,用于贴图映射

                uvs[index] = new Vector2(x * scale, z * scale);



                // 应用 Perlin 噪声,为网格增加波动效果

                float pX = (vertices[index].x * perlinScale) + offset;

                float pY = (vertices[index].y * perlinScale) + offset;

                Vector3 center = new Vector3(0, 0, vertices[index].z);

                vertices[index] += (center - vertices[index]).normalized * Mathf.PerlinNoise(pX, pY) * waveHeight;



                index++; // 移动到下一个顶点索引

            }

        }

# 圆柱体平面

row
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 计算当前 x 的角度(沿圆周分布)

                float angle = x * Mathf.PI * 2f / xCount;



                // 使用三角函数计算圆周上的 x 和 y 坐标,z 方向拉伸

                vertices[index] = new Vector3(

                    Mathf.Cos(angle) * radius,    // x 位置

                    Mathf.Sin(angle) * radius,    // y 位置

                    z * scale                     // z 位置

                );

可以参考一个圆的横截面,radius 是半径,通过不同的 x/xCount 遍历不同的角度以达到不同的角度
同时我认为 xCount 的大小会影响圆面的平滑情况,因为遍历是以 1 为递增的,xCount 越大,两个圆顶点的距离就越小,圆就越平滑

# 柏林噪声

# 1. (center - vertices[index])
  • center 是一个三维向量,代表当前顶点在 z 轴方向上的中心点坐标 Vector3(0, 0, vertices[index].z)
  • vertices[index] 是当前顶点的坐标。
  • (center - vertices[index]) 计算了从 vertices[index] 指向 center 的向量,即当前顶点指向中心的方向。

这样,我们就可以让噪声影响顶点在朝向中心的方向上产生波动效果,使得顶点偏移不固定在某一方向,而是会朝向柱体中心产生偏移。

# 2. (center - vertices[index]).normalized
  • .normalized 将这个向量单位化,使其长度为 1,仅保留方向而去除大小。
  • 这样可以确保后续的偏移量是沿着顶点指向中心的方向发生的,而不会影响偏移量的大小。
# 3. Mathf.PerlinNoise(pX, pY)
  • Mathf.PerlinNoise(pX, pY) 使用 Perlin 噪声生成一个 0 到 1 之间的浮点数。
  • 其中 pXpY 是顶点的 xy 值经过缩放和偏移后的坐标,用于产生不同的噪声值。
  • Perlin 噪声生成的数值是平滑连续的,使用它可以创建出平滑的波动效果,而不是完全随机的变化。
# 4. Mathf.PerlinNoise(pX, pY) * waveHeight
  • waveHeight 是一个常数,控制波动的幅度,决定噪声的强度或影响力。
  • Mathf.PerlinNoise(pX, pY) * waveHeight 表示噪声值的实际偏移距离。噪声值在 0 到 waveHeight 之间波动,控制了每个顶点的偏移幅度。
# 5. (center - vertices[index]).normalized * Mathf.PerlinNoise(pX, pY) * waveHeight
  • 将噪声值乘以 (center - vertices[index]).normalized ,将波动效果应用在顶点指向中心的方向上。
  • 因此, Mathf.PerlinNoise(pX, pY) * waveHeight 生成的随机偏移量沿着 center 指向当前顶点的方向,产生平滑的 “凹凸” 波动效果。
# 6. vertices[index] += (...)
  • vertices[index] += 将最终的偏移量加到 vertices[index] 上,改变当前顶点的位置。
  • 这样每个顶点都会根据 Perlin 噪声的值产生偏移,效果上使得圆柱表面在朝向中心的方向上发生不规则的波动。

# 三角形的遍历创建

row
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
        int triIndex = 0; // 三角形数组的索引

        // 为每个网格单元创建两个三角形

        for (int x = 0; x < xCount; x++)

        {

            for (int z = 0; z < zCount; z++)

            {

                // 计算当前单元格左上角和左下角顶点的索引

                int topLeft = x * (zCount + 1) + z;

                int bottomLeft = (x + 1) * (zCount + 1) + z;



                // 第一个三角形的顶点

                triangles[triIndex++] = topLeft;

                triangles[triIndex++] = topLeft + 1;

                triangles[triIndex++] = bottomLeft;



                // 第二个三角形的顶点

                triangles[triIndex++] = topLeft + 1;

                triangles[triIndex++] = bottomLeft + 1;

                triangles[triIndex++] = bottomLeft;

            }

        }

# 顶点问题

row
1
2
3
4
5
                // 计算当前单元格左上角和左下角顶点的索引

                int topLeft = x * (zCount + 1) + z;

                int bottomLeft = (x + 1) * (zCount + 1) + z;

这里的 topLeftbottomLeft 计算当前网格单元的左上角和左下角的顶点索引。

# 1. topLeft = x * (zCount + 1) + z;
  • x * (zCount + 1) :计算在 x 行的开始位置的顶点索引。
    • 由于每行包含 zCount + 1 个顶点(多一个用于共享边界),所以每增加一行,顶点索引需要加上 zCount + 1
  • + z :在当前行的基础上,增加 z 偏移,用来定位该行的特定顶点位置。
# 2. bottomLeft = (x + 1) * (zCount + 1) + z;
  • (x + 1) * (zCount + 1) :定位到下一行的开始位置。
    • 由于是 x 行的下一行( x + 1 ),所以这里要加上 (x + 1) * (zCount + 1)
  • + z :在下一行的基础上,通过 z 偏移找到下一行的特定顶点位置。
# 示例

假设我们有一个 2 x 2 的网格(即 xCount = 2zCount = 2 ),需要 3 x 3 个顶点:

row
1
2
3
4
5
6
7
顶点布局:
0 --- 1 --- 2
| | |
3 --- 4 --- 5
| | |
6 --- 7 --- 8

# 计算顶点索引
  • 对于第一个单元格( x = 0 , z = 0 ):

    • topLeft = 0 * (2 + 1) + 0 = 0 :顶点索引为 0
    • bottomLeft = (0 + 1) * (2 + 1) + 0 = 3 :顶点索引为 3

    这样我们可以确定第一个单元格的顶点 03 的索引位置。

  • 对于第二个单元格( x = 0 , z = 1 ):

    • topLeft = 0 * (2 + 1) + 1 = 1 :顶点索引为 1
    • bottomLeft = (0 + 1) * (2 + 1) + 1 = 4 :顶点索引为 4

    这样我们可以确定第二个单元格的顶点 14 的索引位置。

# 遍历过程

row
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
        for (int x = 0; x < xCount; x++)

        {

            for (int z = 0; z < zCount; z++)

            {

                // 计算当前单元格左上角和左下角顶点的索引

                int topLeft = x * (zCount + 1) + z;

                int bottomLeft = (x + 1) * (zCount + 1) + z;



                // 第一个三角形的顶点

                triangles[triIndex++] = topLeft;

                triangles[triIndex++] = topLeft + 1;

                triangles[triIndex++] = bottomLeft;



                // 第二个三角形的顶点

                triangles[triIndex++] = topLeft + 1;

                triangles[triIndex++] = bottomLeft + 1;

                triangles[triIndex++] = bottomLeft;

            }

        }

这里三角形的顶点值并不是具体的大小,而是对应的顶点下标。
而为什么需要六个点呢,因为两个三角形构成一个矩形,而矩形有四个点,两个三角形有六个点,需要通过两个三角形才能确定一个矩形,故需要六个点来创建。