Skip to content

第8章:帧缓冲与离屏渲染

8.1 章节概述

默认情况下,WebGL 渲染直接绘制到屏幕(Canvas)。但很多高级效果需要离屏渲染——先渲染到一个纹理,再对这个纹理进行处理。

本章将深入讲解:

  • 帧缓冲基础:什么是帧缓冲、渲染缓冲
  • 渲染到纹理:将场景渲染到纹理
  • 后处理效果:模糊、边缘检测、颜色校正
  • 多目标渲染:一次渲染输出多个纹理(WebGL 2.0)
  • 高级应用:阴影贴图、环境映射

8.2 帧缓冲基础

8.2.1 什么是帧缓冲?

帧缓冲的概念

帧缓冲(Framebuffer)是渲染目标的集合

默认帧缓冲(屏幕):
┌─────────────────────────────────────┐
│            帧缓冲                    │
│  ┌───────────────────────────────┐  │
│  │     颜色附件(Color)         │  │──→ Canvas 显示
│  │     存储像素颜色              │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │     深度附件(Depth)         │  │──→ 深度测试
│  │     存储深度值                │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │     模板附件(Stencil)       │  │──→ 模板测试
│  │     存储模板值                │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘


自定义帧缓冲:
┌─────────────────────────────────────┐
│            帧缓冲                    │
│  ┌───────────────────────────────┐  │
│  │     颜色附件 → 纹理           │  │──→ 可以作为下一步的输入
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │     深度附件 → 渲染缓冲       │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

8.2.2 附件类型

帧缓冲的附件

1. 颜色附件(Color Attachment)
──────────────────────────────────
存储片段着色器输出的颜色

可以是:
- 纹理(Texture):可以作为后续渲染的输入
- 渲染缓冲(Renderbuffer):更快但无法采样


2. 深度附件(Depth Attachment)
──────────────────────────────────
存储深度值,用于深度测试

gl.DEPTH_COMPONENT16  (16位深度)
gl.DEPTH_COMPONENT24  (24位深度, WebGL 2.0)
gl.DEPTH_COMPONENT32F (32位浮点, WebGL 2.0)


3. 模板附件(Stencil Attachment)
──────────────────────────────────
存储模板值,用于模板测试

gl.STENCIL_INDEX8


4. 深度模板组合附件
──────────────────────────────────
同时存储深度和模板

gl.DEPTH_STENCIL
gl.DEPTH24_STENCIL8 (WebGL 2.0)

8.3 创建帧缓冲

8.3.1 基本流程

帧缓冲创建流程

1. 创建帧缓冲对象
   gl.createFramebuffer()


2. 绑定帧缓冲
   gl.bindFramebuffer(gl.FRAMEBUFFER, fb)


3. 创建颜色附件(纹理或渲染缓冲)
   gl.createTexture() / gl.createRenderbuffer()


4. 将颜色附件绑定到帧缓冲
   gl.framebufferTexture2D(...) / gl.framebufferRenderbuffer(...)


5. 创建并绑定深度/模板附件
   同上


6. 检查帧缓冲完整性
   gl.checkFramebufferStatus(...)


7. 解绑,使用默认帧缓冲
   gl.bindFramebuffer(gl.FRAMEBUFFER, null)

8.3.2 创建渲染到纹理的帧缓冲

javascript
/**
 * 创建帧缓冲,渲染到纹理
 */
function createFramebuffer(gl, width, height) {
    // 1. 创建帧缓冲
    const framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    
    // 2. 创建颜色纹理
    const colorTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, colorTexture);
    gl.texImage2D(
        gl.TEXTURE_2D,
        0,                 // mipmap 级别
        gl.RGBA,           // 内部格式
        width, height,     // 尺寸
        0,                 // 边框(必须为 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. 将纹理附加到帧缓冲的颜色附件
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER,           // 目标
        gl.COLOR_ATTACHMENT0,     // 附件点
        gl.TEXTURE_2D,            // 纹理类型
        colorTexture,             // 纹理对象
        0                         // mipmap 级别
    );
    
    // 4. 创建深度渲染缓冲
    const depthBuffer = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
    gl.renderbufferStorage(
        gl.RENDERBUFFER,
        gl.DEPTH_COMPONENT16,     // 16 位深度
        width, height
    );
    
    // 5. 将深度缓冲附加到帧缓冲
    gl.framebufferRenderbuffer(
        gl.FRAMEBUFFER,
        gl.DEPTH_ATTACHMENT,
        gl.RENDERBUFFER,
        depthBuffer
    );
    
    // 6. 检查帧缓冲完整性
    const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
    if (status !== gl.FRAMEBUFFER_COMPLETE) {
        console.error('帧缓冲不完整:', getFramebufferStatusName(status));
    }
    
    // 7. 解绑
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.bindRenderbuffer(gl.RENDERBUFFER, null);
    
    return {
        framebuffer,
        colorTexture,
        depthBuffer,
        width,
        height
    };
}

