大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 4 篇文章。本系列文章從 WebGL 之基礎開始介紹,最後建構出繪製 3D、光影效果之網頁。本章節講述的是 WebGL 基本的運作機制以及如何使用其提供的功能,如果在閱讀本文時覺得有什麼未知的東西被當成已知的,可能可以在前面的文章中找到相關的內容
在上一篇雖然把三角形畫出來了,但是在傳入 a_position
時要先算出頂點在 clip space 中 -1 ~ +1
的值,如果要畫更多 2D 三角形,可以用 pixel 為單位直接在畫布上定位會方便許多,本篇就以這個為目標進行修改
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:
這些 uniformXXX
是針對不同型別所使用的,像是筆者上面使用的 uniform2f
的 2f
表示 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 中的位置。這邊我想讀者會有兩個疑問:
vec2
可以跟 vec2
直接做加減乘除運算?對,相當於每個元素分別做運算,以加法為例像是這樣:vec2(x1, y1) + vec2(x2, y2) = vec2(x1+x2, y1+y2)
。筆者看到這樣的寫法第一個瞬間也是『這樣會動?』像 Javascript [1,2] * [3,4]
只會得到 NaN
,畢竟一般常見的程式語言的用途比較通用 (general) 不像 GLSL 很常有這樣的運算特化出 vec
之間加減乘除的寫法* 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>
但是重整之後看到的只是放大的樣子,就像是把圖片放大的感覺:
在 <canvas />
元素上有自己的寬高資訊,類似於圖片的原始大小,可以在 Console 上輸入 gl.canvas.width
從 WebGL instance 找回 canvas
元素並取得『原始大小』的寬度:
顯然還是原本預設的值,幸好 DOM API 有另外一組提供實際的寬高 .clientWidth
, .clientHeight
,我們可以直接把 .clientWidth
/ .clientHeight
設定回這個 canvas
圖片的原始大小:
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// before gl.clearColor(...)
模糊的現象消失了,看起來實際大小的更動有效,但是那個三角形的位置顯然不太對...
事實上,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
來修正
若在網頁載入渲染完成後調整視窗大小,一樣會發生拉伸的狀況,這時
canvas.width
,canvas.height
跟 WebGL 繪製區域都得再進行調整並重新繪製
終於正確了,三角形的頂點位置符合 a_position
傳入的 pixel 座標值,本篇的完整程式碼可以在這邊找到:
畫面上只有一個三角形顯然有點孤單,待下篇來畫多個、顏色不同的三角形