(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)

一、坐标空间

上篇文章讲述了如何使用矩阵来表示基本的变换,如平移、旋转和缩放,在本节我们将关注如何使用这些变换来对坐标空间进行变换。

渲染游戏的过程可以理解成是把一个个顶点经过层层处理最终转换为屏幕上的过程,本节我们就将学习这个转换过程是如何实现的。更具体的来说,顶点经过了哪些坐标空间后,最后被画在了我们的屏幕上。

1.1为什么要使用这么多不同的坐标空间

我们需要在不同的情况下使用不同的坐标空间,因为一些概念只有在特定的坐标空间下才有意义,才更容易理解。这也是为什么在渲染中我们要使用这么多坐标空间。现在我们就来看一下游戏渲染流水线中,一个顶点到底经过了怎样的空间变换。

1.2坐标空间的变换

在渲染流水线中,我们往往需要把一个点或方向矢量从一个坐标空间转换到另一个坐标空间。这个过程是怎么实现的呢?

我们可以定义一个坐标空间,指明其原点位置和3个坐标轴的方向,而这些数值实际上相对于另一个坐标空间的。坐标空间会形成一个层次结构——每个坐标空间都是另一个坐标空间的子空间。通过数学公式可以退出从这个空间到子空间或其父空间的变换矩阵的。

一般从模型空间对方向矢量的坐标空间转换到世界空间是通过一个4X4的变换矩阵的,一个有趣的情况是,对方向矢量的坐标空间变换,矢量是没有位置的,因此坐标空间的原点变换是可以忽略的。也就是说我们仅仅平移坐标系的原点是不会对矢量造成任何影响的。那么对矢量的坐标空间就可以使用3X3的矩阵来表示,因为我们不需要表示平移变换。在shader中,我们常常会看到截取变换矩阵的前三行前三列来对法线方向、光照方向来进行空间变换,这正是原因所在。

1.3顶点的坐标空间的变换过程

在渲染流水线中,一个顶点要经过多个坐标空间的变换才能最终被画在屏幕上。一个顶点最开始是在模型空间中定义的,最后他将会变换到屏幕空间中,得到真正的屏幕像素坐标。接下来的内容将解释顶点要进行的各种空间变换的过程。现在模拟一个场景,一群牛在草原吃草,前边有一个摄像机一直在观察它们,小牛想知道自己的鼻子是怎么被画到屏幕上的,下面将一步步进行了解。

1.4模型空间

模型空间是和某个模型或者说是对象有关的。有时模型空间也被称为对象空间或局部空间。每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。比如自己是游戏模型,我们移动的时候我们的模型空间也在跟着移动,我们转身时本身的前后左右方向也跟着改变。

在模型空间中经常使用一些方向概念,比如前后左右上下等,我们把这些方向称为自然方向,模型空间的坐标轴通常会使用这些自然方向。unity在模型空间中使用左手坐标系,因此在模型空间中,+x,+y,+z轴分别对应的是模型的右、上和前向。我们在Hierarchy视图中单击任意对象就可以看见他们对应的模型空间的3个坐标轴:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第1张图片

模型空间的原点和坐标轴通常由美术人员在建模软件里确定好的。当导入到unity中,我们可以在顶点着色器中访问到模型的顶点信息,其中包含了每个顶点的坐标,这些坐标都是相对于模型空间中的原点(通常位于模型的重心)定义的。若现在小牛的鼻子可以通过访问顶点属性来得到的,假设位置是(0,2,4),由于顶点变换中往往包含了平移变换,因此需要把其扩展到齐次坐标系下,得到顶点坐标是(0,2,4,1)。

1.5世界空间

世界空间是一个特殊的坐标系,因为它建立了我们所关心的最大的空间。世界空间可以被用于描述绝对位置(这里指的是世界坐标系中的位置),通常我们把世界空间的原点放置在游戏空间的中心。

在unity中,世界空间同样使用了左手坐标系。但他的x轴,y轴,z轴是固定不变的。在unity中我们可以通过调整Transfrom组件中的Position属性来改变模型的位置,这里的位置指的是相对于这个Transform的父节点的模型坐标空间中的原点定义的。如果一个transform没有任何父节点,那么这个位置就是在世界坐标系的位置。同样transform的rotation和scale也是同样道理。如果B模型有父节点A,那么这里的position就是在其父节点A的模型空间中的位置。

