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 的请标注本站原文地址