為了便於進行後續的範例,在〈使用 Buffer〉中,gl-comm-1.js 的 getGLContext
、installProgram
等函式,封裝對象是 Context 的取得、著色器編譯等任務。
同樣地,為了便於進行後續的範例,這邊進一步地針對基本幾何的建立、貼圖的設置等做封裝,例如,希望在建立立方體時,只需要簡單地建立 CubeGeometry
,設定面的顏色或貼圖時,只要建立 BasicMaterial
、TextureAtlasMaterial
設置 RGBA、頂點數或者是指定 Image
物件等。
這類的封裝更像是朝著通用的程式庫發展,不過通用性不是目前這個階段之目的,而是為了後續在範例撰寫時,能集中在想認識的觀念上,不用撰寫重複煩瑣的程式碼,由於後續還會需要認識著色器,必須保留與著色器程式溝通的能力,然而同時又可以把一些通用的設定隱藏在介面之下。
基本上,建議自行嘗試這類的封裝,這會是個好的練習,先試著抽取相同的操作,接著看看是否適用其他範例的撰寫,這時會需要做些重構,藉由重構程式碼過程,對 WebGL 會有更多深刻的認識,最後我這邊重構後的成果是:
Renderer
:封裝了 Context 取得、著色器編譯,以及與 Context、著色器溝通(像是貼圖產生、屬性、緩衝區設定等)、圖像渲染等行為。Geometry
:具有頂點、索引特性,用來封裝基本幾何資訊,目前有四面體TetrahedronGeometry
、立方體CubeGeometry
、二十面體IcosahedronGeometry
等子類別。BasicMaterial
、TextureAtlasMaterial
、CubeMapMaterial
:各自封裝了顏色、貼圖圖集、CubeMap 資訊與操作,雖然名稱上都有個 Material 結尾,不過目前還沒有發展出共同特性,因此彼此間沒有繼承關係。Mesh
:封裝了Geometry
子類實例以及BasicMaterial
等類別實例,是最後被Renderer
渲染的對象。
來看看怎麼使用封裝後的成果,例如,試著實現〈深度測試與面剔除〉中的正四面體範例,首先要建立 Renderer
實例:
const canvas = document.getElementById('glCanvas');
const renderer = new Renderer(
shaderSourceById('vertex-shader'),
shaderSourceById('fragment-shader'),
canvas);
renderer.uniform1f('aspect', canvas.clientWidth / canvas.clientHeight);
renderer.setClearColor(0, 0, 0, 1);
Renderer
實例建立時必須傳入著色器原始碼與 Canvas 實例,要設置相關屬性時,可以透過 uniform1f
等方法,設置清除色等操作時也是,簡單來說,目前它會負責著與著色器溝通的任務,預設會啟用面剔除與深度測試。
接著要建立幾何、材質、Mesh 物件:
const geometry = new TetrahedronGeometry(0.5); // 邊長為 0.5
const material = new BasicMaterial(3, [
// 四個面的顏色
[1.0, 1.0, 1.0, 1.0], // 白
[1.0, 0.0, 0.0, 1.0], // 紅
[0.0, 1.0, 0.0, 1.0], // 綠
[0.0, 0.0, 1.0, 1.0], // 藍
]);
const tetrahedron = new Mesh(geometry, material);
為了讓事情單純化一些,Geometry
的子類別們,一律採用索引陣列,只要建立對應的 Geometry
實例,就會自動生成頂點與索引;在材質方面,BasicMaterial
用來產生純色面,你要指定每個面會有的頂點數,為了簡化,目前每個面的頂點數必須是相同的;Mesh
目前只是對幾何與材質的簡單封裝,作用是餵給 Renderer
時比較方便。
接下來必須與著色器做些溝通,因為幾何、顏色資訊等,都各自封裝在 TetrahedronGeometry
、BasicMaterial
中,因此只要透過各自對應的特性或方法呼叫取得就可以了:
renderer.buffer(GL.ELEMENT_ARRAY_BUFFER, geometry.indexes, GL.STATIC_DRAW);
const colorBuffer = renderer.buffer(GL.ARRAY_BUFFER, material.vertexColors(), GL.DYNAMIC_DRAW);
renderer.enableVertexAttribArray(colorBuffer, 'color', 4);
const vertBuffer = renderer.buffer(GL.ARRAY_BUFFER, geometry.verteices, GL.DYNAMIC_DRAW);
renderer.enableVertexAttribArray(vertBuffer, 'position', 3);
這邊沒什麼太多需要解釋的,每個方法背後,就是一些 WebGL 相關程式碼的封裝,該談的之前文件都談過了,若你看一下程式庫的原始碼就可以理解,至於為何會是以上的呼叫風格,是因為重構過程中,希望能封裝重複的程式碼同時,也能保有與著色器溝通的彈性。
接下來就可以繪圖了:
rotateXY(geometry.verteices, Math.PI / 3, 0);
let animation = true;
function drawTetrahedron() {
renderer.clear();
renderer.bindBuffer(GL.ARRAY_BUFFER, vertBuffer);
renderer.bufferSubData(GL.ARRAY_BUFFER, 0, geometry.verteices);
renderer.render(tetrahedron);
rotateXY(geometry.verteices, 0, 0.025);
if(animation) {
requestAnimationFrame(drawTetrahedron);
}
}
canvas.addEventListener('mousedown', () => {
animation = !animation;
if(animation) {
drawTetrahedron();
}
});
drawTetrahedron();
相較於〈深度測試與面剔除〉中的正四面體範例,這邊的範例簡潔許多,可以看看完整的範例來瞭解完整的範例怎麼撰寫。
把有規則的幾何封裝起來,寫程式時就不用數頂點數到眼花,例如,要將上頭的範例改為隨機顏色的二十面體,最主要修改的部份是:
const geometry = new IcosahedronGeometry(0.5);
const faceColors = [];
for(let i = 0; i < 20; i++) {
faceColors.push([Math.random(), Math.random(), Math.random(), 1.0]);
}
const material = new BasicMaterial(3, faceColors);
const icosahedron = new Mesh(geometry, material);
可以看看範例網頁,這可以建立以下的效果:
那麼貼圖呢?若以〈貼圖圖集〉中的範例來說,可以如下建立 TextureAtlasMaterial
等物件:
const image = await downloadImage('images/cube_earth.png');
...
const material = new TextureAtlasMaterial(
image, TextureAtlasMaterial.DEFAULT_CUBEMAP_TEXTURE_CORDS);
const geometry = new CubeGeometry(0.5);
const cube = new Mesh(geometry, material);
TextureAtlasMaterial.DEFAULT_CUBEMAP_TEXTURE_CORDS
是配合 CubeMap 貼圖集合而設定的貼圖座標,當然,如果你有其他的貼圖來源,也可以設定自己的貼圖座標。
至於其他與著色器的溝通是差不多類似的設定,主要是注意一下,可以使用 Renderer
的 texture2D
來產生貼圖實例:
const vertBuffer = renderer.buffer(GL.ARRAY_BUFFER, geometry.verteices, GL.DYNAMIC_DRAW);
renderer.enableVertexAttribArray(vertBuffer, 'vertexPosition', 3);
const textureCordBuffer = renderer.buffer(GL.ARRAY_BUFFER, material.textureCords, GL.STATIC_DRAW);
renderer.enableVertexAttribArray(textureCordBuffer, 'texturePosition', 2);
renderer.buffer(GL.ELEMENT_ARRAY_BUFFER, geometry.indexes, GL.STATIC_DRAW);
// 使用 Renderer 的 texture2D 來產生貼圖實例
renderer.bindTexture(GL.TEXTURE_2D, renderer.texture2D(material.image));
renderer.activeTexture(GL.TEXTURE0);
renderer.uniform1i('sampler', 0);
因為後續的文件,還是會談談著色器,因此與著色器設定等細節並沒有完全封裝,實際上,你也還是可以從 Renderer
實例上取得 g1
、program
特性,直接與之互動。完成的效果可以看看範例網頁,效果是相同的。
依照類似的方式,也可以為四面體貼圖,若想在四個面都貼上同一圖案的話,可以只設定一個貼圖座標,並告知重複幾個面:
const material = new TextureAtlasMaterial(image, [0, 0, 1/2, 1, 1, 0], 4);
const geometry = new TetrahedronGeometry(0.5);
const cube = new Mesh(geometry, material);
完成的範例會有這樣的效果:
如果想要利用著色器的 samplerCube
也是可以的話,例如,實現〈動態反射〉的例子,這用二十面體來做特別有感:
const geometry = new IcosahedronGeometry(0.5);
const material = new CubeMapMaterial({
posz: await downloadImage('images/cube_yokohama2_posz.jpg'),
negz: await downloadImage('images/cube_yokohama2_negz.jpg'),
posy: await downloadImage('images/cube_yokohama2_posy.jpg'),
negy: await downloadImage('images/cube_yokohama2_negy.jpg'),
posx: await downloadImage('images/cube_yokohama2_posx.jpg'),
negx: await downloadImage('images/cube_yokohama2_negx.jpg')
});
const icosahedronGeometry = new Mesh(geometry, material);
const texture = renderer.textureCubeMap(material.images);
CubeMapMaterial
只是封裝了六個 Image
,然後提供給 Renderer
的 textureCubeMap
來產生貼圖實例,點一下範例網頁的話,就可以看到以下的效果:
若有興趣,也可以進一步看看 gl-comm-2.js 的原始碼,當然,還可以封裝地更好一些,不過就後續文件在範例的簡化上夠用就好。