Skip to content

Latest commit

 

History

History
219 lines (111 loc) · 22.6 KB

Unity Shader顶点坐标空间转换推导.md

File metadata and controls

219 lines (111 loc) · 22.6 KB

Unity Shader坐标系统整理

在下面的内容中,我们将了解妞妞的鼻子是如何一步步画到屏幕上的。

image-20220505194304373

1 模型空间

模型空间(model space),如它的名字所暗示的那样,是和某个模型或者说是对象有关的。有时模型空间也被称为对象空间(object space)或局部空间(local space)。每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。Unity在模型空间中使用的是左手坐标系。

模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的。当导入到Unity中后,我们可以在顶点着色器中访问到模型的顶点信息,其中包含了每个顶点的坐标。这些坐标都是相对于模型空间中的原点(通常位于模型的重心)定义的。

当我们把妞妞放到场景中时,就会有一个模型坐标空间时刻跟随着它。妞妞鼻子的位置可以通过访问顶点属性来得到。假设这个位置是(0, 2, 4),由于顶点变换中往往包含了平移变换,因此需要把其扩展到齐次坐标系下,得到顶点坐标是**(0, 2, 4,1)**,如图4.32所示。

image-20220505194716990

2 世界空间

世界空间(world space)是一个特殊的坐标系,因为它建立了我们所关心的最大的空间。以我们的农场游戏为例,在这个游戏里世界空间指的就是农场,我们不关心这个农场是在什么地方,在这个虚拟的游戏世界里,农场就是最大的空间概念。在Unity中,世界空间同样使用了左手坐标系。

顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。这个变换通常叫做模型变换(model transform)。现在,我们来对妞妞的鼻子进行模型变换。为此,我们首先需要知道妞妞在世界坐标系中进行了哪些变换,这可以通过面板中的Transform组件来得到相关的变换信息,如图4.34所示。

image-20220505195159457

根据Transform组件上的信息,我们知道在世界空间中,妞妞进行了(2, 2, 2)的缩放,又进行了(0, 150, 0)的旋转以及(5, 0, 25)的平移。注意这里的变换顺序是不能互换的,即先进行缩放,再进行旋转,最后是平移。据此我们可以构建出模型变换的变换矩阵:

image-20220505195333779

现在我们可以用它来对妞妞的鼻子进行模型变换了:

image-20220505195406167

也就是说,在世界空间下,妞妞鼻子的位置是**(9, 4, 18.072)**。

3 观察空间

观察空间(view space)也被称为摄像机空间(camera space)。顶点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中,这个变换通常叫做观察变换(view transform)Unity在观察空间中使用的是右手坐标系。

回到我们的农场游戏。现在我们需要把妞妞的鼻子从世界空间变换到观察空间中。为此,我们需要知道世界坐标系下摄像机的变换信息。这同样可以通过摄像机面板中的Transform组件得到,如图4.35所示。

image-20220505195829352

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

这里我们使用第二种方法。由Transform组件可以知道,摄像机在世界空间中的变换是先按(30, 0, 0)进行旋转,然后按(0, 10, -10)进行了平移。那么,为了把摄像机重新移回到初始状态(这里指摄像机原点位于世界坐标的原点、坐标轴与世界空间中的坐标轴重合),我们需要进行逆向变换,即先按(0, -10, 10)平移,以便将摄像机移回到原点,再按(-30, 0, 0)进行旋转,以便让坐标轴重合。因此,变换矩阵就是:

image-20220505200206819

但是,由于观察空间使用的是右手坐标系,因此需要对z分量进行取反操作。我们可以通过乘以另一个特殊的矩阵来得到最终的观察变换矩阵:

image-20220505200244009

现在我们可以用它来对妞妞的鼻子进行顶点变换了:

image-20220505200310095

这样,我们就得到了观察空间中妞妞鼻子的位置——(9, 8.84, -27.31)

4 裁剪空间

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

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

视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。视锥体由六个平面包围而成,这些平面也被称为裁剪平面(clip planes)。视锥体有两种类型,这涉及两种投影类型:一种是正交投影(orthographic projection),一种是透视投影(perspective projection)。图4.36显示了从同一位置、同一角度渲染同一个场景的两种摄像机的渲染结果。

image-20220505200842767

从图中可以发现,在透视投影中,地板上的平行线并不会保持平行,离摄像机越近网格越大,离摄像机越远网格越小。而在正交投影中,所有的网格大小都一样,而且平行线会一直保持平行。可以注意到,透视投影模拟了人眼看世界的方式,而正交投影则完全保留了物体的距离和角度。因此,在追求真实感的3D游戏中我们往往会使用透视投影,而在一些2D游戏或渲染小地图等其他HUD元素时,我们会使用正交投影。

