在前面章节的学习中,大家已经接触过矩阵、向量等数学概念了。那么除了矩阵和向量, 关于 3D 开发我们还需要哪些数学知识?这些数学知识又能够帮助我们解决什么问题呢?
本节为大家做一个概括,接下来的章节我们就深入学习这些数学知识。
那么,3D 应用涵盖哪些数学知识呢?
坐标系是一个重中之重的概念,我们在开发 3D 应用的过程中,经常会涉及到坐标系之间的转换,最经典的坐标转换流水线就涉及到六种坐标系,由此可见坐标系的重要性。
点在 3D 领域通常指顶点坐标,3D 世界由很多模型组成,模型又由很多面组成,而面又由很多点组成。因此,点是组成 3D 世界的基本元素。WebGL 渲染的过程,就是将组成模型的顶点传递给 GPU,GPU 按照指定图元装配并插值这些顶点数据,然后通过片元着色器对像素进行着色,最终渲染成 3D 模型。由此可见顶点是模型的起始数据,重要性可见一斑。
向量是另一个非常重要的知识点。物理和数学中的向量代表既有大小又有方向的量。
通常我们使用向量容器来表示数学中的点和向量。
比如我们经常会使用 3 维向量来表示顶点坐标,3 维向量还可以理解为一个 3 x 1 阶矩阵,将 3 x 3 阶变换矩阵左乘顶点坐标(3 * 1 阶矩阵),按照矩阵的运算法则,m行n列 矩阵左乘 n 行 o 列矩阵可以得到一个 m 行 o 列矩阵,所以我们矩阵和向量相乘结果是一个新的 3 * 1 阶 矩阵,也就是 3 维向量,它表示变换后的顶点坐标。
这从数学角度解释了
变换矩阵左乘顶点坐标向量的结果是一个新的顶点坐标的原因。
通常在对模型执行坐标转换的时候,变换矩阵左乘顶点坐标代表对这个顶点执行坐标转换,这是顶点着色器中对顶点执行的最常用的操作。
又比如在为模型表面计算光照强度的时候,我们会用到法向量和光线入射向量,利用他们之间的点积计算光照强度。
向量的重要性可见一斑。
3D 编程中经常使用的向量运算有以下几种。
后续章节我们会详细学习它们的运算规律,并提供 JavaScript 版本的分析与实现。
3D 数学中最重要的一个知识点我想就是矩阵了,矩阵能够帮助我们以一种非常简单的方式解决大量运算的问题。
比如坐标转换。
那有同学会问了,坐标转换无非就是对顶点坐标的计算,我用普通的加减乘除和三角函数运算也能做到呀。
说的没错,那大家有没有想过,如果要对一个坐标执行多种变换,大家需要写多少计算逻辑?
我想大家会崩溃的。
矩阵就是为了帮助我们解决这个问题,一个矩阵代表一种变换,多个矩阵相乘就代表多个变换。有了矩阵,我们就不用再使用三角函数,加减乘除等繁杂的数学公式来完成坐标转换,仅仅使用一个矩阵就可以代替多种运算步骤。
前面在构建立方体的章节我们已经接触到了矩阵,仅仅让一个旋转矩阵左乘顶点坐标,就实现了立方体的旋转,由此可见矩阵在 3D 数学运算中的重要性。
这就是矩阵的主要作用。但使用矩阵还有一点考虑,就是在 GPU 中执行矩阵运算性能会高很多,这得益于 GPU 并行计算的优势,在绘制大量顶点的场景下会有很大的性能提升。
3D 编程中经常用到的矩阵运算有以下几种。
如果我们不理解矩阵和向量的运算规则,我们就写不出数学库,自然就没办法开发 3D 应用。接下来我们就要着重讲解这方面的内容,另外我还会详细推导变换矩阵的 JavaScript 算法实现,封装出一个我们自己的数学库。
3D 开发时我们经常需要使用一些数学函数完成一些数值运算,常用的有如下几种:
sin (θ)
asin (value)
cos (θ)
acos (value)
atan (value)
tan (θ)
abs (value)
max (value1, value2)
min (value1, value2)
clamp (value, min, max)
pow (x, n)
……
还有一些函数就不一一介绍了,在需要的时候大家可以查一下。
上面的数学知识,足以支撑我们完成大部分 3D 效果了,但在做一些曲线相关应用的时候,我们还会用到一些曲线公式来求坐标,如正弦、余弦、贝塞尔公式等。本文不对它们做过多介绍,感兴趣的话大家可以去看看 ThreeJS 对它们的实现。
以上就是我们在 3D 编程中将会用到的数学知识,业界一般将这些数学算法抽象出来以方便调用,比如 Threejs 就有单独的 matrix 、vector、euler等数学类。JavaScript没有提供向量和矩阵的表示和运算,所以我们需要封装。但是 GLSL 内置了大部分数学运算。
有的同学会问,既然 GLSL 内置了这些计算,Threejs 为什么还要封装呢?
这是因为有些运算没有必要放在 GLSL 中,如果放在 GLSL 中反而会影响性能。比如全局变换矩阵,如果放在 GLSL 中计算,那么每个顶点变换前,都要重新计算出矩阵。模型的顶点都是很多的,这会造成大量重复运算。尽管 GPU 的运算能力很强,但那么多的无用运算还是会造成性能问题。
下一节,我们开始详细介绍这些数学知识。
今天我们着重讲解计算机图形学中的数学理论,这部分内容不仅适合 WebGL 开发同学阅读,同样适用 OpenGL、 Direct3D 等 3D 领域的开发同学。
数学之于图形学,就像空气与人类的关系,人类的生存离不开空气,同样地,如果要学好图形开发,数学功底也是必不可少的。因此,从本节开始,我会向大家详细地介绍图形学相关的数学知识,帮助大家打好这方面的基础。之后,大家不管是去学 WebGL 还是其他 3D 开发,都会轻松很多。
图形学内容其实不仅会让 3D 领域的开发同学受益,也能让 Web 开发同学受益。CSS3 新出了transform和perspective 属性,我们可以用这两个属性实现 3D 效果,这两个属性的底层也是基于矩阵实现的。再比如一些手机适配库,也都是用一些简单的数学公式实现的。
计算机图形学是一种使用数学算法将 2D、3D 模型渲染到 2D 计算机屏幕的科学,主要应用在游戏、数据可视化、地图导航、虚拟现实(AR)、增强现实(VR)等领域。
大家要知道一点,我们在计算机中模拟现实生活,只能达到近似模拟,而不能精确模拟。所以我们的数学算法只要能满足视觉上的近似效果就可以了。这也是图形学中的一个近似准则。
3D 世界中的每个物体都有自己的位置,为了精确地表示这种位置,我们需要引入坐标系的概念,坐标系我相信大家在初中都学过,使用最为广泛的就是笛卡尔坐标系。
标准的笛卡尔坐标系 X 轴向右,Y 轴向上,如下图所示:
而屏幕坐标系往往是 X 轴向右,Y 轴向下:
扩展到 3D时,笛卡尔坐标系的 Z 轴方向则有两种表示方式:左手坐标系和右手坐标系。左手坐标系的判定方式是张开左手,拇指伸直指向的方向代表 X 轴正方向,食指伸直指向的方向代表 Y 轴正方向,其余手指弯曲指向的方向则代表 Z 轴正方向。右手坐标系则是用右手,得到的 Z 轴正方向和左手坐标系相反,如下图所示:
大家可以用左手和右手比划一下。
WebGL 中的坐标系遵循右手坐标系,Z 轴正方向朝向屏幕外。
有了坐标系,我们就可以精确定位坐标系上的一个物体了,模型由点组成,点在坐标系中的位置用坐标来表示。比如 2D 笛卡尔坐标系上有一点 P。
我们就可以用坐标 (3, 4) 表示这个点的位置,扩展到 3 维坐标系时,只需要增加顶点在 Z 轴上的坐标就可以了。
坐标系和点的知识相信大家在初中就已经学过了,而且比较简单,这里就不做过多阐述了。
接下来学习向量的相关内容。
向量是既有大小,又有方向的量,在物理和工程学中又称为矢量。
与之对应的是标量,标量只有大小,没有方向。如身高、体重、距离等。
日常生活中常见的向量有位移,速度,加速度、光线方向等等。
向量在坐标系中通常用一根带箭头的线段来表示。
向量一般用一个上方带一个箭头符号的字母表示, 如 代表一个名称为
a 的向量。在 2D 笛卡尔坐标系中,假设某向量起点为 O(0, 0),终点为 P(X, Y),那么通常使用 来表示这个 2 维向量,并且这个向量也可以用坐标的形式来表示:
3D 笛卡尔坐标系中:
看到这里,有的同学或许有疑问了。GLSL 程序中有一个向量数据结构 vec,我们经常使用它来定义点坐标或者向量,那么如果给定一个变量:
vec3 p = vec3(x, y, z);如何判断 P 代表向量还是代表顶点坐标呢?
这是一个好问题。同时也是一个比较重要的知识点,我们必须有能力区分。
通常使用齐次坐标系来解决这种混乱。齐次坐标系使用 N + 1 维向量来表示 N 维点坐标和 N 维向量。假设在 3 维坐标系中,有一个点(X, Y, Z),那么在齐次坐标系中会使用 4 维向量来表示它 (X, Y, Z, W)。注意:W > 0。如果是向量的话,齐次坐标将向量表示为(X, Y, Z, 0)。
请谨记:W 为 0 时代表向量。W 不为 0 代表点。
齐次坐标系的引入除了解决这种概念混乱,还有一个重要的作用,透视除法。稍后讲到投影矩阵时我们再细说。
比较两个标量是否相等,只要比较他们大小是否相同即可。但是两个向量相等需要满足以下两个条件:
在 2D 坐标系中,假设有两个向量 和
,这两个向量虽然起点和终点都不相同,但是它们的方向相同,大小相同,所以这两个向量相等。
可以看出,这两个向量在各个坐标轴的坐标相同。
因此,判断两个向量是否相等,还可以通过它们在各个坐标轴上的分量是否相等来判断。
向量有多种不同于标量的运算规则,我们必须要熟练掌握。
假设有如下两个向量:
我们看下它们是如何进行运算的。
向量相加在坐标系中表示如下:
向量相减在坐标系中表示如下:
可见 和
的结果大小相同,方向相反,所以不满足交换律。
零向量是唯一一个大小为 0 的向量,对于其他任意大小不为 0 的向量,它们都存在无数个方向不同的向量,这些向量构成一个圆,零向量我们用的比较少。
负向量其实是原向量的反方向向量,大小不变,方向相反。求一个向量的负向量,只需将原向量的各个分量变成它们的相反数即可,并且负向量和原向量维度相同。
负向量可以理解为原向量与 -1 的乘法运算。
向量的大小,也就是向量的长度(也叫向量的模),通常用 来表示,向量的大小等于向量各个分量平方之和的平方根。
向量不能和标量相加减,但是向量可以和标量相乘除,向量和标量相乘或者相除返回一个新向量,新向量的各个分量等于原向量的各个分量和标量的乘积或者商。
向量与标量相乘的几何解释是:向量乘以标量 n 的意义是以因子|n|缩放向量的长度,例如:为了使向量的长度加倍,应使向量乘以 2 。如果 n < 0,则向量的方向与原向量相反。
单位向量是长度为 1 的向量,对于大部分向量,我们只关心向量的方向,而不在意向量的长度,这种情况下就适合用单位向量来表示。比如光线入射方向、反射方向等向量,单位向量通常也被称为标准向量。
对于任意一个不为 0 的向量,我们都能将它转变成同方向的单位向量,这个转变过程我们称之为归一化向量或者标准化向量。
归一化向量只需要将原向量除以原向量的长度(模)即可,一定要注意,原向量不能是零向量。
归一化向量 的过程:
标量和向量可以相乘,向量和向量也可以相乘,向量之间乘法包含两种点乘和叉乘。
向量点乘就是将两个向量的各个分量的乘积相加,返回一个标量。
点乘的计算方式如下:
点乘的几何意义是两个向量的模相乘然后再乘以夹角的余弦。
所以,我们经常会用点乘来计算两个向量之间的夹角大小,比如在光照模型中,我们在计算漫反射分量时,就使用了点乘公式,求出入射光和法向量之间夹角的大小,通常只求夹角的话,一般先将两个向量归一化,这样就不用再去计算向量模了,直接取点乘结果即可。
向量叉乘是另一个重要的运算,两个向量叉乘结果是一个新向量,新向量的方向垂直于原来两个向量所在的平面,方向可以通过右手定则来判定,大小等于两个向量模的乘积再乘以向量夹角的正弦值,即向量组成的平行四边形的面积。
如上图,利用右手定则,当右手的四指从向量 a 以不超过 180 度的转角转向 b 时,竖起的大拇指指向是叉乘结果向量的方向。
叉乘一般用于3D坐标系中。
以下公式可以用向量来表示叉乘结果。
假设有向量和向量
:
那么,向量 和
的叉乘:
向量用的最多的运算形式就是点乘和叉乘,我们经常会使用点乘判断两个向量夹角大小。
比如在计算光照时,判断物体表面是否被光源照射,就是通过法向量和光源入射光线的点积来判断,为正表示光源在表面正面,能被照射到,为负表示光源在表面背面,不能被照到。
又比如在游戏中判断怪物在自己前面还是后面,也会用点积来判断,点积结果为正,表示怪物在自己前面。点积结果为负,表示怪物在自己后面。
至此,向量的相关知识就介绍完了,大家看了也许当时懂了,但是过不久又忘了,所以一定要多加练习,让这些运算规则深入骨髓,就像我们对加减乘除的理解,能够很熟练地手写计算,才算真正掌握。
前面讲了向量,接下来该亮出另一大神器矩阵了。
矩阵是按照行列排列的一系列数值得的集合,一个矩阵通常是由 m 行 n 列组成,我们称之为 矩阵,如果 m 和 n 相同,该矩阵代表一个方阵,我们就可称这个方阵为 m 阶矩阵(方阵),矩阵一般用大写字母来表示。
这个矩阵由两行两列组成,也就是 2 阶矩阵。
可以看出,这个 2 阶矩阵还可以理解为 2 个行向量或者 2 个列向量的集合。
要注意,向量可以理解为一个特殊的矩阵,4 维向量既可以理解为一个 1 行 4 列矩阵,此时这个向量被称为行向量。
也可以理解为一个 4 行 1 列矩阵,此时这个向量被称为列向量。
矩阵的表示方式比较简单,接下来我们看矩阵有哪些运算规则。
矩阵的运算在图形学中非常重要,而矩阵的运算又包括矩阵之间的加减乘除等基本运算,以及转置矩阵、逆矩阵的求法。
两个矩阵相加或者相减需要满足一个条件,即两个矩阵必须同型,同型的意思是,行数和列数都必须一样。一个 m x n 矩阵 和一个 n x m 矩阵(m 不等于 n )是不能进行加减的。
如果满足了以上条件,矩阵加法和减法的运算只需将两个矩阵对应位置上的元素相加或相减即可,得到的新矩阵和原矩阵同型:
矩阵既可以和标量相乘,也能和矩阵相乘,也能和向量相乘。在讲解乘法运算之前,我们必须先明确相乘的顺序,因为矩阵相乘不满足交换律。
所谓左/右乘,是指参与运算的两个因子(向量或者矩阵)在运算中的相对位置,A 左乘 B 即,A 右乘 B即
。
因此在说左乘或者右乘时,必须指定参与运算的两个因子。比如在 OpenGL 或者 WebGL 中对顶点进行矩阵变换,通常使用左乘,即矩阵 M 左乘向量 P,。
比较容易的理解方式就是: 左乘就是从左边过来乘,右乘就是从右边过来乘。
之所以要明确左右乘,是因为矩阵和矩阵不满足交换律,左乘和右乘的结果不一定相同。
矩阵和标量相乘,返回一个新矩阵,新矩阵的各个元素等于原矩阵各个元素与标量的乘积。
矩阵与矩阵相乘也要满足一定条件,假设两个矩阵能够相乘:
那么,他们之间必须满足以下条件:
M 的列数等于 N 的行数
其次,一个 m 行 n 列矩阵 乘以 n 行 q 列矩阵,所得到的新矩阵为 m 行 q 列,即 m行n列 * n行q列 = m行q列。
新矩阵的各个位置的值按照如下规则计算:
矩阵 P 第 m 行 q 列位置的元素等于矩阵 M 第 m 行的各个分量和矩阵 N 第 q 列的各个分量的乘积之和,说起来比较绕,我们用一个公式来表示,矩阵 M 和 N 相乘,得到新的矩阵 P:
矩阵 P 第一行第一列位置的元素等于矩阵 M 第一行的各个分量分别乘以矩阵 N 的第一列的各个分量,然后相加。
矩阵 P 第二行第一列位置的元素等于矩阵 M 的第二行的各个分量分别乘以矩阵 N 第一列的各个分量,然后相加。
以此类推。
如下图:
总的来说,矩阵乘法比较复杂,但是不难,掌握好这个规律就很容易理解了。
另外需要谨记的是,矩阵乘法不满足交换律,但是满足结合律,有如下矩阵:
矩阵与向量相乘其实等价于矩阵与一阶矩阵相乘,其实质是将向量变换为另一个向量,但是要注意乘法的顺序。
假设有一个4 维向量 P 和一个4 阶方阵 M,当 P 左乘 M 时,P 一定要表示成行向量,即 1 * 4 矩阵。当 P 右乘 M 时,P要表示成列向量,即 4 * 1 矩阵。
矩阵转置就是将原来矩阵的行向量转变为列向量,矩阵 M的转置矩阵用符号 来表示,假设有一个矩阵M:
,那么 M的转置矩阵为:
转置运算比较简单。
假设有一个 m 阶方阵 A ,如果存在一个m 阶方阵 B,使得
其中 I 是单位矩阵,那么 B 是 A 的逆矩阵, A 矩阵就是可逆矩阵,也称非奇异矩阵,矩阵 A 的逆矩阵 用
表示。
这里要说明一下什么是单位矩阵,单位矩阵首先是一个方阵,其次方阵对角线上的元素都为 1,其余元素为 0,比如下面就是一个 3 阶单位矩阵:
但并不是所有矩阵都存在逆矩阵,逆矩阵首先必须是方阵,其次存在另一个矩阵与之相乘,能够得到一个单位矩阵。
逆矩阵在图形学中有着非常重要的作用,在图形学中,将一个变换矩阵左乘一个列向量(此处列向量代表顶点坐标),代表了对原始顶点执行了某种变换,比如旋转、缩放、平移等。逆矩阵的意义就是能够撤销这种变换,将变换后的坐标再还原回去。
假设有一个缩放矩阵 M,该矩阵代表放大两倍的变换,还有一个表示顶点坐标的向量 P,那么变换后的顶点 P1:
P1 是 将顶点 P 放大两倍后的新顶点。如果,我们想撤销这种变换,那么只需要求得 M 矩阵的逆矩阵,然后将逆矩阵左乘变换后的顶点 P1 即可。
当然,推导过程也比较简单:
那么,既然逆矩阵这么有用,如何求出逆矩阵呢?
逆矩阵的求解比较复杂,但是不难,只需要按照步骤进行简单的算术运算即可,但是一定要保证每一步不出差错。
这里是求解逆矩阵的步骤,大家可以按照步骤练习一下,限于篇幅本节不再赘述计算过程。
假设有一个方阵M,当且仅当 M 与其转置矩阵的乘积等于单位矩阵时,称其为正交矩阵。即:
所以很容易地得出,
正交矩阵的一个好处是,如果一个矩阵是正交矩阵,那么计算它的逆矩阵时,只需要对原矩阵转置即可,从而减少了计算量(逆矩阵的求解过程是很繁琐的),3D图形学中的最常见的旋转和镜像变换就都是正交的。
判断一个矩阵 M 是否正交的重要条件是:M 的行向量是一个相互正交的单位向量组,什么意思呢? 假设有一个矩阵 M:
其中 为矩阵每一行的行向量,那么矩阵 M 是正交矩阵的充分必要条件是:
可能大家看不懂,其实就是两个条件
这两个条件可以利用公式来证明。利用这个充要条件可以快速判断一个矩阵是否是正交矩阵。同时也可以得到,如果
是正交矩阵,则
也是正交矩阵。
如下是图形学中的一个旋转矩阵,它表示一个绕X轴旋转
角度的变换:
大家可以将这个矩阵验证上面正交重要条件,看看是否符合。
其实是符合的,所以它是正交矩阵。
其实这个也比较容易理解,撤销旋转 角度的变换,就是将变换后的坐标旋转
角度。
以上就是常见的图形学数学理论,这些方法在图形学语言中普遍内置了,比如 GLSL。GLSL 内置了大部分关于向量的运算,以及一小部分的矩阵运算。
有同学或许会问,既然内置了,我们只要会用就可以了,为什么还要学习这些东西呢?
刚才也说了,只是在某些语言内实现了,大部分语言还是没有实现这些运算的,比如 JavaScript,在JavaScript 中计算向量和矩阵需要我们自己实现。那么实现这些运算算法就要必须了解运算规则。而且掌握了这些数学理论,也方便我们扩展新的运算方法。
当然,业界已经有一些成熟的数学库了。我们可以直接拿来使用,但是作为一个有追求的开发者,还是要能够知道它们实现的机理是什么。只有夯实基础功底,才能打造出更稳定,更高质的应用。
趁热打铁,下一节我们使用 JavaScript 实现这些运算,打造出属于自己的数学库。