由于GAMES101与LearnOpenGL课程中,对透视投影变换、视口变换、法线变换等介绍相对简单,因此本文通过总结大量资料,手推了相关公式,可以说这是网上能看到的最详实、严谨的推导过程之一(谦虚)。
关于一个模型投影到屏幕上所要经历过程如下:
一、透视投影过程推导
1.1 正交投影矩阵推导
正交投影过程主要为先将中心点平移到原点,再缩放至
cube[−1,1]3
正交投影本身使用相对较少,这里推导的目的为后续透视投影变换做准备。
1.1.1 纯右手坐标系的推导过程
说明:
- 这里统一都是用右手坐标系,即z轴始终朝外。
- 这里n、f都在z轴负半轴上的坐标值,都是负数。
- 为了行文方便下面统一称这种为
标准右手坐标系
,GAMES101即是在这种情况下推导的。
其中,平移变换矩阵为:
T=⎣⎢⎢⎢⎡100001000010−2r+l−2t+b−2f+n1⎦⎥⎥⎥⎤(平移变换矩阵)
缩放变换矩阵(注意这里边长为2)为:
S=⎣⎢⎢⎢⎡r−l20000t−b20000n−f200001⎦⎥⎥⎥⎤(缩放变换矩阵)
因此,正交投影矩阵
Mortho=S∗T:
Mortho=⎣⎢⎢⎢⎡r−l20000t−b20000n−f20−r−lr+l−t−bt+b−n−ff+n1⎦⎥⎥⎥⎤(标准右手正交投影矩阵)
如果视椎体是上下、左右对称(r=-l, t=-b, r+l=0, t+b=0)的,则
Mortho 矩阵可以简化为:
Mortho=⎣⎢⎢⎢⎡r10000t10000n−f2000−n−ff+n1⎦⎥⎥⎥⎤(简化版右手正交投影矩阵)
1.1.2 OpenGL中的正交投影变换推导
在OpenGL中需要特别注意:
- 观察空间坐标系为右手系,而NDC中+z轴向内,为左手系。 因此在做缩放时不再是将n映射到1,f映射到-1而是[f,n]映射到NDC中的[1,-1]
- 在OpenGL中n、f分别定义为
近平面
和远平面
的距离值,都是正数,因此这里代入矩阵前需要加负号以表示正确的坐标值。
- 为了行文方便下面统一称这种情况为
OpenGL坐标系
。
此时平移变换矩阵变为:
T=⎣⎢⎢⎢⎡100001000010−2r+l−2t+b2f+n1⎦⎥⎥⎥⎤(OpenGL平移变换矩阵)
缩放变换矩阵为:
注意这里f、n有一个翻转,即由
(−n)−(−f)2 翻转为
−(−n)−(−f)2,最终结果如下所示:
S=⎣⎢⎢⎢⎡r−l20000t−b20000n−f200001⎦⎥⎥⎥⎤(OpenGL缩放变换矩阵)
这里亦可以用更数学的形式理解为z轴做了一次翻转,相当于变换过程中左乘了一个翻转矩阵
⎣⎢⎢⎢⎡1000010000−100001⎦⎥⎥⎥⎤
因此,OpenGL正交投影矩阵
Mortho=S∗T:
Mortho=⎣⎢⎢⎢⎡r−l20000t−b20000n−f20−r−lr+l−t−bt+bn−ff+n1⎦⎥⎥⎥⎤(OpenGL正交投影矩阵)
同样可推出简化版OpenGL正交投影坐标系
Mortho=⎣⎢⎢⎢⎡r10000t10000n−f2000n−ff+n1⎦⎥⎥⎥⎤(简化OpenGL正交投影矩阵)
1.2 透视投影矩阵推导
对于透视投影,分成两步操作:
-
首先,“压扁”视锥体成一个长方体(n->n,f->f)(
Mpersp−>ortho);
注意:这里是在右手坐标系下进行推导,不再考虑坐标变换问题,但根据n、f的正负情况仍然有两种推导情况。
-
然后,利用上面的正交投影矩阵得到最终结果。
这里主要以n、f为正数为主,用-n、-f代入坐标的情况进行推导,过程如下:
在把视锥体压缩成长方体的过程中,我们规定三个原则:
-
近平面的所有点坐标不变
-
远平面的所有点坐标z值不变 都是-f
-
远平面的中心点坐标值不变 为(0,0,-f)
下图表示观察空间中的点
(xe,ye,ze) 是投影到近平面
(xp,yp,zp) 上的情况,
从视锥体的顶视图(Top View of Frustum)看,观察空间的
xe映射到
xp,利用相似三角形的比值计算可得:
xexp=ze−n,xp=−zen⋅xe
同理通过侧视图(Side View of Frustum)可以得到:
yeyp=ze−n,yp=−zen⋅ye
故对于视椎体中任一点
(xe,ye,ze,1),它在视椎体压缩成长方体之后的坐标应该是
(−zen∗xe,−zen∗ye,unknown,1),即我们需要一个矩阵
Mpersp−>ortho使得下面公式成立:
⎣⎢⎢⎢⎡−zen∗xe−zen∗yeunknown1⎦⎥⎥⎥⎤=Mpersp−>ortho∗⎣⎢⎢⎢⎡xeyeze1⎦⎥⎥⎥⎤
假设
Mpersp−>ortho矩阵的第一行为A,B,C,D。可以得到等式:
Axe+Bye+Cze+D=−zen∗xe ,这个等式并不容易求解,如果让A =
−zen,其他等于0,的确可以得到结果,但是矩阵的值应该是常数,而
−zen是个变量。
同理对于矩阵的第二行求
ye时也会遇到如上问题。
因此,这里需要利用其次坐标的性质来进行求解。已知
(x,y,z,1)与(kx,ky,kz,k!=0)这两个点是完全等价的关系,所以这里将上面等式左边统一乘以
−ze可改写为如下形式:
⎣⎢⎢⎢⎡nxenyeunknown−ze⎦⎥⎥⎥⎤=Mpersp−>ortho∗⎣⎢⎢⎢⎡xeyeze1⎦⎥⎥⎥⎤
此时可以很容易推出:
Mpersp−>ortho矩阵的第一行:Ax+By+Cz+D = nx,求出 A=n,B=C=D=0
第二行:Ex+Fy+Gz+H = ny,求出F=n,E=G=H=0
第四行:Mx+Ny+Oz+P = z,求出O=-1,M=N=P=0
至此,仅第三行元素还是未知状态, 此时再回过头来思考上面所规定的三个原则:
-
第一个原则:近平面的所有点坐标不变
也就是点
(xe,ye,−n,1)通过矩阵
Mpersp−>ortho变换后,应该还是等于
(xe,ye,−n,1)。代入等式可得:
⎣⎢⎢⎢⎡xeye−n1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡n0?00n?000?−100?0⎦⎥⎥⎥⎤∗⎣⎢⎢⎢⎡xeye−n1⎦⎥⎥⎥⎤
对于第一、二、四行,我们写出推导等式:
nx+0y+0n+0*1=x
0x+ny+0n+0*1=y
0x+0y+n+0*1=1
这里,n应该是任意常数,但是现在只有在n等于1时,一、二、四行的运算才成立,这并不是一个合理的解。
因此这里再次利用齐次坐标系
的性质将等式左边再乘以n,得到:
⎣⎢⎢⎢⎡nxenye−n2n⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡n0?00n?000?−100?0⎦⎥⎥⎥⎤∗⎣⎢⎢⎢⎡xeye−n1⎦⎥⎥⎥⎤
此时再对第一、二、四行,写出推导等式:
nx+0y+0n+0*1=nx,
0x+ny+0n+0*1=ny,
0x+0y+n+0*1=n
发现上面三个等式左右两边相等,且对n没有任何限制,此时设第三行的四个数分别为A、B、C、D可得:Ax+By-Cn+D = -n^2,
不难发现等式右边的x,y与结果没有任何关系,因此可求出:A=0, B=0, 且:
C∗n−D=n2(1)
- 第三个原则:远平面的中心点坐标值不变 为(0,0,-f)
同样为了保证之前求的矩阵一、二、四行成立,这里亦需要利用齐次坐标系
的性质把(0,0,-f,1)写成(0,0,-f*f,f),得到如下等式:
⎣⎢⎢⎢⎡00−f2f⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡n0000n0000C−100D0⎦⎥⎥⎥⎤∗⎣⎢⎢⎢⎡00−f1⎦⎥⎥⎥⎤
C∗f−D=f2(2)
联合(1) (2)可以很容易求得
C = n + f
D = nf
至此,我们终于顺利推导出
Mpersp−>ortho矩阵:
Mpersp−>ortho=⎣⎢⎢⎢⎡n0000n0000n+f−100nf0⎦⎥⎥⎥⎤(n,f为正值情况)
注意:若上面一开始设定n、f为负值则:
xp=zen⋅xe,yp=zen⋅ye,后面的推导与上述过程完全一致,最终结果为:
Mpersp−>ortho=⎣⎢⎢⎢⎡n0000n0000n+f100−nf0⎦⎥⎥⎥⎤(n,f为负值情况)
此时即可得到透视投影变换矩阵的最终形式
Mpersp=Mortho∗Mpersp−>ortho
Mpersp=⎣⎢⎢⎢⎡r−l20000t−b20000n−f20−r−lr+l−t−bt+bn−ff+n1⎦⎥⎥⎥⎤∗⎣⎢⎢⎢⎡n0000n0000n+f−100nf0⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡r−l2n0000t−b2n00r−lr+lt−bt+b−f−nf+n−100−f−n2fn0⎦⎥⎥⎥⎤(OpenGL透视投影矩阵)
Mpersp=⎣⎢⎢⎢⎡rn0000tn0000−f−nf+n−100−f−n2fn0⎦⎥⎥⎥⎤(简化OpenGL透视投影矩阵)
这里通过查阅GLM
库中glm::perspective
发现,源码与我们手推的结果完全一致。
Result[0][0] = (static_cast<T>(2) * nearVal) / (right - left);
Result[1][1] = (static_cast<T>(2) * nearVal) / (top - bottom);
Result[2][0] = (right + left) / (right - left);
Result[2][1] = (top + bottom) / (top - bottom);
Result[2][2] = -(farVal + nearVal) / (farVal - nearVal);
Result[2][3] = static_cast<T>(-1);
Result[3][2] = -(static_cast<T>(2) * farVal * nearVal) / (farVal - nearVal);
1.3 透视除法与NDC空间
符号术语说明:
-
xc,yc,zc,wc 表示剪裁空间(clip space)坐标
-
xe,ye,ze,we 表示观察空间(eye\view space)坐标
-
xn,yn,zn 表示NDC空间坐标
裁剪坐标系与观察坐标系的关系:
⎣⎢⎢⎢⎡xcyczcwc⎦⎥⎥⎥⎤=Mpersp∗⎣⎢⎢⎢⎡xeyezewe⎦⎥⎥⎥⎤
根据齐次坐标的定义我们知道 (x,y,z,w) 与 (x/w,y/w,z/w,1) 是等价的,即把裁剪空间下的(x,y,z,w)分别除以w,这一步我们称之为透视除法(Perspective Divide)
。
⎣⎢⎡xnynzn⎦⎥⎤=⎣⎢⎡xc/wcyc/wczc/wc⎦⎥⎤
由前文推导可知,透视投影变换会把视椎体压缩成一个标准立方体,那么视椎体内的顶点 (x/w,y/w,z/w,1),其取值范围即为:
⎩⎪⎪⎨⎪⎪⎧−1<x/w<1=>−w<x<w,−1<y/w<1=>−w<y<w,−1<z/w<1=>−w<z<w
因此在裁剪空间中任意顶点(x,y,z,w)如果不满足 (-w<x<w && -w<y<w && -w<z<w) 的条件即说明不在视椎体内,需要被裁剪。
裁剪后剩下的顶点,我们做一次透视除法,即可把它们从裁剪空间变换到NDC空间。
1.4 Zfighting问题
由透视投影矩阵以及1.3节中的坐标变换关系可知:
zn=zc/wcwc=−zezc=−f−nf+nze−f−n2fnzn=zc/wc=−ze−f−nf+nze−f−n2fn=f−nf+n+f−n2fn/ze
从上面
zn公式可画出下图坐标映射关系图,可以看到,在近平面附近值变化比较大,精确度较好;而在远平面附近,有一段距离内,近乎持平,精确度不好。
当增大远近裁剪平面的范围后,如下右图所示,我们可以看到在远平面附近,
ze坐标值投影到
zn坐标后值几乎相同,精确度低的现象更为明显,这种深度的精确度引起的问题称之为zFighting
。
因此要尽量减小[-n,-f]的范围,以减轻zFighting问题。
1.5 使用FOV推导透视投影
另外一种经常使用 的方式是通过视角(Fov),宽高比(Aspect)来指定透视投影。
其中指定fovy指定视角,aspect指定宽高比,zNear和zFar指定剪裁平面。fovy的理解如下图所示:
这些参数指定的是一个对称的视见体,如下图所示:
由这些参数,代入上面推导出的OpenGL透视投影矩阵
(简化版)可以得到:
t=n∗tan(fov/2)tr=hw=aspect=>r=aspect∗t=aspect∗n∗tan(fov/2)
代入上述透视变换矩阵可得Fov透视投影矩阵
为:
P=⎣⎢⎢⎢⎢⎡aspectcot(2θ)0000cot(2θ)0000f−n−(f+n)−100f−n−2fn0⎦⎥⎥⎥⎥⎤(Fov透视投影矩阵)
二、视口变换过程推导
视口变换
是将NDC坐标转换为显示屏幕坐标的过程,如下图所示:
⎩⎪⎪⎨⎪⎪⎧−1≤x≤1−1≤y≤10≤z≤1
说明:
-
xn,yn,zn 表示NDC空间坐标值
-
xs,ys,zs 表示屏幕空间坐标值
-
ns,fs 表示屏幕空间近平面、远平面的位置
-
Sx,Sy 表示屏幕的起始像素点
-
Ws,Hs 表示屏幕的像素宽、高
2xn−(−1)=Wsxs−Sx=>xs=2Wsxn+2Ws+Sx2yn−(−1)=Hsys−Sy=>ys=2Hsyn+2Hs+Sy2zn−(−1)=fs−nszs−ns=>zs=2fs−ns+2ns+fs
则由上述式子得到视口变换矩阵为:
viewPort=⎣⎢⎢⎢⎡2Ws00002Hs00002fs−ns0Sx+2WsSy+2Hs2ns+fs1⎦⎥⎥⎥⎤(视口变换矩阵)
在OpenGL中,一般情况下
Sx=0,Sy=0,ns=0,fs=1,因此viewPort可简化为:
viewPort=⎣⎢⎢⎢⎡2Ws00002Hs00002102Ws2Hs211⎦⎥⎥⎥⎤(简化视口变换矩阵)
三、线性与非线性深度之间的变换关系
3.1 从NDC空间深度值反推观察空间下的线性深度
已知在1.4节中已经推导出
zn与ze之间的变换关系:
zn=zc/wc=−ze−f−nf+nze−f−n2fn=f−nf+n+(f−n)ze2fn
因此很容易可反推出:
ze=f+n−zn(f−n)−2fn
这里
ze表示观察空间(右手坐标系)下的坐标值,是负数;
而深度这个概念是针对屏幕空间(左手坐标系)来定义的,这里对
ze取反,可从NDC空间反推出观察空间下的线性深度
:
linearDepth=−ze=zn(f−n)−(f+n)2nf
3.2 从观察空间的线性深度值推导屏幕空间下的非线性深度值
根据5.2 中推出的视口变换矩阵可以很容易得到:
zs=0.5zn+0.5zn=zc/wc
代入可得:
\begin{equation*} \begin{aligned} z_s &= \frac{1}{2}(z_n + 1) \\ &= \frac{1}{2}(\frac{f+n}{f-n} + \frac{2fn}{z_e(f-n)} + 1)\\ &= \frac{f - \frac{nf}{z_e}}{f - n} \\ &= \frac{\frac{1}{-z_e} - \frac{1}{n}}{\frac{1}{f} - \frac{1}{n}} \end{aligned} \end{equation*}
同上文,这里
ze表示观察空间(右手坐标系)下的坐标值
但在深度测试文中,定义的 z是代表了近、远平面之间的距离值,它们都是正数,因此这里也要对
ze取反。
最终可以从观察空间推导出屏幕空间的非线性深度值
为:
Fdepth=zs=f1−n1z1−n1
3.3 通过深度信息还原位置和法线信息
参考资料:
https://zhuanlan.zhihu.com/p/367257314
https://mynameismjp.wordpress.com/2010/09/05/position-from-depth-3
以上两篇资料推理过程比较详尽,这里不再赘述。
四、法线变换说明
参考资料:https://blog.csdn.net/u012419410/article/details/42174839
由上面两图可知,当模型存在缩放变换时,法线向量将会被破坏,因此不能简单使用
Mview∗Mmodel矩阵来变换法线向量。
法线变换矩阵推导过程如下:
假设Model space中的某条切线向量是T,法线向量是N。
那么由他们是垂直的可得到:
TTN=0
假设他们变换到Eye space中后分别是
T′和
N′。那么他们应该仍然是相互垂直的:
T’TN’=0
假设切线向量T和法线向量N的变换矩阵为M、G。则有:
(MT)T(GN)=0
进一步推出:
TTMTGN=0
由于
TTN=0,因此我们猜想
MTG=I
最终求得:
G=(M−1)T
即:应用于法线向量的变换矩阵是顶点变换矩阵的逆转置矩阵。
评论(0)