每一个游戏可以呈现炫丽效果的背后, 需要进行一系列的复杂计算, 同时也伴随着各种各样的顶点空间变换. 渲染游戏的过程可以理解成是把一个个顶点经过层层处理最终转化到屏幕上的过程, 本文就旨在说明, 顶点是经过了哪些坐标空间后, 最终被画在了我们的屏幕上.
空间变换的原理
首先, 我们来看一个简单的问题: 当给定一个坐标空间以及其中一点 (a, b, c) 时, 我们是如何知道该点的位置的呢?
从坐标空间的原点开始
向 x 轴方向移动 a 个单位
向 y 轴方向移动 b 个单位
向 z 轴方向移动 c 个单位
坐标空间的变换就蕴含在上面的 4 个步骤中. 现在, 我们已知坐标空间 C 的 3 个坐标轴在坐标空间 P 下的表示 Xc, Yc, Zc, 以及其原点位置 Oc. 当给定坐标空间 C 中的一点 Ac = (a, b, c), 我们同样可以依照上面 4 个步骤来确定其在坐标空间 P 下的位置 Ap
从坐标空间的原点开始, 即 Oc
向 x 轴方向移动 a 个单位, 即 Oc + aXc
向 y 轴方向移动 b 个单位, 即 Oc + aXc + bYc
向 z 轴方向移动 c 个单位, 即 Oc + aXc + bYc + cZc
对得到的表达式做如下变换, 其中 "|" 符号表示按列展开
继续对其中的加法表达式做变换, 即扩展到齐次坐标空间做平移变换
现在, 我们得到了坐标空间 C 到坐标空间 P 的变换矩阵 Mc->p
可以看出 Mc->p 实际上是通过坐标空间 C 在坐标空间 P 中的原点和坐标轴的矢量表示构建出来的: 把 3 个坐标轴依次放入矩阵的前 3 列, 把原点矢量放到最后一列, 再用 0 和 1 填充最后一行即可.
我们可以利用反向思维, 从这个变换矩阵中提取出坐标空间 C 的原点和坐标轴在坐标空间 P 的表示. 例如, 当我们已知从模型空间到世界空间的 4*4 变换矩阵, 我们可以提取出它的第一列, 再进行归一化 (为了消除缩放的影响) 来得到模型空间的 x 轴在世界空间下的单位矢量表示. 同样的方法可以提取 y 轴和 z 轴.
当对方向矢量进行坐标空间变换时, 由于矢量是没有位置的, 因此坐标空间的原点变换是可以忽略的. 那么对方向矢量的坐标空间变换就可以使用 3*3 的矩阵来表示, 即
在 Shader 中, 我们常常看到截取变换矩阵的前 3 行前 3 列来对法线方向, 光照方向进行空间变化, 这正是原因所在.
一旦求出来 Mc->p,Mp->c 就可以通过求逆矩阵的方式求出来, 因为从坐标空间 C 变换到坐标空间 P 与从坐标空间 P 变换到坐标空间 C 是互逆的两个过程. 当 Mc->p 是一个正交矩阵时, Mc->p 的逆矩阵就等于它的转置矩阵, 即
此时, 我们还可以通过 Mc->p 反推出坐标空间 P 的坐标轴在坐标空间 C 中的表示 Xp, Yp, Zp, 这些坐标轴对应的就是 Mc->p 的每一行.
模型空间
模型空间, 是和某个模型或者说是对象有关的, 模型空间也被称为对象空间或局部空间.
每个模型都有自己独立的坐标空间, 当它移动或旋转的时候, 模型空间也会跟着移动和旋转.
Unity 在模型空间中使用的是左手坐标系, 因此在模型空间中,+x 轴,+y 轴,+z 轴分别对应的是模型的右, 上, 前向.
模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的. 当导入到 Unity 中后, 我们可以在顶点着色器中访问到模型的顶点信息, 其中就包含了每个顶点的坐标. 这些坐标都是相对于模型空间中的原点 (通常位于模型的重心) 定义的.
世界空间
世界空间是一个特殊的坐标系, 因为它建立了我们所关心的最大空间, 即整个游戏空间
在 Unity 中, 世界空间同样使用了左手坐标系. 它的 x 轴, y 轴, z 轴是固定不变的.
顶点变换的第一步, 就是将顶点坐标从模型空间转换到世界空间中, 这个变换通常叫做模型变换
在 Unity 中, 我们可以通过 Transform 组件中的值得知模型做了哪些变换. 这个值是根据 Transform 的父节点的模型坐标空间中的原点定义的, 如果这个 Transform 没有任何父节点, 那么这个值就是相对于世界坐标空间定义的.
要将模型空间中的一点转换到其父空间中, 需要获取 M 子 ->父, 这个矩阵可以通过模型的 Transform 值得到. Transform 中包含了旋转, 缩放和平移值, 则 M 子 ->父 = Mtranslation Mrotate Mscale. 而从模型空间转换到世界空间的变换矩阵 M 模型 ->世界可以通过子空间到父空间变换矩阵, 父空间到爷爷空间变换矩阵, 连乘, 直到世界空间为止得到.
观察空间
观察空间也被称为摄像机空间, 可以认为是模型空间的一个特例, 即摄像机的模型空间.
在 Unity 中, 观察空间使用的是右手坐标系, 即 + x 轴指向右方,+y 轴指向上方,+z 轴指向摄像机后方
顶点变换的第二步就是将顶点坐标从世界空间变换到观察空间中. 这个变换通常叫做观察变换.
从观察空间到世界空间的变换矩阵我们同样可以通过 Transform 中的值得到, 再对该矩阵求逆得到从世界空间到观察空间的变换矩阵. 我们还可以使用另一种方法, 对 Transform 组件中的值直接取反(做逆向变换), 然后得到从世界空间到观察空间的变换矩阵. 注意, 由于观察空间使用的是右手坐标系, 因此还需要对变换矩阵的 z 分量进行取反操作.
裁剪空间
顶点接下来要从观察空间转换到裁剪空间中, 这个变换可以被称为投影变换. 这个用于变换的矩阵叫做裁剪矩阵或是投影矩阵
裁剪空间的目的是能够方便地对渲染图元进行裁剪: 完全位于这块空间内部的图元将会被保留, 完全位于这块空间外部的图元将会被剔除, 与这块空间边界相交的图元就会被裁剪. 而这块空间就是由视椎体来决定的.
视椎体有两种类型, 分别对应两种投影类型: 透视投影 (下图左) 和正交投影(下图右). 透视投影模拟了人眼看世界的方式, 而正交投影则完全保留了物体的距离和角度.
投影矩阵虽然叫做投影矩阵, 但并没有真正进行投影, 而是为投影做准备. 目的是对 x,y,z 分量进行缩放, 经过投影矩阵的缩放后, 我们可以直接使用 w 分量作为范围值, 只有 x,y,z 分量都位于这个范围内的顶点才认为是在裁剪空间内. 并且 w 分量在真正的投影时也会用到.
透视投影和正交投影分别对应了不同的投影矩阵. 还需要注意的是投影矩阵会改变空间的旋向性: 空间从右手坐标系变换到了左手坐标系
透视投影
其中 FOV 表示视椎体垂直方向的张开角度, 而 Near 和 Far 分别控制了近裁剪平面和远裁剪平面距离摄像机的远近. 这样我们可以求出近裁剪平面的高度, 如下所示. 远裁剪平面类似.
而根据摄像机的横纵比信息, 我么就可以得到近裁剪平面的宽度
一个顶点和透视投影的投影矩阵相乘后得到的结果如下
视椎体的变化如下所示
此时我们就可以按如下不等式来判断一个变换后的顶点是否位于视椎体内
正交投影
其中 Size 表示视椎体竖直方向上高度的一半, 而 Near 和 Far 同样分别控制了近裁剪平面和远裁剪距离摄像机的远近. 则近裁剪平面的高度如下所示. 远裁剪平面类似.
近裁剪平面的高度同样可以通过摄像机的纵横比得到
一个顶点和正交投影的投影投影矩阵相乘后得到的结果如下
视椎体的变化如下所示
判断一个变换后的顶点是否位于视椎体内使用的不等式和透视投影中的一样, 这种通用性也是为什么要使用投影矩阵的原因之一.
屏幕空间
当完成了所有的裁剪工作后, 就需要进行真正的投影了, 即把视椎体投影到屏幕空间中, 这个过程可以被称为屏幕映射. 经过这一步变换, 我们会得到真正的像素位置, 对应的 2D 坐标, 而不是虚拟的三维坐标. 这个过程可以理解成有两步:
进行标准齐次除法, 也被称为透视除法. 就是把齐次坐标系的 x,y,z 分量都除以 w 分量. 在 OpenGL 中把这一步得到的坐标叫做归一化的设备坐标(NDC). 经过透视投影变换后的裁剪空间会变换到一个立方体内, 而正交投影的裁剪空间本身就是一个立方体(它的 w 分量是 1, 齐次除法不会对它产生影响). 在 Unity 中这个立方体的 x,y,z 分量的范围都是[-1, 1], 和 OpenGL 保持一致.
经过齐次除法后, 透视投影和正交投影的视椎体都变换到一个相同的立方体内. 现在, 我们可以根据变换后的 x,y 坐标来映射输出窗口对应的像素坐标.
在 Unity 中, 屏幕空间左下角的像素是(0, 0), 右上角的像素坐标是(pixelWidth, pixelHeight). 齐次除法和屏幕映射的过程可以使用下面的公式来表示
- \[screen_x = \frac{
- clip_x * pixelWidth
- }{
- 2 * clip_w
- } + \frac{
- pixelWidth
- }{
- 2
- } \]
- \[screen_y = \frac{
- clip_y * pixelHeight
- }{
- 2 * clip_w
- } + \frac{
- pixelHeight
- }{
- 2
- } \]
在 Unity 中, 从裁剪空间到屏幕空间的转换是由底层帮我们完成的. 我们的顶点着色器只需要把顶点转换到裁剪空间即可(模型空间 - 世界空间 - 观察空间 - 裁剪空间, 对应的矩阵通常会串联成一个 MVP 矩阵).
法线变换
最后, 我们再来看一种特殊的变换: 法线变换. 在游戏中, 模型的一个顶点往往会携带额外的信息, 而顶点法线和切线就是其中的两种信息, 切线和法线是互相垂直的.
由于切线是由两个顶点之间的差值计算得到的, 因此我们可以直接使用变换顶点的矩阵 MA->B 来变换切线. 但如果直接使用 MA->B 来变换法线, 得到的新法线可能就不会和切线垂直了. 例如下图所示.
那么应该使用哪个矩阵来变换法线呢? 我们可以通过数学约束条件推出这个矩阵. 由于顶点法线 NA 和切线 TA 垂直, 则 TANA = 0. 给定变换矩阵 MA->B, 我们已知 TB = MA->BTA. 现在我们要找到一个矩阵 G 来变换法线 NA, 使得变换后的法线仍然与切线垂直, 即
通过一些推导可得
由于 TANA = 0, 因此可得
即
这说明使用原变换矩阵的逆转置矩阵来变换法线就可以得到正确的结果.
值得注意的是, 如果矩阵 MA->B 是正交矩阵, 则我们可以直接使用原变换矩阵作为法线的变换矩阵. 如果变换只包含旋转和统一缩放, 我们可以利用统一缩放系数 k 来得到变换矩阵 MA->B 的逆转置矩阵, 这样可以避免计算逆矩阵的过程.
视口空间
视口空间中的坐标被称为视口坐标, 就是把屏幕归一化, 这样屏幕左下角就是(0, 0), 右上角就是(1, 1). 如果已知屏幕坐标的话, 我们只需要把 x,y 分量除以屏幕分辨率即可得到视口坐标. 如果已知裁剪空间中的坐标, 可以通过以下公式得到视口坐标
来源: https://www.cnblogs.com/iwiniwin/p/12547800.html