4、绘制三角形:顶点缓冲区、顶点属性、绘制命令、三角形图元
好,咱们直接进入正题。上一章我们把 WebGL 的上下文搞定了,画布也擦干净了。但光有画布和画笔,你总得告诉它「画什么」对吧?
这一章,我们就来画一个三角形。别小看它,三角形是 WebGL 世界里最基础的图元。你看到的 3D 模型,说白了就是成千上万个三角形拼起来的。我当年刚学的时候,画出一个彩色三角形,那种成就感,嗯,至今还记得。
4.1 顶点数据:一切图形的起点
在 WebGL 里,任何图形都是由顶点(Vertex)定义的。一个三角形有三个顶点,每个顶点包含位置信息(x, y, z),有时候还有颜色、纹理坐标等。
我们先定义一个最简单的三角形,三个顶点坐标如下:
// 三个顶点的位置(二维,z 默认为 0)
const positions = [
0.0, 0.5, // 顶点 A:上
-0.5, -0.5, // 顶点 B:左下
0.5, -0.5 // 顶点 C:右下
];
为什么坐标范围是 -1 到 1?因为 WebGL 的裁剪空间(Clip Space)就是这个范围。超出这个范围的顶点会被裁剪掉,不会显示在屏幕上。我在项目中遇到过新手把坐标写成像素值,结果啥也看不到——嗯,这是个经典坑。
4.2 顶点缓冲区:把数据交给 GPU
数据定义好了,但它在 JavaScript 的内存里,GPU 拿不到。我们需要把数据上传到 GPU 的显存中。这个「上传」的动作,就是通过顶点缓冲区对象(VBO,Vertex Buffer Object)来完成的。
步骤很简单:
- 创建一个缓冲区对象
- 绑定到目标(GL.ARRAY_BUFFER)
- 把数据填进去
// 1. 创建缓冲区
const buffer = gl.createBuffer();
// 2. 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 3. 上传数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
这里有个细节:gl.STATIC_DRAW 表示数据基本不变,适合静态几何体。如果你要频繁更新顶点数据(比如粒子系统),可以用 gl.DYNAMIC_DRAW 或 gl.STREAM_DRAW。我曾经在做一个实时变形效果时,忘了改这个参数,结果帧率直接掉到 20 帧——教训啊。
Float32Array 而不是普通数组。WebGL 底层需要二进制数据,普通数组会多一次类型转换,影响性能。
4.3 顶点属性:告诉 GPU 怎么读数据
数据上传了,但 GPU 不知道这些数字代表什么。是位置?是颜色?还是别的?这就需要我们配置顶点属性(Vertex Attribute)。
配置顶点属性需要四步:
- 获取属性位置(location)
- 启用该属性
- 指定数据格式
- 关联到当前绑定的缓冲区
// 获取着色器中 a_Position 变量的位置
const a_Position = gl.getAttribLocation(program, 'a_Position');
// 启用该属性
gl.enableVertexAttribArray(a_Position);
// 指定数据格式
gl.vertexAttribPointer(
a_Position, // 属性位置
2, // 每个顶点取几个分量(这里是 x, y,所以是 2)
gl.FLOAT, // 数据类型
false, // 是否归一化
0, // 步长(stride),0 表示紧密排列
0 // 偏移量(offset)
);
这里重点说一下 vertexAttribPointer 的参数。第二个参数是「每个顶点的分量数」。如果是二维坐标就是 2,三维就是 3。如果你传了 3 但数据只有 2 个值,GPU 会读错数据——我调试过这种 bug,画面会变成一团乱麻。
4.4 绘制命令:真正把三角形画出来
数据准备好了,属性配置好了,最后一步就是发出绘制命令。WebGL 提供了两个主要绘制函数:
gl.drawArrays(mode, first, count):直接按顺序绘制顶点gl.drawElements(mode, count, type, offset):通过索引绘制(后面章节会讲)
画三角形,我们用 drawArrays:
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
参数说明:
gl.TRIANGLES:图元类型,告诉 GPU 每三个顶点构成一个三角形0:从第 0 个顶点开始3:一共画 3 个顶点
你可能会问:为什么是 TRIANGLES 而不是 TRIANGLE?因为 WebGL 支持多种图元类型:
| 图元类型 | 说明 | 顶点数要求 |
|---|---|---|
| gl.POINTS | 点 | 每个顶点一个点 |
| gl.LINES | 线段 | 每两个顶点一条线 |
| gl.LINE_STRIP | 线条带 | 连续连接 |
| gl.TRIANGLES | 三角形 | 每三个顶点一个三角形 |
| gl.TRIANGLE_STRIP | 三角形带 | 共享边,节省顶点 |
| gl.TRIANGLE_FAN | 三角形扇 | 共享第一个顶点 |
我个人习惯,刚开始学的时候只用 TRIANGLES,逻辑最清晰。等后面做复杂模型时,再用 TRIANGLE_STRIP 来节省顶点数。
4.5 完整代码示例
把上面所有步骤串起来,就是一个完整的三角形绘制程序:
// 顶点数据
const positions = [
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
];
// 创建并绑定缓冲区
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 获取并配置顶点属性
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 绘制
gl.drawArrays(gl.TRIANGLES, 0, 3);
运行这段代码,你应该能看到一个白色的三角形出现在画布中央。如果看不到,检查一下:
- 着色器是否编译成功?
- program 是否链接成功?
- 顶点坐标是否在 -1 到 1 范围内?
gl.useProgram(program)——这种低级错误,嗯,谁还没犯过呢。
4.6 避坑指南
最后分享几个我踩过的坑:
- 忘记绑定缓冲区: 每次调用
vertexAttribPointer前,确保当前绑定了正确的缓冲区。否则它会读取上一次绑定的缓冲区数据。 - 步长和偏移量算错: 如果你的顶点数据是交错排列的(比如位置+颜色交替),步长要设置为两个属性的总字节数。初学者建议先用非交错布局,不容易出错。
- 顶点顺序问题: WebGL 默认使用顺时针顺序剔除背面。如果你画的三角形看不见,试试交换两个顶点的顺序。
好了,这一章的内容就到这里。你现在应该能画出一个三角形了。下一章我们会给三角形加上颜色,让它看起来更生动一些。