Skip to content

WebGL - 快速指南

本指南提供 WebGL 概念和基础知识的快速概述。

历史上,在 Web 上渲染 3D 图形需要浏览器插件,如 Java applets(使用 JOGL)或 Adobe Flash(使用 Stage3D)。这些方法面临兼容性、安全性和对专有技术的依赖等挑战。

为了提供一种标准化的、无需插件的方式来直接从 Web 浏览器访问 GPU(图形处理器),WebGL 应运而生。

OpenGL(Open Graphics Library)是一个历史悠久的跨平台 API,用于渲染 2D 和 3D 图形。WebGL 直接基于专为嵌入式系统设计的特定版本的 OpenGL。

API/技术描述
OpenGL ESOpenGL 的一个子集,专为移动电话、游戏机、车辆等嵌入式系统量身定制。WebGL 1.0 基于 OpenGL ES 2.0,WebGL 2.0 基于 OpenGL ES 3.0。由 Khronos Group (www.khronos.org) 维护。
WebGL一个 JavaScript API,通过 HTML <canvas> 元素向 Web 开发人员公开 OpenGL ES 功能。允许在兼容的 Web 浏览器中直接进行硬件加速图形渲染。WebGL 1.0 和 WebGL 2.0 是当前标准,由 Khronos Group 维护。
GLSLOpenGL 着色语言(OpenGL Shading Language)。一种类似于 C 的语言,用于编写着色器(shaders)——直接在 GPU 上运行的小程序,用于控制顶点位置和像素颜色。WebGL 使用 GLSL ES(适用于嵌入式系统的 GLSL)。

WebGL(Web Graphics Library)是一个 JavaScript API,用于在任何兼容的 Web 浏览器中渲染交互式 2D 和 3D 图形,无需使用插件。它通过 HTML <canvas> 元素提供对设备 GPU 的低级别访问来实现。

WebGL 与 HTML、CSS 和 JavaScript 等其他 Web 标准无缝集成,从而能够在 Web 应用程序中实现丰富的图形体验。

最初的工作始于 2006 年 Mozilla 的 Vladimir Vukićević,他进行了一项将 OpenGL 移植到 Canvas 元素的实验。这项工作后来演变成 Khronos Group 下的一个合作项目,涉及苹果、谷歌、微软和 Mozilla 等主要浏览器厂商。WebGL 1.0 于 2011 年 3 月正式发布。

WebGL 在现代桌面和移动浏览器中享有广泛的支持。

桌面浏览器(截至 2023 年末/2024 年初)

Section titled “桌面浏览器(截至 2023 年末/2024 年初)”
浏览器支持状态
Google Chrome是 (WebGL 1 & 2)
Mozilla Firefox是 (WebGL 1 & 2)
Apple Safari是 (WebGL 1 & 2)
Microsoft Edge是 (WebGL 1 & 2, 基于 Chromium)
Opera是 (WebGL 1 & 2, 基于 Chromium)

移动浏览器(截至 2023 年末/2024 年初)

Section titled “移动浏览器(截至 2023 年末/2024 年初)”
浏览器支持状态
Android 版 Chrome是 (WebGL 1 & 2)
iOS 版 Safari是 (WebGL 1 & 2)
Android 版 Firefox是 (WebGL 1 & 2)
Samsung Internet是 (WebGL 1 & 2)

