iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Modern Web

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

Varying - fragment shader 之資料

大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 5 篇文章。本系列文章從 WebGL 之基礎開始介紹,最後建構出繪製 3D、光影效果之網頁。本章節講述的是 WebGL 基本的運作機制以及如何使用其提供的功能,如果在閱讀本文時覺得有什麼未知的東西被當成已知的,可能可以在前面的文章中找到相關的內容

畫多個三角形

Day 3 畫出三角形時,在 positionBuffer 中傳入了 3 個頂點,每個頂點分別有兩個值 (x, y) 表示座標乘下來共 6 個值,並且在 gl.drawArrays() 的最後一個參數 count 參數傳入 3 表示畫三個頂點;若要畫更多三角形,我想讀者也已經想到,分別在 positionBuffer 傳入更多『組』三角形的每個頂點座標,接著修改 gl.drawArrays()count 即可,筆者直接畫三個三角形:

gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([
    150, 60,
    180, 82.5,
    120, 82.5,

    100, 60,
    80, 40,
    120, 40,

    200, 60,
    180, 40,
    220, 40,
  ]),
  gl.STATIC_DRAW,
);

// ...

gl.drawArrays(gl.TRIANGLES, 0, 9);

看起來像是這樣:

multiple-triangles

這些只是筆者隨便想到的圖案,讀者們可以自行發揮想像力調整頂點座標

筆者當時很好奇:如果 buffer 資料長度不足,或是 count 不是三的倍數,那麼會怎麼樣呢?如果把最後一組頂點刪除(220, 40 那組),count 保持為 9,不僅不會有什麼陣列超出的錯誤,感覺上 vertex attribute array 還給不足的部份填上預設值 0

buffer-not-full

或者把 count 改成 8,也不會有錯誤,只是最後一組三角形沒有完整,兩個點湊不出一個面,因此最後一個三角形就消失了:

draw-count-8

顏色不同的三角形

Day 2 我們實做的 fragment shader 只是純粹把顏色指定上去,所以現在不論畫幾個三角形,顏色都是當初寫死在 fragment shader 中的顏色:

gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);

要讓不同三角形有不同的顏色,要思考的是輸入資料/參數給 fragment shader 的方式,在 fragment shader 中可以使用 uniform,但是那樣的話所有三角形的顏色依然會是一樣,得用類似 attribute / buffer 『每次 shader 呼叫不同』的東西,不過 fragment shader 中是不能使用 attribute 的功能的,回想 Day 2 fragment shader 的運作方式:fragment shader 是每個 pixel 執行一次,不像是 vertex shader 以頂點為單位,取用 array buffer 的方式顯然對不起來,因此需要另外一種傳輸工具 -- varying

Varying

varying 這功能可以讓 vertex shader 輸出資料給 fragment shader 使用,但是兩者執行的回合數顯然是對不起來,假設回到一個低解析度三角形的狀況如下圖,vertex shader 執行三次得到三個頂點,灰色的方格每格執行一次 fragment shader 計算顏色:

vertex-fragment

vertex #1 輸出一組資料、vertex #2 輸出一組資料、vertex #3 輸出一組資料,那麼 fragment #2, fragment #3, fragment #4 這些介於中間 pixel 執行的 fragment shader 會拿到什麼資料?答案是:WebGL 會把頂點與頂點之間輸出的 varying 做平滑化!

假設 vertex #1 輸出 v_number = 0.2、vertex #2 輸出 v_number = 1.1,那麼介於 vertex #1, #2 之間的 fragment #2 將拿到兩個點輸出的中間值,並且越接近某個頂點的 pixel 就會得到越接近該頂點輸出的 varying,筆者畫了一張簡易的示意圖舉例 varying 平滑化的樣子:

varying

這個特性不僅解決問題,也讓筆者覺得相當有意思,有種當初玩 flash 移動補間動畫的感覺

輸入顏色資訊到 varying 給 fragment shader 使用

我們從 fragment shader 開始修改,varying 宣告方式、使用上跟 attribute 差不多,只是把 attribute 改成 varying:

precision mediump float;
varying vec3 v_color;

void main() {
  gl_FragColor = vec4(v_color, 1);
}

輸出顏色從原本寫死的改用一個叫做 v_color 的 varying vec3

