计算机图形学之所以深奥难懂,很大原因是在于它是建立在虚拟世界上的数学模型。数学渗透到图形学的方方面面,当然也包括Shader。在学习Shader 的过程中,我们最常使用的就是矢量和矩阵(即数学的分支之一一一线性代数)。
很多读者认为图形学中的数学复杂难懂。的确,一些数学模型在初学者看来晦涩难懂。但很多情况下,我们需要打交道的只是一些基础的数学运算,而只要掌握了这些内容, 就会发现很多事情可以迎刃而解。我们在研究和学习他人编写的Shader 代码时,也不再会疑问: “他为什么要这么写”, 而是“哦,这里就是使用矩阵进行了一个变换而己。”
为了让读者能够参与到计算中来,而不是填压式地阅读,在一些小节的最后我们会给出一些练习题。练习题的答案会在本章最后给出(不要偷看答案! )。需要注意的是,这些练习题并不是可有可无的,我们并非想利用题海战术来让读者掌握这些数学运算,而是想利用这些练习题来阐述一些容易出错或实践中常见的问题。通过这些练习题,读者可以对本节内容有更加深刻的理解。
那么,拿起笔来,让我们一起走进数学的世界吧!
为了让读者更加理解数学计算的几何意义,我们先来假定一个场景。现在,假设我们正在开发一款卡通风格的农场游戏。在这个游戏里,玩家可以在农场里养很多可爱的奶牛。与普通农场游戏不同的是,我们的主角不是玩家,而是一头牛一一妞妞, 如图4.1 所示。妞妞不仅长得壮,它对很多事情都充满了好奇心。
读者: 为什么游戏主角不是玩家呢? 我们: 因为我们的策划就是这么任性。
在故事的一开始,农场世界是没有数学概念的。通过下面的学习,我们会见证数学给这个世界带来了怎样翻天覆地的变化。
在游戏制作中,我们使用数学绝大部分都是为了计算位置、距离和角度等变量。而这些计算大部分都是在笛卡儿坐标系( Cartesian Coordinate System ) 下进行的。这个名字来源于法国伟大的哲学家、物理学家、心理学家、数学家笛卡儿(Rene Descartes )。
那么,我们为什么需要笛卡儿坐标系呢?有这样一个传说,在笛卡儿的一生中,他每天的上午时光儿乎都是在床上度过的。笛卡儿并没有把这段时间用在睡懒觉上,而是思考了很多关于数学和哲学上的问题。有一天,笛卡儿发现一只苍蝇在天花板上爬来爬去,他观察了很长一段时间。笛卡儿想: 我要如何来描述这只苍蝇的运动轨迹呢?最后,笛卡儿意识到,他可以使用这只苍蝇距离房间内不同墙面的位置来描述,如图4.2 所示。他从床上起身,写下了他的发现。然后,他试图描述一些点的位置,正如他要描述苍蝇的位置一样。最后,笛告儿就发明了这个坐标平面。而这个坐标平面后来逐渐发展,就形成了坐标系系统。人们为了纪念笛卡儿的工作,就用他的名字来给这种坐标系进行命名。
一个二维的笛卡儿坐标系包含了两个部分的信息:
而有了这个坐标系我们就可以精确地定位一个点的位置。例如,如果说:“在(1, 2)的位置上画一个点。”那么相信读者肯定知道这个位置在哪里。
我们来看一下笛卡儿坐标系给奶牛农场带来了什么变化。在没有笛卡儿坐标系的时候,奶牛们根本没有明确的位置概念。如果一头奶牛问:“妞妞,你现在在哪里啊?”姐姐只能回答说“我在这里”或者“我在那里”这些模糊的词语。但那头奶牛永远不会知道妞妞的确切位置。而把笛卡儿坐标系引入到奶牛农场后,所有的一切都变得清晰起来。我们把奶牛农场的中心定义成坐标原点,而把地理方向中的东、北定义成坐标轴方向。现在,如果奶牛再问:“姐姐,你现在在哪里啊?”妞妞就可以回答说: “我在东1米、北3米的地方。”,如图4.5 所示。
在三维笛卡儿坐标系中,我们需要定义3 个坐标轴和一个原点。图4.6 显示了一个三维笛卡儿坐标系。
这3个坐标轴也被称为是该坐标系的基矢量(basis vector)。通常情况下,这3 个坐标轴之间是互相垂直的,且长度为1,这样的基矢量被称为标准正交基( orthonormal basis ),但这并不是必须的。例如,在一些坐标系中坐标轴之间互相垂直但长度不为1,这样的基矢量被称为正交基(orthogonal basis ) 。如非特殊说明,本书默认情况下使用的坐标轴指的都是标准正交基。
读者:正交这个词是什么意思呢?
我们:正交可以理解成互相垂直的意思。在下面矩阵的内容中,我们还会看到正交矩阵的概念。
和二维笛卡儿坐标系类似, 三维笛卡儿坐标系中的坐标轴方向也不是固定的, 即不一定是像图4 .6 中那样的指向。但这种不同导致了两种不同种类的坐标系:左手坐标系( left-handed coordinate space )和右手坐标系( right-handed coordinate space) 。
同样,读者可以通过右手来得到一个右手坐标系。举起你的右手,这次食指仍然指向上,中指指向前方,不同的是,大拇指将指向左侧,如图4.8 所示。
正如我们之前所说,左手坐标系和右手坐标系之间无法通过旋转来同时使它们的3 个坐标轴指向重合,如果你不信,你现在可以拿自己的双手来试验一下。
另外一个确定是左手还是右手坐标系的方法是,判断前向( forward ) 的方向。请读者坐直,向右伸直你的右手,此时右手方向就是x 轴的正向,而你的头顶向上的方向就是y 轴的正向。这时,如果你的正前方的方向是z 轴的正向,那么你本身所在的坐标系就是一个左手坐标系: 如果你的正前方的方向对应的是z 轴的负向,那么这就是一个右手坐标系。
除了坐标轴朝向不同之外,左手坐标系和右手坐标系对于正向旋转的定义也不同,即在初高中物理中学到的左手法则(left-hand rule ) 和右手法贝lj (right-hand rule )。假设现在空间中有一条直线,还有一个点,找们希望把这个点以该直线为旋转轴旋转某个角度,比如旋转30度。读者可以拿一支笔当成这个旋转轴,再拿自己的手当成这个需要旋转的点,可以发现,我们有两个旋转方向可以选择。那么,我们应该往哪个方向旋转呢?这意味着,我们需要在坐标系中定义一个旋转的正方向。在左手坐标系中,这个旋转正方向是由左手法则定义的,而在右手坐标系中则是由右手法则定义的。
在左手坐标系中,我们可以这样来应用左手法则:还是举起你的左手,握拳,伸出大拇指让它指向旋转轴的正方向,那么旋转的正方向就是剩下4 个手指的弯曲方向。在右手坐标系中,使用右手法则对旋转正方向的判断类似,如图4.9 所示。
对于开发者来说,使用左手坐标系还是右手坐标系都是可以的,它们之间并没有优劣之分。无论使用哪种坐标系,绝大多数情况下并不会影响底层的数学运算,而只是在映射到视觉上时会有差别(见练习题2 )。这是因为,一个点或者旋转在空间内来说是绝对的。一些较真儿的读者可能会看不惯“绝对”这个词: “你怎么能忽略相对论呢?这世上一切都是相对的!”这些读者请容我解释。这里所说的绝对是说, 在我们所关心的最广阔的空间中,这些值是绝对的。例如我说,把你的书从桌子的左边移到右边,你不会对这个过程产生什么疑问, 此时我们关心的整个空间就是桌子这个空间,而在这个空间中,书的运动是绝对的。但是,在数学的世界中,我们需要使用一种数学模型来精确地描述它们,这个模型就是坐标系。一旦有了坐标系,每个点的位置就不再是绝对的,而是相对于这个坐标系来说的。这种相对关系导致,即便从数学表示上来说两种表示方式完全一样,但从视觉上来说是不一样的。
我们可以在奶牛农场的例子中体会左手坐标系和右手坐标系的分别。我们假设,妞妞想要到一个新的地方,因为那里的草很美味。妞妞知道到达这个目标点的“绝对路径”是怎样的,如图4.10 所示。
我们可以分别在一个左手坐标系和右手坐标系中描述这样一次运动, 即使用数学表达式来描述它。我们会发现,在不同的坐标系中描述这样同一次运动是不一样的,如图4.11 所示。
在左手坐标系中, 3 个坐标轴的朝向如图4.11 左图所示。妞妞首先向x 轴正方向平移1个单位,然后再向z 轴负方向移动4 个单位,最后朝旋转的正方向旋转60。。而在右手坐标系中,+z轴的方向和左手坐标系中刚好相反,因此妞妞首先向x 轴正方向平移1个单位(与左手坐标系中的移动一致),然后再向z 轴正方向移动4 个单位( 与左手坐标系中的移动相反) ,最后朝旋转的负方向旋转60。(与左手坐标系中的旋转相反)。
可以看出,为了达到同样的视觉效果(这里指把妞妞移动到视觉上的同一个位置),左右手坐标系在z 轴上的移动以及旋转方向是不同的。如果使用相同的数学运算(指均向z 轴某方向移动或均朝旋转正方向旋转等〉,那么得到的视觉效果就是不一样的。因此,如果我们需要从左手坐标系迁移到右手坐标系,并且保持视觉上的不变,就需要进行一些转换。读者可以参见本章最后的扩展阅读部分。
对于一个需要可视化虚拟的三维世界的应用(如Unity )来说,它的设计者就要进行一个选择。对于模型空间和世界空间(在4.6 节中会具体讲解这两个空间是什么) , Unity 使用的是左手坐标系。这可以从Scene 视图的坐标轴显示看出来,如图4.12 所示。这意味着,在模型空间中,一个物体的右侧( right)、上侧( up )和前侧( forward )分别对应了x 轴、y 轴和z 轴的正方向。
但对于观察空间来说, Unity 使用的是右手坐标系。观察空间,通俗来讲就是以摄像机为原点的坐标系。在这个坐标系中,摄像机的前向是z 轴的负方向,这与在模型空间和世界空间中的定义相反。也就是说, z 轴坐标的减少意味着场景深度的增加,如图4.13 所示。
关于Unity 中使用的坐标系的旋向性,我们会在4.5.9 节中详细地讲解。
在摄像机的观察空间下,该球体的z 值是多少?在摄像机的模型空间下,该球体的z 值又是多少?
点( point ) 是n 维空间〈游戏中主要使用二维和三维空间〉中的一个位置, 它没有大小、宽度这类概念。在笛卡儿坐标系中,我们可以使用2 个或3 个实数来表示一个点的坐标,如P=(Px, Py)表示二维空间的点, P=(Px, Py, Pz)表示三维空间中的点。
矢量( vector,也被称为向量〉的定义则复杂一些。在数学家看来,矢量就是一串数字。你可能要问了, 点的表达式不也是一串数字吗?没错,但矢量存在的意义更多是为了和标量(scalar)区分开来。通常来讲,矢量是指n 维空间中一种包含了模( magnitude ) 和方向( direction ) 的有向线段,我们通常讲到的速度(velocity 〉就是一种典型的矢量。例如,这辆车的速度是向南80km/h
(向南指明了矢量的方向, 80km/h 指明了矢量的模〉。而标量只有模没有方向, 生活中常常说到的距离( distance )就是一种标量。例如, 我家离学校只有200m (200m 就是一个标量)。
具体来讲。
矢量的表示方法和点类似。我们可以使用v=(x,y)来表示二维矢量,用v=(x,y,z)来表示三维矢量,用v=(x, y, z, w)来表示四维矢量。为了方便阐述, 我们对不同类型的变量在书写和印刷上使用不同的样式。
那么一个矢量要放在哪里呢?从矢量的定义来看,它只有模和方向两个属性,并没有位置信息。这听起来很难理解, 但实际上在生活中我们总是会和这样的矢量打交道。例如,当我们讲到一个物体的速度时,可能会这样说“那个小偷正在以100km/h的速度向南逃窜”(快抓住他!〉,这里的“以100km/h的速度向南”就可以使用一个矢量来表示。通常,矢量被用于表示相对于某个点的偏移
( displacement ), 也就是说它是一个相对量。只要矢量的模和方向保持不变, 无论放在哪里,都是同一个矢量。
尽管上面的内容看起来显而易见,但区分点和矢量之间的不同是非常重要的,尽管它们在数学表达式上是一样的,都是一串数字而已。如果一定要给它们之间建立一个联系的话,我们可以认为,任何一个点都可以表示成一个从原点出发的矢量。为了明确点和矢量的区别,在本书后面的内容中,我们将用于表示方向的矢量称为方向矢量。
4. 单位矢量
在很多情况下,我们只关心矢量的方向而不是模。例如,在计算光照模型时,我们往往需要得到顶点的法线方向和光源方向,此时我们不关心这些矢量有多长。在这些情况下,我们就需要计算单位矢量( unit vector ) 。
单位矢盘指的是那些模为1 的矢量。单位矢量也被称为被归一化的矢量( normalized vector)。对任何给定的非零矢量,把它转换成单位矢量的过程就被称为归一化(normalization)。
给定任意非零矢量v,我们可以计算和v 方向相同的单位矢量。在本书中,我们通过在一个矢量的头上添加一个戴帽符号来表示单位矢量,例如v。为了对矢量进行归一化,我们可以用矢量除以该矢量的模来得到。公式如下:
零矢量(即矢量的每个分量值都为0 ,如v=(0,0,0))是不可以被归一化的。这是因为做除法运算时分母不能为0 。
从几何意义上看,对二维空间来说,我们可以画一个单位圆,那么单位矢量就可以是从圆心出发、到圆边界的矢量。在三维空间中,单位矢量就是从一个单位球的球心出发、到达球面的矢量。
图4.21 给出了三维空间内的一些单位矢量。
5. 矢量的点积
矢量之间也可以进行乘法,但是和标量之间的乘法有很大不同。矢量的乘法有两种最常用的种类:点积(dot product,也被称为内积, inner product ) 和叉积(cross product,也被称为外积, outer product )。在本节中,我们将讨论第一种类型:点积。
读者可能认为上面几节的内容部很简单,“这些都显而易见嘛”。那么从这一节开始,我们就会遇到一些真正需要花费力气(真的只要一点点〉去记忆的公式。幸运的是,绝大多数公式是有几何意义的,也就是说,我们可以通过画图的方式来理解和帮助记忆。
比仅仅记住这些公式更加重要的是,我们要真正理解它们是做什么的。只有这样,我们才能在需要时想起来,“噢,这个需求我可以用这个公式来实现!”在我们编写Shader 的过程中,通常程序接口都会提供这些公式的实现,因此我们往往不需要手工输入这些公式。例如,在Unity Shader中,我们可以直接使用形如dot(a,b)的代码来对两个矢量值进行点积的运算。
点积的名称来源于这个运算的符号: a·b。中间的这个圆点符号是不可以省略的。点积的公式有两种形式,我们先来看第一种。两个三维矢量的点积是把两个矢量对应分量相乘后再取和,最后的结果是一个标量。
下面是一些例子:
需要注意的是,投影的值可能是负数。投影结果的正负号与a 和b 的方向有关:当它们的方向相反(夹角大于90度〉时,结果小于0:当它们的方向互相垂直(夹角为90度)时,结果等于0;当它们的方向相同(夹角小于90度〉时,结果大于0 。
图4.23 给出了这3 种情况的图示。
也就是说,点积的符号可以让我们知道两个矢量的方向关系。
那么,如果a 不是一个单位矢量会如何呢?这很容易想到任何两个矢量的点积a·b 等同于b在a 方向上的投影值,再乘以a 的长度。
点积具有一些很重要的性质,在Shader 的计算中,我们会经常利用这些性质来帮助计算。
性质一: 点积可结合标量乘法。
上面的“结合”是说,点积的操作数之一可以是另一个运算的结果,即矢量和标量相乘的结果。公式如下:
这意味着,我们可以直接利用点积来求矢量的模,而不需要使用模的计算公式。当然,我们需要对点积结果进行开平方的操作来得到真正的模。但很多情况下,我们只是想要比较两个矢量的长度大小,因此可以直接使用点积的结果。毕竟,开平方的运算需要消耗一定性能。
现在是时候来看点积的另一种表示方法了。这种方法是从三角代数的角度出发的,这种表示方法更加具有几何意义,因为它可以明确地强调出两个矢量之间的角度。我们先直接给出第二个公式。
初看之下,似乎和公式一没有什么联系,怎么会相等呢?我们先来看最简单的情况。假设,我们对两个单位矢量进行点积,
即 a·b,如图4.24 所示。
6. 矢量的叉积
另一个重要的矢量运算就是叉积(cross product ),也被称为外积( outer product )。与点积不同的是,矢量叉积的结果仍是一个矢量,而非标量。
和点积类似,叉积的名称来源于它的符号: a × b。同样, 这个叉号也是不可省略的。两个矢量的叉积可以用如下公式计算:
上面的公式看起来很复杂,但其实是有一定规律的。图4.25 给出了这样的规律图示。
例如:
(1,2,3)×(-2,-1,4)=((2)(4)-(3)(-1), (3)(-2)- (1)( 4), (1)(-1) - (2)(-2)) = (8 -(-3),(-6)-4, (-1)-(-4))=( 11 , 10, 3)
需要注意的是,叉积不满足交换律,即a x b ≠ bxa 。实际上,叉积是满足反交换律的, 即a x b= - (b x a)。而且叉积也不满足结合律,即(a × b) ×c ≠ a x (b x c) 。
从叉积的几何意义出发,我们可以更加深入地理解它的用处。对两个矢量进行叉积的结果会得到一个同时垂直于这两个矢量的新矢量。我们已经知道, 矢量是由一个模和方向来定义的, 那么这个新的矢量的模和方向是什么呢?
我们先来看它的模。ax b 的长度等于a 和b 的模的乘积再乘以它们之间夹角的正弦值。公式如下:
读者可能已经发现,上述公式和点积的计算公式很类似,不同的是,这里使用的是正弦值。
如果读者对中学数学还有记忆的话,可能还会发现,这和平行四边形的面积计算公式是一样的。
如果你忘记了,没关系,我们在这里回忆一下。
如图4.26 所示,我们使用a 和b 构建一个平行四边形。
你可能会问,如果a 和b 平行(可以是方向完全相同,也可以是完全相反〉怎么办, 不就不能构建平行四边形了吗?我们可以认为构建出来的平行四边形面积为0,那么a x b=0。注意,这里得到的是零向量,而不是标量0 。
下面,我们来看结果矢量的方向。你可能会说:“方向?不是已经说了方向了嘛,就是和两个矢量都垂直就可以了啊。”但是,如果你仔细想一下就会发现, 实际上我们有两个方向可以选择,这两个方向都和这两个矢量垂直。那么,我们要选择哪个方向呢?
这里就要和之前提到的左手坐标系和右手坐标系联系起来了,如图4.27 所示。
这个结果是怎么得到的呢?来,先举起你的右手。在右手坐标系中, a×b 的方向将使用右手法则来判断。我们先想象把手心放在了a 和b 的尾部交点处,然后张开你的手掌让手掌方向和a 的方向重合,再弯曲你的四指让它们向b 的方向靠拢,最后伸出你的大拇指! 大拇指指向的方向就是右手坐标系中a x b 的方向了。如果你实在不明白怎么摆放和扭动你的手,那么就看图4.28 好了.
同理,我们可以使用左手法则来判断左手坐标系中ax b 的方向。赶紧举起你的左手试试吧(你可能会发现这个姿势比较扭曲。〉!
需要注意的是,虽然看起来左右手坐标系的选择会影响叉积的结果,但这仅仅是“看起来”而己。从叉积的数学表达式可以发现,使用左手坐标系还是右手坐标系不会对计算结果产生任何影响,它影响的只是数字在三维空间中的视觉化表现而己。当从右手坐标系转换为左手坐标系时,所有点和矢量的表达和计算方式都会保持不变,只是当呈现到屏幕上时, 我们可能会发现, “咦,怎么图像反过来了! ”。当我们想要两个坐标系达到同样的视觉效果时,可能就需要改变一些数学运算公式, 这不在本书的范畴内。有兴趣的读者可以参考本章的扩展阅读部分。
那么,叉积到底有什么用呢?最常见的一个应用就是计算垂直于一个平面、三角形的矢量。另外,还可以用于判断三角面片的朝向。读者可以在本节的练习题中找到这些应用。