Vulkan的相機矩陣與投影矩陣

  • 簡介

3D世界中,點是三維的,但是我們的屏幕是二維的,如何將三維的點變換成二維的是圖形學中最重要的一步,也是最基礎(chǔ)的一步。

我們的物體是在世界坐標系的,如果直接變換成屏幕坐標系,那么比較麻煩。我們需要先把點變到相機坐標系(因為相機坐標系轉(zhuǎn)換到屏幕坐標系比較簡單)。然后再把點變換到屏幕坐標系。
相機矩陣跟投影矩陣要配合著一起使用。


OpenGL坐標變換流程圖,引用別人的哈

有關(guān)矩陣的知識的補充:

假如有一個二維點,我們想實現(xiàn)平移,矩陣該是什么樣子的?
無論怎么找,我們都發(fā)現(xiàn)矩陣無法實現(xiàn)平移操作!
而且我們得到的結(jié)果(X,Y)中不可能存在1/Xo或者1/Yo這種結(jié)果
 |X|   =   |A11,A12| x |Xo|
 |Y|       |A21,A22|   |Yo|
于是有人一直鉆研這個問題,終于想出了一個解決辦法,就是上提升一個維度。
|X|    |A11,A12,Xt|     |Xo|    // Xt,Yt是平移的距離
|Y| =  |A21,A22,Yt|  X  |Yo|  
|1|    | 0 , 0 ,1 |     |1 |
并且約定最后最后一個維度為1,這樣矩陣有個特點,
最后一個維度代表了這個向量的整體縮放程度。

OpenGL下的坐標變換

  • 首先我們要講一下相機矩陣。

相機在世界坐標系的位置

物體的繪制在屏幕上的位置取決于我們從哪里看,從正面看跟從背面看得到的是不一樣的形狀。
不管從哪里看我們最終還是要變換到屏幕上。
一般攝像機在原點,并且朝向和頭頂方向跟坐標軸平行才比較好變換到屏幕坐標系上去。

下圖是OpenGL常用的相機坐標系

準備變換到屏幕坐標的相機坐標系和標準化設(shè)備坐標(不要把標準設(shè)備坐標系當作左手坐標系,其實這是個平面,只有X、Y被需要,Z只是輔助深度緩沖用的,用完即丟,而且Z不是和X、Y一樣的線性變換)

我們就拿這個相機坐標系舉例子。

我們需要做的是把世界的點變換到相機坐標系下,其實變換前的點跟變換后的點,都是一樣的,不過在不同的坐標系下面,表示不同,體現(xiàn)在坐標數(shù)值上的變化(可以理解為,你是90后,對于70后來說,你是嫩草,對于10后來說,你是老牛,只不過叫法不同了,你還是那個你)。

即此,我們需要把世界坐標系的點映射到相機坐標系上面去。

在上面的相機坐標系中,相機的位置是原點,Z軸與相機朝向相反,X朝右,Y朝上。

學過線性代數(shù)的也知道,A、B兩個向量 點乘 得到的是A向量在B向量的長度乘上B向量的長度(反過來也成立),那么如果A向量長度為1,那么A、B點乘得到的是B向量映射在A向量的長度,如果我們想知道世界坐標系的點在相機坐標系所對應(yīng)的X,Y,Z值,我們把世界坐標系的點跟相機X、Y、Z軸在世界坐標系的單位向量相乘,就能得到世界點在相機坐標系下的X\Y\Z軸下的長度,因此就能得到對應(yīng)的相機坐標。

假設(shè),我們已經(jīng)求出來X,Y,Z三個單位向量(單位向量指長度為1,也成為歸一化向量),那么我們只需要把點跟這三個向量相乘就能得到新坐標系下的x\y\z值。
因此變換矩陣可以寫成

|X1,X2,X3,0|  // 此矩陣為行優(yōu)先
|Y1,Y2,Y3,0| // OpenGL需要傳入的是列優(yōu)先
|Z1,Z2,Z3,0| // 要么把矩陣轉(zhuǎn)置傳進去
|0 ,0 ,0 ,1| // 要么直接寫列優(yōu)先矩陣