注意:具体功能支持和性能可能有所不同。请查看 caniuse.com (https://caniuse.com/?search=webgl) 等资源获取详细、最新的信息。

  • 无需插件:在浏览器中原生运行,无需外部插件。
  • 硬件加速:利用 GPU 进行高性能图形渲染。
  • 跨平台:在兼容浏览器支持的各种操作系统和设备上工作。
  • JavaScript 集成:与标准 Web 技术(HTML、CSS、DOM、其他 JS 库)无缝集成。
  • 开放标准:由 Khronos Group 开发和维护,确保互操作性。
  • 不断发展的生态系统:由众多简化开发的库和框架(如 Three.js、Babylon.js)支持。

除了支持 WebGL 的现代 Web 浏览器和用于编写代码的文本编辑器外,无需特殊设置。浏览器开发者工具对于调试至关重要。

理解核心计算机图形概念是使用 WebGL 的基础。

渲染是指使用计算机程序从 2D 或 3D 模型描述生成 2D 图像的过程。此描述包含几何体(形状)、视点(相机)、纹理(表面外观)、光照和着色等信息。渲染管线接收此场景描述并生成最终用于显示的像素数据。

渲染类型:

  • 软件渲染(Software Rendering):计算主要由 CPU 执行。对于复杂场景通常较慢。
  • 硬件渲染(Hardware Rendering):计算被卸载到专用的图形处理器(GPU)。WebGL 利用硬件渲染来实现高性能。

WebGL 执行客户端硬件渲染。浏览器使用用户的 GPU 处理通过 JavaScript API 发送的图形指令。

GPU 是一种专用的电子电路,设计用于快速操作和更改内存,以加速在帧缓冲区中创建图像,用于输出到显示设备。GPU 具有高度并行性,这意味着它们可以同时执行许多计算,这使其非常适合图形渲染中涉及的重复性任务(例如处理数千个顶点或数百万个像素)。

与 WebGL 相关的 GPU 关键特性:

  • 并行处理:并发执行许多小程序(着色器)实例。
  • 专用内存(VRAM):拥有自己的高速内存,用于存储图形数据(纹理、顶点数据)。数据必须从主系统 RAM 传输到 VRAM,GPU 才能高效访问。
  • 专用管线:硬件针对特定的图形任务进行了优化,如变换、裁剪、光栅化和纹理映射。

在 WebGL 中,GPU 加速意味着浏览器将计算密集型图形任务(由你的 JavaScript 代码和 GLSL 着色器定义)卸载到 GPU。CPU 处理主应用程序逻辑,准备数据,并通过 WebGL API 向 GPU 发送绘制命令。

高效的 WebGL 开发涉及最大程度地减少 CPU 和 GPU 之间的通信开销。这通常意味着在初始化期间一次性将数据(顶点、纹理)上传到 GPU 内存(缓冲区),然后可以在多个绘制命令中引用这些数据,而不是为每一帧发送新数据。

HTML5 <canvas> 元素在 HTML 文档中提供了一个绘图表面。然后 JavaScript 可以访问此表面以动态绘制图形。WebGL 使用 <canvas> 元素作为其渲染目标。

<canvas> 元素本身很简单,主要由其 width 和 height 属性定义,这些属性指定绘图缓冲区的尺寸(以像素为单位)。

<canvas id="myCanvas" width="640" height="480">
<!-- Fallback content for browsers that don't support canvas -->
您的浏览器不支持 HTML canvas 元素。
</canvas>
  • id:用于通过 JavaScript 访问元素的唯一标识符(例如,使用 document.getElementById)。
  • width:canvas 绘图表面的宽度,以 CSS 像素为单位。
  • height:canvas 绘图表面的高度,以 CSS 像素为单位。

重要:通常建议直接在 <canvas> 标签上设置 width 和 height 属性,而不是使用 CSS。通过 CSS 设置尺寸会缩放 canvas 元素及其内容,可能导致模糊,而设置属性会定义绘图缓冲区的实际大小。

此 HTML 创建一个 300x300 的 canvas,并带有红色边框以便查看。

<!DOCTYPE html>
<html>
<head>
<title>Canvas Example</title>
<style>
#myRenderingCanvas {
border: 1px solid red;
}
</style>
</head>
<body>
<canvas id="myRenderingCanvas" width="300" height="300"></canvas>
</body>
</html>

这将显示一个带有红色边框的空白 300x300 正方形框。

<canvas> 元素只是一个容器。要在其上绘图,你需要使用 JavaScript 中的 getContext() 方法获取渲染上下文。

常见上下文类型:

  • ‘2d’:提供 CanvasRenderingContext2D 接口,用于绘制 2D 形状、文本和图像。
  • ‘webgl’:提供 WebGLRenderingContext 接口(基于 OpenGL ES 2.0),用于硬件加速的 2D 和 3D 图形。
  • ‘webgl2’:提供 WebGL2RenderingContext 接口(基于 OpenGL ES 3.0),提供更高级的功能。
const canvas = document.getElementById('myRenderingCanvas');
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100); // Draw a green rectangle
} else {
console.error('2D context not supported');
}

