第三章 学习Shader所需的数学基础(3)

@[TOC]编程

一、 顶点的坐标空间变换过程

咱们知道,在渲染流水线中,一个顶点要通过多个坐标空间的变换才能最终被画在屏幕上。一个顶点最开始是在模型空间中定义的,它最后会被变换到屏幕空间中,获得真正的屏幕像素坐标。所以接下来咱们将解释顶点要进行的各类空间变换的过程。 为了帮助你们理解这个过程,咱们将创建在农场游戏的实例背景下,每讲到一种空间变换,咱们都会解释如何应用到这个案例中。 在咱们的农场游戏中,妞妞很好奇本身是如何渲染到屏幕上的。它只知道本身和一群小伙伴快乐的在农场里吃草,而前面一直有一个摄像机在观察它们。妞妞特别喜欢本身的鼻子,它想知道本身的鼻子是怎么被画到屏幕上的。 在这里插入图片描述 在下面的内容中咱们将了解妞妞的鼻子是如何一步步画到屏幕上的。缓存

2. 模型空间

模型空间(model space),如它的名字所暗示那样,是和某个模型或者对象有关的。有时模型空间也被称为对象空间(object space)或局部空间(local space)。每一个模型都有本身独立的坐标空间,当它移动或旋转的时候,模型空间会跟着它移动和旋转。把咱们本身当成游戏中的模型的话,当咱们在办公室里移动的时候,咱们的模型空间也跟着移动,当咱们转身时,咱们自己的先后左右方向也在跟着改变。 在模型空间中,咱们常用一些方向概念,例如“前(forward)”“后(back)”“左(left)”“右(right)”“上(up)”“下(down)”。在咱们的文章中,咱们把这些方向称为天然方向。模型空间中的坐标轴一般会使用这些天然方向。之前咱们说过,unity在模型空间中使用的是左手坐标系,所以在模型空间中,+x轴、+y轴、+z轴分别对应的是模型的右、上和前向。须要注意的是模型空间模型坐标空间中的x轴、y轴、z轴和天然方向上的对应不必定是上述这种关系,但因为unity使用的是这样的约定,所以本文将使用这种方式。 模型空间的原点和坐标轴一般由美术人员在建模软件里肯定好的。当导入unity后,咱们能够在顶点着色器中访问到模型的顶点信息,其中包含了每一个顶点的坐标。这些坐标都是相对于模型空间中的原点(一般位于模型的重心)定义的。 当咱们把妞妞放到场景中时,就有一个模型坐标空间时刻跟随者它。妞妞鼻子的位置能够经过访问顶点属性获得。假设这个位置是(0,2,4),因为顶点变换中每每包含了平移变换,因次须要把其扩展到齐次坐标系下,获得顶点坐标是(0,2,4,1),以下图所示。 在这里插入图片描述spa

3 世界空间

世界空间(world space)是一个特殊的坐标系,由于它创建了咱们所关心的最大的空间。一些读者可能会指出,空间能够是无限大的,怎么会有最大这一说呢?这里的最大指的是一个宏观的概念,也就是说它是咱们所关心的最外层的坐标空间。以咱们的农场游戏为例,在这个游戏里世界空间指的就是农场,咱们不关心这个农场是在什么地方,在这个虚拟的游戏世界里,农场就是最大的空间概念。 世界空间能够被用于描述绝对位置(较真的读者可能会再次提醒我,没有绝对的位置。没错,但我相信读者能够明白这里绝对的意思)。在本文中,绝对位置指的就是在世界坐标系中的位置。一般,咱们会把世界空间的原点放置在游戏空间的中心。 在unity中,世界空间一样使用了左手坐标系。但它的x轴、y轴、z轴是固定不变的。在unity中,咱们能够经过调整Transform组件中的Position属性来改变模型的位置,这里的位置指的是相对于这个Transform的父节点(parent)的模型坐标空间中的原点定义的。若是一个Transform没有任何父节点,那么这个位置就是在世界坐标系中的位置,以下图所示 在这里插入图片描述 咱们能够想象还有一个虚拟的根模型,这个根模型的模型空间就是世界空间,全部的游戏对象都属于这个根模型。一样,Transform中的Rotation和Scale也是一样的道理。 顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。这个变换一般叫作模型变换(model transform)。 如今咱们对妞妞的鼻子进行模型变换。为此,咱们首先须要知道妞妞在世界坐标系中进行了哪些变换,这能够经过面板中的Transform组件来获得相关的变换信息,以下图所示: 在这里插入图片描述 根据Transform组件上的信息,咱们知道在世界空间中,妞妞进行了(2,2,2)的缩放,又进行了(0,150,0)的旋转以及(5,0,25)的平移。注意这里的变换顺序是不能互换的,即先进行缩放,再进行旋转,最后是平移。据此,咱们能够构建出模型变换的变换矩阵: 在这里插入图片描述 如今咱们能够用它来对妞妞的鼻子进行模型变换了: 在这里插入图片描述 也就是说,在世界空间下,妞妞的鼻子位置是(9,4,18.072)。注意,这里的浮点数都是近似值,这里近似到小数点后3位。实际的数值和unity采用的浮点值精度有关。3d

