立方體映射


如果打算在立方體上貼圖,WebGL 有立方體映射(Cube mapping)方案,可以不用處理貼圖座標與頂點的對應問題,而且可以有更多的應用。

立方體映射簡單來說,由 WebGL 來組織立方體六個面的貼圖,在為像素取樣著色時的依據則是方向向量,方向向量的原點為裁剪空間的 (0, 0, 0),例如在下圖中,黑色虛線表示被指定的方向向量,WebGL 依此方向向量的指向,確認出取樣的位置會是在 +X 面上的像素:

立方體映射

因此在貼圖的安排上,也就必須制訂出 +X、-X、+Y、-Y、+Z、-Z 六個面,例如:

立方體映射

乍看你可能會以為我標示錯誤了,+Z 的右邊怎麼會是 -X?別忘了,方向向量是從原點開始,貼圖六個面會包住原點,也就是你會是位於原點,往外看六個面的圖,因此你看到的這個貼圖圖集,不是貼在立方體外圍,而是貼在立方體內側,就像在房間內貼壁紙的概念。

來看看 WebGL 實際上怎麼撰寫程式碼來做立方體映射,首先著色器會是:

<script id="vertex-shader" type="x-shader/x-vertex">
    uniform float aspect;

    attribute vec3 cubeMapPositon;
    attribute vec3 vertexPosition;

    varying vec3 normal;

    void main(void) {
        gl_Position = vec4(vertexPosition.x, vertexPosition.y * aspect, vertexPosition.z, 1.0);
        normal = cubeMapPositon;
    }
</script>          
<script id="fragment-shader" type="x-shader/x-fragment">
    precision mediump float;

    uniform samplerCube sampler;

    varying vec3 normal;

    void main(void) {
        gl_FragColor = textureCube(sampler, normal);
    }
</script>

在頂點著色器中,vertexPosition 接受的值來自於轉動後的一組立方體頂點,然而 cubeMapPositon 接受的值來自於一組固定不變的立方體頂點,這組頂點會傳給片段著色器的 normal 作為計算方向向量之用,因為這組立方體頂點固定不變,每個面上取得的各個像素顏色也就會固定,也就是不管立方體怎麼轉動,轉動後看到的六個面,圖案都會是固定的。

想要使用立方體映射,取樣器必須是 samplerCube,實際的顏色可以使用 textureCube 函式來計算,它接受一個 samplerCube,以及一個向量,你指定的向量會被規範為各分量為 0 ~ 1,作為取樣的依據。

接下來就可以撰寫 JavaScript 來下載貼圖了,你可以使用貼圖圖集,或者是將六個面分開下載,〈貼圖圖集〉中使用 Humus 的 Earth,實際上提供的是六張圖,檔名都標示了是哪個面使用,這邊為了簡化範例,也就直接使用這六張圖,分開下載後再進行設定。

WebGL 必須在一個 WebGLTexture 指定完六面貼圖後,才能呼叫 generateMipmap 來產生立方體映射,因此這邊修改一下 asyncDownloadTexture,僅下載圖片並指定圖片基本資訊:

function asyncDownloadTexture(src, target) {
    const image = new Image();
    return new Promise(resolve => {
        image.addEventListener('load', () => {
            gl.texImage2D(target, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
            resolve(image);
        });
        image.src = src;
    });
}

然後在建立、綁定 WebGLTexture 後,下載貼圖圖片,指定各個面要使用哪個貼圖,最後呼叫 generateMipmap 來產生立方體映射,留意一下使用 gl.TEXTURE_CUBE_MAP 的地方:

const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);

await asyncDownloadTexture('images/cube_earth_posz.jpg', gl.TEXTURE_CUBE_MAP_POSITIVE_Z); 
await asyncDownloadTexture('images/cube_earth_negz.jpg', gl.TEXTURE_CUBE_MAP_NEGATIVE_Z);    
await asyncDownloadTexture('images/cube_earth_posy.jpg', gl.TEXTURE_CUBE_MAP_POSITIVE_Y); 
await asyncDownloadTexture('images/cube_earth_negy.jpg', gl.TEXTURE_CUBE_MAP_NEGATIVE_Y); 
await asyncDownloadTexture('images/cube_earth_posx.jpg', gl.TEXTURE_CUBE_MAP_POSITIVE_X); 
await asyncDownloadTexture('images/cube_earth_negx.jpg', gl.TEXTURE_CUBE_MAP_NEGATIVE_X); 

gl.generateMipmap(gl.TEXTURE_CUBE_MAP);

接下來就是設定頂點了:

const n = 0.2;
const verteices = [
    // 前
    -n, n,  -n,
    -n, -n,  -n,
    n,  -n, -n,
    n,  n, -n,
    // 後
    n, n, n,
    n, -n, n,
    -n, -n, n,
    -n, n, n,
    // 上
    -n, n, n,
    -n, n, -n,
    n,  n,  -n,
    n,  n, n,
    // 下
    -n, -n, -n,
    -n, -n, n,
    n, -n, n,
    n, -n, -n,
    // 右
    n, n, -n,
    n, -n, -n,
    n, -n, n,
    n, n, n,
    // 左
    -n, n, n,
    -n, -n, n,
    -n, -n, -n,
    -n, n, -n
];

enableBufferOfAttr(gl, prog, 'cubeMapPositon', 3, verteices, gl.STATIC_DRAW);

rotateXY(verteices, -Math.PI / 3, Math.PI / 3);

const vertBuffer = enableBufferOfAttr(gl, prog, 'vertexPosition', 3, verteices, gl.DYNAMIC_DRAW);

轉動後的頂點,才是指定給 vertexPosition,一開始指定的 cubeMapPosition 始終沒有變動,因此就立方體來說,每個面就是固定了,你可以看看範例網頁的效果,基本上跟〈貼圖圖集〉中的效果是相同的。

實際上,立方體映射的效果,不單只是為了這個目的,它還可以用來做動態反射(Dynamic reflection),讓任何模型的表面,呈現鏡面反射週遭景物的效果,也可以用來製作天空盒(Skybox),也就是如同身入其境的環境貼圖。