2017.10.16更新,分割線下面是以前的文字,有表達的意思,卻言不達意,實屬羞恥,看官只需看前面文字即可。
twinsen大神的《深入探索透視投影變換》有幾個點說得不夠清晰,我這裡提一下:
1.p'代表的是**面的投影點,p'代表的是p'對映到單位立方體後的投影點;
2.原文:事實上是透視投影變換由兩步組成:
1) 用透視變換矩陣把頂點從視錐體中變換到裁剪空間的cvv中。
2) cvv裁剪完成後進行透視除法(一會進行解釋)。
故,該文求的是第1)步的那個變換矩陣,只不過用到了第2步的資訊來約束,因為這些約束很有用(單位立方體),所以該文先求得透視除法後的座標點p',然後返回來*w求得變換矩陣。這也是該文大量篇幅所做的:在2)的約束下求p'。
3.那p'*w和p'什麼關係?即p'和1)中那個變換矩陣什麼關係?看下圖:
視空間=》p'=》單位立方體空間
視空間=》裁剪空間=》單位立方體空間
p'*w就是裁剪空間的座標,而p'不是,但因為他們都處於中間流程,所以容易混淆!事實上,p'不過是求單位立方空間座標的過渡。
注:頂點shader輸入是模型空間座標,輸出是裁剪空間座標!
4.以上3點很重要,也是原文沒注意到卻影響閱讀效果的細節,那問題來了,為什麼要分1)2)步,不直接拿p'給後續流水線?其他的俺不知,但顯然p'如下格式是不易構造變換矩陣的:
********************===羞恥分割線********************===
透視投影變換
好,有了上面兩個理論知識,我們開始分析這次的主角——透視投影變換。這裡我們選擇opengl的透視投影變換進行分析,其他的apis會存在一些差異,但主體思想是相似的,可以類似地推導。經過相機矩陣的變換,頂點被變換到了相機空間。這個時候的多邊形也許會被視錐體裁剪,但在這個不規則的體中進行裁剪並非那麼容易的事情,所以經過圖形學前輩們的精心分析,裁剪被安排到規則觀察體(canonical view volume, cvv)中進行,cvv是乙個正方體,x, y, z的範圍都是[-1,1],多邊形裁剪就是用這個規則體完成的。所以,事實上是透視投影變換由兩步組成:
1) 用透視變換矩陣把頂點從視錐體中變換到裁剪空間的cvv中。
2) cvv裁剪完成後進行透視除法(一會進行解釋)。
我們一步一步來,我們先從乙個方向考察投影關係。
上圖是右手座標系中頂點在相機空間中的情形。設p(x,z)是經過相機變換之後的點,視錐體由eye——眼睛位置,np——*裁剪*面,fp——遠裁剪*面組成。n是眼睛到*裁剪*面的距離,f是眼睛到遠裁剪*面的距離。投影面可以選擇任何*行於*裁剪*面的*面,這裡我們選擇*裁剪*面作為投影*面。設p』(x』,z』)是投影之後的點,則有z』 = -n。通過相似三角形性質,我們有關係:
同理,有
這樣,我們便得到了p投影後的點p』
從上面可以看出,投影的結果z』始終等於-n,在投影面上。實際上,z』對於投影後的p』已經沒有意義了,這個資訊點已經沒用了。但對於3d圖形管線來說,為了便於進行後面的片元操作,例如z緩衝消隱演算法,有必要把投影之前的z儲存下來,方便後面使用。
假設p'完成透視除法後變成p'',由於各種原因(原文有說明),需要把p''的z'變數變成 -(az+b)/z,即
p' = (x', y', z') = (x', y', -(az+b)/z)
你一定會問為什麼要把z'寫成那樣子
有三個原因:
0)後面投影之後的光柵化階段,要通過x'和y'對z進行線性插值,以求出三角形內部片元的z,進行z緩衝深度測試。在數學上,投影後的x'和y',與z不是線性關係,與1/z才是線性關係。而正是1/z的線性關係,即-a+b/z。用這個1/z的線性組合值和x'、y'進行插值才是正確的。(2023年11月補充條目。對此感到迷惑的讀者可以參考《深入探索透視紋理對映》,裡面從細節上說明了這個問題。)
1) p』的3個代數分量統一地除以分母-z,易於使用齊次座標變為普通座標來完成,使得處理更加一致、高效。
2) 後面的cvv是乙個x',y',z'的範圍都為[-1,1]的規則體,便於進行多邊形裁剪。而我們可以適當的選擇係數a和b,使得這個式子在z = -n的時候值為-1,而在z = -f的時候值為1,從而在z方向上構建cvv。
接下來我們就求出a和b:
但這個時候我們只完成了z'統一到[-1,1],x'和y'還沒完成,這個也同理根據線性差值,由p'和p''得到:
對於p',我們知道-nx / z的有效範圍是投影*面的左邊界值(記為left)和右邊界值(記為right),即[left, right],-ny / z則為[bottom, top]。而現在我們想把把p''的x'束縛在[-1,1]中,即-nx / z屬於[left, right]對映到[-1, 1],-ny / z屬於[bottom, top]對映到y屬於[-1, 1]中。你想到了什麼?哈,就是我們簡單的線性插值,你都已經掌握了!我們解決掉它:
【注,上面我用的是原文的,實際上,nx的x是x,其他單獨的x應該改成x',y同理】
這樣就可以得到p''=(x', y' ,z')了,我們前面說過的透視變換分2步,第一步是用透視矩陣變換,第二步是透視除法,我們得到的p''是透視除法的結果,而我們需要得到的是透視矩陣,所以必須「回退」一下,即p'''是透視矩陣變換的結果:
p''' = p'' * -z(-z才是正的),然後從p'''中拆解出透視矩陣即可p''' = m * (x, y, z,1)t:
m就是最終的透視變換矩陣。相機空間中的頂點,如果在視錐體中,則變換後就在cvv中。如果在視錐體外,變換後就在cvv外。而cvv本身的規則性對於多邊形的裁剪很有利。opengl在構建透視投影矩陣的時候就使用了m的形式。注意到m的最後一行不是(0 0 0 1)而是(0 0 -1 0),因此可以看出透視變換不是一種仿射變換,它是非線性的。另外一點你可能已經想到,對於投影面來說,它的寬和高大多數情況下不同,即寬高比不為1,比如640/480。而cvv的寬高是相同的,即寬高比永遠是1。這就造成了多邊形的失真現象,比如乙個投影面上的正方形在cvv的面上可能變成了乙個長方形。解決這個問題的方法就是在對多變形進行透視變換、裁剪、透視除法之後,在歸一化的裝置座標(normalized device coordinates)上進行的視口(viewport)變換中進行校正,它會把歸一化的頂點之間按照和投影面上相同的比例變換到視口中,從而解除透視投影變換帶來的失真現象。進行校正前提就是要使投影*面的寬高比和視口的寬高比相同。
此外,原文中說cvv是乙個正方體,x, y, z的範圍都是[-1,1],但推導過程中一直以透視除法後的座標為[-1,1]為基準計算的,是不是透視除法後才得到cvv?
總結:本文認為原文這樣的寫法是錯誤的:
,也因此導致了一些疑惑,記此文方便和twinsen交流,如有誤,麻煩指正。
深入探索call apply bind的內部機制
前一篇文章this的情況總結中,我們了解了bind方法的用法,簡單回顧 function call context params1,params2,預先修改this,預先儲存引數,而函式不被立即執行。下面我們就來康康到底是如何實現這個功能的!let obj function func functio...
深入探索C 引用的實現原理
1.引用的概念及用法 引用不是定義乙個新的變數,而是給乙個已經定義的變數重新起乙個別名。定義的格式為 型別 引用變數名 已定義過的變數名 int a 1 int b a 引用的特點 1.乙個變數可取多個別名 2.引用必須初始化 3.引用只能在初始化的時候引用一次,不能改變為再引用其他的變數。可見,a...
C 的黑科技(深入探索C 物件模型)
如何產生乙個不能被繼承的類 這道題我反反覆覆只想到,將父類的建構函式私有,讓子類不能呼叫,最後歸結出乙個單例模式,但面試官說,單例模式作為此題的解答不夠靈活,後來面試官提示說,可以用友元 虛繼承,可以完美實現這樣乙個類 當然那時我還不太明白,友元與虛繼承我都極少接觸過,只是知道有這些東西,回頭搜了一...