4. 观察空间

观察空间(view space)也被称为摄像机空间(camera space)。观察空间能够认为是模型空间的一个特例——在全部的模型中有一个很是特殊的模型,即摄像机(虽然一般来讲摄像机自己是不可见的),它的模型空间值得咱们单独拿出来讨论,也就是观察空间。 摄像机决定了咱们渲染游戏所使用的视角。在观察空间中,摄像机位于原点,一样其坐标轴的选择能够是任意的,但因为咱们是以unity为主,而unity中观察空间的坐标轴选择是:+x指向右方,+y指向上方,而正z轴指向摄像机的后方。在这里,读者可能会以为奇怪,咱们以前讨论的模型空间和世界空间中+z轴指的都是物体的前方,为何这里不同了呢?这是由于Unity在模型空间和世界空间选用的都是左手坐标系,而观察空间中使用的是右手坐标系。这是复合OpenGL的传统的,在这样的观察空间中,摄像机的正前方指向的是-z轴方向。 这种左右手坐标系之间改变不多会对咱们在unity中的编程产生影响,由于unity为咱们作了许多渲染底层的工做。可是若是读者须要调用相似Camera.cameraToWorldMatrix、Camera.WorldToCameraMatrix等接口自行计算某模型在观察空间中的位置上,就要当心这样的差别。 最后提醒读者的一点是,观察空间和屏幕空间是不一样的。观察空间是一个三维空间,而屏幕空间是一个二维空间。从观察空间到屏幕空间须要一个操做,那就是投影(projection)。咱们后面会讲到。 顶点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中。这个变换叫观察变换(view transform) 回到咱们的农场游戏。如今咱们须要把妞妞的鼻子从世界空间变换到观察空间中。为此咱们须要知道世界坐标系下摄像机的变换信息。这一样能够经过摄像机面板中的Transform组件获得,以下图所示: 在这里插入图片描述 为了获得顶点在观察空间中的位置,咱们能够有两种方法。一种方法是计算观察空间的三个坐标轴在世界空间下的表示,而后根据之前所讲的方法,构建出从观察空间变换到世界空间的变换矩阵,再对该矩阵求逆来获得从世界空间变换到观察空间的变换矩阵。咱们还可使用另外一种方法,即想象平移整个观察空间,让摄像机原点位于世界坐标原点,坐标轴与世界空间中的坐标轴重合便可。这两种方法获得的变换矩阵都是同样的,不一样的是咱们的思考方式。 这里咱们使用第二种方法。由Transform组件能够知道,摄像机在世界空间中的变换是先按(30,0,0)进行旋转,而后按(0,10,-10)进行了平移。那么为了把摄像机从新移回到初始状态(这里指摄像机原点位于世界坐标原点、坐标轴与世界空间中的坐标轴重合),咱们须要进行逆向变换,即先按(0,-10,10)进行平移,以便摄像机回到原点,再按(-30,0,0)进行旋转,以便让坐标轴重合。所以变换矩阵就是: 在这里插入图片描述 可是,因为观察空间使用的是右手坐标系,所以须要对z份量进行取反操做。咱们能够经过乘以另外一个特殊矩阵来获得最终的观察变换矩阵: 在这里插入图片描述 如今咱们能够用它来对妞妞的鼻子进行顶点变换了: 在这里插入图片描述 这样,咱们就获得了观察空间中妞妞鼻子的位置——(9,8.84,-27.31)。orm

5 裁剪空间

