Unity Shader学习(三)Shader 所需的数学基础
- 一个特殊的位置,即原点,它是整个坐标系的中心。
- 两条过原点的互相垂直的矢量,即X轴和Y轴。这些坐标轴也被称为是该坐标的矢量。
一旦求出来,就可以通过求逆矩阵的方式求出来,因为从坐标空间C变换到坐标空间P 与 从坐标空间P变换到坐标空间C是互逆的两个过程。
可以看出来,变换矩阵实际上可以通过坐标空间C在坐标空间P的原点和坐标轴的矢量表示来构建出来:把3ge坐标轴一次放入矩阵的前3列,把原点矢量放到最后一列,再用0和1填充最后一行即可。
我们可以利用反向思维,从这个变换矩阵反推来获取子坐标空间的元点和坐标轴方向!例如,我们已知从模型空间到世界空间的一个4*4的变换矩阵,可以提取它的第一列再进行归一化后来得到模型空间的x轴在世界空间下的单位矢量表示。同样的方法可以提取y轴和z轴。
另一个有趣的是,对方向矢量的坐标空间变换。我们知道,矢量是没有位置的,因此坐标空间的原点变换是可以忽略的。也就是说,我们仅仅平移坐标系的原点是不会对矢量造成任何影响的。
在Shader中,我们常常会看到截取变换矩阵的前3行前3列来对法线方向、光照方向来进行空间变换,这正是原因所在。
前面说到,可以通过求的逆矩阵的方式求解出来反向变换。但有一种情况我们不需求求解逆矩阵就可以得到,这种情况就是是一个正交矩阵。如果它是一个正交矩阵的话,的逆矩阵就是等于它的转置矩阵。这意味着我们不需要进行复杂的求逆操作就可以得到反向变换。也就是说:
而现在,我们不仅可以根据变换矩阵反推出子坐标空间的坐标轴方向在父坐标空间中的表示,还可以反推出父坐标空间的坐标轴方向在子坐标空间的表示,这些坐标轴对应的就是的每一行!也就是说,如果我们只打坐标空间变换矩阵是一个正交矩阵,那么我们可以提取它的第一列来得到坐标空间A的x轴在坐标空间B下的表示,还可以提取它的第一行来得到坐标空间B的x轴在坐标空间A下的表示。反过来,如果我们知道坐标空间B的x轴、y轴和z轴在坐标空间A下的表示,就可以把它们依次放在矩阵的每一行就可以得到从A到B的变换矩阵了。
模型空间,如它的名字一样,是和某个模型或者说是对象有关的。有时候模型空间也被称为对象空间或局部空间。每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。把我们自己家当成游戏中的模型的话,当我们在办公室里移动时,我们的模型空间也在跟着移动,当我们转身时,我们本身的前后左右方向也在跟着改变。
模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的。当导入到Unity中后,我们可以在顶点着色器中访问到模型的顶点信息,其中包含了每个顶点的坐标。这些坐标都是相对于模型空间中的原点定义的。
世界空间是一个特殊的坐标系,因为它建立了我们所关心的最大的空间。时间空间可以被用于描述绝对位置。
在Unity中,世界空间同样使用了左手坐标系。但它的x轴,y轴,z轴是固定不变的。在Unity中,我们可以通过调整Transform组件中的Position属性来改变模型的位置,这里的位置值是相对于这个Transform的父节点的模型坐标空间中的原点定义的。如果一个Transform没有任何父节点,那么这个位置就是在世界坐标系中的位置。
顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。这个变换通常叫做模型变换。
我们可以对妞妞的鼻子进行模型变换。如下图
根据Transform 组件上的信息,我们知道在世界空间中,妞妞进行了(2,2,2)的缩放,又进行了(0,150,0)的旋转,以及(5,0,25)的平移。注意这里的变换顺序是不能互换的,即先进行缩放,再进行旋转,最后是平移。据此我们可以构建出模型变换的变换矩阵:
现在我们可以用它来对妞妞的鼻子进行模型变换了:
也就是说,在世界空间下,妞妞鼻子的位置是(9,4,18.072).注意,这个的浮点数都是近似值。实际数值和Unity采用的浮点值精度有关。
观察空间也被称为摄像机空间。观察空间可以认为是模型空间的一个特例。在所有的模型中有一个非常特殊的模型,就是摄像机。这个模型空间就是观察空间。
摄像机决定了我们渲染游戏所使用的视角。在观察空间中,摄像机位于原点,同样,其坐标轴的选择可以是任意的,但本文以Unity为主,而Unity中观察空间的坐标轴选择是:+x轴指向右方,+y轴指向上方,而+z轴指向的是摄像机的后方。Unity在模型空间和世界空间中选用的都是左手坐标系,而在观察空间中使用的是右手坐标系。
这种左右手坐标系之间的改变很少会对我们再Unity中的编程产生影响,因为Unity为我们做了很多渲染的底层工作,包括很多坐标空间的转换。但是,如果我们需要调用类似Camera.cameraToWorldMatrix、Camera.worldToCameraMatrix等接口自行计算某模型在观察空间中的位置,就要小心这样的差异。
观察空间和屏幕空间是不同的,观察空间是三维的,而屏幕空间是二维的。
顶点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中,这个变换通常叫做观察变换。
现在我们需要把妞妞的鼻子从世界空间变换到观察空间中。为此,我们需要知道世界坐标系下摄像机的变换信息。这同样可以通过摄像机面板的Transform 组件得到。如下图。
为了得到定在在观察空间中的位置,有两种方法。一种是计算观察空间的3个坐标轴在世界空间下的表示,构建出从观察空间变换到世界空间的变换矩阵,再对该矩阵求逆来得到从世界空间变换到观察空间的变换矩阵。我们还可以使用另一种方法,即想象平移整个观察空间,让摄像机原点位于世界坐标的原点,坐标轴与世界空间中的坐标轴重合即可。这两种方法的变换矩阵都是一样的。
这里我们使用第二种方法:由Transform 组件可以知道,摄像机在世界空间中的变换是先按(30,0,0)进行旋转,然后按(0,10,-10)进行了平移。那么,为了把摄像机重新移回到初始状态,我们需要进行逆向变换,即先按(0,-10,10)平移,以便摄像机回到原点,再按(-30,0,0)进行旋转,以便让坐标轴重合。因此,变换矩阵就是:
但是,由于观察空间使用的是右手坐标系,因此需要对z分量进行取反操作。我们可以通过乘以另一个特殊的矩阵来得到最终的观察变换矩阵:
现在我们可以用它来对妞妞的鼻子进行顶点变换了:
这样我们就得到了观察空间中妞妞鼻子的位置——(9,8.84,-27.31)。
顶点接下来要从观察空间转换到裁剪空间(也称为齐次裁剪空间)中,这个用于变换的矩阵叫做裁剪矩阵,也被称为投影矩阵。
裁剪空间的目标是能够方便地对渲染图元进行裁剪:完全位于这块空间内部的图元将会被保留,完全位于这个空间外部的图元将会被剔除,而与这块空间边界相交的图元就会被裁剪。这块空间由视锥体来决定。
视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。视锥体由留个平面包围而成,这些平面也被称为裁剪平面。视锥体有两种类型,这涉及两种头像类型:一种是正交投影,一种是透视投影。
下图显示了从同一个位置、同一角度渲染同一个场景的两种摄像机的渲染结果。(左图为透视投影,右图为正交投影)
从图中可以发现,在透视投影中,地板的平行线并不会保持平行,离摄像机越近网格越大,离摄像机越远网格越小。而在正交投影中,所有的网格大小都一样,而且平行线会一直保持平行。可以注意到,透视投影模拟了人眼看世界的方式,而正交投影则完全保留了物体的距离和角度。因此在追求真实感的3D游戏中我们往往会使用透视投影,而在一些2D游戏或渲染小地图等其他HUD元素时,我们会使用正交投影。
在视锥体的6块裁剪平面中,有两块裁剪平面比较特殊,它们分别称为近裁剪平面和远裁剪平面。它们决定了摄像机可以看到的深度范围。正交投影和透视投影的视锥体如下图所示。
由上图可以看出,透视投影的视锥体是一个金字塔形,侧面的4个裁剪平面将会在摄像机处相交。它更符合视锥体这个词语。正交投影的视锥体是一个长方体。前面讲到,我们希望根据视锥体围城的区域对图元进行裁剪,但是,如果直接使用视锥体定义的空间来进行裁剪,那么不同的视锥体就需要不同的处理过程,而且对于透视投影的视锥体来说,想要判断一个顶点是否处于一个金字塔内部是比较麻烦的,因此,我们相拥一种更加通用、方便、整洁的方式来进行裁剪的工作,这种方式就是通过一个投影矩阵把顶点转换到一个裁剪空间中。
投影矩阵有两个目的。
首先是为投影做准备。这是个迷惑点,虽然投影矩阵的名称包含了投影二字,但是它并没有记性真正的投影工作,而是在为投影做准备。真正的投影发生在后面的齐次除法过程中。而经过投影矩阵的变换后,顶点的w分量将会具有特殊的意义。
齐次是对x、y、z分量进行缩放。我们上面讲到直接使用视锥体的6个裁剪平面来进行裁剪会比较麻烦。而经过投影矩阵的缩放后,我们可以直接使用w分量作为一个范围值,如果x、y、z都在这个范围内,就说明该顶点位于裁剪空间内。
在裁剪空间之前,虽然我们使用了齐次坐标来表示点和矢量,但它们的第四个分量都是固定的:点的w分量为1,方向矢量的w分量是0。经过投影矩阵的变换后,我们就会赋予齐次坐标的第4个坐标更丰富的含义。下面,我们看下两种投影类型使用的投影矩阵具体是什么。
透视投影
视锥体的意义在于定义了场景中的一块三维空间。所有位于这块空间内的物体将会被渲染,否则就会被剔除或裁剪。我们已经知道,这块区域由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值将越大。
正交投影
首先,我们看下正交投影中的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]。这样的选择是由原因的,是为了为其次除法做准备。
判断一个变换后的顶点是否位于视锥体内使用的不等式和透视投影中的一样,这种通用性也是为什么要使用投影矩阵的原因之一。下图显示了经过上述投影矩阵后,正交投影的视锥体的变化。
同样,裁剪矩阵改变了空间的旋向性。可以注意到,经过正交投影变换后的顶点实际已经位于一个立方体内了。
现在,我们要计算妞妞鼻子在裁剪空间的位置。
我们使用了透视摄像机,摄像机参数和Game视图的横纵比如下图所示:
据此,我们可以知道透视投影的参数:FOV为60°,Near 为 5,Far 40,Aspect 为4/3 =1.333。
那么,对应的投影矩阵就是:
然后,我们用这个投影矩阵来把妞妞的鼻子从观察空间转换到裁剪空间中。如下:
我们就求出了妞妞鼻子在裁剪空间中的位置(11.691,15.311,23.692,27.31)。接来下,Unity会判断妞妞的鼻子是否需要裁剪,通过比较得到,妞妞的鼻子满足下面的不等式:
由此,我们判断出,妞妞的鼻子位于视锥体内,不需要被裁剪。
屏幕空间
经过投影矩阵的变换后,我们可以进行裁剪操作。当完成了所有的裁剪工作后,就需要进行真正的投影了,也就是说,我们需要把视锥体投影到屏幕空间中。经过这一步变换,我们会得到真正的像素位置,而不是虚拟的三维坐标。
屏幕空间是一个二维空间,因此,我们必须把顶点从裁剪空间投影到屏幕空间中,来生成对应的2D坐标。这个过程可以理解成两个步骤。
首先,我们需要进行标准齐次除法,也被称为透视除法。虽然这个步骤听起来很陌生,但是它实际上非常简单,就是用齐次坐标的w分量去除以x、y、z分量。在OpenGL中,我们把这一步得到的坐标叫做归一化的设备坐标。经过这一步,我们可以把坐标从齐次裁剪坐标空间转到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分量被用于深度缓冲。一个传统的方式是把的值直接存进深度缓冲中,但这并不是必须的。通常驱动生产商会根据硬件来选择最好的存储格式。此时clipw也并不会被抛弃,虽然它已经完成了它的主要工作——在齐次除法中作为分母来得到NDC,但它仍然会在后续的一些工作中起到重要的工作,例如进行透视校正插值。
在Unity中,从裁剪空间到屏幕空间的转换是由Unity帮我们完成的。我们的顶点着色器只需要把顶点转换到裁剪空间即可。
现在我们可以确定妞妞的鼻子在屏幕上的像素位置了,假设屏幕像素宽度为400,高度为300。十一选不我们需要进行齐次除法,把裁剪空间的坐标投影到NDC中。然后,再映射到屏幕空间中。这个过程如下:
由此,我们就知道了妞妞鼻子在屏幕上的位置——(285.617,234.096).
法线变换
法线,也被称为法矢量。在游戏中,模型的一个顶点往往会携带额外的信息,而顶点法线就是其中的一种信息。当我们变换一个模型的时候,不仅需要变换它的顶点,还需要变换顶点法线,以便在后续处理中计算光照。
一般来说,点和绝大部分方向矢量都可以使用同一个4*4或3*3的变换矩阵把其从坐标空间A变换到坐标空间B中。但在变换发现的 时候,如果使用同一个变换矩阵,可能就无法确保维持法线的垂直线。
切线,也被称为切矢量与法线类似,切线往往也是模型顶点携带的一种信息,它通常与纹理空间对其,而且与法线方向垂直。如下图:
由于切线是两个顶点之间的差值计算得到的,因此我们可以直接使用用于变换顶点的变换矩阵来变换切线。假设,我们使用3*3的变换矩阵来变换顶点,可以由下面的式子直接得到变换后的切线。
其中T(a)和T(b)分别表示在坐标看空间A下和坐标空间B下的切线方向。但如果直接使用来变换法线,得到的新的法线方向可能就不会与表面垂直了。下图给出了一个例子:
我们知道同一个顶点的切线T(a)和法线N(a)必须满足垂直条件,T(a)·N(a) = 0.给定变换矩阵,我们已经知道了。现在我们想要找到一个矩阵G来变换法线N(a),使得变换后的法线仍然与切线垂直。即
对上式进行一些推导可以得到:
由于,因此如果,那么上式即可成立。也就是说,如果,即使用原变换矩阵的逆转置矩阵来变换法线就可以得到正确的结果。
Unity Shader 的内置变量
使用Unity 写 Shader 的一个好处在于,它提供了很多内置的参数,这使得我们不再需要自己手动计算一些值。本节将给出Unity内置的用于空间变换和摄像机以及屏幕参数的内置变量。这些内置变量可以在UnityShaderVariables.chnic文件中找到定义和说明。
下表给出了Unity5.2 版本提供的所有内置变换矩阵,下面所有的矩阵都是float4×4类型的。
其中有一个矩阵比较特殊,即UNITY_MATRIX_T_MV矩阵。
下表给出了Unity5.2版本提供的摄像机和屏幕参数信息
对于线性变换来说(例如旋转和缩放),仅适用3×3的矩阵就足够表示所有的变换了。但如果存在平移变换,我们就需要使用4×4的矩阵,因此,在对顶点的变换中个,我们通常使用4×4的变换矩阵。当然,在变换前我们需要把点坐标转换成齐次坐标的表示会,即把顶点的w分量设为1。而在对方向矢量的变换中,我们通常使用3×3的矩阵就足够了,这是因为平移变换对方向矢量是没有影响的。
我们通常在Unity Shader中使用CG作为着色器编程语言。在CG中变量类型有很多种。
在CG中,矩阵类型是由float3×3、float4×4等关键词进行声明和定义的。而对于float3、float4等类型的变量,我们即可以把它当成一个矢量,也可以把它当成是一个1×n的行矩阵或者一个n×1的列矩阵。这取决于运算的 种类和它们在运算中的位置。例如,当我们进行点积操作时,两个操作数就被当成矢量类型,如下:
float4 a = float4(1.0,2.0,3.0,4.0); float4 b = float4(1.0,2.0,3.0,4.0); //对两个矢量进行点积操作 float result = dot(a, b);
但在进行矩阵相乘时,参数的位置将决定是按列矩阵还是行矩阵进行乘法。在CG中,矩阵乘法是通过mul函数实现的。例如:
float4 v = float4(1.0, 2.0, 3.0, 4.0); float4×4 M = float4×4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0); //把v当成列矩阵和矩阵M进行右乘 float4 column_mul_result = mul(M, v); //把v当成行矩阵和矩阵M进行左乘 float4 row_mul_result = mul(v, M);
因此,参数的位置会直接影响结果值。通常在变换顶点时,我们都是使用右乘的方式来按列矩阵进行乘法。这是因为,Unity提供的内置矩阵(如UNITY_MATRIX_MVP等)都是按列存储的。但有时,我们也会使用左乘的方式,这是因为可以省去对矩阵的转置的操作。
需要注意的一点是,CG对矩阵类型中元素的初始化和访问顺序。在CG中,对float4×4等类型的变量是按行优先进行填充的。假设我们使用数字(1,2,3,4,5,6,7,8,9)去填充一个3×3的矩阵,如果是按照行优先的方式,得到的矩阵是:
如果是按列优先的话,得到的矩阵是:
CG使用的是行优先的方法,即使一行一行地填充矩阵的
类似地,当我们再CG中访问一个矩阵中的元素时,也是按行来索引的。例如:
//按行优先的方式初始化矩阵M float3×3 M = float3×3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); //得到M的第一行,即(1.0, 2.0, 3.0) float3 row = M[0]; //得到M的第2行第一列的元素,即4.0 float ele = M[1][0]
之所以Unity Shader中的矩阵类型满足上述规则,是因为使用的是CG语言,换句话说,上面的特性都是CG的规定。
在顶点/片元着色器中,有两种方式来获得片元的屏幕坐标。
一种是在片元着色器的输入中声明VPOS或WPOS语义。VPOS是HLSL中对屏幕坐标的语义,而WPOS是CG中对屏幕坐标的语义,两者在Unity Shader都是等价的。我们可以在HLSL/CG中通过语义的方式来定义顶点/片元着色器的默认输入,而不需要自己定义输入输出的数据结构。使用这种写法,可以在片元着色器中这样写:
fixed4 frag(float4 sp : VPOS) : SV_TARGET { //用屏幕坐标除以屏幕分辨率_ScreenParams.xy,得到视口空间中的坐标 return fixed4(sp.xy/_ScreenParams.xy, 0.0, 1.0); }
得到的效果如下图所示:
VPOS/WPOS语义定义的输入是一个float4类型的变量。我们已经知道它的xy值代表了在屏幕空间中的像素坐标。如果屏幕的分辨率为400×300,那么x的范围就是[0.5,400.5],y的范围就是[0.5,300.5]。注意,这里的像素坐标并不是整数值,这是因为openg 和DirectX 10 以后的版本认为像素中心对应的是浮点值中的0.5。在Unity中,VPOS/WPOS的z分量范围是[0,1],在摄像机的近裁剪平面处,z值为0,在远裁剪平面处,z值为1.对于w分量,我们需要考虑摄像机的投影类型。如果是透视投影, 那么w分量的范围是
,Near和Far对应了Camera组件中设置的近裁剪平面和远裁剪平面矩阵摄像机的远近;如果使用的是正交投影,那么w分量的值恒为1.这些值是通过对经过投影矩阵变换后的w分量取倒数后得到的。在代码的最后,我们把屏幕空间除以屏幕分辨率来得到的视口空间中的坐标。视口坐标很简单,就是把屏幕坐标归一化,这样屏幕左下角就是(0,0),右上角就是(1,1)。如果已知屏幕坐标的话,我们只需要把xy值除以屏幕分辨率即可。
另一种方式是通过Unity提供的ComputeScreenPos函数。这个函数在UnityCGcginc里被定义。通常的用法需要两个步骤,首先在顶点着色器中将ComputeScreenPos的结果保存在输出结构体中,然后在片元着色器中进行一个齐次除法运算后得到视口空间下的坐标。例如:
struct vertOut { float4 pos : SV_POSITION; float4 scrPos : TEXCOORD0; } vertOut vert(appdata_base v) { vertOut o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); //第一步:把ComputeScreenPos的结果保存到scrPos中 o.scrPos = ComputeScreenPos(o.pos) return 0; } fixed4 frag(vertOut i) : SV_Target { //第二步,用scrPos.xy除以scrPos.w得到视口空间中的坐标 float2 wcoord = (i.scrPos.xy / i.scrPos.w); return fixed4(wcoord, 0.0, 1.0); }
上面代码的实现效果和上面的代码一样。这种方法实际上是手动实现了屏幕映射的过程,而且它得到的坐标直接就是视口空间中的坐标。我们已经知道了如何将裁剪坐标空间中的点映射到屏幕坐标中。据此,我们可以得到视口空间中的坐标,公式如下:
上面公式的思想就是,首先对裁剪空间下的坐标进行齐次除法,得到阀内在[-1,1]的NDC,然后再将其映射到范围在[0,1]的视口空间下的坐标。