OpenGL取景變換(視圖變換)矩陣推導

OpenGL取景變換(視圖變換)矩陣推導

標籤(空格分隔): OpenGL VR 遊戲開發


前言

關於取景變換(視圖變換)矩陣的推導本人查過許多資料, 包過關於openGL的和數學方面, 數學方面的資料很嚴謹, 推導過程環環相扣, 但是數書知識畢竟是理論, 怎麼將理論變成實現的代碼, 數學知識是不涉及的. 代碼原理方面的知識只能查找openGL或圖形學相關的資料, 但是這些資料有個缺點就是涉及數學原理的地方不會寫的很清楚, 很多東西都是一筆帶過, 沒有那種環環相扣的嚴謹性, 所以到最後就發現查了很多資料都發現推導過程是脫節的, 代碼不是從數學知識平滑過渡過來的.
本文旨在結合數學知識一步一步的推導出取景變換的變換矩陣, 從最基本的向量相乘到最終的代碼實現.

使用線性變換推導基本座標變換公式

先從簡單的情況入手, 讓兩個座標系的原點相同, 基向量不同, 設世界座標系W{O;i,j,k} , 觀察座標系V{O;u,v,n} , 其中觀察座標系的基向量分別爲 u⃗ =(xu,yu,zu) , v⃗ =(xv,yv,zv) , n⃗ =(xn,yn,zn) , 從而存在:

u=xui+yuj+zukv=xvi+yvj+zvkn=xni+ynj+znk

即:

(u,v,n)=(i,j,k)xuyuzuxvyvzvxnynzn

令:

A=xuyvznxvyvznxnyvzn

則有 V=WA , 由線性代數知識得知, A 稱爲從 WV過渡矩陣. 由上式可以得出 W=VA1 , 即:

(i,j,k)=(u,v,n)A1

稱之爲座標轉換公式.

設世界座標系中W 的一點X0 座標爲(x0,y0,z0) , 現在求這個點在觀察座標系V 中的座標,
由:

WX0=(i,j,k)x0y0z0=(u,v,n)A1x0y0z0

得出X0 在觀察座標系的座標爲A1x0y0z0 , 即:

xuyuzuxvyvzvxnynzn1x0y0z0

由於A(1) , 所以有 A1=AT , 設 B=AT , 則有 W=VB , 其中

B=xuxvxnyuyvynzuzvzn

從而得出X0V 中的座標爲 Bx0y0z0 , 即:

X0=xuxvxnyuyvynzuzvznx0y0z0

引入齊次座標推導最終矩陣

上面只涉及了線性變換部分, 由於平移不能用線性變換來表示, 所以要引入仿射變換, 首先要做的是將上述座標轉換爲齊次座標, 同時將變換矩陣改升級爲4x4的齊次矩陣, X0V 中的座標可以等價的表示爲:

X0=xuxvxn0yuyvyn0zuzvzn00001x0y0z01

而原來的變換矩陣就變成了:

T1=xuxvxn0yuyvyn0zuzvzn00001

有這麼一條理論: 在發生座標系轉換時, 如果已知一個點在老座標系的座標, 需要求這個點在新座標系的座標, 只需要對這個點做 新座標系發生過的變換 的逆過程 (2) .
這個逆過程就是將觀察座標系變換回世界座標系的過程, 這個過程相對應的矩陣就是取景變換矩陣. 對於任意觀察座標系, 都可以通過先平移再旋轉, 從而變換回世界座標系.
設世界座標系中的一點 P=(dx,dy,dz) 爲觀察座標系的原點, 先將觀察座標系的原點移至世界座標的原點, 變換矩陣爲:

T2=100001000010dxdydz1

原點重合後, 再應用上面推導過的線性變換就可以與世界座標系完全重合了, 於是左乘之前的線性變換矩陣 T1 , 就得到最終的變換矩陣 T=T1T2 , 即:

T=xuxvxn0yuyvyn0zuzvzn00001100001000010dxdydz1

結果是:

T=xuxvxn0yuyvyn0zuzvzn0(xudx+yudy+zudz)(xvdx+yvdy+zvdz)(xndx+yndy+zndz)1

即:

T=xuxvxn0yuyvyn0zuzvzn0uPvPnP1

OpenGL取景變換矩陣源代碼解析

