iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Modern Web

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

More About Textures

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

換張圖試試看?

繼上篇顯示出繪製出圖片後,不知道有沒有讀者好奇使用自己的圖片試試?如果有,那麼有很高的機率圖片是顯示不出來的,假設換成這張好了:

another-image

也就是改這行:

-  const image = await loadImage('https://i.imgur.com/ISdY40yh.jpg');
+  const image = await loadImage('https://i.imgur.com/vryPVknh.jpg');

圖片就顯示不出來了,並且可以在 Console 看到 WebGL 的警告:

non-power-of-two

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 個 pixel
    • gl.LINEAR: 從原圖選擇 4 個 pixel 平均
    • gl.NEAREST_MIPMAP_NEAREST: 從 mipmap 中選最接近的縮圖,再選擇 1 個 pixel
    • gl.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 個 pixel
    • gl.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_FILTERgl.NEARESTgl.LINEAR,也因為沒有產生 mipmap,使用這兩個選項才不會用到 mipmap 功能
  • gl.TEXTURE_WRAP_S / gl.TEXTURE_WRAP_Tgl.CLAMP_TO_EDGE,為什麼是這樣筆者也不清楚...

設定完成之後非 2 次方高的貓圖可以顯示了:

cat-is-shown

關於 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.NEAREST

gl.LINEAR:

gl.LINEAR

gl.NEAREST_MIPMAP_NEAREST:

gl.NEAREST_MIPMAP_NEAREST

gl.LINEAR_MIPMAP_NEAREST:

gl.LINEAR_MIPMAP_NEAREST

gl.NEAREST_MIPMAP_LINEAR:

gl.NEAREST_MIPMAP_LINEAR

gl.LINEAR_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,接下來繪製看起來像這樣的賽車格紋:

block-pattern

可以看得出來整張圖就是重複這邊紅色框起來的區域:

repeated-area

也就是說,這張圖只需要 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() 註解起來,存檔重整可以看到:

2x2-block-linear

現在顯示大小顯然比原圖來的大,所以是 gl.TEXTURE_MAG_FILTER 預設的 gl.LINEAR 導致的平滑效果,但是現在的狀況不想要平滑效果,因此加上這行:

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

2x2-block-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 的結果:

8x8-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);

大功告成:

block-pattern

本篇各個階段的完整程式碼:

介紹 texture 功能至此,之後甚至可以讓 GPU 渲染到 texture 上,有相關需求時再接續討論。到目前為止我們使用 WebGL 網頁讀取完畢只會繪製一次,待下篇來加入控制項接收事件重新渲染畫面,並製作成動畫


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

尚未有邦友留言

立即登入留言