顶点变换的第一步就是将顶点从模型空间变换到世界空间中。这个变换通常叫做模型变换。

假设我们对小牛先进行(2,2,2)的缩放,又进行了(0,150,0)的旋转以及(5,0,25)的平移。注意这里的顺序是不能互换的,即先进行缩放,然后进行旋转,最后是平移,然后我们可以构建出模型变换的变换矩阵。然后我们可以用它来对小牛的鼻子进行模型变换了。就是用变换矩阵乘上之前我们假设的小牛的鼻子在模型空间下的坐标(0,2,4,1),算出来的最终值就是这个鼻子在世界空间下的位置(假设是(9,4,18.072)),一般这里浮点数都是近似值,实际数值和unity采用的浮点数进度有关。

1.6观察空间

观察空间也被称为摄像机空间。观察空间可以认为是模型空间的一个特例——所有模型中有一个非常特殊的模型,即摄像机,它的模型空间值得我们单独拿出来说,也就是观察空间。

摄像机决定了我们渲染游戏所使用的视角。在观察空间中,摄像机位于原点,同样其坐标轴的选择可以是任意的,unity中观察空间的坐标轴选择是:+x轴指向右方,+y轴指向上分,而+z轴指向的是摄像机的后方。我们之前讨论的模型空间和世界空间中+z轴指向的都是物体的前方,但是unity的模型空间和世界空间选用的都是左手坐标系,而在观察空间使用的是右手坐标系,这是符合OpenGL传统的,再这样的观察空间中,摄像机的正前方向指的是-z轴方向。

观察空间和屏幕空间是不同的,观察空间是一个三维空间,而屏幕空间是一个二维空间,从观察空间袋屏幕空间的转换需要经过一个操作,那就是投影,后面会说到。

顶点变换的第二步,就是将顶点坐标从时间空间变换到观察空间中,这个变换通常叫做观察变换。

现在我们把小牛的鼻子从世界空间变换到观察空间中,为此我们需要知道世界坐标系下摄像机的变换信息,这同样可以通过摄像机面板中的Transform组件得到。

为了得到顶点在观察空间中的位置,我们有两种方法,第一种是计算观察空间中的三个坐标轴在世界空间下的表示,然后构建出观察空间变换到世界空间的变换矩阵,再对该矩阵求逆得到从世界空间变换到观察空间的变换矩阵。还可以使用另一种方法,即想象平移整个观察空间,让摄像机原点位于世界坐标的原点,坐标轴与世界空间中的坐标轴重合即可。这两种方法得到的变换矩阵都是一样的,不同的只是我们思考的方式。

这里使用第二种方法,由摄像机的transform组件可知道(假设现在的position是(0,10,-10),rotation是(0,0,0),scale是(1,1,1)),摄像机在世界空间中的变换先是按(30,0,0)进行旋转,然后按(0,10,-10)进行了平移。那么为了把摄像机重新移回到初始状态(这里指摄像机原点位置世界坐标的原点,坐标轴与世界空间中的坐标轴重合),我们需要进行逆向变换,即先平移,在旋转,因此求得变换矩阵。但是由于观察空间使用的是右手坐标系,因此要对z分量进行取反操作,可以乘一个特殊的单位矩阵来得到最终的观察变换矩阵。然后在乘以之前算出来的鼻子在世界空间下的位置(9,4,18.072),最后我们得到观察空间中小牛鼻子的位置,假设是(9,8.84,-27.31)。

1.7裁剪空间

顶点接下来要从观察空间转换到裁剪空间(也被称为齐次裁剪空间)中,这个用于变换的矩阵叫做裁剪矩阵,也被称为投影矩阵。

裁剪空间的目标是能够方便地对渲染图元进行裁剪:完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,而与这块空间边界相交的图元就会被裁剪。那么这块空间是如何决定的呢?答案是由视椎体来决定。

视椎体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。视椎体由两种类型,这涉及两种投影:一种是正交投影,一种是透视投影(相机默认是这个),如下图。

透视投影:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第2张图片