在视锥体的6块裁剪平面中,有两块裁剪平面比较特殊,它们分别被称为近剪裁平面(near clip plane)和远剪裁平面(far clip plane)。它们决定了摄像机可以看到的深度范围。正交投影和透视投影的视锥体如图4.37所示。

image-20220505201001825

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

读者:投影到底是什么意思呢?我们:可以理解成是一个空间的降维,例如从四维空间投影到三维空间中。而**投影矩阵实际上并不会真的进行这个步骤,它会为真正的投影做准备工作。真正的投影会在屏幕映射时发生,通过齐次除法来得到二维坐标。**具体会在4.6.8节中讲到。

投影矩阵有两个目的:

1)**首先是为投影做准备。**这是个迷惑点,虽然投影矩阵的名称包含了投影二字,但是它并没有进行真正的投影工作,而是在为投影做准备。真正的投影发生在后面的齐次除法(homogeneous division)过程中。而经过投影矩阵的变换后,顶点的w分量将会具有特殊的意义。

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

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

4.1 透视投影

视锥体的意义在于定义了场景中的一块三维空间。所有位于这块空间内的物体将会被渲染,否则就会被剔除或裁剪。我们已经知道,这块区域由6个裁剪平面定义,那么这6个裁剪平面又是怎么决定的呢?在Unity中,它们由Camera组件中的参数和Game视图的横纵比共同决定,如图4.38所示。

image-20220505201828445

由图4.38可以看出,我们可以通过Camera组件的Field of View(简称FOV)属性来改变视锥体竖直方向的张开角度,而Clipping Planes中的Near和Far参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,我们可以求出视锥体近裁剪平面和远裁剪平面的高度,也就是:

image-20220505201933689

现在我们还缺乏横向的信息。这可以通过摄像机的横纵比得到。在Unity中,一个摄像机的横纵比由Game视图的横纵比和Viewport Rect中的W和H属性共同决定(实际上,Unity允许我们在脚本里通过Camera.aspect进行更改,但这里不做讨论)。假设,当前摄像机的横纵比为Aspect,我们定义:

image-20220505202026666

现在,我们可以根据已知的Near、Far、FOV和Aspect的值来确定透视投影投影矩阵。如下:

image-20220505202052675

上面公式的推导部分可以参见本章的扩展阅读部分。需要注意的是,这里的投影矩阵是建立在Unity对坐标系的假定上面的,也就是说,我们针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换后z分量范围将在[-w, w]之间的情况。而在类似DirectX这样的图形接口中,它们希望变换后z分量范围将在[0,w]之间,因此就需要对上面的透视矩阵进行一些更改。这不在本书的讨论范围内。而一个顶点和上述投影矩阵相乘后,可以由观察空间变换到裁剪空间中,结果如下:

image-20220505202146102

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

image-20220505202252912

任何不满足上述条件的图元都需要被剔除或者裁剪。图4.39显示了经过上述投影矩阵后,视锥体的变化。

image-20220505202326917

从图4.39还可以注意到,裁剪矩阵会改变空间的旋向性:空间从右手坐标系变换到了左手坐标系。这意味着,离摄像机越远,z值将越大。

4.2 正交投影

首先,我们还是看一下正交投影中的6个裁剪平面是如何定义的。和透视投影类似,在Unity中,它们也是由Camera组件中的参数和Game视图的横纵比共同决定,如图4.40所示。

image-20220505202833310

正交投影的视锥体是一个长方体,因此计算上相比透视投影来说更加简单。由图可以看出,我们可以通过Camera组件的Size属性来改变视锥体竖直方向上高度的一半,而Clipping Planes中的Near和Far参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,我们可以求出视锥体近裁剪平面和远裁剪平面的高度,也就是:

image-20220505202949365

现在我们还缺乏横向的信息。同样,我们可以通过摄像机的横纵比得到。假设,当前摄像机的横纵比为Aspect,那么:

image-20220505203012284

现在,我们可以根据已知的Near、Far、Size和Aspect的值来确定正交投影的裁剪矩阵。如下:

image-20220505203108537

上面公式的推导部分可以参见本章的扩展阅读部分。同样,这里的投影矩阵是建立在Unity对坐标系的假定上面的。一个顶点和上述投影矩阵相乘后的结果如下:

image-20220505203151180

