1,
We have finally reached the item that represents 3D graphics best - the projection from the 3D world on a 2D plane while maintaining the appearance of depth. A good example is a picture of a road or railway-tracks that seem to converge down to a single point far away in the horizon.
通過在保留物體深度立體感的前提下將3d世界的物體投影到2d平面上最優(yōu)化顯示3D圖形。
2,
對圖形的透視變換需要提供四個參數:
1.屏幕寬高比:矩形屏幕的寬高比例是投影的目標;
2.垂直視野:相機窗口看向3d世界的垂直方向上的角度;
3.Z軸近平面的位置:近平面用于將離相機太近的物體裁剪掉;
4.Z軸遠平面的位置:遠平面用于將離相機太遠的物體裁剪掉;
屏幕寬高比是一個必要的參數,因為我們要在一個寬高相等的單位化的盒子內展示所有的坐標系,而通常屏幕的寬度是大于屏幕的高度的,所以需要在水平方向上的軸線上布置更加密集的坐標點,豎直方向上相對稀疏。這樣經過變換,我們就可以在保證看到更寬闊屏幕圖像的需求下,根據X軸在單位盒子空間內的比例,在X方向上添加更多的X坐標。
垂直視野(The vertical field of view)讓我們可以(放大和縮小zoom in and out)3D世界。
左邊的圖片中相機的視角較大而在屏幕上物體看上去應該會較小;
右邊的視角較小而物體看上去應該會更大。
α > β, L2 > L1
這是由于相機的位置導致的效果,有點違背直覺,(同樣的物體咋就看上去一個大一個小了)。
左邊的相機靠近投影屏幕使視角變大而右邊的相機遠離屏幕視角變小。
然而,在程序中這個現象不會有實際的效果,因為投影的坐標系和屏幕是映射匹配的,相機的位置不會產生任何影響。
3,
投影屏幕是一個和XY平面平行的平面。
整個平面不會都可見,因為它太大了。
我們只能從一個和屏幕有相同的寬高比的矩形區(qū)域(投影窗projection window)來看物體,屏幕寬高比為:
ar = screen width / screen height
如果投影窗高度為2,則寬度為2ar。
所有超出這個矩形(寬[-ar, ar],高[-1, 1])的物體都會被裁剪掉
4,
上面求得3D空間的P點在投影窗上坐標P'
5,
由于我們的屏幕尺寸寬為2*ar,高為2,所以3d世界的點只要X坐標在-ar到ar之間,Y坐標在-1到1之間那么就會在屏幕上有投影點。
這樣在Y分量上我們實際上是單位化的了,但X分量上沒有單位化。
我們可以讓Xp除以屏幕寬高比來將其單位化,單位化后原本X坐標為+ar的就變成+1了。
比如如果投影后X坐標是+0.5,屏幕寬高比是1.333,單位化后的新X坐標變?yōu)?.375??傊?,除以屏幕寬高比進行單位化可以起到在X軸上濃縮更多的坐標點的效果。
6,
在求解最終結果之前讓我們先嘗試看一下這個投影變換矩陣大致應該是什么樣子的。也就是說我們要使用矩陣來表示上面的等式。
那么問題來了,在兩個等式中,我們分別都需要讓X或Y除以Z,
而Z同時也是表示位置的向量的一個分量,Z的值從一個頂點到下個頂點會改變,也就是不是常量,
所以不可能把它放在一個矩陣中來對所有頂點進行變換。
為了好理解我們可以先看變換矩陣頂上第一行的四個分量(a,b,c,d)。我們需要找到一組值使下面的等式成立:
這是第一行的這組值和頂點位置向量的點積,
最后要作為變換后的頂點位置向量中X分量的值。我們可以使'b'和'd'都為0,但是無論'a'和'c'怎么取值也得不到等號右邊的結果。
OpenGL中的解決辦法是將這個變換分解成兩步:先乘以一個投影變換矩陣,然后再單獨除以Z分量的值。
應用中會提供那個投影變換矩陣,shader中要進行頂點和投影變換矩陣相乘的這個步驟,除以Z分量的單獨步驟在GPU中是固定的,而且是在光柵器中進行(在頂點著色器和片段著色器之間的某個地方)。那么GPU怎么知道頂點著色器輸出的哪些頂點需要除以它們的Z值呢?簡單的是,這個會由內置的gl_Position變量來負責實現,不需要我們操心?,F在我們要做的是找到上面只關于X和Y兩個分量的投影變換矩陣。乘以這個投影變換矩陣之后GPU之后會自動幫我們進行Z值得相除變換使我們可以得到我們想要的最終結果。
但是這里還有一個復雜的點是:如果我們將頂點位置和變換矩陣相乘,然后除以Z值我們事實上會丟失Z值,因為每個頂點中Z的值都變成1了。最初的Z值是必須要保存下來的,因為之后要用來進行深度檢測(depth test)。這里的技巧是將原始的Z值保存在結果向量的W分量中,然后只將XYZ除以W分量而不是Z。W保存Z的原始值用于最后的深度檢測。將gl_Position除以W分量的自動步驟稱為'透視分割'?,F在我們就可以創(chuàng)建一個實現上面兩個等式的中間變換矩陣,以及在W分量中保存Z的值:
7,
我們想實現既將Z值單位化,還要使裁剪器更容易的對圖形進行深度裁剪,而不需要知道Near Z和Far Z的值。然而上面的矩陣將Z都變成0了。
向量變換之后系統(tǒng)會自動進行透視分離,我們需要選擇變換矩陣第三行的一組值,使視窗內(比如:NearZ <= Z <= FarZ)Z分量上的分離操作櫻映射到[-1,1]范圍內。
這個映射操作由兩部分組成:首先我們將 [NearZ, FarZ]這個范圍縮放到任意一個寬度為2的范圍內,然后進行平移使其起點從-1開始,也就是[-1,1]的范圍了。對Z進行先縮放然后平移的操作可以用下面的一個通用函數表示:F(z) = A*z + B;
但是之后的透視分離會將等號右側的函數變成:A + B/z;
現在要找出將Z的范圍映射到[-1,1]范圍的A和B的值。特別的我們知道當Z等于NearZ時映射結果為-1,而當Z等于FarZ時映射結果為1
∵ A + B/NearZ = -1
∴ A = -1 -B/NearZ
∵A + B/FarZ = 1
∴ A = 1 -B/ FarZ
∴ -1 -B/NearZ = 1 -B/ FarZ
∴ B*(1/FarZ - 1/NearZ) = 2
∴ B*(NearZ - FarZ) = 2* NearZ* FarZ
∴ B =(2* NearZ* FarZ)/(NearZ - FarZ)
∴ A = -1 -B/NearZ
= -1 - (2* FarZ) /(NearZ - FarZ)
= (-NearZ + FarZ – 2* FarZ)/(NearZ - FarZ)
= -(NearZ + FarZ) /(NearZ - FarZ)
現在我們需要設置變換矩陣的第三行(a,b,c,d)的值滿足下面的等式:
aX + bY + cZ + dW = AZ + B
首先我么立即會將'a'和'b'的值設置為0,因為我們不想X和Y在變換過程中對Z產生影響。然后我們可以讓A作為'c'的值而B作為'd'的值(W已知為1)。因此我們最終的變換矩陣為:
將頂點位置向量和投影變換矩陣相乘之后,坐標系將會變換到裁剪空間中,并且在透視分離之后坐標系會變換到NDC 空間(Normalized Device Coordinates:單位化設備坐標系)中。
之前在沒有投影變換的情況下,我們只能從頂點著色器VS中簡單地輸出XYZ各分量都在[-1,1]范圍內的頂點,保證他們在屏幕當中,并且通過讓W的值為1我們可以防止透視分離產生的影響,然后將坐標變換到屏幕空間就結束了。當使用了投影變換矩陣之后,透視分離就成了3d投射到2d平面的一個集成的部分了。
8,
PersProjTrans = InitPersProjTransform();
m_Wtransformation = TranslationTrans * RotateTrans * ScaleTrans;
m_WPtransformation = PersProjTrans * m_Wtransformation;
gl_Position = gworld * vec4(Position, 1.0);