大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 3 篇文章。本系列文章從 WebGL 之基礎開始介紹,最後建構出繪製 3D、光影效果之網頁。本章節講述的是 WebGL 基本的運作機制以及如何使用其提供的功能
在上一篇 WebGL 的繪製流程,同時也建立了 shader 並鍊結成 program,如果有需要可以回到上一篇複習:Day 2: 畫一個三角形(上),而接下來要告訴 GPU 『畫什麼』,精確來說,就是提供上一篇 vertex shader 中 a_position
所需的資料
這個有點指標的感覺,gl.getAttribLocation
可以取得 attribute 在 program 中的位置,同時也把取得的值印出來看看:
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
console.log({ positionAttributeLocation })
// => {positionAttributeLocation: 0}
就是單純的數字,待會這個數字會用來跟 buffer 綁定
找不到的時候這個數字會是
-1
,如果 GLSL 裡面寫了一些沒有被使用到的 attribute 變數,那在 GPU 編譯的過程中會消失,所以就算 GLSL 原始碼有宣告 attribute,有可能因為被判定沒有使用到變數導致gl.getAttribLocation
拿到-1
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
使用 gl.createBuffer()
便可建立 buffer,然後使用 gl.bindBuffer()
『設定目前使用中的 array buffer』;在 WebGL API 中,有許多內部的『對準的目標』(binding point),而看到 bind
的字眼時,他們的功能往往是去設定這些『對準的目標』,設定完成後,接下來呼叫的其他 WebGL API 就會對著設定好的目標做事
除此之外,gl.bindBuffer()
第一個參數傳入了 gl.ARRAY_BUFFER
,在 mdn 上這個參數叫做 target,表示 buffer 不只有一種,而 gl.ARRAY_BUFFER
這種 buffer 才能與 vertex shader 的 attribute 連結,描述這層連結關係的功能叫做 vertex attribute array
首先在 attribute a_position
位置上啟用這個功能:
gl.enableVertexAttribArray(positionAttributeLocation);
啟用之後,設定 attribute 拿資料的方法:
gl.vertexAttribPointer(
positionAttributeLocation,
2, // size
gl.FLOAT, // type
false, // normalize
0, // stride
0, // offset
);
雖然看似沒有提到任何 buffer 的東西,但是經過筆者測試這行執行下去的時候會讓 attribute 設定成與目前『對準的 ARRAY_BUFFER
目標』關聯,同時來看一下各個參數:
index
: 看到傳入 positionAttributeLocation
應該可以猜到,就是要設定的 attribute 位置size
: 筆者認為這是這個 API 最重要的參數,設定了每次 vertex shader 執行時該 attribute 要從 buffer 中拿出多少個數值,依序填入 vecX
的各個元素,這邊使用 2
剛好填滿 shader 中的 attribute vec2 a_position
vec4
且 size 只餵 2 進去也是可以的,剩下的空間 WebGL 會有預設值填上,預設值的部份與之後 3D 相關,之後再來討論type
與第四個參數 normalized
: 設定原始資料與 attribute 的轉換,type
指的是原始資料的型別,此範例直接一點傳入 gl.FLOAT
,而 normalize
在整數型別時可以把資料除以該型別的最大值使 attribute 變成介於 -1 ~ +1 之間的浮點數,此範例不使用此功能傳 false
進去即可
type: gl.UNSIGNED_BYTE
搭配 normalize: true
使用,在 shader 中 attribute 就會直接是符合 gl_FragColor
的顏色資料stride
與第六個參數 offset
: 控制讀取 buffer 時的位置,stride
表示這次與下次 vertex shader 執行時 attribute 讀取的起始位置的距離,設定為 0
表示每份資料是緊密排列的,offset
則是第一份資料距離開始位置的距離,這兩個參數的單位皆為 byte筆者畫了一份示意圖表示這個範例呼叫 gl.vertexAttribPointer()
後 buffer 與 attribute 的運作關係
本範例沒有使用到 stride
與 offset
,既然都有上圖了那就舉個使用 stride
與 offset
的狀況:
在上面已經使用 gl.bindBuffer()
設定好『對準的 ARRAY_BUFFER
目標』,接下來呼叫 gl.bufferData()
對 buffer 輸入資料
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, 0.2,
0.2, -0.1,
-0.2, -0.1,
]),
gl.STATIC_DRAW,
);
第二個參數即為 buffer 的資料,也就是三角形頂點的位置,注意要傳入與 gl.vertexAttribPointer()
type
符合的 TypedArray,關於這些數值:
-1
至 +1
才會落在畫布中-1
, 右為 +1
、y 軸方向,上為 +1
,下為 -1
假設三角形頂點分別依序為 A, B, C,示意圖如下:
gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);
設定使用上篇建立好的 program,接著 gl.drawArrays()
就是『畫』這個動作,其參數功能:
mode
: 透過這個參數可以請 WebGL 畫 點、線,而面的部份就是三角形 gl.TRIANGLES
了first
: 類似上面的 offset,精確來說是『略過多少個頂點』count
: 有多少個頂點,我們畫一個三角形,共三個頂點三角形畫出來了:
到最後一刻才呼叫
gl.useProgram()
,整個資料設定的過程還是透過 attribute 的『位置』設定的,這表示資料在一定程度上可以與 shader 脫鉤,打個比方,同一個 3D 物件可以根據情況使用不同的 shader 來渲染達成不同的效果,但是在記憶體中這個 3D 物件只需要一份就好
三角形的顏色是寫死在 fragment shader 內,讀者們可以試著調顏色、頂點位置玩玩看,本篇的完整程式碼可以在這邊找到:
花了這麼多力氣,就只是一個純色的三角形,而且定位還得先用畫布的 -1 ~ 1
來算,接下來繼續介紹更多 shader 接收資料、參數的方式來使繪製更加靈活