WebGL 的渲染管道和编程接口

162 阅读1小时+

WebGL (Web Graphics Library) 是一个基于 OpenGL ES 的 Web 标准,它允许在浏览器中进行硬件加速的图形渲染,广泛应用于游戏、数据可视化、CAD 等场景。本文将介绍 WebGL 的渲染管道工作流程,以及其提供的核心编程接口和能力。

从 OpenGL 到 WebGL

WebGL 是 OpenGL ES 在浏览器环境中的 JavaScript 绑定,使得 Web 应用能够通过标准 API 访问 GPU 进行硬件加速渲染。

如何将 OpenGL 编译成 WebGL

Emscripten 可以将 C/C++ 的 OpenGL 代码编译为 WebAssembly + WebGL:

# 编译 OpenGL C++ 代码为 WebAssembly
emcc main.cpp -o output.html -s USE_WEBGL2=1 -s FULL_ES3=1

Emscripten 的工作原理:

  • 将 C++ 代码编译为 WebAssembly (wasm)
  • 自动生成 JavaScript 胶水代码 (glue code)
  • 将 OpenGL ES 调用转换为 WebGL 调用
  • 处理内存管理和指针映射

这种方式适合将现有的 OpenGL 应用快速移植到 Web。更多编译选项和配置细节请参考 Emscripten OpenGL 支持文档

WebGL1 与 WebGL2 的区别

WebGL1 基于 OpenGL ES 2.0,WebGL2 基于 OpenGL ES 3.0。两者的渲染管道结构相同,但 WebGL2 增强了管道各阶段的能力。下表列出了主要差异,完整的功能对比和技术细节请参考 WebGL2 规范

渲染管道增强

管道阶段WebGL1WebGL2 新增
顶点处理顶点着色器Transform Feedback(可捕获顶点数据回传到缓冲区)
片段处理单一渲染目标多渲染目标 MRT(同时输出到多个颜色附件)
纹理采样2D 纹理、立方体贴图3D 纹理、2D 纹理数组、采样器对象
着色器语言GLSL ES 1.00GLSL ES 3.00(支持整数运算、位运算、更多内置函数)

核心功能对比

功能WebGL1WebGL2
顶点数组对象 (VAO)需要扩展 OES_vertex_array_object原生支持
实例化渲染需要扩展 ANGLE_instanced_arrays原生支持 drawArraysInstanced
多重采样不支持支持 MSAA 抗锯齿
Uniform 缓冲对象不支持支持 UBO,减少 Uniform 上传开销
整数纹理不支持支持 INTUNSIGNED_INT 纹理格式
深度纹理需要扩展 WEBGL_depth_texture原生支持
非 2 次幂纹理限制严格(不能 Mipmap、必须 CLAMP_TO_EDGE)完全支持

浏览器支持

WebGL1 在所有现代浏览器中均得到支持,WebGL2 在大部分现代浏览器中支持(Chrome 56+, Firefox 51+, Safari 15+, Edge 79+),iOS Safari 在 15 之前不支持。最新的浏览器兼容性数据请参考 Can I use WebGLCan I use WebGL2

WebGL 上下文创建和配置

创建 WebGL 上下文时可以通过 WebGLContextAttributes 配置默认帧缓冲(Default Framebuffer)的缓冲区、性能偏好、抗锯齿等选项。这些配置在上下文创建后无法修改,影响渲染管道的行为和性能。

注意:这些配置只影响默认帧缓冲(直接渲染到 Canvas 的帧缓冲),不影响自定义的 FBO(通过 createFramebuffer 创建的帧缓冲对象)。自定义 FBO 的深度、模板、抗锯齿等需要单独配置附件。

创建 WebGL 上下文

const canvas = document.getElementById("canvas");

// 创建 WebGL2 上下文,配置深度缓冲和抗锯齿
const gl = canvas.getContext("webgl2", {
  ...WebGLContextAttributes,
});

if (!gl) {
  console.error("WebGL2 not supported, fallback to WebGL1");
  gl = canvas.getContext("webgl", {
    /* ... */
  });
}

// 获取实际使用的上下文属性
const attrs = gl.getContextAttributes();
console.log("Context attributes:", attrs);

WebGLContextAttributes 属性说明

属性说明
alpha默认帧缓冲是否包含 alpha 通道(默认 true)。false 时默认帧缓冲背景不透明,无法与 HTML 元素混合
depth默认帧缓冲是否创建深度缓冲区(默认 true)。false 时无法对默认帧缓冲执行深度测试(gl.DEPTH_TEST 无效)
stencil默认帧缓冲是否创建模板缓冲区(默认 false)。false 时无法对默认帧缓冲执行模板测试(gl.STENCIL_TEST 无效)
antialias默认帧缓冲是否启用 MSAA 抗锯齿(默认 true)。实际抗锯齿效果由浏览器和硬件决定,可能被忽略
premultipliedAlpha默认帧缓冲的 alpha 通道是否预乘(默认 true)。true 时片段着色器输出 (r*a, g*a, b*a, a)false 时输出 (r, g, b, a)
preserveDrawingBuffer默认帧缓冲是否在渲染后保留内容(默认 false)。false 时每帧渲染后内容未定义(可能被清空),true 时内容保留到下一帧
powerPreferenceGPU 选择偏好(默认 default)。可选high-performance low-power default
failIfMajorPerformanceCaveat如果系统性能较差(如使用软件渲染)是否创建失败(默认 false)。true 时在检测到性能问题时 getContext() 返回 null
desynchronized是否禁用垂直同步(VSync)(默认 false)。true 时渲染不等待显示器刷新,可降低输入延迟但可能导致画面撕裂

参考文档

渲染管道

渲染管道 (Rendering Pipeline) 是 GPU 将顶点数据转换为屏幕像素的处理流程。本章基于 Khronos OpenGL 官方文档 Rendering Pipeline Overview 介绍 WebGL 的渲染管道。

根据 Khronos 官方定义,OpenGL/WebGL 渲染管道包含以下阶段:

  1. Vertex Specification(顶点规范)
  2. Vertex Processing(顶点处理)
  3. Vertex Post-Processing(顶点后处理)
  4. Rasterization(光栅化)
  5. Fragment Processing(片段处理)
  6. Per-Sample Operations(逐样本操作)
  7. Framebuffer(帧缓冲)

Vertex Specification(顶点规范)

定义顶点数据的格式和来源,通过以下 API 配置:

// 创建并绑定缓冲区
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([...]), gl.STATIC_DRAW);

// 指定顶点属性格式
const positionLoc = gl.getAttribLocation(program, 'aPosition');
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc);

此阶段不处理数据,仅描述数据结构。

Vertex Processing(顶点处理)

可编程阶段,通过 Vertex Shader 处理每个顶点。

  • 输入:Attribute 变量(顶点位置、法线、UV 等)、Uniform 变量(变换矩阵等)
  • 输出gl_Position(裁剪空间坐标,必需)、Varying 变量(传递给 Fragment Shader)
// Vertex Shader 示例(GLSL ES 1.00)
attribute vec3 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uMVPMatrix;
varying vec2 vTexCoord;

void main() {
  gl_Position = uMVPMatrix * vec4(aPosition, 1.0);  // 输出裁剪空间坐标
  vTexCoord = aTexCoord;
}

Vertex Post-Processing(顶点后处理)

固定功能阶段,执行以下操作:

  1. Transform Feedback(WebGL2 支持):捕获顶点着色器输出的数据回传到缓冲区,用于 GPU 粒子系统等场景
// WebGL2 示例
const transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
gl.transformFeedbackVaryings(program, ["vPosition"], gl.SEPARATE_ATTRIBS);
  1. Primitive Assembly(图元装配):将顶点序列组装成图元(三角形、线段或点),类型由 gl.drawArrays(mode, ...)mode 参数决定
gl.drawArrays(gl.TRIANGLES, 0, 6); // 三角形(每 3 个顶点)
gl.drawArrays(gl.LINES, 0, 4); // 线段(每 2 个顶点)
gl.drawArrays(gl.POINTS, 0, 10); // 点
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 8); // 三角形带
  1. Clipping(裁剪):裁剪超出视锥体的图元,丢弃完全在视锥体外的图元,对部分在视锥体内的图元进行裁剪。判断条件:-w ≤ x,y,z ≤ w

  2. Perspective Divide(透视除法/归一化设备坐标-NDC 空间):将裁剪空间坐标 (x, y, z, w) 转换为 NDC 坐标 (x/w, y/w, z/w),NDC 坐标范围为 [-1, 1]

  3. Viewport Transform(视口变换/屏幕空间):将 NDC 坐标 [-1, 1] 映射到屏幕像素坐标。通过 gl.viewport() 设置映射到的屏幕像素范围

  4. Face Culling(面剔除):根据三角形的正反面剔除背面三角形(需通过 gl.enable(gl.CULL_FACE) 启用)

gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK); // 剔除背面(默认)
gl.frontFace(gl.CCW); // 逆时针为正面(默认)

Rasterization(光栅化)

将图元转换为片段 (Fragment),确定图元覆盖哪些像素。

  1. Scan Conversion(扫描转换):确定图元覆盖的像素位置。输入几何图元,输出像素集合
  2. Interpolation(插值):对 Varying 变量进行插值,为每个片段生成插值后的数据
    • 例如:三角形三个顶点颜色为 (1,0,0)、(0,1,0)、(0,0,1),内部片段颜色为插值结果

Fragment Processing(片段处理)

可编程阶段,通过 Fragment Shader 处理每个片段,执行着色计算和逻辑判断。

输入:Varying 变量(插值后的数据)、Uniform 变量(纹理、光照参数等)

输出

  • 片段数据:
    • webgl1: 通过内置变量 gl_FragColor 输出颜色
      • gl_FragColor:颜色输出变量,输出后会经过 blend 后写入颜色缓冲
      • gl_FragDepth:深度输出变量,输出值直接写入深度缓冲
    • webgl2: 通过自定义 out 变量输出(单个颜色附件或多渲染目标 MRT)
  • 可选:通过 discard 丢弃片段
// Fragment Shader 示例(GLSL ES 1.00)
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;

void main() {
  gl_FragColor = texture2D(uTexture, vTexCoord);  // 纹理采样
}

Per-Sample Operations(逐样本操作)

固定功能阶段,执行一系列测试决定片段是否写入帧缓冲。

执行顺序(按照 OpenGL 规范):

  1. Scissor Test(裁剪测试)
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height); // 只渲染矩形区域
  1. Stencil Test(模板测试)
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.EQUAL, 1, 0xff); // 比较函数
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); // 操作
  1. Depth Test(深度测试)
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS); // 深度值更小时通过
  1. Blending(颜色混合)
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Alpha 混合
gl.blendEquation(gl.FUNC_ADD); // 混合方程
  1. Masking(写入遮罩):控制哪些数据可以写入帧缓冲。可以独立控制颜色、深度、模板的写入,甚至可以单独屏蔽颜色的各个通道
// 颜色遮罩:控制 RGBA 各通道是否可写
gl.colorMask(true, true, true, false); // 禁止写入 Alpha 通道

// 深度遮罩:控制深度缓冲区是否可写
gl.depthMask(false); // 禁止写入深度值

// 模板遮罩:控制模板缓冲区哪些位可写
gl.stencilMask(0xff); // 允许写入所有位

通过所有测试的片段,根据遮罩设置写入帧缓冲对应的附件。

Framebuffer(帧缓冲)

存储最终渲染结果。WebGL 支持两种帧缓冲:

  • 默认帧缓冲:直接渲染到 Canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  • 自定义帧缓冲 (FBO):离屏渲染(用于后处理、阴影贴图等)
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

帧缓冲包含多个附件 (Attachment):

  • 颜色附件 (Color Attachment):存储颜色值
  • 深度附件 (Depth Attachment):存储深度值
  • 模板附件 (Stencil Attachment):存储模板值

顶点数据准备

数据准备接口用于将顶点数据从 CPU 传输到 GPU,并指定数据的格式和布局,为渲染管道的 Vertex Specification 阶段提供输入。

