返回知识库

渲染

后处理技术

后处理技术 封面
WebGPU/WebGLShaderGLSL/WGSL后处理BloomDOFMotion Blur

直觉问题:为什么游戏中的光晕如此真实?

Q1: 为什么电影中强烈的阳光会产生光晕效果?GPU 如何模拟?

Q2: 为什么相机快速移动时会有拖影?游戏如何模拟这种效果?


核心概念白话讲

后处理流程

后处理是在场景渲染完成后对最终图像进行的处理。

graph LR
    A[场景渲染<br/>HDR 纹理] --> B[提取亮部<br/>Bloom阈值]
    B --> C[模糊处理<br/>高斯模糊]
    C --> D[混合叠加<br/>Bloom混合]
    D --> E[色调映射<br/>HDR→LDR]
    E --> F[最终输出<br/>显示]

高斯模糊

高斯分布

G(x,σ)=12πσex22σ2G(x, \sigma) = \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{x^2}{2\sigma^2}}

分离卷积:将 2D 卷积拆分为两次 1D 卷积

  • 水平 Pass:沿 X 轴模糊
  • 垂直 Pass:沿 Y 轴模糊
  • 性能提升:O(N²) → O(2N)

Bloom(光晕)

原理:提取场景亮部,模糊后叠加到原始图像

步骤

  1. 阈值提取brightness > threshold
  2. 多级模糊:降采样 + 高斯模糊
  3. 叠加混合final = original + blurred

TIP

  • Unreal Engine 使用 Kawase 模糊替代高斯模糊,性能更高

Vignette(暗角)

原理:模拟镜头边缘光照衰减

V(u,v)=1.0dist(center,uv)radiusintensityV(u, v) = 1.0 - \frac{\text{dist}(center, uv)}{\text{radius}}^{\text{intensity}}

Chromatic Aberration(色差)

原理:不同波长的光折射率不同,导致 RGB 分离

Cr=texture(uv+δrdirection)C_{r} = \text{texture}(uv + \delta_{r} \cdot \text{direction}) Cg=texture(uv)C_{g} = \text{texture}(uv) Cb=texture(uvδbdirection)C_{b} = \text{texture}(uv - \delta_{b} \cdot \text{direction})

SSAO(Screen-Space Ambient Occlusion)

原理:在屏幕空间采样周围像素,计算遮蔽比例

步骤

  1. 采样当前像素周围 N 个点
  2. 比较深度,计算遮挡比例
  3. 将 AO 值应用到环境光

HBAO(Horizon-Based AO):考虑地平线遮挡,效果更准确

DOF(Depth of Field,景深)

原理:模拟相机焦距,焦距范围内清晰,范围外模糊

参数

  • Focus Distance:焦点距离
  • Focus Range:清晰范围
  • Blur Strength:模糊强度
blur=zpixelzfocusfocusRange\text{blur} = \frac{|z_{\text{pixel}} - z_{\text{focus}}|}{\text{focusRange}}

Motion Blur(运动模糊)

原理:根据运动向量混合历史帧

Cfinal=(1α)Ccurrent+αCpreviousC_{final} = (1 - \alpha) \cdot C_{current} + \alpha \cdot C_{previous}

TAA(Temporal Anti-Aliasing):结合运动向量和历史帧重建,同时实现抗锯齿和运动模糊


原理与数学机制

高斯模糊卷积核

3×3 高斯核(σ=1):

116[121242121]\frac{1}{16} \begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix}

5×5 高斯核(σ=1.4):

1256[1474141626164726412674162616414741]\frac{1}{256} \begin{bmatrix} 1 & 4 & 7 & 4 & 1 \\ 4 & 16 & 26 & 16 & 4 \\ 7 & 26 & 41 & 26 & 7 \\ 4 & 16 & 26 & 16 & 4 \\ 1 & 4 & 7 & 4 & 1 \end{bmatrix}

Bloom 亮度计算

亮度公式

l=0.2126r+0.7152g+0.0722bl = 0.2126 \cdot r + 0.7152 \cdot g + 0.0722 \cdot b

阈值过滤

