深度測試與面剔除


在〈varying 傳遞著色資訊〉中談到,仔細看範例網頁效果話,會發現應該是看不見的背面也被繪製了。解決的方式之一是,讓較深的點不會繪製,這只要啟用測度測試就可以了:

gl.enable(gl.DEPTH_TEST);           // 啟用深度測試

預設的深度比較函式是 gl.LESS,在輸入值小於深度緩衝中的值時,才會通過進行像素繪製,也就是近物遮擋遠物,如果想要指定其他的深度比較函式,可以使用 depthFunc 函式,例如:

gl.depthFunc(gl.LEQUAL);            // 輸入值小於或等於深度緩衝中的值時才會通過

除了 gl.LESSgl.LEQUAL 之外,depthFunc 中還列出了其他的值,可以自行參考,就正四面體來說,只要使用預設的 gl.LESS 就可以了,例如按一下修改後的範例網頁,可以看到修正後的正確版本。

啟用深度測試之後,在清除繪製時,通常也會同時清除深度緩衝:

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

不過!嗯…因為使用漸層色,某些角度看不時出來是正四面體耶!那麼來試著把四個面分別著上白、紅、綠、藍好了,這該怎麼做?透過程式碼來計算,目前是在畫哪個面嗎?

嗯!這也是個方式啦!不過麻煩也容易出錯,透過索引陣列的方式,可以運用一個簡單的技巧,在談這個技巧之前,先改用索引陣列的方式來畫同一個四面體好了,主要就是索引陣列不要設錯就好了:

gl.enable(gl.DEPTH_TEST);

// 正四面體
const n = 0.25;
const verteices = [
    n, -n, -n,
    -n, -n, n,
    n, n, n,
    -n, n, -n
];

rotateXY(verteices, Math.PI / 3, 0);

// 頂點 Buffer
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verteices), gl.DYNAMIC_DRAW);

// 頂點索引
const indexes = [
    0, 1, 2,
    1, 3, 2,
    0, 2, 3,
    0, 3, 1
];

// 索引 Buffer
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indexes), gl.STATIC_DRAW);

const attr_position = gl.getAttribLocation(prog, 'position');
gl.vertexAttribPointer(attr_position, 3, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_position);

...略

按一下範例網頁來看看效果,就呈現結果而言是相同的,在進一步討論如何把四個面分別著上白、紅、綠、藍之前,先來想一個問題,在頂點索引的配置上,每個三角形的順序應該是順時針還是反時針或者是都可以呢?

如果你不在乎你看到的三角形是正面或反面的問題的話,就不用在意這個問題,例如,若只繪製正四面體的三個面,因為有個面沒畫,可以觀察到正四面體的內側,這時你會希望看到三角形的反面嗎?

就正四面體來說,某個面朝 z 正方面時,一定會被近面遮擋住,雖然啟用深度測試的話,就算畫了朝 z 軸的三角形之反面,近面在繪製時也會遮蓋掉,然而,若可以在一開始,就剔除掉三角形的反面不繪製,在效能上是可以有所助益的。

對 WebGL 來說,三角形頂點順序若是逆時針,對被視為正面,若為順時針就被視為反面,若想要有面剔除的功能,可以撰寫:

gl.enable(gl.CULL_FACE);

將這行程式碼,取代上面範例片段的 gl.enable(gl.DEPTH_TEST),呈現效果上也是相同,就效能上應該會比較好,因為這是個正四面體,而且我的索引陣列是逆時針順序檢索頂點,至今看過的範例也是,在可以的情況下,我會用逆時針來排列頂點。

當然,有些情況下沒辦法,例如〈varying 傳遞著色資訊〉中使用無索引頂點繪製四面體時,因為共用邊的關係,就沒辦法區分三角形正面或反面了。

在啟用面剔除功能後,預設是剔除反面,然而,也可以透過 cullFace 指定 gl.FRONTgl.BACK(預設)或 gl.FRONT_AND_BACK

有時你必須同時啟用深度測試與面剔除,例如繪製多個正四面體的時候,近端的正四面體要遮蓋遠端的正四面體,同時啟用深度測試與面剔除就會很方便。

回過頭來看看如何把四個面分別著上白、紅、綠、藍,目前的顏色資訊,是透過頂點資訊自動計算而來,也就是依索引陣列檢索的頂點值,自動計算出座標轉換為顏色資訊,那麼,如果有個顏色緩衝區,索引陣列檢索出來的三組顏色值都是相同,那麼計算出來的每個內插值也都一樣,不就是繪製出一整個同色的面嗎?

因此,這邊分別為正四面體的各個三角面規畫頂點如下:

// 正四面體
const n = 0.25;
const verteices = [
    n, -n, -n,
    -n, -n, n,
    n, n, n,

    -n, -n, n,
    -n, n, -n,
    n, n, n,

    n, -n, -n,
    n, n, n,
    -n, n, -n,

    n, -n, -n,
    -n, n, -n,
    -n, -n, n
];

檢索這些頂點時,使用的索引陣列為:

const indexes = [
    0, 1, 2,
    3, 4, 5,
    6, 7, 8,
    9, 10, 11
];

要放入顏色 Buffer 的顏色陣列為 colors,它必須也有 12 組值,以配合上頭的索引陣列:

const faceColors = [
    [1.0,  1.0,  1.0,  1.0],    // 白
    [1.0,  0.0,  0.0,  1.0],    // 紅
    [0.0,  1.0,  0.0,  1.0],    // 綠
    [0.0,  0.0,  1.0,  1.0],    // 藍
];

// 每個面會有三個頂點,因此相同顏色設定必須是三個一組
const colors = [];
for(let j = 0; j < faceColors.length; ++j) {
    const c = faceColors[j];
    Array.prototype.push.apply(colors, c.concat(c, c));
}

這麼一來,就可以繪製出四個純色的面了,可以看一下範例網頁的效果,完整的原始碼也請自行從中察看,因為只有一個正四面體,只要啟用面剔除就可以正確繪製了。

深度測試與面剔除