WebGL 后期处理概述
后期处理(Post-processing)是在场景渲染完成后,对渲染结果进行图像处理的技术。通过后期处理可以实现各种视觉效果,如模糊、辉光、色调映射、抗锯齿等。
后期处理的基本原理
后期处理的核心流程:
- 将场景渲染到纹理(离屏渲染)
- 对纹理进行各种图像处理
- 将处理后的结果渲染到屏幕
shell场景渲染 → 颜色纹理 → 后期处理着色器 → 屏幕 ↓ 深度纹理(可选)
基本后期处理框架
创建后期处理所需的资源
javascriptclass PostProcess { constructor(gl, width, height) { this.gl = gl; this.width = width; this.height = height; // 创建帧缓冲区 this.framebuffer = gl.createFramebuffer(); // 创建颜色纹理 this.colorTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.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); // 绑定到帧缓冲区 gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.colorTexture, 0); // 创建深度渲染缓冲区 this.depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthBuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 创建全屏四边形 this.quad = this.createFullscreenQuad(); } createFullscreenQuad() { // 返回 VAO 或顶点数据 const vertices = new Float32Array([ // 位置 // 纹理坐标 -1, 1, 0, 1, -1, -1, 0, 0, 1, 1, 1, 1, 1, -1, 1, 0 ]); // 创建 VBO 等... return { vertices, vbo: this.gl.createBuffer() }; } beginScene() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer); this.gl.viewport(0, 0, this.width, this.height); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); } endScene() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); } apply(effectProgram) { this.gl.useProgram(effectProgram); this.gl.activeTexture(this.gl.TEXTURE0); this.gl.bindTexture(this.gl.TEXTURE_2D, this.colorTexture); this.gl.uniform1i(this.gl.getUniformLocation(effectProgram, 'u_texture'), 0); // 绘制全屏四边形 this.drawQuad(); } drawQuad() { // 绑定 VAO 并绘制 this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); } }
后期处理顶点着色器
glslattribute 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; }
常用后期处理效果
1. 灰度效果(Grayscale)
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { vec4 color = texture2D(u_texture, v_texCoord); // 加权灰度转换 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor = vec4(vec3(gray), color.a); }
2. 模糊效果(Blur)
高斯模糊
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; uniform vec2 u_texelSize; // 1.0 / textureSize uniform vec2 u_direction; // 模糊方向 (1,0) 或 (0,1) void main() { vec4 color = vec4(0.0); // 5x5 高斯核 float weights[5]; weights[0] = 0.227027; weights[1] = 0.1945946; weights[2] = 0.1216216; weights[3] = 0.054054; weights[4] = 0.016216; // 中心像素 color += texture2D(u_texture, v_texCoord) * weights[0]; // 两侧像素 for (int i = 1; i < 5; i++) { vec2 offset = u_direction * u_texelSize * float(i); color += texture2D(u_texture, v_texCoord + offset) * weights[i]; color += texture2D(u_texture, v_texCoord - offset) * weights[i]; } gl_FragColor = color; }
双通道模糊(优化性能)
javascript// 先水平模糊 horizontalBlurProgram.setUniform('u_direction', [1, 0]); postProcess.apply(horizontalBlurProgram); // 再垂直模糊 verticalBlurProgram.setUniform('u_direction', [0, 1]); postProcess.apply(verticalBlurProgram);
3. 边缘检测(Edge Detection)
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; 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; vec2 offsets[9]; offsets[0] = vec2(-1, -1); offsets[1] = vec2(0, -1); offsets[2] = vec2(1, -1); offsets[3] = vec2(-1, 0); offsets[4] = vec2(0, 0); offsets[5] = vec2(1, 0); offsets[6] = vec2(-1, 1); offsets[7] = vec2(0, 1); offsets[8] = vec2(1, 1); float edgeX = 0.0; float edgeY = 0.0; for (int i = 0; i < 9; i++) { vec2 coord = v_texCoord + offsets[i] * u_texelSize; float gray = dot(texture2D(u_texture, coord).rgb, vec3(0.299, 0.587, 0.114)); edgeX += gray * kernelX[i]; edgeY += gray * kernelY[i]; } float edge = sqrt(edgeX * edgeX + edgeY * edgeY); gl_FragColor = vec4(vec3(edge), 1.0); }
4. 辉光效果(Bloom)
glsl// 提取高亮部分 precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; uniform float u_threshold; void main() { vec4 color = texture2D(u_texture, v_texCoord); float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); if (brightness > u_threshold) { gl_FragColor = color; } else { gl_FragColor = vec4(0.0); } } // 合成辉光 precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_sceneTexture; uniform sampler2D u_bloomTexture; uniform float u_bloomIntensity; void main() { vec4 sceneColor = texture2D(u_sceneTexture, v_texCoord); vec4 bloomColor = texture2D(u_bloomTexture, v_texCoord); gl_FragColor = sceneColor + bloomColor * u_bloomIntensity; }
5. 色调映射(Tone Mapping)
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; uniform float u_exposure; // Reinhard 色调映射 vec3 reinhardToneMapping(vec3 color) { return color / (color + vec3(1.0)); } // ACES 色调映射 vec3 acesToneMapping(vec3 color) { const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14; return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0); } void main() { vec4 color = texture2D(u_texture, v_texCoord); // 曝光调整 vec3 mapped = vec3(1.0) - exp(-color.rgb * u_exposure); // Gamma 校正 mapped = pow(mapped, vec3(1.0 / 2.2)); gl_FragColor = vec4(mapped, color.a); }
6. 色彩调整
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; uniform float u_brightness; uniform float u_contrast; uniform float u_saturation; void main() { vec4 color = texture2D(u_texture, v_texCoord); // 亮度 color.rgb += u_brightness; // 对比度 color.rgb = (color.rgb - 0.5) * u_contrast + 0.5; // 饱和度 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); color.rgb = mix(vec3(gray), color.rgb, u_saturation); gl_FragColor = color; }
7. 屏幕空间环境光遮蔽(SSAO)
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_colorTexture; uniform sampler2D u_depthTexture; uniform sampler2D u_normalTexture; uniform mat4 u_projectionMatrix; uniform mat4 u_viewMatrix; uniform vec2 u_texelSize; uniform vec3 u_samples[64]; // 采样核 vec3 getViewPosition(vec2 texCoord) { float depth = texture2D(u_depthTexture, texCoord).r; vec4 clipPos = vec4(texCoord * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); vec4 viewPos = inverse(u_projectionMatrix) * clipPos; return viewPos.xyz / viewPos.w; } void main() { vec3 viewPos = getViewPosition(v_texCoord); vec3 normal = texture2D(u_normalTexture, v_texCoord).xyz * 2.0 - 1.0; float occlusion = 0.0; float radius = 0.5; for (int i = 0; i < 64; i++) { // 采样点 vec3 samplePos = viewPos + u_samples[i] * radius; // 投影到屏幕空间 vec4 offset = u_projectionMatrix * vec4(samplePos, 1.0); offset.xyz = offset.xyz / offset.w * 0.5 + 0.5; // 获取采样点深度 float sampleDepth = getViewPosition(offset.xy).z; // 范围检查 float rangeCheck = smoothstep(0.0, 1.0, radius / abs(viewPos.z - sampleDepth)); // 如果采样点深度小于当前点深度,则遮挡 occlusion += (sampleDepth >= samplePos.z ? 1.0 : 0.0) * rangeCheck; } occlusion = 1.0 - (occlusion / 64.0); vec3 color = texture2D(u_colorTexture, v_texCoord).rgb; gl_FragColor = vec4(color * occlusion, 1.0); }
多效果链式处理
javascriptclass PostProcessChain { constructor(gl, width, height) { this.gl = gl; // 创建两个帧缓冲区用于乒乓渲染 this.fbo1 = new PostProcess(gl, width, height); this.fbo2 = new PostProcess(gl, width, height); this.effects = []; } addEffect(effect) { this.effects.push(effect); } render(sceneRenderFunc) { // 第一步:渲染场景到 FBO1 this.fbo1.beginScene(); sceneRenderFunc(); this.fbo1.endScene(); let readFBO = this.fbo1; let writeFBO = this.fbo2; // 应用每个效果 for (let effect of this.effects) { writeFBO.beginScene(); effect.apply(readFBO.colorTexture); writeFBO.endScene(); // 交换读写缓冲区 [readFBO, writeFBO] = [writeFBO, readFBO]; } // 最后渲染到屏幕 this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); finalPass.apply(readFBO.colorTexture); } }
性能优化建议
-
降低分辨率:
- 后期处理可以使用半分辨率或四分之一分辨率
- 特别适合模糊等效果
-
合并效果:
- 将多个简单效果合并到一个着色器中
- 减少渲染通道
-
使用 Mipmap:
- 模糊效果可以使用 mipmap 快速降采样
-
智能更新:
- 静态场景不需要每帧都进行后期处理
- 可以隔帧更新某些效果
-
移动端优化:
- 减少采样次数
- 使用简单的近似算法