大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
深入理解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坐标满足 − W c < = X c < = W c -W_c<=X_c<=W_c −Wc<=Xc<=Wc, − W c < = Y c < = W c -W_c<=Y_c<=W_c −Wc<=Yc<=Wc, − W c < = Z c < = W c -W_c<=Z_c<=W_c −Wc<=Zc<=Wc,ClipSpace的齐次坐标经过透视除法将 X c , Y c , Z c X_c,Y_c,Z_c Xc,Yc,Zc都除以 W c W_c Wc就转换到了CVV中的NDC三维坐标。对于透视投影,我们将会看到, W c W_c Wc的值是 − Z e -Z_e −Ze, ( X c , Y c , Z c ) (X_c,Y_c,Z_c) (Xc,Yc,Zc)除以 − Z e -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 = − 1 z=-1 z=−1平面,far plane被映射到 z = 1 z=1 z=1平面,而near pane离眼睛更近,因此NDC的+z轴就是指向屏幕内(+x, +y方向和观察空间相同),因此可以看出观察空间是右手坐标系,CVV(NDC)是左手坐标系。
两种投影矩阵
没错,我们要分别推导透视投影矩阵和平行投影矩阵。这两种投影使用的视景体的形状不同。对于透视投影采用frustum(平截头体),而平行投影采用一个轴对齐六面体。但是两种投影都是要变换(映射)到相同的CVV中。
推导OpenGL透视投影矩阵
目标:将视图坐标系中的顶点 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)变换到NDC坐标系中的顶点 P n = ( X n , Y n , Z n ) Pn=(X_n,Y_n,Z_n) Pn=(Xn,Yn,Zn),其中投影矩阵完成从 P e P_e Pe到裁剪空间顶点 P c = ( X c , Y c , Z c , W c ) P_c=(X_c,Y_c,Z_c,W_c) Pc=(Xc,Yc,Zc,Wc)的变换,然后 P c P_c Pc进行透视除法得到 P n P_n Pn。
结合上面的讨论,我们使用以下惯例和约定:
- 视图坐标系使用右手坐标系,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)
。其中 l e f t , r i g h t , b o t t o m , t o p left,right,bottom,top left,right,bottom,top为frustum的四个边平面在近视截面上所截出的矩形区域的 左 边 x = l e f t , 右 边 x = r i g h t , 底 边 y = b o t t o m 和 顶 边 y = t o p 左边x=left,右边x=right,底边y=bottom和顶边y=top 左边x=left,右边x=right,底边y=bottom和顶边y=top。 n e a r V a l 和 f a r V a l nearVal和farVal nearVal和farVal则为距离观察点的最近和最远距离,这两个是距离值必须为正(而由于观察空间中视线是看向负Z轴的,因此近远剪裁面的坐标为 z = − n e a r V a l z=-nearVal z=−nearVal和 z = − f a r V a l z=-farVal z=−farVal)。为了书写方便,下面这六个参数简写为 l , r , b , t , n , f l,r,b,t,n,f l,r,b,t,n,f。 - NDC和屏幕的对应关系为: x = 1 x=1 x=1的点在屏幕右边, x = − 1 x=-1 x=−1在左边; y = 1 y=1 y=1在顶部, y = − 1 y=-1 y=−1在底部; z = − 1 z=-1 z=−1的点距离观察者最近, z = 1 z=1 z=1的点距离观察者最远。
约定很重要,因为约定不一样,推导出的矩阵不一样,比如n和f,OpenGL的约定为不含符号的正数距离值,而有些文章推导时n和f是包含符号的坐标值。再如OpenGL约定 z = − n z=-n z=−n 映射到 z = − 1 z=-1 z=−1; z = − f z=-f z=−f映射到 z = 1 z=1 z=1,而有些图形学教材是将 n n n映射到 z = 1 z=1 z=1, f f f映射到 z = − 1 z=-1 z=−1,这样矩阵的第三行符号就是反的。
推导过程
首先,在视图空间中,我们以近裁剪面为投影面,计算视图空间中的一个点 ( X e , Y e , Z e ) (X_e,Y_e,Z_e) (Xe,Ye,Ze)在投影面上的坐标 ( X p , Y p , Z p ) (X_p,Y_p,Z_p) (Xp,Yp,Zp),从俯视图可看出,根据相似三角形的比例关系:
X p X e = Z p Z e \frac{X_p}{X_e} = \frac{Z_p}{Z_e} XeXp=ZeZp,而 Z p = − n Z_p=-n Zp=−n
因此 X p X e = − n Z e \frac{X_p}{X_e} = \frac{-n}{Z_e} XeXp=Ze−n X p = − n X e Z e = n X e − Z e X_p = \frac{-nX_e}{Z_e}=\frac{nX_e}{-Z_e} Xp=Ze−nXe=−ZenXe
同样,根据侧视图,可计算得到 Y p = n Y e − Z e Y_p = \frac{nY_e}{-Z_e} Yp=−ZenYe
即 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)被投影到 P p = ( n X e − Z e , n Y e − Z e , − n ) P_p=( \frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, -n) Pp=(−ZenXe,−ZenYe,−n)。注意投影后的z坐标总是 − n -n −n,但是我们想在投影后仍然保留投影前z坐标的信息以便进行深度测试等工作。如果我们直接保留 Z e Z_e Ze行不行呢?即 P p = ( n X e − Z e , n Y e − Z e , Z e ) 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的映射函数: Z p = A Z e + B Z_p = \frac{A}{Z_e}+B Zp=ZeA+B。综上所述,投影后得到的顶点为:
P p = ( n X e − Z e , n Y e − Z e , A Z e + B ) P_p = (\frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, \frac{A}{Z_e}+B) Pp=(−ZenXe,−ZenYe,ZeA+B)
而投影面上(近视截面)的顶点满足 l ≤ X p ≤ r l \leq X_p \leq r l≤Xp≤r和 b ≤ Y p ≤ t b \leq Y_p \leq t b≤Yp≤t
如上所说,视锥体通过投影矩阵(以及透视除法)最终变换为CVV,即 ( X p , Y p , Z p ) (X_p,Y_p,Z_p) (Xp,Yp,Zp)变换为NDC坐标 ( X n , Y n , Z n ) (X_n,Y_n,Z_n) (Xn,Yn,Zn)。而 X n , Y n , Z n X_n,Y_n,Z_n Xn,Yn,Zn的范围都是 [ − 1 , 1 ] [-1,1] [−1,1]。首先我们处理x,y坐标,将 X p , Y p X_p,Y_p Xp,Yp映射到 X n , Y n X_n,Y_n Xn,Yn,即将 [ l , n ] [l,n] [l,n]和 [ b , t ] [b,t] [b,t]映射到 [ − 1 , 1 ] [-1,1] [−1,1]的范围,这通过简单的线性函数就可以实现:
X n = 2 ( X p − l ) r − l − 1 X_n = \frac{2(Xp-l)}{r-l}-1 Xn=r−l2(Xp−l)−1
Y n = 2 ( Y p − b ) t − b − 1 Y_n = \frac{2(Yp-b)}{t-b}-1 Yn=t−b2(Yp−b)−1
代入上面关于 X p , Y p X_p,Y_p Xp,Yp的表达式:
X n = 2 ( n X e − Z e − l ) r − l − 1 = 2 n r − l ( X e − Z e ) − 2 l r − l − 1 X_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}-1 Xn=r−l2(−ZenXe−l)−1=r−l2n(−ZeXe)−r−l2l−1
X n = 2 n r − l ( X e − Z e ) − r + l r − l X_n = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{r+l}{r-l} Xn=r−l2n(−ZeXe)−r−lr+l
同样可得
Y n = 2 n t − b ( Y e − Z e ) − t + b t − b Y_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坐标的映射关系 Z n = f ( Z e ) Z_n=f(Z_e) Zn=f(Ze),即视图空间Z坐标和NDC的Z坐标的函数。
由于我们将视图空间投影后的z坐标设置为 A Z e + B \frac{A}{Z_e}+B ZeA+B的形式,而从投影坐标到NDC坐标是线性映射,因此可将NDC坐标 Z n Z_n Zn也记为 A Z e + B \frac{A}{Z_e}+B ZeA+B,只是相对于 Z p Z_p Zp其A,B值不同。
已知视图空间z坐标 Z e Z_e Ze的范围是 [ − f , − n ] [-f,-n] [−f,−n],对应了NDC中的z坐标范围 [ − 1 , 1 ] [-1,1] [−1,1],且 − n -n −n映射到 − 1 -1 −1, − f -f −f映射到 1 1 1,因此将 − n , − f -n,-f −n,−f分别代入 Z n = A Z e + B Zn=\frac{A}{Ze}+B Zn=ZeA+B得:
− 1 = A − n + B -1 = \frac{A}{-n}+B −1=−nA+B
1 = A − f + B 1 = \frac{A}{-f}+B 1=−fA+B
可解出A,B为:
A = 2 n f f − n A=\frac{2nf}{f-n} A=f−n2nf
B = f + n f − n B=\frac{f+n}{f-n} B=f−nf+n
将A,B代入 Z n = A Z e + B Zn=\frac{A}{Ze}+B Zn=ZeA+B的表达式后,即可得到 Z e 和 Z n Z_e和Z_n Ze和Zn的关系式:
Z n = 2 n f f − n Z e + f + n f − n Z_n=\frac{\frac{2nf}{f-n}}{Ze}+\frac{f+n}{f-n} Zn=Zef−n2nf+f−nf+n,即:
Z n = − 2 n f f − n ( 1 − Z e ) + f + n f − n Z_n = \frac{-2nf}{f-n}(\frac{1}{-Z_e})+\frac{f+n}{f-n} Zn=f−n−2nf(−Ze1)+f−nf+n
至此,我们已经得到了视图空间坐标 ( X e , Y e , Z e ) (X_e,Y_e,Z_e) (Xe,Ye,Ze)到NDC坐标 ( Z n , Y n , Z n ) (Z_n,Y_n,Z_n) (Zn,Yn,Zn)的函数:
X n = 2 n r − l ( X e − Z e ) − r + l r − l X_n = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{r+l}{r-l} Xn=r−l2n(−ZeXe)−r−lr+l
Y n = 2 n t − b ( Y e − Z e ) − t + b t − b Y_n=\frac{2n}{t-b}(\frac{Y_e}{-Z_e})-\frac{t+b}{t-b} Yn=t−b2n(−ZeYe)−t−bt+b
Z n = − 2 n f f − n ( 1 − Z e ) + f + n f − n Z_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坐标 ( X n , Y n , Z n ) (X_n,Y_n,Z_n) (Xn,Yn,Zn),为了得到投影矩阵,需要得到裁剪空间的齐次坐标 ( X c , Y c , Z c , W c ) (X_c,Y_c,Z_c,W_c) (Xc,Yc,Zc,Wc)。由于 X n = X c W c X_n = \frac{X_c}{W_c} Xn=WcXc, Y n = Y c W c Y_n = \frac{Y_c}{W_c} Yn=WcYc, Z n = Z c W c Z_n = \frac{Z_c}{W_c} Zn=WcZc,且上面的 X n , Y n , Z n X_n,Y_n,Z_n Xn,Yn,Zn的表达式中,都有 − 1 Z e -\frac{1}{Z_e} −Ze1,显然可以令 W c = − Z e W_c=-Z_e Wc=−Ze, X n , Y n , Z n X_n,Y_n,Z_n Xn,Yn,Zn分别乘以 − Z e -Z_e −Ze得到 ( X c , Y c , Z c , W c ) (X_c,Y_c,Z_c,W_c) (Xc,Yc,Zc,Wc)为:
X c = 2 n r − l X e + r + l r − l Z e X_c = \frac{2n}{r-l}X_e+\frac{r+l}{r-l}Z_e Xc=r−l2nXe+r−lr+lZe
Y c = 2 n t − b Y e + t + b t − b Z e Y_c=\frac{2n}{t-b}Y_e+\frac{t+b}{t-b}Z_e Yc=t−b2nYe+t−bt+bZe
Z c = − f + n f − n Z e − 2 n f f − n Z_c = -\frac{f+n}{f-n}Z_e-\frac{2nf}{f-n} Zc=−f−nf+nZe−f−n2nf
W c = − Z e W_c=-Z_e Wc=−Ze
以上都是关于 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)的线性函数,可以用矩阵表示为:
P p r o j = [ 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − f + n f − n − 2 n f f − n 0 0 − 1 0 ] 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值插值的一点补充
上文说到,为了对 1 Z e \frac{1}{Z_e} Ze1进行插值,我们将 Z n Z_n Zn定义成 A Z e + B \frac{A}{Z_e}+B ZeA+B的形式,然后在光栅化时经过glDepthRange的映射,将 [ − 1 , 1 ] [-1,1] [−1,1]的 Z n Z_n Zn映射为 [ 0 , 1 ] [0,1] [0,1]的Z值,这个Z值被写到Z Buffer中。按理说插值Z应该就是用这个将写入Z Buffer的Z值了。但是我在某本书上看到,使用clip space的W值的倒数进行插值。clip space顶点是vertex shader的输出,其顶点的W值就是 − Z e -Z_e −Ze,因此感觉也是挺科学的。具体什么情况,等我弄清楚了再补充。
gluPerspective风格的透视投影矩阵
OpenGL固定流水线的传统函数
void gluPerspective( GLdouble fovy,
GLdouble aspect,
GLdouble zNear,
GLdouble zFar);
这其实是另外一种定义frustum视截体的方式,不同的是这种方式定义的视截体的中心在Z轴,也就是说,glFrustum矩阵中当 l = − r , b = − t l=-r, b=-t l=−r,b=−t时的情况。
fovy为视截体在yz平面上的夹角,aspect为裁剪面的宽高比。因为左右上下对称,因此可知对于glFrustum矩阵中的 l , r , b , t l,r,b,t l,r,b,t, l , b l,b l,b为负值, r , t r,t r,t为正值,因此可计算得到:
t a n ( f o v y / 2 ) = t n tan(fovy/2) = \frac{t}{n} tan(fovy/2)=nt
t = n ∗ t a n ( f o v y / 2 ) t = n*tan(fovy/2) t=n∗tan(fovy/2)
b = − t = − n ∗ t a n ( f o v y / 2 ) b=-t = -n*tan(fovy/2) b=−t=−n∗tan(fovy/2)
r = a s p e c t ∗ t = n ∗ a s p e c t ∗ t a n ( f o v y / 2 ) r = aspect * t = n*aspect*tan(fovy/2) r=aspect∗t=n∗aspect∗tan(fovy/2)
l = − r = − n ∗ a s p e c t ∗ t a n ( f o v y / 2 ) l = -r = -n*aspect*tan(fovy/2) l=−r=−n∗aspect∗tan(fovy/2)
将 l , r , b , t l,r,b,t l,r,b,t代入上面的glFrustum矩阵中,可得gluPerspective矩阵:
P g l u P e r s p e c t i v e = [ 1 a s p e c t ∗ t a n ( f o v y / 2 ) 0 0 0 0 1 t a n ( f o v y / 2 ) 0 0 0 0 − f + n f − n − 2 n f f − n 0 0 − 1 0 ] 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。
目标:将平行投影视图坐标系中的顶点 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)变换到NDC坐标系中的顶点 P n = ( X n , Y n , Z n ) P_n=(X_n,Y_n,Z_n) Pn=(Xn,Yn,Zn)。
约定:
NDC的约定同透视投影,视景体的定义同传统OpenGL函数 g l O r t h o ( l e f t , r i g h t , t o p , b o t t o m . n e a r , f a r ) 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 , f l, r, t, b, n, f l,r,t,b,n,f。这样 ( r , t , − n ) (r,t,-n) (r,t,−n)表示的是近裁剪面的右上角。
推导过程
如上所述,由于平行投影的视景体是一个轴对称六面体,而NDC是一个立方体,也是轴对称的。因此只需要简单的线性映射,即可将视景体中的顶点 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)变换到NDC中的顶点 P n = ( X n , Y n , Z n ) P_n=(X_n,Y_n,Z_n) Pn=(Xn,Yn,Zn)。这只需要先将六面体的长宽高缩放到2,然后将中心点移动到立方体中心即可。
以X坐标为例,我们需要将 X e X_e Xe映射到 X n X_n Xn,其实这和上面透视投影将 X p X_p Xp映射到 X n X_n Xn是一样的,但是之前没有具体推导,一笔带过了。这儿稍微详细推导一下:
由于 X e X_e Xe的范围是 [ l , r ] [l,r] [l,r], X n X_n Xn的范围是 [ − 1 , 1 ] [-1,1] [−1,1],因此通过
1 − ( − 1 ) r − l . X e \frac{1-(-1)}{r-l}.X_e r−l1−(−1).Xe即可把 X e X_e Xe缩放到 [ − 1 , 1 ] [-1,1] [−1,1],然后再进行一个偏移将中心点移动到原点,假设偏移量为 B B B,则可得:
X n = 1 − ( − 1 ) r − l X e + B X_n = \frac{1-(-1)}{r-l}X_e+B Xn=r−l1−(−1)Xe+B
为了计算出 B B B,我们将 X e = r 和 X n = 1 X_e=r和X_n=1 Xe=r和Xn=1带入上式
1 = 2 r − l r + B 1 = \frac{2}{r-l}r+B 1=r−l2r+B,可得
B = − r + l r − l B=-\frac{r+l}{r-l} B=−r−lr+l,将其代入上式,可得:
X n = 2 r − l X e − r + l r − l X_n = \frac{2}{r-l}X_e-\frac{r+l}{r-l} Xn=r−l2Xe−r−lr+l
同样可得
Y n = 2 t − b Y e − t + b t − b Y_n = \frac{2}{t-b}Y_e-\frac{t+b}{t-b} Yn=t−b2Ye−t−bt+b
Z n Z_n Zn的推导过程一样,只是由于 n , f n,f n,f是距离值,因此其坐标表示为 − n , − f -n,-f −n,−f,不失一般性在上图所示的情况下, − f 映 射 到 1 , − n 映 射 到 − 1 -f映射到1,-n映射到-1 −f映射到1,−n映射到−1,因此:
Z n = 1 − ( − 1 ) − f − ( − n ) Z e + B Z_n = \frac{1-(-1)}{-f-(-n)}Z_e+B Zn=−f−(−n)1−(−1)Ze+B
代人 Z n = 1 , Z e = − f Z_n=1, Z_e=-f Zn=1,Ze=−f
1 = 2 n − f ( − f ) + B 1 = \frac{2}{n-f}(-f)+B 1=n−f2(−f)+B,得
B = 2 f n − f + 1 = n + f n − f B = \frac{2f}{n-f}+1=\frac{n+f}{n-f} B=n−f2f+1=n−fn+f,因此:
Z n = 2 n − f Z e + n + f n − f Z_n = \frac{2}{n-f}Z_e+\frac{n+f}{n-f} Zn=n−f2Ze+n−fn+f
Z n = − 2 f − n Z e − f + n f − n Z_n = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n} Zn=f−n−2Ze−f−nf+n
由此,我们得到了 P e P_e Pe到 P n P_n Pn的线性映射关系,我们实际需要的是 P e P_e Pe到 P c P_c Pc的线性关系,因为投影矩阵变换后得到的是Clip Space的顶点。但对于平行投影,w值没有意义,因此可以任意指定,这样我们指定w=1,即可直接将 P c P_c Pc用 P n P_n Pn表示,最终我们得到如下表达式:
X c = 2 r − l X e − r + l r − l X_c = \frac{2}{r-l}X_e-\frac{r+l}{r-l} Xc=r−l2Xe−r−lr+l
Y c = 2 t − b Y e − t + b t − b Y_c= \frac{2}{t-b}Y_e-\frac{t+b}{t-b} Yc=t−b2Ye−t−bt+b
Z c = − 2 f − n Z e − f + n f − n Z_c = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n} Zc=f−n−2Ze−f−nf+n
W c = 1 W_c= 1 Wc=1
以上都是关于 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)的线性函数,可以用矩阵表示为:
P p r o j = [ 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 − 2 f − n − f + n f − n 0 0 0 1 ] 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就是一样的了。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/188592.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...