初探Stage3D(三) 深刻研究透視投影矩陣

關於本文html

 

      本文主要講解從數學的角度如何推導出Stage3D中用到的兩個投影矩陣編程

perspectiveLH緩存

複製代碼
public function perspectiveLH(width:Number,height:Number,zNear:Number,zFar:Number):void
{
   this.copyRawDataFrom(Vector.<Number>([
    2.0 * zNear / width, 0.0, 0.0, 0.0,
    0.0, 2.0 * zNear / height, 0.0, 0.0,
    0.0, 0.0, zFar / (zFar - zNear), 1.0,
    0.0, 0.0, zNear * zFar / (zNear - zFar), 0.0
    ]));
}
複製代碼

perspectiveFieldOfViewLHide

複製代碼
public function perspectiveFieldOfViewLH(fieldOfViewY:Number,aspectRatio:Number,zNear:Number,zFar:Number):void
{
    var yScale:Number = 1.0 / Math.tan(fieldOfViewY / 2.0);
    var xScale:Number = yScale / aspectRatio;
    this.copyRawDataFrom(Vector.<Number>([
        xScale, 0.0, 0.0, 0.0,
        0.0, yScale, 0.0, 0.0,
        0.0, 0.0, zFar / (zFar - zNear), 1.0,
        0.0, 0.0, (zNear * zFar) / (zNear - zFar), 0.0
        ]));
}
複製代碼

參考資料函數

在此感謝Twinsen的幫助和點化 呵呵測試

 

 

 

關於線性插值的解釋this

       Twinsen文章中已經給出了線性插值的公式,包括Wiki裏面也給出了推導。以前對這個公式很困惑,感受似曾相識卻又不是很好理解。當徹底搞透矩陣轉換後,返回來再看線性插值,發現這徹底就是初中時候學的直線方程嘛!spa

線性插值的描述:.net

給一個x屬於[a, b],找到y屬於[c, d],使得x與a的距離比上ab長度所獲得的比例,等於y與c的距離比上cd長度所獲得的比例,用數學表達式描述很容易理解:3d

線性插值

這樣,從a到b的每個點都與c到d上的惟一一個點對應。有一個x,就能夠求得一個y。 
此外,若是x不在[a, b]內,好比x < a或者x > b,則獲得的y也是符合y < c或者y > d,比例仍然不變,插值一樣適用。

這個是直接複製Twinsen上面的原話,如今讓咱們換個角度考慮這句話,看Wiki上面這張圖

線性插值圖2

其中x0,x1就是對應的a,b. y0,y1就是對應的c,d。換成咱們比較熟悉的表述方法就是,已知直線上的兩點求該直線方程。這樣你們就明白應該怎麼作了吧

 

 

 

 

線性插值的做用

 

     在矩陣推導過程當中線性插值有何用處?其實線性插值解決的就是一個壓縮座標點區域的做用,好比p點屬於區域A(0,600),但願將其壓縮成區域B(0,400).使得在區域B中的p’和區域A中的p存在一一對應的關係。

線性插值3

解決方式就是畫圖,注意橫縱座標的名稱,分別爲壓縮前的區域壓縮後的區域。而那條紅線的數學表示方法也就是線性插值

線性插值

 

 

二維平面上面的投影

 

     首先讓咱們拋開z值,先推導x,y方向上面的投影 (具體的推導過程請參考Twinsen的文章)

x和y兩個方向的最終投影爲

x和y

換句話表述就是,根據公式 x'=N*(x/z) 也就能夠獲得世界座標系中的x點在投影平面上面的x’點。

 

 

 

 

關於CCV(Canonical View Volume)和NDC(Normalized Device Coordinates)

 

      在Adobe的Working with Stage3D and perspective projection文章中,介紹過NDC的概念。文章在介紹該概念的那段最後一句話提到

Stage3D and the GPU use the data(此時data指的是NDC後的頂點數據) from the output of your Shader in clip space form to carry on internally with the perspective divide.

也就是在後續的渲染管道中Stage3D和GPU都認爲你已經經過矩陣轉化將x,y座標(目前先不討論z),轉化到了(-1,1)這個範圍內。

回頭再來看以前推導出的x'點(x'=N*(x/z)),該點的區域在沒壓縮前是投影面的(left,right),

投影體

而咱們須要作的也就是將這個區域A(left,right)壓縮到區域(-1,1)

具體的數學推導能夠看Twinsen的文章,經過推導得出特殊形式下(投影平面的中心和x-y平面的中心重合) 新的x,y座標方程爲

特殊方程形式

此時 right-left 也就是width ,top-bottom 就是height.

