在〈貼圖上色〉中暫時被忽略的 generateMipmap
,現在要在這邊認識了,在這之前,要先來談談採樣器,基本上貼圖與採樣器的關係可以比擬為下圖:
右邊採樣器的每個方格,可以看成對應至畫面上的一個像素,當貼圖座標定義的範圍內之像素,與採樣器的像素是一對一,基本上就沒什麼問題,例如上圖中,紅色部份表示正從貼圖採樣出黃色。
然而,實際上多半不會就這麼剛好一對一,貼圖選定的範圍可能會被放大或縮小,例如,若上圖只選擇貼圖的部份範圍,那相當於貼圖被放大,這時採樣器與貼圖的關係會是如下:
這樣就有疑問了,正在採樣的部份涵蓋了黃色與藍色,最後該採樣出哪種顏色?這就要看你的貼圖過濾器(Texture Filter)設定了,當貼圖被放大(Magnification)時,要看 gl.TEXTURE_MAG_FILTER
設定為何,當貼圖被縮小(Minification)時,要看 gl.TEXTURE_MIN_FILTER
設定為何。
如果被設定為 gl.NEAREST
的話,就看貼圖的像素中心哪個最接近取樣的中心,以上圖來說的話,應該是右下的黃色像素,因此取得的會是黃色,這種方式會產生比較多的刷齒;若被設為 gl.LINEAR
的話,會將取樣的中心四個方向的像素值拿來平均,因此這種方式在顏色上會有漸變的效果。
gl.LINEAR
是 gl.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.NEAREST
或 gl.LINEAR
,計算出來的效果並不總是你想要的,這時就會想,我自己來製作各種尺寸的貼圖好了,這麼一來,就可以在不同縮放下選擇適合的貼圖尺寸,以便控制品質。
然而,你不可能製作全部的尺寸,因此大概就是製作個全尺寸、一半、四分之一、八分之一、十六分之一…
這就是 MIPMAP,也就是一系列的縮小圖,如果你的貼圖寬、高各是 2 的次方(不必是正方形),〈貼圖上色〉中談到的 generateMipmap
,可以自動產生 MIPMAP,直到寬、高像素其中之一為 1,最原尺寸的貼圖稱為 Level 0,還記得〈貼圖上色〉中 texImage2D
的第二個參數被指定為 0 嗎?對 generateMipmap
來說,這就是原尺寸貼圖的來源。
這也表示,其他各級尺寸的貼圖也可以自行使用 texImage2D
指定,之後再 generateMipmap
建立 MIPMAP,這對於主要的幾個尺寸的貼圖,在品質上可以自行掌握,指定至 MIPMAP 的貼圖,寬、高也必須是 2 的次方。
然而這也就沿生另一個問題了,如果貼圖被縮放時,不是正好落在你提供的尺寸,怎麼處理呢?這又得回歸到你設定的過濾器為何了。
對於放大超過原尺寸的情況,無從選擇地,只能為 gl.TEXTURE_MAG_FILTER
提供 gl.NEAREST
或 gl.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.NEAREST
或 gl.LINEAR
。
因為 gl.TEXTURE_MIN_FILTER
過濾器的預設值是 gl.NEAREST_MIPMAP_LINEAR
,這表示…
對於寬、高非 2 次方的貼圖,
gl.TEXTURE_MIN_FILTER
一定要改為gl.NEAREST
或gl.LINEAR
,不然就會發生錯誤。
不過,如果你有個非 2 次方寬高的貼圖,就算使用 texParameteri
更改了 gl.TEXTURE_MIN_FILTER
為 gl.NEAREST
或 gl.LINEAR
,也沒辦法繪製出來,這是因為 gl.TEXTURE_WRAP_S
與 gl.TEXTURE_WRAP_T
的預設值都是 gl.REPEAT
,然而,對於非 2 次方寬高的貼圖來說,WebGL 只允許 gl.TEXTURE_WRAP_S
與 gl.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);
}