乐闻世界logo
搜索文章和话题

What are framebuffers and offscreen rendering in WebGL?

3月6日 21:57

WebGL Framebuffer Overview

A framebuffer (FBO) in WebGL is a target for offscreen rendering. Unlike the default screen buffer, framebuffers can output rendering results to textures or renderbuffers instead of displaying directly on screen.

Framebuffer Components

A complete framebuffer can contain the following attachments:

  1. Color Attachment: Stores color information, can be a texture or renderbuffer
  2. Depth Attachment: Stores depth values
  3. Stencil Attachment: Stores stencil values
shell
┌─────────────────────────────────────┐ │ Framebuffer │ ├─────────────────────────────────────┤ │ Color Attachment │ Texture/ │ │ │ Renderbuffer │ ├─────────────────────────────────────┤ │ Depth Attachment │ Renderbuffer │ ├─────────────────────────────────────┤ │ Stencil Attachment │ Renderbuffer │ └─────────────────────────────────────┘

Creating a Framebuffer

Basic Creation Process

javascript
// 1. Create framebuffer const framebuffer = gl.createFramebuffer(); // 2. Create color attachment (texture) 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. Create depth attachment (renderbuffer) const depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); // 4. Bind framebuffer and attach attachments 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. Check framebuffer completeness const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error('Framebuffer incomplete:', status); } // 6. Unbind gl.bindFramebuffer(gl.FRAMEBUFFER, null);

Offscreen Rendering

Basic Rendering Process

javascript
function renderToTexture() { // 1. Bind framebuffer (offscreen rendering) gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // 2. Set viewport gl.viewport(0, 0, framebufferWidth, framebufferHeight); // 3. Clear buffers gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 4. Render scene drawScene(); // 5. Unbind framebuffer (restore screen rendering) gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 6. Restore viewport gl.viewport(0, 0, canvas.width, canvas.height); // 7. Use render result (colorTexture now contains the rendered result) gl.bindTexture(gl.TEXTURE_2D, colorTexture); drawFullscreenQuad(); }

Texture vs Renderbuffer

FeatureTextureRenderbuffer
ReadabilityCan be sampled as textureCannot be read directly
PerformanceSlightly slowerFaster (optimized for writing)
Use CaseColor attachments that need post-processingDepth/stencil attachments
FlexibilityHighLow

Selection Recommendations

  • Color attachments: Usually use textures (need subsequent sampling)
  • Depth/stencil attachments: Usually use renderbuffers (better performance)

Multiple Render Targets (MRT - WebGL 2.0)

javascript
// WebGL 2.0 supports rendering to multiple color attachments simultaneously const framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // Create multiple color attachments 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); } // Specify which attachments to draw to gl.drawBuffers([ gl.COLOR_ATTACHMENT0, // Output to texture 0 gl.COLOR_ATTACHMENT1, // Output to texture 1 gl.COLOR_ATTACHMENT2, // Output to texture 2 gl.COLOR_ATTACHMENT3 // Output to texture 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); }

Common Application Scenarios

1. Post-processing

javascript
// Render scene to texture renderToTexture(); // Apply post-processing effects applyBloomEffect(); applyToneMapping();

2. Shadow Mapping

javascript
// Step 1: Render depth from light's perspective gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer); renderSceneFromLightPerspective(); // Step 2: Render scene using depth texture gl.bindFramebuffer(gl.FRAMEBUFFER, null); useShadowMapTexture(); renderSceneWithShadows();

3. Reflection/Refraction

javascript
// Render environment to cubemap for (let face = 0; face < 6; face++) { gl.bindFramebuffer(gl.FRAMEBUFFER, cubeFramebuffer[face]); setupCameraForFace(face); renderEnvironment(); } // Use environment map useCubeMapTexture(); renderReflectiveObject();

4. Deferred Rendering

javascript
// G-Buffer rendering gl.bindFramebuffer(gl.FRAMEBUFFER, gBuffer); gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]); renderGeometry(); // Lighting calculation gl.bindFramebuffer(gl.FRAMEBUFFER, null); useGBufferTextures(); applyLighting();

Framebuffer Management Class

javascript
class 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); // Create color attachment if (options.color !== false) { this.addColorAttachment(0, options.colorFormat || gl.RGBA); } // Create depth attachment if (options.depth) { this.addDepthAttachment(options.depthFormat || gl.DEPTH_COMPONENT16); } // Create stencil attachment if (options.stencil) { this.addStencilAttachment(); } // Check completeness 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; // Recreate textures and renderbuffers... } 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)); } } // Usage example const fbo = new Framebuffer(gl, 1024, 1024, { color: true, depth: true }); fbo.bind(); renderScene(); fbo.unbind(); // Use render result const texture = fbo.getTexture('color0'); gl.bindTexture(gl.TEXTURE_2D, texture);

Performance Optimization Tips

  1. Reuse framebuffers: Avoid frequent creation and destruction
  2. Use appropriate sizes: Don't create oversized framebuffers
  3. Share depth buffers: Multiple framebuffers can share the same depth buffer
  4. Lazy creation: Create framebuffers only when needed
  5. Use renderbuffers for depth: More efficient than textures

Common Issues

Framebuffer Incomplete

javascript
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); switch (status) { case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: console.error('Incomplete attachment'); break; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: console.error('Missing attachment'); break; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: console.error('Attachment dimensions mismatch'); break; case gl.FRAMEBUFFER_UNSUPPORTED: console.error('Unsupported format combination'); break; }

Texture Size Limits

javascript
const 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}`);
标签:WebGL