function getFramebufferStatusName(status) {
    const names = {
        [WebGLRenderingContext.FRAMEBUFFER_COMPLETE]: 'COMPLETE',
        [WebGLRenderingContext.FRAMEBUFFER_INCOMPLETE_ATTACHMENT]: 'INCOMPLETE_ATTACHMENT',
        [WebGLRenderingContext.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT]: 'MISSING_ATTACHMENT',
        [WebGLRenderingContext.FRAMEBUFFER_INCOMPLETE_DIMENSIONS]: 'INCOMPLETE_DIMENSIONS',
        [WebGLRenderingContext.FRAMEBUFFER_UNSUPPORTED]: 'UNSUPPORTED'
    };
    return names[status] || `UNKNOWN (${status})`;
}

8.3.3 使用帧缓冲

javascript
// 创建帧缓冲
const fbo = createFramebuffer(gl, 512, 512);

function render() {
    // ========== Pass 1: 渲染到帧缓冲 ==========
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
    gl.viewport(0, 0, fbo.width, fbo.height);
    
    gl.clearColor(0.1, 0.1, 0.1, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    
    // 使用场景着色器
    gl.useProgram(sceneProgram);
    drawScene();
    
    // ========== Pass 2: 渲染到屏幕 ==========
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);  // 使用默认帧缓冲
    gl.viewport(0, 0, canvas.width, canvas.height);
    
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    // 使用后处理着色器,用前一步的纹理
    gl.useProgram(postProcessProgram);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, fbo.colorTexture);
    gl.uniform1i(textureLocation, 0);
    
    drawFullscreenQuad();
    
    requestAnimationFrame(render);
}

8.4 后处理效果

8.4.1 全屏四边形

后处理需要一个覆盖整个屏幕的四边形:

javascript
/**
 * 创建全屏四边形
 */
function createFullscreenQuad(gl) {
    // 顶点数据:位置 (x, y) + 纹理坐标 (u, v)
    const vertices = new Float32Array([
        // 位置      // 纹理坐标
        -1, -1,      0, 0,
         1, -1,      1, 0,
        -1,  1,      0, 1,
         1,  1,      1, 1
    ]);
    
    const vbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
    
    return {
        buffer: vbo,
        draw: (positionLoc, texCoordLoc) => {
            gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
            
            // 位置属性
            gl.enableVertexAttribArray(positionLoc);
            gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 16, 0);
            
            // 纹理坐标属性
            gl.enableVertexAttribArray(texCoordLoc);
            gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 16, 8);
            
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        }
    };
}

8.4.2 基础后处理着色器

glsl
// ========== 顶点着色器 ==========
attribute vec2 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;

void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
    v_texCoord = a_texCoord;
}


// ========== 片段着色器(直通)==========
precision mediump float;

varying vec2 v_texCoord;
uniform sampler2D u_texture;

void main() {
    gl_FragColor = texture2D(u_texture, v_texCoord);
}

8.4.3 常见后处理效果

灰度化

glsl
void main() {
    vec4 color = texture2D(u_texture, v_texCoord);
    
    // 方法1:平均值
    // float gray = (color.r + color.g + color.b) / 3.0;
    
    // 方法2:亮度加权(更符合人眼感知)
    float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    
    gl_FragColor = vec4(vec3(gray), color.a);
}

反色

glsl
void main() {
    vec4 color = texture2D(u_texture, v_texCoord);
    gl_FragColor = vec4(1.0 - color.rgb, color.a);
}

亮度/对比度调整

glsl
uniform float u_brightness;  // -1 到 1
uniform float u_contrast;    // 0 到 2

void main() {
    vec4 color = texture2D(u_texture, v_texCoord);
    
    // 亮度调整
    vec3 result = color.rgb + vec3(u_brightness);
    
    // 对比度调整
    result = (result - 0.5) * u_contrast + 0.5;
    
    gl_FragColor = vec4(clamp(result, 0.0, 1.0), color.a);
}

模糊(Box Blur)

glsl
uniform vec2 u_texelSize;  // 1.0 / 纹理尺寸