正交投影: 

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第3张图片

透视投影模拟了人眼看世界的方式,而正交投影完全保留了物体的距离和角度,因此在追求真实感的3D游戏中我们往往使用透视投影,而在一些2D游戏中或渲染小地图等其他HUD元素时,我们会使用正交投影。

在视椎体的6块剪裁平面中,有两块剪裁平面比较特殊,它们分别被称为近剪裁平面和远剪裁平面。它们决定了摄像机可以看到的深度范围。

由上图 可以看出透视投影的视椎体是一个金字塔形,而正视投影是一个长方形,前面说到,我们希望根据视椎体围城的区域对图元进行裁剪,但是如果直接使用视椎体定义的空间来进行裁剪,那么不同的视椎体就需要不同的处理过程,而且对于透视投影的视椎体来说,想要判断一个顶点是否处理一个金字塔内部都是比较麻烦的。因此我们想用一个更加通用、方便和整洁的方式来进行裁剪的工作,这种方式是通过一个投影矩阵把顶点转换到一个裁剪空间中

投影矩阵由两个目的:

(1)首先是为投影做准备,真正的投影发生在后面的齐次除法过程中,而经过投影矩阵变换后,顶点的w分量将会有特殊的意义。

投影的意思:可以理解成一个空间的降维,例如从四维空间投影到三维空间,而投影矩阵实际上并不会真正的进行这个步骤,它会为真正的投影做准备工作。真正的投影会在屏幕映射时发生,通过齐次除法来得到二维坐标。

(2)其次是对x,y,z分量进行缩放,我们上面讲过直接使用视椎体的6个裁剪平面来进行裁剪会比较麻烦。而经过投影矩阵的缩放后,我们可以直接使用w分量作为一个范围的值,如果x,y,z分量都位于这个范围内,就说明该点位于裁剪空间内。

在裁剪空间之前,虽然我们使用了齐次坐标来表示点和矢量,但他们的第四个分量都是固定的:点的w分量是1,方向矢量的w分量是0,经过投影矩阵变换后,我们会赋予齐次坐标的第四个坐标更加丰富的含义,下面看一下两种投影类型使用的投影矩阵具体是什么:

1.7.1透视投影

视椎体的意义在于定义了场景中的一块三维空间,所有位于这块空间的物体将会被渲染,否则会被剔除或裁剪。这6块裁剪平面由camera组件的参数和Game视图的横纵比共同决定。主要是Field of View滑动条,改变竖直方向张开角度,Clipping Planes中两个参数控制视椎体远近剪裁平面距离摄像机的远近,这样我们可以求出视椎体近剪裁平面和远剪裁平面的高度。

现在还缺乏横向信息,这可以通过摄像机横纵比得到,在unity中,一个摄像机的横纵比由Game视图的横纵比和Viewport Rect中的W和H属性共同决定。(也可以在脚本通过Camera.aspect进行更改),假设摄像机的横纵比为Aspect,现在我们根据已知的Near,Far,Field of view,Aspect的值来确定透视投影的投影矩阵。

而一个顶点和上述投影矩阵相乘后,可以由观察空间变换到裁剪空间。

这个投影矩阵的本质就是对x,y,z进行了不同程度的缩放(z分量还做了一个平移),缩放的目的是为了方便裁剪。此时顶点w分量不再是1,而是原先z分量的取反结果。现在我们可以按如下不等式来判断一个变换后的顶点是否位于视椎体内,如果一个顶点在视椎体内,那么它变换后的坐标必须满足

-w≤x≤w

-w≤y≤w

-w≤z≤w

任何不满足上述条件的图元都需要被剔除或者裁剪。

1.7.2正交投影

6个裁剪平面和透视投影类似,在unity中由Camera组件的参数和Game视图的横纵比共同决定,正交投影是个长方体,计算比较简单,可以通过组件的Size属性改变视椎体竖直方向上高度的一半。Near和Far参数跟上节的透视投影一样,控制平面距摄像机远近,求出高度。横向信息通过横纵比得到,最后确定正交投影的裁剪矩阵。

和透视投影不同的是,使用正交投影的投影矩阵对顶点进行变换后,其w分量仍然是1。判断变换后的顶点是否位于视椎体内使用的不等式和透视投影中的一样,这种通用性也是为什么要使用投影矩阵的原因之一。

