您好!在我的博客当中,我将持续挑选一些优质的国外技术文章进行翻译,如果文章内容翻译有误,欢迎在评论区指正,感谢:) 以下是原文链接:Creating a Mesh (catlikecoding.com)
5.1 构造你的第一个三角形
在这篇文章当中,我将为你从零开始进行讲解:如何利用 Unity 中提供的 Mesh 网格组件,对网格中的顶点、三角形、法线、贴图等等这些内容进行控制,并可以使用脚本来程序化的控制、生成它们。
从三角形开始 先让我们从一个最简单的问题开始:如何在 Unity 中渲染一个三角形? 首先可以想到的方式是:渲染一个网格,然后为其使用一个材质。Unity 对于基础的形体,如立方体、球体都提供了内置的网格,其他的网格可以你可以从网上购买、下载或自己制作,然后导入到项目中。 而这不是我们本文讨论的重点,Unity 除了可以导入其他来源的网格之外,还可以在运行时通过代码动态的创建网格,而这边是这个系列文章的主题。这种使用代码控制网格的技术通常被称之为程序化生成,因为它们是通过使用特定算法的代码生成,而不是通过手工建模。
项目配置 我们将会在后续的系列文章中使用到 Mathematics,因此你可以先使用 Package Manager 来导入它,虽然我们在本教程中暂时还不会使用到。除此之外我还导入了 Burst 包,并且使用 URP 作为项目的渲染管线。
5.1.1 程序化生成 Mesh
一共有两种方式可以用于程序化生成 Mesh:简单的方式以及更加进阶的方式。这两种方式都有其单独的 API;
- *什么是 API?
- API 代表应用程序编程接口。在这里,它指的是 CSharp 类型及其成员的集合,这些类型和成员一起允许我们生成网格;
这两种创建网格的方式我们都会进行教学,在这里我们先学习简单的 Mesh API,这种方法一直是 Unity 的一部分。首先让我们为它创建一个组件类型,并给它一个命名。
C#
using UnityEngine;
public class SimpleProceduralMesh : MonoBehaviour { }
我们将使用这个自定义组件类型来在我们进入游戏时,生成网格。为了能够绘制网格,我们需要一个 gameobject 物体并且此物体身上拥有 MeshFilter
以及 MeshRenderer
组件。我们可以强制将这些组件添加到自己组件绑定的游戏对象上:
C#
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class SimpleProceduralMesh : MonoBehaviour { }
接下来,在 Unity 的 Scene 中创建一个空物体,并为其添加上 SimpleProceduralMesh
组件。这将同时自动的为当前 gameobject 添加上 MeshFilter
以及 MeshRenderer
组件。然后需要再创建一个新的默认的 URP 材质,并将其分配给我们的游戏对象,因为默认的 MeshRenderer
组件没有材质。下图是添加完成后的样子:
我们在 OnEnable
方法中生成网格,它将会创建一个 Mesh 对象,并为其 name 赋值:
C#
public class SimpleProceduralMesh : MonoBehaviour {
void OnEnable () {
var mesh = new Mesh {
name = "Procedural Mesh"
};
}
}
在实例化了 Mesh 对象后,我们可以把这个 Mesh 对象的引用给添加到 MeshFilter
组件的 Mesh 属性上:
C#
var mesh = new Mesh {
name = "Procedural Mesh"
};
GetComponent<MeshFilter>().mesh = mesh;
如图在 Inspector 视图中所示:
当我们进入 Play 模式,可以看到当前引用的 Mesh 将会出现在 Inspector 视图当中,尽管此时还没有绘制任何具体的图形。
上图所示文字展示了当前 Mesh 的基础信息,它当前并不含有任何的顶点或者索引,它拥有一个拥有零个三角形的子网格。
5.1.2 添加顶点
一个网格可以包含三角形,他是最简单的一种可以在 3D 空间中被描述的表面图像。每一个三角形拥有三个角,这也是顶点所在的位置,接下来我们就回来学习如何定义它们。 在最简单的情况下,顶点就是 3D 空间中的一个位置点,它可以使用 Unity 中的 Vector3
类型进行描述。我们将会为三角形创造多个顶点,通过使用 Vector3 定义好的 zero、right、up 来进行定义。以下定义了一个在 XY 平面上的等腰直角三角形,它的九十度角位于坐标原点:
有很多种方式可以用于为 Mesh 设置顶点,最简单的方式便是直接创建 Vector3 的数组,并将其分配给 Mesh 的 vertices 属性:
C#
var mesh = new Mesh {
name = "Procedural Mesh"
};
mesh.vertices = new Vector3[] {
Vector3.zero, Vector3.right, Vector3.up
};
GetComponent<MeshFilter>().mesh = mesh;
以下是生成的三个顶点的数据:
在进入到 Play 模式之后,将可以在 Inspector 视图中看到上述内容。每一个顶点定义了一个位置,它存储为一个 32 比特大小的 float
值。 虽然目前定义了三个顶点,但目前还没有真正创建三角形,但 Mesh 已经从我们给它的顶点中自动得到了它们的边界;
5.1.3 定义三角形
单单只有顶点还不够,我们还需要描述当前网格的三角形将会如何被绘制,即使是对于只有一个三角形的普通网格。在拥有顶点之后,我们将会使用 int
类型的数组,来为三角形的 triangles
属性分配索引目录。这些索引引用了这些位置顶点,最直接的设置方式便是将索引实例化为 0、1、2:
C#
mesh.vertices = new Vector3[] {
Vector3.zero, Vector3.right, Vector3.up
};
mesh.triangles = new int[] {
0, 1, 2
};
现在,我们的网格便具有了被三个顶点定义的三角形。这个索引总是从下标 0 开始索引起,因为当前只有一个子网格(submesh)。这个索引花费了 6 个 bytes 而不是 12 个,因为它们在计算机中被存储为 UInt16
类型,它使用了 16bit 大小的 CSharp 类型,它定义了一个只有两个字节而不是四个字节的无符号整数。
三角形在游戏的场景当中,但它并不是从所有视角都可见。在默认情况下,三角形只有在看它们的正面时才可见,而不是它们的背面。你看的是正面还是反面取决于顶点的顺序,如果你沿着三角形的边、按照索引指示的顺序经过顶点,你最终会沿着顺时针或逆时针方向绕三角形。从视觉上看,顺时针方向是正面,这是三角形可见的一面,如图所示:
以上规则意味着:我们只能在负 Z 的方向上看到三角形。我们可以通过交换第二个与第三个顶点索引的顺序来扭转这个局面,然后便可以看到正 Z 方向的三角形:
C#
mesh.triangles = new int[] {
0, 2, 1
};
通过 Z 轴观测三角形:
5.2 法向量、贴图与法线贴图
5.2.1 法向量
尽管已经设置好了三角形的顶点,但目前三角形的照明依然是不正确的,因为我们还没有定义对于三角形而言,当前三个顶点构成的面它面朝的方向。产生这种问题的原因,在于我们还没有为它定义法向量。
法向量可以被存储为一个单位向量,它描述了一个垂直于平面上的、局部向上的方向。因此对于当前我们演示用的三角形而言,它的法向量就是 Vector3.back
,在我们网格的局部坐标中直接指向负 Z 轴方向。如果我们没有提供法向量,Unity 会默认使用正方向的法向量,因此当前三角形看起来总是从错误的一边照亮。
尽管法向量这一概念只在定义曲面时才有意义,但 mesh 还是为每一个顶点都提供了可定义的法向量,用于着色的最终表面的法向量是通过对三角形表面的各个顶点法线进行插值,进而计算得到的。通过使用不同的法向量,可以将一个曲面 mesh 的错觉添加到一个平面三角形中,换言之:可以使某些固定未知连接的平面,看起来十分光滑,通过法向量来使它们的画面效果变成曲面,经过存储位置信息时依然是非平滑的变化。
我们可以使用 Vector3
的数组来添加法向量,将其添加到 mesh
的 normals
属性上(需要在添加顶点之后)。Unity 将会检查当前传入的数组是否是同样的长度,并且当我们使用不同长度的 Vector3
时发生报错。以下时添加 Vector3
的代码:
C#
mesh.vertices = new Vector3[] {
Vector3.zero, Vector3.right, Vector3.up
};
mesh.normals = new Vector3[] {
Vector3.back, Vector3.back, Vector3.back
};
光照正常时的三角形:
其在 Inspector 视图中定义如下:
5.2.2 纹理
mesh 的表面细节需要通过应用纹理来添加到网格中,而最简单的纹理就是给表面使用纯色的图片。在 URP 渲染管线的情况下,这被称为 basemap 。以下是一个简单的 basemap :
你可以下载这张照片,将其导入至你的项目中。可以将其放入到项目的 Assets 文件夹中,也可以将文件拖放到项目窗口中。然后将其分配给材质的 Base Map 属性:
看起来似乎没有发生什么变化,这是因为我们还没有定义任何的纹理坐标,它们默认为零。这意味着,当前纹理的左下角用于整个三角形,而它是白色的。
由于纹理是一张 2 D 的图像,而三角形表面也是 2 D 的,因此可以将纹理坐标定义为 Vector2
值。这些 Vector2
用于指定每个顶点采样纹理的位置,它们将在三角形的表面上进行插值。
在 Unity 中,原点默认位于纹理的左下角,我们将一个数组赋值给 uv
属性,来将纹理添加到网格中。纹理坐标通常被描述为 UV ,它们是纹理空间中的二维坐标,命名为 U 和 V,而非 X 和 Y。
C#
mesh.vertices = new Vector3[] {
Vector3.zero, Vector3.right, Vector3.up
};
mesh.normals = new Vector3[] {
Vector3.back, Vector3.back, Vector3.back
};
mesh.uv = new Vector2[] {
Vector2.zero, Vector2.right, Vector2.up
};
补充:是否可以令一个 mesh 拥有多个纹理?
- 答案是 Yes。Unity 支持最多设置 8 个,它们可通过单独的属性访问。也可以定义 1 D、3 D 或 4 D 坐标,但只能通过方法,而不能通过属性来访问。
当前 mesh 在 Inspector 中可以看到将纹理坐标列为 UV0,并显示它们为顶点大小增加了8个字节。
也可以用不同的方式映射纹理,例如使用 Vector 2时将第三个顶点设置为 1 ,这将扭曲图像,如下图所示。
5.2.3 法线贴图
法线贴图基本概念
- 另一个为表面添加细节的方法是使用法线贴图,它通常使用一个 normal map 的纹理(Texture)来实现,它使用图片的格式存储了表面的所有法线向量。
- 比如以下是一个纹理,它描述了一个呈棋盘图案、交替升高和降低斜面的表面,并且加上了一些细微的不均匀变化;
- 导入图像后,需要将其纹理的类型设置为法线贴图,否则 Unity 无法正确的解析它:
- 然后使用 Normal map 作为材质的法线贴图:
就如同顶点法向量,法线贴图是用来调整表面法向量的一种描述,它可以影响光照在表面产生的变化,进而产生视觉上的效果,即使三角形在数学上是平坦的,但在观看时却有着凹凸不平的效果。
虽然我们已经绘制了一个图形,但它还有些错误需要我们改正。正确的图形应该是有着往里凹和往外凸两个部分。因为法线贴图中的向量存在于纹理空间坐标系中,所以它必须先转换到世界空间坐标系才能影响光照。为了达成这一目标需要设计一个变换矩阵,它定义了一个相对于曲面的三维空间,称为 tangent space 。它由一个向右的坐标轴、一个向上的坐标轴和一个向前的坐标轴组成。
向上轴指向远离表面的方向,右轴在我们的例子中就是 Vector3.right
,它必须始终与曲面曲率相切。我们通过将向量分配给网格的切线属性来定义每个顶点。 着色器可以通过计算已经定义的两个轴的方式来构建第三个轴。然而,计算出来的轴会有两种不同的可能,即一个指向前方或向后的向量。这就是为什么 tangent vector 必须使用 Vector4
的值:它们的第四个分量应该是 1 或- 1,以控制第三个轴的方向。默认的 tangent vector 指向右边,并且它们的第四个分量设置为 1。
C#
mesh.normals = new Vector3[] {
Vector3.back, Vector3.back, Vector3.back
};
mesh.tangents = new Vector4[] {
new Vector4(1f, 0f, 0f, -1f),
new Vector4(1f, 0f, 0f, -1f),
new Vector4(1f, 0f, 0f, -1f)
};
以下是正确以及不正确的法线贴图的对比: