貼圖過濾與包裹


在〈貼圖上色〉中暫時被忽略的 generateMipmap,現在要在這邊認識了,在這之前,要先來談談採樣器,基本上貼圖與採樣器的關係可以比擬為下圖:

貼圖過濾與包裹

右邊採樣器的每個方格,可以看成對應至畫面上的一個像素,當貼圖座標定義的範圍內之像素,與採樣器的像素是一對一,基本上就沒什麼問題,例如上圖中,紅色部份表示正從貼圖採樣出黃色。

然而,實際上多半不會就這麼剛好一對一,貼圖選定的範圍可能會被放大或縮小,例如,若上圖只選擇貼圖的部份範圍,那相當於貼圖被放大,這時採樣器與貼圖的關係會是如下:

貼圖過濾與包裹

這樣就有疑問了,正在採樣的部份涵蓋了黃色與藍色,最後該採樣出哪種顏色?這就要看你的貼圖過濾器(Texture Filter)設定了,當貼圖被放大(Magnification)時,要看 gl.TEXTURE_MAG_FILTER 設定為何,當貼圖被縮小(Minification)時,要看 gl.TEXTURE_MIN_FILTER 設定為何。

如果被設定為 gl.NEAREST 的話,就看貼圖的像素中心哪個最接近取樣的中心,以上圖來說的話,應該是右下的黃色像素,因此取得的會是黃色,這種方式會產生比較多的刷齒;若被設為 gl.LINEAR 的話,會將取樣的中心四個方向的像素值拿來平均,因此這種方式在顏色上會有漸變的效果。

gl.LINEARgl.TEXTURE_MAG_FILTER 過濾器的預設值,而 gl.TEXTURE_MIN_FILTER 過濾器的預設值是 gl.NEAREST_MIPMAP_LINEAR。可以透過 texParameteri 來設定過濾器,例如:

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

至於什麼是 gl.NEAREST_MIPMAP_LINEAR,在談這個之前,先談什麼是 MIPMAP,因為無論採 gl.NEARESTgl.LINEAR,計算出來的效果並不總是你想要的,這時就會想,我自己來製作各種尺寸的貼圖好了,這麼一來,就可以在不同縮放下選擇適合的貼圖尺寸,以便控制品質。

然而,你不可能製作全部的尺寸,因此大概就是製作個全尺寸、一半、四分之一、八分之一、十六分之一…

貼圖過濾與包裹

這就是 MIPMAP,也就是一系列的縮小圖,如果你的貼圖寬、高各是 2 的次方(不必是正方形),〈貼圖上色〉中談到的 generateMipmap,可以自動產生 MIPMAP,直到寬、高像素其中之一為 1,最原尺寸的貼圖稱為 Level 0,還記得〈貼圖上色〉中 texImage2D 的第二個參數被指定為 0 嗎?對 generateMipmap 來說,這就是原尺寸貼圖的來源。

這也表示,其他各級尺寸的貼圖也可以自行使用 texImage2D 指定,之後再 generateMipmap 建立 MIPMAP,這對於主要的幾個尺寸的貼圖,在品質上可以自行掌握,指定至 MIPMAP 的貼圖,寬、高也必須是 2 的次方

然而這也就沿生另一個問題了,如果貼圖被縮放時,不是正好落在你提供的尺寸,怎麼處理呢?這又得回歸到你設定的過濾器為何了。

對於放大超過原尺寸的情況,無從選擇地,只能為 gl.TEXTURE_MAG_FILTER 提供 gl.NEARESTgl.LINEAR,然而,比原尺寸小的情況,因為會落在 MIPMAP 兩個縮小尺寸的貼圖之間,該選哪個,就看 gl.TEXTURE_MIN_FILTER 是設定底下哪個來決定,當然,這些值也只在有 MIPMAP 時才能指定:

  • gl.NEAREST_MIPMAP_NEAREST:選擇最接近的尺寸,並使用 g.NEAREST 來處理縮放。
  • gl.LINEAR_MIPMAP_NEAREST:選擇最接近的尺寸,並使用 g.LINEAR 來處理縮放。
  • gl.NEAREST_MIPMAP_LINEAR:選擇兩個最接近的尺寸,各使用 g.NEAREST 來處理縮放,然後取兩者的平均值,它是 gl.TEXTURE_MIN_FILTER 預設值。
  • gl.LINEAR_MIPMAP_LINEAR:選擇兩個最接近的尺寸,各使用 g.LINEAR 來處理縮放,然後取兩個的平均值。