Buffer 对象的创建

Buffer 对象是 GPU 内存中的一块存储空间,用于存储顶点数据(位置、颜色、法线、UV 等)。

// 创建缓冲区对象
const buffer = gl.createBuffer();
  • createBuffer() 在 GPU 中分配一块内存空间,返回一个 Buffer 对象的引用
  • 此时只是创建了引用,尚未分配实际的存储空间

上传数据到 Buffer

将 JavaScript 的类型化数组数据复制到 GPU 内存,分为两个步骤:

步骤 1:绑定 Buffer

// 将 Buffer 绑定到 ARRAY_BUFFER 绑定点
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

步骤 2:上传数据

// 准备顶点数据
const vertices = new Float32Array([
  -0.5,
  -0.5,
  0.0, // 顶点 1
  0.5,
  -0.5,
  0.0, // 顶点 2
  0.0,
  0.5,
  0.0, // 顶点 3
]);

// 上传完整数据到当前绑定的 Buffer
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 或者只更新一部分数据
const offset = 12; // 字节偏移量
const newData = new Float32Array([1.0, 1.0, 0.0]);
gl.bufferSubData(gl.ARRAY_BUFFER, offset, newData);
  • 绑定只是配置时的中介bindBuffer 的作用是告诉 WebGL "接下来的配置操作作用于哪个 Buffer"
  • 使用提示(Usage Hint)

bufferData 的第三个参数告诉 GPU 数据的使用模式,GPU 驱动可以据此优化内存分配。

常量说明适用场景GPU 优化策略
gl.STATIC_DRAW数据一次上传,多次绘制静态模型存储在 GPU 显存中,提高读取速度
gl.DYNAMIC_DRAW数据多次修改,多次绘制动画模型存储在可快速更新的内存区域
gl.STREAM_DRAW数据一次上传,少量绘制临时数据存储在写入快但读取慢的缓冲区

配置顶点属性读取方式

告诉 GPU 如何从那个 Buffer 中解析数据,如何将 Buffer 映射到顶点着色器的 Attribute 变量。

// 绑定包含顶点数据的 Buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

// 配置位置属性(偏移 0)
const positionLoc = gl.getAttribLocation(program, "aPosition");
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, stride, 0);

// 配置颜色属性(偏移 3 个 float)
const colorLoc = gl.getAttribLocation(program, "aColor");
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, stride, 3 * 4);
  • 调用 vertexAttribPointer 前必须先 bindBuffer,否则无法知道从哪个 Buffer 读取数据
  • 后续渲染与当前绑定无关:绘制时使用的是 vertexAttribPointer 配置时记录的 Buffer 引用,不是当前绑定到 ARRAY_BUFFER 的 Buffer

vertexAttribPointer 和 drawArrays 的配合

gl.vertexAttribPointer(index, size, type, normalized, stride, offset)
gl.drawArrays(mode, first, count)
  • vertexAttribPointeroffset:顶点属性数组中第一个组件的字节偏移量(从当前绑定的 buffer 起始位置算起)
  • vertexAttribPointerstride:相邻两个顶点之间的字节间隔
  • drawArraysfirst:从第几个顶点开始读取(顶点索引,GPU 会自动计算:buffer起始 + offset + first * stride
  • drawArrayscount:绘制多少个顶点

启用顶点属性数组

从常量模式切换到数组模式,让顶点着色器从 Buffer 中读取数据。

// 配置完 vertexAttribPointer 后,启用数组模式
gl.enableVertexAttribArray(positionLoc);
gl.enableVertexAttribArray(colorLoc);

为什么需要启用?

  • vertexAttribPointer 只是配置了读取规则,但不会自动启用。默认处于常量模式。使用固定值 (0, 0, 0, 1)
  • 必须调用 enableVertexAttribArray 才能切换到数组模式,从 Buffer 读取数据

索引缓冲区:减少顶点数据冗余

通过索引复用顶点,避免重复存储,减少内存占用。

示例:绘制矩形

// 顶点数据(4 个顶点)
const vertices = 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: 左上
]);

// 索引数据(2 个三角形复用顶点)
const indices = new Uint16Array([
  0,
  1,
  2, // 第一个三角形
  0,
  2,
  3, // 第二个三角形
]);

// 上传顶点数据。此处省略。
// 上传索引数据
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

// 使用索引绘制
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

数据对比

方式顶点数据索引数据总计节省
drawArrays72 字节(6 顶点)72 字节-
drawElements48 字节(4 顶点)12 字节(6 索引)60 字节16.7%
// drawArrays:按顺序读取顶点
gl.drawArrays(gl.TRIANGLES, 0, 6);

// drawElements:按索引读取顶点
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

顶点数组对象:快速切换配置

VAO (Vertex Array Object) 保存所有顶点配置状态,一次绑定恢复所有状态,用于多模型渲染的快速切换。WebGL2 原生支持,WebGL1 需要扩展 OES_vertex_array_object

VAO 保存的状态

  • 所有 Attribute 的 vertexAttribPointer 配置(Buffer 引用、size、type、stride、offset)
  • 所有 Attribute 的启用状态(enableVertexAttribArray / disableVertexAttribArray
  • 当前绑定到 ELEMENT_ARRAY_BUFFER 的索引缓冲区

使用 VAO

  1. 创建 VAO
const vao = gl.createVertexArray();
  1. 配置 VAO
gl.bindVertexArray(vao);

// 配置顶点属性(配置会被 VAO 记录)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc);

// 绑定索引缓冲区(也会被 VAO 记录)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

// 解绑 VAO
gl.bindVertexArray(null);
  1. 绘制时只需绑定 VAO,自动恢复所有配置
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

纹理数据准备

纹理接口用于将图像数据上传到 GPU,并配置纹理的采样方式,为片段着色器提供纹理采样功能。

纹理对象的创建

Texture 对象是 GPU 内存中的一块存储空间,用于存储图像数据。

// 创建纹理对象
const texture = gl.createTexture();
  • createTexture() 在 GPU 中分配一块内存空间,返回一个 Texture 对象的引用
  • 此时只是创建了引用,尚未分配实际的存储空间

上传数据到 Texture

将图像数据复制到 GPU 内存中,分为两个步骤:

步骤 1:绑定 Texture

// 将 Texture 绑定到 TEXTURE_2D 绑定点
gl.bindTexture(gl.TEXTURE_2D, texture);

常用绑定点

  • gl.TEXTURE_2D:2D 纹理
  • gl.TEXTURE_CUBE_MAP:立方体贴图

步骤 2:上传数据

// 从 Image 对象上传纹理
const image = new Image();
image.onload = () => {
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(
    gl.TEXTURE_2D, // 目标
    0, // Mipmap 级别(0 为基础级别)
    gl.RGBA, // 内部格式
    gl.RGBA, // 源数据格式
    gl.UNSIGNED_BYTE, // 源数据类型
    image // 数据源
  );
};
image.src = "texture.png";

// 或从 TypedArray 上传纹理
const pixels = new Uint8Array([
  255,
  0,
  0,
  255, // 红色像素
  0,
  255,
  0,
  255, // 绿色像素
]);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  2,
  1, // 宽度、高度
  0, // 边界(必须为 0)
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  pixels
);

// 或部分更新纹理数据
gl.texSubImage2D(
  gl.TEXTURE_2D,
  0, // Mipmap 级别
  10,
  10, // x、y 偏移
  64,
  64, // 宽度、高度
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  partialImage
);

重要说明

  • 绑定只是配置时的中介bindTexture 的作用是告诉 WebGL "接下来的配置操作作用于哪个 Texture"
  • 后续渲染与当前绑定无关:绘制时使用的是 uniform1i 设置时指定的纹理单元绑定的 Texture,不是当前绑定到 TEXTURE_2D 的 Texture

配置纹理采样参数

告诉 GPU 如何从 Texture 中采样数据(过滤模式和环绕模式)。

// 绑定需要配置的 Texture
gl.bindTexture(gl.TEXTURE_2D, texture);

// 配置过滤模式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // 放大过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // 缩小过滤

// 配置环绕模式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); // S 方向(横向)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // T 方向(纵向)

:调用 texParameteri 前必须先 bindTexture,否则无法知道配置哪个 Texture

过滤模式

常量说明
gl.NEAREST最近邻,无 Mipmap
gl.LINEAR线性插值,无 Mipmap
gl.NEAREST_MIPMAP_NEAREST最近邻 Mipmap,最近邻插值
gl.LINEAR_MIPMAP_NEAREST最近邻 Mipmap,线性插值
gl.NEAREST_MIPMAP_LINEAR线性 Mipmap,最近邻插值
gl.LINEAR_MIPMAP_LINEAR线性 Mipmap,线性插值(三线性过滤)

环绕模式

常量说明
gl.REPEAT重复纹理
gl.CLAMP_TO_EDGE边缘拉伸
gl.MIRRORED_REPEAT镜像重复(WebGL1 需要扩展,WebGL2 原生支持)

非 2 次幂纹理限制

WebGL1 对非 2 次幂 (NPOT) 纹理有限制:不能生成 Mipmap、环绕模式必须为 CLAMP_TO_EDGE。 WebGL2 完全支持 NPOT 纹理。

像素存储参数(pixelStorei)

pixelStorei 用于配置纹理数据的解包(上传到 GPU 时)和打包(从 GPU 读取时)方式,影响 texImage2DtexSubImage2DreadPixels 的行为。

// 设置解包对齐方式(默认 4 字节对齐)
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); // 1 字节对齐,适用于 RGB 等非 4 字节对齐的格式

// 设置 Y 轴翻转(默认 false)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // 加载图像时垂直翻转

// 设置预乘 Alpha(默认 false)
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); // 预乘 Alpha 通道

// 设置色彩空间转换(默认 BROWSER_DEFAULT_WEBGL)
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); // 禁用色彩空间转换

注意pixelStorei 是全局状态,会影响后续所有纹理上传操作,使用后记得恢复默认值。

Mipmap 生成

Mipmap 是纹理的多级分辨率版本,用于提高渲染质量和性能。

// 上传基础级别纹理后生成 Mipmap
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.generateMipmap(gl.TEXTURE_2D);

注意

  • WebGL1 只对 2 次幂纹理 有效;WebGL2 没有此限制
  • Mipmap 会增加约 33% 的内存占用
  • 远距离渲染时 Mipmap 可以减少锯齿和闪烁

在片段着色器中使用纹理

纹理通过纹理单元传递给着色器,涉及三个步骤。

步骤 1:将 Texture 分配给纹理单元

WebGL 提供多个纹理单元(Texture Unit),每个单元是一个独立的"插槽",可以绑定一个纹理。

// 切换当前活动的纹理单元为 纹理单元 0
gl.activeTexture(gl.TEXTURE0);
// 将 texture1 绑定到纹理单元 0
gl.bindTexture(gl.TEXTURE_2D, texture1);

// 切换当前活动的纹理单元为 纹理单元 1
gl.activeTexture(gl.TEXTURE1);
// 将 texture2 绑定到纹理单元 1。此时两个纹理单元有各自的绑定
gl.bindTexture(gl.TEXTURE_2D, texture2);

activeTexture 做了什么:切换当前活动的纹理单元,后续的 bindTexture 操作将纹理绑定到该单元

步骤 2:设置 Uniform 使用哪个纹理单元

通过 uniform1i 告诉着色器的 sampler2D 变量使用哪个纹理单元。

// 告诉 uDiffuse 使用纹理单元 0
const diffuseLoc = gl.getUniformLocation(program, "uDiffuse");
gl.uniform1i(diffuseLoc, 0); // 0 表示 gl.TEXTURE0

// 告诉 uNormal 使用纹理单元 1
const normalLoc = gl.getUniformLocation(program, "uNormal");
gl.uniform1i(normalLoc, 1); // 1 表示 gl.TEXTURE1

步骤 3:顶点着色器传递纹理坐标:通过 Attribute 接收纹理坐标(UV),通过 Varying 传递给片段着色器

// 顶点着色器: 传递纹理坐标
attribute vec3 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;