顶点接下来要从观察空间转换到裁剪空间(clip space,也被称为齐次裁剪空间)中,这个用于变换的矩阵叫作裁剪矩阵(clip matrix),也被称为投影矩阵(projection matrix)。 裁剪矩阵的目标是可以方便的对渲染图元进行裁剪:彻底位于这块空间内部的图元会被保留,彻底位于这块空间外部的图元将会被剔除,而与这块空间相交的图元将会被裁剪。那么这块空间是如何决定的呢?答案是由视椎体(view frustum)来决定。 视椎体是指空间中的一片区域,这块区域决定了摄像机能够看到的空间。视椎体由6个平面包围而成,这些平面也被称为裁剪平面(clip planes)。视椎体有两种类型,这涉及到两种投影类型:一种是正交投影(orthographic projection),一种是透视投影(perspective projection)。下图显示了从同一位置、同一角度渲染同一个场景的两种摄像机的渲染结果。 在这里插入图片描述 从图中能够发现,在透视投影中,地板上的平行线不会保持平行,离摄像机越近网格越大,离摄像机越远网格越小。而在正交投影中,全部的网格大小都同样,并且平行线会一直保持平行。能够注意到,透视投影模拟了人眼看世界的方式,而正交投影则彻底保留了物体的距离和角度。所以,在追求真实感的3D游戏中咱们每每会使用透视投影,而在一些2D游戏或渲染小地图等其余HUD元素时,咱们会使用正交投影。 在视椎体的6块裁剪平面中,有两块裁剪平面比较特殊,它们分别被称为近裁剪平面(near clip plane)和远裁剪平面(far clip plane)。它们决定了摄像机能够看到的深度的范围。正交投影和透视投影的视椎体以下图所示。 在这里插入图片描述 从上图能够看出,透视投影的视椎体是一个金字塔形,侧面的4个裁剪平面会在摄像机处相交。它更符合视椎体这个词语。正交投影的视椎体是一个长方体。前面讲到,咱们但愿根据视椎体围成的区域对图元进行裁剪,可是若是直接使用视椎体定义的空间来进行裁剪,那么不一样的视椎体就须要不一样的处理过程,并且对于透视投影的视椎体来讲,想要判断一个顶点是否处于一个金字塔内部是比较麻烦的。所以咱们想要一种更加通用、方便和整洁的方式来进行裁剪的工做,这种方式就是经过一个投影矩阵把顶点转换到一个裁剪空间中。 投影矩阵有两个目的: (1)首先为投影作准备。这是个迷惑点,虽然投影矩阵的名称包含了投影2字,但它并无进行真正的投影工做,而是在为投影作准备。真正的投影发生在后面的齐次除法(homogeneous division)过程当中。而经历过投影矩阵的变换后,顶点w的份量会具备特殊的意义。 读者:投影究竟是什么意思呢? 咱们:能够理解成是一个空间的降维,例如从四维空间投影到三维空间中。而投影矩阵实际上并不会真的进行这个步骤,它会为真正的投影作准备工做。真正的投影会在屏幕映射时发生,经过齐次除法来获得二维坐标。 (2)其次是对x、y、z份量进行缩放。咱们上面讲过直接使用视椎体的6个裁剪平面进行裁剪会比较麻烦。而通过投影矩阵的缩放后,咱们能够直接使用w份量做为一个范围值,若是x、y、z份量都位于这个范围内,就说明该顶点位于裁剪空间内。 在裁剪空间以前,虽然咱们使用了齐次坐标来表示点和矢量,但它们的第四个份量都是固定的:点的w份量是1,方向矢量的w份量是0。通过投影矩阵变换后,咱们会赋予齐次坐标的第四个坐标更加丰富的含义。下面,咱们来看一下两种投影类型使用的投影矩阵具体是什么。对象

5.1 透视投影

