大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 7 篇文章。本系列文章從 WebGL 基本運作機制以及使用的原理開始介紹,最後建構出繪製 3D、光影效果之網頁。講完 WebGL 基本機制後,本章節講述的是 texture,如果在閱讀本文時覺得有什麼未知的東西被當成已知的,可能可以在前面的文章中找到相關的內容
繼上篇顯示出繪製出圖片後,不知道有沒有讀者好奇使用自己的圖片試試?如果有,那麼有很高的機率圖片是顯示不出來的,假設換成這張好了:
也就是改這行:
- const image = await loadImage('https://i.imgur.com/ISdY40yh.jpg');
+ const image = await loadImage('https://i.imgur.com/vryPVknh.jpg');
圖片就顯示不出來了,並且可以在 Console 看到 WebGL 的警告:
GL_INVALID_OPERATION: The texture is a non-power-of-two texture.
這張貓圖的解析度是 1024x768,寬是 2 的次方,但是高不是,因此產生了錯誤,為什麼會這樣呢?事實上 WebGL1 的 texture 的取樣預設運作模式以及 gl.generateMipmap(gl.TEXTURE_2D)
製作縮圖功能都只支援寬高皆為 2 次方的圖,在上篇使用的圖片解析度剛好是 1024x1024 所以可以動,這樣的限制可能是效能考量吧,或是說 WebGL1 基於的 openGL 其實已經是蠻古老的了,當時的 GPU 硬體可能只能在 2 次方寬高的圖片上做運算
所以圖片就一定要事先把寬高調整成 2 的次方嗎?其實也不用,無法進行的是『預設運作模式』的 texture 取樣以及 gl.generateMipmap(gl.TEXTURE_2D)
,先把 gl.generateMipmap()
這行註解起來,接著使用 gl.texParameteri()
設定一些參數修改 texture 取樣運作模式:
// gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri
的第二個輸入值表示要設定的參數名稱,第三的輸入值表示要設定的參數值,在 mdn 文件中可以看到有這些可以設定:
gl.TEXTURE_MIN_FILTER
: 顯示大小比原圖小的時候,顯示的策略,可以填入的值:
gl.NEAREST
: 從原圖選擇 1 個 pixelgl.LINEAR
: 從原圖選擇 4 個 pixel 平均gl.NEAREST_MIPMAP_NEAREST
: 從 mipmap 中選最接近的縮圖,再選擇 1 個 pixelgl.LINEAR_MIPMAP_NEAREST
: 從 mipmap 中選最接近的縮圖,再選擇 4 個 pixel 平均gl.NEAREST_MIPMAP_LINEAR
: 從 mipmap 中選最接近的 2 張縮圖,分別選擇 1 個 pixel 平均,此為預設模式
gl.LINEAR_MIPMAP_LINEAR
: 從 mipmap 中選最接近的 2 張縮圖,分別選擇 4 個 pixel 平均gl.TEXTURE_MAG_FILTER
: 顯示大小比原圖大的時候,顯示的策略,可以填入的值:
gl.NEAREST
: 從原圖選擇 1 個 pixelgl.LINEAR
: 從原圖選擇 4 個 pixel 平均,此為預設運作模式
gl.TEXTURE_WRAP_S
/ gl.TEXTURE_WRAP_T
: 對 texture 取樣時,座標超出範圍的行為,_S
_T
分別對 x, y 軸方向進行設定,可以填入的值:
gl.REPEAT
: 重複 pattern,此為預設運作模式
gl.CLAMP_TO_EDGE
: 延伸邊緣顏色gl.MIRRORED_REPEAT
: 重複並鏡像 pattern根據 WebGL 官方 wiki,非 2 次方寬高 texture 只支援:
gl.TEXTURE_MIN_FILTER
為 gl.NEAREST
或 gl.LINEAR
,也因為沒有產生 mipmap,使用這兩個選項才不會用到 mipmap 功能gl.TEXTURE_WRAP_S
/ gl.TEXTURE_WRAP_T
為 gl.CLAMP_TO_EDGE
,為什麼是這樣筆者也不清楚...設定完成之後非 2 次方高的貓圖可以顯示了:
關於 texture 這邊常常提到 WebGL1 只有支援什麼什麼,稍微看了一下 mdn 文件的話可以發現有 WebGL2,而且看起來很多支援會好很多,像是這邊 2 平方寬高的限制就會直接消失,為什麼不改用 WebGL2 呢?因為相容性不夠好,WebGL2 在 2017 年正式推出,現今在 Chrome, Firefox 上都沒問題,但是在 Safari 上還是預設不支援,選用 WebGL2 就表示捨棄 iOS 裝置,因此筆者在撰寫本系列文章這個時間點還是使用 WebGL1 就好
經過筆者測試,iOS 15 beta 版的 Safari 有支援 WebGL2 了,看來不久正式推出後主流瀏覽器就都支援 WebGL2 了,耶!
筆者當時看到 mipmap 相關資料時有個疑惑,明明在 fragement shader 內只是使用
texture2D()
給予取樣的位置,是怎麼知道縮放比例的?查到這篇 stackoverflow 回答,看起來因為 fragment shader 是平行運算的,所以各個鄰近 pixel 運算會同時呼叫texture2D()
,這樣 GPU 就可以知道縮放的比例
gl.TEXTURE_MIN_FILTER
運作模式gl.TEXTURE_MIN_FILTER
可以控制縮小顯示時的方式,而這些顯示方式顯然跟顯示品質、效能有關,這邊先把圖片改回來,並啟用 gl.generateMipmap()
,比較一下 gl.TEXTURE_MIN_FILTER
各個模式的視覺差異:
gl.NEAREST
:
gl.LINEAR
:
gl.NEAREST_MIPMAP_NEAREST
:
gl.LINEAR_MIPMAP_NEAREST
:
gl.NEAREST_MIPMAP_LINEAR
:
gl.LINEAR_MIPMAP_LINEAR
:
使用越多 pixel 做平均的顯示出來的成像就越平滑,但是也比較消耗效能,最後這個 gl.LINEAR_MIPMAP_LINEAR
從 mipmap 中選最接近的 2 張縮圖,分別選擇 4 個 pixel 平均,意思就是一次 texture2D()
得讀取 texture 中 2x4 = 8 個 pixel 出來平均,結果也最平滑
為了同時試玩 gl.TEXTURE_MAG_FILTER
以及重複 pattern 的 texture,接下來繪製看起來像這樣的賽車格紋:
可以看得出來整張圖就是重複這邊紅色框起來的區域:
也就是說,這張圖只需要 2x2 的大小,左上、右下為白色,右上、左下為黑色。事實上,輸入 texture 的 gl.texImage2D()
支援各式各樣的輸入來源,其中一個是 ArrayBufferView
,也就是可以傳(有型別的)陣列資料進去:
const whiteColor = [255, 255, 255, 255];
const blackColor = [0, 0, 0, 255];
gl.texImage2D(
gl.TEXTURE_2D,
0, // level
gl.RGBA, // internalFormat
2, // width
2, // height
0, // border
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type
new Uint8Array([
...whiteColor, ...blackColor,
...blackColor, ...whiteColor,
])
);
因為 type
使用 gl.gl.UNSIGNED_BYTE
,也就是每個 pixel 的每個顏色 channel 為一個 Uint8Array 的元素,白色 RGBA 即為 [255, 255, 255, 255]
、黑色 RGBA 即為 [0, 0, 0, 255]
;另外直接傳陣列進去時,需要額外給的是 width
, height
, border
,這張圖為 2x2 且沒有 border,給予的參數如上所示
為什麼要使用
gl.RGBA
? 因為有個gl.UNPACK_ALIGNMENT
的設定值,這個值預設為 4,表示每行的儲存單位為 4 bytes,如果這樣 2x2 的小 texture 要使用 RGB 就得透過gl.pixelStorei()
改成 1,我們這邊使用gl.RGBA
符合預設值就好
把原本使用外部圖片的 texImage2D()
註解起來,存檔重整可以看到:
現在顯示大小顯然比原圖來的大,所以是 gl.TEXTURE_MAG_FILTER
預設的 gl.LINEAR
導致的平滑效果,但是現在的狀況不想要平滑效果,因此加上這行:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
這張圖顯然不會用到 mipmap,可以把 gl.generateMipmap()
註解起來並使用 gl.TEXTURE_MIN_FILTER => gl.LINEAR
以停用 mipmap:
// gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
接下來讓圖片重複,直接修改 texcoord gl.bufferData()
時傳入的值:
// after gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, 0, // A
8, 0, // B
8, 8, // C
0, 0, // D
8, 8, // E
0, 8, // F
]),
gl.STATIC_DRAW,
);
筆者把 1 改成 8,所以 x, y 軸皆將重複 8 次,我們確實還沒改完,不過可以來看一下 gl.CLAMP_TO_EDGE
的結果:
可以看到黑色的邊緣被延伸到最後,要得到想要的結果得把 gl.TEXTURE_WRAP_S
, TEXTURE_WRAP_T
改成 gl.REPEAT
重複圖樣:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
大功告成:
本篇各個階段的完整程式碼:
gl.LINEAR_MIPMAP_LINEAR
:
介紹 texture 功能至此,之後甚至可以讓 GPU 渲染到 texture 上,有相關需求時再接續討論。到目前為止我們使用 WebGL 網頁讀取完畢只會繪製一次,待下篇來加入控制項接收事件重新渲染畫面,並製作成動畫