void main() {
  gl_Position = vec4(aPosition, 1.0);
  vTexCoord = aTexCoord;
}

步骤 4:片段着色器采样纹理:通过 sampler2D 类型的 Uniform 接收纹理,使用 texture2D 函数采样

// 片段着色器: 采样多个纹理
precision mediump float;

uniform sampler2D uDiffuse;  // 漫反射纹理
uniform sampler2D uNormal;   // 法线纹理
varying vec2 vTexCoord;

void main() {
  vec4 diffuse = texture2D(uDiffuse, vTexCoord);
  vec4 normal = texture2D(uNormal, vTexCoord);

  // 混合纹理
  gl_FragColor = diffuse * 0.8 + normal * 0.2;
}

WebGL2 通过采样器对象配置纹理采样参数

WebGL2 支持将纹理数据和采样参数分离管理。在 WebGL1 中,采样参数(过滤模式、环绕模式)是纹理对象的一部分,通过 texParameteri 设置;WebGL2 引入采样器对象,可以独立管理采样参数。

// 创建采样器对象
const sampler = gl.createSampler();
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, gl.REPEAT);

gl.bindSampler(0, sampler); // gl.TEXTURE0 使用此采样器
gl.bindSampler(1, sampler); // gl.TEXTURE1 使用此采样器

// 采样器参数会覆盖纹理对象的参数

采样器对象的优势

  • 同一纹理可以使用不同的采样参数(例如:同一纹理在不同位置使用不同的过滤模式)
  • 减少纹理对象的状态切换开销
  • 更灵活的纹理采样配置

纹理和帧缓冲的复制操作 (Blit)

WebGL 提供了多种 API 用于在纹理、帧缓冲之间复制像素数据,以及从渲染结果复制数据到纹理。这些操作统称为 Blit(Block Transfer,块传输)操作。

blitFramebuffer vs copyTexSubImage2D

特性blitFramebuffer(WebGL2)copyTexSubImage2D
缩放支持支持任意缩放不支持缩放,源和目标大小必须一致
翻转支持支持水平/垂直翻转不支持翻转
过滤模式支持 NEARESTLINEAR不支持过滤(等同于 NEAREST
MSAA 解析原生支持多重采样解析不支持
复制目标帧缓冲附件(可以是纹理或渲染缓冲)只能复制到纹理
复制范围支持复制颜色/深度/模板的任意组合只能复制颜色缓冲区
WebGL 版本要求仅 WebGL2WebGL1 和 WebGL2 都支持

最佳实践

  • 优先使用 blitFramebuffer(WebGL2):功能更强大,性能更好
  • 使用 copyTexSubImage2D(WebGL1 兼容):需要兼容 WebGL1 或只需简单复制时
  • MSAA 场景必须使用 blitFramebuffer 解析多重采样
  • 避免频繁的 CPU-GPU 数据传输(readPixels + texImage2D),优先使用 GPU 端的 Blit 操作

copyTexImage2D 和 copyTexSubImage2D

这两个 API 用于将帧缓冲(当前绑定的 READ_FRAMEBUFFERFRAMEBUFFER)的像素数据复制到纹理中,在 WebGL1 和 WebGL2 中都可用。

copyTexImage2D:从帧缓冲复制像素数据并初始化整个纹理

// 绑定目标纹理
gl.bindTexture(gl.TEXTURE_2D, texture);

// 从帧缓冲复制数据到纹理
gl.copyTexImage2D(
  gl.TEXTURE_2D, // target: 纹理目标
  0, // level: Mipmap 级别
  gl.RGBA, // internalformat: 纹理内部格式
  0,
  0, // x, y: 帧缓冲读取起点(像素坐标)
  512,
  512, // width, height: 复制区域大小
  0 // border: 必须为 0
);

copyTexSubImage2D:从帧缓冲复制像素数据到纹理的指定区域(部分更新)

// 绑定目标纹理
gl.bindTexture(gl.TEXTURE_2D, texture);

// 部分更新纹理数据
gl.copyTexSubImage2D(
  gl.TEXTURE_2D, // target: 纹理目标
  0, // level: Mipmap 级别
  10,
  10, // xoffset, yoffset: 纹理写入起点
  0,
  0, // x, y: 帧缓冲读取起点
  256,
  256 // width, height: 复制区域大小
);

两者区别

特性copyTexImage2DcopyTexSubImage2D
纹理初始化会重新分配纹理存储空间不改变纹理存储空间
更新范围更新整个纹理(必须匹配 width/height)更新纹理的指定矩形区域
性能较慢(涉及内存分配)较快(只更新数据)
使用场景首次创建纹理或改变纹理尺寸部分更新已存在的纹理

注意事项

  • 这两个 API 会从当前绑定的读取帧缓冲(WebGL2 的 READ_FRAMEBUFFER 或 WebGL1 的 FRAMEBUFFER)读取数据
  • 复制操作发生在 GPU,比 CPU 读写(readPixels + texImage2D)更高效
  • 源帧缓冲的格式必须与目标纹理格式兼容
  • 复制立方体贴图时,target 参数使用 TEXTURE_CUBE_MAP_POSITIVE_X

blitFramebuffer(WebGL2)

WebGL2 新增的 blitFramebuffer 提供了更强大的帧缓冲间复制功能,支持缩放、翻转和多重采样解析(MSAA resolve)。

基本用法

// 绑定源帧缓冲和目标帧缓冲
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, sourceFBO); // 读取源
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, targetFBO); // 写入目标

// 执行 Blit 操作
gl.blitFramebuffer(
  0,
  0,
  512,
  512, // srcX0, srcY0, srcX1, srcY1: 源矩形区域
  0,
  0,
  256,
  256, // dstX0, dstY0, dstX1, dstY1: 目标矩形区域
  gl.COLOR_BUFFER_BIT, // mask: 复制哪些缓冲区
  gl.LINEAR // filter: 过滤模式(缩放时使用)
);

参数说明

  • 源和目标矩形:可以指定不同大小的矩形,实现缩放和翻转

    • 缩放:源矩形和目标矩形大小不同时,自动进行缩放
    • 翻转:通过交换坐标实现翻转(如 srcY0 > srcY1 实现 Y 轴翻转)
  • 缓冲区掩码(mask):指定复制哪些附件,可以组合使用

    • gl.COLOR_BUFFER_BIT:复制颜色附件
    • gl.DEPTH_BUFFER_BIT:复制深度附件
    • gl.STENCIL_BUFFER_BIT:复制模板附件
    • 组合:gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT
  • 采样模式(filter):缩放时的插值方式

    • gl.NEAREST:最近邻采样(快速,适用于所有缓冲区)
    • gl.LINEAR:线性插值(更平滑,仅适用于颜色缓冲区)

限制条件

  • 源和目标帧缓冲的格式必须兼容(颜色、深度、模板格式需匹配或兼容)
  • 复制深度或模板缓冲时,源和目标矩形大小必须相同(不支持缩放)
  • 复制深度或模板缓冲时,必须使用 gl.NEAREST
  • 不能在同一个帧缓冲内进行 Blit(源和目标必须不同)

着色器接口

着色器接口用于编译 GLSL 着色器代码并链接成可执行的着色器程序 (Program),以及管理着色器中的变量(Attribute、Uniform)。

着色器创建和编译

着色器 (Shader) 使用 GLSL 语言编写,需要经过创建、上传源码、编译三个步骤。

// 创建着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

// 上传着色器源码
const vertexSource = `
  attribute vec3 aPosition;
  uniform mat4 uMVPMatrix;
  void main() {
    gl_Position = uMVPMatrix * vec4(aPosition, 1.0);
  }
`;
gl.shaderSource(vertexShader, vertexSource);

// 编译着色器
gl.compileShader(vertexShader);

编译做了什么?

compileShader 将 GLSL 源码编译为 GPU 可执行的中间代码:

  1. 词法和语法分析:解析 GLSL 语法,构建抽象语法树(AST)
  2. 语义检查:验证类型、变量声明、函数调用是否正确
  3. 优化:移除未使用的变量和代码、常量折叠、内联函数
  4. 生成中间代码:编译为驱动内部的 IR(Intermediate Representation)

此时着色器已完成编译,但还未链接为可执行程序。生成的代码格式由驱动决定(如 SPIR-V、AMD IL、NVIDIA PTX 等)。

编译错误处理

编译着色器后,可以获取错误信息用于调试。

// 检查编译状态
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
  // 获取编译错误日志
  const log = gl.getShaderInfoLog(shader);
  console.error(`Shader compilation failed: ${log}`);
  gl.deleteShader(shader);
  return null;
}

常见编译错误:

  • 语法错误:拼写错误、缺少分号等
  • 类型不匹配:vec3 赋值给 vec4
  • 未声明变量:使用了未定义的 Uniform 或 Attribute
  • GLSL 版本不兼容:WebGL1 只支持 GLSL ES 1.00

程序创建和链接

将顶点着色器和片段着色器链接成一个可执行程序。

// 创建程序对象
const program = gl.createProgram();

// 附加着色器
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// 链接程序
gl.linkProgram(program);

// 使用程序
gl.useProgram(program);

链接做了什么?

linkProgram 将编译后的顶点着色器和片段着色器链接为可执行程序:

  1. 接口匹配验证:检查顶点着色器的 varying 输出与片段着色器的 varying 输入是否匹配(变量名、类型、精度)
  2. 资源分配:为 Attribute、Uniform、Varying 变量分配 location 索引
  3. 优化:跨着色器优化(如常量传播)、移除未使用的 Varying 变量
  4. 生成可执行代码:将中间代码转换为 GPU 原生指令(如 AMD GCN ISA、NVIDIA SASS)

链接成功后,程序可以通过 useProgram 绑定并用于渲染。可以通过 getAttribLocationgetUniformLocation 查询变量的 location。

链接错误处理

链接程序后,可以检查链接状态。

// 检查链接状态
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
  // 获取链接错误日志
  const log = gl.getProgramInfoLog(program);
  console.error(`Program linking failed: ${log}`);
  gl.deleteProgram(program);
  return null;
}

常见链接错误:

  • Varying 变量不匹配:顶点着色器输出的 Varying 与片段着色器输入的不一致
  • 缺少必需输出:顶点着色器未写入 gl_Position,或片段着色器未写入 gl_FragColor(WebGL1)
  • Attribute 数量超限:超过 GPU 支持的最大 Attribute 数量(通过 gl.getParameter(gl.MAX_VERTEX_ATTRIBS) 查询)

指定 Attribute Location

默认情况下,Attribute 的 location 由链接器自动分配。可以通过以下方式显式指定:

方式 1:链接前通过 API 指定

// 在链接前绑定 location
gl.bindAttribLocation(program, 0, "aPosition");
gl.bindAttribLocation(program, 1, "aColor");

// 然后链接程序
gl.linkProgram(program);

方式 2:在着色器中指定(WebGL2)

#version 300 es
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aColor;

void main() {
  gl_Position = vec4(aPosition, 1.0);
}

使用场景

  • 多个着色器程序共享相同的 location 布局,便于复用顶点配置
  • 避免运行时查询 location 的开销

Attribute 变量管理

Attribute 变量用于接收顶点数据,只能在顶点着色器中使用。

获取 Attribute 位置

// 获取 Attribute 变量的位置
const positionLoc = gl.getAttribLocation(program, "aPosition");

// 返回值:
// - 非负整数:变量存在且活跃(被着色器实际使用)
// - -1:变量不存在,或未被使用(被编译器优化掉)

if (positionLoc === -1) {
  console.warn('Attribute "aPosition" not found or not used');
}

使用 Attribute 变量

// GLSL 顶点着色器
const vertexSource = `
  attribute vec3 aPosition;  // 位置
  attribute vec3 aColor;     // 颜色
  varying vec3 vColor;

  void main() {
    gl_Position = vec4(aPosition, 1.0);
    vColor = aColor;
  }
`;

// JavaScript
const positionLoc = gl.getAttribLocation(program, "aPosition");
const colorLoc = gl.getAttribLocation(program, "aColor");

// 绑定位置数据
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc); // 启用 Attribute

