WebGL 渲染管线的工作流程是什么?
WebGL 渲染管线是将 3D 顶点数据转化为屏幕像素的一系列处理阶段,分为应用程序阶段(CPU)和 GPU 管线阶段两大部分。其中 GPU 管线又包含可编程阶段和固定功能阶段,面试中常围绕"哪些阶段可编程、各阶段输入输出是什么"展开追问。
管线总览
shellCPU 应用程序阶段 │ 提交绘制命令、设置状态 ▼ 顶点着色器(可编程) ▼ 图元装配 + 裁剪(固定) ▼ 光栅化(固定) ▼ 片段着色器(可编程) ▼ 逐片段测试与混合(固定) ▼ 帧缓冲区
关键点:整条管线中只有顶点着色器和片段着色器是可编程的,其余阶段由 GPU 硬件固定执行。WebGL 2.0 新增了变换反馈(Transform Feedback),可以将顶点着色器的输出回收到缓冲区,实现 GPU 端的粒子计算等效果。
一、应用程序阶段(CPU 端)
这是开发者通过 JavaScript 控制的阶段,主要负责:
- 可见性判断:视锥体剔除、遮挡剔除,只提交可见物体给 GPU
- 准备几何数据:将顶点位置、法线、UV、颜色等属性写入缓冲区
- 设置渲染状态:绑定着色器程序、设置 uniform 变量、切换纹理
- 发起绘制调用:
gl.drawArrays()或gl.drawElements()
这一阶段的性能瓶颈通常在 draw call 数量,合并网格和使用实例化渲染(gl.drawArraysInstanced)是核心优化手段。
二、顶点着色器(可编程阶段)
顶点着色器对每个顶点执行一次,是管线的第一个可编程阶段。
输入:
- 顶点属性(
attribute):位置、法线、UV、颜色 - 全局变量(
uniform):变换矩阵、光照参数
核心处理 — MVP 矩阵变换:
glslattribute vec3 a_position; attribute vec2 a_texCoord; uniform mat4 u_model; // 模型矩阵:模型空间 → 世界空间 uniform mat4 u_view; // 视图矩阵:世界空间 → 观察空间 uniform mat4 u_projection; // 投影矩阵:观察空间 → 裁剪空间 varying vec2 v_texCoord; void main() { vec4 worldPos = u_model * vec4(a_position, 1.0); vec4 viewPos = u_view * worldPos; gl_Position = u_projection * viewPos; v_texCoord = a_texCoord; }
输出:裁剪空间坐标(Clip Space),gl_Position 的四个分量 (x, y, z, w) 中 w 用于后续透视除法。
面试追问:为什么用四维齐次坐标?——因为透视投影需要 w 分量来做透视除法,将裁剪空间转为 NDC;平移变换也需要齐次坐标才能用矩阵乘法表示。
三、图元装配与裁剪(固定阶段)
图元装配:将顶点按绘制模式(gl.TRIANGLES、gl.LINES、gl.POINTS)组装成图元。
裁剪:丢弃完全在视锥体外的图元,裁剪部分在视锥体内的图元(可能产生新顶点)。
透视除法:将裁剪坐标除以 w 分量,得到标准化设备坐标(NDC),x/y/z 范围均为 [-1, 1]。
视口变换:将 NDC 坐标映射到屏幕坐标,由 gl.viewport(x, y, width, height) 控制。
四、光栅化(固定阶段)
光栅化是将几何图元转换为片段(Fragment)的过程:
- 三角形遍历:检查哪些像素被三角形覆盖
- 插值计算:顶点属性(颜色、UV、法线)在片段间线性插值,透视校正插值由硬件自动完成
- 生成片段:每个被覆盖的像素生成一个片段,携带插值后的属性和深度值
片段不同于像素——片段是候选像素,还需要通过后续测试才能写入帧缓冲。
五、片段着色器(可编程阶段)
片段着色器对每个片段执行一次,是管线的第二个可编程阶段。
输入:插值后的顶点属性(varying)、纹理采样器(uniform sampler2D)
处理:纹理采样、光照计算、颜色混合
输出:最终颜色值,写入 gl_FragColor
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); }
六、逐片段操作(固定阶段)
片段着色器输出的颜色需要通过一系列测试才能写入帧缓冲:
| 操作 | 作用 |
|---|---|
| 模板测试 | 用模板缓冲区做掩码,限制绘制区域 |
| 深度测试 | 比较片段深度与深度缓冲区,丢弃被遮挡的片段 |
| 混合 | 将片段颜色与帧缓冲已有颜色按 alpha 值混合,实现半透明 |
| 抖动 | 用有限色深模拟更多颜色,减少色带 |
注意:深度测试默认关闭,需 gl.enable(gl.DEPTH_TEST) 开启。混合也需要 gl.enable(gl.BLEND) 并设置混合函数。
七、帧缓冲输出
通过所有测试的片段颜色被写入帧缓冲区。当一帧所有绘制完成后,前后缓冲区交换(双缓冲),画面显示到屏幕。
性能优化要点
- 减少 draw call:合并静态网格为一次绘制;使用实例化渲染(
gl.drawArraysInstanced)绘制大量相同几何体 - 优化顶点着色器:MVP 矩阵在 CPU 端预计算
projection * view * model,不要在着色器中逐顶点相乘 - 减少过度绘制:不透明物体从前到后绘制,利用深度测试提前丢弃被遮挡片段;半透明物体从后到前绘制
- 控制片段着色器复杂度:移动端 GPU 是 tile-based 架构,片段着色器是性能瓶颈,避免
discard、复杂分支和过多纹理采样 - 纹理压缩:使用 ASTC/ETC2 等压缩格式减少显存带宽
WebGL 1.0 与 2.0 管线差异
| 特性 | WebGL 1.0 | WebGL 2.0 |
|---|---|---|
| GLSL 版本 | 100 (GLSL ES 1.0) | 300 es (GLSL ES 3.0) |
| 变换反馈 | 不支持 | 支持,顶点着色器输出可回收到缓冲区 |
| 多重渲染目标 | 需扩展 | 原生支持 MRT |
| 3D 纹理 | 需扩展 | 原生支持 |
| 实例化渲染 | 需扩展 | 原生支持 |
| 顶点数组对象 | 需扩展 | 原生支持 VAO |
面试追问
Q: WebGL 管线中哪些阶段可编程? 顶点着色器和片段着色器。WebGL 2.0 新增变换反馈但不算独立阶段,几何着色器 WebGL 不支持。
Q: 为什么需要透视除法? 裁剪空间的齐次坐标 (x, y, z, w) 除以 w 后得到 NDC,使不同深度的物体正确投影到屏幕上。没有透视除法,远处的物体不会变小。
Q: WebGL 和 OpenGL 管线的主要区别? WebGL 基于 OpenGL ES,去掉了几何着色器、曲面细分等着色器;运行在浏览器沙箱中,通过 JavaScript API 调用;着色器编译由浏览器驱动完成,不同浏览器可能有性能差异。