写在前面的废话
这篇博客算是学习百人计划的一个小小的插曲,在看完入门精要的基础篇后,我想着无论如何要出一篇博客总结一下出现的这么多个坐标空间。虽然坐标空间简直太基础不过了!但我认为还是有总结的必要——它们实在太多太多啦!万一记不住,说不定还可以直接翻一翻曾经写过的这篇博客呢!
坐标空间有:世界空间、模型空间、摄像机空间、齐次裁剪空间、屏幕空间,以及法线映射会用到的切线空间(之前的纹理基础篇就讲到过)。
那为什么会有这么多个坐标空间呢?
一些概念只有在特定的坐标空间下才有意义,才更容易理解。这就是为什么在渲染中我们要使用这么多坐标空间。
——《Unity Shader 入门精要》
客观来讲,所有空间都是平等的,没有优劣之分;但主观来看,特定情况下有些空间确实“更香”。
接下来我将顺着《入门精要》中的叙述方式,基于一个顶点在渲染流水线中经历了哪些坐标变换,来引出一个个坐标空间。
坐标空间转换这一说,其实就是在渲染流水线中,把一个点或者方向矢量从一个坐标空间转换到另一个坐标空间,比如模型空间 -> 裁剪空间,这个过程的实现靠的就是一个变换矩阵。
1 模型空间
模型空间(model space),还可以叫做对象空间(object space)或局部空间(local space),每个模型都有自己的模型空间。
- 会随着模型的旋转/移动而旋转/移动
- 包含前、后、左、右等自然方向概念
Unity中每个游戏对象都可以看到自己的模型空间的3个坐标轴:
当然,在模型空间中描述模型上的某一点位置时坐标会被扩展到齐次坐标系下:(1,0,0)->(1,0,0,1),为顶点变换中的平移变换做准备。
2 世界空间
还是借助《入门精要》的农场游戏,对于这个虚拟的游戏世界而言,整个农场就是最大的空间,这个最大的空间就是世界空间(world space),一般世界空间的原点会放在游戏空间的正中心,同时世界空间的位置就是绝对位置——这个绝对位置你可以理解成Unity里没有父节点(parent)的游戏对象的位置,例如下图中:
- Sphere没有任何父节点,Transform组件中的P/R/S信息都是在世界空间下的绝对位置信息
- Cube由于有Sphere这个父节点,那Cube的Transform组件就是在Sphere模型空间下的位置信息
顶点变换Step1-模型变换
模型变换(model transform),其实都快被讲烂了!它就是MVP变换中的那个M!但为了内容的完整性,我们再来浅浅过一遍。
这里就不再给那些书上都有的纯计算公示了,直接拿上面的Sphere举例子:
还是把Sphere列为Cube的父节点,Sphere看作“妞妞”,并在世界空间中进行的变换信息给Sphere赋相同的组件信息:
Cube当作“妞妞的鼻子”,并给上对应的鼻子在“妞妞”的模型空间中的位置信息:(0,2,4,1)
行,模型变换要做啥?——把Cube从模型空间转换到世界空间,在Unity中我们直接给Cube拖出来就行,脱离了Sphere后,Cube的信息如下:
我们发现,Cube位置从(0,2,4,1) --> (9,4,18.071),这与书中的矩阵计算结果一致。
3 观察空间
观察空间(view space)也叫做摄像机空间/照相机空间(camera space),摄像机本身不可见,但是一般会给他一个图标并在场景窗口中可视化出来,例如下图:
一些值得注意的点:
- Unity中观察空间用的右手系,即摄像机的正前方指向的是-z轴方向
- 注意区分观察空间和屏幕空间,前者是三维的,后者是二维、平面的,观察空间 -> 屏幕空间需要经过投影操作
顶点变换Step2-观察变换
观察变换(view transform)——MVP变换中的V!还是顺着上面的Cube和Sphere,已知当前摄像机的变换信息(取书中的摄像机信息):
观察变换要做啥?——把世界空间的Cube变换到观察空间下
怎么做?——计算变换矩阵的一种方法是把观察空间当作一个模型空间变换到世界空间(用模型变换的方法),然后取变换矩阵的逆即可;另一种更直观:直接把观察空间原点挪到世界空间,并且让三轴均对齐就行!
如果是在Unity里,直接把Cube从世界空间拖到Main Camera下,这时Cube的Transform组件不就是在观察空间下的信息了吗?
果然,此时的位置为(9,8.839,27.310),但是组件位置信息是左手系下的,因此正确描述在观察空间下的Cube位置应该为(9,8.839,-27.310)。
4 裁剪空间
经历了模型空间->世界空间->观察空间,这一步是观察空间->裁剪空间(clip space,也被叫做齐次裁剪空间),这里就到了之前图形渲染管线-几何阶段涉及的裁剪(Clipping)环节了,这一阶段里我们无法操控,完全由GPU去做。
哦对了!顶点变换Step3-投影变换(MVP中的P!)就是在观察空间->裁剪空间这步进行的。
4.1 视锥体
看到这里,不禁发问:难道变换的最终目的不是映射在屏幕上吗?为什么不直接观察空间->屏幕空间,而是要有一步裁剪呢?
浅复习一下裁剪的目的:剔除在视野范围外的图元,而由视锥体(view frustum)决定的裁剪空间为这一剔除过程提供了便利。那很显然——场景越大,裁剪的优越性更加突出,可以节省很多后续渲染不必要的开销。
上面提到的视锥体,是空间中的一块儿区域,决定着摄像机可见空间。它由六个面组成,被称为裁剪平面(clip planes)。
4.1.1 透视投影
根据投影类型分,一个是正交投影(orthographic projection)一个是透视投影(perspective projection)。透视投影(真实感的3D游戏中常用)很好的模拟了人眼看世界的方式,而正交投影(简单的2D小游戏)完全保留了物体的距离和角度。
下图是透视投影,Unity中打开Gizmos可以将视锥体可视化在Scene视图中:
相关参数:
FOV——视角度数,同时FOV Axis决定这个视角是横还是纵向的
Near——近裁剪平面距离,Far——远裁剪平面距离
Viewport Rect的X/Y/W/H->(0,0)和(1,1)组成一个Rect
Aspect——摄像机的纵横比,由W和H和Game视图的纵横比共同决定,W和H在[0,1]范围内时,Aspect=(W * 窗口宽度) / (H * 窗口高度)
4.1.2 正交投影
下图是正交投影的视锥体:
相关参数:
4.2 投影矩阵的目的
观察空间->裁剪空间的坐标变换矩阵有了更加准确的称呼——裁剪矩阵(clip matrx),也被叫做投影矩阵(projection matrix)。
前面已经讨论过了裁剪的必要性——渲染前剔除在视野范围外的图元;这里需要讨论的是:为什么不直接在视锥体裁剪,而是要先变换到裁剪空间再进行裁剪?
这个问题《入门精要》做了很清楚的解释:“直接在视锥体定义的空间进行裁剪,对于透视投影的视锥体想要判断一个顶点是否在这个空间内是十分麻烦的,我们需要一种更加通用、整洁方便的方式进行裁剪,因此就需要一个投影矩阵将顶点转换到一个裁剪空间中。”
4.2.1 为真正的投影做准备
真正的投影可以理解成空间的降维,四维->三维、三维->二维平面。真正的投影发生在屏幕映射过程中齐次除法后获得二维坐标这一步,而投影矩阵只是进行了坐标空间转换,它并没有实实在在实现投影这个操作。
4.2.2 对x、y、z进行缩放
经过投影矩阵的缩放后,齐次坐标的w(点的w是1,方向矢量是0)被赋予了意义,它可以作为一个范围值来判断点是否在裁剪空间内。至于上面提到的正交投影和透视投影矩阵具体是如何计算的,在之前101的学习和《入门精要》书中都有涉及,这里我就不再贴计算公式了。
5 屏幕空间
完成了裁剪工作,下一步就是进行真正的投影了,将视锥体投影到屏幕空间(screen space)中,这一步会得到真正的像素位置,而不是虚拟的三维坐标。这一步可以理解为做了以下两步:
5.1 齐次除法
首先进行标准齐次除法(homogeneous division),也被称为透视除法(perspective division),其实就是x、y、z分别除以w,经过齐次除法后的裁剪空间会变成一个单位立方体,这个立方体空间里的坐标叫做归一化的设备坐标(也就是之前提到的NDC)。因此,也可以说齐次除法是做了空间裁剪坐标到NDC坐标的转换操作。
细心一点会发现,齐次坐标对于透视投影空的裁剪空间变化更大,而对正交投影的裁剪空间没有影响(正交投影的裁剪空间中顶点的w已经是1了) 。
5.2 屏幕映射(渲染管线中几何阶段的一步)
这里就顺利的跟之前的渲染管线GPU负责的几何阶段部分联系在一起了。在获得了NDC立方体后,接下来就是根据变换后的x、y坐标映射输出窗口对应的像素坐标,本质就是个缩放的过程。
需要注意:
- z分量——并没有被抛弃,会被储存起来(深度缓存或者其他的储存格式)
分量——后续还可以用来进行重要的操作(例如做透视矫正插值等)
6 总结
上面就是一个顶点是怎么由模型空间转换到屏幕空间的过程,相当于从坐标空间的角度再过了一遍渲染流水线中的几何阶段。
记忆流程图
下图是《入门精要》最后总结的渲染流水线中顶点的空间变换过程,和各个坐标空间的旋向性的流程图。