如果打算在立方體上貼圖,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),也就是如同身入其境的環境貼圖。