要使用 WebGL,你需要请求 'webgl' 或 'webgl2' 上下文。检查是否成功获取上下文是一个好习惯,因为 WebGL 可能不受支持或未启用。

const canvas = document.getElementById('myRenderingCanvas');
// Try getting WebGL 2, fallback to WebGL 1
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) {
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
// Handle the error appropriately
} else {
// WebGL context obtained successfully! Start setting up.
gl.clearColor(0.9, 0.9, 0.8, 1.0); // Set clear color to light gray
gl.clear(gl.COLOR_BUFFER_BIT); // Clear the canvas
console.log('WebGL initialized successfully!');
}

获取 WebGL 上下文后(通常存储在名为 gl 的变量中),你可以使用其方法来配置渲染管线、创建缓冲区、编译着色器和绘制图形。

有关 2D Canvas API 的更多详细信息,请参阅 MDN Web Docs:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API

WebGL 提供了基于 OpenGL ES 的低级 API。理解其坐标系以及顶点、缓冲区和着色器等基本概念至关重要。

顶点处理后,WebGL 要求坐标位于一个称为**裁剪空间(Clip Space)**的系统中。这是一个在所有三个轴(X、Y、Z)上范围从 -1 到 +1 的立方体。

  • X 轴:+1 向右,-1 向左。
  • Y 轴:+1 向上,-1 向下。
  • Z 轴:+1 朝向观察者(更近),-1 远离观察者(更远)。如果你期望 Z 随着深度增加而增加,这可能看起来违反直觉,但这是 OpenGL/WebGL 裁剪空间中的标准约定。

在此 [-1, +1] 立方体之外的任何维度的任何内容都将被“裁剪”(丢弃)而不予渲染。你的顶点着色器主要职责是将模型的原始坐标(通常称为模型空间或世界空间)变换到此裁剪空间,通常通过将其与模型、视图和投影矩阵相乘。

轴的文本表示:

+Y
|
|
-X ----+---- +X
/|
/ |
+Z -Y (Towards Viewer)

WebGL 编程中的关键术语和概念:

顶点(单数:vertex,复数:vertices)是 2D 或 3D 空间中的一个点,通常由其坐标(X、Y、Z)定义。顶点是几何形状(如三角形、线条、点)的基本构建块。顶点还可以具有关联的数据,如颜色、纹理坐标或法线(指示表面方向的向量,用于光照)。

// Data for three vertices (X, Y, Z)
const triangleVertices = [
0.0, 0.5, 0.0, // Top vertex
-0.5, -0.5, 0.0, // Bottom-left vertex
0.5, -0.5, 0.0 // Bottom-right vertex
];

索引(索引缓冲区)(Indices (Index Buffer))

Section titled “索引(索引缓冲区)(Indices (Index Buffer))”

通常,网格中的多个多边形共享相同的顶点。为避免重复顶点数据,我们可以使用索引缓冲区(index buffer)。我们在顶点缓冲区中定义每个唯一的顶点一次。然后,索引缓冲区包含一个整数列表(索引),指定从顶点缓冲区中绘制顶点的顺序,以形成基本图元(primitive)(如三角形)。这节省了内存并可以提高性能。

示例(使用 4 个顶点的四边形的索引):

Section titled “示例(使用 4 个顶点的四边形的索引):”
const quadVertices = [
-0.5, 0.5, 0.0, // Vertex 0: Top-left
-0.5, -0.5, 0.0, // Vertex 1: Bottom-left
0.5, -0.5, 0.0, // Vertex 2: Bottom-right
0.5, 0.5, 0.0 // Vertex 3: Top-right
];
// Indices define two triangles (0,1,2) and (0,2,3) to form the quad
const quadIndices = [
0, 1, 2, // First triangle
0, 2, 3 // Second triangle
];

WebGL 要求几何数据(顶点、索引、颜色等)以类型化数组(Typed Arrays)(例如,Float32Array、Uint16Array)形式传递。这些是 JavaScript 对象,提供了访问原始二进制数据的机制,这对于高效传输到 GPU 至关重要。

const vertices = new Float32Array([ 0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ]);
const indices = new Uint16Array([ 0, 1, 2 ]);

缓冲区(VBO 和 IBO)(Buffers (VBOs and IBOs))

Section titled “缓冲区(VBO 和 IBO)(Buffers (VBOs and IBOs))”