void main() {
    vec4 sum = vec4(0.0);
    
    // 3x3 box blur
    for (int x = -1; x <= 1; x++) {
        for (int y = -1; y <= 1; y++) {
            vec2 offset = vec2(float(x), float(y)) * u_texelSize;
            sum += texture2D(u_texture, v_texCoord + offset);
        }
    }
    
    gl_FragColor = sum / 9.0;
}

高斯模糊

glsl
// 高斯模糊通常分两个 Pass:水平 + 垂直

// Pass 1: 水平模糊
uniform vec2 u_texelSize;
uniform float u_kernel[5];  // 高斯权重

void main() {
    vec4 sum = vec4(0.0);
    
    sum += texture2D(u_texture, v_texCoord + vec2(-2.0, 0.0) * u_texelSize) * u_kernel[0];
    sum += texture2D(u_texture, v_texCoord + vec2(-1.0, 0.0) * u_texelSize) * u_kernel[1];
    sum += texture2D(u_texture, v_texCoord) * u_kernel[2];
    sum += texture2D(u_texture, v_texCoord + vec2(1.0, 0.0) * u_texelSize) * u_kernel[3];
    sum += texture2D(u_texture, v_texCoord + vec2(2.0, 0.0) * u_texelSize) * u_kernel[4];
    
    gl_FragColor = sum;
}

// Pass 2: 垂直模糊(类似,改变偏移方向)

边缘检测(Sobel)

glsl
uniform vec2 u_texelSize;

void main() {
    // Sobel 算子
    float kernelX[9];
    kernelX[0] = -1.0; kernelX[1] = 0.0; kernelX[2] = 1.0;
    kernelX[3] = -2.0; kernelX[4] = 0.0; kernelX[5] = 2.0;
    kernelX[6] = -1.0; kernelX[7] = 0.0; kernelX[8] = 1.0;
    
    float kernelY[9];
    kernelY[0] = -1.0; kernelY[1] = -2.0; kernelY[2] = -1.0;
    kernelY[3] =  0.0; kernelY[4] =  0.0; kernelY[5] =  0.0;
    kernelY[6] =  1.0; kernelY[7] =  2.0; kernelY[8] =  1.0;
    
    float gx = 0.0;
    float gy = 0.0;
    
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            vec2 offset = vec2(float(i - 1), float(j - 1)) * u_texelSize;
            float sample = dot(texture2D(u_texture, v_texCoord + offset).rgb, vec3(0.299, 0.587, 0.114));
            
            gx += sample * kernelX[i * 3 + j];
            gy += sample * kernelY[i * 3 + j];
        }
    }
    
    float edge = sqrt(gx * gx + gy * gy);
    gl_FragColor = vec4(vec3(edge), 1.0);
}

Bloom 效果

Bloom 实现流程

1. 正常渲染场景


2. 提取高亮区域
   brightness > threshold


3. 对高亮区域进行模糊
   多次高斯模糊,或不同尺寸


4. 将模糊结果叠加到原图
   finalColor = original + bloom * intensity
javascript
// Bloom 实现
function renderBloom(gl, scene) {
    // Pass 1: 渲染场景到 HDR 纹理
    gl.bindFramebuffer(gl.FRAMEBUFFER, sceneFBO.framebuffer);
    drawScene(scene);
    
    // Pass 2: 提取高亮
    gl.bindFramebuffer(gl.FRAMEBUFFER, brightFBO.framebuffer);
    gl.useProgram(brightPassProgram);
    gl.uniform1f(thresholdLocation, 0.8);
    drawQuad(sceneFBO.colorTexture);
    
    // Pass 3: 多次模糊
    let inputFBO = brightFBO;
    for (let i = 0; i < blurPasses; i++) {
        // 水平模糊
        gl.bindFramebuffer(gl.FRAMEBUFFER, blurFBO1.framebuffer);
        gl.useProgram(hBlurProgram);
        drawQuad(inputFBO.colorTexture);
        
        // 垂直模糊
        gl.bindFramebuffer(gl.FRAMEBUFFER, blurFBO2.framebuffer);
        gl.useProgram(vBlurProgram);
        drawQuad(blurFBO1.colorTexture);
        
        inputFBO = blurFBO2;
    }
    
    // Pass 4: 合成
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.useProgram(compositeProgram);
    
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, sceneFBO.colorTexture);
    gl.uniform1i(originalLocation, 0);
    
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, blurFBO2.colorTexture);
    gl.uniform1i(bloomLocation, 1);
    
    gl.uniform1f(bloomIntensityLocation, 0.5);
    drawFullscreenQuad();
}

