在〈使用 Buffer〉中談到,可以在 Buffer 中指定頂點資料,透過 gl.TRIANGLE_STRIP
等模式,以 drawArrays
來繪製頂點,這是無索引頂點的實現。
當然,對於複雜的結構,有序的頂點並不適合,這時可以採用頂點索引,搭配指定的 gl.TRIANGLE_STRIP
等模式,使用另一個陣列來列出頂點順序,並使用 drawElements
來繪圖。
舉例來說,〈使用 Buffer〉中曾經以 gl.TRIANGLE_STRIP
繪製出平行四邊形:
同樣的頂點,若以不同頂點順序繪製,就可以分別得到不同的圖案:
以左上圖為例,可以使用底下的方式來設置:
const verteices = [
0.25, 0.0, 0.0,
0.0, 0.433, 0.0,
-0.25, 0.0, 0.0,
-0.5, 0.433, 0.0
];
// 頂點 Buffer
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verteices), gl.STATIC_DRAW);
// 索引 Buffer
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
// 索引陣列
new Uint8Array([
0, 3, 2,
0, 1, 2
]),
gl.STATIC_DRAW
);
const attr_position = gl.getAttribLocation(prog, 'position');
gl.vertexAttribPointer(attr_position, 3, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_position);
gl.uniform1f(
gl.getUniformLocation(prog, 'aspect'),
gl.canvas.clientWidth / gl.canvas.clientHeight
);
// 繪製
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
主要就是在設置了頂點 Buffer 之後,進行索引 Buffer 的設置,最後使用 drawElements
方法,第一個參數決定了取用索引 Buffer 的方式,以 gl.TRIANGLES
的設置來說,就是每次取三個索引來繪製三角形,第二個參數決定了總共要繪製幾個頂點,第三個參數是索引 Buffer 中的元素型態,由於是索引 Buffer 是 Uint8Array
,因此這邊使用 gl.UNSIGNED_BYTE
,第四個參數是位元組偏移量,表示從哪個位元組開始讀取。
這邊進一步來做個可以按下滑鼠後,切換上圖中兩個圖案的範例,這時因為索引 Buffer 會經常變動,在呼叫 bufferData
時,可以提示 gl.DYNAMIC_DRAW
:
// 索引 Buffer
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indexes), gl.DYNAMIC_DRAW);
這麼一來,之後若想變動 Buffer 內容,可以使用 bufferSubData
方法,這個方法會更新最後一次被 bindBuffer
的 Buffer,例如:
let vi = 0;
gl.canvas.addEventListener('mousedown', () => {
vi = (vi === 0 ? 1 : 0);
indexes_for_swapping[vi].forEach((elem, i) => {
indexes[i] = elem;
});
// 使用 bufferSubData
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, new Uint8Array(indexes));
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
});
你可以試試看範例網頁來看看效果,順便看看其中完整的原始碼。
方才談到,索引陣列的取用方式,是由 gl.TRIANGLE_STRIP
等類型來決定,例如,想要畫出個立方體線框的話:
頂點索引陣列會有許多方式組成,因為只是要畫線框,一個簡單的索引方式是:
這樣就每個邊就都畫到了,這顯然可以使用 gl.LINE_STRIP
,因此索引陣列可以這麼編:
// 立方體八個頂點
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
];
const indexes = [
0, 1, 2, 3,
7, 0, 3, 4,
6, 7, 4, 5,
1, 6, 5, 2
];
如果索引 Buffer 使用 Uint8Array
,在使用 drawElements
繪製時,最後一個參數可以如下指定:
for(let i = 0; i < 4; i++) {
gl.drawElements(gl.LINE_STRIP, 4, gl.UNSIGNED_BYTE, 4 * i);
}
也就是每繪製四個點,就偏移 4 個位元組,作為下一組索引的存取起點,這邊有個範例網頁,可以使用滑鼠左鍵控制是否轉動立方體,為了能看出是個立方體,將立方體進行了旋轉,公式可以參考三維直角座標之繞軸旋轉,實際上,有程式庫可以輔助 WebGL 進行這類任務,像是 glMatrix,這之後再來談。
因為只畫了線框,這就發生了個有趣的現象,你看到的立方體是順時針還是逆時針旋轉呢(順逆是指 Y 軸正方向往下看的話)?可以用大腦控制它的轉動嗎?
真正的答案是順時針轉動,不過你也許會覺得困惑,因為函式是依三維直角座標之繞軸旋轉中的公式實作出來的,然而該文件中是基於逆時針而導出公式的,怎麼在這邊變成了順時針?這是因為該文件中的 Z 軸正方向,與裁剪空間的 Z 軸正方向是相反的,如果你使用該文件中的公式,實際上旋轉動效果都正好相反,也就是效果都會是順時針了。
在動畫處理的部份,使用了 requestAnimationFrame
(而不是使用 setInterval
、setTimeout
等),這個方法會通知瀏覽器,在更新畫面時呼叫指定的函式,好處是瀏覽器可以最佳化,像是綜合考量 CSS 漸變等其他設定來更新畫面,在切換網頁而看不到動畫頁面,自動停止動畫等,詳細實作的方式就直接參考範例網頁內容了。