Skip to content

WebGL - 立方体旋转

本章演示了如何使用 WebGL 绘制一个旋转的 3D 立方体,其中结合了现代 JavaScript 实践和一个标准的数学库。

手动编写矩阵运算(投影、视图、模型变换、旋转)代码容易出错且冗长。实际开发中,开发者会使用经过充分测试的数学库。gl-matrix 是一个流行的选择。我们将在本示例中使用它。通常可以通过 <script> 标签引入,或者在使用模块打包器时进行导入。

示例 – 绘制一个旋转的 3D 立方体

Section titled “示例 – 绘制一个旋转的 3D 立方体”

以下 HTML 和 JavaScript 代码设置了一个 WebGL canvas 并绘制一个持续旋转的彩色立方体。

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebGL Rotating Cube</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.3.0/gl-matrix-min.js"></script>
<style>canvas { border: 1px solid black; }</style>
</head>
<body>
<canvas width="570" height="570" id="my_Canvas"></canvas>
<script>
// Ensure glMatrix library is available
const { mat4, vec3 } = glMatrix;
/*============= 创建 canvas =================*/
const canvas = document.getElementById('my_Canvas');
const gl = canvas.getContext('webgl'); // 使用 'webgl' 或 'webgl2'
if (!gl) {
alert('WebGL not supported!');
throw new Error('WebGL not supported');
}
/*============ 定义并存储几何体 =========*/
// 定义立方体各个面的顶点
const vertices = [
// 前面
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
// 后面
-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
// 顶面
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
// 底面
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
// 右面
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
// 左面
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
];
// 定义每个顶点的颜色(归一化 R, G, B)
const faceColors = [
[1.0, 0.0, 0.0], // 前面:红色
[0.0, 1.0, 0.0], // 后面:绿色
[0.0, 0.0, 1.0], // 顶面:蓝色
[1.0, 1.0, 0.0], // 底面:黄色
[1.0, 0.0, 1.0], // 右面:品红色
[0.0, 1.0, 1.0], // 左面:青色
];
// 将面颜色转换为每个顶点的颜色
let colors = [];
for (let j = 0; j < faceColors.length; ++j) {
const c = faceColors[j];
// 为面的四个顶点重复该颜色四次
colors = colors.concat(c, c, c, c);
}
// 定义立方体三角形的索引
const indices = [
0, 1, 2, 0, 2, 3, // 前
4, 5, 6, 4, 6, 7, // 后
8, 9, 10, 8, 10, 11, // 顶
12, 13, 14, 12, 14, 15, // 底
16, 17, 18, 16, 18, 19, // 右
20, 21, 22, 20, 22, 23, // 左
];
// --- 创建缓冲区 ---
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
/*=================== 着色器 =========================*/
// 使用 GLSL ES 1.00(WebGL 1 的标准)
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec3 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying lowp vec3 vColor;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
`;
const fsSource = `
varying lowp vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor, 1.0);
}
`;
// 用于编译着色器的辅助函数
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
// 创建着色器程序
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
throw new Error('Shader program linking failed');
}
// 存储 attribute 和 uniform 的位置信息
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
},
};
/* ====== 将 attribute 与顶点着色器关联 =====*/
// 告诉 WebGL 如何从顶点缓冲区中提取位置数据
{
const numComponents = 3; // 每次迭代提取 3 个值 (x, y, z)
const type = gl.FLOAT; // 缓冲区中的数据是 32 位浮点数
const normalize = false; // 不进行归一化
const stride = 0; // 从一组值到下一组值需要跳过多少字节
// 0 = 使用上面的 type 和 numComponents
const offset = 0; // 从缓冲区内部的哪个位置(字节偏移)开始
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}
// 告诉 WebGL 如何从颜色缓冲区中提取颜色数据
{
const numComponents = 3; // R, G, B
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexColor,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
}
// 告诉 WebGL 使用哪些索引来引用顶点
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.useProgram(programInfo.program);
/*==================== 矩阵设置 =====================*/
const fieldOfView = 45 * Math.PI / 180; // 弧度制
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
const modelViewMatrix = mat4.create();
// 将绘制位置稍微向后移动以便看到
mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -6.0]);
/*================= 绘制 ===========================*/
let lastTime = 0;
let cubeRotation = 0.0;
function render(now) {
now *= 0.001; // 转换为秒
const deltaTime = now - lastTime;
lastTime = now;
// 旋转立方体
mat4.rotate(modelViewMatrix, // 目标矩阵
modelViewMatrix, // 要旋转的矩阵
cubeRotation, // 旋转量(弧度)
[0, 0, 1]); // 旋转轴 (Z)
mat4.rotate(modelViewMatrix, modelViewMatrix, cubeRotation * .7, [0, 1, 0]); // Y
mat4.rotate(modelViewMatrix, modelViewMatrix, cubeRotation * .3, [1, 0, 0]); // X
cubeRotation += deltaTime;
// --- 绘制 ---
gl.clearColor(0.5, 0.5, 0.5, 0.9); // 清除为灰色,完全不透明
gl.clearDepth(1.0); // 清除所有内容
gl.enable(gl.DEPTH_TEST); // 启用深度测试
gl.depthFunc(gl.LEQUAL); // 近处的物体遮挡远处的物体
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 设置着色器 uniform
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix);
{
const vertexCount = 36; // 6 个面 * 2 个三角形/面 * 3 个顶点/三角形
const type = gl.UNSIGNED_SHORT;
const offset = 0;
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>

预期结果:

这段代码将在 canvas 中央绘制一个具有不同颜色面、持续围绕多个轴旋转的 3D 立方体。