视椎体的意义在于定义了场景中的一块三维空间。全部位于这块空间内的物体都会被渲染,不然就会被剔除或裁减。咱们已经知道这块区域由6个裁剪平面定义,那么这6个裁剪平面又是怎么决定的呢?在Unity中,它们由Camera组件中的参数和Game视图的横纵比共同决定,如图所示。 在这里插入图片描述 由图能够看出,咱们能够经过Camera组件的Field of View(简称FOV)属性来改变视椎体竖直方向的张开角度,而Clipping Planes中的Near和Far参数能够控制视椎体的近裁剪平面和远裁剪平面距离摄像机的远近。这样咱们能够求出视椎体近裁剪平面和远裁剪平面的高度,也就是: 在这里插入图片描述 如今咱们还缺少横向信息。这能够经过摄像机的横纵比获得。在Unity中,一个摄像机的横纵比由Game视图的横纵比和Viewport Rect中的W和H属性共同决定(实际上,Unity容许咱们在脚本里经过Camera.aspect进行更改,但这里不作讨论)。假设,当前摄像机的横纵比为Aspect,咱们定义: 在这里插入图片描述 如今,咱们能够根据已知的Near、Far、FOV和Aspect的值来决定透视投影的投影矩阵。以下: 在这里插入图片描述 上面公式的推导部分能够参见本章的扩展阅读部分。须要注意的是,这里的投影矩阵是创建在Unity对坐标系的假定上面,也就是说,咱们针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换z份量范围将在[-w,w]之间的状况。而在相似DirectX这样的图形接口中,它们但愿变换后z份量范围将在[0,w]之间,所以就须要对上面的透视矩阵进行更改。这不在本书的讨论范围内。 而一个顶点和上述矩阵相乘后,能够由观察空间变换到裁剪空间中,结果以下: 在这里插入图片描述 从结果能够看出,这个投影矩阵本质就是对x、y和z份量进行了不一样程度的缩放(固然,z份量还作了一个平移),缩放的目的是为了方便裁剪。咱们能够注意到,此时顶点的w份量再也不是1,而是原先z份量的取反结果。如今,咱们就能够按以下不等式来判断一个变换后的顶点会否位于视锥体内,若是一个顶点在视锥体内,那么它变换后的坐标必须知足: 在这里插入图片描述 任何不知足上述条件的图元都须要被剔除或裁减。下图显示了通过上述投影矩阵后,视椎体的变化: 在这里插入图片描述 从上图还能够注意到,裁剪矩阵会改变空间的旋向性:空间从右手坐标系变换到了坐标系,这意味着离摄像机越远,z值将越大。blog

5.2 正交投影

首先,咱们仍是看一下正交投影中的6个裁剪平面是如何定义的。和透视投影相似,在Unity中,它们也是由Camera组件中的参数和Game视图的纵横比共同决定,以下图所示: 在这里插入图片描述 正交投影的视椎体是一个长方体,所以计算上相比透视投影来讲更简单。由图能够看出,咱们能够经过Camera组件的Size属性来改变视椎体竖直方向高度的一半,而Clipping Planes中的Near和Far参数能够控制视椎体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,咱们能够求出视椎体近裁剪平面和远裁剪平面的高度,也就是: 在这里插入图片描述 如今咱们还缺少横向的信息。一样咱们能够经过摄像机的纵横比获得。假设,当前摄像机的纵横比为Aspect,那么: 在这里插入图片描述 如今,咱们能够根据已知的Near、Far、Size和Aspect的值来肯定正交投影的裁剪矩阵。以下: 在这里插入图片描述 一样,这里的投影矩阵是创建在Unity对坐标系的假定上面的。 一个顶点和上述投影矩阵相乘后的结果以下: 在这里插入图片描述 注意到,和透视投影不一样的是,使用正交投影的投影矩阵对顶点进行变换后,其w份量仍然为1。本质是由于投影矩阵的最后一行不一样,透视投影的投影矩阵的最后一行是[0,0,-1,0],而正交投影的投影矩阵的最后一行是[0,0,0,1]。这样的选择是有缘由的,是为了齐次除法作准备,在后面咱们会讲到。 判断一个变换后的顶点是否位于视椎体内使用的等式和透视投影中同样,这种通用性也是为何要使用投影矩阵的缘由之一。下图显示了通过上述投影矩阵后,正交投影的视椎体变化。 在这里插入图片描述 一样,裁剪矩阵改变了空间的旋向性。能够注意到,通过正交投影变换后的顶点实际已经位于一个立方体内了。 但愿看到这里读者的脑壳尚未爆炸,如今咱们继续来看咱们的农场游戏。在上面,咱们已经帮妞妞肯定了它的鼻子在观察空间中的位置——(9,8。84,-27.31)。如今,咱们要计算它在裁剪空间中的位置。 首先,咱们须要知道农场游戏中使用的摄像机类型。因为农场游戏是一个3D游戏,所以这里咱们使用了透视摄像机。摄像机参数和Game视图的纵横好比图所示: 在这里插入图片描述 据此,咱们能够知道透视投影的参数:FOV为60度,Near为5,Far为40,Aspect为4/3=1.33。那么对应的投影矩阵是: 在这里插入图片描述 而后,咱们用这个投影矩阵来吧妞妞的鼻子从观察空间转换到裁剪空间中。以下 在这里插入图片描述 这样,咱们就求出了妞妞的鼻子在裁剪空间中的位置——(11.691,15.311,23.692,27.31)。接下来Unity会判断妞妞的鼻子是否须要裁剪。经过比较获得,妞妞的鼻子知足下面的不等式: 在这里插入图片描述 由此,咱们能够判断,妞妞的鼻子位于视椎体内,不须要被裁减。接口

