深入理解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}Xe​Xp​​=Ze​Zp​​,而Zp=−nZ_p=-nZp​=−n
因此 XpXe=−nZe\frac{X_p}{X_e} = \frac{-n}{Z_e} Xe​Xp​​=Ze​−n​ Xp=−nXeZe=nXe−ZeX_p = \frac{-nX_e}{Z_e}=\frac{nX_e}{-Z_e}Xp​=Ze​−nXe​​=−Ze​nXe​​
同样,根据侧视图,可计算得到 Yp=nYe−ZeY_p = \frac{nY_e}{-Z_e}Yp​=−Ze​nYe​​
即 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​=(−Ze​nXe​​,−Ze​nYe​​,−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​=(−Ze​nXe​​,−Ze​nYe​​,Ze​)。看上去没毛病,但是这是不行的。因为投影之后的光栅化阶段,需要在屏幕空间对顶点属性进行插值,以得到每个像素的深度值和其他属性如纹理坐标光照亮度等。而光栅化时在屏幕空间从点A到点B均匀的遍历像素,并根据像素到AB的距离对Z坐标进行线性插值,得到在屏幕空间均匀分布的Z值,可是每个像素逆投射回视图空间就会发现,这些像素在视图空间对应的Z值并不是均匀分布。具体请参考图形学基础之透视校正插值。实际上,光栅化时应该对Z坐标的倒数进行插值,因此需要建立关于1/Z的映射函数:Zp=AZe+BZ_p = \frac{A}{Z_e}+BZp​=Ze​A​+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​=(−Ze​nXe​​,−Ze​nYe​​,Ze​A​+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(−Ze​nXe​​−l)​−1=r−l2n​(−Ze​Xe​​)−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​(−Ze​Xe​​)−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​(−Ze​Ye​​)−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}+BZe​A​+B的形式,而从投影坐标到NDC坐标是线性映射,因此可将NDC坐标ZnZ_nZn​也记为AZe+B\frac{A}{Z_e}+BZe​A​+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​(−Ze​1​)+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​(−Ze​Xe​​)−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​(−Ze​Ye​​)−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​(−Ze​1​)+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​=Wc​Xc​​, Yn=YcWcY_n = \frac{Y_c}{W_c}Yn​=Wc​Yc​​, Zn=ZcWcZ_n = \frac{Z_c}{W_c}Zn​=Wc​Zc​​,且上面的Xn,Yn,ZnX_n,Y_n,Z_nXn​,Yn​,Zn​的表达式中,都有−1Ze-\frac{1}{Z_e}−Ze​1​,显然可以令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−l2n​Xe​+r−lr+l​Ze​
Yc=2nt−bYe+t+bt−bZeY_c=\frac{2n}{t-b}Y_e+\frac{t+b}{t-b}Z_eYc​=t−b2n​Ye​+t−bt+b​Ze​
Zc=−f+nf−nZe−2nff−nZ_c = -\frac{f+n}{f-n}Z_e-\frac{2nf}{f-n}Zc​=−f−nf+n​Ze​−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−l2n​000​0t−b2n​00​r−lr+l​t−bt+b​−f−nf+n​−1​00−f−n2nf​0​⎦⎥⎥⎤​
即得到了OpenGL的透视投影矩阵

关于Z值插值的一点补充

上文说到,为了对1Ze\frac{1}{Z_e}Ze​1​进行插值,我们将ZnZ_nZn​定义成AZe+B\frac{A}{Z_e}+BZe​A​+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)1​000​0tan(fovy/2)1​00​00−f−nf+n​−1​00−f−n2nf​0​⎦⎥⎥⎥⎤​