注意到,和透视投影不同的是,**使用正交投影的投影矩阵对顶点进行变换后,其w分量仍然为1。本质是因为投影矩阵最后一行的不同,透视投影的投影矩阵的最后一行是[0 0 -1 0],而正交投影的投影矩阵的最后一行是[0 0 0 1]。这样的选择是有原因的,是为了为齐次除法做准备。**具体会在下一节中讲到。

判断一个变换后的顶点是否位于视锥体内使用的不等式和透视投影中的一样,这种通用性也是为什么要使用投影矩阵的原因之一。图4.41显示了经过上述投影矩阵后,正交投影的视锥体的变化。

image-20220505203329496

同样,裁剪矩阵改变了空间的旋向性。可以注意到,经过正交投影变换后的顶点实际已经位于一个立方体内了。现在,我们继续来看我们的农场游戏。在4.6.6节的最后,我们已经帮助妞妞确定了它的鼻子在观察空间中的位置——(9,8.84, -27.31)。现在,我们要计算它在裁剪空间中的位置

首先,我们需要知道农场游戏中使用的摄像机类型。由于农场游戏是一个3D游戏,因此这里我们使用了透视摄像机。摄像机参数和Game视图的横纵比如图4.42所示。

image-20220505203448338

据此,我们可以知道**透视投影的参数:FOV为60°, Near为5, Far为40, Aspect为4/3 = 1.333。**那么,对应的投影矩阵就是:

image-20220505203510429

然后,我们用这个投影矩阵来把妞妞的鼻子从观察空间转换到裁剪空间中。如下:

image-20220505203602887

这样,我们就求出了妞妞的鼻子在裁剪空间中的位置——(11.691, 15.311,23.692, 27.31)。接下来,Unity会判断妞妞的鼻子是否需要裁剪。通过比较得到,妞妞的鼻子满足下面的不等式:

image-20220505203642961

由此,我们可以判断,妞妞的鼻子位于视锥体内,不需要被裁剪。

5 屏幕空间

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

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

首先,我们需要进行标准齐次除法(homogeneous division),也被称为透视除法(perspective division)。虽然这个步骤听起来很陌生,但是它实际上非常简单,就是用齐次坐标系的w分量去除以x、y、z分量。在OpenGL中,我们把这一步得到的坐标叫做归一化的设备坐标(Normalized Device Coordinates,NDC)。经过这一步,我们可以把坐标从齐次裁剪坐标空间转换到NDC中。

经过透视投影变换后的裁剪空间,再经过齐次除法后会变换到一个立方体内。
透视投影变到裁剪空间后,裁剪空间÷w之前为视锥体,÷w之后变成NDC,为立方体;正交投影变到裁剪空间后,就直接是立方体。

按照****OpenGL的传统,这个立方体的x、y、z分量的范围都是[-1, 1]**。**但在DirectX这样的API中,z分量的范围会是[0, 1]。而Unity选择了OpenGL这样的齐次裁剪空间,如图4.43所示。

image-20220505204320321

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

image-20220505204348711

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

在Unity中,屏幕空间左下角的像素坐标是(0, 0),右上角的像素坐标是(pixelWidth, pixelHeight)。由于当前x和y坐标都是[-1, 1],因此这个映射的过程就是一个缩放的过程。齐次除法屏幕映射的过程可以使用下面的公式来总结:

image-20220505204527901

上面的式子对x和y分量都进行了处理,那么z分量呢?通常,z分量会被用于深度缓冲。一个传统的方式是把clipz/clipw的值直接存进深度缓冲中,但这并不是必须的。通常驱动生产商会根据硬件来选择最好的存储格式。此时clipw也并不会被抛弃,虽然它已经完成了它的主要工作——在齐次除法中作为分母来得到NDC,但它仍然会在后续的一些工作中起到重要的作用,例如进行透视校正插值。

在Unity中,从裁剪空间到屏幕空间的转换是由Unity帮我们完成的。我们的顶点着色器只需要把顶点转换到裁剪空间即可。

在上一步中,我们知道了裁剪空间中妞妞鼻子的位置——(11.691, 15.311,23.692, 27.31)。现在,我们终于可以确定妞妞的鼻子在屏幕上的像素位置。假设,当前屏幕的像素宽度为400,高度为300。首先,我们需要进行齐次除法,把裁剪空间的坐标投影到NDC中。然后,再映射到屏幕空间中。这个过程如下:

image-20220505204820862

由此,我们知道了妞妞鼻子在屏幕空间的位置——(285.617, 234.096)

6 总结

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

image-20220505205211250

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

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

image-20220505205519736

从图4.46中可以发现,只有在观察空间中Unity使用了右手坐标系

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