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)来完成的。

步骤很简单:

  1. 创建一个缓冲区对象
  2. 绑定到目标(GL.ARRAY_BUFFER)
  3. 把数据填进去
// 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_DRAWgl.STREAM_DRAW。我曾经在做一个实时变形效果时,忘了改这个参数,结果帧率直接掉到 20 帧——教训啊。

小提示: 使用 Float32Array 而不是普通数组。WebGL 底层需要二进制数据,普通数组会多一次类型转换,影响性能。

4.3 顶点属性:告诉 GPU 怎么读数据

数据上传了,但 GPU 不知道这些数字代表什么。是位置?是颜色?还是别的?这就需要我们配置顶点属性(Vertex Attribute)。

配置顶点属性需要四步:

  1. 获取属性位置(location)
  2. 启用该属性
  3. 指定数据格式
  4. 关联到当前绑定的缓冲区
// 获取着色器中 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,画面会变成一团乱麻。

核心理解: 顶点属性就是「数据解析说明书」。它告诉 GPU:从当前绑定的缓冲区里,每几个数字算一个顶点,每个数字是什么类型,从哪里开始读。

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 范围内?
注意: 如果三角形没有显示,先检查浏览器控制台有没有报错。WebGL 的错误信息有时候不太友好,但至少能告诉你「哪里出问题了」。我曾经花了两小时找 bug,最后发现是忘记调用 gl.useProgram(program)——这种低级错误,嗯,谁还没犯过呢。

4.6 避坑指南

最后分享几个我踩过的坑:

  • 忘记绑定缓冲区: 每次调用 vertexAttribPointer 前,确保当前绑定了正确的缓冲区。否则它会读取上一次绑定的缓冲区数据。
  • 步长和偏移量算错: 如果你的顶点数据是交错排列的(比如位置+颜色交替),步长要设置为两个属性的总字节数。初学者建议先用非交错布局,不容易出错。
  • 顶点顺序问题: WebGL 默认使用顺时针顺序剔除背面。如果你画的三角形看不见,试试交换两个顶点的顺序。

好了,这一章的内容就到这里。你现在应该能画出一个三角形了。下一章我们会给三角形加上颜色,让它看起来更生动一些。