在之前的文章中,我們使用WebGL繪製了很多二維的圖形和影像,在學習2D繪圖的時候,我們提過很多次關於GPU的高效渲染,但是2D圖形的繪製只展示了WebGL部分的能力,WebGL更強大的地方在於,它可以繪製各種3D圖形,而3D圖形能夠極大地增強視覺化的表現能力。
相信很多小夥伴都對此有所耳聞,也有不少人學習WebGL,就是衝著它的3D繪圖能力。
接下來,我就用一個簡單的正立方體的例子來演示在WebGL中如何繪製3D物體。
本文可配合影片食用
從二維到三維
首先,我們先來繪製一個熟悉的2D圖形,正方形。
// vertex
attribute vec2 a_vertexPosition;
attribute vec4 color;
varying vec4 vColor;
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = vec4(a_vertexPosition, 1, 1);
}
// fragment
#ifdef GL_ES
precision highp float;
#endif
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
// ...
renderer.setMeshData([{
positions: [
[-0.5, -0.5],
[-0.5, 0.5],
[0.5, 0.5],
[0.5, -0.5]
],
attributes: {
color: [
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
]
},
cells: [[0, 1, 2], [2, 0, 3]]
}]);
// ...
上述這些程式碼比較簡單,我就不過多解釋了。
在畫布上我們看到,繪製了一個紅色的正方形,它是一個平面圖形。
接下來,我們就在這個圖形的基礎上,將它擴充為3D的正立方體。
要想把2維圖形擴充為3維幾何體,第一步就是要把頂點擴充套件到3維。也就是把vec2擴充套件為vec3。
// vertex
attribute vec3 a_vertexPosition;
attribute vec4 color;
varying vec4 vColor;
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = vec4(a_vertexPosition, 1);
}
當然僅僅修改Shader是不夠的,因為資料是從JavaScript傳遞過來的,所以我們需要在JavaScript中計算立方體的頂點資料,然後再傳遞給Shader。
一個立方體有8個頂點,能組成6個面。在WebGL中需要用12個三角形來繪製它。
如果6個面的屬性相同的話,我們可以複用8個頂點來繪製;
但如果屬性不完全相同,比如每個面要繪製成不同的顏色,或者新增不同的紋理圖片,就得把每個面的頂點分開。這樣的話,就需要24個頂點來分別處理6個面。
為了方便使用,我們可以定義一個JavaScript函式,用來生成立方體6個面的24個頂點,以及12個三角形的索引,並且定義每個面的顏色。
/**
* 生成立方體6個面的24個頂點,12個三角形的索引,定義每個面的顏色資訊
* @param size
* @param colors
* @returns {{cells: *[], color: *[], positions: *[]}}
*/
export function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
const h = 0.5 * size;
const vertices = [
[-h, -h, -h],
[-h, h, -h],
[h, h, -h],
[h, -h, -h],
[-h, -h, h],
[-h, h, h],
[h, h, h],
[h, -h, h]
];
const positions = [];
const color = [];
const cells = [];
let colorIdx = 0;
let cellsIdx = 0;
const colorLen = colors.length;
function quad(a, b, c, d) {
[a, b, c, d].forEach(item => {
positions.push(vertices[item]);
color.push(colors[colorIdx % colorLen]);
});
cells.push(
[0, 1, 2].map(i => i + cellsIdx),
[0, 2, 3].map(i => i + cellsIdx)
);
colorIdx ++;
cellsIdx += 4;
}
quad(1, 0, 3, 2); // 內
quad(4, 5, 6, 7); // 外
quad(2, 3, 7, 6); // 右
quad(5, 4, 0, 1); // 左
quad(3, 0, 4, 7); // 下
quad(6, 5, 1, 2); // 上
return {positions, color, cells};
}
現在我們就可以透過呼叫cube這個函式,構建出立方體的頂點資訊。
const geometry = cube(1.0, [
[1, 0, 0, 1], // 紅
[0, 0.5, 0, 1], // 綠
[0, 0, 1, 1] // 藍
]);
透過這段程式碼,我們就能建立出一個稜長為1的立方體,並且六個面的顏色分別是“紅、綠、藍、紅、綠、藍”。
接下來我們就要把這個立方體的頂點資訊傳遞給Shader。
在傳遞資料之前,我們需要先了解一個知識點,是關於繪製3D圖形與2D圖形存在的一點不同,那就是繪製3D圖形時,必須要開啟深度檢測和啟用深度緩衝區。
在WebGL中,我們可以透過gl.enable(gl.DEPTH_TEST);
這段程式碼來開啟深度檢測;在清空畫布的時候,也要用gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
這段程式碼來同時清空顏色緩衝區和深度緩衝區。
啟動和清空深度檢測和深度緩衝區這兩個步驟,非常重要。但是一般情況下,我們幾乎不會用原生的方式來編寫程式碼,所以瞭解一下即可。為了方便使用,在本文演示的例子中,我們還是直接使用gl-renderer這個庫,它封裝了深度檢測,我們在使用時,在建立renderer的時候配置一個引數depth: true
就可以了。
現在我們就把這個三維立方體用gl-renderer渲染出來。
// ...
renderer = new GlRenderer(glRef.value, {
depth: true // 開啟深度檢測
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([{
positions: geometry.positions,
attributes: {
color: geometry.color
},
cells: geometry.cells
}]);
renderer.render();
現在我們在畫布上看到的是一個紅色正方形,這是因為其他面被遮擋住了。
投影矩陣:變換WebGL座標系
但是,等等,為什麼我們看到的是紅色的一面呢?按照我們所編寫的程式碼,預期看到的應該是綠色的一面,也就是說我們預期Z軸是向外的,因為規範的直角座標系是右手座標系。所以按照現在的繪製結果,我們發現WebGL的座標系其實是左手系的?
但一般來說,不管什麼圖形庫或者圖形框架,在繪圖的時候,都會預設將座標系從左手系轉換為右手系,因為這更符合我們的使用習慣。所以這裡,我們也去把WebGL的座標系從左手系轉換為右手系,簡單來說,就是將Z軸座標方向反轉。關於座標轉換,可以透過齊次矩陣來完成。對座標轉換不熟悉的小夥伴,可以參考我之前的一篇關於仿射變換的文章。
將Z軸座標方向反轉,對應的齊次矩陣是這樣的:
[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1
]
這種轉換座標的齊次矩陣,也被稱為投影矩陣,ProjectionMatrix。
現在我們修改一下頂點著色器,把這個投影矩陣新增進去。
// vertex
attribute vec3 a_vertexPosition; // 1:把頂點從vec2擴充套件到vec3
attribute vec4 color; // 四維向量
varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩陣-變換座標系
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * vec4(a_vertexPosition, 1.0);
}
現在我們就能看到畫布上顯示的是綠色的正方形了。
模型矩陣:讓立方體旋轉起來
現在我們只能看到立方體的一個面,因為Z軸是垂直於螢幕的,這樣子從視覺上看好像和2維圖形沒什麼區別,沒法讓人很直觀地聯想、感受到這是一個三維的幾何體,為了將其他的面露出來,我們可以去旋轉立方體。
要想旋轉立方體,我們同樣可以透過矩陣運算來實現。這個矩陣叫做模型矩陣,ModelMatrix,它定義了被繪製的物體變換。
把模型矩陣加入到頂點著色器中,將它與投影矩陣相乘,再乘上齊次座標,就得到最終的頂點座標了。
attribute vec3 a_vertexPosition; // 1:把頂點從vec2擴充套件到vec3
attribute vec4 color; // 四維向量
varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩陣-變換座標系
uniform mat4 modelMatrix; // 3:模型矩陣-使幾何體旋轉
void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
}
現在我們定義一個JavaScript函式,用立方體沿x、y、z軸的旋轉來生成模型矩陣。
以x、y、z三個方向的旋轉得到三個齊次矩陣,然後將它們相乘,就能得到最終的模型矩陣。
import { multiply } from 'ogl/src/math/functions/Mat4Func.js';
// ...
export function fromRotation(rotationX, rotationY, rotationZ) {
let c = Math.cos(rotationX);
let s = Math.sin(rotationX);
const rx = [
1, 0, 0, 0, // 繞X軸旋轉
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
c = Math.cos(rotationY);
s = Math.sin(rotationY);
const ry = [
c, 0, s, 0,
0, 1, 0, 0, // 繞Y軸旋轉
-s, 0, c, 0,
0, 0, 0, 1
];
c = Math.cos(rotationZ);
s = Math.sin(rotationZ);
const rz = [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0, // 繞Z軸旋轉
0, 0, 0, 1
];
const ret = [];
multiply(ret, rx, ry);
multiply(ret, ret, rz);
return ret;
}
我們把模型矩陣傳給頂點著色器,不斷更新三個旋轉角度,就能實現立方體旋轉的效果。
// ...
let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
rotationX += 0.003;
rotationY += 0.005;
rotationZ += 0.007;
renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
requestAnimationFrame(update);
}
update();
// ...
現在我們就能在旋轉中看到立方體的其他幾個面了,能更直觀地感受到這是一個三維物體。
總結
至此,我們就實現了正立方體的繪製。在3D物體的繪製中,正立方體屬於是比較簡單的一類,螢幕前的小夥伴們都可以來動手嘗試下,感興趣的小夥伴,還可以嘗試去實現圓柱體、正四面體等等這些幾何體的繪製。
參考程式碼
效果預覽