简介

在 3D 开发领域,我们会经常看到一些术语,有些是和数学相关的,有些是和模型相关的,也有一些是和程序相关的。本文是我对 3D 开发中常见术语的总结。

数学

数学是 3D 开发的基石,无论你想要操作物体移动,还是在屏幕中显示一个 3D 物体,都涉及到数学计算。在 3D 开发中,最常见的数学工具是向量、矩阵、四元数。

向量(Vector)

向量是既有大小又有方向的量。下面是常见的类型:

  • 二维向量(Vec2) 主要用来记录平面相关的值,如鼠标事件/触摸事件的发生的坐标,二维物体坐标等。 它有两个分量,分别是 xy

  • 三维向量(Vec3) 主要用来记录空间坐标,如三维物体的位置。也可以用来表示方向,如一个平面的法线方向。 它有三个分量,分别是 xyz

向量常见的操作有以下几种:

  • 加法
  • 减法

  • 规范化(normalize) 即把该向量变成单位向量(长度为1)。

  • 点乘 计算公式为:

    1
    
    a.dot(b) = a.length() * b.length() * Math.cos(theta)

    点乘的结果是一个标量,通常用来求向量的在另一个向量上的投影长度,也可以用来求两向量之间的夹角。 另外,当两向量都是单位向量时,我们可以简化上面的公式:

    1
    2
    
    a.dot(b) = Math.cos(theta)
    theta = Math.acos(a.dot(b));
  • 叉乘 叉乘的结果是一个向量,该向量会垂直于另外两个变量。它满足下面公式:

    1
    2
    
    a.cross(b) = c
    c.length() = a.length() * b.length() * Math.sin(theta)

    我们知道,三个不在同一直线上的点可以构成一个平面,我们可以通过用这三个点计算两个向量,然后再计算这两个向量的叉乘,结果就是这个平面的法线了。

矩阵(Matrix)

矩阵在 3D 开发中常常用来变换顶点坐标。 计算方法是把矩阵与顶点坐标相乘,即可得到变换后的顶点坐标。 即:

1
vertex' = matrix * vertex

其中,最常见的矩阵有下面几种:

  • 模型矩阵(Model Matrix) 模型矩阵的作用是把局部坐标中的顶点转换成世界坐标。 每个模型从三维软件导出时,都会基于一套坐标系来生成顶点坐标,这个坐标系就是局部坐标。 当我们把这个模型放到场景中的指定位置时,由于该位置是世界坐标中的一点,我们的操作相当于把模型从原点平移到该位置。同理,我们也可以在场景中对模型进行旋转和缩放操作。 实际上这些操作是在操作模型矩阵,利用这个模型矩阵把模型局部坐标中的顶点转换成世界坐标的顶点。

  • 视图矩阵(View Matrix) 视图矩阵用来把顶点的世界坐标转换成相对于观察者的坐标。

  • 投影矩阵(Projection Matrix) 投影矩阵用来把顶点相对于观察者的坐标转换成屏幕坐标。

这三个矩阵统称成 MVP 矩阵,我们在屏幕上看到的点都是通过如下公式进行转换的:

1
vertex' = Projection * View * Model * vertex

欧拉角(EulerAngles)

欧拉角用来表示物体旋转的角度,它由三部分组成:

  • 俯仰角(Pitch),表示绕 X 轴旋转的角度。
  • 偏航角(Yaw 或 Heading),表示绕 Y 轴旋转的角度。
  • 翻滚角(Roll 或 Bank),表示绕 Z 轴旋转的角度。

由于欧拉角非常直观易用,因此它对于新手来说非常友好。 但是,对于任意一个角度,欧拉角的表示方式由旋转顺序来决定,即可以按照 X-Y-Z 的顺序来旋转,也可以按照 Y-Z-X 的顺序来旋转,也可以按照其他顺序来旋转,这样会导致一个角度会有多种表示方式。 另外,如果想要在两个欧拉角之间进行插值(即想要实现平滑过度),那是非常困难的。 最致命的一点是,欧拉角存在万向锁。想了解的话可以观看这个视频

四元数(Quaternion)

四元数是用来表达旋转的另一种方式,一个四元数表示绕一个方向旋转特定的角度。如绕 Y 轴旋转 90 度,用四元数来表示的话只需要一个方向 Vector3(0, 1, 0) 和一个角度 90 即可。 四元数没有万向锁,插值非常简单。

3D 模型

任何一个 3D 模型都是由多个简单的几何形状组成的,而这些几何形状又是通过三角形来组成的。

顶点(Vertex)

在三维空间中,一个孤立的点是普通的点。如果一个点是用来构成多边形的,那么该点就被称为顶点。