之前我们计算出小牛的鼻子在观察空间中的位置(9,8.84,-27.31)。现在我们要计算它在裁剪空间中的位置。我们使用的是透视摄像机,根据相机组件参数算出来的投影矩阵,把鼻子从观察空间转换到裁剪空间中,求出的鼻子在裁剪空间中的位置——假设是(11.691,15.311,23.692,27.31)。接下来unity判断鼻子是否需要裁剪,通过不等式带入,满足不等式。鼻子在视椎体内,不需要裁剪。

1.8屏幕空间

经过投影矩阵变换后,我们可以进行裁剪操作。当完全了所有裁剪工作后,就需要进行真正的投影了,也就是说需要把视椎体投影到屏幕空间中。经过这一步的变换,我们会得到真正的像素位置,而不是虚拟的三维坐标。

屏幕空间是一个二维空间,因此必须把顶点从裁剪空间投影到屏幕空间中,来生成对应的2D坐标,这个过程可以理解为两个步骤。

首先进行标准齐次除法,也被称为透视除法。就是用齐次坐标系的w分量去除以x,y,z分量。经过这一步,我们把坐标从齐次裁剪空间转换到NDC中,经过透视投影变换后的裁剪空间,经过齐次除法后会变换到一个立方体内:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第4张图片

而对正交投影来说,它的裁剪空间实际上已经是一个立方体了,因为经过正交投影矩阵变换后顶点的w分量是1,因此齐次除法不会对顶点的x,y,z坐标产生影响。

经过齐次除法后,透视投影和正交投影的视椎体都变换到一个相同的立方体内。接下来可以根据 变换后的x和y坐标来映射输出窗口的对应像素坐标。

在unity中,屏幕空间左下角的像素坐标是(0,0),右上角的像素坐标是(pixelWidth,pixelHeight),由于当前x和y坐标都是[-1,1],因此这个映射的过程就是一个缩放的过程。z分量一般会被用于深度缓冲。

在unity中,从裁剪空间到屏幕空间的转换一般是底层帮我们完成的,我们顶点着色器只需要把顶点转换到裁剪空间即可。最后由此通过齐次除法和映射到屏幕,我们知道了小牛的鼻子在屏幕上的位置。

1.9总结

以上就是一个顶点如何从模型空间变换到屏幕空间的过程,下图总结了这些空间和用于变换的矩阵:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第5张图片

顶点着色器最基本的任务就是把顶点坐标从模型空间转换到裁剪空间中,这对应了上图前三个顶点变换过程,而在片元着色器中,我们通常也可以得到该片元在屏幕空间的像素位置。下节会看到怎么得到这些像素位置。

在unity中,坐标系的旋向性也随着变换发生了改变,下图总结了unity各个空间使用的坐标系旋向性:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第6张图片

发现只有观察空间中unity使用了右手坐标系。

需要注意的是,这里仅仅给出的是一些最重要的坐标空间,还有一些空间在实际开发中会遇到,例如切线空间,切线空间通常用于法线映射,在后面一节我们会讲到。

二、法线变换

我们来看一种特殊的变换:法线变换。

法线也被称为法矢量,是需要我们特殊处理的一种方向矢量。在游戏中,模型的一个顶点往往会携带额外的信息,而顶点法线就是其中一种信息。当我们变换一个模型的时候,不仅需要变换它的顶点,还需要变换顶点法线,以便在后续处理(如片元着色器)中计算光照等。

一般来说,点和绝大部分方向矢量都可以使用同一个4 X4或3X3的变换矩阵把其从坐标空间A变换到坐标空间B中,但在变换法线的时候,如果使用同一个变换矩阵,可能就无法确保维持法线的垂直性。

我们先了解一下另一种方向矢量——切线,也被称为切矢量。与法线类似,切线往往也是模型的顶点携带的一种信息,它通常与纹理空间对齐,而且也法线方向垂直。