WebGL 缓冲区是 GPU 管理的内存块,用于存储大量图形数据。JavaScript 类型化数组中的数据被复制到这些缓冲区中。

  • **顶点缓冲区对象(Vertex Buffer Object,VBO):**存储每个顶点的数据,如位置、颜色、纹理坐标、法线。数据通常绑定到 gl.ARRAY_BUFFER 目标。
  • **索引缓冲区对象(Index Buffer Object,IBO):**存储顶点索引,用于与 gl.drawElements 一起使用。数据通常绑定到 gl.ELEMENT_ARRAY_BUFFER 目标。
  • **帧缓冲区对象(Frame Buffer Object,FBO):**表示渲染目标(颜色缓冲区、深度缓冲区、模板缓冲区)的集合。默认情况下,WebGL 绘制到 canvas 的主帧缓冲区,但 FBO 允许进行离屏渲染,这对于后处理或反射等效果非常有用。

使用缓冲区对于性能至关重要,因为它最大限度地减少了 CPU (JavaScript) 和 GPU 之间的数据传输。通常在初始化期间上传数据一次。

使用 gl.createBuffer() 请求 WebGL 分配一个缓冲区对象。

const vertexBuffer = gl.createBuffer();
const indexBuffer = gl.createBuffer();

使用 gl.bindBuffer(target, buffer) 将缓冲区在特定目标绑定点上设为“活动”。随后对该目标进行的缓冲区操作将影响绑定的缓冲区。

  • target:对于顶点数据,为 gl.ARRAY_BUFFER;对于索引数据,为 gl.ELEMENT_ARRAY_BUFFER。
  • buffer:由 gl.createBuffer() 返回的缓冲区对象。
// Bind the VBO
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// Bind the IBO
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

使用 gl.bufferData(target, data, usage) 将数据从 JavaScript 类型化数组复制到当前绑定到 GPU 的缓冲区。

  • target:与 bindBuffer 中的相同(gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER)。
  • data:包含数据的类型化数组(例如,new Float32Array(...))。
  • usage:一个提示,告诉 WebGL 你打算如何使用这些数据。这有助于驱动程序优化内存放置。常见值:
    • gl.STATIC_DRAW:数据设置一次,多次使用(例如,静态模型几何体)。最常见。
    • gl.DYNAMIC_DRAW:数据频繁更新,多次使用(例如,动画角色顶点)。
    • gl.STREAM_DRAW:数据设置一次,仅使用几次。
// Assuming 'vertices' is a Float32Array and 'indices' is a Uint16Array
// Transfer vertex data to the bound ARRAY_BUFFER (vertexBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Transfer index data to the bound ELEMENT_ARRAY_BUFFER (indexBuffer)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

4. 解绑缓冲区(可选但良好的实践):

Section titled “4. 解绑缓冲区(可选但良好的实践):”

你可以通过将 null 绑定到目标来解绑缓冲区:gl.bindBuffer(gl.ARRAY_BUFFER, null);。这可以防止以后意外修改缓冲区。

gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

数据进入缓冲区后,你需要使用属性(attributes)(下一节介绍)告诉着色器如何访问这些数据。

序号方法描述
1createBuffer()创建一个新的、空的 WebGL 缓冲区对象。
2deleteBuffer(buffer)删除一个 WebGL 缓冲区对象,释放 GPU 内存。
3bindBuffer(target, buffer)将一个缓冲区对象绑定到一个目标(gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER)。
4bufferData(target, data, usage)将数据从类型化数组(data)复制到当前绑定到 target 的缓冲区。指定 usage 提示。
5bufferSubData(target, offset, data)更新现有缓冲区的一部分数据,而无需重新分配整个缓冲区。
6isBuffer(buffer)如果提供的对象是有效的 WebGL 缓冲区,则返回 true。
7getBufferParameter(target, pname)检索关于绑定到 target 的缓冲区的信息(例如,pname = gl.BUFFER_SIZE)。

着色器是 WebGL 中可编程图形的核心。它们是使用 GLSL ES 编写的在 GPU 上运行的小程序,用于定义顶点变换和像素颜色。

GLSL ES(OpenGL Shading Language for Embedded Systems)是一种类似于 C 的语言,具有专为图形处理定制的功能。