6 屏幕空间

通过投影矩阵的变换后,咱们能够进行裁剪操做。当完成了全部的裁剪操做后,就须要进行真正的投影了,也就是说咱们须要把视椎体投影到屏幕空间(screen space)中。通过这一步变换,咱们会获得真正的像素位置,而不是虚拟的三维坐标。 屏幕空间是一个二维空间,所以咱们必须把顶点从裁剪空间投影到屏幕空间中,来生成对应的2D坐标。这个过程能够理解成有两个步骤。 首先,咱们须要进行齐次除法(homogeneous division),也被称为透视除法(perspective division)。虽然这个步骤听起来很陌生,但实际上它很是简单,就是用齐次坐标的w份量去除以x,y,z份量。在OpenGL中,咱们把这一步获得的坐标叫作归一化的设备坐标(Normalized Device Coordinates,NDC)。通过这一步,咱们能够把坐标从齐次裁剪坐标空间转换到NDC中。通过透视投影变换后的裁剪空间,通过齐次除法后会变到一个立方体内。按照OpenGl传统,这个立方体的x,y,z份量的范围都是[-1,1]。可是在DirectX这样的API中,z的份量范围会是[0,1]。而Unity选择了OpenGL这样的裁剪空间,以下图所示: 在这里插入图片描述 而对于正交投影来讲,它的裁剪空间实际上已是一个立方体了,并且因为通过正交投影矩阵变换后的顶点的w份量是1,所以齐次除法并不会对顶点的x,y,z坐标产生影响,以下图所示: 在这里插入图片描述 通过齐次除法后,透视投影和正交投影的视椎体都变换到相同的立方体内,如今咱们能够根据变换后的x和y坐标来映射输出窗口的对应像素坐标。 在Unity中,屏幕空间左下角的像素坐标是(0,0),右上角的像素坐标是(pixelWidth,pixelHeight),因为当前x和y坐标都是[-1,1],所以,这个映射的过程就是一个缩放的过程。 齐次除法和屏幕映射的过程可使用下面的公式来总结: 在这里插入图片描述 上面的式子对x和y份量都进行了处理,那么z份量呢?一般,z份量会被用于深度缓冲。一个传统的方式是把clipz/clipx的值直接存进深度缓存中,但这并非必须的。一般驱动产商会根据硬件来选择最好的存储格式。此时clipw也并不会被抛弃,虽然它完成了它的主要工做——在齐次除法中做为分母来获得NDC,但它仍然会在后续的一些工做中起到重要做用,例如进行透校订插值。 在Unity中,从裁剪空间到屏幕空间的转换是由底层帮咱们完成的,咱们的顶点着色器只须要把顶点转换到裁剪空间便可。 在上一步中,咱们知道了裁剪空间中妞妞鼻子的位置——(11.691,15.311,23.692,27.31)。如今咱们终于能够肯定妞妞鼻子在屏幕上像素的位置。假设,当前屏幕的宽度为400,高度为300。首先咱们要进行齐次除法,把裁剪的坐标投影到NDC中,而后再映射到屏幕空间中。这个过程以下: 在这里插入图片描述 由此,咱们知道了妞妞鼻子在屏幕上的位置——(285.617,234.096)游戏

7. 总结

以上就是一个顶点如何从模型空间变换到屏幕坐标的过程,下图总结了这些空间和用于变换的矩阵: 在这里插入图片描述 顶点着色器最基本的任务就是把顶点坐标从模型空间转换到裁剪空间中。这对应了图中前三个顶点变换过程。而在片元着色器中,咱们一般也能够获得该片元在屏幕空间的像素位置。咱们会在之后的讲解中看到如何获得这些像素的位置。 在Unity中,坐标系的旋向性也随着变换发生了改变。下图总结了Unity各个空间使用的坐标系旋向性。 在这里插入图片描述 从图中能够发现,只有在观察空间中Unity使用了右手坐标系。 须要注意的是,这里给出的仅仅是一些重要的坐标空间。还有一些空间在实际开发中也会遇到,例如切线空间(tangent space)。切线空间一般用于法线映射,在后面咱们会说到。图片

相关文章
相关标签/搜索