网格(Mesh)

网格是一种复杂的形状,它由多个顶点按照某个规律连接而成,换句话说,一个网格包含了多个顶点。 一个模型可以包含多个网格。

材质(Material)

材质管理着颜色、明暗、光泽等信息。当一个网格加上一个材质之后,它就可以在屏幕上显示特定的「色彩」了。 一个网格只有一种材质,而一种材质可以应用在多个网格上。

贴图(Texture)

贴图是指把一张普通的图片贴到材质的表面。每种材质都可以有多种类型的贴图,如漫反射贴图、法线贴图、高光贴图、光照贴图 等。材质除了这些贴图之外,还有其他属性,可以用来调节这些贴图的属性。 换句话说,每种材质都可以包含多种贴图。

轴向包围盒(Axis Align Bounding Box)

轴向包围盒简称 AABB,是指包含一个模型,且边平行于坐标轴的最小六面体。 请看看下面两张图: AABB 就是在模型最外面的由细线构成包围盒,它会随着模型旋转而变化。 实际上,AABB 并不可见,上图中的只是为了让我们更好理解才把包围盒画上去的。 另外,第一张图中为了让我们更好看见 AABB,特意把 AABB 调大了一点点,实际上该模型的 AABB 和所有边都是重合的。

有向包围盒(Orient Bounding Box)

有向包围盒简称 OBB,它是包含模型且相对于坐标轴方向任意的最小的长方体。 看看下面两张图: 白色线框表示的就是 OBB,而蓝色线框表示的则是 AABB。 如果想看更直观的对比,可以看看这个视频:AABB vs OBB

程序

顶点缓冲区(Vertex Buffer)

顶点缓冲区的作用是用来存储顶点数据,然后提供给底层 API 进行绘制。 顶点缓冲区用一维数组的方式来存储顶点信息,但绘制3D 图形每个顶点需要三个维度的数据(x, y, z),因此,底层在绘图的时候会通过「偏移」的方式来取点。如:

1
2
3
4
5
var vertexBuffer = new Float32Array([
  0.0,  0.5,  0.0,
  -0.5, -0.5, 0.0,
  0.5,  -0.5, 0.0
]);

上面这个顶点缓冲区有9个元素,但其实它包含了3个顶点的信息,每个顶点会占用3个位置。 把数据传递给底层时,需要设置偏移量为 3,才能正确绘制图形。

索引缓冲区(Index Buffer)

在绘制复杂图形的时候,我们需要用到非常多的顶点,如:

1
2
3
4
5
6
7
8
var vertexBuffer = new Float32Array([
  0.0,  0.5,  0.0,
  -0.5, -0.5, 0.0,
  0.5,  -0.5, 0.0,
  0.0,  0.5,  0.0,
  0.5,  -0.5, 0.0,
  0.75, 0.5,  0.0
]);

上面的顶点缓冲区中有有两个顶点是重复的,分别是:[0.0, 0.5, 0.0][0.5, -0.5, 0.0]。如果需要绘制的图形非常复杂,那么重复的顶点会非常多,这样就造成浪费了。 其实,我们只需要在绘制时指定顶点的顺序,就可以解决顶点重复的问题了,而索引缓冲区就是用来做这事情的。我们可以把上面的 vertexBuffer 进行简化,然后引入 indexBuffer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var vertexBuffer = new Float32Array([
  0.0,  0.5,  0.0,
  -0.5, -0.5, 0.0,
  0.5,  -0.5, 0.0,
  0.75, 0.5,  0.0
]);
var indexBuffer = new Uint8Array([
  0, 1, 2,
  0, 2, 3
]);

这样,我们就可以简化顶点缓冲区的数据了。

着色(Shading)

简单来说,为整个三维场景上色的过程叫做着色。在真实世界中,在光线的作用下,我们可以辨认出物体的颜色,还有它们产生的阴影。 在 3D 场景中,仅仅通过顶点所绘制出来的图形并没有光照信息,我们可以在场景中添加灯光,然后通过计算才能得到某个位置的颜色信息,再为该位置进行上色,这就是着色的过程。

着色器(Shader)

着色器是运行在 GPU 上的程序,负责处理顶点信息和片元信息,是 3D 绘图中最重要的部分。 着色器通常有两种,分别是顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。 顶点着色器负责处理顶点的信息,上面提到的顶点缓冲区就是由顶点着色器来处理的。 片元着色器负责为片元上色,它会在顶点着色器后面执行。

Draw Call

Draw Call 是一次绘图操作,当我们把数据(如顶点、贴图、着色器等信息)准备好,然后传给 GPU 绘制就是一次 Draw Call。可以认为,Draw Call 次数越少,性能越好。

参考资料