WebGL 的渲染管道和编程接口

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 上传开销
整数纹理 不支持 支持 INTUNSIGNED_INT 纹理格式
深度纹理 需要扩展 WEBGL_depth_texture 原生支持
非 2 次幂纹理 限制严格(不能 Mipmap、必须 CLAMP_TO_EDGE) 完全支持

浏览器支持

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

WebGL 上下文创建和配置

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

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

创建 WebGL 上下文

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 渲染管道包含以下阶段:

  1. Vertex Specification(顶点规范)
  2. Vertex Processing(顶点处理)
  3. Vertex Post-Processing(顶点后处理)
  4. Rasterization(光栅化)
  5. Fragment Processing(片段处理)
  6. Per-Sample Operations(逐样本操作)
  7. 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(顶点后处理)

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

  1. Transform Feedback(WebGL2 支持):捕获顶点着色器输出的数据回传到缓冲区,用于 GPU 粒子系统等场景
javascript 复制代码
// WebGL2 示例
const transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
gl.transformFeedbackVaryings(program, ["vPosition"], gl.SEPARATE_ATTRIBS);
  1. Primitive Assembly (图元装配):将顶点序列组装成图元(三角形、线段或点),类型由 gl.drawArrays(mode, ...)mode 参数决定
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); // 三角形带
  1. Clipping(裁剪):裁剪超出视锥体的图元,丢弃完全在视锥体外的图元,对部分在视锥体内的图元进行裁剪

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

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

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

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

Rasterization(光栅化)

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

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

Fragment Processing(片段处理)

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

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

输出

  • 片段数据:
    • webgl1: 通过内置变量 gl_FragColor 输出颜色
    • webgl2: 通过自定义 out 变量输出(单个颜色附件或多渲染目标 MRT)
  • 可选:通过 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 规范):

  1. Scissor Test(裁剪测试)
javascript 复制代码
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height); // 只渲染矩形区域
  1. Stencil Test(模板测试)
javascript 复制代码
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.EQUAL, 1, 0xff); // 比较函数
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); // 操作
  1. Depth Test(深度测试)
javascript 复制代码
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS); // 深度值更小时通过
  1. Blending(颜色混合)
javascript 复制代码
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Alpha 混合
gl.blendEquation(gl.FUNC_ADD); // 混合方程
  1. 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

  1. 创建 VAO
js 复制代码
const vao = gl.createVertexArray();
  1. 配置 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);
  1. 绘制时只需绑定 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 读取时)方式,影响 texImage2DtexSubImage2DreadPixels 的行为。

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

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

扩展文档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_FragColorgl_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 渲染管道中涉及多个坐标空间的变换,这些变换发生在不同的管道阶段:

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

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

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

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

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

透视投影 vs 正交投影

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

透视投影(Perspective Projection)

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

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 通常在光栅化阶段就丢弃裁剪区域外的片段
  • 这些片段不会进入片段着色器,不产生浪费

2. Early Fragment Test

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

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

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

WebGL 中的 Target 是什么?

Target(目标/绑定点)是 WebGL 状态机中的全局绑定点,用于指定后续操作作用于哪个资源对象。调用 gl.bind*() 时,会将资源对象绑定到指定的 Target,之后对该 Target 的配置操作都会作用于这个对象。

Target 的用途:某些 API 执行时会从 Target 读取当前绑定的对象,常见的有:

  • 配置资源对象bufferDatatexImage2DtexParameteri 等从对应 Target 读取要配置的对象
  • 绘制操作drawElementsELEMENT_ARRAY_BUFFER 读取索引数据;drawArrays / drawElementsFRAMEBUFFER 读取渲染目标
  • 数据读取readPixelsREAD_FRAMEBUFFER(WebGL2)或 FRAMEBUFFER(WebGL1)读取像素数据
  • 数据拷贝copyBufferSubDataCOPY_READ_BUFFERCOPY_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) 读取源。用于 readPixelsblitFramebuffer

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 不需要。

为什么需要精度限制?

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

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

    • lowp(低精度):寄存器占用少、计算单元简单、功耗低
    • highp(高精度):寄存器占用多、计算复杂、功耗高
  3. 资源节约:精度影响 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)

为什么需要齐次坐标?

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

// 有齐次坐标:所有变换统一为矩阵乘法
vec4 transformed = matrix * vec4(position, 1.0);  // 统一的矩阵运算
  1. 表示无穷远点/向量:w = 0 表示方向向量(没有平移)或无穷远点
scss 复制代码
(1, 0, 0, 0)  // X 轴方向的无穷远点
(0, 1, 0, 0)  // Y 轴方向的无穷远点
  1. 实现透视投影:通过修改 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 保存顶点属性配置状态(vertexAttribPointerenableVertexAttribArray、索引缓冲区绑定) 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)
  • 删除后的对象引用不应再使用,否则可能导致错误
相关推荐
帅的被人砍xxx2 小时前
【vue演练场安装 element-plus框架】
前端
麦麦大数据2 小时前
F051-vue+flask企业债务舆情风险预测分析系统
前端·vue.js·人工智能·flask·知识图谱·企业信息·债务分析
1024肥宅2 小时前
现代 JavaScript 特性:ES6+ 新特性深度解析与实践
前端·javascript·面试
速易达网络2 小时前
基于Java Servlet的用户登录系统设计与实现
java·前端·mvc
晨光32112 小时前
Day34 模块与包的导入
java·前端·python
BD_Marathon3 小时前
Vue3_关于CSS样式的导入方式
前端·css
苹果电脑的鑫鑫3 小时前
vue和react缩进规则的配置项如何配置
前端·vue.js·react.js
BD_Marathon3 小时前
Vue3_工程文件之间的关系
前端·javascript·vue.js
weibkreuz3 小时前
模块与组件、模块化与组件化的理解@3
开发语言·前端·javascript