但是我們相機的位置不在原點,也就是說相機坐標系跟世界坐標系的原點不重合,如果不把兩個坐標系的點重合到一起,是求不出正確的結(jié)果的,我們需要先把相機坐標系的點平移到世界坐標系。你可以想象成把相機跟點一起平移了相同的位置,這樣都平移了,這些點的相機坐標系下的表示都沒有變,而且由于世界跟相機坐標原點重合,能夠進行向量坐標映射。
于是我們要先平移后再進行上面坐標映射的矩陣相乘。下面是平移矩陣

// 設(shè)相機的位置為C
|1,0,0,-Cx|  //此矩陣為行優(yōu)先
|0,1,0,-Cy|
|0,0,1,-Cz|
|0,0,0, 1 |

因此返回的矩陣為

|X1,X2,X3,0|           |1,0,0,-Cx|  // 矩陣都是左乘點于點
|Y1,Y2,Y3,0|     X     |0,1,0,-Cy|    
|Z1,Z2,Z3,0|           |0,0,1,-Cz|
|0 ,0 ,0 ,1|           |0,0,0, 1 |

下面貼代碼

// 這是最近寫的js代碼,列優(yōu)先
// OpenGL以前寫過,找不到了,懶得手擼了,因為還要測試Bug
// 這里記住,向量一定要歸一化,要不然得不到正確結(jié)果
// OpenGL用的是右手坐標系
//              ^ Y
//              |
//              |
//              。----------------->  X
//            /
//     Z    / 
//         V
function getMatrix_LookAt(eyePosition,lookDirection,upDirection)
{
      let result=new Matrix4();
      let Y=new Point3();
      let Z=new Point3();
      for(let i=0;i<3;i++)
      {
        Z.data[i]=-lookDirection[i];
        Y.data[i]=upDirection[i];
      }
      Z.normalize();
      let X=cross(Y,Z);
      X.normalize();
      Y=cross(Z,X);
      Y.normalize();
      let matrix_move=getMatrix_translate_xyz(-eyePosition[0],-eyePosition[1],-eyePosition[2]);
      let matrix_transform=new Matrix4();
      matrix_transform.identity();
      for(let i=0;i<3;i++)
      {
        matrix_transform.data[4*i] = X.data[i];
        matrix_transform.data[1+4*i]=Y.data[i];
        matrix_transform.data[2+4*i]=Z.data[i];
      }
      return multiply_matrix(matrix_transform,matrix_move);
}

  • OpenGL下的投影坐標系

OpenGL投影方法為兩種,一個是正交一個是透視,正交投影就是把X,Y,Z老老實實平移到相應(yīng)屏幕點上,在工程制圖上比較常用


正交投影

透視投影就是模擬人眼遠小近大的原理,投射到屏幕上。這里暫時只講透視投影,后面哪天有時間想起來再補充正交,自己可以推導(dǎo)出來,很簡單的。

opengl屏幕坐標系為Y朝上,X朝右。下圖為視錐體的一個側(cè)面圖。


透視投影截面

X,Y,Z為[-1,1]以內(nèi)的才會保留,在外面的會被裁剪掉。我們需要把在視錐體里的點變換到[-1,1]之內(nèi)。

這里先提前聲明一下概念:

Xe : Xeye的縮寫,代表相機坐標系下的X坐標
Ye: 同上
Ze: 同上
Xp: Xprojection的縮寫,代表被線性變換到近平面的X坐標 這時候還沒有被縮放到[-1,1]。
Yp: 同上
Xc: Xclip的縮寫,代表被變換到裁剪坐標的X坐標,Xc的產(chǎn)生完全是因為矩陣沒有辦法直接一步除-Ze的折衷辦法,他在推理上沒必要存在的。在推理上直接一步到Xn.
Yc: 同上
Zc: 同上
Xn: Xndc的縮寫,代表標準設(shè)備坐標系的X坐標
Yn: Yndc的縮寫,代表標準設(shè)備坐標系的Y坐標
Zn: Zndc的縮寫,代表標準設(shè)備坐標系的Z坐標

