深入理解OpenGL之投影矩阵推导
深入理解OpenGL之投影矩阵推导
OpenGL流水线中的投影矩阵以及坐标变换
OpenGL中,投影矩阵在Vertex shader中使用,用于变换顶点。一般和Model, View矩阵结合成MVP矩阵后使用。Vertex shader的输出gl_Position是一个处于Clip Space中的齐次坐标。之所以叫做Clip Space,是因为OpenGL会在此空间中对图元进行裁剪(所谓图元就是三角形,线,点)。再这之后,进行透视除法,将通过clip的顶点从clip space的齐次坐标变换成一个3D坐标,这个坐标被称为归一化设备坐标(NDC: normalized device coordinates)。之所以叫归一化,因为这个坐标系的范围对于x,y,z都是从-1到+1,另外这个坐标系形成的几何体被称为规则观察体(CVV: canonical view volume)。再这之后,进行viewport transform将3D的NDC坐标转换成2D的屏幕坐标。
投影矩阵的作用,投影究竟是什么操作?
之所以在上面把经过投影之后的坐标的变换复习一遍,是因为我们需要从最终的目标出发理解投影矩阵的作用。因为如果仅仅从投影这个名词出发,是不能理解为何要变换到Clip Space再变换到NDC,然后最终变换到屏幕坐标。因为毕竟对于透视投影,将x,y坐标除以z就是从3D投影到2D,z越大,x和y越小,近大远小的效果就有了;而对于平行投影,直接将z值舍弃就完成了3D到2D的转换。OpenGL搞了那么多事情,都是为了最终能正确高效的进行渲染。
首先,在观察空间中,图元有可能在视景体内部也可能在外部,对于完全在外部的图元,没有必要进行渲染,所以需要丢弃这些图元,而完全在内部的图元需要保留;部分在内部的图元则需要进行剪裁,对于三角形,需要找出边和视景体边界的交点,将视景体内的部分生成一个或多个新的三角形图元,而在视景体外的顶点进行抛弃。但是直接在观察空间进行裁剪计算起来很麻烦,因为视景体形状和范围各不相同,需要比较复杂的计算才能完成裁剪。因此OpenGL将观察空间变换到规则观察体CVV,这样所有的坐标范围都是-1到+1,就比较容易计算了。需要指出的是,实际进行剪裁不是在CVV中,而是在裁剪空间(Clip Space)中。CVV中的NDC坐标范围是-1到+1,而Clip space中的x,y,z坐标满足−Wc<=Xc<=Wc-W_c<=X_c<=W_c−Wc<=Xc<=Wc, −Wc<=Yc<=Wc-W_c<=Y_c<=W_c−Wc<=Yc<=Wc, −Wc<=Zc<=Wc-W_c<=Z_c<=W_c−Wc<=Zc<=Wc,ClipSpace的齐次坐标经过透视除法将Xc,Yc,ZcX_c,Y_c,Z_cXc,Yc,Zc都除以WcW_cWc就转换到了CVV中的NDC三维坐标。对于透视投影,我们将会看到,WcW_cWc的值是−Ze-Z_e−Ze,(Xc,Yc,Zc)(X_c,Y_c,Z_c)(Xc,Yc,Zc)除以−Ze-Z_e−Ze后得到了投影后的坐标,因此除以Wc被称为透视除法。
其次,对于投影来说,从3D转换到2D减少了一个维度,屏幕坐标只需要x,y值。但是为了进行深度测试以及裁剪,需要保留Z值。而且在后面的光栅化阶段,需要对顶点进行插值,得到中间的像素,除了对x,y插值,z也要插值。所以除了要保留Z值,还要保证插值后Z值的正确性。
再次,对于透视投影,还需要让生成的x,y坐标和z坐标成反比,以达到近大远小的效果。
所以,要完成以上这些目标,投影矩阵就需要多考虑一些事情,实际上有些图形学教材上例举的投影矩阵比OpenGL的简单一些,比如只是把x,y坐标按透视效果投影到投影面(OpenGL实际是投影到CVV),而投影前的z值没有保留;或者在此基础上保留了z值且插值后的z值是透视正确的,但是没转换到Clip Space,即对x,y坐标没有进行范围的映射。这些矩阵往往只是出于教学目的,OpenGL投影矩阵可以说是这些矩阵的超集。
小结一下,OpenGL坐标转换的过程:
【模型坐标 ----> [Vertex Shader] —> 裁剪坐标 ---->[透视除法]---->NDC—>[Viewport变换]---->窗口坐标】
1-1)ModelView矩阵将模型顶点从模型坐标变换到View Space。
1-2)投影矩阵变换View space的顶点,得到的是Clip Space中的裁剪坐标(齐次坐标)。
2)在Clip Space中进行剪裁
3)进行透视除法,得到的是CVV中的NDC坐标
4)进行viewport变换,得到屏幕坐标。进行depthRange变换,得到定点数深度值。
5)光栅化阶段对顶点的屏幕坐标和深度值进行插值,得到图元所覆盖的像素(片段)的坐标和深度。
其中1-1,1-2在vertex shader中经常合并成MVP矩阵。而本文要讨论的投影矩阵就是将顶点从View space变换到Clip space。
展开一点讨论:上面的模型空间,视图空间,NDC都是3D坐标空间,尽管计算时顶点使用齐次坐标表示,但顶点的w值为1,直接提取x,y,z即得到3D坐标。而裁剪空间很特殊,其中的点也用齐次坐标表示,但w值通常不为1。(例如通过透视投影变换得到的裁剪空间坐标,w值为-Ze)。这样的一个裁剪空间不能简单的提取x,y,z得到一个对应的3D坐标空间,为了得到3D坐标,需要除以w,而除以w得到的就是NDC这个3D坐标空间。一般没法用图示表示裁剪空间,他真的不是一个立方体,因为它就不是3D空间。其实模型空间,视图空间从数学上说也是齐次坐标空间,因为你运算的时候使用的都是齐次坐标表示的顶点。只不过由于w为1,所以这些齐次点对应的3D点构成了3D空间的模型,视图空间。而裁剪空间也是一个齐次坐标空间,它对应的3D空间就是NDC,所以不精确的你也可以说裁剪空间是个立方体。
OpenGL的一些重要约定
理解了投影究竟是干什么的,我们就可以开始推导投影矩阵了。但在这之前先让我们明确OpenGL的一些重要约定。
在投影之前,顶点处于View Space观察空间中,对于OpenGL,观察空间是+x向右,+y向上,+z向屏幕外的一个右手坐标系,观察方向沿着-z轴,即看向屏幕内部。也就是说如果我们没有模型和视图变换,vertex shader中指定顶点坐标默认使用的坐标系就是这样的一个右手坐标系。
通过投影(以及透视除法),顶点被变换到CVV中,在OpenGL中,CVV是一个坐标范围从(−1,−1,−1)(-1,-1,-1)(−1,−1,−1)到(1,1,1)(1,1,1)(1,1,1)的轴对齐立方体。而且重要的是,OpenGL的CVV是左手坐标系。这其实也好理解,因为OpenGL的视景体中,near plane被映射到NDC的z=−1z=-1z=−1平面,far plane被映射到z=1z=1z=1平面,而near pane离眼睛更近,因此NDC的+z轴就是指向屏幕内(+x, +y方向和观察空间相同),因此可以看出观察空间是右手坐标系,CVV(NDC)是左手坐标系。
两种投影矩阵
没错,我们要分别推导透视投影矩阵和平行投影矩阵。这两种投影使用的视景体的形状不同。对于透视投影采用frustum(平截头体),而平行投影采用一个轴对齐六面体。但是两种投影都是要变换(映射)到相同的CVV中。
推导OpenGL透视投影矩阵
目标:将视图坐标系中的顶点Pe=(Xe,Ye,Ze)P_e=(X_e,Y_e,Z_e)Pe=(Xe,Ye,Ze)变换到NDC坐标系中的顶点Pn=(Xn,Yn,Zn)Pn=(X_n,Y_n,Z_n)Pn=(Xn,Yn,Zn),其中投影矩阵完成从PeP_ePe到裁剪空间顶点Pc=(Xc,Yc,Zc,Wc)P_c=(X_c,Y_c,Z_c,W_c)Pc=(Xc,Yc,Zc,Wc)的变换,然后PcP_cPc进行透视除法得到PnP_nPn。
结合上面的讨论,我们使用以下惯例和约定:
- 视图坐标系使用右手坐标系,NDC使用左手坐标系。NDC范围为−1<=x<=1,−1<=y<=1,−1<=z<=1-1<= x <=1, -1<= y <=1, -1<= z <=1−1<=x<=1,−1<=y<=1,−1<=z<=1
- 透视投影的视景体(frustum)由六个参数定义,对应了OpenGL的传统函数
glFrustum(left, right, bottom, top, nearVal, farVal)
。其中left,right,bottom,topleft,right,bottom,topleft,right,bottom,top为frustum的四个边平面在近视截面上所截出的矩形区域的左边x=left,右边x=right,底边y=bottom和顶边y=top左边x=left,右边x=right,底边y=bottom和顶边y=top左边x=left,右边x=right,底边y=bottom和顶边y=top。nearVal和farValnearVal和farValnearVal和farVal则为距离观察点的最近和最远距离,这两个是距离值必须为正(而由于观察空间中视线是看向负Z轴的,因此近远剪裁面的坐标为z=−nearValz=-nearValz=−nearVal和z=−farValz=-farValz=−farVal)。为了书写方便,下面这六个参数简写为l,r,b,t,n,fl,r,b,t,n,fl,r,b,t,n,f。 - NDC和屏幕的对应关系为:x=1x=1x=1的点在屏幕右边, x=−1x=-1x=−1在左边;y=1y=1y=1在顶部,y=−1y=-1y=−1在底部;z=−1z=-1z=−1的点距离观察者最近,z=1z=1z=1的点距离观察者最远。
约定很重要,因为约定不一样,推导出的矩阵不一样,比如n和f,OpenGL的约定为不含符号的正数距离值,而有些文章推导时n和f是包含符号的坐标值。再如OpenGL约定 z=−nz=-nz=−n 映射到z=−1z=-1z=−1; z=−fz=-fz=−f映射到z=1z=1z=1,而有些图形学教材是将nnn映射到z=1z=1z=1, fff映射到z=−1z=-1z=−1,这样矩阵的第三行符号就是反的。
推导过程
首先,在视图空间中,我们以近裁剪面为投影面,计算视图空间中的一个点(Xe,Ye,Ze)(X_e,Y_e,Z_e)(Xe,Ye,Ze)在投影面上的坐标(Xp,Yp,Zp)(X_p,Y_p,Z_p)(Xp,Yp,Zp),从俯视图可看出,根据相似三角形的比例关系:
XpXe=ZpZe\frac{X_p}{X_e} = \frac{Z_p}{Z_e}XeXp=ZeZp,而Zp=−nZ_p=-nZp=−n
因此 XpXe=−nZe\frac{X_p}{X_e} = \frac{-n}{Z_e} XeXp=Ze−n Xp=−nXeZe=nXe−ZeX_p = \frac{-nX_e}{Z_e}=\frac{nX_e}{-Z_e}Xp=Ze−nXe=−ZenXe
同样,根据侧视图,可计算得到 Yp=nYe−ZeY_p = \frac{nY_e}{-Z_e}Yp=−ZenYe
即 Pe=(Xe,Ye,Ze)P_e=(X_e,Y_e,Z_e)Pe=(Xe,Ye,Ze)被投影到Pp=(nXe−Ze,nYe−Ze,−n)P_p=( \frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, -n)Pp=(−ZenXe,−ZenYe,−n)。注意投影后的z坐标总是−n-n−n,但是我们想在投影后仍然保留投影前z坐标的信息以便进行深度测试等工作。如果我们直接保留ZeZ_eZe行不行呢?即Pp=(nXe−Ze,nYe−Ze,Ze)P_p=( \frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, Z_e)Pp=(−ZenXe,−ZenYe,Ze)。看上去没毛病,但是这是不行的。因为投影之后的光栅化阶段,需要在屏幕空间对顶点属性进行插值,以得到每个像素的深度值和其他属性如纹理坐标光照亮度等。而光栅化时在屏幕空间从点A到点B均匀的遍历像素,并根据像素到AB的距离对Z坐标进行线性插值,得到在屏幕空间均匀分布的Z值,可是每个像素逆投射回视图空间就会发现,这些像素在视图空间对应的Z值并不是均匀分布。具体请参考图形学基础之透视校正插值。实际上,光栅化时应该对Z坐标的倒数进行插值,因此需要建立关于1/Z的映射函数:Zp=AZe+BZ_p = \frac{A}{Z_e}+BZp=ZeA+B。综上所述,投影后得到的顶点为:
Pp=(nXe−Ze,nYe−Ze,AZe+B)P_p = (\frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, \frac{A}{Z_e}+B) Pp=(−ZenXe,−ZenYe,ZeA+B)
而投影面上(近视截面)的顶点满足 l≤Xp≤rl \leq X_p \leq rl≤Xp≤r和 b≤Yp≤tb \leq Y_p \leq tb≤Yp≤t
如上所说,视锥体通过投影矩阵(以及透视除法)最终变换为CVV,即(Xp,Yp,Zp)(X_p,Y_p,Z_p)(Xp,Yp,Zp)变换为NDC坐标(Xn,Yn,Zn)(X_n,Y_n,Z_n)(Xn,Yn,Zn)。而Xn,Yn,ZnX_n,Y_n,Z_nXn,Yn,Zn的范围都是[−1,1][-1,1][−1,1]。首先我们处理x,y坐标,将Xp,YpX_p,Y_pXp,Yp映射到Xn,YnX_n,Y_nXn,Yn,即将[l,n][l,n][l,n]和[b,t][b,t][b,t]映射到[−1,1][-1,1][−1,1]的范围,这通过简单的线性函数就可以实现:
Xn=2(Xp−l)r−l−1X_n = \frac{2(Xp-l)}{r-l}-1Xn=r−l2(Xp−l)−1
Yn=2(Yp−b)t−b−1Y_n = \frac{2(Yp-b)}{t-b}-1Yn=t−b2(Yp−b)−1
代入上面关于Xp,YpX_p,Y_pXp,Yp的表达式:
Xn=2(nXe−Ze−l)r−l−1=2nr−l(Xe−Ze)−2lr−l−1X_n = \frac{2(\frac{nX_e}{-Z_e}-l)}{r-l}-1 = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{2l}{r-l}-1Xn=r−l2(−ZenXe−l)−1=r−l2n(−ZeXe)−r−l2l−1
Xn=2nr−l(Xe−Ze)−r+lr−lX_n = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{r+l}{r-l}Xn=r−l2n(−ZeXe)−r−lr+l
同样可得
Yn=2nt−b(Ye−Ze)−t+bt−bY_n=\frac{2n}{t-b}(\frac{Y_e}{-Z_e})-\frac{t+b}{t-b}Yn=t−b2n(−ZeYe)−t−bt+b
这就得到了从视图坐标的xy到NDC坐标的xy的映射关系,下面找一下z坐标的映射关系Zn=f(Ze)Z_n=f(Z_e)Zn=f(Ze),即视图空间Z坐标和NDC的Z坐标的函数。
由于我们将视图空间投影后的z坐标设置为AZe+B\frac{A}{Z_e}+BZeA+B的形式,而从投影坐标到NDC坐标是线性映射,因此可将NDC坐标ZnZ_nZn也记为AZe+B\frac{A}{Z_e}+BZeA+B,只是相对于ZpZ_pZp其A,B值不同。
已知视图空间z坐标ZeZ_eZe的范围是[−f,−n][-f,-n][−f,−n],对应了NDC中的z坐标范围[−1,1][-1,1][−1,1],且−n-n−n映射到−1-1−1,−f-f−f映射到111,因此将−n,−f-n,-f−n,−f分别代入Zn=AZe+BZn=\frac{A}{Ze}+BZn=ZeA+B得:
−1=A−n+B-1 = \frac{A}{-n}+B−1=−nA+B
1=A−f+B1 = \frac{A}{-f}+B1=−fA+B
可解出A,B为:
A=2nff−nA=\frac{2nf}{f-n}A=f−n2nf
B=f+nf−nB=\frac{f+n}{f-n}B=f−nf+n
将A,B代入Zn=AZe+BZn=\frac{A}{Ze}+BZn=ZeA+B的表达式后,即可得到Ze和ZnZ_e和Z_nZe和Zn的关系式:
Zn=2nff−nZe+f+nf−nZ_n=\frac{\frac{2nf}{f-n}}{Ze}+\frac{f+n}{f-n}Zn=Zef−n2nf+f−nf+n,即:
Zn=−2nff−n(1−Ze)+f+nf−nZ_n = \frac{-2nf}{f-n}(\frac{1}{-Z_e})+\frac{f+n}{f-n}Zn=f−n−2nf(−Ze1)+f−nf+n
至此,我们已经得到了视图空间坐标(Xe,Ye,Ze)(X_e,Y_e,Z_e)(Xe,Ye,Ze)到NDC坐标(Zn,Yn,Zn)(Z_n,Y_n,Z_n)(Zn,Yn,Zn)的函数:
Xn=2nr−l(Xe−Ze)−r+lr−lX_n = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{r+l}{r-l}Xn=r−l2n(−ZeXe)−r−lr+l
Yn=2nt−b(Ye−Ze)−t+bt−bY_n=\frac{2n}{t-b}(\frac{Y_e}{-Z_e})-\frac{t+b}{t-b}Yn=t−b2n(−ZeYe)−t−bt+b
Zn=−2nff−n(1−Ze)+f+nf−nZ_n = \frac{-2nf}{f-n}(\frac{1}{-Z_e})+\frac{f+n}{f-n}Zn=f−n−2nf(−Ze1)+f−nf+n
上文说过,从视图坐标到NDC坐标的变换分为两个过程,即先通过投影矩阵变换得到裁剪空间的齐次坐标,然后经过透视除法得到NDC坐标。我们已经得到了NDC坐标(Xn,Yn,Zn)(X_n,Y_n,Z_n)(Xn,Yn,Zn),为了得到投影矩阵,需要得到裁剪空间的齐次坐标(Xc,Yc,Zc,Wc)(X_c,Y_c,Z_c,W_c)(Xc,Yc,Zc,Wc)。由于Xn=XcWcX_n = \frac{X_c}{W_c}Xn=WcXc, Yn=YcWcY_n = \frac{Y_c}{W_c}Yn=WcYc, Zn=ZcWcZ_n = \frac{Z_c}{W_c}Zn=WcZc,且上面的Xn,Yn,ZnX_n,Y_n,Z_nXn,Yn,Zn的表达式中,都有−1Ze-\frac{1}{Z_e}−Ze1,显然可以令Wc=−ZeW_c=-Z_eWc=−Ze,Xn,Yn,ZnX_n,Y_n,Z_nXn,Yn,Zn分别乘以−Ze-Z_e−Ze得到(Xc,Yc,Zc,Wc)(X_c,Y_c,Z_c,W_c)(Xc,Yc,Zc,Wc)为:
Xc=2nr−lXe+r+lr−lZeX_c = \frac{2n}{r-l}X_e+\frac{r+l}{r-l}Z_eXc=r−l2nXe+r−lr+lZe
Yc=2nt−bYe+t+bt−bZeY_c=\frac{2n}{t-b}Y_e+\frac{t+b}{t-b}Z_eYc=t−b2nYe+t−bt+bZe
Zc=−f+nf−nZe−2nff−nZ_c = -\frac{f+n}{f-n}Z_e-\frac{2nf}{f-n}Zc=−f−nf+nZe−f−n2nf
Wc=−ZeW_c=-Z_eWc=−Ze
以上都是关于Pe=(Xe,Ye,Ze)P_e=(X_e,Y_e,Z_e)Pe=(Xe,Ye,Ze)的线性函数,可以用矩阵表示为:
Pproj=[2nr−l0r+lr−l002nt−bt+bt−b000−f+nf−n−2nff−n00−10]P_{proj} = \left[\begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2nf}{f-n} \\ 0 & 0 & -1 & 0 \end{matrix}\right] Pproj=⎣⎢⎢⎡r−l2n0000t−b2n00r−lr+lt−bt+b−f−nf+n−100−f−n2nf0⎦⎥⎥⎤
即得到了OpenGL的透视投影矩阵
关于Z值插值的一点补充
上文说到,为了对1Ze\frac{1}{Z_e}Ze1进行插值,我们将ZnZ_nZn定义成AZe+B\frac{A}{Z_e}+BZeA+B的形式,然后在光栅化时经过glDepthRange的映射,将[−1,1][-1,1][−1,1]的ZnZ_nZn映射为[0,1][0,1][0,1]的Z值,这个Z值被写到Z Buffer中。按理说插值Z应该就是用这个将写入Z Buffer的Z值了。但是我在某本书上看到,使用clip space的W值的倒数进行插值。clip space顶点是vertex shader的输出,其顶点的W值就是−Ze-Z_e−Ze,因此感觉也是挺科学的。具体什么情况,等我弄清楚了再补充。
gluPerspective风格的透视投影矩阵
OpenGL固定流水线的传统函数
void gluPerspective( GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar);
这其实是另外一种定义frustum视截体的方式,不同的是这种方式定义的视截体的中心在Z轴,也就是说,glFrustum矩阵中当l=−r,b=−tl=-r, b=-tl=−r,b=−t时的情况。
fovy为视截体在yz平面上的夹角,aspect为裁剪面的宽高比。因为左右上下对称,因此可知对于glFrustum矩阵中的l,r,b,tl,r,b,tl,r,b,t,l,bl,bl,b为负值,r,tr,tr,t为正值,因此可计算得到:
tan(fovy/2)=tntan(fovy/2) = \frac{t}{n}tan(fovy/2)=nt
t=n∗tan(fovy/2)t = n*tan(fovy/2)t=n∗tan(fovy/2)
b=−t=−n∗tan(fovy/2)b=-t = -n*tan(fovy/2)b=−t=−n∗tan(fovy/2)
r=aspect∗t=n∗aspect∗tan(fovy/2)r = aspect * t = n*aspect*tan(fovy/2)r=aspect∗t=n∗aspect∗tan(fovy/2)
l=−r=−n∗aspect∗tan(fovy/2)l = -r = -n*aspect*tan(fovy/2)l=−r=−n∗aspect∗tan(fovy/2)
将l,r,b,tl,r,b,tl,r,b,t代入上面的glFrustum矩阵中,可得gluPerspective矩阵:
PgluPerspective=[1aspect∗tan(fovy/2)00001tan(fovy/2)0000−f+nf−n−2nff−n00−10]P_{gluPerspective} = \left[\begin{matrix} \frac{1}{aspect*tan(fovy/2)} & 0 &0 & 0 \\ 0 & \frac{1}{tan(fovy/2)} & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2nf}{f-n} \\ 0 & 0 & -1 & 0 \end{matrix}\right] PgluPerspective=⎣⎢⎢⎢⎡aspect∗tan(fovy/2)10000tan(fovy/2)10000−f−nf+n−100−f−n2nf0⎦⎥⎥⎥⎤
推导OpenGL平行投影矩阵
如图所示,平行投影的视景体是一个轴对齐六面体,由于没有透视效果,我们只需要将视景体映射到NDC。
目标:将平行投影视图坐标系中的顶点Pe=(Xe,Ye,Ze)P_e=(X_e,Y_e,Z_e)Pe=(Xe,Ye,Ze)变换到NDC坐标系中的顶点Pn=(Xn,Yn,Zn)P_n=(X_n,Y_n,Z_n)Pn=(Xn,Yn,Zn)。
约定:
NDC的约定同透视投影,视景体的定义同传统OpenGL函数glOrtho(left,right,top,bottom.near,far)glOrtho(left, right, top, bottom. near, far)glOrtho(left,right,top,bottom.near,far)。前4个参数分别定义了视景体的左右上下四个面。near, far是近裁剪面和远裁剪面相对于视点的距离,但是和透视投影不同,near, far不一定是正数。如果near或far小于0,则表示位于视点后面(视点位于(0,0,0)(0,0,0)(0,0,0))。同样为了书写方便,这六个参数简写为l,r,t,b,n,fl, r, t, b, n, fl,r,t,b,n,f。这样(r,t,−n)(r,t,-n)(r,t,−n)表示的是近裁剪面的右上角。
推导过程
如上所述,由于平行投影的视景体是一个轴对称六面体,而NDC是一个立方体,也是轴对称的。因此只需要简单的线性映射,即可将视景体中的顶点Pe=(Xe,Ye,Ze)P_e=(X_e,Y_e,Z_e)Pe=(Xe,Ye,Ze)变换到NDC中的顶点Pn=(Xn,Yn,Zn)P_n=(X_n,Y_n,Z_n)Pn=(Xn,Yn,Zn)。这只需要先将六面体的长宽高缩放到2,然后将中心点移动到立方体中心即可。
以X坐标为例,我们需要将XeX_eXe映射到XnX_nXn,其实这和上面透视投影将XpX_pXp映射到XnX_nXn是一样的,但是之前没有具体推导,一笔带过了。这儿稍微详细推导一下:
由于XeX_eXe的范围是[l,r][l,r][l,r],XnX_nXn的范围是[−1,1][-1,1][−1,1],因此通过
1−(−1)r−l.Xe\frac{1-(-1)}{r-l}.X_er−l1−(−1).Xe即可把XeX_eXe缩放到[−1,1][-1,1][−1,1],然后再进行一个偏移将中心点移动到原点,假设偏移量为BBB,则可得:
Xn=1−(−1)r−lXe+BX_n = \frac{1-(-1)}{r-l}X_e+BXn=r−l1−(−1)Xe+B
为了计算出BBB,我们将Xe=r和Xn=1X_e=r和X_n=1Xe=r和Xn=1带入上式
1=2r−lr+B1 = \frac{2}{r-l}r+B1=r−l2r+B,可得
B=−r+lr−lB=-\frac{r+l}{r-l}B=−r−lr+l,将其代入上式,可得:
Xn=2r−lXe−r+lr−lX_n = \frac{2}{r-l}X_e-\frac{r+l}{r-l}Xn=r−l2Xe−r−lr+l
同样可得
Yn=2t−bYe−t+bt−bY_n = \frac{2}{t-b}Y_e-\frac{t+b}{t-b}Yn=t−b2Ye−t−bt+b
ZnZ_nZn的推导过程一样,只是由于n,fn,fn,f是距离值,因此其坐标表示为−n,−f-n,-f−n,−f,不失一般性在上图所示的情况下, −f映射到1,−n映射到−1-f映射到1,-n映射到-1−f映射到1,−n映射到−1,因此:
Zn=1−(−1)−f−(−n)Ze+BZ_n = \frac{1-(-1)}{-f-(-n)}Z_e+BZn=−f−(−n)1−(−1)Ze+B
代人Zn=1,Ze=−fZ_n=1, Z_e=-fZn=1,Ze=−f
1=2n−f(−f)+B1 = \frac{2}{n-f}(-f)+B1=n−f2(−f)+B,得
B=2fn−f+1=n+fn−fB = \frac{2f}{n-f}+1=\frac{n+f}{n-f}B=n−f2f+1=n−fn+f,因此:
Zn=2n−fZe+n+fn−fZ_n = \frac{2}{n-f}Z_e+\frac{n+f}{n-f}Zn=n−f2Ze+n−fn+f
Zn=−2f−nZe−f+nf−nZ_n = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n}Zn=f−n−2Ze−f−nf+n
由此,我们得到了PeP_ePe到PnP_nPn的线性映射关系,我们实际需要的是PeP_ePe到PcP_cPc的线性关系,因为投影矩阵变换后得到的是Clip Space的顶点。但对于平行投影,w值没有意义,因此可以任意指定,这样我们指定w=1,即可直接将PcP_cPc用PnP_nPn表示,最终我们得到如下表达式:
Xc=2r−lXe−r+lr−lX_c = \frac{2}{r-l}X_e-\frac{r+l}{r-l}Xc=r−l2Xe−r−lr+l
Yc=2t−bYe−t+bt−bY_c= \frac{2}{t-b}Y_e-\frac{t+b}{t-b}Yc=t−b2Ye−t−bt+b
Zc=−2f−nZe−f+nf−nZ_c = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n}Zc=f−n−2Ze−f−nf+n
Wc=1W_c= 1Wc=1
以上都是关于Pe=(Xe,Ye,Ze)P_e=(X_e,Y_e,Z_e)Pe=(Xe,Ye,Ze)的线性函数,可以用矩阵表示为:
Pproj=[2r−l00−r+lr−l02t−b0−t+bt−b00−2f−n−f+nf−n0001]P_{proj} = \left[\begin{matrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{-2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \end{matrix}\right] Pproj=⎣⎢⎢⎡r−l20000t−b20000f−n−20−r−lr+l−t−bt+b−f−nf+n1⎦⎥⎥⎤
即得到了OpenGL的平行投影矩阵
补充
最近学习了GAMES101课程,闫令琪老师讲解了图形学约定下投影矩阵的推导,非常值得一看:
https://www.bilibili.com/video/BV1X7411F744?p=4&t=3007
其中的约定和OpenGL稍微有些不同,一是OpenGL中NDC空间是左手坐标系,而闫老师推导的是右手坐标系,即和视图坐标系一致。二是关于n和f,OpenGL是距离值,而闫老师使用的是坐标值。
推导的过程非常好,比如平行投影矩阵,只是先将frustum平移到原点,然后坐一个缩放,直接将两个矩阵相乘就得到投影矩阵。由于约定的不同,在闫老师的矩阵中将n和f取反,并且将z乘以-1,最终得到的矩阵和OpenGL就是一样的了。
深入理解OpenGL之投影矩阵推导相关推荐
- OpenGL学习: 投影矩阵和视口变换矩阵(math-projection and viewport matrix)
转自:https://blog.csdn.net/wangdingqiaoit/article/details/51589825 本文主要翻译并整理自 songho OpenGL Projection ...
- c++实现软光栅(二)实现立方体的绘制几个视图矩阵变换投影矩阵推导
文章目录 顶点数据分析 如何变换到世界空间:Model_Matrix 缩放rotate_matrix 旋转 平移 如何变换到摄像机空间:View_Matrix 如何使视图更加符合人眼视角(产生近大远小 ...
- 斜视锥体投影矩阵推导
参考网址: https://gameinstitute.qq.com/community/detail/106203 翻译 http://www.terathon.com/lengyel/Lengye ...
- OpenGL中投影矩阵(Projection Matrix)详解
在游戏开发中,一个物体模型从它自身的坐标系转换至我们在屏幕上所见的样子,需要进行一系列的坐标变换以及其他的操作.该过程称为渲染管线.以OpenGL为例: 该过程在以前是被封装的,不能访问.但是现在我们 ...
- webgl投影矩阵推导(正射投影、透视投影)
文章目录 前言 正射投影 透视投影 总结 前言 在webgl中,三维空间中的所有物体不是会都被绘制出来,只有当它在可视范围内时,才会进行绘制.因为不在可视范围中的物体即使绘制也不会在屏幕上显示.除了水 ...
- 【OpenGL】透视投影矩阵推导
项目场景: 系统:ubuntu glad + glfw + opengl3.3 复习games101MVP变换,在使用OpenGL检验推导透视投影矩阵时,发现得出结果的Z坐标与把不符合目标预期.可以看 ...
- opoengl 投影矩阵的推导
原文:http://blog.csdn.net/wangdingqiaoit/article/details/39010077 OpenGL学习脚印: 投影矩阵的推导 写在前面 本节内容翻译和整理自h ...
- 【脚下生根】之深度探索安卓OpenGL投影矩阵
世界变化真快,前段时间windows开发技术热还在如火如荼,web技术就开始来势汹汹,正当web呈现欣欣向荣之际,安卓小机器人,咬过一口的苹果,winPhone开发平台又如闪电般划破了混沌的web世界 ...
- 【转】投影矩阵的推导
[转]投影矩阵的推导 原文:https://www.cnblogs.com/wonderKK/p/5695116.html 博主: 这篇文章写得非常好,对投影矩阵的推导清晰明了,但有个错误:推导的全程 ...
最新文章
- ActiveMQ的使用
- HTML学习02之基础;元素;属性
- React开发(139):ant design学习指南之下载文件
- mysql升级后乱码_Mysql转换或者升级以后出现乱码情况的说明
- 几行代码实现谷歌百度搜索对比
- 笔试算法题(26):顺时针打印矩阵 求数组中数对差的最大值
- MATLAB不能用了,哪些替代品可以继续搞科研?
- 教程贴--DISM 安装系统
- 数学建模:现代优化算法之粒子群算法
- 推荐6本React在线电子版书籍
- 前后端api参考手册
- 反斜杠“\”的几个用法!
- AOSP、AOKP、CM ROM 究竟有哪些区别?
- 爬虫大作业-爬取B站弹幕
- 转移APK从手机到PC和PC到手机
- Stern-Brocot树 (生成0-1之间的所有真分数)
- 以matlab为基础数学分析,matlab与数学分析.docx
- 使用loadrunner javavuser协议开发脚本实战
- ACM/ICPC 2018亚洲区预选赛北京赛站网络赛 D. 80 Days
- 报名系统网页导出html,【网页报名表如何导出pdf】_网页的报名表怎样转换为word或者PDF格式...
热门文章
- 简单的C语言航班管理系统
- mysql企业备份工具(MEB)之mysqlbackup安装及使用
- batocera_旧电脑变身影音游戏主机,支持70多个平台上万个游戏
- 响铃:智能微投兴起,卧室影院会成为真正的爆发点吗?
- nas存储用网线直连服务器,几分钟就搞定 搭建NAS存储必备秘籍
- 管理者的五大能力十大素质
- “木桶原理”——吾之见学习法,成长法
- 报表模块-report
- 新闻速递 | 恭喜肖晓容工程师获得Domo专业认证!
- RANSAC(随机采样一致算法)原理及openCV代码实现