B(l)={lthreshold,l>threshold0,lthresholdB(l) = \begin{cases} l - \text{threshold}, & l > \text{threshold} \\ 0, & l \leq \text{threshold} \end{cases}

SSAO 遮蔽计算

AO=1Ni=1N{1zsamplezcenterradius,zsample<zcenter0,zsamplezcenterAO = \frac{1}{N} \sum_{i=1}^{N} \begin{cases} 1 - \frac{z_{\text{sample}} - z_{\text{center}}}{\text{radius}}, & z_{\text{sample}} < z_{\text{center}} \\ 0, & z_{\text{sample}} \geq z_{\text{center}} \end{cases}

DOF 模糊半径

r=zzfocusfocusRangemaxBlurr = \frac{|z - z_{\text{focus}}|}{\text{focusRange}} \cdot \text{maxBlur}

圆盘采样

Cblur=1Ni=1Ntexture(uv+rdisk(i))C_{blur} = \frac{1}{N} \sum_{i=1}^{N} \text{texture}(uv + r \cdot \text{disk}(i))

Motion Blur 速度向量

相机速度

vcamera=PcurrentPpreviousΔtv_{\text{camera}} = \frac{P_{\text{current}} - P_{\text{previous}}}{\Delta t}

世界空间速度

vworld=vcamera+vobjectv_{\text{world}} = v_{\text{camera}} + v_{\text{object}}

屏幕空间速度

vscreen=projectionvworldv_{\text{screen}} = \text{projection} \cdot v_{\text{world}}

GLSL vs WGSL 代码对照

高斯模糊实现

GLSL 版本

水平模糊

#version 300 es
precision highp float;

in vec2 v_texcoord;
out vec4 frag_color;

uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform vec2 u_direction;

void main() {
  vec2 texelSize = 1.0 / u_resolution;
  vec3 result = vec3(0.0);

  float weights[5] = float[](0.227027, 0.1945946, 0.120641, 0.0546702, 0.016216);
  result += texture(u_texture, v_texcoord).rgb * weights[0];

  for (int i = 1; i < 5; i++) {
    result += texture(u_texture, v_texcoord + u_direction * texelSize * float(i)).rgb * weights[i];
    result += texture(u_texture, v_texcoord - u_direction * texelSize * float(i)).rgb * weights[i];
  }

  frag_color = vec4(result, 1.0);
}

WGSL 版本

struct FragmentInput {
  @location(0) texcoord: vec2<f32>,
}

struct FragmentOutput {
  @location(0) color: vec4<f32>,
}

@group(0) @binding(0)
var texture_input: texture_2d<f32>;

@group(0) @binding(1)
var texture_sampler: sampler;

@group(0) @binding(2)
var<uniform> resolution: vec2<f32>;

@group(0) @binding(3)
var<uniform> direction: vec2<f32>;

@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
  var output: FragmentOutput;

  let texelSize = 1.0 / resolution;
  var result = vec3<f32>(0.0);

  let weights = array<f32, 5>(0.227027, 0.1945946, 0.120641, 0.0546702, 0.016216);
  result = result + textureSample(texture_input, texture_sampler, input.texcoord).rgb * weights[0];

  for (var i: i32 = 1; i < 5; i++) {
    result = result + textureSample(texture_input, texture_sampler, input.texcoord + direction * texelSize * f32(i)).rgb * weights[i];
    result = result + textureSample(texture_input, texture_sampler, input.texcoord - direction * texelSize * f32(i)).rgb * weights[i];
  }

  output.color = vec4<f32>(result, 1.0);
  return output;
}

差异点

  • WGSL 使用 array<f32, N> 定义数组
  • float(i) 需要显式转换为 f32(i)
  • WGSL 的 for 循环需要显式声明变量类型

Bloom 实现

GLSL 版本

亮度提取

#version 300 es
precision highp float;

in vec2 v_texcoord;
out vec4 frag_color;

uniform sampler2D u_texture;
uniform float u_threshold;

void main() {
  vec3 color = texture(u_texture, v_texcoord).rgb;
  float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722));

  if (brightness > u_threshold) {
    frag_color = vec4(color, 1.0);
  } else {
    frag_color = vec4(0.0, 0.0, 0.0, 1.0);
  }
}

Bloom 混合

#version 300 es
precision highp float;

in vec2 v_texcoord;
out vec4 frag_color;

