一起學(xué) WebGL:繪制立方體
大家好,我是前端西瓜哥。
之前我們繪制三角形,是一個(gè)二維的圖形。
現(xiàn)在我們來繪制一個(gè)立方體,其實(shí)本質(zhì)和繪制二維圖形是一樣,也是繪制三角形,只是繪制很多個(gè),然后組合起來,作為立方體的幾個(gè)面,拼在一起就是一個(gè)立方體了。
繪制三角形,我們用的 API 是:
gl.drawArrays(gl.TRIANGLES, 0, n);
那畫一個(gè)立方體,假設(shè)選擇 gl.TRIANGLE_FAN 圖元模式,也就是畫 6 個(gè)面,每個(gè)面有 4 個(gè)點(diǎn)。所以我們需要定義 24 個(gè)點(diǎn)。
這其實(shí)重復(fù)定義了頂點(diǎn),因?yàn)橐粋€(gè)立方體也就 8 個(gè)點(diǎn),數(shù)據(jù)是有冗余的。
WebGL 提供了 gl.drawElements() 方法,通過索引值映射的方式來解決這個(gè)問題。
首先定義立方體的 8 個(gè)頂點(diǎn)(我們命名為 v0 到 v7)的位置和顏色。
const verticesColors = new Float32Array([
1, 1, 1, 1, 1, 1, // 點(diǎn) 0 白
-1, 1, 1, 1, 0, 1, // 點(diǎn) 1 品紅
-1, -1, 1, 1, 0, 0, // 點(diǎn) 2 紅
1, -1, 1, 1, 1, 0, // 點(diǎn) 3 黃
1, -1, -1, 0, 1, 0, // 點(diǎn) 4 綠色
1, 1, -1, 0, 1, 1, // 點(diǎn) 5 青色
-1, 1, -1, 0, 0, 1, // 點(diǎn) 6 藍(lán)色
-1, -1, -1, 0, 0, 0, // 點(diǎn) 7 黑色
]);
然后用索引值構(gòu)造好 6 個(gè)面,每個(gè)面 2 個(gè)三角形:
const indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // 正面
0, 3, 4, 0, 4, 5, // 右面
0, 5, 6, 0, 6, 1, // 上面
1, 6, 7, 1, 7, 2, // 左面
7, 4, 3, 7, 3, 2, // 下面
4, 7, 6, 4, 6, 5, // 背面
]);
和頂點(diǎn)數(shù)據(jù)類似,索引值也要?jiǎng)?chuàng)建一個(gè)緩沖區(qū),并進(jìn)行綁定,綁定目標(biāo)變成了 gl.ELEMENT_ARRAY_BUFFER
上。
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
最后調(diào)用 gl.drawElements 方法進(jìn)行繪制。這里要傳入 indices 數(shù)組的長度,WebGL 就會(huì)讀取索引值得到對(duì)應(yīng)的頂點(diǎn)信息去一個(gè)個(gè)繪制三角形啦。
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
繪制結(jié)果:
完整代碼:
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const infoDiv = document.createElement('div');
document.body.appendChild(infoDiv);
const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix; // 視圖矩陣
uniform mat4 u_ProjMatrix; // 正射投影矩陣
mat4 u_MvpMatrix = u_ProjMatrix * u_ViewMatrix;
varying vec4 v_Color;
void main() {
gl_Position = u_MvpMatrix * a_Position;
v_Color = a_Color;
}
`;
const fragmentShaderSrc = `
precision highp float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`;
/**** 渲染器生成處理 ****/
// 創(chuàng)建頂點(diǎn)渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 創(chuàng)建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序?qū)ο?const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;
// prettier-ignore
const verticesColors = new Float32Array([
1, 1, 1, 1, 1, 1, // 點(diǎn) 0 白
-1, 1, 1, 1, 0, 1, // 點(diǎn) 1 品紅
-1, -1, 1, 1, 0, 0, // 點(diǎn) 2 紅
1, -1, 1, 1, 1, 0, // 點(diǎn) 3 黃
1, -1, -1, 0, 1, 0, // 點(diǎn) 4 綠色
1, 1, -1, 0, 1, 1, // 點(diǎn) 5 青色
-1, 1, -1, 0, 0, 1, // 點(diǎn) 6 藍(lán)色
-1, -1, -1, 0, 0, 0, // 點(diǎn) 7 黑色
]);
// prettier-ignore
const indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // 正面
0, 3, 4, 0, 4, 5, // 右面
0, 5, 6, 0, 6, 1, // 上面
1, 6, 7, 1, 7, 2, // 左面
7, 4, 3, 7, 3, 2, // 下面
4, 7, 6, 4, 6, 5, // 背面
]);
// 每個(gè)數(shù)組元素的字節(jié)數(shù)
const SIZE = verticesColors.BYTES_PER_ELEMENT;
// 創(chuàng)建緩存對(duì)象
const vertexColorBuffer = gl.createBuffer();
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
// 獲取 a_Position 變量地址
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
/****** 正射投影 ******/
const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
// prettier-ignore
const viewMatrix = createViewMatrix(
3, 4, 8, // 觀察點(diǎn)
0, 0, 0, // 視點(diǎn)
0, 1, 0 // 上方向
)
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
/****** 正射投影 ******/
const u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
// prettier-ignore
const projMatrix = createPerspective(
30, canvas.width / canvas.height, 1, 100
)
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix);
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, SIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, SIZE * 6, SIZE * 3);
gl.enableVertexAttribArray(a_Color);
/*** 繪制 ***/
// 清空畫布,并指定顏色
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST); // 啟動(dòng)深度檢測,處理錯(cuò)誤的像素覆蓋問題
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 繪制三角形
// gl.drawArrays(gl.TRIANGLES, 0, 8);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
/************ 后面都是一些工具類方法 ******/
/**** 構(gòu)造視圖矩陣 ****/
function createViewMatrix(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ) {
const normalize = (v) => {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / length, v[1] / length, v[2] / length];
};
const subtract = (v1, v2) => {
return [v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]];
};
const cross = (v1, v2) => {
return [
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0],
];
};
const zAxis = normalize(subtract([eyeX, eyeY, eyeZ], [atX, atY, atZ]));
const xAxis = normalize(cross([upX, upY, upZ], zAxis));
const yAxis = normalize(cross(zAxis, xAxis));
return new Float32Array([
xAxis[0],
yAxis[0],
zAxis[0],
0,
xAxis[1],
yAxis[1],
zAxis[1],
0,
xAxis[2],
yAxis[2],
zAxis[2],
0,
-(xAxis[0] * eyeX + xAxis[1] * eyeY + xAxis[2] * eyeZ),
-(yAxis[0] * eyeX + yAxis[1] * eyeY + yAxis[2] * eyeZ),
-(zAxis[0] * eyeX + zAxis[1] * eyeY + zAxis[2] * eyeZ),
1,
]);
}
function angleToRadian(angle) {
return (Math.PI * angle) / 180;
}
/***** 構(gòu)建透視矩陣 *****/
function createPerspective(fov, aspect, near, far) {
fov = angleToRadian(fov); // 角度轉(zhuǎn)弧度
const f = 1.0 / Math.tan(fov / 2);
const nf = 1 / (near - far);
// prettier-ignore
return new Float32Array([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) * nf, -1,
0, 0, 2 * far * near * nf, 0,
]);
}
線上體驗(yàn) demo:
https://codesandbox.io/s/upx8yz?file=/index.js。