// 绑定颜色数据
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(colorLoc); // 启用 Attribute

注意

  • 只有被着色器实际使用的 Attribute 才会被保留,未使用的会被编译器优化掉
  • WebGL1 最多支持 8-16 个 Attribute(设备相关,通过 gl.getParameter(gl.MAX_VERTEX_ATTRIBS) 查询)

WebGL2 的 Attribute 优化

WebGL2 支持在 GLSL 中使用 layout(location = N) 显式指定 Attribute 位置,无需调用 getAttribLocation,可以提高性能并简化代码。

使用示例

// WebGL2 GLSL ES 3.00 顶点着色器
#version 300 es
layout(location = 0) in vec3 aPosition;  // 显式指定位置为 0
layout(location = 1) in vec3 aColor;     // 显式指定位置为 1

out vec3 vColor;

void main() {
  gl_Position = vec4(aPosition, 1.0);
  vColor = aColor;
}
// JavaScript 中直接使用指定的位置,无需 getAttribLocation
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); // 位置 0
gl.enableVertexAttribArray(0);

gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0); // 位置 1
gl.enableVertexAttribArray(1);

WebGL1 与 WebGL2 对比

特性WebGL1WebGL2
Attribute 声明attribute vec3 aPosition;in vec3 aPosition;layout(location = 0) in vec3 aPosition;
获取位置必须使用 getAttribLocation可以使用 layout(location) 显式指定,或使用 getAttribLocation
最大数量8-16 个(设备相关)至少 16 个

优势

  • 避免运行时查询位置的开销
  • 代码更清晰,Attribute 位置在着色器中明确定义
  • 便于多个着色器程序共享相同的 Attribute 布局

Uniform 变量管理

Uniform 变量用于传递常量数据(如变换矩阵、光照参数),可在顶点着色器和片段着色器中使用。

// 获取 Uniform 变量的位置
const mvpMatrixLoc = gl.getUniformLocation(program, "uMVPMatrix");
const colorLoc = gl.getUniformLocation(program, "uColor");

// 返回值:
// - WebGLUniformLocation 对象:变量存在且活跃
// - null:变量不存在,或未被使用

if (!mvpMatrixLoc) {
  console.warn('Uniform "uMVPMatrix" not found or not used');
}

上传 Uniform 数据

根据 Uniform 类型使用不同的 uniform 函数:

// 标量(Scalar)
gl.uniform1f(colorLoc, 1.0); // float
gl.uniform1i(textureLoc, 0); // int / sampler2D

// 向量(Vector)
gl.uniform2f(resolutionLoc, 800, 600); // vec2
gl.uniform3f(colorLoc, 1.0, 0.0, 0.0); // vec3
gl.uniform4f(colorLoc, 1.0, 0.0, 0.0, 1.0); // vec4

// 使用数组
gl.uniform3fv(colorLoc, [1.0, 0.0, 0.0]); // vec3

// 矩阵(Matrix)
const matrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
gl.uniformMatrix4fv(
  mvpMatrixLoc,
  false, // transpose(WebGL 必须为 false)
  matrix
);

Uniform 类型对照表

GLSL 类型JavaScript 函数示例
floatuniform1f(loc, v)gl.uniform1f(loc, 1.0)
vec2uniform2f(loc, x, y)uniform2fv(loc, [x, y])gl.uniform2f(loc, 1.0, 2.0)
vec3uniform3f(loc, x, y, z)uniform3fv(loc, [x, y, z])gl.uniform3fv(loc, [1, 0, 0])
vec4uniform4f(loc, x, y, z, w)uniform4fv(loc, [x, y, z, w])gl.uniform4fv(loc, [1, 0, 0, 1])
int / booluniform1i(loc, v)gl.uniform1i(loc, 1)
sampler2Duniform1i(loc, unit)gl.uniform1i(loc, 0)
mat2uniformMatrix2fv(loc, false, data)gl.uniformMatrix2fv(loc, false, mat2)
mat3uniformMatrix3fv(loc, false, data)gl.uniformMatrix3fv(loc, false, mat3)
mat4uniformMatrix4fv(loc, false, data)gl.uniformMatrix4fv(loc, false, mat4)

Uniform 数组

// GLSL 中定义 Uniform 数组
uniform vec3 uLightPositions[4];
// JavaScript 中上传数组
const positions = new Float32Array([
  1,
  2,
  3, // 光源 0
  4,
  5,
  6, // 光源 1
  7,
  8,
  9, // 光源 2
  10,
  11,
  12, // 光源 3
]);
const loc = gl.getUniformLocation(program, "uLightPositions");
gl.uniform3fv(loc, positions);

// 或者单独设置每个元素
const loc0 = gl.getUniformLocation(program, "uLightPositions[0]");
gl.uniform3f(loc0, 1, 2, 3);

Uniform 数组限制:数组大小受着色器的最大 Uniform 向量数量限制(通过 gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS) 查询

性能建议

  • Uniform 上传有性能开销,避免每帧上传未变化的 Uniform
  • 将多个标量 Uniform 合并为向量 Uniform 可以减少调用次数
  • 大量 Uniform 数据推荐使用 WebGL2 的 UBO

WebGL2 的 Uniform Buffer Objects (UBO)

WebGL2 支持 Uniform Buffer Objects,可以将多个 Uniform 打包到缓冲区中批量上传,相比逐个调用 uniform* 函数性能更好。

UBO 的优势

  • 批量上传多个 Uniform,减少 API 调用次数
  • 可以在多个着色器程序之间共享 Uniform 数据(如相机矩阵、光照参数)
  • 更新部分 Uniform 时使用 bufferSubData,比逐个调用 uniform* 更高效

使用示例

// WebGL2 GLSL ES 3.00 顶点着色器
#version 300 es

// 定义 Uniform Block
uniform Matrices {
  mat4 uModelMatrix;
  mat4 uViewMatrix;
  mat4 uProjectionMatrix;
};

in vec3 aPosition;

void main() {
  gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
}
// JavaScript 创建和使用 UBO
const program = /* 编译链接的着色器程序 */;

// 获取 Uniform Block 的索引
const blockIndex = gl.getUniformBlockIndex(program, 'Matrices');

// 将 Uniform Block 绑定到绑定点 0
gl.uniformBlockBinding(program, blockIndex, 0);

// 查询 Uniform Block 的大小
const blockSize = gl.getActiveUniformBlockParameter(
  program,
  blockIndex,
  gl.UNIFORM_BLOCK_DATA_SIZE
);

// 创建 UBO
const ubo = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
gl.bufferData(gl.UNIFORM_BUFFER, blockSize, gl.DYNAMIC_DRAW);

// 准备矩阵数据(3 个 mat4 = 3 * 16 * 4 = 192 字节)
const matrices = new Float32Array(48); // 3 个 mat4
matrices.set(modelMatrix, 0);   // 0-15
matrices.set(viewMatrix, 16);   // 16-31
matrices.set(projMatrix, 32);   // 32-47

// 上传数据到 UBO
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, matrices);

// 将 UBO 绑定到绑定点 0
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo);

// 绘制时,所有使用这个 Uniform Block 的着色器程序都能访问数据

std140 内存布局

UBO 使用 std140 内存不仅,确保跨平台对齐一致性。关键规则:

  • 标量(floatint):4 字节对齐
  • vec2:8 字节对齐;vec3vec4:16 字节对齐(vec3 也占 16 字节)
  • 矩阵:按列主序,每列对齐到 16 字节(mat4 占 64 字节)
  • 数组:每个元素对齐到 16 字节

示例:

layout(std140) uniform ExampleBlock {
  float value;    // 偏移 0,  占 4 字节(+ 12 字节填充)
  vec3 vector;    // 偏移 16, 占 12 字节(+ 4 字节填充)
  mat4 matrix;    // 偏移 32, 占 64 字节
};                // 总计 96 字节
// 查询实际偏移量(避免手动计算填充)
const indices = gl.getUniformIndices(program, ["value", "vector", "matrix"]);
const offsets = gl.getActiveUniforms(program, indices, gl.UNIFORM_OFFSET);
// offsets: [0, 16, 32]

注意:WebGL1 不支持 UBO

渲染控制接口

渲染控制接口用于配置渲染管道的固定功能阶段(视口、测试、混合等)和执行绘制命令,控制如何将几何数据渲染到帧缓冲。

视口设置

视口 (Viewport) 定义 NDC 坐标到屏幕像素坐标的映射区域。

// 设置视口(通常与 canvas 尺寸一致)
gl.viewport(0, 0, canvas.width, canvas.height);
// 参数:x, y, width, height(像素单位)

// 示例:渲染到左半屏
gl.viewport(0, 0, canvas.width / 2, canvas.height);

// 查询当前视口
const viewport = gl.getParameter(gl.VIEWPORT); // [x, y, width, height]

视口变换将 NDC 范围 [-1, 1] 映射到屏幕像素坐标:

  • NDC (-1, -1) → 屏幕 (x, y)
  • NDC (1, 1) → 屏幕 (x + width, y + height)

裁剪测试 (Scissor Test)

裁剪测试限制渲染到一个矩形区域,区域外的像素被丢弃。

// 启用裁剪测试
gl.enable(gl.SCISSOR_TEST);

// 设置裁剪矩形
gl.scissor(100, 100, 200, 200); // x, y, width, height

// 禁用裁剪测试
gl.disable(gl.SCISSOR_TEST);

使用场景

  • 分屏渲染(多视口)
  • UI 裁剪(限制绘制区域)
  • 优化性能(只渲染可见区域)

深度测试 (Depth Test)

深度测试根据深度值决定片段是否可见,实现 3D 物体的遮挡关系。

// 启用深度测试
gl.enable(gl.DEPTH_TEST);

// 设置深度比较函数
gl.depthFunc(gl.LESS); // 深度值更小(更近)时通过

// 控制深度缓冲区是否可写
gl.depthMask(true); // 允许写入深度值

// 禁用深度测试
gl.disable(gl.DEPTH_TEST);

深度比较函数

常量说明
gl.NEVER永不通过
gl.ALWAYS总是通过
gl.LESS深度值 < 当前值时通过(默认,用于正常 3D 渲染)
gl.LEQUAL深度值 ≤ 当前值时通过
gl.EQUAL深度值 = 当前值时通过
gl.GEQUAL深度值 ≥ 当前值时通过
gl.GREATER深度值 > 当前值时通过
gl.NOTEQUAL深度值 ≠ 当前值时通过

工作原理

depthFunc 决定了片段深度值与深度缓冲区中已有深度值的比较方式。当片段通过深度测试时,如果 depthMasktrue,该片段的深度值会更新到深度缓冲区;如果测试失败,片段被丢弃,深度缓冲区保持不变。

// 示例:渲染天空盒(总是在最远处)
gl.depthFunc(gl.LEQUAL); // 允许深度值等于 1.0(远平面)通过
gl.drawArrays(gl.TRIANGLES, 0, skyboxVertexCount);
gl.depthFunc(gl.LESS); // 恢复默认

注意

  • 需要在创建 WebGL 上下文时请求深度缓冲区:gl = canvas.getContext('webgl', { depth: true })(默认为 true)
  • 深度值范围为 [0, 1],0 表示近平面,1 表示远平面

模板测试 (Stencil Test)

模板测试使用模板缓冲区(至少 8 位整数)进行像素级的遮罩控制。它的工作分为两步:先通过 stencilFunc 判断片段是否通过测试,再通过 stencilOp 更新模板值。

步骤 1:配置测试条件

// 启用模板测试
gl.enable(gl.STENCIL_TEST);

// 设置测试条件
gl.stencilFunc(
  gl.EQUAL, // func: 比较函数
  1, // ref: 参考值
  0xff // mask: 掩码(哪些位参与比较)
);

stencilFunc 通过比较公式决定片段是否通过:(ref & mask) func (模板缓冲区当前值 & mask)

  • 比较函数 func 可以是 EQUALLESSGREATER 等(与深度测试相同)
  • 例如 stencilFunc(gl.EQUAL, 1, 0xff) 表示:只有模板值等于 1 的像素才通过

