Skip to content

WebGL - 绘制四边形

虽然 WebGL 的基本绘制原始类型是点、线和三角形,但我们经常需要绘制更复杂的形状,如四边形 (quad) 或正方形。本章演示了如何使用索引缓冲区 (index buffer),通过将四边形分解为两个三角形来绘制它。

四边形可以表示为两个共享一条边的相邻三角形。为了在 WebGL 中高效地绘制它,我们定义四边形的四个独特顶点,然后使用索引缓冲区 (ELEMENT_ARRAY_BUFFER) 来指定哪些顶点组成了这两个三角形。

考虑一个顶点为 0、1、2、3 的四边形:

0-------3
| \ |
| \ |
| \ |
| \ |
| \ |
1-------2

我们可以定义顶点,然后使用索引来形成三角形 1(顶点 0、1、2)和三角形 2(顶点 0、2、3)。

这个过程与使用索引绘制单个三角形非常相似,但几何体数据不同:

步骤 1: 设置 Canvas 和 WebGL 上下文

标准初始化。

步骤 2: 定义几何体(顶点和索引)和缓冲区

在 Float32Array 中定义四边形的 4 个独特顶点。在 Uint16Array(或类似类型)中定义 6 个索引(每个三角形 3 个)。创建两个 WebGL 缓冲区:一个用于顶点的 VBO (gl.ARRAY_BUFFER) 和一个用于索引的 IBO (gl.ELEMENT_ARRAY_BUFFER)。使用 gl.bufferData 将相应的数据加载到每个缓冲区中。

// 定义四边形的 4 个顶点 (X, Y, Z)
const quadVertices = new Float32Array([
-0.5, 0.5, 0.0, // 顶点 0:左上
-0.5, -0.5, 0.0, // 顶点 1:左下
0.5, -0.5, 0.0, // 顶点 2:右下
0.5, 0.5, 0.0 // 顶点 3:右上
]);
// 定义构成四边形的两个三角形的索引
const quadIndices = new Uint16Array([
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
]);
// ... (创建 VBO 和 IBO,并为两者填充 bufferData) ...

步骤 3: 创建和编译着色器

基本的顶点着色器和片段着色器就足够了。顶点着色器需要接收位置属性并输出 gl_Position。片段着色器设置 gl_FragColor。

// 顶点着色器(简单直传)
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
// 片段着色器(纯色)
precision mediump float;
void main() {
gl_FragColor = vec4(0.3, 0.8, 0.4, 1.0); // 偏绿色
}

步骤 4: 链接着色器和缓冲区

获取属性位置,启用位置属性数组,并配置 gl.vertexAttribPointer 以从 VBO 中读取数据。

步骤 5: 绘制四边形

清除 canvas,设置视口等。关键是,在绘制之前将索引缓冲区对象 (IBO) 绑定到 gl.ELEMENT_ARRAY_BUFFER 目标。然后,使用 gl.drawElements(gl.TRIANGLES, count, type, offset)。

  • mode:gl.TRIANGLES
  • count:要绘制的索引数量(由两个三角形组成的四边形为 6)。
  • type:IBO 中索引的数据类型(例如,gl.UNSIGNED_SHORT)。
  • offset:IBO 中的起始偏移量(通常为 0)。
// 在绘制前绑定索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferObject);
// 使用索引绘制四边形
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

这个例子使用上述步骤绘制了一个纯偏绿色的四边形。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebGL Drawing a Quad</title>
<style>
body { margin: 0; overflow: hidden; background-color: #444; }
canvas { display: block; border: 1px solid lightblue; }
</style>
</head>
<body>
<canvas id="webgl-canvas" width="500" height="500"></canvas>
<script>
// --- 着色器源码 ---
const vsSource = `
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
`;
const fsSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(0.3, 0.8, 0.4, 1.0); // 偏绿色
}
`;
// --- 工具函数:createShader, createProgram (假定已定义) ---
function createShader(gl, type, source) { /* ... */
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`着色器编译错误 (${type === gl.VERTEX_SHADER ? 'Vertex' : 'Fragment'}):`, gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vsSource, fsSource) { /* ... */
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
if (!vertexShader || !fragmentShader) return null;
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));
gl.deleteProgram(program);
return null;
}
return program;
}
// --- 主 WebGL ---
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) { alert('WebGL 不受支持。'); throw new Error('WebGL 不受支持'); }
const shaderProgram = createProgram(gl, vsSource, fsSource);
if (!shaderProgram) throw new Error('创建程序失败。');
// --- 几何体 ---
const quadVertices = new Float32Array([
-0.5, 0.5, 0.0, // 0:左上
-0.5, -0.5, 0.0, // 1:左下
0.5, -0.5, 0.0, // 2:右下
0.5, 0.5, 0.0 // 3:右上
]);
const quadIndices = new Uint16Array([
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
]);
// --- 缓冲区 ---
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
// --- 属性设置 ---
const positionAttribLocation = gl.getAttribLocation(shaderProgram, 'a_position');
if (positionAttribLocation < 0) console.error("未找到属性 'a_position'");
gl.enableVertexAttribArray(positionAttribLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 设置指针前先绑定 VBO
gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, 0, 0);
// --- 绘制 ---
gl.useProgram(shaderProgram); // 激活程序
gl.clearColor(0.25, 0.25, 0.25, 1.0); // 深灰色背景
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// ** 绑定索引缓冲区 **
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// ** 使用索引绘制 **
gl.drawElements(
gl.TRIANGLES, // 原始类型
6, // 要绘制的索引数量
gl.UNSIGNED_SHORT, // 索引类型(与 Uint16Array 匹配)
0 // 缓冲区中的偏移量
);
// 良好实践:如果在其他地方需要,绘制后解绑缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
</script>
</body>
</html>

预期输出:

这段代码将渲染一个纯偏绿色的正方形,居中显示在 canvas 上,填充裁剪空间中从 (-0.5, -0.5) 到 (0.5, 0.5) 的区域。

  • 修改顶点坐标以创建一个矩形而不是正方形。
  • 改变索引顺序(例如,1, 2, 3, 1, 3, 0),看看它如何影响绘制的三角形(它应该仍然构成相同的四边形)。
  • 使用其他章节中的技术为四边形应用颜色或变换。
  • 尝试使用 gl.TRIANGLE_STRIP 模式和 drawArrays 绘制四边形,只使用正确排序的 4 个顶点(例如,1、0、2、3)。比较所需的设置。