序号类型描述
1void表示无值(用于函数返回类型)。
2bool布尔值(true 或 false)。
3int有符号整数。
4float浮点数(需要指定精度)。
5vec2, vec3, vec4具有 2、3 或 4 个分量的浮点向量(例如,用于位置、颜色、纹理坐标)。
6bvec2, bvec3, bvec4布尔向量。
7ivec2, ivec3, ivec4有符号整数向量。
8mat2, mat3, mat42x2、3x3、4x4 浮点矩阵(用于变换)。
9sampler2D表示一个 2D 纹理单元,用于访问纹理数据。
10samplerCube表示一个立方体贴图纹理单元。

存储限定符(WebGL 1 / GLSL ES 1.00):

Section titled “存储限定符(WebGL 1 / GLSL ES 1.00):”
序号限定符描述
1attribute顶点着色器输入。链接来自 WebGL 缓冲区的每个顶点的数据。值随每个顶点调用而变化。
2uniform顶点/片段着色器输入。链接来自 JavaScript 的绘制调用中保持不变的数据(gl.uniform*)。在着色器中是只读的。示例:uniform mat4 uProjectionMatrix;
3varying链接从顶点着色器输出传递到片段着色器输入的数据。值在图元表面上进行插值。示例:varying vec3 vColor;
4const在着色器中声明一个编译时常量变量。

指定 float 和 int 类型的计算精度:highp、mediump 和 lowp。片段着色器中强制要求默认浮点精度(precision mediump float;)。可以按变量指定精度。

// Fragment Shader Snippet
precision mediump float;
varying vec3 vColor; // Interpolated color from vertex shader
void main() {
gl_FragColor = vec4(vColor, 1.0);
}

GLSL 提供特殊的内置变量,用于与关键管线交互:

  • 顶点着色器输出:
    • gl_Position (highp vec4):必须写入。裁剪空间中的最终顶点位置。
    • gl_PointSize (mediump float):可选。设置以 gl.POINTS 绘制的图元的大小(以像素为单位)。
  • 片段着色器输入/输出:
    • gl_FragCoord (mediump vec4):只读输入。包含片段的窗口相关坐标 (x, y, z, 1/w)。
    • gl_FrontFacing (bool):只读输入。如果片段属于正面图元,则为 True(对于双面光照有用)。
    • gl_PointCoord (mediump vec2):只读输入。包含点图元内的坐标(对于纹理映射点有用)。
    • gl_FragColor (mediump vec4):在 WebGL 1 中必须写入。片段的最终 RGBA 输出颜色。
    • gl_FragData[n] (mediump vec4):输出数组,用于 WebGL 1 中的多重渲染目标(MRT)扩展 / WebGL 2 中的标准功能。
// GLSL ES 1.00 Vertex Shader
attribute vec3 aVertexPosition; // Input: Per-vertex position
uniform mat4 uModelViewMatrix; // Input: Model-view transform
uniform mat4 uProjectionMatrix; // Input: Projection transform
varying lowp vec4 vColor; // Output: Pass color to fragment shader
void main(void) {
// Calculate final clip space position
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
// Example: Set a fixed color to pass (could also be an attribute)
vColor = vec4(0.5, 0.0, 0.5, 1.0); // Purple
}
// GLSL ES 1.00 Fragment Shader
precision mediump float; // Mandatory precision for floats
varying lowp vec4 vColor; // Input: Interpolated color from vertex shader
void main(void) {
// Output the interpolated color
gl_FragColor = vColor;
}

在 JavaScript 中编译和链接着色器

Section titled “在 JavaScript 中编译和链接着色器”

你需要 JavaScript 代码来获取 GLSL 字符串,将其发送到 GPU 进行编译,然后将它们链接到一个可用的 WebGL 程序。

  • **1. 创建着色器对象:**使用 gl.createShader(type),其中 type 是 gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER。
  • **2. 附加源代码:**使用 gl.shaderSource(shader, sourceString) 提供 GLSL 代码。
  • **3. 编译着色器:**使用 gl.compileShader(shader)。
  • **4. 检查编译状态:**使用 gl.getShaderParameter(shader, gl.COMPILE_STATUS) 和 gl.getShaderInfoLog(shader) 检查错误。这对于调试至关重要!
  • **5. 创建程序对象:**使用 gl.createProgram()。
  • **6. 附加着色器:**使用 gl.attachShader(program, vertexShader) 和 gl.attachShader(program, fragmentShader)。
  • **7. 链接程序:**使用 gl.linkProgram(program)。
  • **8. 检查链接状态:**使用 gl.getProgramParameter(program, gl.LINK_STATUS) 和 gl.getProgramInfoLog(program) 检查错误。
  • **9. 使用程序:**在绘制前使用 gl.useProgram(program) 激活此着色器程序。