步骤 2:配置模板值更新规则

// 设置三种情况下如何更新模板值
gl.stencilOp(
  gl.KEEP, // sfail: 模板测试失败时
  gl.KEEP, // dpfail: 模板通过但深度测试失败时
  gl.REPLACE // dppass: 两个测试都通过时
);

GPU 根据测试结果选择对应操作更新模板缓冲区。REPLACE 会将模板值更新为 stencilFunc 中的参考值 ref

为什么设计成 ref 和 mask 的形式?

这种设计提供了灵活的位级控制能力:

  1. ref(参考值):作为比较的基准值,也是 REPLACE 操作写入的值
  2. mask(掩码):选择性地屏蔽某些位,只对关心的位进行比较
// 示例:使用低 4 位存储物体 ID,高 4 位存储其他信息
gl.stencilFunc(
  gl.EQUAL,
  0x03, // ref = 3,只关心低 4 位的值
  0x0f // mask = 0000 1111,只比较低 4 位
);

// 模板值 0x73 (0111 0011):
// (0x03 & 0x0f) == (0x73 & 0x0f)
// (0000 0011) == (0000 0011) ✓ 通过

// 模板值 0x85 (1000 0101):
// (0x03 & 0x0f) == (0x85 & 0x0f)
// (0000 0011) != (0000 0101) ✗ 不通过

这种设计允许在一个模板缓冲区中存储多种信息,通过不同的掩码分别读写不同的位域,实现更复杂的渲染效果。

模板操作常量

常量说明
gl.KEEP保持当前值
gl.ZERO设置为 0
gl.REPLACE替换为参考值 ref
gl.INCR递增(达到最大值时保持)
gl.INCR_WRAP递增(达到最大值时回绕到 0)
gl.DECR递减(达到 0 时保持)
gl.DECR_WRAP递减(达到 0 时回绕到最大值)
gl.INVERT按位取反

控制写入权限

gl.stencilMask(0xff); // 允许写入所有位,0x00 则禁止写入

示例:创建圆形遮罩区域

// 第一遍:在模板缓冲区标记圆形区域
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0xff); // 所有片段都通过
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); // 通过时写入 1
gl.colorMask(false, false, false, false); // 不写颜色
drawCircle(); // 圆形区域模板值变为 1

// 第二遍:只在圆形区域内渲染
gl.stencilFunc(gl.EQUAL, 1, 0xff); // 只有模板值为 1 的通过
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); // 不修改模板值
gl.colorMask(true, true, true, true); // 恢复颜色写入
drawScene(); // 只在圆形内可见

正反面分别配置

对于封闭几何体(如阴影体算法),可以为正反面设置不同规则。

// 正面:递增
gl.stencilFuncSeparate(gl.FRONT, gl.ALWAYS, 0, 0xff);
gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.INCR_WRAP);

// 背面:递减
gl.stencilFuncSeparate(gl.BACK, gl.ALWAYS, 0, 0xff);
gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.DECR_WRAP);

颜色混合 (Blending)

颜色混合将片段着色器输出的颜色(源颜色)与帧缓冲区已有颜色(目标颜色)按照指定公式组合。混合公式由 blendFunc 设置的因子和 blendEquation 设置的运算符共同决定。

混合公式

最终颜色 = (源颜色 × 源因子) ⊕ (目标颜色 × 目标因子)

其中 blendEquation 指定(默认为加法)。

配置混合因子

// 启用混合
gl.enable(gl.BLEND);

// 设置混合因子
gl.blendFunc(
  gl.SRC_ALPHA, // 源因子
  gl.ONE_MINUS_SRC_ALPHA // 目标因子
);

混合因子是一个 4 维向量,用于缩放颜色的 RGBA 各分量。例如 SRC_ALPHA 对应 (As, As, As, As),会将源颜色的每个分量乘以源 Alpha 值。

标准 Alpha 混合的计算过程:

源颜色 = (Rs, Gs, Bs, As)  // 片段着色器输出
目标颜色 = (Rd, Gd, Bd, Ad) // 帧缓冲区当前值
源因子 = (As, As, As, As)
目标因子 = (1-As, 1-As, 1-As, 1-As)

最终颜色 = (Rs×As + Rd×(1-As), Gs×As + Gd×(1-As), Bs×As + Bd×(1-As), As×As + Ad×(1-As))

常用混合因子

常量说明
gl.ZERO(0, 0, 0, 0)
gl.ONE(1, 1, 1, 1)
gl.SRC_COLOR(Rs, Gs, Bs, As)
gl.ONE_MINUS_SRC_COLOR(1-Rs, 1-Gs, 1-Bs, 1-As)
gl.SRC_ALPHA(As, As, As, As)
gl.ONE_MINUS_SRC_ALPHA(1-As, 1-As, 1-As, 1-As)
gl.DST_COLOR(Rd, Gd, Bd, Ad)
gl.ONE_MINUS_DST_COLOR(1-Rd, 1-Gd, 1-Bd, 1-Ad)
gl.DST_ALPHA(Ad, Ad, Ad, Ad)
gl.ONE_MINUS_DST_ALPHA(1-Ad, 1-Ad, 1-Ad, 1-Ad)
gl.CONSTANT_COLOR(Rc, Gc, Bc, Ac) 常量颜色
gl.ONE_MINUS_CONSTANT_COLOR(1-Rc, 1-Gc, 1-Bc, 1-Ac)
gl.CONSTANT_ALPHA(Ac, Ac, Ac, Ac) 常量 Alpha
gl.ONE_MINUS_CONSTANT_ALPHA(1-Ac, 1-Ac, 1-Ac, 1-Ac)
gl.SRC_ALPHA_SATURATE(f, f, f, 1) 其中 f = min(As, 1-Ad)

设置常量颜色

当使用 CONSTANT_COLORCONSTANT_ALPHA 作为混合因子时,需要通过 blendColor 设置常量颜色值。

// 设置混合常量颜色
gl.blendColor(0.5, 0.5, 0.5, 0.8); // (Rc, Gc, Bc, Ac)

// 使用常量颜色作为混合因子
gl.blendFunc(gl.CONSTANT_COLOR, gl.ONE_MINUS_CONSTANT_COLOR);

// 示例:使用常量 Alpha 控制全局透明度
gl.blendColor(0, 0, 0, 0.5); // 设置常量 Alpha 为 0.5
gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA);
// 所有片段都会乘以 0.5 的透明度

配置混合运算

// 设置混合方程(默认为 FUNC_ADD)
gl.blendEquation(gl.FUNC_ADD);

常用混合方程

常量说明
gl.FUNC_ADD加法:源×源因子 + 目标×目标因子
gl.FUNC_SUBTRACT减法:源×源因子 - 目标×目标因子
gl.FUNC_REVERSE_SUBTRACT反向减法:目标×目标因子 - 源×源因子
gl.MIN (WebGL2)最小值:min(源, 目标) (忽略因子)
gl.MAX (WebGL2)最大值:max(源, 目标) (忽略因子)

分别配置 RGB 和 Alpha

// RGB 和 Alpha 使用不同的混合因子
gl.blendFuncSeparate(
  gl.SRC_ALPHA,
  gl.ONE_MINUS_SRC_ALPHA, // RGB 因子
  gl.ONE,
  gl.ZERO // Alpha 因子
);

// RGB 和 Alpha 使用不同的混合方程
gl.blendEquationSeparate(gl.FUNC_ADD, gl.MAX);

面剔除 (Face Culling)

面剔除根据三角形的正反面剔除不可见的面,提高渲染性能。

// 启用面剔除
gl.enable(gl.CULL_FACE);

// 设置剔除的面
gl.cullFace(gl.BACK); // 剔除背面(默认)
// gl.cullFace(gl.FRONT);  // 剔除正面
// gl.cullFace(gl.FRONT_AND_BACK);  // 剔除正反面

// 设置正面的定义(顶点环绕顺序)
gl.frontFace(gl.CCW); // 逆时针为正面(默认)
// gl.frontFace(gl.CW);    // 顺时针为正面

// 禁用面剔除
gl.disable(gl.CULL_FACE);

绘制命令

执行实际的渲染操作,将顶点数据通过渲染管道转换为屏幕像素。

使用顶点数组绘制

// 绘制三角形
gl.drawArrays(
  gl.TRIANGLES, // 图元类型
  0, // 起始顶点索引
  3 // 顶点数量
);

// 其他图元类型
gl.drawArrays(gl.POINTS, 0, count); // 点
gl.drawArrays(gl.LINES, 0, count); // 线段(每 2 个顶点)
gl.drawArrays(gl.LINE_STRIP, 0, count); // 连续线段
gl.drawArrays(gl.LINE_LOOP, 0, count); // 闭合线段
gl.drawArrays(gl.TRIANGLE_STRIP, 0, count); // 三角形带
gl.drawArrays(gl.TRIANGLE_FAN, 0, count); // 三角形扇

使用索引缓冲区绘制

// 绑定索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

// 绘制
gl.drawElements(
  gl.TRIANGLES, // 图元类型
  6, // 索引数量
  gl.UNSIGNED_SHORT, // 索引类型
  0 // 偏移量(字节)
);
特性drawArraysdrawElements
顶点读取方式按顺序读取顶点通过索引读取顶点
顶点复用不支持,重复顶点需要重复存储支持,通过索引复用顶点
内存效率较低(有重复数据)较高(无重复数据)
适用场景简单图形、顶点不重复复杂模型、共享顶点的网格
count 参数顶点数量索引数量
需要额外缓冲区是(ELEMENT_ARRAY_BUFFER)

WebGL2 的实例化渲染

// 绘制多个实例(用于渲染大量相同物体)
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100); // 绘制 100 个实例
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 100);

写入遮罩 (Write Mask)

写入遮罩控制哪些缓冲区或颜色通道可以被写入,用于保护特定数据不被修改。

颜色遮罩

// 控制 RGBA 各通道是否可写
gl.colorMask(true, true, true, false); // 禁止写入 Alpha 通道

// 示例:只写入红色通道
gl.colorMask(true, false, false, false);

// 恢复写入所有通道
gl.colorMask(true, true, true, true);

深度遮罩

// 控制深度缓冲区是否可写
gl.depthMask(false); // 禁止写入深度值(深度测试仍然执行)

// 恢复深度写入
gl.depthMask(true);

模板遮罩

// 控制模板缓冲区哪些位可写
gl.stencilMask(0xff); // 允许写入所有位
gl.stencilMask(0x00); // 禁止写入任何位
gl.stencilMask(0x0f); // 只允许写入低 4 位

// 分别设置正反面的模板遮罩(WebGL2)
gl.stencilMaskSeparate(gl.FRONT, 0xff); // 正面
gl.stencilMaskSeparate(gl.BACK, 0x00); // 背面

使用场景

  • 深度遮罩:渲染半透明物体时禁止写入深度,避免遮挡后续物体

    // 渲染半透明物体
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.depthMask(false); // 禁止写入深度
    gl.drawArrays(gl.TRIANGLES, 0, transparentObjectCount);
    gl.depthMask(true); // 恢复深度写入
    
  • 颜色遮罩:只更新特定颜色通道

    // 只更新 Alpha 通道
    gl.colorMask(false, false, false, true);
    gl.drawArrays(gl.TRIANGLES, 0, count);
    gl.colorMask(true, true, true, true);
    
  • 模板遮罩:保护模板缓冲区的特定位

    // 只允许写入模板值的低 4 位
    gl.stencilMask(0x0f);
    

清屏操作

清空帧缓冲区的颜色、深度、模板缓冲。

// 设置清屏颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0); // RGBA,黑色不透明

// 设置清屏深度值
gl.clearDepth(1.0); // 深度值 1.0(最远)

// 设置清屏模板值
gl.clearStencil(0); // 模板值 0

// 清空缓冲区
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区
gl.clear(gl.DEPTH_BUFFER_BIT); // 清空深度缓冲区
gl.clear(gl.STENCIL_BUFFER_BIT); // 清空模板缓冲区

// 同时清空多个缓冲区(推荐)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

