Skip to content

WebGL - 绘制三角形

绘制一个简单的形状(如三角形)通常是图形编程的“Hello, World!”。它包含了 WebGL 应用程序的所有基本步骤:设置 Canvas、定义几何体、编写着色器、将数据发送到 GPU,以及触发绘制调用。

本章演示了如何使用索引缓冲区(index buffer,drawElements)绘制一个三角形,这是一种常用且高效的方法,对于顶点在多个三角形之间共享的更复杂模型尤其如此。

以下是必要的步骤分解:

步骤 1: 准备 Canvas 并获取 WebGL Context

Section titled “步骤 1: 准备 Canvas 并获取 WebGL Context”

首先,你需要一个 HTML <canvas> 元素。然后,使用 JavaScript 获取它的引用,并请求一个 WebGL 渲染上下文(Rendering Context)。

<canvas id="glCanvas" width="640" height="480"></canvas>
const canvas = document.getElementById('glCanvas');
// Try getting the standard WebGL context first
const gl = canvas.getContext('webgl');
if (!gl) {
console.error("Unable to initialize WebGL. Your browser or machine may not support it.");
// Optionally try 'experimental-webgl' for older browsers
// gl = canvas.getContext('experimental-webgl');
// if (!gl) { ... handle error ... }
}
// If context is obtained, proceed...

步骤 2: 定义几何体并存储在缓冲区中

Section titled “步骤 2: 定义几何体并存储在缓冲区中”

定义顶点位置和组成三角形的索引。然后,创建 WebGL 缓冲区对象(buffer objects)(ARRAY_BUFFER 用于顶点,ELEMENT_ARRAY_BUFFER 用于索引),并将数据加载到其中。

// Define vertex positions (X, Y, Z)
const positions = [
-0.5, -0.5, 0.0, // Vertex 0: Bottom-left (顶点 0: 左下)
0.5, -0.5, 0.0, // Vertex 1: Bottom-right (顶点 1: 右下)
0.0, 0.5, 0.0 // Vertex 2: Top-center (顶点 2: 中上)
];
// Define the indices that form the triangle (using vertex indices 0, 1, 2)
const indices = [0, 1, 2]; // 定义组成三角形的索引(使用顶点索引 0, 1, 2)
// --- Create and load position buffer ---
const positionBuffer = gl.createBuffer(); // 创建位置缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 绑定为当前 ARRAY_BUFFER
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); // 将数据加载到缓冲区
// Note: It's good practice to unbind after configuration, but not strictly necessary
// gl.bindBuffer(gl.ARRAY_BUFFER, null); // 注意:配置后解除绑定是个好习惯,但非强制要求
// --- Create and load index buffer ---
const indexBuffer = gl.createBuffer(); // 创建索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定为当前 ELEMENT_ARRAY_BUFFER
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 将数据加载到缓冲区
// gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

gl.STATIC_DRAW 是给 WebGL 的一个提示,表明数据不会经常改变。

步骤 3: 创建、编译和链接着色器程序

Section titled “步骤 3: 创建、编译和链接着色器程序”

用 GLSL 编写顶点和片元着色器代码,然后使用 WebGL 函数编译它们,并将它们链接成一个可用的着色器程序。记住要包含错误检查!

顶点着色器 (GLSL):

attribute vec3 a_position; // Receive vertex position attribute (接收顶点位置属性)
void main() {
// Pass the position directly to the output (no transformation)
// We use vec4 because gl_Position requires (x, y, z, w)
gl_Position = vec4(a_position, 1.0); // 将位置直接传递给输出(无变换),使用 vec4 因为 gl_Position 需要 (x, y, z, w)
}

片元着色器 (GLSL):

precision mediump float; // Necessary precision declaration (必要的精度声明)
void main() {
// Set the fragment color to opaque orange (将片元颜色设置为不透明橙色)
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0);
}

JavaScript(编译/链接 - 概念性,使用辅助函数):

// Assume vsSource and fsSource contain the GLSL code strings
// Assume loadShader and initShaderProgram helper functions exist (as shown previously)
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); // 假设 vsSource 和 fsSource 包含 GLSL 代码字符串
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); // 假设 loadShader 和 initShaderProgram 辅助函数存在(如前所示)
const shaderProgram = initShaderProgram(gl, vertexShader, fragmentShader);
// If shaderProgram is null, an error occurred during setup
if (!shaderProgram) { // 如果 shaderProgram 为 null,则设置过程中发生错误
// Handle error - likely reported by helper functions
return; // 处理错误 - 可能由辅助函数报告
}

步骤 4: 将着色器属性与缓冲区数据关联

Section titled “步骤 4: 将着色器属性与缓冲区数据关联”