因為這些值只在有 MIPMAP 時才能指定,而 MIPMAP 限制只能是寬、高為 2 的次方貼圖,這表示,對於寬、高非 2 次方的貼圖,gl.TEXTURE_MIN_FILTER 就只能設定 gl.NEARESTgl.LINEAR

因為 gl.TEXTURE_MIN_FILTER 過濾器的預設值是 gl.NEAREST_MIPMAP_LINEAR,這表示…

對於寬、高非 2 次方的貼圖,gl.TEXTURE_MIN_FILTER 一定要改為 gl.NEARESTgl.LINEAR,不然就會發生錯誤

不過,如果你有個非 2 次方寬高的貼圖,就算使用 texParameteri 更改了 gl.TEXTURE_MIN_FILTERgl.NEARESTgl.LINEAR,也沒辦法繪製出來,這是因為 gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T 的預設值都是 gl.REPEAT,然而,對於非 2 次方寬高的貼圖來說,WebGL 只允許 gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T 被設定為 gl.CLAMP_TO_EDGE

gl.TEXTURE_WRAP_S 是指貼圖座標 S 方向的包裹模式,gl.TEXTURE_WRAP_T 是指 T 方向的包裹模式,它們在貼圖座標被指定了 0 ~ 1 範圍以外的值時(也就是範圍大於貼圖),應該要有的行為,例如若〈貼圖上色〉中的範例這麼指定貼圖座標:

enableBufferOfAttr(gl, prog, 'texturePosition', 2, [
    0.0,  0.0,
    1.0,  0.0,
    0.0,  1.0,
    0.0,  1.0,
    1.0,  0.0,
    1.0,  1.0
]);

由於 ST 方向預設都是 g.REPEAT,這表示兩個方向都重複貼圖:

貼圖過濾與包裹

可以只設其中一個方向,例如,將 S 方向改為 g.MIRRORED_REPEAT

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);

那麼 S 方向就會自動鏡像重複:

貼圖過濾與包裹

至於 gl.CLAMP_TO_EDGE,0 ~ 1 範圍外的部份,會尋找最接近的有效採樣點,然後在指定的方向繪製,對於邊綠都相同顏色的圖來說,若如下設置:

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

結果感覺就只是一張圖:

貼圖過濾與包裹

如果故意在圖上加點邊框,例如用這張圖:

貼圖過濾與包裹

那麼 ST 方向都 gl.CLAMP_TO_EDGE 的話,就會有以下的效果:

貼圖過濾與包裹

那麼,該是來個總結了,WebGL 希望你可以儘量提供寬高為 2 次方的貼圖,如果沒辦法的話,那麼一定要改變三個值,才能正確繪製:

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);
// 因為無法產生 MINMAP,必須是 gl.LINEAR 或 gl.NEAREST
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

如果提供寬高為 2 次方的貼圖,不呼叫 generateMipmap 情況下,一定要改變 gl.TEXTURE_MIN_FILTER 的值:

// 因為沒有產生 MINMAP,必須是 gl.LINEAR 或 gl.NEAREST
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

如果提供寬高為 2 次方的貼圖,並呼叫 generateMipmap 產生 MINMAP,那設定上就沒有限制了,就看你要什麼效果了。

然而,有時會想要能夠運用隨意尺寸貼圖的場合,怎麼辦呢?這時可以寫個函式來判斷 2 次方:

function isPowerOf2(value) {
  return (value & (value - 1)) == 0;
}

然後如下做分支判斷:

if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
    // 寬高都是 2 次方,產生 MINMAP
    gl.generateMipmap(gl.TEXTURE_2D);
} else {
    // 寬或高不是 2 次方,只能用 gl.CLAMP_TO_EDGE
    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);
    // 因為無法產生 MINMAP,必須是 gl.LINEAR 或 gl.NEAREST
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}