帧缓冲接口

帧缓冲接口用于创建自定义帧缓冲对象 (Framebuffer Object, FBO),实现离屏渲染,支持阴影贴图、后处理、动态纹理等高级渲染技术。

帧缓冲对象创建

帧缓冲对象是渲染目标的容器,包含颜色、深度、模板附件。

// 创建帧缓冲对象
const fbo = gl.createFramebuffer();

// 绑定帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // 后续操作作用于此 FBO

// 绑定到默认帧缓冲(渲染到 Canvas)
gl.bindFramebuffer(gl.FRAMEBUFFER, null);

WebGL2 支持分别绑定读取和绘制帧缓冲:

// WebGL2
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo); // 绘制目标
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); // 读取源

渲染缓冲对象

渲染缓冲对象 (Renderbuffer) 用于存储渲染数据,通常用作深度或模板附件。

// 创建渲染缓冲对象
const renderbuffer = gl.createRenderbuffer();

// 绑定渲染缓冲
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);

// 分配存储空间
gl.renderbufferStorage(
  gl.RENDERBUFFER,
  gl.DEPTH_COMPONENT16, // 内部格式
  512,
  512 // 宽度、高度
);

常用内部格式

格式说明
gl.RGBA44 位 RGBA
gl.RGB5655-6-5 RGB
gl.RGB5_A15-5-5-1 RGBA
gl.DEPTH_COMPONENT1616 位深度
gl.STENCIL_INDEX88 位模板
gl.DEPTH_STENCIL深度 + 模板(WebGL1 需要扩展,WebGL2 原生支持)

WebGL2 的多重采样渲染缓冲

// WebGL2: 创建 MSAA 渲染缓冲(抗锯齿)
gl.renderbufferStorageMultisample(
  gl.RENDERBUFFER,
  4, // 采样数(通常 4 或 8)
  gl.RGBA8, // 内部格式
  512,
  512 // 宽度、高度
);

颜色附件

颜色附件用于存储渲染的颜色数据,可以是纹理或渲染缓冲对象。

使用纹理作为颜色附件

// 创建纹理
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

// 附加到帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(
  gl.FRAMEBUFFER, // 目标
  gl.COLOR_ATTACHMENT0, // 附件点
  gl.TEXTURE_2D, // 纹理目标
  texture, // 纹理对象
  0 // Mipmap 级别
);

注意:WebGL1 只支持一个颜色附件 (COLOR_ATTACHMENT0),WebGL2 支持多个颜色附件(详见【多渲染目标】章节)。

使用渲染缓冲对象作为颜色附件

const colorRB = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, colorRB);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, 512, 512);

gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorRB);

深度和模板附件

深度和模板附件用于深度测试和模板测试。

深度附件

// 使用渲染缓冲对象
const depthRB = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthRB);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 512, 512);

gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthRB);

// 或使用深度纹理(需要扩展 WEBGL_depth_texture,WebGL2 原生支持)
const depthTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, 512, 512, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);

深度 + 模板附件

// WebGL1 需要扩展 WEBGL_depth_texture
// WebGL2 原生支持
const depthStencilRB = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencilRB);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, 512, 512);

gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilRB);

帧缓冲完整性检查

帧缓冲配置完成后需要检查完整性。

gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

// 检查帧缓冲完整性
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
  console.error("Framebuffer is not complete:", getStatusString(status));
}

function getStatusString(status) {
  switch (status) {
    case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
      return "INCOMPLETE_ATTACHMENT: 附件配置不完整";
    case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
      return "INCOMPLETE_MISSING_ATTACHMENT: 缺少附件";
    case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
      return "INCOMPLETE_DIMENSIONS: 附件尺寸不一致";
    case gl.FRAMEBUFFER_UNSUPPORTED:
      return "UNSUPPORTED: 不支持的附件组合";
    default:
      return "UNKNOWN ERROR";
  }
}

常见错误

  • 没有附加任何颜色附件
  • 附件尺寸不一致
  • 使用了不支持的内部格式组合

多渲染目标 (Multiple Render Targets, MRT)

WebGL2 支持多渲染目标 (MRT),允许片段着色器在一次绘制调用中同时输出到多个颜色附件,用于延迟渲染 (Deferred Rendering)、G-Buffer 等高级渲染技术。

配置多个颜色附件

// 创建多个纹理作为颜色附件
const texture0 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture0);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

const texture1 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

const texture2 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture2);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

// 附加到帧缓冲的不同颜色附件
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture0, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, texture1, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, texture2, 0);

// 指定绘制到哪些颜色附件
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);

片段着色器输出到多个附件

// WebGL2 GLSL ES 3.00 片段着色器
#version 300 es
precision mediump float;

in vec3 vPosition;
in vec3 vNormal;
in vec2 vTexCoord;

// 定义多个输出变量
layout(location = 0) out vec4 outColor;      // COLOR_ATTACHMENT0
layout(location = 1) out vec4 outNormal;     // COLOR_ATTACHMENT1
layout(location = 2) out vec4 outPosition;   // COLOR_ATTACHMENT2

void main() {
  // 输出到不同的颜色附件
  outColor = vec4(1.0, 0.5, 0.2, 1.0);       // 颜色
  outNormal = vec4(normalize(vNormal), 1.0); // 法线
  outPosition = vec4(vPosition, 1.0);        // 位置
}

颜色附件数量限制

// 查询支持的最大颜色附件数量
const maxDrawBuffers = gl.getParameter(gl.MAX_DRAW_BUFFERS);
console.log("最大颜色附件数量:", maxDrawBuffers); // WebGL2 至少支持 4 个

注意事项

  • WebGL1 不支持 MRT,只能输出到一个颜色附件
  • WebGL2 至少支持 4 个颜色附件,具体数量由设备决定
  • 所有颜色附件的尺寸必须一致
  • MRT 会增加带宽和内存占用,需要权衡性能

WebGL 扩展

WebGL 通过扩展机制提供额外的功能,允许浏览器暴露硬件特定的能力。扩展需要显式启用才能使用,开发者应检查扩展的可用性以确保跨平台兼容性。本章介绍常用扩展的功能和使用方法,完整的扩展列表请参考 WebGL 扩展注册表

扩展的查询和启用

查询支持的扩展

// 获取所有支持的扩展名称
const extensions = gl.getSupportedExtensions();
console.log("Supported extensions:", extensions);

// 示例输出:
// ["ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_color_buffer_half_float", ...]

启用扩展

// 启用扩展
const ext = gl.getExtension("OES_texture_float");

if (!ext) {
  console.warn("OES_texture_float not supported");
} else {
  console.log("OES_texture_float enabled");
  // 现在可以使用浮点纹理
}

注意

  • getExtension 返回扩展对象(如果支持),或 null(如果不支持)
  • 扩展名称区分大小写,必须使用官方名称
  • 某些扩展在启用后会添加新的常量或方法到 gl 对象

WEBGL_debug_renderer_info

获取 GPU 渲染器和供应商信息,用于调试和性能分析。

扩展文档WEBGL_debug_renderer_info

const ext = gl.getExtension("WEBGL_debug_renderer_info");

if (ext) {
  const vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
  const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);

  console.log("GPU Vendor:", vendor); // 例如: "NVIDIA Corporation"
  console.log("GPU Renderer:", renderer); // 例如: "NVIDIA GeForce GTX 1080"
}

注意:出于隐私考虑,某些浏览器可能会限制此扩展的可用性。

OES_standard_derivatives

在片段着色器中启用导数函数 dFdxdFdyfwidth,用于计算屏幕空间梯度。

扩展文档OES_standard_derivatives

// 启用扩展
const ext = gl.getExtension("OES_standard_derivatives");

if (ext) {
  console.log("Standard derivatives enabled");
}

片段着色器使用

#extension GL_OES_standard_derivatives : enable

precision mediump float;
varying vec2 vTexCoord;

void main() {
  // 计算纹理坐标的屏幕空间导数
  float dx = dFdx(vTexCoord.x);
  float dy = dFdy(vTexCoord.y);

  // fwidth(p) = abs(dFdx(p)) + abs(dFdy(p))
  float gradient = fwidth(vTexCoord.x);

  gl_FragColor = vec4(gradient, gradient, gradient, 1.0);
}

使用场景

  • 程序化纹理的抗锯齿
  • 计算法线(通过位置或高度图)
  • 实现自定义 Mipmap 级别选择

注意:WebGL2 原生支持导数函数,无需扩展。

OES_texture_float

允许使用浮点纹理格式,存储高精度数据。

扩展文档OES_texture_float

// 启用扩展
const ext = gl.getExtension("OES_texture_float");

if (ext) {
  // 创建浮点纹理
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  const data = new Float32Array([
    1.5,
    2.7,
    -3.2,
    4.8, // RGBA 值可以超出 [0, 1] 范围
    0.1,
    0.2,
    0.3,
    0.4,
  ]);

  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA, // 内部格式
    2,
    1, // 宽度、高度
    0,
    gl.RGBA, // 格式
    gl.FLOAT, // 类型(需要扩展)
    data
  );

  // 设置纹理参数(浮点纹理通常不支持线性过滤,需要 OES_texture_float_linear)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}

使用场景

  • HDR 渲染
  • 物理模拟(粒子系统、流体模拟)
  • 延迟渲染的 G-Buffer
  • GPGPU 计算

相关扩展

  • OES_texture_float_linear:启用浮点纹理的线性过滤
  • WEBGL_color_buffer_float:允许渲染到浮点颜色缓冲区
  • EXT_color_buffer_float(WebGL2):WebGL2 的浮点渲染目标扩展

注意:WebGL2 原生支持浮点纹理,但渲染到浮点纹理仍需扩展。

WEBGL_depth_texture

允许使用深度纹理,将深度缓冲区作为纹理采样。

扩展文档WEBGL_depth_texture

// WebGL1 需要扩展
const ext = gl.getExtension("WEBGL_depth_texture");

if (ext) {
  // 创建深度纹理
  const depthTexture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, depthTexture);
  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.DEPTH_COMPONENT, // 内部格式
    512,
    512,
    0,
    gl.DEPTH_COMPONENT, // 格式
    gl.UNSIGNED_SHORT, // 类型
    null
  );

  // 设置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  // 附加到帧缓冲的深度附件
  const fbo = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
}

使用场景

  • 阴影贴图 (Shadow Mapping)
  • 深度驱动的后处理效果(景深、体积光)
  • SSAO (Screen Space Ambient Occlusion)

注意:WebGL2 原生支持深度纹理。

WEBGL_blend_func_extended

允许在片段着色器中输出双源混合(Dual Source Blending)数据,提供更灵活的混合控制。

扩展文档WEBGL_blend_func_extended

核心概念

双源混合允许片段着色器输出两个颜色值(gl_FragColorgl_SecondaryFragColorEXT),分别作为混合公式中的源颜色(Source0)和第二源颜色(Source1)。

片段着色器使用

#extension GL_EXT_blend_func_extended : require

precision mediump float;

void main() {
  // 输出源0颜色(正常的片段颜色)
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);

  // 输出源1颜色(用于双源混合)
  gl_SecondaryFragColorEXT = vec4(0.5, 0.5, 0.5, 0.5);
}

设置混合函数

const ext = gl.getExtension("WEBGL_blend_func_extended");

if (ext) {
  // 扩展提供的新混合因子常量
  // ext.SRC1_COLOR_WEBGL          - 源1颜色
  // ext.SRC1_ALPHA_WEBGL          - 源1 Alpha
  // ext.ONE_MINUS_SRC1_COLOR_WEBGL - 1 - 源1颜色
  // ext.ONE_MINUS_SRC1_ALPHA_WEBGL - 1 - 源1 Alpha

  // 使用双源混合因子
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, ext.SRC1_COLOR_WEBGL);
  // 最终颜色 = 源0颜色 × 1 + 目标颜色 × 源1颜色
}

QA

WebGL 中的坐标系变换