首先簡單介紹一下觀察座標系三個基向量的計算, 在代碼中一般會傳入三個信息:
- 觀察點: 就是相機的位置.
- 觀察中心: 就是觀測的中心.
- up向量: 相機向上的大致方向, 是個向量.
利用觀察點和觀察中心就可以算出觀察平面的法向量n(與觀測方向相反), 法向量與up向量叉乘就可以得出第三個向量u.
(由於傳入的up向量不一定是嚴格的向上, 可能是斜的, 所以還要用n叉乘u得出矯正後的up向量v)
觀察座標系

以下是iOS平臺在xcode中看到的OpenGL求取景變換矩陣的代碼:

GLK_INLINE GLKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
                                                  float centerX, float centerY, float centerZ,
                                                  float upX, float upY, float upZ)
{
    GLKVector3 ev = { eyeX, eyeY, eyeZ };   // 觀察點
    GLKVector3 cv = { centerX, centerY, centerZ };  // 觀測中心
    GLKVector3 uv = { upX, upY, upZ };  // up向量, 可以與觀測方向不垂直, 但是不能平行

    // 被觀察平面的法向量, 與觀測方向相反, n對應於OpenGL的z軸
    GLKVector3 n = GLKVector3Normalize(GLKVector3Add(ev, GLKVector3Negate(cv)));
    // 叉乘得到第一根軸, u對應於OpenGL的x軸
    GLKVector3 u = GLKVector3Normalize(GLKVector3CrossProduct(uv, n));
    // 再次叉乘得到矯正後的up向量, v對應於OpenGL的y軸
    GLKVector3 v = GLKVector3CrossProduct(n, u);

    // 採用了列主序儲存(用一個一維數組依次儲存了第一列, 第二列, ...),
    GLKMatrix4 m = { u.v[0], v.v[0], n.v[0], 0.0f,
                     u.v[1], v.v[1], n.v[1], 0.0f,
                     u.v[2], v.v[2], n.v[2], 0.0f,
                     GLKVector3DotProduct(GLKVector3Negate(u), ev),
                     GLKVector3DotProduct(GLKVector3Negate(v), ev),
                     GLKVector3DotProduct(GLKVector3Negate(n), ev),
                     1.0f };

    return m;
}

代碼註釋應該已經很清晰了, 這裏再額外解釋幾個點:
1. GLKVector3Normalize: 功能是把向量歸一化, 就是轉換成單位向量.
2. GLKVector3DotProduct: 求向量的點乘.
3. GLKVector3Negate: 將向量轉成負的.
4. 外部傳入的up向量(upX, upY, upZ)不一定與觀測方向垂直, 所以要通過叉乘算出.

結語

雖然這裏只證明了取景變換的推導過程, 但是這個原理可以推廣到OpenGL中所有的座標變換中去.
在OpenGL裏, 任何變換都可以用一個齊次矩陣表示, 因爲所以的變換都可以分解成多個基本變換(旋轉, 縮放, 平移), 每個基本變換都能用一個齊次矩陣表示, 這些矩陣相乘就得到了最終的變換矩陣.


附錄:

(1) 正交矩陣: 正交矩陣的定義就是A1=AT , 其充要條件是其內部的向量全部爲單位向量, 且兩兩相互垂直.

(2) 逆向變換理論: 其實這條理論在高中學二維座標系轉換就學過了, 只是時間久了就忘了, 但是很容易理解. 空間中有一個座標系I , 對I 做一系列的變換T1,T2,,Tn 變成II , III 中分別有一點AB 的座標爲都爲(a,b) , A 經過一系列變換T1,T2,,Tn 變成I 中一點C , 那麼C 就會與B 重合, 因爲B 跟隨II 做了相同的變換. 相反的, 對B做這一系列變換的逆過程, 即做變換T1n,,T12,T11 , 得到的II 中的一點D會與A完全重合. 所以, 求I 中一點A在II 中的座標就相當於求D 的座標, 而D 座標只需用B 做逆向變換就可以, 又因爲B 座標的數值與A 的相等, 所以只要用A 的座標做逆向變換就可以了.
這樣說可能很繞, 其實可以簡單點理解, 想象座標轉換隻是複製了一個立方體, 然後旋轉平移到另一個地方, 原來立方體裏的所有點都在新立方體裏有一個替身, 欲求老立方體中的一個點在新立法體座標, 只要將它的替身做相逆的變換, 他倆就重合了, 這時替身的座標就是欲求的.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章