WebGL 帧缓冲区概述
帧缓冲区(Framebuffer)是 WebGL 中用于离屏渲染的目标。与默认的屏幕缓冲区不同,帧缓冲区可以将渲染结果输出到纹理或渲染缓冲区,而不是直接显示在屏幕上。
帧缓冲区的组成
一个完整的帧缓冲区(FBO)可以包含以下附件:
- 颜色附件(Color Attachment):存储颜色信息,可以是纹理或渲染缓冲区
- 深度附件(Depth Attachment):存储深度值
- 模板附件(Stencil Attachment):存储模板值
shell┌─────────────────────────────────────┐ │ Framebuffer │ ├─────────────────────────────────────┤ │ Color Attachment │ Texture/ │ │ │ Renderbuffer │ ├─────────────────────────────────────┤ │ Depth Attachment │ Renderbuffer │ ├─────────────────────────────────────┤ │ Stencil Attachment │ Renderbuffer │ └─────────────────────────────────────┘
创建帧缓冲区
基本创建流程
javascript// 1. 创建帧缓冲区 const framebuffer = gl.createFramebuffer(); // 2. 创建颜色附件(纹理) const colorTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, colorTexture); 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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 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); // 3. 创建深度附件(渲染缓冲区) const depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); // 4. 绑定帧缓冲区并附加附件 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0 ); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer ); // 5. 检查帧缓冲区完整性 const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error('Framebuffer incomplete:', status); } // 6. 解绑 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
离屏渲染
基本渲染流程
javascriptfunction renderToTexture() { // 1. 绑定帧缓冲区(离屏渲染) gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // 2. 设置视口 gl.viewport(0, 0, framebufferWidth, framebufferHeight); // 3. 清除缓冲区 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 4. 渲染场景 drawScene(); // 5. 解绑帧缓冲区(恢复屏幕渲染) gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 6. 恢复视口 gl.viewport(0, 0, canvas.width, canvas.height); // 7. 使用渲染结果(colorTexture 现在包含渲染结果) gl.bindTexture(gl.TEXTURE_2D, colorTexture); drawFullscreenQuad(); }
纹理 vs 渲染缓冲区
| 特性 | 纹理(Texture) | 渲染缓冲区(Renderbuffer) |
|---|---|---|
| 可读性 | 可以作为纹理采样 | 不能直接读取 |
| 性能 | 稍慢 | 更快(针对写入优化) |
| 用途 | 需要后续处理的颜色附件 | 深度/模板附件 |
| 灵活性 | 高 | 低 |
选择建议
- 颜色附件:通常使用纹理(需要后续采样)
- 深度/模板附件:通常使用渲染缓冲区(性能更好)
多重渲染目标(MRT - WebGL 2.0)
javascript// WebGL 2.0 支持同时渲染到多个颜色附件 const framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // 创建多个颜色附件 const textures = []; for (let i = 0; i < 4; i++) { 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.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0 ); textures.push(texture); } // 指定要绘制的附件 gl.drawBuffers([ gl.COLOR_ATTACHMENT0, // 输出到纹理 0 gl.COLOR_ATTACHMENT1, // 输出到纹理 1 gl.COLOR_ATTACHMENT2, // 输出到纹理 2 gl.COLOR_ATTACHMENT3 // 输出到纹理 3 ]);
glsl#version 300 es layout(location = 0) out vec4 gPosition; layout(location = 1) out vec4 gNormal; layout(location = 2) out vec4 gAlbedo; layout(location = 3) out vec4 gMaterial; void main() { gPosition = vec4(worldPos, 1.0); gNormal = vec4(normal, 0.0); gAlbedo = texture(u_diffuseMap, texCoord); gMaterial = vec4(roughness, metallic, ao, 1.0); }
常见应用场景
1. 后期处理(Post-processing)
javascript// 渲染场景到纹理 renderToTexture(); // 应用后期处理效果 applyBloomEffect(); applyToneMapping();
2. 阴影贴图(Shadow Mapping)
javascript// 第一步:从光源视角渲染深度 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer); renderSceneFromLightPerspective(); // 第二步:使用深度纹理渲染场景 gl.bindFramebuffer(gl.FRAMEBUFFER, null); useShadowMapTexture(); renderSceneWithShadows();
3. 反射/折射
javascript// 渲染环境到立方体贴图 for (let face = 0; face < 6; face++) { gl.bindFramebuffer(gl.FRAMEBUFFER, cubeFramebuffer[face]); setupCameraForFace(face); renderEnvironment(); } // 使用环境贴图 useCubeMapTexture(); renderReflectiveObject();
4. 延迟渲染(Deferred Rendering)
javascript// G-Buffer 渲染 gl.bindFramebuffer(gl.FRAMEBUFFER, gBuffer); gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]); renderGeometry(); // 光照计算 gl.bindFramebuffer(gl.FRAMEBUFFER, null); useGBufferTextures(); applyLighting();
帧缓冲区管理类
javascriptclass Framebuffer { constructor(gl, width, height, options = {}) { this.gl = gl; this.width = width; this.height = height; this.framebuffer = gl.createFramebuffer(); this.textures = {}; this.renderbuffers = {}; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); // 创建颜色附件 if (options.color !== false) { this.addColorAttachment(0, options.colorFormat || gl.RGBA); } // 创建深度附件 if (options.depth) { this.addDepthAttachment(options.depthFormat || gl.DEPTH_COMPONENT16); } // 创建模板附件 if (options.stencil) { this.addStencilAttachment(); } // 检查完整性 const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { throw new Error(`Framebuffer incomplete: ${status}`); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); } addColorAttachment(index, internalFormat) { const gl = this.gl; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 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); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + index, gl.TEXTURE_2D, texture, 0); this.textures[`color${index}`] = texture; return texture; } addDepthAttachment(internalFormat) { const gl = this.gl; const renderbuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, this.width, this.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); this.renderbuffers.depth = renderbuffer; return renderbuffer; } bind() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer); this.gl.viewport(0, 0, this.width, this.height); } unbind() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); } getTexture(name = 'color0') { return this.textures[name]; } resize(width, height) { this.width = width; this.height = height; // 重新创建纹理和渲染缓冲区... } destroy() { const gl = this.gl; gl.deleteFramebuffer(this.framebuffer); Object.values(this.textures).forEach(t => gl.deleteTexture(t)); Object.values(this.renderbuffers).forEach(r => gl.deleteRenderbuffer(r)); } } // 使用示例 const fbo = new Framebuffer(gl, 1024, 1024, { color: true, depth: true }); fbo.bind(); renderScene(); fbo.unbind(); // 使用渲染结果 const texture = fbo.getTexture('color0'); gl.bindTexture(gl.TEXTURE_2D, texture);
性能优化建议
- 复用帧缓冲区:避免频繁创建和销毁
- 使用适当尺寸:不要创建过大的帧缓冲区
- 共享深度缓冲区:多个帧缓冲区可以共享同一个深度缓冲区
- 延迟创建:只在需要时创建帧缓冲区
- 使用渲染缓冲区存储深度:比纹理更高效
常见问题
帧缓冲区不完整
javascriptconst status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); switch (status) { case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: console.error('附件不完整'); break; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: console.error('缺少附件'); break; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: console.error('附件尺寸不匹配'); break; case gl.FRAMEBUFFER_UNSUPPORTED: console.error('不支持的格式组合'); break; }
纹理尺寸限制
javascriptconst maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); const maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); console.log(`Max texture size: ${maxSize}`); console.log(`Max renderbuffer size: ${maxRenderbufferSize}`);