获取着色器程序中 attribute 变量(a_position)的位置。然后,告诉 WebGL 如何从当前绑定的 ARRAY_BUFFER(我们的 positionBuffer)中为该属性提取数据。

// Get the location of the attribute
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position'); // 获取属性位置
// Bind the position buffer again (if not already bound)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 再次绑定位置缓冲区(如果尚未绑定)
// Tell WebGL how to pull data out of the position buffer
const numComponents = 3; // (X, Y, Z) (分量数量)
const type = gl.FLOAT; // Data type (数据类型)
const normalize = false; // Don't normalize (不归一化)
const stride = 0; // Bytes to skip between elements (0 = tightly packed) (元素之间的跳过字节数,0 = 紧密打包)
const offset = 0; // Bytes offset from the beginning (从开头偏移的字节数)
gl.vertexAttribPointer(
positionAttributeLocation,
numComponents,
type,
normalize,
stride,
offset
);
// Enable the vertex attribute array
gl.enableVertexAttribArray(positionAttributeLocation); // 启用顶点属性数组

步骤 5: 设置渲染状态并绘制三角形

Section titled “步骤 5: 设置渲染状态并绘制三角形”

清除 Canvas,设置视口,告诉 WebGL 使用着色器程序,确保索引缓冲区已绑定,最后调用 drawElements。

// Set clear color to light gray and clear the canvas
gl.clearColor(0.8, 0.8, 0.8, 1.0); // 设置清除颜色为浅灰色
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清除颜色和深度缓冲区
// Set the viewport to match the canvas size
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口匹配 Canvas 大小
// Tell WebGL to use our shader program
gl.useProgram(shaderProgram); // 告诉 WebGL 使用我们的着色器程序
// Bind the index buffer (important before drawElements)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定索引缓冲区(在 drawElements 之前很重要)
// Draw the triangle!
const primitiveType = gl.TRIANGLES; // Draw triangles (绘制三角形)
const count = indices.length; // Number of indices to draw (要绘制的索引数量)
const indexType = gl.UNSIGNED_SHORT; // Type of data in index buffer (索引缓冲区中的数据类型)
const drawOffset = 0; // Start at the beginning of the index buffer (从索引缓冲区开头开始)
gl.drawElements(primitiveType, count, indexType, drawOffset);

下面是 HTML 和 JavaScript 文件的概念性结构。你将把上面步骤中的代码片段填充到其中,包括健壮的错误检查。

<!doctype html>
<html>
<head>
<title>WebGL Triangle</title>
<meta charset="utf-8">
</head>
<body>
<canvas id="glCanvas" width="640" height="480"></canvas>
<script>
// --- Helper function: loadShader ---
function loadShader(gl, type, source) { /* ... see previous chapter ... */ } // 辅助函数:加载着色器
// --- Helper function: initShaderProgram ---
function initShaderProgram(gl, vsSource, fsSource) { /* ... see previous chapter ... */ } // 辅助函数:初始化着色器程序
// --- Main function ---
function main() { // 主函数
// Step 1: Get Canvas & Context
const canvas = document.getElementById('glCanvas'); // 步骤 1: 获取 Canvas 和 Context
const gl = canvas.getContext('webgl');
if (!gl) { /* handle error */ return; } // 处理错误
// Step 3: Define Shader Sources
const vsSource = `...`; // Vertex shader GLSL code (步骤 3: 定义着色器源)
const fsSource = `...`; // Fragment shader GLSL code (片元着色器 GLSL 代码)
// Step 3: Compile and Link Shaders
const shaderProgram = initShaderProgram(gl, vsSource, fsSource); // 步骤 3: 编译和链接着色器
if (!shaderProgram) { /* handle error */ return; } // 处理错误
// Step 2: Define Geometry
const positions = [/* ... vertex data ... */]; // 步骤 2: 定义几何体
const indices = [/* ... index data ... */]; // 索引数据
// Step 2: Create and Load Buffers
const positionBuffer = gl.createBuffer(); // 步骤 2: 创建和加载缓冲区
/* ... bind and bufferData for positions ... */ // 绑定并加载位置数据
const indexBuffer = gl.createBuffer();
/* ... bind and bufferData for indices ... */ // 绑定并加载索引数据
// Step 4: Associate Shaders and Buffers
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position'); // 步骤 4: 关联着色器和缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(/* ... parameters ... */); // 设置属性指针
gl.enableVertexAttribArray(positionAttributeLocation);
// Step 5: Set Up Render State and Draw
gl.clearColor(0.8, 0.8, 0.8, 1.0); // 步骤 5: 设置渲染状态并绘制
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}
// Run the main function when the page loads
window.onload = main; // 页面加载时运行主函数
</script>
</body>
</html>

在支持 WebGL 的浏览器中运行此代码,应该会显示一个水平居中、尖端朝上的纯橙色三角形,背景为浅灰色。