多個貼圖


在為模型上色時,實際上經常使用多個貼圖,例如,你可能會希望立方體的面上有不同的貼圖,這有幾種方式可以做到,把多個貼圖傳給個別的取樣器,並判斷哪個面使用哪個貼圖?嗯!這會讓著色器變得複雜,而且著色器除錯不易,你應該不會想這麼做。

可以準備多個貼圖,然而著色器中只有一個取樣器,在 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,這是個簡單的濾鏡處理方式。

接下來準備兩個貼圖,並指定給 originalfilter

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.TEXTURE0gl.TEXTURE1gl.TEXTURE2 ~ gl.TEXTURE31 的常數,實際的字面量是個整數,計算上可以遞增 1 來取得下個常數,gl.TEXTURE0 + 1 的話會是 gl.TEXTURE1gl.TEXTURE1 + 1 的話會是 gl.TEXTURE2,依此類推。

在上頭的範例中,tiled 使用的是這張圖片:

多個貼圖

那麼範例網頁完成的效果如下:

多個貼圖