# 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
表示,包括x
、y
和z
坐标。 - 用途:顶点的集合决定了物体的形状,通过连接顶点可以形成边和面。顶点位置的改变会直接影响模型的形状。
- 示例:一个简单的正方形平面可以由 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
表示,包括x
、y
和z
坐标。 - 用途:法线决定了光照如何应用在模型表面。正确的法线可以让光照看起来更自然,模型更有立体感。
- 示例:如果一个平面在 X-Z 平面上,法线通常会指向 Y 轴正方向
(0, 1, 0)
,表示平面朝上。
# 4. UV 坐标 (UV Coordinates)
UV 坐标用于将 2D 纹理贴图映射到 3D 模型的表面。UV 坐标定义了模型表面上每个顶点在纹理图上的位置。
- 表示方式:UV 坐标通常用
Vector2
表示,包括U
和V
分量,范围为[0, 1]
。U
表示纹理的水平坐标。V
表示纹理的垂直坐标。
- 用途:UV 坐标控制了纹理在模型表面的显示方式。通过调整 UV 坐标,可以改变纹理的平铺、缩放和对齐方式。
- 示例:一个简单的正方形平面的 UV 坐标可能是
[(0, 0), (1, 0), (1, 1), (0, 1)]
,表示将整个纹理贴图到平面上。
# 示例:在 Unity 中创建一个简单的 Mesh
假设我们要创建一个 1x1 的正方形平面,以下是顶点、三角形、法线和 UV 的示例:
1 | Mesh mesh = new Mesh(); |
# 4. 动态生成和修改 Mesh
在游戏过程中,可以动态修改 Mesh 数据。例如,改变顶点的位置可以让 Mesh 变形。以下是如何动态修改 Mesh 顶点位置的示例:
1 | void Update() |
# 5. Mesh 的应用
- 地形生成:可以通过 Perlin Noise 或其他算法生成地形。
- 角色建模和动画:Mesh 可以应用于角色建模,还可以通过骨骼动画(Skinned Mesh)进行变形。
- 破碎效果:通过分割 Mesh,可以实现物体破碎的视觉效果。
- 动态水面:修改 Mesh 顶点可以实现水面波动的效果。
# 6. 注意事项
- 性能:修改 Mesh 可能影响性能,尤其是复杂的 Mesh 或频繁更新的情况。
- 法线和 UV:需要正确设置法线和 UV,确保光照和纹理显示正常。
- Recalculate:修改 Mesh 后需要调用
RecalculateNormals
和RecalculateBounds
等方法来更新法线和边界框。
# 完整示例代码
1 | using System.Collections; |
# 1. 顶点创建代码解析
1 | // 获取横向(圆周)和纵向(高度)方向的分段数 |
为什么数组的下标初始化为 (xCount + 1) * (zCount + 1)
呢?
考虑一个简单的例子:一个 2×2 的网格。这个网格包含 4 个单元格(每个单元格可以被两个三角形定义)。但要定义这 4 个单元格的顶点,实际需要 9 个顶点。
1 | 顶点布局 (xCount = 2, zCount = 2): |
# 解释为什么需要额外的顶点
边界共享:每个单元格的边界与相邻单元格共享顶点,因此在
x
和z
方向上都需要比单元格数量多 1 个顶点来包围网格的边界。顶点数量计算:这样,每一行有
xCount + 1
个顶点,每一列有zCount + 1
个顶点。总顶点数为 (xCount+1)×(zCount+1)。
# 2. 顶点循环初始化
1 | int index = 0; // 用于遍历顶点和 UV 数组的索引 |
# 圆柱体平面
1 | // 计算当前 x 的角度(沿圆周分布) |
可以参考一个圆的横截面,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 之间的浮点数。- 其中
pX
和pY
是顶点的x
和y
值经过缩放和偏移后的坐标,用于产生不同的噪声值。 - 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 噪声的值产生偏移,效果上使得圆柱表面在朝向中心的方向上发生不规则的波动。
# 三角形的遍历创建
1 | int triIndex = 0; // 三角形数组的索引 |
# 顶点问题
1 | // 计算当前单元格左上角和左下角顶点的索引 |
这里的
topLeft
和 bottomLeft
计算当前网格单元的左上角和左下角的顶点索引。# 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 = 2
, zCount = 2
),需要 3 x 3 个顶点:
1 | 顶点布局: |
# 计算顶点索引
对于第一个单元格(
x = 0
,z = 0
):topLeft = 0 * (2 + 1) + 0 = 0
:顶点索引为0
bottomLeft = (0 + 1) * (2 + 1) + 0 = 3
:顶点索引为3
这样我们可以确定第一个单元格的顶点
0
和3
的索引位置。对于第二个单元格(
x = 0
,z = 1
):topLeft = 0 * (2 + 1) + 1 = 1
:顶点索引为1
bottomLeft = (0 + 1) * (2 + 1) + 1 = 4
:顶点索引为4
这样我们可以确定第二个单元格的顶点
1
和4
的索引位置。
# 遍历过程
1 | for (int x = 0; x < xCount; x++) |
这里三角形的顶点值并不是具体的大小,而是对应的顶点下标。
而为什么需要六个点呢,因为两个三角形构成一个矩形,而矩形有四个点,两个三角形有六个点,需要通过两个三角形才能确定一个矩形,故需要六个点来创建。