第4章 学习Shader所需的数学基础-坐标空间
注意:图片的来源基本来自作者冯乐乐的GitHub,感谢作者分享
https://github.com/candycat1992/Unity_Shaders_Book
矩阵可以表示基本变换:平移、旋转和缩放
使用矩阵对坐标空间进行变换:
1、顶点着色器 的基本功能就是把模型的顶点坐标从 模型空间 转换到 齐次裁剪坐标空间 中(前三个顶点变换过程)。
2、片元着色器 也可以得到该片元在屏幕空间的像素位置。
在Unity中,坐标系的旋向性也随着变换而发生改变:(注意:只有在观察空间中 Unity使用了右手坐标系,其他都是使用左手坐标系)
渲染游戏的过程其实就是把一个个顶点经过层层处理最终转化为屏幕上的过程
(一个顶点如何从模型空间变换到屏幕坐标的过程)
为什么在渲染中要使用这么多不同的坐标空间:
需要在不同的情况下使用不同的坐标空间,因为一些概念只有在特定的坐标空间下才有意义,才更容易理解。
在编写shader的过程中,很多看起来很难理解和复杂的数学运算,都是为了在不同坐标空间之间转换点和矢量。
所有的坐标空间在理论上都是平等的,不会因为从一个坐标空间转换到另一个坐标空间计算就出错。
在游戏渲染流水线中,一个顶点或者方向矢量,从一个坐标空间转换到另一个坐标空间的过程:
想要定义一个坐标空间,必须指明其原点位置和3个坐标轴的方向。这些数值实际上是相对于另一个坐标空间的(所有都是相对的)。坐标空间会形成一个层次结构,这个层次结构中的每个坐标空间都是另外一个坐标空间的子空间(即 每个空间都有一个父坐标空间)。
对坐标空间的变换实际上就是在父空间和子空间之间对点和矢量进行变换。
父坐标空间 P
子坐标空间 C
1、把子坐标空间下表示的点或者矢量 转换到 父坐标空间下的表示
2、把 父坐标空间下的表示的点或者矢量 转换到 子坐标空间下的表示
如何求得从 子坐标空间 到 父坐标空间 的变换矩阵 M(C->P):
推导过程:
1、从坐标空间的原点开始,子坐标空间的原点位置:
2、x轴的矢量表示:
所以向x轴方向移动 a 个单位的表示:
再向y轴方向移动b个单位:
最后向z轴方向移动c个单位:
最后表示为:
3、对公式进行求解
所以,最后得到从 子坐标空间 到 父坐标空间 的变换矩阵 M(C->P):
通过坐标空间C在坐标空间P中的原点和坐标轴的矢量表示构建出来:把3个坐标轴依次放入矩阵的前3列,把原点矢量放到最后一列,在用0和1填充最后一行
因为从坐标空间 C 变换到坐标空间 P 与 从坐标空间 P 变换到坐标空间 C 是互逆的
所以求逆矩阵,可以得到从 父坐标空间 到 子坐标空间 的变换矩阵 M(P->C):
利用反向思维,从这个变换矩阵 反推 子坐标空间的原点 和 坐标轴方向:
例如,当已知从模型空间到世界空间的一个 4×4 的变换矩阵 M(C->P)
1、得到模型的 x轴 在世界空间下的单位矢量表示:可以提取这个矩阵的第一列再进行归一化(归一化可以消除缩放的影响)
2、得到模型的 y轴 在世界空间下的单位矢量表示:可以提取这个矩阵的第二列再进行归一化(归一化可以消除缩放的影响)
3、得到模型的 z轴 在世界空间下的单位矢量表示:可以提取这个矩阵的第三列再进行归一化(归一化可以消除缩放的影响)
已知 矩阵 M(C->P) 可以把一个方向矢量从 坐标空间C 变换到 坐标空间P 中,那么只需要用这个矩阵来变换坐标空间C 中的x轴 ( 1, 0, 0, 0 ),也就是说是 矩阵乘法:
得到的结果就是 矩阵 M(C->P) 的第一列
对 方向矢量 的 坐标空间 变换:
因为矢量没有位置,所以坐标空间的原点变换可以忽略。
因此仅仅平移坐标系的原点不会对矢量造成任何影响。
因为不需要表示平移变换,所以对于 矢量的坐标空间变换,可以使用 3×3矩阵 表示:
因此,在Shader中,截取变换矩阵的 前3行前3列 就可以对 法线方向、光照方向 进行空间变换
注意:如果是一个正交矩阵,那么就不需要求解逆矩阵就可以得到 从父坐标空间 到 子坐标空间 的变换矩阵,因为 正交矩阵的逆矩阵就是它的转置矩阵,意味着不需要进行复杂的求逆操作就可以得到反向变换。直接求转置矩阵即可:
如果 空间变换矩阵 M(C->P) 是一个 正交矩阵,那么就可以:
比如,对于x轴:
1、提取这个 正交矩阵 的第一列,得到 坐标空间C 的 x轴 在 坐标空间P 下的表示,
2、提取这个 正交矩阵 的第一行,得到 坐标空间P 的 x轴 在 坐标空间C 下的表示
反过来,如果知道 坐标空间P 的x轴、y轴 和 z轴(必须是单位矢量,否则构建出来的就不是正交矩阵)在 坐标空间C 下的表示,就可以把 这几个坐标轴 依次放在矩阵 的每一行,就可以得到从 C 到 P 的变换矩阵。
在渲染流水线中,一个顶点要经过多个坐标的变换才能最终显示在屏幕上。
顶点最开始是在模型空间中定义的,最后换变换到屏幕空间中,从而得到真正的屏幕像素坐标。
模型空间:(model space)
和模型(对象)有关,模型空间也称为 对象空间 或 局部空间
每个模型都有自己独立的坐标空间,当它移动或者旋转的时候模型空间也会跟着一起移动和旋转。
方向(自然方向):前、后、左、右
Unity在模型空间中使用的是左手坐标系
模型空间的原点和坐标通常是由美术人员在建模软件里面确定好的。当导入Unity后就可以在顶点着色器中访问到模型的顶点信息,其中包含了每个顶点的坐标。这些坐标都是相对于模型空间中的原点(通常位于模型的重心)定义的。
世界空间:(world space)
特殊坐标系,这个空间是开发者所关心的最外层的坐标空间。
世界空间可以被用于描述绝对位置,这个绝对位置指的是在世界坐标系中的位置。
通常会把世界空间的原点放置在游戏空间的中心
在Unity中,世界空间同样使用的是左手坐标系。但是它的x轴、y轴、z轴是固定不变的。
在Unity中调整 Transform 组件的 Position属性来改变模型的位置(这个位置指的是,相对于Transform的父节点的模型坐标空间中的原点定义的)
顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。
农场游戏中的世界空间。世界空间的原点被放置在农场的中心。左下角显示了妞妞在世界空间中所做的变换。我们想要把妞妞的鼻子从模型空间变换到世界空间中。
先进行了( 2, 2, 2) 的缩放,再进行 (0, 150, 0) 的旋转,最后进行 (5, 0, 25) 的平移。
注意:这里的变换顺序是不能互换的,即 先进行缩放,再进行旋转,最后是平移
构建出模型变换的变换矩阵(从右往左):
使用这个变换矩阵,对模型的鼻子进行模型变换,鼻子的模型坐标是 ( 0, 2, 4 ):
最后就得到了模型的鼻子在世界空间中的位置 ( 9, 4, 18.072 ),浮点数是近似值(近似到小数点后3位)。实际数值和Unity采用的浮点值精度有关。
观察空间:(摄像机空间 view space)
即:摄像机的模型空间
摄像机决定了渲染游戏使用的视角,在观察空间中摄像机位于原点。
Unity中观察空间的坐标轴选择的是:+x 轴指向右方,+y 轴 指向上方,+z 轴 指向摄像机的后方
注意:Unity在模型空间 和 世界空间中,都是左手坐标系,而在观察空间中使用的是 右手坐标系。
因为这样符合 OpenGL 的规范,在这样的观察空间中,摄像机的正前方指向的是 -z 轴的方向
如果在调用 类似 Camera.cameraToWorldMatrix、Camera.worldToCameraMatrix 等接口去计算模型在观察空间中的位置的时候,要注意差异。
注意:观察空间 和 屏幕空间 是不同的
观察空间:三维
屏幕空间:二维
从观察空间 到 屏幕空间 需要转换操作:投影
将 顶点坐标 从 世界空间 变换到 观察空间 中,顶点变换的第二步,
从 世界空间 变换到 观察空间:需要知道世界坐标系下摄像机的交换信息
农场游戏中摄像机的观察空间。观察空间的原点位于摄像机处。注意在观察空间中,摄像机的前向是z轴的负方向(图中只画出了z轴正方向),这是因为Unity在观察空间中使用了右手坐标系。左下角显示了摄像机在世界空间中所做的变换。我们想要把妞妞的鼻子从世界空间变换到观察空间中
得到顶点在观察空间中的位置的两种方法:(得到的变换矩阵的都是一样的,只是思考的方式不同)
方法1:计算观察空间的三个坐标轴在世界空间下的表示,然后构建出从观察空间变换到世界空间的变换矩阵,再对该矩阵求逆从而得到从世界空间变换到观察空间的变换矩阵
方法2:想象平移整个观察空间,让摄像机原点位于世界坐标的的原点,坐标轴与世界空间中的坐标轴重合。
从 摄像机Transform组件 可以得到 摄像机的变换:先按 ( 30, 0, 0 ) 旋转,再按照 ( 0, 10, -10 ) 进行平移。
进行逆向变换,将摄像机重新移回到初始状态(摄像机原点位于世界坐标的原点、坐标轴与世界空间中的坐标轴重合)
先按 ( 0, -10, 10 ) 进行平移,再按 ( -30, 0, 0 ) 旋转,最后坐标轴重合。得到变换矩阵:
由于观察空间使用的是右手坐标系,因此需要对 z分量进行取反操作。
最后就可以使用得到的观察变换矩阵,对模型进行顶点变换:
得到观察空间中模型的位置:( 9, 8.84, -27.31 )
裁剪空间:(齐次裁剪空间 clip space)
使用裁剪矩阵(投影矩阵)进行变换,从观察空间转换到裁剪空间。
目标:方便对渲染图元进行裁剪。完全位于这块空间内部的图元将会被保留用于渲染,完全位于这块空间外部的图元将会被剔除,而与这块空间边界相交的图元就会被裁剪。
视锥体平面的设定:
Camera 组件的 Field of View(FOV)属性:改变视锥体竖直方向的张开角度
Clipping Planes:Near 和 Far 参数 控制视锥体的 近裁剪平面 和 远裁剪平面 距离摄像机的远近。
求出视锥体 近裁剪平面 和 远裁剪平面 的高度:
横向信息:通过摄像机的横纵比得到。
Unity中一个摄像机的横纵比由 Game 视图的横纵比 和 Viewport Rect 中的 W 和 H 属性共同决定
假设当前摄像机的横纵比为 Aspect,定义:
确定透视投影的投影矩阵:(针对观察空间是右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换后 z分量 范围在 [ -w , w ] 之间,如果是 DirectX 这样的图形接口,那么变换后 z分量 范围要在 [ 0 , w ]之间,需要投影矩阵进行修改)
在透视投影中,一个顶点 和 投影矩阵 相乘后,可以由 观察空间 变换到 裁剪空间 中:
投影矩阵的本质是对 x、y 和 z分量 进行不同程度的缩放(z分量 还做了一个平移)
缩放的目的是为了方便裁剪
此时的 w分量 不再是1,而是原来的 z分量 的取反结果。
按照如下不等式判断一个变换后的顶点是否位于视锥体内。如果一个顶点在视锥体内,那么变换后的坐标必须满足:
任何不满足这个条件的图元都需要被剔除或者裁剪
在透视投影中,投影矩阵对顶点进行了缩放。4个关键点经过投影矩阵变换后的结果。从这些结果可以看出x、y、z和w分量的范围发生的变化
裁剪矩阵 会改变空间 旋向性:空间从右手坐标系变换到了左手坐标系。离摄像机越远,z值将会越大
正交投影的6个裁剪平面的定义:
由 Camera 组件中的参数和 Game视图的 横纵比共同决定
长方体。
通过 Camera 组件的 size 属性改变视锥体竖直方向上高度的一半。
Clipping Planes中的 Near 和 Far 参数控制视锥体的 近裁剪平面 和 远裁剪平面 距离摄像机的远近。
求出 视锥体 近裁剪平面 和 远裁剪平面 的高度:
横向信息:通过摄像机的横纵比得到。
假设当前摄像机的横纵比为 Aspect,定义:
正交投影 的 投影矩阵:(针对观察空间是右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换后 z分量 范围在 [ -w , w ] 之间,如果是 DirectX 这样的图形接口,那么变换后 z分量 范围要在 [ 0 , w ]之间,需要对投影矩阵进行修改)
在正交投影中,一个顶点 和 投影矩阵 相乘后,可以由 观察空间 变换到 裁剪空间 中:
注意:正交投影 的 投影矩阵 对顶点进行变换后,其 w分量 仍然为 1。(和透视投影不同)
本质原因是因为 投影矩阵 的最后一行的不同,为齐次除法做准备。
透视投影 的 投影矩阵 的 最后一行是 [ 0 0 -1 0 ]
正交投影 的 投影矩阵 的 最后一行是 [ 0 0 0 1 ]
按照如下不等式判断一个变换后的顶点是否位于视锥体内。如果一个顶点在视锥体内,那么变换后的坐标必须满足:(与 透视投影 一样)
任何不满足这个条件的图元都需要被剔除或者裁剪
在正交投影中,投影矩阵对顶点进行了缩放。4个关键点经过投影矩阵变换后的结果。从这些结果可以看出x、y、z和w分量的范围发生的变化
裁剪矩阵 会改变空间 旋向性:空间从右手坐标系变换到了左手坐标系。离摄像机越远,z值将会越大
经过正交投影变换后的顶点实际上已经位于一个立方体内了
视锥体决定了空间的裁剪(摄像机可以看到的空间)。
视锥体:六个裁剪平面包围。视锥体的意义在于定义了场景中的一块三维空间。
两种类型:
1、正交投影(orthographic projection)
所有网格大小都一样,且平行线会一直保持平行
保留物体的距离和角度
长方体
2、透视投影(perspective projection)
离摄像机越近网格越大,离摄像机越远网格越小
模拟人眼看世界
金字塔形
侧面四个裁剪平面会在摄像机处相交
特殊的裁剪平面:近裁剪平面(near clip plane) 和 远裁剪平面(far clip plane)。决定了摄像机看到的深度范围。
如果直接使用视锥体定义的空间进行裁剪,那么不同的视锥体就需要不同的处理过程,而且对于透视投影的视锥体来说,想要判断一个顶点是否处于一个金字塔内部也是比较麻烦的。因此就通过一个投影矩阵的方式,把顶点转换到一个裁剪空间中。(更加通用、方便和整洁的方式去进行裁剪)
投影裁剪的目的:
1、为投影做准备。
注意:
投影矩阵虽然名称包含投影二字,但是并没有进行真正的投影工作,而是在位投影做准备。
真正的投影发生在后面的齐次除法过程中。
而经过投影矩阵的变换后,顶点的 w分量会有特殊的意义。
投影:空间的降维。四维空间投影到三维空间。投影矩阵实际上并不会真的执行空间降维,但是会为真正的投影做准备工作。真正的投影会在屏幕映射时发生,通过齐次除法得到二维坐标。
2、对 x、y、z 分量进行缩放。直接使用视锥体的6个裁剪平面进行裁剪会比较麻烦,而经过投影矩阵的缩放之后,可以直接使用 w分量 作为一个范围值,如果 x、y、z 分量都在这个范围内,那么就说明这个顶点在裁剪空间内。
裁剪空间之前使用的是齐次坐标表示点和矢量,这时的 w分量 是固定的,点的 w分量 是1,方向矢量的 w分量 是0。
投影矩阵后,齐次坐标 的 w分量会有更加丰富的含义
已知使用的是透视摄像机, 模型 在 观察空间 中的位置(9, 8.84, -27.31),通过 透视投影 的参数:FOV 为 60°,Near 为 5,Far 为 40,Aspect 为 4/3 = 1.333。得到对应的 投影矩阵:
使用 投影矩阵 将模型 从 观察空间 转换到 裁剪空间
得到 模型 在 裁剪空间 中的位置—— ( 11.691, 15.311, 23.692, 27.31)
得到模型在裁剪空间中的位置之后,Unity会判断模型是否需要裁剪,通过比较的公式:
得出结论:模型位于 视锥体 内,不需要被裁剪。
将 视锥体 投影到 屏幕空间(Screen space)中,变换后得到真正的像素位置,而不是虚拟的三维坐标。
屏幕空间:二维空间
必须把顶点 从 裁剪空间 投影到 屏幕空间 中,生成对应的 2D 坐标。
进行 标准齐次除法(透视除法):用 齐次坐标系 的 w分量 去除以 x、y、z分量。
在 OpenGL 中,经过这一步得到的坐标称为 归一化的设备坐标(NDC)。
坐标 从 齐次裁剪坐标空间 转换到 NDC 中。变换到立方体内。
OpenGL:这个立方体 的 x、y、z 分量 的范围 都是 [ -1, 1 ]。
DirectX:z 分量 的范围 是 [0, 1 ]。
经过齐次除法后,透视投影的裁剪空间会变换到一个立方体
经过齐次除法后,正交投影的裁剪空间会变换到一个立方体
齐次除法不会对正交投影产生影响:因为正交投影的裁剪空间实际上已经是一个立方体,而且由于经过正交投影矩阵变换后的顶点的w分量是1,因此齐次除法并不会对顶点的 x、y、z坐标产生影响。
经过齐次除法后,透视投影 和 正交投影 的视锥体 都变换到一个相同的立方体内。
变换到立方体后,就可以根据变换后的 x 和 y 坐标来映射输出窗口的对应像素坐标
在 Unity 中,屏幕空间左下角的像素的坐标是 (0, 0),右上角的像素坐标是 (pixelWidth , pixelHeight)。由于当前 x 和 y 坐标都是 [ -1, 1 ],因此这个映射的过程就是一个缩放的过程。
齐次除法 和 屏幕映射 的过程使用公式表示:
z分量被用于深度缓冲 :
直接存进深度缓冲中。(这个操作不是必须的)驱动生产商会根据硬件去选择最好的存储格式。
在这里完成了主要工作:在齐次除法中作为分母得到NDC
后续还会用于 进行透视校正插值
注意:在Unity中,从裁剪空间到屏幕空间的转换是由底层完成的。顶点着色器只需要把顶点转换到裁剪空间就可以了。
比如:裁剪空间中模型为的位置为 ( 11.691, 15.311, 23.692, 27.31 ),得到在屏幕上的像素位置。
假设,当前屏幕的像素宽度为 400,高度为 300.
1、首先,进行齐次除法,把裁剪空间的坐标投影到NDC中
2、再映射到屏幕空间中
最后得到模型在屏幕上的位置:( 285.617 , 234.096 )