上面的關(guān)系是:
Xc是Xp縮放到[-1,1]的坐標,投影矩陣包括了將Xe映射到近平面的Xp,然后縮放Xp到Xc的操作
Xc=投影矩陣*Xe
Xn=Xc/Wc (這一步是渲染管線自動算的,我們只需要把-Ze存到Wc分量)
由于后面推導(dǎo)的Xn、Yn都要除-Ze,然而矩陣有個缺點,就是不能進行一次性進行除-Ze操作,所以我們把-Ze放在W向量上,相當于X,Y,Z分量不變的情況下,擴大了W倍,到光柵前,管線會自動進行除W分量操作。

將-Ze放在W分量上

我們無法一次性變換到屏幕坐標系(但是渲染管線自動除W分量),那我們就變換到?jīng)]有除W分量的裁剪坐標系。
因此我們推出最后一行,這樣變換后的向量的W分量就是-Ze:
如果這里不理解不要緊,我講的順序有點問題,先看后面Xn、Yn的推導(dǎo),再來看這里,然后再看Zn的推導(dǎo)。
先求出最后一行

利用相似三角形求出近平面的值

利用相似三角形:
Xp=n*Xe/-Ze
Yp=n*Ye/-Ze
Xp、Yp是在近平面上的坐標但是在[-1,1]范圍外,我們需要把在近平面內(nèi)的映射到[-1,1],因此
Xn=Xp/近平面的長度/2
Yn=Yp/近平面的高度/2

因此,推導(dǎo)出:


       2*n*Xe
Xn=  ——————————
      -Ze*(r-l)


       2*n*Ye
Yn=  ——————————
      -Ze*(t-b)

如果你的視錐體表示方式是Fovy,apect
Fovy指相機垂直方向的角度,aspect指近平面寬高比

          2*Xe
Xn=  ————————————————————
      -Ze*tan(fovy/2)*aspect


          2*Ye
Yn=  ———————————————
      -Ze*tan(fovy/2)

由于我們-Ze是單獨除的,換一種說法是Clip坐標系=NDC坐標系*-Ze;
上面的公式去掉Xe/Ye和-Ze就是系數(shù)。因此我們推導(dǎo)出了前兩行矩陣

|2n/(r-l)       0         0       0|             |Xe|
|     0      2n/(t-b)     0       0|      X      |Ye|
|     0          0        A       B|             |Ze|
|     0          0       -1       0|             |We|  //這里如果正常變換的話We應(yīng)該為1

如果參數(shù)是是Fovy 和Aspect,
矩陣為:
|1/(tan(fovy/2)*aspect)      0           0       0|                |Xe|
|     0                1/tan(fovy/2)     0       0|        X       |Ye|
|     0                      0           A       B|                |Ze|
|     0                      0          -1       0|                |We|   //這里如果正常變換的話We應(yīng)該為1

Z變換肯定和X、Y沒有關(guān)系,因此我們把第三行前兩個系數(shù)設(shè)為0,那么Z的變化就跟后面兩個系數(shù)有關(guān)系,我們設(shè)他們?yōu)锳、B,這是就要解方程了,

Zn=Zc/-Ze // 裁剪坐標系除-Ze才是齊次化標準坐標系
Zc=A*Ze+B // 裁剪坐標系
Zn=(A*Ze+B)/-Ze

我們要把視錐體里的Z映射到[-1,1]

因此
當Ze=-n的時候,Zn=-1;
當Ze=-f的時候,Zn=1;
帶入方程,得:
-1=(A*-n+B)/n
1=(A*-f+B)/f
解方程得:
A=(f+n)/(n-f)
B=2*f*n/(n-f)

因此投影矩陣為:

|2n/(r-l)       0           0              0        |                  |Xe|
|     0      2n/(t-b)       0              0        |         X        |Ye|
|     0          0      (f+n)/(n-f)   2*f*n/(n-f)   |                  |Ze|
|     0          0         -1              0        |                  |We|  //這里如果正常變換的話We應(yīng)該為1

如果參數(shù)是是Fovy 和Aspect,
矩陣為:
|1/(tan(fovy/2)*aspect)      0                0               0        |                 |Xe|
|     0                1/tan(fovy/2)          0               0        |        X        |Ye|
|     0                      0           (f+n)/(n-f)     2*f*n/(n-f)   |                 |Ze|
|     0                      0               -1               0        |                 |We|   //這里如果正常變換的話We應(yīng)該為1

