第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 * intensityjavascript
// 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.08.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 练习题
基础练习
创建一个帧缓冲,实现灰度后处理效果
实现模糊效果(box blur)
将场景渲染到较小的纹理,然后放大显示(像素化效果)
进阶练习
实现 Bloom 效果
实现基本的阴影贴图
挑战练习
- 实现延迟渲染(G-Buffer + 光照 Pass)
下一章预告:在第9章中,我们将学习混合和深度测试,实现透明物体和正确的渲染顺序。
文档版本:v1.0
字数统计:约 13,000 字
代码示例:50+ 个
