索引 Buffer


在〈使用 Buffer〉中談到,可以在 Buffer 中指定頂點資料,透過 gl.TRIANGLE_STRIP 等模式,以 drawArrays 來繪製頂點,這是無索引頂點的實現。

當然,對於複雜的結構,有序的頂點並不適合,這時可以採用頂點索引,搭配指定的 gl.TRIANGLE_STRIP 等模式,使用另一個陣列來列出頂點順序,並使用 drawElements 來繪圖。

舉例來說,〈使用 Buffer〉中曾經以 gl.TRIANGLE_STRIP 繪製出平行四邊形:

索引 Buffer

同樣的頂點,若以不同頂點順序繪製,就可以分別得到不同的圖案:

索引 Buffer

以左上圖為例,可以使用底下的方式來設置:

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 等類型來決定,例如,想要畫出個立方體線框的話:

索引 Buffer

頂點索引陣列會有許多方式組成,因為只是要畫線框,一個簡單的索引方式是:

索引 Buffer

這樣就每個邊就都畫到了,這顯然可以使用 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(而不是使用 setIntervalsetTimeout 等),這個方法會通知瀏覽器,在更新畫面時呼叫指定的函式,好處是瀏覽器可以最佳化,像是綜合考量 CSS 漸變等其他設定來更新畫面,在切換網頁而看不到動畫頁面,自動停止動畫等,詳細實作的方式就直接參考範例網頁內容了。