由于切线是由两个顶点之间的差值计算得到的,因此我们可以直接使用变换顶点的变换矩阵来变换切线。根据推导和公式,得出我们可以使用用于变换顶点的变换矩阵来直接变换法线,如果变换只包括旋转变换,那么这个变换矩阵就是正交矩阵。如果变换中包含了非统一变换,那么我们必须要求解逆矩阵来得到变换法线的矩阵。

三、unity shader的内置变量(数学篇)

unity写shader提供了很多内置参数,不需要手动算一些值了,这节给出unity内置的用于空间变换和摄像机以及屏幕参数的内置变量。这些变量可以在UnityShaderVariables.cginc文件中找到定义和说明。

3.1变换矩阵

首先是用于坐标空间的变换矩阵。下表给出了unity5.X提供的所有内置变换矩阵,所有矩阵都是float4X4类型的:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第7张图片

3.2摄像机和屏幕参数

unity提供了一些内置变量让我们访问当前正在渲染的摄像机的参数信息,这些参数对应了摄像机上Camera组件中的属性值:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第8张图片

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第9张图片 四、答疑解惑

看完上面的内容可能还有些概念比较疑惑,下面进行答疑解惑。

4.1使用3X3还是4X4的变换矩阵

对应线性变换(例如旋转和缩放来说),仅使用3X3的矩阵就足够表示所有的变换了。但如果存在平移变换,就需要使用4X4的矩阵,因此在顶点变换中,我们通常使用4X4的变换矩阵。当然在变换前需要把点坐标换成齐次坐标表示,把顶点w分量设置为1.而对方向矢量的变换中,我们通常使用3X3的矩阵就够了,因为平移变换对矢量是没有影响的。

4.2Cg中的矢量和矩阵类型

通常在shader中使用Cg作为着色器编程语言,在进行矩阵乘法时,参数的位置决定按列矩阵还是行矩阵进行乘法。在Cg中,矩阵乘法是通过mul函数实现的。通常在变换顶点时,我们都使用右乘的方式来按列矩阵进行乘法。unity提供的内置矩阵UNITY_MATRIX_T_MVP等都是按列存储的。

Cg对矩阵类型中元素的初始化和访问顺序。Cg中float4X4等类型的变量是按行优先的方式进行填充的,如果声明一个3X4矩阵,提供12个数字,那么行填充和列优先填充的矩阵是不同的。当然unity也提供了一种矩阵类型——Matrix4x4,脚本中的这个矩阵采用列优先的方式,这与unity shader中的规定不一样。

4.3unity中的屏幕坐标:ComputeScreenPos/VPOS/WPOS

在写shader时,有时候希望能够获得片元在屏幕上的像素位置。在顶点片元着色器中有两种方式获取片元的屏幕坐标。

一种是在片元着色器输入中声明VPOS或WPOS语义。VPOS是HLSL中对屏幕坐标的语义,而WPOS是Cg对屏幕坐标的语义。我们通过语义的方式来定义顶点片元着色器的默认输入:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第10张图片

视口坐标就是把屏幕坐标归一化,这样屏幕左下角就是(0,0),右上角(1,1),如果已知屏幕坐标的话,只需把xy值除以屏幕分辨率即可。

另一种方式是通过unity提供的ComputeScreenPos函数,这个函数在UnityCG.cginc里被定义。通常的用法需要两个步骤,首先在顶点着色器中将 ComputeScreenPos的结果保存在输出结构体中,然后在片元着色器进行一个齐次除法运算后得到视口空间下的屏幕坐标。例如:

(五)unity shader基础之——————学习shader所需的数学基础:下篇(坐标空间:模型空间、世界空间、观察空间、裁剪空间、屏幕空间、法线变换等)_第11张图片

这种方法实际上手动实现了屏幕映射的过程,而且它得到的坐标直接就是视口空间中的坐标。为什么不在这个函数中直接封装进去齐次除法得到屏幕空间的位置呢?因为unity在顶点着色器这么做的话会破坏插值的结果,从顶点着色器到片元着色器的过程会有一个插值的过程,在顶点着色器进行这个除法,得到的插值结果不准确,原因是我们不可以在投影空间中进行插值,因为这并不是一个线性空间,而插值往往是线性的。 

你可能感兴趣的:(unity,Shader,游戏开发,shader数学基础,unity,shader,游戏开发,坐标空间,顶点变换,shader数学基础)