WebGL 渲染管道中涉及多个坐标空间的变换,这些变换发生在不同的管道阶段:

  1. 模型空间 → 世界空间 → 视图/相机空间 → 裁剪空间

    • 发生在:Vertex Shader(顶点着色器)
    • 通过 MVP 矩阵(Model-View-Projection)完成,输出到 gl_Position
    • 投影矩阵分为透视投影(锥台形)和正交投影(长方体),决定了视锥体的形状(详见下文)
    • 裁剪空间命名原因:此空间坐标用于后续的裁剪操作,GPU 在此阶段判断顶点是否在视锥体内(通过比较 x, y, zw 的关系)
  2. 裁剪空间 → NDC 空间(标准化设备坐标)

    • 发生在:Vertex Post-Processing(顶点后处理) 阶段
    • GPU 自动执行透视除法:(x/w, y/w, z/w),将裁剪空间坐标转换为 NDC 范围 [-1, 1]
  3. NDC 空间 → 屏幕空间(像素坐标)

    • 发生在:Vertex Post-Processing(顶点后处理) 阶段
    • GPU 自动执行视口变换,根据 gl.viewport() 设置将 [-1, 1] 映射到屏幕像素坐标

总结:开发者只需在顶点着色器中完成前三个空间的变换,透视除法和视口变换由 GPU 固定功能阶段自动完成。

透视投影 vs 正交投影

投影矩阵决定了从视图空间到裁剪空间的变换方式,有两种类型:

透视投影(Perspective Projection)

视锥体形状为锥台(截断的金字塔),模拟人眼透视效果。

       far plane (远平面)
      +-----------+
     /|          /|
    / |         / |    ← 视锥体(frustum)
   /  |        /  |
  /   +-------/---+ near plane (近平面)
 /   /       /   /
+---+-------+   /
   eye (相机)
  (0, 0, 0)

特点:

  • 近大远小:物体大小随距离变化,远处物体看起来更小
  • 深度感强:符合真实世界的透视规律
  • 应用场景:3D 游戏、建筑可视化、真实感渲染
// 使用 gl-matrix 库创建透视投影矩阵
const projectionMatrix = mat4.create();
mat4.perspective(
  projectionMatrix,
  Math.PI / 4, // fov: 视野角度(Field of View,弧度)
  canvas.width / canvas.height, // aspect: 宽高比
  0.1, // near: 近平面距离
  100.0 // far: 远平面距离
);

正交投影(Orthographic Projection)

视景体形状为长方体,平行投影,物体大小不随距离变化。

       far plane
      +-----------+
      |           |
      |           |    ← 视景体(长方体)
      |           |
      +-----------+ near plane

  所有投影线平行
      ↓ ↓ ↓ ↓ ↓
    投影方向

特点:

  • 平行投影:远近物体大小相同,无透视变形
  • 无深度感:适合需要精确测量的场景
  • 应用场景:2D 游戏、CAD 软件、工程图纸、建筑平面图、UI 界面
// 创建正交投影矩阵
const projectionMatrix = mat4.create();
mat4.ortho(
  projectionMatrix,
  -10,
  10, // left, right: 左右边界(视景体宽度)
  -10,
  10, // bottom, top: 上下边界(视景体高度)
  0.1,
  100.0 // near, far: 近远平面距离
);

片段着色器在测试阶段之前执行,会产生多余计算吗?

不一定,现代 GPU 有多种硬件优化机制避免浪费计算。

1. Scissor Test 和光栅化阶段优化

  • Scissor Test 只需屏幕坐标,GPU 通常在光栅化阶段就丢弃裁剪区域外的片段
  • 这些片段不会进入片段着色器,不产生浪费

2. Early Fragment Test

现代 GPU 支持在片段着色器之前执行测试(Scissor/Stencil/Depth),提前剔除不可见片段,避免浪费计算。

  • 硬件优化:Early Fragment Test 是 GPU 的性能优化,非 OpenGL/WebGL 规范强制要求,具体行为由硬件和驱动决定
  • 显式声明(WebGL2/OpenGL ES 3.1+):layout(early_fragment_tests) in;

最佳实践:避免在片段着色器中修改 gl_FragDepth 或使用 discard,先渲染不透明物体(从前往后),半透明物体从后往前渲染。

Attachment

Attachment(附件)是帧缓冲对象(Framebuffer)的存储槽位,用于指定渲染管道输出数据的存储位置。每个 Attachment 可以附加一个纹理(Texture)或渲染缓冲对象(Renderbuffer),用于存储渲染结果的不同类型数据。

WebGL 中的 Attachment

Attachment作用说明
COLOR_ATTACHMENT0颜色附件 0。存储片段着色器输出的颜色数据(WebGL1 只支持一个颜色附件)
COLOR_ATTACHMENT1 ~ COLOR_ATTACHMENT15颜色附件 1-15。WebGL2 支持多渲染目标(MRT),片段着色器可同时输出到多个颜色附件
DEPTH_ATTACHMENT深度附件。存储深度测试的深度值,范围 [0, 1]
STENCIL_ATTACHMENT模板附件。存储模板测试的模板值,8 位整数
DEPTH_STENCIL_ATTACHMENT深度 + 模板附件。同时存储深度和模板数据(WebGL1 需要扩展 WEBGL_depth_texture,WebGL2 原生)

RenderBuffer

RenderBuffer (渲染缓冲对象) 是一种 GPU 内存对象,专门用作帧缓冲的附件 (Attachment),存储渲染数据。它是帧缓冲系统的一部分,通常用于深度缓冲或模板缓冲。

核心特点

  • 不能在着色器中采样:RenderBuffer 可以作为渲染输出目标(写入),GPU 固定管道可以读取它进行测试(如深度测试、模板测试),但不能在着色器中通过 texture2D() 等函数采样读取(与纹理不同)
  • 性能优化:相比纹理,RenderBuffer 针对渲染输出优化,在某些情况下性能更好

RenderBuffer vs Texture

特性RenderBufferTexture
着色器采样不支持(不能在着色器中读取)支持(可以在着色器中采样)
GPU 固定管道支持(深度测试、模板测试自动读取)支持(深度测试、模板测试自动读取)
性能渲染输出性能更好(某些硬件)采样性能更好
Mipmap不支持支持
使用场景深度/模板缓冲,不需要着色器采样的附件需要在着色器中采样的附件(阴影贴图等)
多重采样WebGL2 支持 MSAA需要特殊处理
灵活性仅用于帧缓冲附件可用于帧缓冲附件、着色器采样、数据传输

WebGL2 的多重采样

// WebGL2: 创建 MSAA RenderBuffer(抗锯齿)
gl.renderbufferStorageMultisample(
  gl.RENDERBUFFER,
  4, // 采样数(通常 4 或 8)
  gl.RGBA8, // 内部格式
  512,
  512 // 宽度、高度
);

GLSL 中的精度限制(precision)

GLSL 中的精度限制(precision)用于指定浮点数和整数变量的精度等级,直接影响计算的数值范围、精度和性能。这是 OpenGL ES(因此也是 WebGL)特有的特性,桌面 OpenGL 不需要。

为什么需要精度限制?

  1. 硬件差异:移动 GPU 和桌面 GPU 的架构不同

    • 移动 GPU:功耗和面积受限,支持多种精度(16 位、24 位、32 位)以平衡性能和精度
    • 桌面 GPU:通常统一使用 32 位浮点,性能足够强
  2. 性能优化:低精度计算更快、更省电

    • lowp(低精度):寄存器占用少、计算单元简单、功耗低
    • highp(高精度):寄存器占用多、计算复杂、功耗高
  3. 资源节约:精度影响 GPU 寄存器和带宽消耗

    • 低精度变量占用更少的寄存器空间
    • Varying 变量的精度影响顶点着色器到片段着色器的数据传输带宽

精度级别

精度浮点数范围(近似)浮点数精度整数范围用途
lowp-2 ~ +28-10 位(1/256)-2^8 ~ 2^8颜色、归一化方向、纹理坐标
mediump-2^14 ~ +2^1410-16 位-2^10 ~ 2^10纹理坐标、法线、中等范围的计算
highp-2^62 ~ +2^6216-32 位-2^16 ~ 2^16位置计算、矩阵变换、高精度需求

注意

  • 规范只定义了最低要求,实际精度由硬件决定
  • 某些移动设备的片段着色器不支持 highp(需检测)

齐次坐标

齐次坐标(Homogeneous Coordinates)是计算机图形学中的核心概念,通过在 N 维坐标中增加一个额外的分量 w,将 N 维空间嵌入到 (N+1) 维空间中。3D 图形中,齐次坐标将 3D 笛卡尔坐标 (x, y, z) 表示为 4D 向量 (x, y, z, w)。在 WebGL 中,顶点着色器输出齐次坐标 gl_Position,GPU 自动执行透视除法得到 笛卡尔坐标(NDC)。

核心思想:齐次坐标通过 w 分量统一表示点和向量,并将平移、旋转、缩放、投影等变换统一为矩阵乘法。

齐次坐标与笛卡尔坐标的转换

笛卡尔坐标 → 齐次坐标:
(x, y, z) → (x, y, z, 1)    // 点(Position)
(x, y, z) → (x, y, z, 0)    // 向量(Direction)

齐次坐标 → 笛卡尔坐标(透视除法):
(x, y, z, w) → (x/w, y/w, z/w)

为什么需要齐次坐标?

  1. 统一变换表示:所有仿射变换(平移、旋转、缩放)和投影变换都可以用 4×4 矩阵表示
// 没有齐次坐标:平移需要特殊处理
vec3 transformed = rotation * position + translation;  // 不是纯矩阵运算

// 有齐次坐标:所有变换统一为矩阵乘法
vec4 transformed = matrix * vec4(position, 1.0);  // 统一的矩阵运算
  1. 表示无穷远点/向量:w = 0 表示方向向量(没有平移)或无穷远点
(1, 0, 0, 0)  // X 轴方向的无穷远点
(0, 1, 0, 0)  // Y 轴方向的无穷远点
  1. 实现透视投影:通过修改 w 分量实现透视效果
透视投影后:gl_Position = (x, y, z, z)  // w = z(深度)
透视除法后:屏幕坐标 = (x/z, y/z, 1)   // 近大远小

WebGL 中的全局状态

WebGL 是一个状态机,维护了大量全局状态来控制渲染行为。这些状态通过各种 API 进行配置,在绘制时生效。

状态名称关联 API值类型说明
viewportgl.viewport{x, y, width, height}视口范围
programgl.useProgramWebGLProgram着色器

绑定状态(Bindings)

状态名称目标类型(Target)值类型说明
array_buffer_bindingARRAY_BUFFERWebGLBuffer当前绑定的顶点缓冲
element_array_buffer_bindingELEMENT_ARRAY_BUFFERWebGLBuffer当前绑定的索引缓冲
vertex_array_bindingVERTEX_ARRAY (WebGL2)WebGLVertexArrayObject当前绑定的 VAO
texture_binding_2dTEXTURE_2DWebGLTexture当前纹理单元绑定的 2D 纹理
texture_binding_cube_mapTEXTURE_CUBE_MAPWebGLTexture当前纹理单元绑定的立方体贴图
framebuffer_bindingFRAMEBUFFERWebGLFramebuffer当前绑定的帧缓冲
renderbuffer_bindingRENDERBUFFERWebGLRenderbuffer当前绑定的渲染缓冲

纹理单元状态

状态名称关联 API值类型说明
active_texturegl.activeTextureGLenum当前活动的纹理单元(TEXTURE0 + i)
texture_bindingsgl.bindTextureArray每个纹理单元绑定的纹理

像素操作状态(Pixel Operations)

