我曾经在Shader山下(十六)坐标空间与转换矩阵中介绍过,一个物体要显示在平面上,需要经过四步空间变换(实际上是五步):
物体空间->世界空间->观察空间->裁剪空间(->归一化设备空间)->屏幕空间
(差不多就是这个意思)
实际上,在可编程渲染管线中,我们往往只需要操作从物体空间到裁剪空间的转换,可以用一个矩阵链乘M(odel)V(iew)P(rojection)表示。本文就介绍一下这三个矩阵。
索性全介绍了吧。
先简单介绍一下变换矩阵。
常用的变换矩阵有三种:平移矩阵、旋转矩阵和缩放矩阵。
平移矩阵:
如果一个物体在世界坐标系中的位置为
(px,py,pz)
,那么它的平移矩阵为:
T(p)=⎡⎣⎢⎢⎢⎢100001000010pxpypz1⎤⎦⎥⎥⎥⎥
旋转矩阵:
绕x轴旋转
θx
,旋转矩阵为:
Rx(θx)=⎡⎣⎢⎢⎢10000cosθxsinθx00−sinθxcosθx00001⎤⎦⎥⎥⎥
绕y轴旋转
θy
,旋转矩阵为:
Ry(θy)=⎡⎣⎢⎢⎢⎢cosθy0−sinθy00100sinθy0cosθy00001⎤⎦⎥⎥⎥⎥
绕z轴旋转
θz
,旋转矩阵为:
Rz(θz)=⎡⎣⎢⎢⎢cosθzsinθz00−sinθzcosθz0000100001⎤⎦⎥⎥⎥
缩放矩阵:
一个向量沿x、y、z轴分别缩放
(qx,qy,qz)
,那么缩放矩阵为:
S(q)=⎡⎣⎢⎢⎢⎢qx0000qy0000qz00001⎤⎦⎥⎥⎥⎥
组合成变换矩阵
只需要将这些矩阵链乘即可。但是需要注意顺序。这里按照T(ranslate)R(otate)S(cale)和H(eading)P(itch)B(ank)的顺序进行链乘。
Q=T(p)Ry(θy)Rx(θx)Rz(θz)S(q)
等等!为什么是4*4的矩阵?参见百度百科齐次坐标
简单来讲,3*3的矩阵只能表示旋转和缩放矩阵,为了表示平移,便扩充为4*4的矩阵。
当然点或向量要与矩阵运算时也要扩充为4维(x,y,z,w)。其中w=1表示点,w=0表示向量。因为向量只有方向没有位置,所以w=0便不会与矩阵第4行进行运算。
模型变换矩阵
这个本身没什么,其实就是相对位置(模型空间)到绝对位置(世界坐标)的一个变换。
已知模型的位置、旋转角度和缩放值,带入之前的公式,便可以得到相应的模型变换矩阵。
当要计算模型内部某个点的世界坐标时,只需要将这个点与变换矩阵相乘即可。
观察矩阵
摄像机的方位可以由四个向量表示,
p
表示位置,
d
表示观察方向,
u
表示上方向,
r
表示右方向。
在空间变换时,我们希望将摄像机移动到世界坐标原点,并且观察方向与z轴重合,上方向与y轴重合,右方向与x轴重合。当然世界中的物体也要随之变换。
2D平面演示:
设V为观察矩阵,那么就有:
Vp=(0,0,0)
Vr=(1,0,0)
Vu=(0,1,0)
Vd=(0,0,1)
因为摄像机没有大小,只有位置和方位,那么就表示,我们通过平移和旋转来完成这个变换。
首先是平移矩阵
没什么好说的:
T=⎡⎣⎢⎢⎢⎢100001000010−px−py−pz1⎤⎦⎥⎥⎥⎥
旋转
我们只考虑矩阵的前三行和前三列即可,设为A:
Ar=⎡⎣⎢a0a3a6a1a4a7a2a5a8⎤⎦⎥⎡⎣⎢rxryrz⎤⎦⎥=⎡⎣⎢100⎤⎦⎥
Au=⎡⎣⎢a0a3a6a1a4a7a2a5a8⎤⎦⎥⎡⎣⎢uxuyuz⎤⎦⎥=⎡⎣⎢010⎤⎦⎥
Ad=⎡⎣⎢a0a3a6a1a4a7a2a5a8⎤⎦⎥⎡⎣⎢dxdydz⎤⎦⎥=⎡⎣⎢001⎤⎦⎥
联立可得:
AB=⎡⎣⎢a0a3a6a1a4a7a2a5a8⎤⎦⎥⎡⎣⎢rxryrzuxuyuzdxdydz⎤⎦⎥=⎡⎣⎢100010001⎤⎦⎥
那么可以看出
A
是
B
的逆矩阵,由于
B
是标准正交矩阵(
r⊥u⊥d
,并为单位向量),所以其逆矩阵与其转置矩阵相等。即:
A=B−1=BT=⎡⎣⎢rxuxdxryuydyrzuzdz⎤⎦⎥
最后整合一下:
V=TA=⎡⎣⎢⎢⎢⎢rxuxdx0ryuydy0rzuzdz0−p⋅r−p⋅u−p⋅d1⎤⎦⎥⎥⎥⎥
投影矩阵
这个要比前两个要复杂一些。先说一下这个变换时做什么的。我们的屏幕实际上是一个2D平面,我们要显示3D物体,就要将3D坐标系投影到2D平面上。如果是正交摄像机,那么相对简单一点,使用降维打击便可以解决问题,但是如果是透视摄像机,那么就要考虑到透视问题,也就是近大远小。
著名的“鸽子为什么这么大”问题:
贴吧链接
透视投影
这里我们就要引入视锥体的概念,之前土圭垚㙓数学课(二)视锥体八个顶点的计算方法简单提到过这个视锥体。我们继续引用文章中的图片:
我们有下面这个等式:
⎡⎣⎢⎢⎢⎢xclipyclipzclipwclip⎤⎦⎥⎥⎥⎥=Mprojection⎡⎣⎢⎢⎢⎢xeyeyeyezeyeweye⎤⎦⎥⎥⎥⎥
投影变换的任务就是要把物体根据近大远小的规则转换到一个长方体。
然后转换到设备归一化(NDC)空间中:
⎡⎣⎢xndcyndczndc⎤⎦⎥=⎡⎣⎢⎢xclip/wclipyclip/wclipzclip/wclip⎤⎦⎥⎥
这是一个立方体空间:
从观察空间到NDC空间,会将x轴的范围从[l,r]压缩到[-1,1],y轴的范围从[b,t]压缩到[-1,1],z轴的范围从[n,f]压缩到[-1,1]。所以可以认为裁剪空间是从观察空间到NDC空间的一个中间状态(除此之外,会在这个空间内做裁剪操作)。
首先考虑如何将一个点映射到视锥体近平面上:
顶视图和右视图
从两张图,我们就可以得到计算
xp
和
yp
的公式:
xpxe=−nze
xp=−n⋅xeze=n⋅xe−ze
ypye=−nze
yp=−n⋅yeze=n⋅ye−ze
因为视图坐标系是右手坐标系,而裁剪空间是左手坐标系,所以才产生了这个负号。
因为在裁剪空间到NDC空间里会把向量的x,y,z分别处以w分量,所以投影矩阵的第四行为(0,0,-1,0)。
⎡⎣⎢⎢⎢xcyczcwc⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢...0...0...−1...0⎤⎦⎥⎥⎥⎡⎣⎢⎢⎢xeyezewe⎤⎦⎥⎥⎥
即:
wclip=−zclip
接着,我们利用线性关系将
xp
和
yp
映射给NDC空间的
xn
和
yn
。
xn=2xpr−l−r+lr−l
yn=2ypt−b−t+bt−b
带入
xp
和
yp
得到:
xn=(2nr−l⋅xe+r+lr−l⋅ze)/−ze
yn=(2nt−b⋅ye+t+bt−b⋅ze)/−ze
即:
xc=(2nr−l⋅xe+r+lr−l⋅ze)
yc=(2nt−b⋅ye+t+bt−b⋅ze)
于是我们得到矩阵:
⎡⎣⎢⎢⎢xcyczcwc⎤⎦⎥⎥⎥=⎡⎣⎢⎢⎢⎢⎢⎢⎢2nr−l0.002nt−b.0r+lr−lt+bt−b.−100.0⎤⎦⎥⎥⎥⎥⎥⎥⎥⎡⎣⎢⎢⎢xeyezewe⎤⎦⎥⎥⎥
虽然投影后,z值貌似没有意义,但是为了裁剪和深度测试,我们需要归一化z值,另外这样做也方便我们逆向转换。因为z值不依赖于x和y,所以我们可以得出下面的方程式:
zn=zc/wc=Aze+Bwe−ze
因为在观察空间中,
we
等于1,所以:
zn=Aze+B−ze
带入近平面和远平面,可得:
⎧⎩⎨−An+bn=−1−Af+bf=1→{−An+B=−n−Af+B=f→⎧⎩⎨A=−f+nf−nB=−2fnf−n
最终我们得到投影矩阵:
Mprojection=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢2nr−l00002nt−b00r+lr−lt+bt−b−f+nf−n−100−2fnf−n0⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥
因为视锥体是上下左右对称的,也就是说r=-l且t=-b,借此,我们可以整理一下投影矩阵:
Mprojection=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢nr0000nt0000−f+nf−n−100−2fnf−n0⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥
正交投影
因为没有近大远小的效果,所以
xc
和
yc
的值也就与近切面无关,而且正交投影的观察空间本身就是长方体,所以在裁剪空间里,我们可以直接将空间压缩到[-1,1]的立方体内,也就不用再对w分量进行计算。于是我们就可以比较容易的推导出投影矩阵:
Morthographicprojection=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢1r00001t0000−2f−n000−f+nf−n1⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥
视口矩阵
也就是从NDC空间到屏幕空间的变换,有些时候我们可以给屏幕做一个偏移量(同屏显示)
(xoffset,yoffset)
,并设置宽度
Width
和高度
Height
,z轴还是从近平面
n
到远平面
f
。那么,x轴
[−1,1]→[xoffset,xoffset+Width]
,y轴
[−1,1]→[yoffset,yoffset+Height]
,z轴
[−1,1]→[n,f]
。
易得:
Mviewport=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢Width20000Height20000f−n20xoffset+Width2yoffset+Height2f+n21⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥
注:
wndc=1
参考文献:
- DirectX 9.0 3D游戏开发编程基础
- 3D数学基础:图形与游戏开发
- OpenGL Transformation
PS:写了这么多累死了,早知道拆成两三篇写了。第一次用markdown编辑器,真TM好用,早就该用这个了。