示例(来自示例应用程序的辅助函数):

Section titled “示例(来自示例应用程序的辅助函数):”
// Function to load and compile a single shader
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`Error compiling ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`, gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Function to initialize the shader program
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
if (!vertexShader || !fragmentShader) return null;
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('Error linking shader program:', gl.getProgramInfoLog(shaderProgram));
gl.deleteProgram(shaderProgram);
return null;
}
return shaderProgram;
}

请记住为着色器编译和程序链接实现健壮的错误检查,因为在开发过程中 GLSL 错误很常见。

关联属性与缓冲区对象 (Associating Attributes & Buffer Objects)

Section titled “关联属性与缓冲区对象 (Associating Attributes & Buffer Objects)”

创建包含顶点数据的缓冲区并编译/链接着色器程序后,你需要告诉 WebGL 如何将缓冲区中的数据与顶点着色器中声明的 attribute 变量对应起来。

这涉及三个主要步骤:

你的着色器程序中的每个 attribute 变量都有一个在程序链接期间由 WebGL 分配的特定位置(一个索引)。你需要使用 gl.getAttribLocation(program, name) 查询此位置。你将在后续步骤中使用此位置索引。

  • program:已链接的 WebGL 着色器程序对象。
  • name:一个字符串,包含顶点着色器中 attribute 变量的精确名称(例如,'aVertexPosition'、'aVertexColor')。

它返回属性的整数位置,如果未找到或不活动(可能被编译器优化掉了,因为它未使用),则返回 -1。务必检查位置是否有效(>= 0)!

const shaderProgram = /* ... your linked program ... */;
const positionAttribLocation = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
const colorAttribLocation = gl.getAttribLocation(shaderProgram, 'aVertexColor');
if (positionAttribLocation < 0) {
console.error("Attribute 'aVertexPosition' not found in shader!");
}
if (colorAttribLocation < 0) {
console.error("Attribute 'aVertexColor' not found in shader!");
}

2. 将属性指向顶点缓冲区对象(VBO)

Section titled “2. 将属性指向顶点缓冲区对象(VBO)”

这是核心步骤,你在此指定 WebGL 应如何解释当前绑定的 ARRAY_BUFFER 中的原始字节数据,以便将数据输入到特定的 attribute 中。这是通过 gl.vertexAttribPointer(location, size, type, normalized, stride, offset) 完成的。

参数:

  • location:attribute 的整数位置,从 getAttribLocation 获取。
  • size:每个顶点 attribute 的分量数量(例如,vec3 位置 [x, y, z] 为 3,vec4 颜色 [r, g, b, a] 为 4)。
  • type:缓冲区中每个分量的数据类型(例如,gl.FLOAT、gl.UNSIGNED_BYTE、gl.SHORT)。必须与 bufferData 中使用的数据类型匹配。
  • normalized:布尔值。对于非浮点数据类型(如 gl.BYTE、gl.UNSIGNED_BYTE),指定当被着色器访问时,是否将值归一化到 [-1, 1](对于有符号类型)或 [0, 1](对于无符号类型)的范围。对于浮点数通常为 false。
  • stride:连续顶点 attribute 之间的字节偏移量。如果你的属性是紧密打包的(如 [P1x, P1y, P1z, P2x, P2y, P2z,…]),步长为 0。如果数据是交错的(如 [P1x, P1y, C1r, C1g, P2x, P2y, C2r, C2g,…]),步长是从一个顶点数据开始到下一个顶点数据开始的字节数。
  • offset:缓冲区中此 attribute 数据开始的字节偏移量。如果缓冲区仅包含此 attribute 的数据,通常为 0;如果数据是交错的,则为偏移量。

