雖然 WebGL 在裁剪空間上有 z 軸來代表深度,然而為頂點設置不同的深度,只是給 WebGL 進行深度測試以及面裁剪等計算時使用,並不會自動創造出視覺上的遠近感,也就是說,同一個模型只是置放的深度不同時,看到的結果都是相同的。
為了要能夠創造遠近感,在紙面上繪畫時會採用透視法,例如,為描繪的對象找個消失點,邊繪製時往消失點方向進行,這會使得近面看來較大,遠面看來較小:
在紙面上繪畫運用透視技法時,實際上觀察點在哪呢?其實是與消失點相反的方向:
在上圖中,若中間的軸線代表畫面,離觀察點較遠的頂點投射至畫面時,是會低於離觀察點較近的頂點,因此才會造成近面看來較大,遠面看來較小的視覺效果。
因為使用 WebGL 繪圖時,會有實際的頂點資料,計算時就可以基於觀察點,而如同〈正交投影矩陣〉中談到的,可以自定義一個空間,對應至裁空間,以決定哪些東西要畫出來而哪些不要,在自定義空間時,正交投影會是個立方體,然而透視投影時,會是個錐形體,或稱為視體(Viewing frustum):
觀察點的近面距離(near)、遠面距離(far),以及觀察點的視場角(fov)、近面寬高比(aspect),決定了要繪製的範圍,也就是模型必須位於圖中藍色部份,才會被繪製出來,計算時視場角可以是近面上下兩個邊的角度(fovy),也可以是左右兩個邊的角度(fovx),若是以上下兩個邊的角度來計算,並將觀察點放在原點的話:
那麼近面的上(top)、下(bottom)、右(right)、左(left)邊界可以計算得出:
top = near * tan(fovy/2)
bottom = -top
right = top * aspect
left = -right
對於視點中的一個點 (x, y, z),投影在近面上的點位置會是什麼呢?以 y 的計算為例:
顯然地,near / y' = z / y,因此 y'= near * y / z,類似地,也可以求得 x' = near * x / z,只是怎麼用矩陣表示呢?好像沒辦法?
別忘了齊次座標 [x, y, z, w] 轉為直角座標是 [x/w, y/w, z/w],之前我們總令 w 為 1,而矩陣運算的最右下角也總是為 1,若是這麼表示就可以了:
這麼一來,新的齊次座標就會是 [near * x, near * y, z, z],轉為直角座標就是 [near * x / z, near * y / z, 1]。
為了要能令自訂的空間座 x、y 值,可以對應至裁剪空間 -1.0 ~ 1.0 的範圍,先對座標進行縮放,因為觀察點位於原點,sX、sY 縮放比例各為:
sX = 2.0 / (right - left) = 1/right
sY = 2.0 / (top - bottom) = 1/top
也就是可以用矩陣表示為:
我們還沒有將視點中的 z 對應至裁剪空間的 -1 ~ 1,這是線性對應,因此假設對應的公式為 c1/z + c2,目的是希望:
c1/near + c2 = -1
c1/far + c2 = 1
因此:
c1 = 2 * near * far / (near - far)
c2 = (near + far) / (far - near)
同樣地,為了能用矩陣來表示 c1/z + c2,必須運用到齊次座標的 w,因此把 c1/z + c2 變成 c1/z + c2 * (z / z),提出 1/z,也就是變成 (c1 + c2 * z) / z,然後就可以用矩陣來表示了:
這麼一來,計算的結果就會是 [x, y, c1 + z * c2, z],轉換為直角座標的話就會是 [x/z, y/z, c1 /z + c2],既然 x 與 y 的部份已經被除了 z,那麼方才為了將 x、y 對應至 -1 ~ 1 的矩陣就不用使用到 w 了,因此最後的投影矩陣會是:
接下來就是小心計算得到以下的投影矩陣:
最後轉換為程式碼實現,這邊也是加入為 mat4
的方法:
const mat4 = {
...
perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
return [
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, -(near + far) * nf, 1,
0, 0, 2 * near * far * nf, 0
];
}
};
在這邊我製作了個範例網頁,裏頭有個方塊在 XZ 平面上轉動,可以用滑鼠左鍵點選,在正交投影與透視投影之間變換,你可以看看兩者的不同,在正交投影不會有遠近變化,然而透視投影時就很明顯:
最後留下的問題是,如果想要基於 left、right、bottom、top、near, far)而不是 fovy、aspect、near, far),也就是視體的觀察點不是位於原點要怎麼計算呢?導證過程其實與上頭很類似,只不過要先找出近面在 XY 平面上的中心點,這個任務就留給你來嘗試了 :p