iT邦幫忙

2021 iThome 鐵人賽

DAY 3
0
Modern Web

如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起系列 第 3

畫一個三角形(下)

大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 3 篇文章。本系列文章從 WebGL 之基礎開始介紹,最後建構出繪製 3D、光影效果之網頁。本章節講述的是 WebGL 基本的運作機制以及如何使用其提供的功能

在上一篇 WebGL 的繪製流程,同時也建立了 shader 並鍊結成 program,如果有需要可以回到上一篇複習:Day 2: 畫一個三角形(上),而接下來要告訴 GPU 『畫什麼』,精確來說,就是提供上一篇 vertex shader 中 a_position 所需的資料

取得 Attribute 位置

這個有點指標的感覺,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

建立並使用 Buffer

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

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
    • 事實上,就算 attribute 是 vec4 且 size 只餵 2 進去也是可以的,剩下的空間 WebGL 會有預設值填上,預設值的部份與之後 3D 相關,之後再來討論
  • 第三個參數 type 與第四個參數 normalized: 設定原始資料與 attribute 的轉換,type 指的是原始資料的型別,此範例直接一點傳入 gl.FLOAT ,而 normalize 在整數型別時可以把資料除以該型別的最大值使 attribute 變成介於 -1 ~ +1 之間的浮點數,此範例不使用此功能傳 false 進去即可
    • 假設今天原始資料是 0~255 整數表示的 RGB,那麼就可以用 type: gl.UNSIGNED_BYTE 搭配 normalize: true 使用,在 shader 中 attribute 就會直接是符合 gl_FragColor 的顏色資料
  • 第五個參數 stride 與第六個參數 offset: 控制讀取 buffer 時的位置,stride 表示這次與下次 vertex shader 執行時 attribute 讀取的起始位置的距離,設定為 0 表示每份資料是緊密排列的,offset 則是第一份資料距離開始位置的距離,這兩個參數的單位皆為 byte

筆者畫了一份示意圖表示這個範例呼叫 gl.vertexAttribPointer() 後 buffer 與 attribute 的運作關係

01-vertex-attrib-pointer

本範例沒有使用到 strideoffset,既然都有上圖了那就舉個使用 strideoffset 的狀況:

02-vertex-attrib-pointer

傳入資料到 buffer,設定三角形的位置

在上面已經使用 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. 在上篇有提到:x, y, z 必須介於 -1+1 才會落在畫布中
  2. 在 x 軸方向,左為 -1, 右為 +1、y 軸方向,上為 +1,下為 -1

假設三角形頂點分別依序為 A, B, C,示意圖如下:

day3-03-triangle-vertice

終於,『畫』這個動作

gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);

設定使用上篇建立好的 program,接著 gl.drawArrays() 就是『畫』這個動作,其參數功能:

  • 第一個參數 mode: 透過這個參數可以請 WebGL 畫 點、線,而面的部份就是三角形 gl.TRIANGLES
  • 第二個參數 first: 類似上面的 offset,精確來說是『略過多少個頂點』
  • 第三個參數 count: 有多少個頂點,我們畫一個三角形,共三個頂點

三角形畫出來了:

result

到最後一刻才呼叫 gl.useProgram(),整個資料設定的過程還是透過 attribute 的『位置』設定的,這表示資料在一定程度上可以與 shader 脫鉤,打個比方,同一個 3D 物件可以根據情況使用不同的 shader 來渲染達成不同的效果,但是在記憶體中這個 3D 物件只需要一份就好

三角形的顏色是寫死在 fragment shader 內,讀者們可以試著調顏色、頂點位置玩玩看,本篇的完整程式碼可以在這邊找到:

花了這麼多力氣,就只是一個純色的三角形,而且定位還得先用畫布的 -1 ~ 1 來算,接下來繼續介紹更多 shader 接收資料、參數的方式來使繪製更加靈活


上一篇
畫一個三角形(上)
下一篇
Uniform - shader 之參數
系列文
如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起30

尚未有邦友留言

立即登入留言