重要:在调用 gl.vertexAttribPointer 之前,你必须使用 gl.bindBuffer() 将相关的 VBO 绑定到 gl.ARRAY_BUFFER。

const positionBuffer = /* ... buffer with vertex positions ... */;
const positionAttribLocation = /* ... location of 'aVertexPosition' ... */;
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // Bind the VBO
gl.vertexAttribPointer(
positionAttribLocation, // Attribute location
3, // size: 3 components per vertex (X, Y, Z)
gl.FLOAT, // type: the data is 32bit floats
false, // normalized: don't normalize
0, // stride: 0 = tightly packed
0 // offset: 0 = start at the beginning
);
// Buffer contains [PosX1, PosY1, PosZ1, ColR1, ColG1, ColB1, PosX2, PosY2, PosZ2, ColR2, ColG2, ColB2, ...]
const interleavedBuffer = /* ... buffer with interleaved data ... */;
const positionAttribLocation = /* ... location of 'aVertexPosition' ... */;
const colorAttribLocation = /* ... location of 'aVertexColor' ... */;
const bytesPerFloat = 4;
const stride = 6 * bytesPerFloat; // 6 floats per vertex (3 pos + 3 color)
gl.bindBuffer(gl.ARRAY_BUFFER, interleavedBuffer);
// Setup position attribute
gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, stride, 0);
// Setup color attribute
const colorOffset = 3 * bytesPerFloat; // Color data starts after 3 position floats
gl.vertexAttribPointer(colorAttribLocation, 3, gl.FLOAT, false, stride, colorOffset);

最后,你必须使用 gl.enableVertexAttribArray(location) 启用顶点 attribute 数组。这告诉 WebGL 在调用 drawArrays 或 drawElements 时,实际根据 vertexAttribPointer 定义的设置从缓冲区拉取数据。

gl.enableVertexAttribArray(positionAttribLocation);
// If using color attribute as well:
// gl.enableVertexAttribArray(colorAttribLocation);

如果需要,你可以稍后使用 gl.disableVertexAttribArray(location) 禁用 attribute。

顶点数组对象(VAOs)- WebGL 2 / 扩展

Section titled “顶点数组对象(VAOs)- WebGL 2 / 扩展”

为每个对象使用 bindBuffer、vertexAttribPointer 和 enableVertexAttribArray 设置多个 attribute 可能很重复。顶点数组对象(Vertex Array Objects,VAOs)(在 WebGL 2 中可用,或通过 WebGL 1 中的 OES_vertex_array_object 扩展可用)允许你将所有这些 attribute 配置状态封装到一个对象中。你设置好 VAO 一次,然后在绘制对象之前只需绑定 VAO,这样可以显著减少每次绘制的 WebGL 调用次数。

虽然可能超出绝对基础知识的范畴,但 VAO 对于具有许多对象或复杂顶点设置的应用程序来说是提高性能的关键最佳实践。有关详细信息,请参阅 MDN:https://developer.mozilla.org/en-US/docs/Web/API/WebGLVertexArrayObject

一旦你初始化了 WebGL,创建了缓冲区,编译了着色器,并配置了顶点属性,最后一步就是发出绘制命令来渲染你的几何体。

两个主要的绘制命令是 gl.drawArrays() 和 gl.drawElements()。

当你的 ARRAY_BUFFER 中的顶点已经按照正确的顺序排列以形成所需的基本图元,并且你没有使用索引缓冲区(ELEMENT_ARRAY_BUFFER)时,使用 drawArrays。

void gl.drawArrays(mode, first, count);

参数:

  • mode:指定要渲染的基本图元的类型。常见值:
    • gl.POINTS:渲染单个点。
    • gl.LINES:渲染单个线段(顶点 0-1, 2-3 等)。
    • gl.LINE_STRIP:渲染连接的线段(0-1, 1-2, 2-3, …)。
    • gl.LINE_LOOP:类似于 LINE_STRIP,但也会将最后一个顶点连接回第一个顶点。
    • gl.TRIANGLES:渲染单个三角形(顶点 0-1-2, 3-4-5 等)。
    • gl.TRIANGLE_STRIP:渲染连接的三角形(0-1-2, 1-2-3, 2-3-4, …)。对于网格很高效。
    • gl.TRIANGLE_FAN:渲染共享第一个顶点的连接三角形(0-1-2, 0-2-3, 0-3-4, …)。适用于圆形/圆锥体。
  • first:启用顶点属性数组中的起始索引。
  • count:要渲染的顶点数量。
