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:
bash
# 编译 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 上下文:
javascript
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 配置:
javascript
// 创建并绑定缓冲区
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)
glsl
// 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 粒子系统等场景
javascript
// WebGL2 示例
const transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
gl.transformFeedbackVaryings(program, ["vPosition"], gl.SEPARATE_ATTRIBS);
- Primitive Assembly (图元装配):将顶点序列组装成图元(三角形、线段或点),类型由
gl.drawArrays(mode, ...)的mode参数决定
javascript
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(裁剪):裁剪超出视锥体的图元,丢弃完全在视锥体外的图元,对部分在视锥体内的图元进行裁剪
-
Perspective Divide (透视除法):将裁剪空间坐标
(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)启用)
javascript
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输出颜色 - webgl2: 通过自定义
out变量输出(单个颜色附件或多渲染目标 MRT)
- webgl1: 通过内置变量
- 可选:通过
discard丢弃片段
glsl
// 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(裁剪测试)
javascript
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height); // 只渲染矩形区域
- Stencil Test(模板测试)
javascript
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.EQUAL, 1, 0xff); // 比较函数
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); // 操作
- Depth Test(深度测试)
javascript
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS); // 深度值更小时通过
- Blending(颜色混合)
javascript
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Alpha 混合
gl.blendEquation(gl.FUNC_ADD); // 混合方程
- Masking(写入遮罩):控制哪些数据可以写入帧缓冲。可以独立控制颜色、深度、模板的写入,甚至可以单独屏蔽颜色的各个通道
javascript
// 颜色遮罩:控制 RGBA 各通道是否可写
gl.colorMask(true, true, true, false); // 禁止写入 Alpha 通道
// 深度遮罩:控制深度缓冲区是否可写
gl.depthMask(false); // 禁止写入深度值
// 模板遮罩:控制模板缓冲区哪些位可写
gl.stencilMask(0xff); // 允许写入所有位
通过所有测试的片段,根据遮罩设置写入帧缓冲对应的附件。
Framebuffer(帧缓冲)
存储最终渲染结果。WebGL 支持两种帧缓冲:
- 默认帧缓冲:直接渲染到 Canvas
javascript
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
- 自定义帧缓冲 (FBO):离屏渲染(用于后处理、阴影贴图等)
javascript
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 等)。
javascript
// 创建缓冲区对象
const buffer = gl.createBuffer();
createBuffer()在 GPU 中分配一块内存空间,返回一个 Buffer 对象的引用- 此时只是创建了引用,尚未分配实际的存储空间
上传数据到 Buffer
将 JavaScript 的类型化数组数据复制到 GPU 内存,分为两个步骤:
步骤 1:绑定 Buffer
javascript
// 将 Buffer 绑定到 ARRAY_BUFFER 绑定点
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
步骤 2:上传数据
javascript
// 准备顶点数据
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" - 后续渲染与当前绑定无关 :绘制时使用的是
vertexAttribPointer配置时记录的 Buffer 引用,不是当前绑定到ARRAY_BUFFER的 Buffer
使用提示(Usage Hint):
bufferData 的第三个参数告诉 GPU 数据的使用模式,GPU 驱动可以据此优化内存分配。
| 常量 | 说明 | 适用场景 | GPU 优化策略 |
|---|---|---|---|
gl.STATIC_DRAW |
数据一次上传,多次绘制 | 静态模型 | 存储在 GPU 显存中,提高读取速度 |
gl.DYNAMIC_DRAW |
数据多次修改,多次绘制 | 动画模型 | 存储在可快速更新的内存区域 |
gl.STREAM_DRAW |
数据一次上传,少量绘制 | 临时数据 | 存储在写入快但读取慢的缓冲区 |
配置顶点属性读取方式
告诉 GPU 如何从那个 Buffer 中解析数据,如何将 Buffer 映射到顶点着色器的 Attribute 变量。
javascript
// 绑定包含顶点数据的 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 读取数据
启用顶点属性数组
从常量模式切换到数组模式,让顶点着色器从 Buffer 中读取数据。
javascript
// 配置完 vertexAttribPointer 后,启用数组模式
gl.enableVertexAttribArray(positionLoc);
gl.enableVertexAttribArray(colorLoc);
为什么需要启用?
vertexAttribPointer只是配置了读取规则,但不会自动启用。默认处于常量模式 。使用固定值(0, 0, 0, 1)- 必须调用
enableVertexAttribArray才能切换到数组模式,从 Buffer 读取数据
索引缓冲区:减少顶点数据冗余
通过索引复用顶点,避免重复存储,减少内存占用。
示例:绘制矩形
javascript
// 顶点数据(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% |
javascript
// 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
js
const vao = gl.createVertexArray();
- 配置 VAO
js
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,自动恢复所有配置
javascript
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
纹理数据准备
纹理接口用于将图像数据上传到 GPU,并配置纹理的采样方式,为片段着色器提供纹理采样功能。
纹理对象的创建
Texture 对象是 GPU 内存中的一块存储空间,用于存储图像数据。
javascript
// 创建纹理对象
const texture = gl.createTexture();
createTexture()在 GPU 中分配一块内存空间,返回一个 Texture 对象的引用- 此时只是创建了引用,尚未分配实际的存储空间
上传数据到 Texture
将图像数据复制到 GPU 内存中,分为两个步骤:
步骤 1:绑定 Texture
javascript
// 将 Texture 绑定到 TEXTURE_2D 绑定点
gl.bindTexture(gl.TEXTURE_2D, texture);
常用绑定点:
gl.TEXTURE_2D:2D 纹理gl.TEXTURE_CUBE_MAP:立方体贴图
步骤 2:上传数据
javascript
// 从 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 中采样数据(过滤模式和环绕模式)。
javascript
// 绑定需要配置的 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 的行为。
javascript
// 设置解包对齐方式(默认 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 是纹理的多级分辨率版本,用于提高渲染质量和性能。
javascript
// 上传基础级别纹理后生成 Mipmap
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.generateMipmap(gl.TEXTURE_2D);
注意:
- WebGL1 只对 2 次幂纹理 有效;WebGL2 没有此限制
- Mipmap 会增加约 33% 的内存占用
- 远距离渲染时 Mipmap 可以减少锯齿和闪烁
在片段着色器中使用纹理
纹理通过纹理单元传递给着色器,涉及三个步骤。
步骤 1:将 Texture 分配给纹理单元
WebGL 提供多个纹理单元(Texture Unit),每个单元是一个独立的"插槽",可以绑定一个纹理。
javascript
// 激活纹理单元 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 变量使用哪个纹理单元。
javascript
// 告诉 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 传递给片段着色器
glsl
// 顶点着色器: 传递纹理坐标
attribute vec3 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = vec4(aPosition, 1.0);
vTexCoord = aTexCoord;
}
步骤 4:片段着色器采样纹理 :通过 sampler2D 类型的 Uniform 接收纹理,使用 texture2D 函数采样
glsl
// 片段着色器: 采样多个纹理
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 引入采样器对象,可以独立管理采样参数。
javascript
// 创建采样器对象
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 使用此采样器
// 采样器参数会覆盖纹理对象的参数
采样器对象的优势:
- 同一纹理可以使用不同的采样参数(例如:同一纹理在不同位置使用不同的过滤模式)
- 减少纹理对象的状态切换开销
- 更灵活的纹理采样配置
着色器接口
着色器接口用于编译 GLSL 着色器代码并链接成可执行的着色器程序 (Program),以及管理着色器中的变量(Attribute、Uniform)。
着色器创建和编译
着色器 (Shader) 使用 GLSL 语言编写,需要经过创建、上传源码、编译三个步骤。
javascript
// 创建着色器对象
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);
着色器编译错误处理:编译着色器后,可以获取错误信息用于调试。
javascript
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
// 检查编译状态
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;
}
return shader;
}
常见编译错误:
- 语法错误:拼写错误、缺少分号等
- 类型不匹配:
vec3赋值给vec4 - 未声明变量:使用了未定义的 Uniform 或 Attribute
- GLSL 版本不兼容:WebGL1 只支持 GLSL ES 1.00
程序创建和链接
将顶点着色器和片段着色器链接成一个可执行程序。
javascript
// 创建程序对象
const program = gl.createProgram();
// 附加着色器
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 链接程序
gl.linkProgram(program);
// 使用程序
gl.useProgram(program);
程序链接错误处理:链接程序后,可以检查链接状态。
javascript
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 检查链接状态
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;
}
return program;
}
常见链接错误:
- Varying 变量不匹配:顶点着色器输出的 Varying 与片段着色器输入的不一致
- 缺少必需输出:顶点着色器未写入
gl_Position,或片段着色器未写入gl_FragColor(WebGL1) - Attribute 数量超限:超过 GPU 支持的最大 Attribute 数量(通过
gl.getParameter(gl.MAX_VERTEX_ATTRIBS)查询)
Attribute 变量管理
Attribute 变量用于接收顶点数据,只能在顶点着色器中使用。
获取 Attribute 位置:
javascript
// 获取 Attribute 变量的位置
const positionLoc = gl.getAttribLocation(program, "aPosition");
// 返回值:
// - 非负整数:变量存在且活跃(被着色器实际使用)
// - -1:变量不存在,或未被使用(被编译器优化掉)
if (positionLoc === -1) {
console.warn('Attribute "aPosition" not found or not used');
}
使用 Attribute 变量:
javascript
// 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,可以提高性能并简化代码。
使用示例:
glsl
// 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
// 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 变量用于传递常量数据(如变换矩阵、光照参数),可在顶点着色器和片段着色器中使用。
javascript
// 获取 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 函数:
javascript
// 标量(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
// GLSL 中定义 Uniform 数组
uniform vec3 uLightPositions[4];
javascript
// 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
- 将多个标量 Uniform 合并为向量 Uniform 可以减少调用次数
WebGL2 的 Uniform Buffer Objects (UBO)
WebGL2 支持 Uniform Buffer Objects,可以将多个 Uniform 打包到缓冲区中批量上传,相比逐个调用 uniform* 函数性能更好。
UBO 的优势:
- 批量上传多个 Uniform,减少 API 调用次数
- 可以在多个着色器程序之间共享 Uniform 数据(如相机矩阵、光照参数)
- 更新部分 Uniform 时使用
bufferSubData,比逐个调用uniform*更高效
使用示例:
glsl
// 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
// 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 的着色器程序都能访问数据
注意事项:
- Uniform Block 的内存布局需要遵循
std140规范(对齐规则复杂) - 可以通过
getActiveUniforms查询每个 Uniform 在块中的偏移量 - WebGL1 不支持 UBO,没有相关扩展
渲染控制接口
渲染控制接口用于配置渲染管道的固定功能阶段(视口、测试、混合等)和执行绘制命令,控制如何将几何数据渲染到帧缓冲。
视口设置
视口 (Viewport) 定义 NDC 坐标到屏幕像素坐标的映射区域。
javascript
// 设置视口(通常与 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)
裁剪测试限制渲染到一个矩形区域,区域外的像素被丢弃。
javascript
// 启用裁剪测试
gl.enable(gl.SCISSOR_TEST);
// 设置裁剪矩形
gl.scissor(100, 100, 200, 200); // x, y, width, height
// 禁用裁剪测试
gl.disable(gl.SCISSOR_TEST);
使用场景:
- 分屏渲染(多视口)
- UI 裁剪(限制绘制区域)
- 优化性能(只渲染可见区域)
深度测试 (Depth Test)
深度测试根据深度值决定片段是否可见,实现 3D 物体的遮挡关系。
javascript
// 启用深度测试
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 |
深度值 ≠ 当前值时通过 |
注意:
- 需要在创建 WebGL 上下文时请求深度缓冲区:
gl = canvas.getContext('webgl', { depth: true })(默认为 true) - 深度值范围为
[0, 1],0 表示近平面,1 表示远平面
模板测试 (Stencil Test)
模板测试使用模板缓冲区进行像素级的遮罩控制,实现复杂的渲染效果。
javascript
// 启用模板测试
gl.enable(gl.STENCIL_TEST);
// 设置模板比较函数
gl.stencilFunc(
gl.EQUAL, // 比较函数
1, // 参考值
0xff // 掩码
);
// 设置模板操作(测试失败、深度测试失败、两者都通过时的操作)
gl.stencilOp(
gl.KEEP, // 模板测试失败时:保持
gl.KEEP, // 深度测试失败时:保持
gl.REPLACE // 两者都通过时:替换为参考值
);
// 控制模板缓冲区的写入掩码
gl.stencilMask(0xff); // 允许写入所有位
// 禁用模板测试
gl.disable(gl.STENCIL_TEST);
模板操作常量:
| 常量 | 说明 |
|---|---|
gl.KEEP |
保持当前值 |
gl.ZERO |
设置为 0 |
gl.REPLACE |
替换为参考值 |
gl.INCR |
递增(不溢出) |
gl.INCR_WRAP |
递增(溢出时回绕) |
gl.DECR |
递减(不溢出) |
gl.DECR_WRAP |
递减(溢出时回绕) |
gl.INVERT |
按位取反 |
使用场景:
- 镜面反射(只在镜面区域渲染反射)
- 阴影体 (Shadow Volume)
- 轮廓渲染
- 遮罩效果
颜色混合 (Blending)
颜色混合用于实现半透明效果,将片段颜色与帧缓冲区颜色混合。
javascript
// 启用混合
gl.enable(gl.BLEND);
// 设置混合函数(源因子、目标因子)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // 标准 Alpha 混合
// 设置混合方程
gl.blendEquation(gl.FUNC_ADD); // 加法混合(默认)
// 分别设置 RGB 和 Alpha 的混合函数
gl.blendFuncSeparate(
gl.SRC_ALPHA,
gl.ONE_MINUS_SRC_ALPHA, // RGB 混合因子
gl.ONE,
gl.ZERO // Alpha 混合因子
);
// 禁用混合
gl.disable(gl.BLEND);
混合方程:
最终颜色 = 源颜色 × 源因子 ⊕ 目标颜色 × 目标因子
其中 ⊕ 由 blendEquation 指定。
常用混合因子:
| 常量 | 说明 |
|---|---|
gl.ZERO |
0 |
gl.ONE |
1 |
gl.SRC_COLOR |
源颜色 |
gl.ONE_MINUS_SRC_COLOR |
1 - 源颜色 |
gl.SRC_ALPHA |
源 Alpha |
gl.ONE_MINUS_SRC_ALPHA |
1 - 源 Alpha |
gl.DST_COLOR |
目标颜色 |
gl.ONE_MINUS_DST_COLOR |
1 - 目标颜色 |
gl.DST_ALPHA |
目标 Alpha |
gl.ONE_MINUS_DST_ALPHA |
1 - 目标 Alpha |
常用混合方程:
| 常量 | 说明 |
|---|---|
gl.FUNC_ADD |
加法:源 + 目标 |
gl.FUNC_SUBTRACT |
减法:源 - 目标 |
gl.FUNC_REVERSE_SUBTRACT |
反向减法:目标 - 源 |
gl.MIN |
最小值(WebGL2) |
gl.MAX |
最大值(WebGL2) |
标准 Alpha 混合示例:
javascript
// 渲染半透明物体
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// 片段着色器输出半透明颜色
// gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5); // 半透明红色
// 混合计算:
// 最终颜色 = (1, 0, 0, 0.5) × 0.5 + 帧缓冲颜色 × (1 - 0.5)
注意:
- 半透明渲染需要从后往前排序(远到近)
- 深度写入通常需要禁用:
gl.depthMask(false)
面剔除 (Face Culling)
面剔除根据三角形的正反面剔除不可见的面,提高渲染性能。
javascript
// 启用面剔除
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);
使用场景:
- 封闭模型(如立方体、球体)只需渲染正面
- 减少约 50% 的片段处理量
绘制命令
执行实际的渲染操作,将顶点数据通过渲染管道转换为屏幕像素。
使用顶点数组绘制:
javascript
// 绘制三角形
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); // 三角形扇
使用索引缓冲区绘制:
javascript
// 绑定索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// 绘制
gl.drawElements(
gl.TRIANGLES, // 图元类型
6, // 索引数量
gl.UNSIGNED_SHORT, // 索引类型
0 // 偏移量(字节)
);
WebGL2 的实例化渲染:
javascript
// 绘制多个实例(用于渲染大量相同物体)
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100); // 绘制 100 个实例
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 100);
写入遮罩 (Write Mask)
写入遮罩控制哪些缓冲区或颜色通道可以被写入,用于保护特定数据不被修改。
颜色遮罩:
javascript
// 控制 RGBA 各通道是否可写
gl.colorMask(true, true, true, false); // 禁止写入 Alpha 通道
// 示例:只写入红色通道
gl.colorMask(true, false, false, false);
// 恢复写入所有通道
gl.colorMask(true, true, true, true);
深度遮罩:
javascript
// 控制深度缓冲区是否可写
gl.depthMask(false); // 禁止写入深度值(深度测试仍然执行)
// 恢复深度写入
gl.depthMask(true);
模板遮罩:
javascript
// 控制模板缓冲区哪些位可写
gl.stencilMask(0xff); // 允许写入所有位
gl.stencilMask(0x00); // 禁止写入任何位
gl.stencilMask(0x0f); // 只允许写入低 4 位
// 分别设置正反面的模板遮罩(WebGL2)
gl.stencilMaskSeparate(gl.FRONT, 0xff); // 正面
gl.stencilMaskSeparate(gl.BACK, 0x00); // 背面
使用场景:
-
深度遮罩:渲染半透明物体时禁止写入深度,避免遮挡后续物体
javascript// 渲染半透明物体 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); // 恢复深度写入 -
颜色遮罩:只更新特定颜色通道
javascript// 只更新 Alpha 通道 gl.colorMask(false, false, false, true); gl.drawArrays(gl.TRIANGLES, 0, count); gl.colorMask(true, true, true, true); -
模板遮罩:保护模板缓冲区的特定位
javascript// 只允许写入模板值的低 4 位 gl.stencilMask(0x0f);
清屏操作
清空帧缓冲区的颜色、深度、模板缓冲。
javascript
// 设置清屏颜色
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);
典型渲染循环:
javascript
function render() {
// 清空缓冲区
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 启用深度测试
gl.enable(gl.DEPTH_TEST);
// 使用着色器程序
gl.useProgram(program);
// 绑定 VAO
gl.bindVertexArray(vao);
// 上传 Uniform
gl.uniformMatrix4fv(mvpMatrixLoc, false, mvpMatrix);
// 绘制
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
requestAnimationFrame(render);
}
render();
帧缓冲接口
帧缓冲接口用于创建自定义帧缓冲对象 (Framebuffer Object, FBO),实现离屏渲染,支持阴影贴图、后处理、动态纹理等高级渲染技术。
帧缓冲对象创建
帧缓冲对象是渲染目标的容器,包含颜色、深度、模板附件。
javascript
// 创建帧缓冲对象
const fbo = gl.createFramebuffer();
// 绑定帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // 后续操作作用于此 FBO
// 绑定到默认帧缓冲(渲染到 Canvas)
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
WebGL2 支持分别绑定读取和绘制帧缓冲:
javascript
// WebGL2
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo); // 绘制目标
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); // 读取源
渲染缓冲对象
渲染缓冲对象 (Renderbuffer) 用于存储渲染数据,通常用作深度或模板附件。
javascript
// 创建渲染缓冲对象
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 的多重采样渲染缓冲:
javascript
// WebGL2: 创建 MSAA 渲染缓冲(抗锯齿)
gl.renderbufferStorageMultisample(
gl.RENDERBUFFER,
4, // 采样数(通常 4 或 8)
gl.RGBA8, // 内部格式
512,
512 // 宽度、高度
);
颜色附件
颜色附件用于存储渲染的颜色数据,可以是纹理或渲染缓冲对象。
使用纹理作为颜色附件:
javascript
// 创建纹理
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 支持多个颜色附件(详见【多渲染目标】章节)。
使用渲染缓冲对象作为颜色附件:
javascript
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);
深度和模板附件
深度和模板附件用于深度测试和模板测试。
深度附件:
javascript
// 使用渲染缓冲对象
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);
深度 + 模板附件:
javascript
// 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);
帧缓冲完整性检查
帧缓冲配置完成后需要检查完整性。
javascript
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 等高级渲染技术。
配置多个颜色附件:
javascript
// 创建多个纹理作为颜色附件
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]);
片段着色器输出到多个附件:
glsl
// 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); // 位置
}
颜色附件数量限制:
javascript
// 查询支持的最大颜色附件数量
const maxDrawBuffers = gl.getParameter(gl.MAX_DRAW_BUFFERS);
console.log("最大颜色附件数量:", maxDrawBuffers); // WebGL2 至少支持 4 个
注意事项:
- WebGL1 不支持 MRT,只能输出到一个颜色附件
- WebGL2 至少支持 4 个颜色附件,具体数量由设备决定
- 所有颜色附件的尺寸必须一致
- MRT 会增加带宽和内存占用,需要权衡性能
WebGL 扩展
WebGL 通过扩展机制提供额外的功能,允许浏览器暴露硬件特定的能力。扩展需要显式启用才能使用,开发者应检查扩展的可用性以确保跨平台兼容性。本章介绍常用扩展的功能和使用方法,完整的扩展列表请参考 WebGL 扩展注册表。
扩展的查询和启用
查询支持的扩展:
javascript
// 获取所有支持的扩展名称
const extensions = gl.getSupportedExtensions();
console.log("Supported extensions:", extensions);
// 示例输出:
// ["ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_color_buffer_half_float", ...]
启用扩展:
javascript
// 启用扩展
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
javascript
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,用于计算屏幕空间梯度。
扩展文档 :OES_standard_derivatives
javascript
// 启用扩展
const ext = gl.getExtension("OES_standard_derivatives");
if (ext) {
console.log("Standard derivatives enabled");
}
片段着色器使用:
glsl
#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
javascript
// 启用扩展
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
javascript
// 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)。
片段着色器使用:
glsl
#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);
}
设置混合函数:
javascript
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)
视锥体形状为锥台(截断的金字塔),模拟人眼透视效果。
javascript
far plane (远平面)
+-----------+
/| /|
/ | / | ← 视锥体(frustum)
/ | / |
/ +-------/---+ near plane (近平面)
/ / / /
+---+-------+ /
eye (相机)
(0, 0, 0)
特点:
- 近大远小:物体大小随距离变化,远处物体看起来更小
- 深度感强:符合真实世界的透视规律
- 应用场景:3D 游戏、建筑可视化、真实感渲染
javascript
// 使用 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)
视景体形状为长方体,平行投影,物体大小不随距离变化。
lua
far plane
+-----------+
| |
| | ← 视景体(长方体)
| |
+-----------+ near plane
所有投影线平行
↓ ↓ ↓ ↓ ↓
投影方向
特点:
- 平行投影:远近物体大小相同,无透视变形
- 无深度感:适合需要精确测量的场景
- 应用场景:2D 游戏、CAD 软件、工程图纸、建筑平面图、UI 界面
javascript
// 创建正交投影矩阵
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,先渲染不透明物体(从前往后),半透明物体从后往前渲染。
WebGL 中的 Target 是什么?
Target(目标/绑定点)是 WebGL 状态机中的全局绑定点,用于指定后续操作作用于哪个资源对象。调用 gl.bind*() 时,会将资源对象绑定到指定的 Target,之后对该 Target 的配置操作都会作用于这个对象。
Target 的用途:某些 API 执行时会从 Target 读取当前绑定的对象,常见的有:
- 配置资源对象 :
bufferData、texImage2D、texParameteri等从对应 Target 读取要配置的对象 - 绘制操作 :
drawElements从ELEMENT_ARRAY_BUFFER读取索引数据;drawArrays/drawElements从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 中的 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 的多重采样:
javascript
// 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 分量统一表示点和向量,并将平移、旋转、缩放、投影等变换统一为矩阵乘法。
齐次坐标与笛卡尔坐标的转换:
scss
笛卡尔坐标 → 齐次坐标:
(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 矩阵表示
glsl
// 没有齐次坐标:平移需要特殊处理
vec3 transformed = rotation * position + translation; // 不是纯矩阵运算
// 有齐次坐标:所有变换统一为矩阵乘法
vec4 transformed = matrix * vec4(position, 1.0); // 统一的矩阵运算
- 表示无穷远点/向量:w = 0 表示方向向量(没有平移)或无穷远点
scss
(1, 0, 0, 0) // X 轴方向的无穷远点
(0, 1, 0, 0) // Y 轴方向的无穷远点
- 实现透视投影:通过修改 w 分量实现透视效果
scss
透视投影后:gl_Position = (x, y, z, z) // w = z(深度)
透视除法后:屏幕坐标 = (x/z, y/z, 1) // 近大远小
WebGL 中有哪些 Object?
WebGL 中的 Object 是封装 GPU 资源的句柄,用于管理渲染所需的各种数据和状态。以下是主要的 Object 类型及其常见简写:
常见 Object 简写:
| 简写 | 全称 | 说明 |
|---|---|---|
| VBO | Vertex Buffer Object | 顶点缓冲对象(存储顶点属性数据) |
| EBO/IBO | Element/Index Buffer Object | 元素/索引缓冲对象(存储索引数据) |
| VAO | Vertex Array Object | 顶点数组对象(保存顶点属性配置状态) |
| FBO | Framebuffer Object | 帧缓冲对象(离屏渲染目标) |
| UBO | Uniform Buffer Object | Uniform 缓冲对象(批量上传 Uniform 数据) |
| PBO | Pixel Buffer Object | 像素缓冲对象(异步读写像素数据) |
| TBO | Texture Buffer Object | 纹理缓冲对象(将缓冲区作为纹理访问) |
| RBO | Renderbuffer Object | 渲染缓冲对象(作为帧缓冲附件) |
数据存储对象:
| Object | 作用说明 | 创建方法 |
|---|---|---|
| Buffer | 存储顶点数据(位置、颜色、法线、UV 等)或索引数据,是 GPU 内存中的数据块 | gl.createBuffer() |
| Texture | 存储图像数据,用于纹理采样(2D 纹理、立方体贴图、3D 纹理等) | gl.createTexture() |
| Renderbuffer | 存储渲染数据,作为帧缓冲的附件(通常用于深度或模板缓冲区),不可直接采样 | gl.createRenderbuffer() |
| Framebuffer | 渲染目标容器,包含颜色、深度、模板附件,用于离屏渲染 | gl.createFramebuffer() |
着色器相关对象:
| Object | 作用说明 | 创建方法 |
|---|---|---|
| Shader | 着色器对象,包含 GLSL 源码(顶点着色器或片段着色器) | gl.createShader() |
| Program | 着色器程序对象,链接顶点着色器和片段着色器,是可在 GPU 上执行的完整程序 | gl.createProgram() |
WebGL2 新增对象:
| Object | 作用说明 | 创建方法 |
|---|---|---|
| VertexArray | 保存顶点属性配置状态(vertexAttribPointer、enableVertexAttribArray、索引缓冲区绑定) |
gl.createVertexArray() |
| Sampler | 纹理采样参数对象,将纹理数据和采样参数分离管理 | gl.createSampler() |
| Query | 查询对象,用于异步查询 GPU 状态(如遮挡查询、变换反馈查询) | gl.createQuery() |
| TransformFeedback | 变换反馈对象,捕获顶点着色器输出数据回传到缓冲区,用于 GPU 粒子系统等 | gl.createTransformFeedback() |
| Sync | 同步对象,用于 CPU-GPU 同步控制(通过 fenceSync 创建栅栏,clientWaitSync 等待完成) |
gl.fenceSync() |
对象的通用特性:
- 所有 Object 都是不透明的引用(WebGLBuffer、WebGLTexture 等类型)
- 通过
gl.bind*()方法绑定到 Target(绑定点)后才能配置或使用 - 通过
gl.delete*()方法释放 GPU 资源(如gl.deleteBuffer(buffer)) - 删除后的对象引用不应再使用,否则可能导致错误