8.5 渲染深度纹理

8.5.1 创建深度纹理

javascript
/**
 * 创建深度纹理帧缓冲(用于阴影贴图)
 */
function createDepthFramebuffer(gl, width, height) {
    const framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    
    // 创建深度纹理
    const depthTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, depthTexture);
    
    // WebGL 1.0 需要扩展
    const ext = gl.getExtension('WEBGL_depth_texture');
    if (!ext) {
        console.error('不支持深度纹理');
        return null;
    }
    
    gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.DEPTH_COMPONENT,
        width, height,
        0,
        gl.DEPTH_COMPONENT,
        gl.UNSIGNED_INT,
        null
    );
    
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    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.DEPTH_ATTACHMENT,
        gl.TEXTURE_2D,
        depthTexture,
        0
    );
    
    // 不需要颜色附件,告诉 WebGL
    gl.drawBuffers([gl.NONE]);  // WebGL 2.0
    gl.readBuffer(gl.NONE);     // WebGL 2.0
    
    // 检查完整性
    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
        console.error('深度帧缓冲不完整');
    }
    
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    
    return { framebuffer, depthTexture, width, height };
}

8.5.2 可视化深度

glsl
// 可视化深度纹理
precision mediump float;

varying vec2 v_texCoord;
uniform sampler2D u_depthTexture;
uniform float u_near;
uniform float u_far;

// 将深度值线性化
float linearizeDepth(float depth) {
    float z = depth * 2.0 - 1.0;  // NDC
    return (2.0 * u_near * u_far) / (u_far + u_near - z * (u_far - u_near));
}

void main() {
    float depth = texture2D(u_depthTexture, v_texCoord).r;
    float linear = linearizeDepth(depth) / u_far;  // 归一化到 [0, 1]
    gl_FragColor = vec4(vec3(linear), 1.0);
}

8.6 多目标渲染(MRT)

WebGL 2.0 支持同时渲染到多个颜色附件:

8.6.1 创建 MRT 帧缓冲

javascript
/**
 * 创建多目标渲染帧缓冲
 */
function createMRTFramebuffer(gl, width, height, numTargets) {
    const framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    
    const textures = [];
    const attachments = [];
    
    for (let i = 0; i < numTargets; i++) {
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(
            gl.TEXTURE_2D, 0, gl.RGBA16F,  // 使用浮点格式
            width, height, 0,
            gl.RGBA, gl.HALF_FLOAT, null
        );
        
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        
        const attachment = gl.COLOR_ATTACHMENT0 + i;
        gl.framebufferTexture2D(
            gl.FRAMEBUFFER,
            attachment,
            gl.TEXTURE_2D,
            texture,
            0
        );
        
        textures.push(texture);
        attachments.push(attachment);
    }
    
    // 指定渲染到哪些附件
    gl.drawBuffers(attachments);
    
    // 深度缓冲
    const depthBuffer = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, width, height);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
    
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    
    return { framebuffer, textures, depthBuffer, width, height };
}

8.6.2 MRT 着色器

glsl
#version 300 es
precision mediump float;

in vec3 v_worldPosition;
in vec3 v_normal;
in vec2 v_texCoord;

uniform sampler2D u_diffuseMap;

// 多个输出
layout(location = 0) out vec4 gPosition;
layout(location = 1) out vec4 gNormal;
layout(location = 2) out vec4 gAlbedo;

void main() {
    // 输出位置
    gPosition = vec4(v_worldPosition, 1.0);
    
    // 输出法线
    gNormal = vec4(normalize(v_normal), 1.0);
    
    // 输出漫反射颜色
    gAlbedo = texture(u_diffuseMap, v_texCoord);
}

8.7 阴影贴图实现

8.7.1 阴影贴图原理

阴影贴图原理

从光源视角渲染深度图
判断片段是否在阴影中

Step 1: 从光源视角渲染深度
──────────────────────────────────

         Light


     ┌─────┼─────┐
     │     │     │  深度缓冲
     │  A  │  B  │  记录到光源的距离
     └─────┼─────┘

    ───────●───────
          场景


Step 2: 从相机视角渲染时比较
──────────────────────────────────