状态名称关联 API值类型说明
scissor_testgl.enable(SCISSOR_TEST)boolean裁剪测试是否启用
scissor_boxgl.scissor{x, y, width, height}裁剪区域
stencil_testgl.enable(STENCIL_TEST)boolean模板测试是否启用
stencil_func_frontgl.stencilFuncSeparate{func, ref, mask}正面模板测试函数
stencil_func_backgl.stencilFuncSeparate{func, ref, mask}背面模板测试函数
stencil_op_frontgl.stencilOpSeparate{sfail, dpfail, dppass}正面模板操作
stencil_op_backgl.stencilOpSeparate{sfail, dpfail, dppass}背面模板操作
depth_testgl.enable(DEPTH_TEST)boolean深度测试是否启用
depth_funcgl.depthFuncGLenum深度比较函数
blendgl.enable(BLEND)boolean颜色混合是否启用
blend_equationgl.blendEquationSeparate{rgb, alpha}RGB 和 Alpha 的混合方程
blend_funcgl.blendFuncSeparate{srcRGB, dstRGB, srcA, dstA}混合因子
blend_colorgl.blendColor{r, g, b, a}混合常量颜色
cull_facegl.enable(CULL_FACE)boolean面剔除是否启用
cull_face_modegl.cullFaceGLenum剔除模式(BACK/FRONT/FRONT_AND_BACK
front_facegl.frontFaceGLenum正面定义(CCW/CW

帧缓冲控制状态(Framebuffer Control)

状态名称关联 API值类型说明
color_write_maskgl.colorMask{r, g, b, a}颜色通道写入掩码
depth_write_maskgl.depthMaskboolean深度写入掩码
stencil_write_maskgl.stencilMasknumber模板写入掩码
clear_colorgl.clearColor{r, g, b, a}清屏颜色值
clear_depthgl.clearDepthnumber清屏深度值
clear_stencilgl.clearStencilnumber清屏模板值

WebGL 中的资源

WebGL 应用需要管理多种 GPU 资源,这些资源占用显存,需要在不使用时及时释放。通过 gl.create*() 创建后返回句柄,通过 gl.delete*() 释放资源。

着色器资源

资源类型说明
Shader顶点/片段着色器源码,编译后可删除
Program链接后的着色器程序,包含可执行的着色器

同步资源(WebGL 2.0)

资源类型说明
Query异步查询 GPU 状态(如遮挡查询、时间查询)
SyncCPU-GPU 同步栅栏

对象资源

对象资源是 WebGL 中可以通过 gl.bind*() 绑定到 Target 的资源。必须先绑定到 Target 才能配置和使用,绑定后的操作隐式作用于该对象。

资源类型Object 缩写Target说明WebGL1 支持
FramebufferFBOFRAMEBUFFER / READ_FRAMEBUFFER离屏渲染目标容器
RenderbufferRBORENDERBUFFER作为帧缓冲附件,存储深度/模板数据
BufferVBO/IBO/UBO/PBO/TBOARRAY_BUFFER存储顶点数据、索引数据、Uniform、像素等VBO/IBO
VertexArrayVAOVERTEX_ARRAY保存顶点属性配置状态
Texture-TEXTURE_2D / TEXTURE_CUBE_MAP存储图像数据,用于着色器采样
Sampler-纹理单元(bindSampler(unit, ...)管理纹理采样参数(独立于纹理对象)
TransformFeedback-TRANSFORM_FEEDBACK捕获顶点着色器输出到 Buffer

Buffer 在 WebGL 中有多种专用类型(通过不同的 Target 区分用途):

  • VBO (Vertex Buffer Object): ARRAY_BUFFER - 存储顶点属性数据(位置、法线、UV 等)
  • IBO (Index Buffer Object): ELEMENT_ARRAY_BUFFER - 存储顶点索引,复用顶点数据
  • UBO (Uniform Buffer Object): UNIFORM_BUFFER (WebGL2) - 批量上传 Uniform 数据,提高性能
  • PBO (Pixel Buffer Object): PIXEL_PACK_BUFFER / PIXEL_UNPACK_BUFFER (WebGL2) - 异步传输像素数据,优化纹理上传/读取
  • TBO (Texture Buffer Object): TEXTURE_BUFFER (WebGL2) - 将 Buffer 作为一维纹理访问,存储大量结构化数据

WebGL 中的 Target

Target(目标/绑定点)是 WebGL 状态机中的全局绑定点,WebGL API 通过 Target 来隐式指定操作的资源对象。调用 gl.bind*() 将资源绑定到 Target 后,后续对该 Target 的操作都会作用于这个对象。

Target 的典型用法

  • 配置资源bufferDatatexImage2DtexParameteri 等通过 Target 指定要配置的对象
  • 绘制操作drawElementsELEMENT_ARRAY_BUFFER 读取索引;drawArrays / drawElements 写入 DRAW_FRAMEBUFFER(或 FRAMEBUFFER
  • 数据读取readPixelsREAD_FRAMEBUFFER(WebGL2)或 FRAMEBUFFER(WebGL1)读取像素
  • 数据拷贝copyBufferSubDataCOPY_READ_BUFFER 读取并写入 COPY_WRITE_BUFFER

WebGL 中的 Target

Target作用说明
ARRAY_BUFFER配置顶点属性数据。
ELEMENT_ARRAY_BUFFER存储顶点索引数据。drawElements 实时读取,绘制时必须保持绑定
FRAMEBUFFER指定渲染输出目标。绘制时实时读取,决定渲染到 FBO 还是 Canvas,必须保持绑定
TEXTURE_2D2D 纹理。配置纹理参数时使用,绘制时使用纹理单元绑定而非此 Target
TEXTURE_CUBE_MAP立方体贴图(6 个面)。用于天空盒、环境反射
RENDERBUFFER离屏渲染存储。作为帧缓冲的深度/模板附件,绘制时不直接使用
COPY_READ_BUFFER (WebGL2)缓冲区拷贝源。用于 copyBufferSubData
COPY_WRITE_BUFFER (WebGL2)缓冲区拷贝目标。用于 copyBufferSubData
UNIFORM_BUFFER (WebGL2)Uniform Buffer Objects。批量上传 Uniform 数据,绘制时读取
PIXEL_PACK_BUFFER (WebGL2)异步读取像素数据(GPU → CPU)。用于 readPixels
PIXEL_UNPACK_BUFFER (WebGL2)异步上传像素数据(CPU → GPU)。用于 texImage2D / texSubImage2D
TEXTURE_3D (WebGL2)3D 纹理(体积纹理)。用于体积渲染、3D 噪声
TEXTURE_2D_ARRAY (WebGL2)2D 纹理数组。纹理集合(如地形分层纹理)
DRAW_FRAMEBUFFER (WebGL2)绘制目标。可与 READ_FRAMEBUFFER 分离绑定
READ_FRAMEBUFFER (WebGL2)读取源。用于 readPixelsblitFramebuffer

WebGL 2.0 有三个 framebuffer 绑定点。bindFramebuffer(FRAMEBUFFER, x) 同时设置 READ 和 DRAW。bindFramebuffer(READ_FRAMEBUFFER, x) 只设置 READ。bindFramebuffer(DRAW_FRAMEBUFFER, x) 只设置 DRAW。FRAMEBUFFER_BINDING 总是返回 DRAW_FRAMEBUFFER_BINDING 的值。WebGL 1.0 只有 FRAMEBUFFER,不能分离读写。

WebGL/OpenGL 架构设计

WebGL/OpenGL 的 API 设计体现了图形硬件的工作方式和历史演进。理解这些设计理念有助于更高效地使用 API,避免常见的性能陷阱。

状态机架构:WebGL 本质上是一个状态机。所有的配置操作(如启用深度测试、设置混合模式、绑定纹理)都会修改全局状态,这些状态会一直保持,直到被显式修改。状态机设计减少了重复的 API 调用,但也带来了副作用——函数的行为依赖于当前状态,调用顺序很关键。

管道化架构:渲染管道是一个严格的单向数据流,数据从 CPU 上传到 GPU,经过各个阶段,最终写入帧缓冲。管道分为固定功能阶段(如光栅化、深度测试)和可编程阶段(顶点着色器、片段着色器)。固定功能阶段只能通过状态开关和参数配置,可编程阶段允许编写 GLSL 代码自定义处理逻辑。

CPU-GPU 异步执行:WebGL 的绘制命令是同步提交但异步执行的。drawArrays 等绘制调用会立即返回,实际的 GPU 渲染发生在后台。查询操作(如 readPixelsgetError)会强制 CPU 等待 GPU 完成,造成性能瓶颈。WebGL2 提供异步查询对象(Query)和同步对象(Sync),允许在不阻塞 CPU 的情况下查询 GPU 状态(如遮挡查询、时间查询),提高 CPU-GPU 并行效率。

绑定系统:WebGL 通过绑定点(Target)来隐式指定操作对象。调用 bind*() 将资源绑定到某个 Target 后,后续对该 Target 的操作都会作用于这个资源。这种设计让 API 更简洁,配置操作不需要每次都传递对象引用。

对象-句柄模型:WebGL 通过句柄间接访问 GPU 资源。create*() 返回的是不透明的句柄对象,而不是直接的内存地址。资源管理需要显式释放,JavaScript 的 GC 只回收句柄对象,不会释放 GPU 内存。

数据与配置分离:WebGL2 引入了多种机制将数据和配置分离。VAO 将顶点属性配置打包成可复用对象,切换模型只需绑定不同的 VAO;Sampler 对象将纹理数据和采样参数分离,同一纹理可以使用不同的过滤模式。这些设计减少了状态切换和 API 调用。

Buffer 资源共享:同一个 Buffer 对象可以用于多种用途。Buffer 创建时不指定类型,只有在绑定到不同的 Target 时才确定其用途。同一个 Buffer 可以作为 VBO/IBO/UBO/PBO/TBO,也可以被多个 VAO 引用,通过不同的 offset 和 stride 复用数据。

插槽式资源绑定:GPU 提供编号的硬件插槽(Slot/Binding Point),允许多个同类资源同时绑定。这些插槽是预先存在的全局绑定点,不依赖着色器编译。

  • 纹理单元(Texture Units):GPU 提供多个纹理单元插槽(编号 0, 1, 2, ...,对应常量 TEXTURE0, TEXTURE1, ...,WebGL1 至少 8 个,WebGL2 至少 16 个)。通过 activeTexture(gl.TEXTURE0 + i) 切换当前活动的纹理单元,bindTexture(gl.TEXTURE_2D, texture) 将纹理对象绑定到当前活动纹理单元。
  • UBO 绑定点(WebGL2):GPU 提供多个 UBO 绑定点插槽。通过 bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, buffer)bindBufferRange(gl.UNIFORM_BUFFER, bindingPoint, buffer, offset, size) 将 Buffer 对象(或其一部分)绑定到编号为 bindingPoint 的绑定点

着色器变量索引:着色器编译链接后,各种变量会被分配内部索引,用于在 JavaScript 中标识和访问这些变量。

  • Attribute 变量索引(location):顶点着色器中的 attribute(WebGL1)或 in(WebGL2)变量会被分配一个 location 索引。通过 getAttribLocation(program, 'aPosition') 查询变量的 location(WebGL2 可在 GLSL 中用 layout(location = N) 显式指定)。
  • Uniform 变量索引(location):着色器中的 uniform 变量会被分配一个 location。通过 getUniformLocation(program, 'uColor') 查询变量的 location(返回 WebGLUniformLocation 对象或 null)。通过 uniform*() 系列函数向该 location 上传数据。纹理变量传入的是 纹理单元
  • Uniform Block 索引(blockIndex,WebGL2):着色器中的 uniform 块会被分配一个 blockIndex。通过 getUniformBlockIndex(program, 'Matrices') 查询 Uniform Block 的索引(返回非负整数或 gl.INVALID_INDEX)。需要通过 uniformBlockBinding 将 blockIndex 映射到一个绑定点,才能与实际的 UBO 关联

扩展机制:核心 API 保持最小化,新功能通过扩展逐步引入。开发者需要显式查询和启用扩展。这种设计保持了向后兼容,让应用可以按需使用硬件特性,但也增加了兼容性测试的复杂度。WebGL1 通过扩展引入的功能,在 WebGL2 中成为核心特性(如 VAO、深度纹理)。

最小化 API 设计:WebGL 基于 OpenGL ES,移除了传统 OpenGL 的立即模式。数据必须先上传到 Buffer,然后通过绘制命令引用。这种设计强制数据预先上传到 GPU,减少了 CPU-GPU 数据传输,但使用门槛更高。