webgl 系列 —— 三角形

2023-03-06 15:00:26

三角形

有人說三維模型的基本單元是三角形。比如複雜的遊戲角色,也只是用許多三角形畫出來的。

不管上述說法是否屬實,本篇先把三角形畫出來。

如何繪製一個三角形

滑鼠點選繪點範例我們寫了這樣的程式碼:

points.forEach(item => {
    gl.vertexAttrib3f(a_Position, item.x, item.y, 0.0);
    gl.drawArrays(gl.POINTS, 0, 1);
})

這種方法一次只能繪製一個點。

比如需要繪製一個三角形,應該是一個連貫的動作。比如在頂點著色器中一次性畫三個點,然後用線連線;而不是繪製一個點,在繪製一個點,在繪製一個點...,不應該是零散的

Tip:提前透露 - 只要一次性將三個點繪製出來,其實三角形也就畫出來了。

一次性繪製多個頂點可以使用緩衝區物件

緩衝區物件

可以使用 webgl 中的緩衝區物件(buffer object) 一次性的向著色器傳入多個頂點資料。

使用緩衝區物件向頂點著色器傳入多個頂點資料,需要如下5個步驟:

  1. 建立緩衝區物件
  2. 繫結緩衝區物件
  3. 寫入資料到緩衝區物件
  4. 分配緩衝區物件給一個 attribute 變數
  5. 開啟 attribute 變數

一次繪製三個點

效果

實現

完整程式碼如下:

// 一次繪製三個點

const VSHADER_SOURCE = `
attribute vec4 a_Position;
void main() {
  gl_Position = a_Position;
  gl_PointSize = 10.0;               
}
`

