在為模型上色時,實際上經常使用多個貼圖,例如,你可能會希望立方體的面上有不同的貼圖,這有幾種方式可以做到,把多個貼圖傳給個別的取樣器,並判斷哪個面使用哪個貼圖?嗯!這會讓著色器變得複雜,而且著色器除錯不易,你應該不會想這麼做。
可以準備多個貼圖,然而著色器中只有一個取樣器,在 JavaScript 要求繪製某個面前,設定取樣器採用哪個貼圖,雖然這個方式在面數龐多時不建議使用,不過接下來會用來它畫個簡單的立方體,順便示範如何替換貼圖,之後文件還會介紹其他方式。
接下來的範例,會使用以下的著色器程式:
<script id="vertex-shader" type="x-shader/x-vertex">
uniform float aspect;
attribute vec3 vertexPosition;
attribute vec2 texturePosition;
varying vec2 vTexturePosition;
void main(void) {
gl_Position = vec4(vertexPosition.x, vertexPosition.y * aspect, vertexPosition.z, 1.0);
vTexturePosition = texturePosition;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D sampler;
varying vec2 vTexturePosition;
void main(void) {
gl_FragColor = texture2D(sampler, vTexturePosition);
}
</script>
貼圖來源通常是圖片,在 WebGL 中要使用多個貼圖,代表要下載多個圖片,圖片必須下載之後才能用來建立、設定為貼圖,這意謂著得在圖片的 load
事件發生之後再來進行這項動作,不過多張圖片代表著,你得在每次的 load
事件的 callback 函式中,再巢狀一次 callback 函式嗎?
callback 地獄不會是個好主意,因此在這邊先使用 Promise
來寫個函式:
function asyncDownloadTexture(src) {
const image = new Image();
return new Promise(resolve => {
image.addEventListener('load', () => {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
resolve(texture);
});
image.src = src;
});
}
這麼一來,在準備貼圖時就簡單多了:
const textures = [
await asyncDownloadTexture('images/caterpillar.jpg'),
await asyncDownloadTexture('images/caterpillar_small.jpg'),
await asyncDownloadTexture('images/book.jpg')
];
接著就是準備立方體頂點、貼圖座標與索引陣列等動作,這就直接看範例網頁吧!
雖然使用多個貼圖,不過著色器中只有一個取樣器,先使用底下的程式碼啟用:
gl.activeTexture(gl.TEXTURE0);
gl.uniform1i(
gl.getUniformLocation(prog, 'sampler'),
0
);
接下來在繪圖時,打算替換貼圖的話,就使用 bindTexture
指定貼圖就可以了:
function drawCube() {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
for(let i = 0; i < verteices.length; i++) {
// 綁定要使用的貼圖
gl.bindTexture(gl.TEXTURE_2D, textures[i % 3]);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(verteices[i]));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
rotateXY(verteices[i], 0, 0.025);
}
requestAnimationFrame(drawCube);
}
drawCube();
可以看看範例網頁,轉動立方體上使用了不同的貼圖:
著色器中也可以有多個取樣器,綁定不同的貼圖,其中一個應用是濾鏡處理,例如,若片段著色器改為底下:
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D original;
uniform sampler2D filter;
varying vec2 vTexturePosition;
void main(void) {
gl_FragColor = texture2D(original, vTexturePosition) * texture2D(filter, vTexturePosition);
}
</script>
texture2D
傳回 Vec4
,在片段著色器中,兩個向量使用 *
表示,對應的分量兩兩相乘,最後也是得到一個 vec4
,因為分量會是 0 ~ 1,若 *
右邊有個分量是白色,相乘表示顏色不變,若是黑色,相乘結果為 0.0,這是個簡單的濾鏡處理方式。
接下來準備兩個貼圖,並指定給 original
與 filter
:
const caterpillar = await asyncDownloadTexture('images/caterpillar.jpg');
const tiled = await asyncDownloadTexture('images/tiled.jpg');
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, caterpillar);
gl.uniform1i(
gl.getUniformLocation(prog, 'original'),
0
);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, tiled);
gl.uniform1i(
gl.getUniformLocation(prog, 'filter'),
1
);
不同的裝置支援的貼圖數量不同,可以使用底下的方式來取得支援的數量:
gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
WebGL 本身則提供了 gl.TEXTURE0
、gl.TEXTURE1
、gl.TEXTURE2
~ gl.TEXTURE31
的常數,實際的字面量是個整數,計算上可以遞增 1 來取得下個常數,gl.TEXTURE0 + 1
的話會是 gl.TEXTURE1
,gl.TEXTURE1 + 1
的話會是 gl.TEXTURE2
,依此類推。
在上頭的範例中,tiled
使用的是這張圖片:
那麼範例網頁完成的效果如下: