貼圖上色


在〈貼圖座標〉中談到,貼圖是上色時採樣的依據,這表示著色器的撰寫上,必須傳入貼圖座標:

<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(uSampler, vTexturePosition);
    }
</script>  

在頂點著色器中,傳入的貼圖座標 texturePosition 指定給 vTexturePosition,因此片段著色器中就可以利用貼圖座標來上色,貼圖本身的資訊會設定給 sampler,型態為 sampler2D,顯然地,採樣的來源是個 2D 圖片,texture2D 函式會從指定的採樣器中依座標取得顏色資訊作為像素的顏色。

為了能在圖片下載後繪製,這邊將程式寫在 load 事件之中:

// 共用的 Buffer 設置函式
function enableBufferOfAttr(gl, prog, attrName, size, data) {
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
    const position = gl.getAttribLocation(prog, attrName);
    gl.vertexAttribPointer(position, size, gl.FLOAT, false, 0 , 0);
    gl.enableVertexAttribArray(position);
}

const gl = getGLContext(document.getElementById('glCanvas'));
const prog = installProgram(gl, 
    shaderSourceById('vertex-shader'), 
    shaderSourceById('fragment-shader')
);       

const image = new Image();
image.addEventListener('load', () => {
    // 在這邊進行繪製
    ...
});
image.src = "images/caterpillar.jpg";

接下來,啟用、建立、綁定貼圖:

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, gl.createTexture());

activeTexture 表示使用哪個貼圖單元,createTexture 建立的物件會作為貼圖操作時的資料存儲與狀態記錄之用,在這邊,bindTexture 會將之綁定在 gl.TEXTURE0

接著把下載的圖片置入:

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB  , gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);

texImage2D 用來設置貼圖來源與相關參數,它有多個重載版本,在這邊,gl.TEXTURE_2D 表示這是個二維貼圖,0 表示這是原始圖片(具體來說,mipmap 的第 0 級,之後文件會說明),第一個 gl.RGB 表示貼圖來源的顏色資訊,因為這是個 JPG,不會有 Alpha 資訊,因此指定為 gl.RGB 就可以了,第二個 gl.RGB 是 Texel(Texture 與 Pixel 的合成字)資料格式,也就是貼圖讀入為位元組之後,如何看待這些位元組資訊,對於 WebGL 1,必須與前一個參數值相同,gl.UNSIGNED_BYTE 是 Texel 的資料型態,這個版本的 texImage2D 會自動依 image 判斷圖片寬高。

在這邊暫且不用關心 generateMipmap,只需要先知道,如果圖片的長、寬都是 2 的次方,那麼 generateMipmap 可以自動處理貼圖縮放的問題,當然,也可以自己控制,對於圖片的長、寬不是 2 次方的圖片,不能使用 generateMipmap,也得自行控制縮放相關參數。

在這邊,caterpillar.jpg 是 256x256,因此可以使用 generateMipmap,之後會再說明它的作用。

接下來指定採樣器要使用哪個貼圖單元:

gl.uniform1i(
    gl.getUniformLocation(prog, "sampler"), 
    0
);

由於著色器中全域變數的預設值為 0,在只有一個貼圖的情況下,gl.activeTexture(gl.TEXTURE0) 與上頭採樣器的設置不寫,其實也是可以運作的,不過這邊還是將之寫出來比較清楚。

剩下的就是頂點、貼圖座標等的設置了,這邊要畫出完整的圖片,因此要使用兩個三角形:

enableBufferOfAttr(gl, prog, 'vertexPosition', 3, [
    -0.25, 0.25, 0.0,
    0.25, 0.25, 0.0,
    -0.25, -0.25, 0.0,
    -0.25, -0.25, 0.0,
    0.25, 0.25, 0.0,
    0.25, -0.25, 0.0
]);

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

gl.uniform1f(
    gl.getUniformLocation(prog, 'aspect'),
    gl.canvas.clientWidth / gl.canvas.clientHeight
);    

// 繪製
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);

可以按一下範例網頁,費盡千辛萬苦,只為了畫一張圖?當然不是這樣的,因為使用三角形構成,只要改變一下頂點座標,就可以完成某些效果,例如:

enableBufferOfAttr(gl, prog, 'vertexPosition', 3, [
    -0.5, 0.25, 0.0,
    0.25, 0.25, 0.0,
    -0.25, -0.25, 0.0,
    -0.25, -0.25, 0.0,
    0.5, 0.25, 0.0,
    0.25, -0.25, 0.0
]);

就可以把挖土機切割了:

貼圖上色

想像一下,多規劃幾個三角形,修改一下程式,就可以完成玻璃心碎掉的效果了。

另一方面,因為片段著色器中,texture2D 傳回 vec4,如果重新組合 RGBA 的資訊,就可以達到變化色彩的效果了,例如:

gl_FragColor = texture2D(sampler, vTexturePosition).gbra;

就可以把挖土機變色了:

貼圖上色

知道這些之後,基本上,要貼圖到立體方塊上,只要頂點、貼圖座標與索引陣列不要搞錯就好了,例如這個範例網頁有個轉動的立體方塊,詳細程式碼就自行從中探索了:

貼圖上色