再來看perspectiveLH中前兩行裏面不爲0的值,就是推導公式中的結果,其中Twinsen公式中的N對應的是perspectiveLH公式中的zNear

複製代碼
public function perspectiveLH(width:Number,height:Number,zNear:Number,zFar:Number):void
{
   this.copyRawDataFrom(Vector.<Number>([
    2.0 * zNear / width, 0.0, 0.0, 0.0,
    0.0, 2.0 * zNear / height, 0.0, 0.0,
    0.0, 0.0, zFar / (zFar - zNear), 1.0,
    0.0, 0.0, zNear * zFar / (zNear - zFar), 0.0
    ]));
}
複製代碼

注意投影平面的left,right可能爲任何值,不必定非要爲-1,1。只有在壓縮後新的x'值的取值範圍才從left,right壓縮到了(-1,1)

 

 

 

 

關於width 和 height 取值的進一步說明

 

        由於Shader最後須要將點範圍縮至(-1,1)這個範圍,因此perspectiveLH中的width參數也就必定爲1-(-1)=2,可是height參數爲2麼?這個就要看你屏幕區域是否爲正方形。

context3D.configureBackBuffer(500, 500, 1, true); //方形區域
context3D.configureBackBuffer(800, 600, 1, true); //普屏4:3
context3D.configureBackBuffer(1440, 900, 1, true); //寬屏16:9

在Stage3D被使用時候都須要設置背景緩衝區大小,這個函數同時也就設置了寬高比.

我之前錯誤的覺得Stage3D會很智能,在configureBackBuffer函數中設置好寬高比後,代碼中用到的投影函數都應該把x,y方向轉化成(-1,1)範圍內。後續模塊處理完這些數據時候,光柵化時候自動根據寬高比顯示在屏幕上。

後來作了一個小Demo測試發現,Stage3D並無處理這件事情。也就是須要使用者本身處理這個問題。

因此,若是你選擇使用perspectiveLH做爲投影矩陣,configureBackBuffer時候選擇的寬高比是4:3(不用關心具體的像素值,只關心比例),

那麼真正的height區域就應該是(-1/aspectRatio,1/aspectRatio). 其中aspectRatio=4/3

 

 

 

 

關於Z值

 

      當理解了x,y如何轉換爲x'和y'而且完成了線性插值,如今可讓咱們來看看關於z值的問題了, 這個問題我琢磨了好久,可是到目前爲止仍是有些疑惑。

整體來講,對於透視投影這件事,實際上是不須要z值。由於最終已經投影到了一個平面上面,一個平面只關心x,y兩個座標。對於z值,實際上是可有可無的。那什麼地方須要用到?

1·排序

2·紋理映射

關於這些又是另外兩個比較大塊的區域,我查閱了一些資料但理解的仍是不太透徹。能夠得出的大概思想是

在世界座標系中可使用線性插值,可是因爲作了投影計算,若是對變換後的點z'(假設就是直接複製z值的變換),使用線性插值其結果是錯誤的

錯誤線性插值

能夠認爲圖中藍色區域的線段爲投影后的區域,紅色爲投影前,藍色區域每一小塊是相等的,可是紅色區對應卻不相等。

這個問題同時會影響排序,紋理映射,因此要選擇1/z 而不是直接使用z值。但使用1/z 並非直接把原來的z值變成1/z而是

要變成z= a(1/z’)+b 這種形式,也就是通常的直線方程形式。

關於爲什麼要使用1/z 而不是使用z值 能夠參考Twinsen的另外兩篇文章 深刻探索透視紋理映射(,),還有 《3D遊戲大師編程技巧》書中的第11章 深度緩存和可見性