這邊還多了一行 precision mediump float;,這是用來設定 shader 要使用多精準的浮點數,如果沒有特別需求使用中等 mediump 就行了

那麼在 vertex shader 得負責輸出這個值,雖然在 vertex shader 這邊 varying 是要輸出,但是寫法一樣是 varying vec3 v_color;:

 attribute vec2 a_position;
+attribute vec3 a_color;
  
 uniform vec2 u_resolution;
+
+varying vec3 v_color;
   
 void main() {
   gl_Position = vec4(
     a_position / u_resolution * vec2(2, -2) + vec2(-1, 1),
     0, 1
   );
+  v_color = a_color;
 }

可以看到筆者加了一個 attribute vec3 a_color,並且直接把 v_color 指定成 a_color 的值,接下來就是重複 Day 3 『畫什麼』的資料輸入、vertex attribute array 等設定:

const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');

// a_position
// ...

// a_color
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);

gl.enableVertexAttribArray(colorAttributeLocation);
gl.vertexAttribPointer(
  colorAttributeLocation,
  3, // size
  gl.UNSIGNED_BYTE, // type
  true, // normalize
  0, // stride
  0, // offset
);

gl.bufferData(
  gl.ARRAY_BUFFER,
  new Uint8Array([
    121, 85, 72,
    121, 85, 72,
    121, 85, 72,

    0, 0, 0,
    255, 255, 255,
    255, 255, 255,

    0, 0, 0,
    255, 255, 255,
    255, 255, 255,
  ]),
  gl.STATIC_DRAW,
);

這段程式碼原理在 Day 3 都有提過,比較需要注意幾點:

  1. gl.vertexAttribPointer() 以及 gl.bufferData() 該行執行的當下要注意 bind 的 ARRAY_BUFFER 是哪個,要不然會對著錯誤的目標做事,當然最好的就是把對於一個 attribute 的操作清楚分好,日後也比較好看出該區域在操作的對象
  2. gl.vertexAttribPointer()size: 3,因為顏色有 3 個 channel: RGB,因此對於每個頂點 gl.bufferData() 要給 3 個值
  3. 筆者在 gl.vertexAttribPointer() 使用 gl.UNSIGNED_BYTE 配合 normalize: true 來使用,在 Day 3 有提到: normalize 配合整數型別時可以把資料除以該型別的最大值使 attribute 變成介於 <= 1 的浮點數,那麼在 gl.bufferData() 時傳入 Uint8Array,並且可以在資料內容寫熟悉的 rgb 值

總結來說資料流如下:

  1. 每個頂點有一組 (x, y) 座標值 a_position 以及顏色資料 a_color
  2. 在 vertex shader 除了計算 clip space 座標外,設定 varying v_color 成為 a_color
  3. 在各個頂點之間 v_color 會平滑化,約接近一個頂點的 pixel v_color 就越接近該頂點當初設定的 a_color
  4. fragment shader 拿到 v_color 並直接輸出該顏色

對於顏色資料的部份,筆者在前三個頂點給一樣的顏色,所以第一個三角形是純色,第二、第三個三角形的第一個頂點為黑色,剩下兩個頂點為白色,因為平滑化的緣故,會得到漸層的效果:

multiple-different-color-triangles

本篇的完整程式碼可以在這邊找到:

筆者認為 WebGL API 最基本的 building block 其實就是 Day 1 到 Day 5 的內容,接下來除了 texture, skybox 之外,幾乎可以說是用這些 building block(搭配線性代數)建構出 3D、光影等效果。下一章就來介紹 texture 並讓 shader 真的開始做一些運算

如果讀者好奇去修改傳入 gl.bufferData() 的資料玩玩的話,應該很快就會發現要自己去對 a_position 的第幾組資料跟 a_color 的第幾組資料是屬於同一個頂點的,他們在程式碼上有點距離,沒有那種 {position: [1,2], color: '#abcdef'} 清楚的感覺,真的要做些應用程式,很快就得自己對這部份做點抽象開始包裝,要不然程式碼一轉眼就會讓人難以摸著頭緒


上一篇
Uniform - shader 之參數
下一篇
在 WebGL 取用、顯示圖片 - Textures
系列文
如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起30

尚未有邦友留言

立即登入留言