对于每个片段 P:
1. 计算 P 在光源空间的坐标 (lightSpacePos)
2. 从深度贴图采样该位置的深度 (closestDepth)
3. 比较 P 到光源的实际距离 (currentDepth)
4. 如果 currentDepth > closestDepth,则在阴影中

           Light

             │ closestDepth(深度图中的深度)

        ─────●───── 被遮挡的表面

             │ currentDepth(实际深度)

        ─────●───── 被照亮的表面


shadow = currentDepth > closestDepth ? 1.0 : 0.0

8.7.2 阴影贴图实现

javascript
/**
 * 阴影贴图渲染器
 */
class ShadowMapRenderer {
    constructor(gl, width = 1024, height = 1024) {
        this.gl = gl;
        
        // 创建深度帧缓冲
        this.depthFBO = createDepthFramebuffer(gl, width, height);
        
        // 简单的深度着色器
        this.depthProgram = createProgram(gl, depthVertexShader, depthFragmentShader);
        
        // 光源视图投影矩阵
        this.lightViewMatrix = mat4.create();
        this.lightProjectionMatrix = mat4.create();
        this.lightSpaceMatrix = mat4.create();
    }
    
    setLight(position, target, near = 1, far = 100) {
        mat4.lookAt(this.lightViewMatrix, position, target, [0, 1, 0]);
        
        // 对于方向光使用正交投影
        mat4.ortho(this.lightProjectionMatrix, -20, 20, -20, 20, near, far);
        
        mat4.multiply(
            this.lightSpaceMatrix,
            this.lightProjectionMatrix,
            this.lightViewMatrix
        );
    }
    
    renderShadowMap(scene) {
        const gl = this.gl;
        
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.depthFBO.framebuffer);
        gl.viewport(0, 0, this.depthFBO.width, this.depthFBO.height);
        gl.clear(gl.DEPTH_BUFFER_BIT);
        
        gl.useProgram(this.depthProgram);
        gl.uniformMatrix4fv(
            gl.getUniformLocation(this.depthProgram, 'u_lightSpaceMatrix'),
            false,
            this.lightSpaceMatrix
        );
        
        // 渲染场景
        scene.forEach(obj => {
            gl.uniformMatrix4fv(
                gl.getUniformLocation(this.depthProgram, 'u_modelMatrix'),
                false,
                obj.modelMatrix
            );
            obj.draw(gl);
        });
        
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }
    
    getShadowMap() {
        return this.depthFBO.depthTexture;
    }
    
    getLightSpaceMatrix() {
        return this.lightSpaceMatrix;
    }
}

8.7.3 阴影采样着色器

glsl
// 带阴影的片段着色器
precision mediump float;

varying vec3 v_worldPosition;
varying vec3 v_normal;
varying vec4 v_lightSpacePosition;

uniform sampler2D u_shadowMap;
uniform vec3 u_lightPosition;
uniform vec3 u_cameraPosition;

// 计算阴影
float calculateShadow(vec4 lightSpacePos) {
    // 透视除法
    vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w;
    
    // 转换到 [0, 1] 范围
    projCoords = projCoords * 0.5 + 0.5;
    
    // 获取深度图中最近深度
    float closestDepth = texture2D(u_shadowMap, projCoords.xy).r;
    
    // 当前片段深度
    float currentDepth = projCoords.z;
    
    // 阴影偏移(防止自阴影)
    float bias = 0.005;
    
    // 比较
    float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
    
    // 超出阴影贴图范围,不在阴影中
    if (projCoords.z > 1.0) {
        shadow = 0.0;
    }
    
    return shadow;
}