推导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−l2​r+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−l2​Xe​−r−lr+l​
同样可得
Yn=2t−bYe−t+bt−bY_n = \frac{2}{t-b}Y_e-\frac{t+b}{t-b}Yn​=t−b2​Ye​−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−f2​Ze​+n−fn+f​
Zn=−2f−nZe−f+nf−nZ_n = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n}Zn​=f−n−2​Ze​−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−l2​Xe​−r−lr+l​
Yc=2t−bYe−t+bt−bY_c= \frac{2}{t-b}Y_e-\frac{t+b}{t-b}Yc​=t−b2​Ye​−t−bt+b​
Zc=−2f−nZe−f+nf−nZ_c = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n}Zc​=f−n−2​Ze​−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−l2​000​0t−b2​00​00f−n−2​0​−r−lr+l​−t−bt+b​−f−nf+n​1​⎦⎥⎥⎤​
即得到了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之投影矩阵推导相关推荐

  1. OpenGL学习: 投影矩阵和视口变换矩阵(math-projection and viewport matrix)

    转自:https://blog.csdn.net/wangdingqiaoit/article/details/51589825 本文主要翻译并整理自 songho OpenGL Projection ...

  2. c++实现软光栅(二)实现立方体的绘制几个视图矩阵变换投影矩阵推导

    文章目录 顶点数据分析 如何变换到世界空间:Model_Matrix 缩放rotate_matrix 旋转 平移 如何变换到摄像机空间:View_Matrix 如何使视图更加符合人眼视角(产生近大远小 ...

  3. 斜视锥体投影矩阵推导

    参考网址: https://gameinstitute.qq.com/community/detail/106203 翻译 http://www.terathon.com/lengyel/Lengye ...

  4. OpenGL中投影矩阵(Projection Matrix)详解

    在游戏开发中,一个物体模型从它自身的坐标系转换至我们在屏幕上所见的样子,需要进行一系列的坐标变换以及其他的操作.该过程称为渲染管线.以OpenGL为例: 该过程在以前是被封装的,不能访问.但是现在我们 ...

  5. webgl投影矩阵推导(正射投影、透视投影)

    文章目录 前言 正射投影 透视投影 总结 前言 在webgl中,三维空间中的所有物体不是会都被绘制出来,只有当它在可视范围内时,才会进行绘制.因为不在可视范围中的物体即使绘制也不会在屏幕上显示.除了水 ...

  6. 【OpenGL】透视投影矩阵推导

    项目场景: 系统:ubuntu glad + glfw + opengl3.3 复习games101MVP变换,在使用OpenGL检验推导透视投影矩阵时,发现得出结果的Z坐标与把不符合目标预期.可以看 ...

  7. opoengl 投影矩阵的推导

    原文:http://blog.csdn.net/wangdingqiaoit/article/details/39010077 OpenGL学习脚印: 投影矩阵的推导 写在前面 本节内容翻译和整理自h ...

  8. 【脚下生根】之深度探索安卓OpenGL投影矩阵

    世界变化真快,前段时间windows开发技术热还在如火如荼,web技术就开始来势汹汹,正当web呈现欣欣向荣之际,安卓小机器人,咬过一口的苹果,winPhone开发平台又如闪电般划破了混沌的web世界 ...

  9. 【转】投影矩阵的推导

    [转]投影矩阵的推导 原文:https://www.cnblogs.com/wonderKK/p/5695116.html 博主: 这篇文章写得非常好,对投影矩阵的推导清晰明了,但有个错误:推导的全程 ...

最新文章

  1. ActiveMQ的使用
  2. HTML学习02之基础;元素;属性
  3. React开发(139):ant design学习指南之下载文件
  4. mysql升级后乱码_Mysql转换或者升级以后出现乱码情况的说明
  5. 几行代码实现谷歌百度搜索对比
  6. 笔试算法题(26):顺时针打印矩阵 求数组中数对差的最大值
  7. MATLAB不能用了,哪些替代品可以继续搞科研?
  8. 教程贴--DISM 安装系统
  9. 数学建模:现代优化算法之粒子群算法
  10. 推荐6本React在线电子版书籍
  11. 前后端api参考手册
  12. 反斜杠“\”的几个用法!
  13. AOSP、AOKP、CM ROM 究竟有哪些区别?
  14. 爬虫大作业-爬取B站弹幕
  15. 转移APK从手机到PC和PC到手机
  16. Stern-Brocot树 (生成0-1之间的所有真分数)
  17. 以matlab为基础数学分析,matlab与数学分析.docx
  18. 使用loadrunner javavuser协议开发脚本实战
  19. ACM/ICPC 2018亚洲区预选赛北京赛站网络赛 D. 80 Days
  20. 报名系统网页导出html,【网页报名表如何导出pdf】_网页的报名表怎样转换为word或者PDF格式...

热门文章

  1. 简单的C语言航班管理系统
  2. mysql企业备份工具(MEB)之mysqlbackup安装及使用
  3. batocera_旧电脑变身影音游戏主机,支持70多个平台上万个游戏
  4. 响铃:智能微投兴起,卧室影院会成为真正的爆发点吗?
  5. nas存储用网线直连服务器,几分钟就搞定 搭建NAS存储必备秘籍
  6. 管理者的五大能力十大素质
  7. “木桶原理”——吾之见学习法,成长法
  8. 报表模块-report
  9. 新闻速递 | 恭喜肖晓容工程师获得Domo专业认证!
  10. RANSAC(随机采样一致算法)原理及openCV代码实现