uniform sampler2D u_scene_texture;
uniform sampler2D u_bloom_texture;
uniform float u_bloom_intensity;

void main() {
  vec3 scene = texture(u_scene_texture, v_texcoord).rgb;
  vec3 bloom = texture(u_bloom_texture, v_texcoord).rgb;

  vec3 final = scene + bloom * u_bloom_intensity;

  frag_color = vec4(final, 1.0);
}

WGSL 版本

亮度提取

struct FragmentInput {
  @location(0) texcoord: vec2<f32>,
}

struct FragmentOutput {
  @location(0) color: vec4<f32>,
}

@group(0) @binding(0)
var texture_input: texture_2d<f32>;

@group(0) @binding(1)
var texture_sampler: sampler;

@group(0) @binding(2)
var<uniform> threshold: f32;

@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
  var output: FragmentOutput;

  let color = textureSample(texture_input, texture_sampler, input.texcoord).rgb;
  let brightness = dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));

  if (brightness > threshold) {
    output.color = vec4<f32>(color, 1.0);
  } else {
    output.color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
  }

  return output;
}

Bloom 混合

struct FragmentInput {
  @location(0) texcoord: vec2<f32>,
}

struct FragmentOutput {
  @location(0) color: vec4<f32>,
}

@group(0) @binding(0)
var scene_texture: texture_2d<f32>;

@group(0) @binding(1)
var scene_sampler: sampler;

@group(0) @binding(2)
var bloom_texture: texture_2d<f32>;

@group(0) @binding(3)
var bloom_sampler: sampler;

@group(0) @binding(4)
var<uniform> bloom_intensity: f32;

@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
  var output: FragmentOutput;

  let scene = textureSample(scene_texture, scene_sampler, input.texcoord).rgb;
  let bloom = textureSample(bloom_texture, bloom_sampler, input.texcoord).rgb;

  let final = scene + bloom * bloom_intensity;

  output.color = vec4<f32>(final, 1.0);
  return output;
}

差异点

  • WGSL 的 if-else 语法与 GLSL 相同
  • vec3<f32>(...) 需要显式类型标注

常见误区与陷阱

  1. 高斯模糊采样数过高

    • 陷阱:使用 9×9 或更大卷积核导致性能暴跌
    • 解决:使用分离卷积或 Kawase 模糊
  2. Bloom 阈值过低

    • 陷阱:阈值 0.0 导致全屏 Bloom,性能消耗大
    • 解决:根据场景动态调整阈值(1.0-2.0)
  3. SSAO 采样模式重复

    • 陷阱:使用规则网格采样导致明显的条纹伪影
    • 解决:使用旋转噪声或哈希函数
  4. DOF 景深范围过大

    • 陷阱:焦距范围过小导致全屏模糊
    • 解决:根据相机距离动态调整焦距
  5. Motion Blur 速度向量未归一化

    • 陷阱:相机运动速度过快导致过度模糊
    • 解决:限制最大模糊半径
  6. Vignette 强度过高

    • 陷阱:暗角强度过大导致画面过暗
    • 解决:限制强度在 0.3-0.5 范围
  7. Chromatic Aberration 采样距离过大

    • 陷阱:RGB 分离距离过大导致画面失真
    • 解决:根据像素到中心的距离动态调整偏移
  8. 后处理顺序错误

    • 陷阱:在 Bloom 前进行色调映射,导致高光丢失
    • 解决:确保 Tone Mapping 是最后一步

延伸阅读与自测

权威资料

开源实现参考

自测题

  1. 思考题: 为什么高斯模糊可以分离为两次 1D 卷积?为什么性能提升如此显著?

  2. 对比题: Bloom 与 HDR 渲染的关系是什么?为什么 Tone Mapping 必须在 Bloom 之后?

  3. 实践题: 如何实现自适应 Bloom 阈值,根据场景平均亮度动态调整?

  4. 扩展题: SSAO 与 HBAO 在算法原理上的区别是什么?为什么 HBAO 效果更准确?

  5. 优化题: 如何优化后处理管线,减少纹理采样次数?是否可以使用 Compute Shader 加速?


参考资料获取时间: 2026-07-06,通过 web-search-prime_web_search_prime 工具检索。