關於第二個問題,爲什麼不直接使用1/z 而要用 a(1/z')+b的形式,我仍是沒太明白,因此就不作共多介紹了,若是有人比較懂的話,麻煩告訴我一聲 謝謝。

 

若是能夠理解爲什麼將z變成 a(1/z')+b這件事以後,後續的數學推論Twinsen的文章中已經寫得很是清楚了,大致思想就是同x,y同樣。將變換後的新z值也線性插值到(-1,1) 或者(0,1) 空間內

 

 

 

 

如何理解perspectiveFieldOfViewLH裏面的矩陣

 

perspectiveFieldOfViewLH和perspectiveLH實際上是同一個東西,要理解這個問題咱們還要看一下以下兩張圖

投影原理

首先先看第一張圖,如何肯定p'的座標,實際上是看是看np平面的位置,和其餘變量沒有任何關係。也就是np平面越靠近fp平面,p'點的位置就越往外。

或者換句話說p'點的x值越大,顯示在平面上則該圖形最大。

另外還有一個已知條件就是np平面的寬度爲1-(-1)=2.

投影示意圖

而後再讓咱們來看第二張圖,黃線和綠線對應的就是np平面。一種方式是給定zNear值,也就是直接指定該平面距離原點的偏移值。

另一種方式是指定角度FOV(Field Of View)也就是指定黑色或者藍色那個角度有多少

而後 根據三角函數能夠獲得 1/zNear= Math.tan(fieldOfViewY / 2.0);  

其中 1/zNear中的1 是由於黃線或者綠線的總寬度爲2,一半就是1。fieldOfViewY/2 就是一半的角度.

此時讓咱們回過頭再來看perspectiveFieldOfViewLH中的矩陣

複製代碼
public function perspectiveFieldOfViewLH(fieldOfViewY:Number,aspectRatio:Number,zNear:Number,zFar:Number):void
{
       var yScale:Number = 1.0 / Math.tan(fieldOfViewY / 2.0);
    var xScale:Number = yScale / aspectRatio;
    this.copyRawDataFrom(Vector.<Number>([
        xScale, 0.0, 0.0, 0.0,
        0.0, yScale, 0.0, 0.0,
        0.0, 0.0, zFar / (zFar - zNear), 1.0,
        0.0, 0.0, (zNear * zFar) / (zNear - zFar), 0.0
        ]));
}
複製代碼

其中yScale也就是上圖解釋的問題,而xScale是根據寬高比得出來的,詳情能夠參考 關於width 和 height 取值的進一步說明 這一小節的內容。

 

 

 

 

總結

      到此我應該已經闡述清楚了perspectiveLH和perspectiveFieldOfViewLH是如何推導出來的,具體推導過程還請參考Twinsen的兩篇文章。我只是換個角度闡述了一下對兩個公式的理解,在此感謝Twinsen對我以前問題熱心的解答,謝謝。

 

 

 

關於Stage3D渲染管道的一點疑惑

 

       目前我對Stage3D的渲染管道仍是有些疑惑的,Stage3D和GPU到底作了什麼又沒作什麼?根據我對3D渲染管道的理解,要渲染一個物體,首先須要將物體從自身的座標系轉化到世界座標系,

而後再進行攝像頭的旋轉平移等操做,這些操做完成後應該作一系列測試以便剔除沒有必要的圖形。

好比若是一個世界上面有1000個圖形,先根據可見度(BPS樹?)剔除大部分物體,而後對剩餘物體作AABB測試,保留那些所有或者一部分在視景體內部的物體。對剩餘物體進行背面消除,而後在對部分在視口內部的物體進行3D剪裁。

將最終的頂點結果傳入GPU,進行後續的處理(打光和加入紋理),最後光柵化到屏幕上。

Stage3D渲染流程

上面那張圖就是Stage3D的渲染管道示意圖,我原本覺得Stage3D會很「聰明」,在Vertex Shader中的矩陣(也就是上面文章在介紹的,注:此矩陣僅包含投影,不是最終使用的矩陣),只要將全部座標點所有轉換爲NDC形式的,Stage3D會作

剩下全部的操做,其中包括:

1·剔除不在屏幕上面顯示的

2·自動將屏幕縮放爲正常尺寸(詳情見 關於width 和 height 取值的進一步說明 小節)

 

第二點根據測試,我發現Stage3D沒有這麼作,因此也就是須要編程者本身考慮這個問題。若是是這樣將全部座標縮放到NDC後其實Stage3D應該僅僅作了「渲染」這件事,至於和剔除相關的,也就是第一點,Stage3D應該沒有涉及。

可是比較疑惑的是,上圖中明顯包含一個 「Viewport clipping」模塊,這個模塊的目的是進行2D剪裁麼?而且Context3D中有setCulling這個函數,也就是說Stage3D能夠根據設置參數進行背面消除。

這讓我對Stage3D到底作了什麼,沒有作什麼非常困惑。

 

可不能夠這麼說:若是咱們須要作一個3D引擎,那引擎部分須要涉及到的部分有

1·根據BSP樹進行剔除(剔除根本不可見的物體,好比一面牆後面的物體,或者說本身視角後方的物體)

2·進行AABB測試(剔除那些不在視景體內部的物體)

3·背面消除(由Stage3D處理,引擎不須要考慮)

4·3D剪裁(Stage3D會作2D剪裁,引擎也不須要考慮。 可是那些z值小於zNear的點怎麼辦?)

 

不知道我理解的是否正確?但願獲得高手的解答,謝謝

相關文章
相關標籤/搜索