1. WebGL 3D立方体绘制实战指南
刚接触WebGL时,看着那些顶点、着色器、矩阵变换的概念,是不是感觉头都大了?别担心,我第一次接触时也是这样。经过几个项目的实战,我发现其实只要掌握了核心流程,WebGL并没有想象中那么难。今天我就用最直白的语言,带你一步步实现一个彩色旋转的3D立方体。
先来看看最终效果:一个中心在原点、边长为2单位的立方体,六个面分别呈现不同颜色,在黑色背景中匀速旋转。这个案例涵盖了WebGL最核心的5个技术点:数据准备、着色器编写、缓冲区创建、矩阵设置和渲染循环。
2. 核心流程拆解
2.1 数据准备:定义立方体的几何结构
一个立方体有8个顶点和6个面(每个面由2个三角形组成)。我们先在3D坐标系中定义这些顶点的位置:
code复制 7----------6
/| /|
/ | / |
3----------2 |
| | | |
| 4-------|--5
| / | /
|/ |/
0----------1
顶点坐标以立方体中心为原点(0,0,0),边长为2单位。具体坐标值如下:
- 顶点0: (-1, -1, 1) - 前面左下(红色)
- 顶点1: (1, -1, 1) - 前面右下(绿色)
- 顶点2: (1, 1, 1) - 前面右上(蓝色)
- 顶点3: (-1, 1, 1) - 前面左上(黄色)
- 顶点4: (-1, -1, -1) - 后面左下(品红)
- 顶点5: (1, -1, -1) - 后面右下(青色)
- 顶点6: (1, 1, -1) - 后面右上(灰色)
- 顶点7: (-1, 1, -1) - 后面左上(白色)
在WebGL中,我们使用Float32Array存储顶点数据,采用交错存储方式将坐标和颜色数据打包在一起:
javascript复制const verticesColors = new Float32Array([
// 坐标xyz // 颜色rgba
-1, -1, 1, 1, 0, 0, 1, // 0: 红色
1, -1, 1, 0, 1, 0, 1, // 1: 绿色
1, 1, 1, 0, 0, 1, 1, // 2: 蓝色
-1, 1, 1, 1, 1, 0, 1, // 3: 黄色
-1, -1, -1, 1, 0, 1, 1, // 4: 品红
1, -1, -1, 0, 1, 1, 1, // 5: 青色
1, 1, -1, 0.5,0.5,0.5,1, // 6: 灰色
-1, 1, -1, 1, 1, 1, 1 // 7: 白色
]);
2.2 索引缓冲区的妙用
如果直接绘制,每个面需要2个三角形,共12个三角形,需要36个顶点数据(有大量重复)。通过索引缓冲区,我们只需要定义8个实际顶点,然后通过索引来复用它们:
javascript复制const indices = new Uint16Array([
// 前面
0, 1, 2, 0, 2, 3,
// 后面
4, 5, 6, 4, 6, 7,
// 左面
0, 3, 7, 0, 7, 4,
// 右面
1, 5, 6, 1, 6, 2,
// 上面
3, 2, 6, 3, 6, 7,
// 下面
0, 4, 5, 0, 5, 1
]);
这种优化可以减少约78%的顶点数据传输量(从36个顶点减少到8个顶点+36个索引),对性能提升非常明显。
2.3 着色器:GPU的"绘图规则"
WebGL的着色器使用GLSL语言编写,分为顶点着色器和片元着色器。
顶点着色器 负责处理每个顶点的变换:
javascript复制const vsSource = `
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying lowp vec4 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
`;
这段代码做了三件事:
- 接收顶点位置和颜色属性
- 应用模型视图和投影矩阵变换
- 将颜色传递给片元着色器
片元着色器 负责确定每个像素的颜色:
javascript复制const fsSource = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`;
这里直接使用从顶点着色器插值得到的颜色。注意lowp表示使用低精度计算,这对颜色计算足够且更高效。
2.4 矩阵变换:从3D到2D的魔法
WebGL需要三种矩阵变换:
- 模型矩阵:物体自身的变换(旋转、缩放、平移)
- 视图矩阵:相机的位置和方向
- 投影矩阵:3D到2D的投影方式
为简化,我们合并模型和视图矩阵:
javascript复制function createModelViewMatrix(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, -6, 1 // 将立方体向后移动6个单位
];
}
透视投影矩阵模拟人眼视角:
javascript复制function createPerspectiveMatrix(fov, aspect, near, far) {
const f = 1.0 / Math.tan(fov / 2);
const nf = 1 / (near - far);
return [
f/aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far+near)*nf, -1,
0, 0, 2*far*near*nf, 0
];
}
2.5 渲染循环:让立方体动起来
最后一步是设置动画循环,不断更新旋转角度并重绘:
javascript复制let angle = 0;
function render() {
angle += 0.01; // 每帧增加0.01弧度
// 更新模型视图矩阵
const modelViewMatrix = createModelViewMatrix(angle);
gl.uniformMatrix4fv(modelViewLoc, false, modelViewMatrix);
// 清空画布
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绘制立方体
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
render(); // 启动循环
3. 关键细节与优化技巧
3.1 顶点属性指针配置
配置顶点属性时,步长(stride)和偏移(offset)的设置很关键:
javascript复制// 坐标属性
gl.vertexAttribPointer(
positionAttrib,
3, // 每个顶点取3个值(x,y,z)
gl.FLOAT,
false,
28, // 步长=7个float×4字节=28
0 // 坐标从缓冲区开头开始
);
// 颜色属性
gl.vertexAttribPointer(
colorAttrib,
4, // 每个顶点取4个值(r,g,b,a)
gl.FLOAT,
false,
28, // 步长相同
12 // 颜色从第12字节开始(跳过3个float)
);
3.2 深度测试的重要性
没有深度测试时,后面的面可能会显示在前面:
javascript复制gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL); // 当片段深度值<=深度缓冲区值时通过
3.3 性能优化建议
-
使用STATIC_DRAW提示:告诉WebGL我们的数据不会频繁改变
javascript复制gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW); -
提前获取uniform位置:避免在渲染循环中查询
javascript复制const modelViewLoc = gl.getUniformLocation(program, 'uModelViewMatrix'); -
减少GL状态切换:在初始化时设置好状态,渲染时不频繁更改
4. 完整代码实现
以下是完整的HTML文件,可以直接保存运行:
html复制<!DOCTYPE html>
<html>
<head>
<title>WebGL彩色立方体</title>
<style>canvas { display: block; }</style>
</head>
<body>
<canvas id="glCanvas" width="800" height="600"></canvas>
<script>
// 初始化WebGL
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('浏览器不支持WebGL');
throw new Error('WebGL not supported');
}
// 顶点和颜色数据(交错存储)
const verticesColors = new Float32Array([
-1, -1, 1, 1,0,0,1, // 红
1, -1, 1, 0,1,0,1, // 绿
1, 1, 1, 0,0,1,1, // 蓝
-1, 1, 1, 1,1,0,1, // 黄
-1, -1, -1, 1,0,1,1, // 品红
1, -1, -1, 0,1,1,1, // 青
1, 1, -1, 0.5,0.5,0.5,1, // 灰
-1, 1, -1, 1,1,1,1 // 白
]);
// 索引数据
const indices = new Uint16Array([
0,1,2,0,2,3, // 前
4,5,6,4,6,7, // 后
0,3,7,0,7,4, // 左
1,5,6,1,6,2, // 右
3,2,6,3,6,7, // 上
0,4,5,0,5,1 // 下
]);
// 顶点着色器
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying lowp vec4 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
`;
// 片元着色器
const fsSource = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`;
// 编译着色器
function compileShader(source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('着色器编译错误:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = compileShader(vsSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(fsSource, gl.FRAGMENT_SHADER);
// 创建着色器程序
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('程序链接错误:', gl.getProgramInfoLog(program));
throw new Error('Shader program link failed');
}
gl.useProgram(program);
// 创建缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
// 获取attribute位置
const positionAttrib = gl.getAttribLocation(program, 'aVertexPosition');
const colorAttrib = gl.getAttribLocation(program, 'aVertexColor');
gl.enableVertexAttribArray(positionAttrib);
gl.enableVertexAttribArray(colorAttrib);
// 配置顶点属性指针
gl.vertexAttribPointer(positionAttrib, 3, gl.FLOAT, false, 28, 0);
gl.vertexAttribPointer(colorAttrib, 4, gl.FLOAT, false, 28, 12);
// 获取uniform位置
const modelViewLoc = gl.getUniformLocation(program, 'uModelViewMatrix');
const projectionLoc = gl.getUniformLocation(program, 'uProjectionMatrix');
// 设置投影矩阵
function createPerspectiveMatrix(fov, aspect, near, far) {
const f = 1.0 / Math.tan(fov / 2);
const nf = 1 / (near - far);
return [
f/aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far+near)*nf, -1,
0, 0, 2*far*near*nf, 0
];
}
const projectionMatrix = createPerspectiveMatrix(
45 * Math.PI/180,
canvas.width/canvas.height,
0.1,
100.0
);
gl.uniformMatrix4fv(projectionLoc, false, projectionMatrix);
// 创建模型视图矩阵
function createModelViewMatrix(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, -6, 1
];
}
// 渲染循环
let angle = 0;
function render() {
angle += 0.01;
const modelViewMatrix = createModelViewMatrix(angle);
gl.uniformMatrix4fv(modelViewLoc, false, modelViewMatrix);
gl.clearColor(0, 0, 0, 1);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
render();
</script>
</body>
</html>
5. 常见问题与解决方案
5.1 为什么我的立方体显示不全?
可能原因:
- 相机位置太近,尝试增大模型视图矩阵中的平移值(如将-6改为-10)
- 近裁剪面(near)设置过大,尝试减小(如从0.1改为0.01)
5.2 为什么颜色显示不正常?
检查:
- 颜色属性指针的偏移量是否正确(应该是3×4=12字节)
- 颜色值是否在0到1范围内(不是0-255)
- 片元着色器是否正确地接收了varying变量
5.3 如何让立方体旋转得更平滑?
优化建议:
-
使用requestAnimationFrame的时间参数计算增量,避免帧率波动:
javascript复制let lastTime = 0; function render(time) { const deltaTime = time - lastTime; lastTime = time; angle += deltaTime * 0.001; // 0.001弧度/毫秒 // ...其余代码 } -
开启抗锯齿:
javascript复制const gl = canvas.getContext('webgl', { antialias: true });
5.4 如何添加纹理?
步骤:
- 准备纹理坐标属性
- 加载纹理图像
- 在片元着色器中进行纹理采样
6. 扩展思路
掌握了基础立方体后,可以尝试以下扩展:
- 添加光照效果(法线向量、光照计算)
- 实现交互控制(鼠标旋转、缩放)
- 组合多个立方体创建复杂模型
- 添加纹理贴图
- 实现阴影效果
WebGL的学习曲线虽然陡峭,但一旦掌握了这些核心概念,就能打开3D图形编程的大门。建议从这个小项目出发,逐步添加新特性,在实践中深化理解。