const FSHADER_SOURCE = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`

function main() {
    const canvas = document.getElementById('webgl');
    const gl = canvas.getContext("webgl");
    if (!gl) {
        console.log('Failed to get the rendering context for WebGL');
        return;
    }

    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('Failed to intialize shaders.');
        return;
    }

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 頂點資料
    const vertices = {
        // 頂點資料。Float32Array 當做普通陣列,對應 C 語言的 float
        data: new Float32Array([
            0.0, 0.5,
            -0.5, -0.5,
            0.5, -0.5
        ]),
        // 頂點數
        vertexNumber: 3,
        // 每個頂點分量數,例如第一個點(0.0, 0.5)
        count: 2,
    }

    // 將頂點的位置寫入頂點著色器
    initVertexBuffers(gl, vertices)

    // gl.drawArrays(mode, first, count) 
    // 還是畫點(gl.POINTS),從緩衝區的第一個位置(0)開始畫,繪製3(vertices.vertexNumber)個點
    gl.drawArrays(gl.POINTS, 0, vertices.vertexNumber);
}

// 將三個頂點通過緩衝物件一次寫入著色器
function initVertexBuffers(gl, {data, count}) {
    // 1. 建立緩衝區物件
    const vertexBuffer = gl.createBuffer();
    if (!vertexBuffer) {
        console.log('建立緩衝區物件失敗');
        return -1;
    }

    // 繫結緩衝區物件繫結到`目標`。只能通過目標向緩衝區寫資料
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

    // 將資料寫入緩衝區
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('Failed to get the storage location of a_Position');
        return -1;
    }

    // 將整個緩衝區物件分配給 atttibute(a_Position) 變數
    gl.vertexAttribPointer(a_Position, count, gl.FLOAT, false, 0, 0);

    // 啟用 attribute 變數,使緩衝區對 attribute 變數分配生效
    gl.enableVertexAttribArray(a_Position);
}

通過 initVertexBuffers() 將3個頂點分配給 attribute 變數,執行 gl.drawArrays(gl.POINTS, 0, vertices.vertexNumber) 時,頂點著色器執行了3次。頂點著色器執行過程和緩衝區資料傳入過程如下:

繪製出所有點後,顏色緩衝區的內容就會自動顯示在瀏覽器中。

initVertexBuffers() 裡面涉及了許多概念和api,請往下閱讀。

Float32Array

繪製三維圖形 webgl 需要處理大量相同的資料,例如頂點座標。為了優化效能,就引入了一種特殊的陣列(型別化陣列),瀏覽器事先知道陣列中的資料型別,處理起來就更有效率。

Float32Array 就是其中一種型別化陣列,通常用於儲存頂點座標或顏色。

Tip: webgl 中很多操作都需要用到型別化陣列

webgl 中有如下型別化陣列:

陣列型別 每個元素所佔位元組數 對應C語言的資料型別
Int8Array 1 8 位有符號整數
Uint8Array 1 8 位無符號整型
Int16Array 2 16 位有符號整數
Uint16Array 2 16 位無符號整型
Int32Array 4 32 位有符號的整型
Uint32Array 4 32 位無符號的整型
Float32Array 4 32 位的浮點數 float
Float64Array 8 64 位的浮點數 double

bindBuffer

gl.bindBuffer(target, buffer) - 繫結緩衝區。例如範例中的: gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

為什麼要將繫結緩衝區物件?不能直接向緩衝區寫入資料,只能通過目標寫入資料。就像這樣:

目標表示緩衝區的用途。例如這裡的 gl.ARRAY_BUFFER 指包含頂點屬性(例如頂點座標、紋理座標資料或頂點顏色資料)的緩衝區。

bufferData

gl.bufferData(target, size, usage) - 將資料寫入緩衝區。例如範例中的:gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

gl.STATIC_DRAW 是 usage 中的一種,指緩衝區的內容可能經常使用,而不會經常更改。內容被寫入緩衝區,但不被讀取。

此方法執行後,webgl 系統內部狀態如下:

vertexAttribPointer

g.vertexAttribPointer(index, size, type, normalized, stride, offset) - 將整個緩衝區物件分配給 atttibute 變數。

例如範例中的:

// count - 指定緩衝區每個頂點的分量個數,缺少則按照 vertexAttrib3f 的補全方式
// gl.FLOAT 與上文的 Float32Array 對應
// normalized - 對於型別gl.FLOAT和gl.HALF_FLOAT,此引數無效
// stride - 指定相鄰兩個頂點間的位元組數。預設為0
// offset - 指定緩衝區物件中的偏移量。即attribute 從緩衝區何處開始儲存
gl.vertexAttribPointer(a_Position, count, gl.FLOAT, false, 0, 0);

此方法執行後,webgl 系統內部狀態如下:

疑惑: 說vertexAttribPointer最後一個引數(offset)必須是型別的位元組長度的倍數。嘗試將 0 改成 4,效果卻是:

enableVertexAttribArray

gl.enableVertexAttribArray(index) - 啟用 attribute 變數,使緩衝區對 attribute 變數分配生效。

此方法執行後,webgl 系統內部狀態如下:

drawArrays

gl.drawArrays(mode, first, count) - 執行頂點著色器,按照 mode 指定的引數繪製圖形。first 指定從哪個點開始繪製,count 指繪製需要幾個點。

能繪製的圖形有:


Tip: drawArrays 更多細節請看這裡

三角形

只要一次性將三個點繪製出來,其實三角形也就畫出來了。

修改上面範例一行程式碼就好(gl.POINTS -> gl.LINE_LOOP):

// 前
gl.drawArrays(gl.POINTS, 0, vertices.vertexNumber);

// 後
gl.drawArrays(gl.LINE_LOOP, 0, vertices.vertexNumber);

效果如下:

矩形

效果如下:

核心程式碼如下:

    // 定義四個點
    const vertices = {
        data: new Float32Array([
            -0.5, 0.5,
            -0.5, -0.5,
            0.5, 0.5,
            0.5, -0.5,
        ]),
        // 頂點數
        vertexNumber: 4,
        count: 2,
    }

    initVertexBuffers(gl, vertices)
    // TRIANGLE_STRIP
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.vertexNumber);

Tip:四個點的順序有要求

三角扇

效果如下:

在矩形的基礎上,改為 gl.TRIANGLE_FAN 即可。

Tip:GL_TRIANGLE_FAN - 繪製各三角形形成一個扇形序列,以 v0 為起點:(v0, v1, v2)、(v0, v2, v3)、(v0, v3, v4)