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 规范。
渲染管道增强
| 管道阶段 | WebGL1 | WebGL2 新增 |
|---|---|---|
| 顶点处理 | 顶点着色器 | Transform Feedback(可捕获顶点数据回传到缓冲区) |
| 片段处理 | 单一渲染目标 | 多渲染目标 MRT(同时输出到多个颜色附件) |
| 纹理采样 | 2D 纹理、立方体贴图 | 3D 纹理、2D 纹理数组、采样器对象 |
| 着色器语言 | GLSL ES 1.00 | GLSL ES 3.00(支持整数运算、位运算、更多内置函数) |
核心功能对比
| 功能 | WebGL1 | WebGL2 |
|---|---|---|
| 顶点数组对象 (VAO) | 需要扩展 OES_vertex_array_object | 原生支持 |
| 实例化渲染 | 需要扩展 ANGLE_instanced_arrays | 原生支持 drawArraysInstanced |
| 多重采样 | 不支持 | 支持 MSAA 抗锯齿 |
| Uniform 缓冲对象 | 不支持 | 支持 UBO,减少 Uniform 上传开销 |
| 整数纹理 | 不支持 | 支持 INT、UNSIGNED_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 WebGL 和 Can 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 时内容保留到下一帧 |
powerPreference | GPU 选择偏好(默认 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 渲染管道包含以下阶段:
- Vertex Specification(顶点规范)
- Vertex Processing(顶点处理)
- Vertex Post-Processing(顶点后处理)
- Rasterization(光栅化)
- Fragment Processing(片段处理)
- Per-Sample Operations(逐样本操作)
- 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(顶点后处理)
固定功能阶段,执行以下操作:
- Transform Feedback(WebGL2 支持):捕获顶点着色器输出的数据回传到缓冲区,用于 GPU 粒子系统等场景
// WebGL2 示例
const transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
gl.transformFeedbackVaryings(program, ["vPosition"], gl.SEPARATE_ATTRIBS);
- 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); // 三角形带
-
Clipping(裁剪):裁剪超出视锥体的图元,丢弃完全在视锥体外的图元,对部分在视锥体内的图元进行裁剪。判断条件:
-w ≤ x,y,z ≤ w -
Perspective Divide(透视除法/归一化设备坐标-NDC 空间):将裁剪空间坐标
(x, y, z, w)转换为 NDC 坐标(x/w, y/w, z/w),NDC 坐标范围为[-1, 1] -
Viewport Transform(视口变换/屏幕空间):将 NDC 坐标
[-1, 1]映射到屏幕像素坐标。通过gl.viewport()设置映射到的屏幕像素范围 -
Face Culling(面剔除):根据三角形的正反面剔除背面三角形(需通过
gl.enable(gl.CULL_FACE)启用)
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK); // 剔除背面(默认)
gl.frontFace(gl.CCW); // 逆时针为正面(默认)
Rasterization(光栅化)
将图元转换为片段 (Fragment),确定图元覆盖哪些像素。
- Scan Conversion(扫描转换):确定图元覆盖的像素位置。输入几何图元,输出像素集合
- 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)
- webgl1: 通过内置变量
- 可选:通过
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 规范):
- Scissor Test(裁剪测试)
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height); // 只渲染矩形区域
- Stencil Test(模板测试)
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.EQUAL, 1, 0xff); // 比较函数
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); // 操作
- Depth Test(深度测试)
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS); // 深度值更小时通过
- Blending(颜色混合)
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Alpha 混合
gl.blendEquation(gl.FUNC_ADD); // 混合方程
- 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)
vertexAttribPointer的offset:顶点属性数组中第一个组件的字节偏移量(从当前绑定的 buffer 起始位置算起)vertexAttribPointer的stride:相邻两个顶点之间的字节间隔drawArrays的first:从第几个顶点开始读取(顶点索引,GPU 会自动计算:buffer起始 + offset + first * stride)drawArrays的count:绘制多少个顶点
启用顶点属性数组
从常量模式切换到数组模式,让顶点着色器从 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);
数据对比:
| 方式 | 顶点数据 | 索引数据 | 总计 | 节省 |
|---|---|---|---|---|
drawArrays | 72 字节(6 顶点) | 无 | 72 字节 | - |
drawElements | 48 字节(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:
- 创建 VAO
const vao = gl.createVertexArray();
- 配置 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);
- 绘制时只需绑定 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 读取时)方式,影响 texImage2D、texSubImage2D 和 readPixels 的行为。
// 设置解包对齐方式(默认 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 |
|---|---|---|
| 缩放支持 | 支持任意缩放 | 不支持缩放,源和目标大小必须一致 |
| 翻转支持 | 支持水平/垂直翻转 | 不支持翻转 |
| 过滤模式 | 支持 NEAREST 和 LINEAR | 不支持过滤(等同于 NEAREST) |
| MSAA 解析 | 原生支持多重采样解析 | 不支持 |
| 复制目标 | 帧缓冲附件(可以是纹理或渲染缓冲) | 只能复制到纹理 |
| 复制范围 | 支持复制颜色/深度/模板的任意组合 | 只能复制颜色缓冲区 |
| WebGL 版本要求 | 仅 WebGL2 | WebGL1 和 WebGL2 都支持 |
最佳实践:
- 优先使用
blitFramebuffer(WebGL2):功能更强大,性能更好 - 使用
copyTexSubImage2D(WebGL1 兼容):需要兼容 WebGL1 或只需简单复制时 - MSAA 场景必须使用
blitFramebuffer解析多重采样 - 避免频繁的 CPU-GPU 数据传输(
readPixels+texImage2D),优先使用 GPU 端的 Blit 操作
copyTexImage2D 和 copyTexSubImage2D
这两个 API 用于将帧缓冲(当前绑定的 READ_FRAMEBUFFER 或 FRAMEBUFFER)的像素数据复制到纹理中,在 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: 复制区域大小
);
两者区别:
| 特性 | copyTexImage2D | copyTexSubImage2D |
|---|---|---|
| 纹理初始化 | 会重新分配纹理存储空间 | 不改变纹理存储空间 |
| 更新范围 | 更新整个纹理(必须匹配 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 可执行的中间代码:
- 词法和语法分析:解析 GLSL 语法,构建抽象语法树(AST)
- 语义检查:验证类型、变量声明、函数调用是否正确
- 优化:移除未使用的变量和代码、常量折叠、内联函数
- 生成中间代码:编译为驱动内部的 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 将编译后的顶点着色器和片段着色器链接为可执行程序:
- 接口匹配验证:检查顶点着色器的
varying输出与片段着色器的varying输入是否匹配(变量名、类型、精度) - 资源分配:为 Attribute、Uniform、Varying 变量分配 location 索引
- 优化:跨着色器优化(如常量传播)、移除未使用的 Varying 变量
- 生成可执行代码:将中间代码转换为 GPU 原生指令(如 AMD GCN ISA、NVIDIA SASS)
链接成功后,程序可以通过 useProgram 绑定并用于渲染。可以通过 getAttribLocation、getUniformLocation 查询变量的 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 对比:
| 特性 | WebGL1 | WebGL2 |
|---|---|---|
| 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 函数 | 示例 |
|---|---|---|
float | uniform1f(loc, v) | gl.uniform1f(loc, 1.0) |
vec2 | uniform2f(loc, x, y) 或 uniform2fv(loc, [x, y]) | gl.uniform2f(loc, 1.0, 2.0) |
vec3 | uniform3f(loc, x, y, z) 或 uniform3fv(loc, [x, y, z]) | gl.uniform3fv(loc, [1, 0, 0]) |
vec4 | uniform4f(loc, x, y, z, w) 或 uniform4fv(loc, [x, y, z, w]) | gl.uniform4fv(loc, [1, 0, 0, 1]) |
int / bool | uniform1i(loc, v) | gl.uniform1i(loc, 1) |
sampler2D | uniform1i(loc, unit) | gl.uniform1i(loc, 0) |
mat2 | uniformMatrix2fv(loc, false, data) | gl.uniformMatrix2fv(loc, false, mat2) |
mat3 | uniformMatrix3fv(loc, false, data) | gl.uniformMatrix3fv(loc, false, mat3) |
mat4 | uniformMatrix4fv(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 内存不仅,确保跨平台对齐一致性。关键规则:
- 标量(
float、int):4 字节对齐 vec2:8 字节对齐;vec3、vec4: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 决定了片段深度值与深度缓冲区中已有深度值的比较方式。当片段通过深度测试时,如果 depthMask 为 true,该片段的深度值会更新到深度缓冲区;如果测试失败,片段被丢弃,深度缓冲区保持不变。
// 示例:渲染天空盒(总是在最远处)
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可以是EQUAL、LESS、GREATER等(与深度测试相同) - 例如
stencilFunc(gl.EQUAL, 1, 0xff)表示:只有模板值等于 1 的像素才通过
步骤 2:配置模板值更新规则
// 设置三种情况下如何更新模板值
gl.stencilOp(
gl.KEEP, // sfail: 模板测试失败时
gl.KEEP, // dpfail: 模板通过但深度测试失败时
gl.REPLACE // dppass: 两个测试都通过时
);
GPU 根据测试结果选择对应操作更新模板缓冲区。REPLACE 会将模板值更新为 stencilFunc 中的参考值 ref。
为什么设计成 ref 和 mask 的形式?
这种设计提供了灵活的位级控制能力:
- ref(参考值):作为比较的基准值,也是
REPLACE操作写入的值 - 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_COLOR 或 CONSTANT_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 // 偏移量(字节)
);
| 特性 | drawArrays | drawElements |
|---|---|---|
| 顶点读取方式 | 按顺序读取顶点 | 通过索引读取顶点 |
| 顶点复用 | 不支持,重复顶点需要重复存储 | 支持,通过索引复用顶点 |
| 内存效率 | 较低(有重复数据) | 较高(无重复数据) |
| 适用场景 | 简单图形、顶点不重复 | 复杂模型、共享顶点的网格 |
| 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.RGBA4 | 4 位 RGBA |
gl.RGB565 | 5-6-5 RGB |
gl.RGB5_A1 | 5-5-5-1 RGBA |
gl.DEPTH_COMPONENT16 | 16 位深度 |
gl.STENCIL_INDEX8 | 8 位模板 |
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
在片段着色器中启用导数函数 dFdx、dFdy、fwidth,用于计算屏幕空间梯度。
// 启用扩展
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_FragColor 和 gl_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 渲染管道中涉及多个坐标空间的变换,这些变换发生在不同的管道阶段:
-
模型空间 → 世界空间 → 视图/相机空间 → 裁剪空间
- 发生在:Vertex Shader(顶点着色器)
- 通过 MVP 矩阵(Model-View-Projection)完成,输出到
gl_Position - 投影矩阵分为透视投影(锥台形)和正交投影(长方体),决定了视锥体的形状(详见下文)
- 裁剪空间命名原因:此空间坐标用于后续的裁剪操作,GPU 在此阶段判断顶点是否在视锥体内(通过比较
x, y, z与w的关系)
-
裁剪空间 → NDC 空间(标准化设备坐标)
- 发生在:Vertex Post-Processing(顶点后处理) 阶段
- GPU 自动执行透视除法:
(x/w, y/w, z/w),将裁剪空间坐标转换为 NDC 范围[-1, 1]
-
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 通常在光栅化阶段就丢弃裁剪区域外的片段
- 这些片段不会进入片段着色器,不产生浪费
现代 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:
| 特性 | RenderBuffer | Texture |
|---|---|---|
| 着色器采样 | 不支持(不能在着色器中读取) | 支持(可以在着色器中采样) |
| 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 不需要。
为什么需要精度限制?
-
硬件差异:移动 GPU 和桌面 GPU 的架构不同
- 移动 GPU:功耗和面积受限,支持多种精度(16 位、24 位、32 位)以平衡性能和精度
- 桌面 GPU:通常统一使用 32 位浮点,性能足够强
-
性能优化:低精度计算更快、更省电
lowp(低精度):寄存器占用少、计算单元简单、功耗低highp(高精度):寄存器占用多、计算复杂、功耗高
-
资源节约:精度影响 GPU 寄存器和带宽消耗
- 低精度变量占用更少的寄存器空间
- Varying 变量的精度影响顶点着色器到片段着色器的数据传输带宽
精度级别:
| 精度 | 浮点数范围(近似) | 浮点数精度 | 整数范围 | 用途 |
|---|---|---|---|---|
lowp | -2 ~ +2 | 8-10 位(1/256) | -2^8 ~ 2^8 | 颜色、归一化方向、纹理坐标 |
mediump | -2^14 ~ +2^14 | 10-16 位 | -2^10 ~ 2^10 | 纹理坐标、法线、中等范围的计算 |
highp | -2^62 ~ +2^62 | 16-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)
为什么需要齐次坐标?
- 统一变换表示:所有仿射变换(平移、旋转、缩放)和投影变换都可以用 4×4 矩阵表示
// 没有齐次坐标:平移需要特殊处理
vec3 transformed = rotation * position + translation; // 不是纯矩阵运算
// 有齐次坐标:所有变换统一为矩阵乘法
vec4 transformed = matrix * vec4(position, 1.0); // 统一的矩阵运算
- 表示无穷远点/向量:w = 0 表示方向向量(没有平移)或无穷远点
(1, 0, 0, 0) // X 轴方向的无穷远点
(0, 1, 0, 0) // Y 轴方向的无穷远点
- 实现透视投影:通过修改 w 分量实现透视效果
透视投影后:gl_Position = (x, y, z, z) // w = z(深度)
透视除法后:屏幕坐标 = (x/z, y/z, 1) // 近大远小
WebGL 中的全局状态
WebGL 是一个状态机,维护了大量全局状态来控制渲染行为。这些状态通过各种 API 进行配置,在绘制时生效。
| 状态名称 | 关联 API | 值类型 | 说明 |
|---|---|---|---|
viewport | gl.viewport | {x, y, width, height} | 视口范围 |
program | gl.useProgram | WebGLProgram | 着色器 |
绑定状态(Bindings)
| 状态名称 | 目标类型(Target) | 值类型 | 说明 |
|---|---|---|---|
array_buffer_binding | ARRAY_BUFFER | WebGLBuffer | 当前绑定的顶点缓冲 |
element_array_buffer_binding | ELEMENT_ARRAY_BUFFER | WebGLBuffer | 当前绑定的索引缓冲 |
vertex_array_binding | VERTEX_ARRAY (WebGL2) | WebGLVertexArrayObject | 当前绑定的 VAO |
texture_binding_2d | TEXTURE_2D | WebGLTexture | 当前纹理单元绑定的 2D 纹理 |
texture_binding_cube_map | TEXTURE_CUBE_MAP | WebGLTexture | 当前纹理单元绑定的立方体贴图 |
framebuffer_binding | FRAMEBUFFER | WebGLFramebuffer | 当前绑定的帧缓冲 |
renderbuffer_binding | RENDERBUFFER | WebGLRenderbuffer | 当前绑定的渲染缓冲 |
纹理单元状态
| 状态名称 | 关联 API | 值类型 | 说明 |
|---|---|---|---|
active_texture | gl.activeTexture | GLenum | 当前活动的纹理单元(TEXTURE0 + i) |
texture_bindings | gl.bindTexture | Array | 每个纹理单元绑定的纹理 |
像素操作状态(Pixel Operations)
| 状态名称 | 关联 API | 值类型 | 说明 |
|---|---|---|---|
scissor_test | gl.enable(SCISSOR_TEST) | boolean | 裁剪测试是否启用 |
scissor_box | gl.scissor | {x, y, width, height} | 裁剪区域 |
stencil_test | gl.enable(STENCIL_TEST) | boolean | 模板测试是否启用 |
stencil_func_front | gl.stencilFuncSeparate | {func, ref, mask} | 正面模板测试函数 |
stencil_func_back | gl.stencilFuncSeparate | {func, ref, mask} | 背面模板测试函数 |
stencil_op_front | gl.stencilOpSeparate | {sfail, dpfail, dppass} | 正面模板操作 |
stencil_op_back | gl.stencilOpSeparate | {sfail, dpfail, dppass} | 背面模板操作 |
depth_test | gl.enable(DEPTH_TEST) | boolean | 深度测试是否启用 |
depth_func | gl.depthFunc | GLenum | 深度比较函数 |
blend | gl.enable(BLEND) | boolean | 颜色混合是否启用 |
blend_equation | gl.blendEquationSeparate | {rgb, alpha} | RGB 和 Alpha 的混合方程 |
blend_func | gl.blendFuncSeparate | {srcRGB, dstRGB, srcA, dstA} | 混合因子 |
blend_color | gl.blendColor | {r, g, b, a} | 混合常量颜色 |
cull_face | gl.enable(CULL_FACE) | boolean | 面剔除是否启用 |
cull_face_mode | gl.cullFace | GLenum | 剔除模式(BACK/FRONT/FRONT_AND_BACK) |
front_face | gl.frontFace | GLenum | 正面定义(CCW/CW) |
帧缓冲控制状态(Framebuffer Control)
| 状态名称 | 关联 API | 值类型 | 说明 |
|---|---|---|---|
color_write_mask | gl.colorMask | {r, g, b, a} | 颜色通道写入掩码 |
depth_write_mask | gl.depthMask | boolean | 深度写入掩码 |
stencil_write_mask | gl.stencilMask | number | 模板写入掩码 |
clear_color | gl.clearColor | {r, g, b, a} | 清屏颜色值 |
clear_depth | gl.clearDepth | number | 清屏深度值 |
clear_stencil | gl.clearStencil | number | 清屏模板值 |
WebGL 中的资源
WebGL 应用需要管理多种 GPU 资源,这些资源占用显存,需要在不使用时及时释放。通过 gl.create*() 创建后返回句柄,通过 gl.delete*() 释放资源。
着色器资源
| 资源类型 | 说明 |
|---|---|
| Shader | 顶点/片段着色器源码,编译后可删除 |
| Program | 链接后的着色器程序,包含可执行的着色器 |
同步资源(WebGL 2.0)
| 资源类型 | 说明 |
|---|---|
| Query | 异步查询 GPU 状态(如遮挡查询、时间查询) |
| Sync | CPU-GPU 同步栅栏 |
对象资源
对象资源是 WebGL 中可以通过 gl.bind*() 绑定到 Target 的资源。必须先绑定到 Target 才能配置和使用,绑定后的操作隐式作用于该对象。
| 资源类型 | Object 缩写 | Target | 说明 | WebGL1 支持 |
|---|---|---|---|---|
| Framebuffer | FBO | FRAMEBUFFER / READ_FRAMEBUFFER 等 | 离屏渲染目标容器 | ✓ |
| Renderbuffer | RBO | RENDERBUFFER | 作为帧缓冲附件,存储深度/模板数据 | ✓ |
| Buffer | VBO/IBO/UBO/PBO/TBO | ARRAY_BUFFER 等 | 存储顶点数据、索引数据、Uniform、像素等 | VBO/IBO |
| VertexArray | VAO | VERTEX_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 的典型用法:
- 配置资源:
bufferData、texImage2D、texParameteri等通过 Target 指定要配置的对象 - 绘制操作:
drawElements从ELEMENT_ARRAY_BUFFER读取索引;drawArrays/drawElements写入DRAW_FRAMEBUFFER(或FRAMEBUFFER) - 数据读取:
readPixels从READ_FRAMEBUFFER(WebGL2)或FRAMEBUFFER(WebGL1)读取像素 - 数据拷贝:
copyBufferSubData从COPY_READ_BUFFER读取并写入COPY_WRITE_BUFFER
WebGL 中的 Target:
| Target | 作用说明 |
|---|---|
ARRAY_BUFFER | 配置顶点属性数据。 |
ELEMENT_ARRAY_BUFFER | 存储顶点索引数据。drawElements 实时读取,绘制时必须保持绑定 |
FRAMEBUFFER | 指定渲染输出目标。绘制时实时读取,决定渲染到 FBO 还是 Canvas,必须保持绑定 |
TEXTURE_2D | 2D 纹理。配置纹理参数时使用,绘制时使用纹理单元绑定而非此 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) | 读取源。用于 readPixels、blitFramebuffer |
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 渲染发生在后台。查询操作(如 readPixels、getError)会强制 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 数据传输,但使用门槛更高。