如果之前沒有在W分量上做整體縮放操作的話的上式結(jié)果向量應(yīng)該為
| Xc | 我們把點變換到這里就結(jié)束了,
| Yc | 在vertex shader里面把點變換成左邊的樣子,
| Zc | 然后送入光柵化階段。
|-Ze | OpenGL會在光柵化之前,自己進行除W分量操作,
 把上面的點變換成
|Xc/-Ze|
|Yc/-Ze|
|Zc/-Ze|
|   1  |
此時,這個坐標就是我們要的齊次化設(shè)備標準坐標系
|Xn|
|Yn|
|Zn|
| 1|

注意投影矩陣的Z變換不是線性變換,引用一下別人的圖


Z的非線性變換

上面函數(shù)顯示,Zn在遠處變化較慢,這就說明,如果兩個非常遠的物體深度相近,變換后Z有可能是相同的值,導(dǎo)致Z值沖突。

Vulkan下的坐標變換

Vulkan跟OpenGL用的都是右手坐標系,但是Vulkan的屏幕坐標系的Y軸跟OpenGL相反。

Vulkan的屏幕坐標系

Vulkan的Z值方向大小跟OpenGL一樣。
X/Y范圍都是[-1,1]。
前面我們說過,其實相機坐標系不是固定的,作為渲染API,它只是給你指出他裁剪的標準化屏幕坐標系范圍,你只需要給他變換到對應(yīng)的裁剪坐標系就好,它不關(guān)心你中間用了坐標系。
介于Vulkan的屏幕坐標系Y軸是向下的,那么我們用的相機坐標系跟他相近就好,如下圖所示:


Vulkan下的相機坐標系

然后投影矩陣在上面坐標系的基礎(chǔ)上變換到Vulkan的屏幕坐標系,這里就不推導(dǎo)了,直接上C++代碼,滿足伸手黨。

// RR是行
//CC 是列
// 只有行列都為4的時候本矩陣才有效。
//  本矩陣為行優(yōu)先,傳入Vulkan需要在Vulkan選項或者GLSL里設(shè)置轉(zhuǎn)置
// 本代碼是自己寫的庫的其中一部分
// 完整庫代碼地址:
// https://github.com/kaqima/MyOwnLibrary
// 在Math目錄下的Matrix.hpp頭文件
        template<typename TT=T,unsigned RR=R,unsigned CC=C>
        static enable_if_t<RR == CC && RR == 4, Matrix<TT, RR, CC> > getMatrix_LookAt_Vulkan(const Point<TT,RR-1>& position,const  Point<TT, RR - 1>& direction,const Point<TT, RR - 1>& up)
        {
            Matrix<TT, RR, CC> result;
            result = Matrix<T, RR, CC>::getMatrix_Translate(-position);
            Point<TT, RR-1> Z = Point<TT,RR-1>::normalize( direction);
            Point<TT, RR - 1> X = Point<TT, RR - 1>::multiply_cross(Z, up).normalize();
            Point<TT, RR - 1> Y = Point<TT, RR - 1>::multiply_cross(Z, X).normalize();
            Matrix<TT, RR, CC> left = { Point<TT,RR>(X,0.0f), Point<TT,RR>(Y,0.0f),Point<TT,RR>( Z,0.0f),Point<TT,RR>(0.0f,0.0f,0.0f,1.0f)};
            return left*result;
            
        }

        template<typename TT = T, unsigned RR = R, unsigned CC = C>
        static enable_if_t<RR == CC && RR == 4, Matrix<TT, RR, CC> > getMatrix_Perspective_Vulkan(const T& fov_H,const T& aspect_WdH,const T &zNear,const T &zFar)
        {
            Matrix<TT, RR, CC> result;
            result.identity();
            constexpr double PI = 3.1415926;
            TT a = (fov_H / 180) * PI;
            TT H = tan(a /2.0f);
            result[0][0] = 1 / (H*aspect_WdH);
            result[1][1] = 1 / H;
            result[2][2] = zFar/(zFar-zNear);
            result[2][3] = zFar*zNear/(zNear-zFar);
            result[3][3] = 0;
            result[3][2] = 1;
            return result;
        }


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容