iT邦幫忙

2021 iThome 鐵人賽

DAY 4
1
Modern Web

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

Uniform - shader 之參數

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

使用『畫布中的 x/y pixel 位置』定位

在上一篇雖然把三角形畫出來了,但是在傳入 a_position 時要先算出頂點在 clip space 中 -1 ~ +1 的值,如果要畫更多 2D 三角形,可以用 pixel 為單位直接在畫布上定位會方便許多,本篇就以這個為目標進行修改

Uniform - 設定在 program 上的參數

Uniform 類似 attribute,可以把資料傳到 shader 內,但是使用上比 attribute 簡單許多,因為 uniform 是直接設定在 program 上的,因此不會有各個頂點讀取 buffer 中哪個位置的問題,也是因為這樣,在每個頂點計算的時候 uniform 的值都一樣,所以才叫做 uniform 吧

使三角形的定位使用『畫布中的 pixel 位置』,要先算出頂點在 clip space 中 -1 ~ +1 的值,要做到這件事情當然可以寫一個簡單的 function 在 gl.bufferData() 之前對座標做一些處理,但是這邊是一個很適合使用 uniform 解決的問題;當傳入 positionBuffer 的頂點座標是畫布上 x/y 軸的 pixel 位置,而輸出給 gl_Position 的值必須介於 -1 ~ +1,shader 需要知道的資訊就是畫布寬高,畫布的寬高不論在哪個頂點值都相同,故適合以 uniform 來處理

在 vertex / fragment shader 中可以以這樣的方式宣告 uniform:

uniform vec2 u_resolution;

接著跟 attribute 一樣,先取得變數位置:

const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution');

在呼叫 gl.useProgram() 設定好使用中的 program 之後,像這樣就可以對著使用中的 program 設定 uniform:

gl.uniform2f(resolutionUniformLocation, canvas.width, canvas.height);

canvas 是第一篇 document.getElementById('canvas') 取得的元素,其身上就有 .width, .height 可用

在 Chrome 的 Console 上,輸入 gl.uniform 可以看到有這麼多 function:

uniform-types

這些 uniformXXX 是針對不同型別所使用的,像是筆者上面使用的 uniform2f2f 表示 2 個元素的 float,也就是 vec2,這邊可以看到一路從 1f 單個 float 到 Matrix4f 設定整個 4x4 矩陣都有。除此之外,對於每種資料型別分別還有一個結尾多了 v 的版本 (以 uniform2f 為例:gl.uniform2fv),其實功能沒什麼不同,只是 function 接收參數的方式改變,從 gl.uniform2f(index, x, y) 變成 gl.uniform2fv(index, [x ,y])

當然也得來修改 vertex shader 使用 u_resolution 做轉換:

void main() {
  gl_Position = vec4(
    a_position / u_resolution * vec2(2, -2) + vec2(-1, 1),
    0, 1
  );
}

講解一下:假設寬高是 300x150,一組頂點位置 a_position 為 (150, 90),除以 u_resolution 得到 (0.5, 0.6) 0 ~ 1 之間的位置,最後分別對 x 座標 * 2 - 1、對 y 座標 * -2 + 1 得到 (0.0, -0.2) 給 gl_Position 在 clip space 中的位置。這邊我想讀者會有兩個疑問:

  1. vec2 可以跟 vec2 直接做加減乘除運算?對,相當於每個元素分別做運算,以加法為例像是這樣:vec2(x1, y1) + vec2(x2, y2) = vec2(x1+x2, y1+y2)。筆者看到這樣的寫法第一個瞬間也是『這樣會動?』像 Javascript [1,2] * [3,4] 只會得到 NaN,畢竟一般常見的程式語言的用途比較通用 (general) 不像 GLSL 很常有這樣的運算特化出 vec 之間加減乘除的寫法
  2. 對 x 座標 * 2 - 1,而對 y 座標 * -2 + 1? 因為在 clip space /畫布 中,上方為 y = 1、下方為 y = -1,因此 y 軸正向指著上方的,這個方向和我們在電腦中圖片、網頁的 y 軸方向是相反的,既然要做轉換,那就把這個問題一起修正

傳入的頂點位置 a_position 可以改用 pixel 座標了

筆者用上面的公式拿原本的值做反向運算可以得知在 300x150 的 pixel 座標:

gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([
    150, 60,
    180, 82.5,
    120, 82.5,
  ]),
  gl.STATIC_DRAW,
);

不過看起來沒有任何改變就是了...

使畫布填滿整個畫面

如果讀者使用過 CSS,並且知道 <canvas /> 元素類似 <img />,那麼應該可以想到簡單的這幾行 CSS,筆者直接寫在 HTML 上:

<style>
  html, body {
    margin: 0;
    height: 100%;
  }
  #canvas {
    width: 100%;
    height: 100%;
  }
</style>

但是重整之後看到的只是放大的樣子,就像是把圖片放大的感覺:

scaled-canvas

<canvas /> 元素上有自己的寬高資訊,類似於圖片的原始大小,可以在 Console 上輸入 gl.canvas.width 從 WebGL instance 找回 canvas 元素並取得『原始大小』的寬度:

canvas-width-height

顯然還是原本預設的值,幸好 DOM API 有另外一組提供實際的寬高 .clientWidth, .clientHeight,我們可以直接把 .clientWidth / .clientHeight 設定回這個 canvas 圖片的原始大小:

canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;

// before gl.clearColor(...)

canvas-resized

模糊的現象消失了,看起來實際大小的更動有效,但是那個三角形的位置顯然不太對...

事實上,WebGL 還有一個內部的『繪製區域』設定,因此還需要:

// after canvas.height = canvas.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);

// before gl.clearColor(...)

參數分別為 x, y, width, height,這個 x, y 是指左下角在畫布中的位置,這邊我們要填滿整張畫布,給 0 即可,並把寬高給滿。大家可能會想說,為什麼 WebGL 有內部的『繪製區域』的設定?不知道各位有沒有玩過馬力歐賽車的多人同樂模式,這種在同一個螢幕『分割畫面』的狀況,就可以用 gl.viewport 設定繪製區域為一個玩家繪製畫面,繪製完再呼叫 gl.viewport 繪製另外一位玩家的畫面,WebGL 沒有幫開發者預設使用情境,因此需要自行呼叫 gl.viewport 來修正

result

若在網頁載入渲染完成後調整視窗大小,一樣會發生拉伸的狀況,這時 canvas.width, canvas.height 跟 WebGL 繪製區域都得再進行調整並重新繪製

終於正確了,三角形的頂點位置符合 a_position 傳入的 pixel 座標值,本篇的完整程式碼可以在這邊找到:

畫面上只有一個三角形顯然有點孤單,待下篇來畫多個、顏色不同的三角形


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

尚未有邦友留言

立即登入留言