第 7 节:WebGL 状态管理——减少冗余 GL 调用的艺术
WebGL 是一个状态机——任何 enable(BLEND)、bindBuffer(...)、useProgram(...) 都会修改全局状态。如果每次 Draw Call 都重新设置所有状态,性能会灾难性地下跌。
Three.js 的 WebGLState 模块就是来解决这个问题的:缓存当前状态,跳过"已经设置过"的冗余调用。
🏗️ WebGLState 模块结构
WebGLState
├── colorBuffer (颜色写入控制)
│ ├── setMask (是否写入 R/G/B/A)
│ ├── setClear (clearColor)
│ └── setLocked
├── depthBuffer (深度测试控制)
│ ├── setTest (DEPTH_TEST 开关)
│ ├── setMask (是否写入深度)
│ ├── setFunc (LESS/EQUAL/...)
│ └── setClear
├── stencilBuffer
│ └── ...
├── polygonOffset
├── blending
│ ├── setEquation
│ ├── setSrcFactor
│ └── setDstFactor
└── viewport / scissor
💾 缓存模式:State Cache Pattern
1. 核心思想
// 没有缓存的写法
function setBlend(enabled) {
if (enabled) gl.enable(gl.BLEND);
else gl.disable(gl.BLEND);
}
// 每次调用都会触发 GL 调用
// 有缓存的写法
let currentBlend = false;
function setBlend(enabled) {
if (currentBlend !== enabled) { // 只有真正变化才调用 GL
if (enabled) gl.enable(gl.BLEND);
else gl.disable(gl.BLEND);
currentBlend = enabled;
}
}
2. 完整示例:blendFunc
// 简化源码:src/renderers/webgl/WebGLState.js
class WebGLState {
constructor(gl) {
this.gl = gl;
this.currentBlendEquation = null;
this.currentBlendSrc = null;
this.currentBlendDst = null;
}
setBlending(equation, srcFactor, dstFactor) {
if (this.currentBlendEquation !== equation) {
this.gl.blendEquation(equation);
this.currentBlendEquation = equation;
}
if (this.currentBlendSrc !== srcFactor || this.currentBlendDst !== dstFactor) {
this.gl.blendFunc(srcFactor, dstFactor);
this.currentBlendSrc = srcFactor;
this.currentBlendDst = dstFactor;
}
}
}
效果:连续画 100 个相同透明设置的物体,只触发1次 gl.blendFunc 调用。
🔗 绑定缓存:Buffer / Texture
1. Buffer 绑定
// 简化源码
const currentBinding = { ARRAY_BUFFER: null, ELEMENT_ARRAY_BUFFER: null };
function bindBuffer(target, buffer) {
if (currentBinding[target] === buffer) return; // 跳过重复绑定
gl.bindBuffer(target, buffer);
currentBinding[target] = buffer;
}
2. 纹理绑定
类似地,WebGLTextures 内部维护一个"当前绑定到 N 号纹理单元的纹理"数组:
const currentTexture = {}; // key: unit, value: WebGLTexture
function bindTexture(unit, texture) {
if (currentTexture[unit] === texture) return;
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, texture);
currentTexture[unit] = texture;
}
📚 状态栈:State Stack
某些场景需要"保存当前状态,临时切到另一套状态,渲染完恢复"——比如多视口渲染:
// 伪代码
state.pushState();
// 修改 viewport / scissor / blend / depth ...
renderToSubViewport();
state.popState(); // 恢复
Three.js 的 WebGLState 通过对象快照实现:
pushState() {
this.stack.push({
blend: this.currentBlend,
depthTest: this.currentDepthTest,
depthMask: this.currentDepthMask,
viewport: this.currentViewport,
// ...
});
}
popState() {
const state = this.stack.pop();
// 把 state 重新"灌"回去(如果值变了,会自动触发 GL 调用)
}
🔄 reset():上下文丢失恢复
当 WebGL 上下文丢失(GPU 重启、Tab 切换、driver crash),所有状态都会被驱动重置:
function reset() {
// 把所有"当前缓存值"标记为 null
this.currentBlend = null;
this.currentDepthTest = null;
this.currentViewport = new Vector4();
// ...
// 下次设置任何状态时都会真正调用 GL
}
Three.js 监听 webglcontextlost / webglcontextrestored 事件,自动调用 reset()。
📊 性能影响实测
| 场景 | 无缓存 | 有缓存 | 差异 |
|---|---|---|---|
| 1000 个相同材质物体 | ~5000 GL 调用 | ~500 GL 调用 | 10x 提升 |
| 1000 个不同材质物体 | ~5000 GL 调用 | ~4500 GL 调用 | 1.1x 提升(无优化空间) |
| 100 个相同纹理物体 | ~300 GL 调用 | ~3 GL 调用 | 100x 提升 |
🎯 Mini 实现
class GLState {
constructor(gl) {
this.gl = gl;
this.cache = new Map();
}
set(key, glEnum, value) {
if (this.cache.get(key) === value) return; // 跳过
if (value) this.gl.enable(glEnum);
else this.gl.disable(glEnum);
this.cache.set(key, value);
}
// 用法
setDepthTest(enabled) {
this.set('depthTest', this.gl.DEPTH_TEST, enabled);
}
setBlend(enabled) {
this.set('blend', this.gl.BLEND, enabled);
}
setCullFace(enabled) {
this.set('cullFace', this.gl.CULL_FACE, enabled);
}
}
🆚 OGL 对比
OGL 的状态管理直接在 Renderer 内部:
// OGL: src/Renderer.js
class Renderer {
constructor(gl) {
this.gl = gl;
this.state = {}; // 直接用对象字面量做缓存
this.state.blend = false;
}
setBlend(blend) {
if (this.state.blend === blend) return;
this.state.blend = blend;
blend ? this.gl.enable(this.gl.BLEND) : this.gl.disable(this.gl.BLEND);
}
}
设计取舍:OGL 把状态缓存逻辑直接 inline 到 Renderer,没有独立模块。代码量小,但复用性差——如果你写第二个 Renderer,要重写一遍。
🎓 深度思考
状态缓存的"代价"是什么?
多了一次if判断 + 内存中的 cache。几乎没代价——收益却巨大。为什么不在 Shader 里用
discard来替代状态切换?
discard是片元级别的"主动放弃",而状态切换是全局生效。两者解决不同问题。状态切换更通用、消耗更低。Three.js 状态管理还有什么优化空间?
- 状态预排序:在 render() 之前按"使用相同 state"的对象分组,进一步减少切换
- State Bucket:把多个对象的状态需求合并,一次性提交