// Assumes a VBO with 3 vertices is bound and attributes are configured
const vertexCount = 3;
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);

示例(绘制两个独立的三角形):

Section titled “示例(绘制两个独立的三角形):”
// Assumes a VBO with 6 vertices (3 for first triangle, 3 for second)
const vertexCount = 6;
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);

当你已经在 ARRAY_BUFFER (VBO) 中定义了唯一的顶点,并且正在使用包含索引的 ELEMENT_ARRAY_BUFFER (IBO) 来指定绘制这些顶点的顺序时,使用 drawElements。

这对于复杂的网格通常更高效,因为它避免了重复的顶点数据。

void gl.drawElements(mode, count, type, offset);

参数:

  • mode:与 drawArrays 相同基本图元类型(例如,gl.TRIANGLES)。
  • count:要从索引缓冲区渲染的索引数量。
  • type:索引缓冲区中值的类型。必须是 gl.UNSIGNED_BYTE、gl.UNSIGNED_SHORT,或者(带有扩展/WebGL 2)gl.UNSIGNED_INT。这必须与用于索引缓冲区的类型化数组类型(例如,Uint16Array 对应 gl.UNSIGNED_SHORT)匹配。
  • offset:ELEMENT_ARRAY_BUFFER 中索引开始的字节偏移量。通常为 0。

重要:在调用 gl.drawElements 之前,你必须使用 gl.bindBuffer() 将相关的 IBO 绑定到 gl.ELEMENT_ARRAY_BUFFER。

示例(使用 4 个顶点和 6 个索引绘制一个四边形):

Section titled “示例(使用 4 个顶点和 6 个索引绘制一个四边形):”
// Assume VBO with 4 vertices is bound to ARRAY_BUFFER and attributes configured
// Assume IBO with 6 indices [0,1,2, 0,2,3] is bound to ELEMENT_ARRAY_BUFFER
const indexCount = 6;
const indexType = gl.UNSIGNED_SHORT; // Assuming Uint16Array was used for indices
const offset = 0;
gl.drawElements(gl.TRIANGLES, indexCount, indexType, offset);

在每一帧(或每次绘制一个对象)调用 drawArrays 或 drawElements 之前,你通常需要执行这些设置操作:

使用 gl.clearColor(r, g, b, a) 设置背景颜色(值范围 0.0 到 1.0)。然后使用 gl.clear(mask) 实际清空缓冲区。

mask 是以下各项的按位或组合:

  • gl.COLOR_BUFFER_BIT:清空颜色缓冲区。
  • gl.DEPTH_BUFFER_BIT:清空深度缓冲区。
  • gl.STENCIL_BUFFER_BIT:清空模板缓冲区。
gl.clearColor(0.1, 0.1, 0.2, 1.0); // Dark slate blue
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear color and depth

对于 3D 渲染,启用深度测试以使更靠近摄像头的对象能够正确遮挡更远的对象:gl.enable(gl.DEPTH_TEST);。你也可以使用 gl.depthFunc(gl.LEQUAL);(默认值)设置深度函数。

gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL); // Default, but good to be explicit

使用 gl.viewport(x, y, width, height) 定义 WebGL 应在 canvas 上绘制的矩形区域。这会将归一化的 [-1, +1] 裁剪空间坐标映射到 canvas 上的像素坐标。

// Typically set to the full canvas size
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

激活所需的着色器程序:gl.useProgram(yourShaderProgram);

5. 绑定缓冲区并设置属性/统一变量:

Section titled “5. 绑定缓冲区并设置属性/统一变量:”

确保绑定了正确的 VBO(gl.bindBuffer(gl.ARRAY_BUFFER, ...)),设置了属性指针(gl.vertexAttribPointer,gl.enableVertexAttribArray),如果使用 drawElements,则绑定了正确的 IBO(gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ...)),并且更新了当前对象/帧所需的任何统一变量(如变换矩阵、颜色、纹理单元)(gl.uniform*)。

完成这些设置步骤后,你最终可以调用 gl.drawArrays(...) 或 gl.drawElements(...)。