WebGL 核心概念
渲染管线
WebGL 渲染管线(Rendering Pipeline)是将 3D 场景转换为 2D 图像的一系列处理步骤。理解渲染管线是学习 WebGL 的基础。
管线流程图
code
┌─────────────────────────────────────────────────────────┐
│ 应用程序阶段 │
│ JavaScript 代码、顶点数据、纹理数据 │
└────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 顶点着色器(可编程) │
│ • 顶点变换 │
│ • 法线变换 │
│ • 纹理坐标生成 │
│ • 光照计算 │
└────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 图元装配(固定功能) │
│ • 图元类型:点、线、三角形 │
│ • 裁剪 │
│ • 剔除 │
└────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 光栅化(固定功能) │
│ • 将图元转换为片段 │
│ • 插值顶点属性 │
└────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 片段着色器(可编程) │
│ • 计算片段颜色 │
│ • 纹理采样 │
│ • 逐片段光照 │
└────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 逐片段操作(固定功能) │
│ • 像素所属测试 │
│ • 裁剪测试 │
│ • 模板测试 │
│ • 深度测试 │
│ • 混合 │
│ • 抖动 │
└────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 帧缓冲 │
│ • 颜色缓冲 │
│ • 深度缓冲 │
│ • 模板缓冲 │
└─────────────────────────────────────────────────────────┘管线阶段详解
| 阶段 | 类型 | 输入 | 输出 | 主要任务 |
|---|---|---|---|---|
| 顶点着色器 | 可编程 | 顶点属性 | 变换后的顶点 | 坐标变换、属性传递 |
| 图元装配 | 固定 | 顶点 | 图元 | 组装图元、裁剪剔除 |
| 光栅化 | 固定 | 图元 | 片段 | 生成片段、属性插值 |
| 片段着色器 | 可编程 | 片段属性 | 颜色 | 颜色计算、纹理采样 |
| 逐片段操作 | 固定 | 片段颜色 | 像素 | 测试、混合 |
| 帧缓冲 | 存储 | 像素 | 图像 | 存储最终图像 |
可编程阶段
WebGL 提供两个可编程阶段:
| 阶段 | 着色器类型 | 主要任务 | GLSL 变量 |
|---|---|---|---|
| 顶点处理 | 顶点着色器 | 坐标变换、属性传递 | gl_Position, gl_PointSize |
| 片段处理 | 片段着色器 | 颜色计算、纹理采样 | gl_FragColor, gl_FragDepth |
顶点着色器
顶点着色器(Vertex Shader)对每个顶点执行一次,负责顶点的几何变换和属性计算
基本结构
glsl
// 顶点着色器
attribute vec3 a_Position; // 顶点位置
attribute vec3 a_Color; // 顶点颜色
uniform mat4 u_MVPMatrix; // 模型视图投影矩阵
varying vec3 v_Color; // 传递给片段着色器
void main() {
// 计算变换后的顶点位置
gl_Position = u_MVPMatrix * vec4(a_Position, 1.0);
// 传递颜色到片段着色器
v_Color = a_Color;
// 设置点的大小(仅用于点绘制模式)
gl_PointSize = 10.0;
}内置变量
| 变量 | 类型 | 说明 |
|---|---|---|
gl_Position | vec4 | 顶点在裁剪空间的位置(必须设置) |
gl_PointSize | float | 点的大小(像素),仅在 gl.POINTS 模式有效 |
gl_ClipVertex | vec4 | 用户裁剪平面坐标 |
顶点属性
顶点属性是每个顶点独有的数据:
javascript
// JavaScript 中设置顶点属性
const positions = new Float32Array([
// x, y, z
0.0, 0.5, 0.0, // 顶点 0
-0.5, -0.5, 0.0, // 顶点 1
0.5, -0.5, 0.0 // 顶点 2
]);
// 创建缓冲区
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
// 获取属性位置
const a_Position = gl.getAttribLocation(program, 'a_Position');
// 配置属性指针
gl.vertexAttribPointer(
a_Position, // 属性位置
3, // 每个顶点的分量数(x, y, z)
gl.FLOAT, // 数据类型
false, // 是否归一化
0, // 步长(0 表示紧密排列)
0 // 偏移量
);
// 启用属性
gl.enableVertexAttribArray(a_Position);实际应用场景
顶点动画
glsl
// 顶点波动动画
uniform float u_Time;
uniform float u_Amplitude;
void main() {
vec3 pos = a_Position;
// 正弦波动画
pos.y += sin(pos.x * 3.0 + u_Time) * u_Amplitude;
gl_Position = u_MVPMatrix * vec4(pos, 1.0);
}顶点位移贴图
glsl
// 根据纹理位移顶点
uniform sampler2D u_HeightMap;
uniform float u_HeightScale;
void main() {
// 采样高度图
float height = texture2D(u_HeightMap, a_TexCoord).r;
// 沿法线方向位移
vec3 pos = a_Position + a_Normal * height * u_HeightScale;
gl_Position = u_MVPMatrix * vec4(pos, 1.0);
}片段着色器
片段着色器(Fragment Shader)对每个片段(像素)执行一次,负责计算片段的最终颜色。
基本结构
glsl
// 片段着色器
precision mediump float; // 设置浮点精度
varying vec3 v_Color; // 从顶点着色器插值得到
uniform sampler2D u_Texture; // 纹理采样器
void main() {
// 计算片段颜色
gl_FragColor = vec4(v_Color, 1.0);
}精度限定符
WebGL 要求在片段着色器中声明浮点精度:
glsl
precision highp float; // 高精度(可选)
precision mediump float; // 中等精度(推荐)
precision lowp float; // 低精度| 精度 | 范围 | 精度 | 适用场景 |
|---|---|---|---|
highp | ±2^62 | 16位 | 位置计算、深度 |
mediump | ±2^14 | 10位 | 纹理坐标、颜色 |
lowp | ±2^8 | 8位 | 颜色、法线 |
内置变量
| 变量 | 类型 | 说明 |
|---|---|---|
gl_FragColor | vec4 | 片段的最终颜色(必须设置) |
gl_FragDepth | float | 片段的深度值 |
gl_FragCoord | vec4 | 片段在窗口坐标系中的位置 |
gl_FrontFacing | bool | 片段是否属于正面 |
实际应用场景
渐变背景
glsl
uniform vec2 u_Resolution;
void main() {
// 计算归一化坐标
vec2 uv = gl_FragCoord.xy / u_Resolution;
// 渐变颜色
vec3 color1 = vec3(0.1, 0.1, 0.3);
vec3 color2 = vec3(0.5, 0.3, 0.8);
vec3 color = mix(color1, color2, uv.y);
gl_FragColor = vec4(color, 1.0);
}圆形绘制
glsl
void main() {
// 计算到中心的距离
float dist = distance(gl_PointCoord, vec2(0.5));
// 圆形裁剪
if (dist > 0.5) discard;
// 平滑边缘
float alpha = 1.0 - smoothstep(0.4, 0.5, dist);
gl_FragColor = vec4(1.0, 0.0, 0.0, alpha);
}缓冲区对象
缓冲区对象(Buffer Object)用于存储顶点数据、索引数据等,是 WebGL 中数据传输的核心机制。
缓冲区类型
| 类型 | 常量 | 用途 |
|---|---|---|
| 顶点缓冲区 | gl.ARRAY_BUFFER | 存储顶点属性数据 |
| 索引缓冲区 | gl.ELEMENT_ARRAY_BUFFER | 存储顶点索引 |
创建和使用缓冲区
javascript
// 1. 创建缓冲区
const buffer = gl.createBuffer();
// 2. 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 3. 写入数据
const data = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// 4. 配置属性指针
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 5. 启用属性
gl.enableVertexAttribArray(a_Position);
// 6. 解绑缓冲区(可选)
gl.bindBuffer(gl.ARRAY_BUFFER, null);缓冲区使用提示
gl.bufferData() 的第三个参数指定数据使用模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
gl.STATIC_DRAW | 一次写入,多次绘制 | 静态模型 |
gl.DYNAMIC_DRAW | 多次写入,多次绘制 | 动态模型 |
gl.STREAM_DRAW | 一次写入,少量绘制 | 粒子系统 |
索引缓冲区
使用索引可以重用顶点,减少数据量:
javascript
// 顶点数据(4个顶点)
const vertices = new Float32Array([
-0.5, 0.5, // 顶点 0
-0.5, -0.5, // 顶点 1
0.5, -0.5, // 顶点 2
0.5, 0.5 // 顶点 3
]);
// 索引数据(绘制两个三角形)
const indices = new Uint16Array([
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
]);
// 创建顶点缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 创建索引缓冲区
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);着色器程序
着色器程序(Shader Program)是顶点着色器和片段着色器的链接组合。
创建流程
code
创建着色器 → 编译着色器 → 创建程序 → 链接着色器 → 使用程序完整示例
javascript
// 创建着色器
function createShader(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) {
return shader;
}
// 输出错误信息
console.error('着色器编译失败:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
// 创建程序
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) {
return program;
}
// 输出错误信息
console.error('程序链接失败:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
// 使用示例
const vertexShaderSource = `...`;
const fragmentShaderSource = `...`;
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
// 使用程序
gl.useProgram(program);变量类型
WebGL 着色器程序中的变量分为三类:
| 类型 | 存储限定符 | 说明 | 示例 |
|---|---|---|---|
| 属性变量 | attribute | 顶点属性,每个顶点不同 | 位置、颜色、纹理坐标 |
| 一致变量 | uniform | 全局变量,所有顶点/片段共享 | 变换矩阵、光照参数 |
| 可变变量 | varying | 从顶点着色器传递到片段着色器 | 插值的颜色、纹理坐标 |
glsl
// 顶点着色器
attribute vec3 a_Position; // 每个顶点不同
uniform mat4 u_ModelMatrix; // 所有顶点相同
varying vec3 v_Color; // 传递给片段着色器
// 片段着色器
varying vec3 v_Color; // 接收插值后的值
uniform sampler2D u_Texture; // 纹理采样器渲染缓冲
渲染缓冲(Renderbuffer)是用于离屏渲染的缓冲区,可以存储颜色、深度或模板数据。
帧缓冲对象
帧缓冲对象(Framebuffer Object, FBO)是一组缓冲区的容器,支持离屏渲染:
javascript
// 创建帧缓冲
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// 创建纹理作为颜色附件
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 将纹理附加到帧缓冲
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
// 创建渲染缓冲作为深度附件
const renderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
// 将渲染缓冲附加到帧缓冲
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);
// 检查帧缓冲状态
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
console.error('帧缓冲不完整');
}
// 渲染到帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.viewport(0, 0, width, height);
// ... 渲染操作 ...
// 恢复默认帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);帧缓冲应用场景
| 应用 | 说明 |
|---|---|
| 后处理 | 将场景渲染到纹理,再应用特效 |
| 阴影贴图 | 从光源视角渲染深度 |
| 延迟渲染 | 分离几何和光照阶段 |
| 镜面反射 | 渲染反射视图 |
深度测试
深度测试(Depth Test)用于确定片段的前后关系,确保正确的遮挡效果。
开启深度测试
javascript
// 开启深度测试
gl.enable(gl.DEPTH_TEST);
// 设置深度测试函数
gl.depthFunc(gl.LESS); // 默认值:小于时通过
// 清除深度缓冲
gl.clearDepth(1.0); // 默认值:1.0(最远)
gl.clear(gl.DEPTH_BUFFER_BIT);深度测试函数
| 函数 | 说明 |
|---|---|
gl.NEVER | 永不通过 |
gl.LESS | 小于时通过(默认) |
gl.EQUAL | 等于时通过 |
gl.LEQUAL | 小于等于时通过 |
gl.GREATER | 大于时通过 |
gl.NOTEQUAL | 不等于时通过 |
gl.GEQUAL | 大于等于时通过 |
gl.ALWAYS | 总是通过 |
深度冲突
当两个面距离过近时,会出现深度冲突(Z-fighting):
glsl
// 在顶点着色器中添加深度偏移
gl_Position = u_MVPMatrix * vec4(a_Position, 1.0);
gl_Position.z -= 0.001; // 微小偏移或者在 JavaScript 中设置多边形偏移:
javascript
gl.enable(gl.POLYGON_OFFSET_FILL);
gl.polygonOffset(1.0, 1.0); // 因子和单位混合模式
混合(Blending)用于实现半透明效果,将片段颜色与帧缓冲中的颜色混合。
开启混合
javascript
// 开启混合
gl.enable(gl.BLEND);
// 设置混合函数
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);混合方程
code
结果 = 源颜色 × 源因子 + 目标颜色 × 目标因子混合因子
| 因子 | 说明 |
|---|---|
gl.ZERO | 0 |
gl.ONE | 1 |
gl.SRC_COLOR | 源颜色 |
gl.ONE_MINUS_SRC_COLOR | 1 - 源颜色 |
gl.DST_COLOR | 目标颜色 |
gl.ONE_MINUS_DST_COLOR | 1 - 目标颜色 |
gl.SRC_ALPHA | 源透明度 |
gl.ONE_MINUS_SRC_ALPHA | 1 - 源透明度 |
gl.DST_ALPHA | 目标透明度 |
gl.ONE_MINUS_DST_ALPHA | 1 - 目标透明度 |
常见混合模式
javascript
// 标准透明度混合
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// 加法混合(发光效果)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
// 乘法混合(阴影效果)
gl.blendFunc(gl.DST_COLOR, gl.ZERO);
// 预乘透明度
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);裁剪与剔除
背面剔除
剔除不面向相机的三角形,提升性能:
javascript
// 开启剔除
gl.enable(gl.CULL_FACE);
// 设置剔除面(默认:BACK)
gl.cullFace(gl.BACK); // 剔除背面
gl.cullFace(gl.FRONT); // 剔除正面
gl.cullFace(gl.FRONT_AND_BACK); // 剔除两面
// 设置正面方向(默认:CCW)
gl.frontFace(gl.CCW); // 逆时针为正面
gl.frontFace(gl.CW); // 顺时针为正面裁剪测试
只渲染指定矩形区域:
javascript
// 开启裁剪测试
gl.enable(gl.SCISSOR_TEST);
// 设置裁剪区域(x, y, width, height)
gl.scissor(100, 100, 200, 200);
// 关闭裁剪测试
gl.disable(gl.SCISSOR_TEST);WebGL 状态机
WebGL 是一个状态机,理解状态管理对于高效开发至关重要。
状态类型
| 状态类型 | 说明 | 示例 |
|---|---|---|
| 全局状态 | 影响整个渲染管线 | 深度测试、混合模式 |
| 资源绑定状态 | 当前绑定的资源 | 缓冲区、纹理、帧缓冲 |
| 着色器状态 | 当前使用的程序 | uniform 变量 |
状态查询
javascript
// 查询当前状态
const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
const viewport = gl.getParameter(gl.VIEWPORT);
const clearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE);
// 查询能力
const depthTest = gl.isEnabled(gl.DEPTH_TEST);
const blend = gl.isEnabled(gl.BLEND);状态缓存最佳实践
javascript
class WebGLStateCache {
constructor(gl) {
this.gl = gl;
this.state = {
program: null,
textures: new Array(16).fill(null),
arrayBuffer: null,
elementArrayBuffer: null,
depthTest: false,
blend: false
};
}
useProgram(program) {
if (this.state.program !== program) {
this.gl.useProgram(program);
this.state.program = program;
}
}
bindTexture(unit, texture) {
if (this.state.textures[unit] !== texture) {
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.state.textures[unit] = texture;
}
}
}小结
核心概念总结
| 概念 | 作用 | 关键 API |
|---|---|---|
| 渲染管线 | 图形处理流程 | - |
| 顶点着色器 | 顶点变换 | gl_Position, gl_PointSize |
| 片段着色器 | 颜色计算 | gl_FragColor, 精度限定符 |
| 缓冲区对象 | 数据存储 | gl.createBuffer(), gl.bufferData() |
| 着色器程序 | 着色器链接 | gl.createProgram(), gl.linkProgram() |
| 深度测试 | 遮挡处理 | gl.enable(gl.DEPTH_TEST) |
| 混合模式 | 半透明效果 | gl.enable(gl.BLEND) |
学习建议
- 理解管线:掌握渲染管线的每个阶段
- 实践着色器:多写 GLSL 代码,理解顶点和片段着色器的协作
- 调试技巧:学会使用浏览器开发工具和 WebGL 调试扩展
- 性能意识:了解状态切换、缓冲区使用对性能的影响
下一步
继续学习着色器语言,深入掌握 GLSL 编程。