// PCF(百分比渐近过滤)软阴影
float calculateShadowPCF(vec4 lightSpacePos) {
    vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w;
    projCoords = projCoords * 0.5 + 0.5;
    
    float currentDepth = projCoords.z;
    float bias = 0.005;
    
    float shadow = 0.0;
    vec2 texelSize = 1.0 / vec2(1024.0);  // 阴影贴图尺寸
    
    // 3x3 采样
    for (int x = -1; x <= 1; x++) {
        for (int y = -1; y <= 1; y++) {
            float pcfDepth = texture2D(u_shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
            shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
        }
    }
    shadow /= 9.0;
    
    return shadow;
}

void main() {
    // ... 光照计算
    
    float shadow = calculateShadowPCF(v_lightSpacePosition);
    
    // 阴影只影响漫反射和镜面反射
    vec3 lighting = ambient + (1.0 - shadow) * (diffuse + specular);
    
    gl_FragColor = vec4(lighting, 1.0);
}

8.8 帧缓冲管理器

javascript
/**
 * 帧缓冲管理器
 */
class FramebufferManager {
    constructor(gl) {
        this.gl = gl;
        this.framebuffers = new Map();
    }
    
    create(name, width, height, options = {}) {
        const gl = this.gl;
        
        const fbo = {
            framebuffer: gl.createFramebuffer(),
            colorTexture: null,
            depthBuffer: null,
            width,
            height
        };
        
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
        
        // 颜色附件
        if (options.color !== false) {
            fbo.colorTexture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, fbo.colorTexture);
            
            const internalFormat = options.hdr ? gl.RGBA16F : gl.RGBA;
            const format = gl.RGBA;
            const type = options.hdr ? gl.HALF_FLOAT : gl.UNSIGNED_BYTE;
            
            gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, null);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, options.filter || gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, options.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, gl.TEXTURE_2D, fbo.colorTexture, 0);
        }
        
        // 深度附件
        if (options.depth !== false) {
            if (options.depthTexture) {
                fbo.depthTexture = gl.createTexture();
                gl.bindTexture(gl.TEXTURE_2D, fbo.depthTexture);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
                gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, fbo.depthTexture, 0);
            } else {
                fbo.depthBuffer = gl.createRenderbuffer();
                gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.depthBuffer);
                gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, fbo.depthBuffer);
            }
        }
        
        // 检查完整性
        const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
        if (status !== gl.FRAMEBUFFER_COMPLETE) {
            console.error(`Framebuffer "${name}" incomplete:`, status);
        }
        
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        
        this.framebuffers.set(name, fbo);
        return fbo;
    }
    
    get(name) {
        return this.framebuffers.get(name);
    }
    
    bind(name) {
        const fbo = this.framebuffers.get(name);
        if (fbo) {
            this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fbo.framebuffer);
            this.gl.viewport(0, 0, fbo.width, fbo.height);
        } else {
            this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
        }
    }
    
    resize(name, width, height) {
        const fbo = this.framebuffers.get(name);
        if (!fbo) return;
        
        const gl = this.gl;
        fbo.width = width;
        fbo.height = height;
        
        if (fbo.colorTexture) {
            gl.bindTexture(gl.TEXTURE_2D, fbo.colorTexture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        }
        
        if (fbo.depthBuffer) {
            gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.depthBuffer);
            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
        }
    }
    
    dispose(name) {
        const fbo = this.framebuffers.get(name);
        if (!fbo) return;
        
        const gl = this.gl;
        gl.deleteFramebuffer(fbo.framebuffer);
        if (fbo.colorTexture) gl.deleteTexture(fbo.colorTexture);
        if (fbo.depthBuffer) gl.deleteRenderbuffer(fbo.depthBuffer);
        if (fbo.depthTexture) gl.deleteTexture(fbo.depthTexture);
        
        this.framebuffers.delete(name);
    }
}

// 使用
const fbManager = new FramebufferManager(gl);

fbManager.create('scene', 1024, 1024);
fbManager.create('blur', 512, 512, { depth: false });
fbManager.create('shadow', 2048, 2048, { color: false, depthTexture: true });

// 渲染
fbManager.bind('scene');
drawScene();

fbManager.bind('blur');
applyBlur(fbManager.get('scene').colorTexture);

fbManager.bind(null);  // 渲染到屏幕

8.9 本章小结

核心概念

概念说明
帧缓冲渲染目标的集合
渲染缓冲快速但不可采样的附件
渲染到纹理将结果存储为纹理供后续使用
后处理对渲染结果进行图像处理
MRT多目标渲染,一次输出多个纹理
阴影贴图从光源渲染深度,实现阴影

关键 API

API作用
createFramebuffer()创建帧缓冲
bindFramebuffer()绑定帧缓冲
framebufferTexture2D()将纹理附加到帧缓冲
framebufferRenderbuffer()将渲染缓冲附加
checkFramebufferStatus()检查完整性
drawBuffers()指定 MRT 附件

8.10 练习题

基础练习

  1. 创建一个帧缓冲,实现灰度后处理效果

  2. 实现模糊效果(box blur)

  3. 将场景渲染到较小的纹理,然后放大显示(像素化效果)

进阶练习

  1. 实现 Bloom 效果

  2. 实现基本的阴影贴图

挑战练习

  1. 实现延迟渲染(G-Buffer + 光照 Pass)

下一章预告:在第9章中,我们将学习混合和深度测试,实现透明物体和正确的渲染顺序。


文档版本:v1.0
字数统计:约 13,000 字
代码示例:50+ 个

如有转载或 CV 的请标注本站原文地址