降噪

覆盖:空间滤波 · 时域抗锯齿 · 学习型降噪 · 离线渲染降噪
参考课程:GAMES202 实时高质量渲染


1. 噪声的本质与降噪分类

1.1 噪声来源

实时/离线渲染中的噪声本质上来自 Monte Carlo 积分的方差

很小(如实时渲染中 ),采样估计的方差 很大,表现为画面上的随机噪点。

降噪的本质:在保留高频信号(边缘、细节)的前提下,对渲染结果进行低通滤波,等效于增大等效采样数

1.2 降噪策略分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
降噪策略
├── 空间域滤波(单帧内)
│ ├── 普通模糊(高斯、Box)- 无边缘感知
│ ├── 双边滤波(Bilateral)- 颜色/位置感知
│ ├── À-Trous 小波变换 - 大半径高效滤波
│ └── SVGF - 方差引导 + 时空联合

├── 时域复用(多帧积累)
│ ├── TAA(Temporal Anti-Aliasing)
│ └── 历史帧重投影 + EMA 混合

├── 学习型降噪(数据驱动)
│ ├── OIDN - 预训练卷积网络(离线)
│ ├── NRD - 实时神经辐射缓存
│ └── 端到端神经降噪(研究方向)

└── 离线特化
├── OptiX AI Denoiser
├── Adaptive Sampling
└── 重要性采样 / 方差缩减

1.3 关键指标

指标 说明
PSNR(峰值信噪比) ,越高越好,通常 >30 dB 视为良好
SSIM(结构相似度) 感知质量度量,0~1,越接近 1 越好
时域稳定性 相邻帧间闪烁量,工程上常用帧差分方差衡量
延迟 滤波在帧时间中的占比,实时要求通常 <2 ms(4K/60fps)

2. 空间滤波

2.1 高斯模糊与双边滤波

高斯模糊(基线)

其中 为空间高斯核。

缺陷:对所有邻域像素一视同仁,导致边缘被模糊。

双边滤波(Bilateral Filter)

在空间核的基础上,增加值域核(Range Kernel),感知像素值的相似度:

直觉:只要你”离我近”“长得像我”,我才受你影响。颜色突变处 ,边缘自然保留。

Joint Bilateral Filter(联合双边滤波):值域核不使用被滤波图本身,而是使用辅助的 G-Buffer(法线、深度等):

线

工程注意:对于直接光照和间接光照应分离滤波,再合并,可避免高频直接光照过度平滑。


2.2 Edge-Avoiding À-Trous 小波变换

核心思想

À-Trous(法语”带孔”):在 5×5 卷积核的偏移中乘以步长 step,以 25 次固定采样覆盖更大半径,不同 Pass 中 step 指数增长:

Pass step 等效覆盖半径
0 1 2 px
1 2 4 px
2 4 8 px
3 8 16 px
4 16 32 px

5 个 Pass 共 125 次采样,等效于半径 32px 的暴力滤波(需 次采样),效率提升约 33×。

B3-Spline 卷积核

标准的 À-Trous 核使用 B3-Spline,是对高斯的近似:

1
2
3
4
5
1/256  1/64   3/128  1/64   1/256
1/64 1/16 3/32 1/16 1/64
3/128 3/32 9/64 3/32 3/128
1/64 1/16 3/32 1/16 1/64
1/256 1/64 3/128 1/64 1/256

所有权重之和 = 1.0(归一化)。

联合权重公式

线
  • :颜色敏感度,越小越保守(越容易判定为边缘)。
  • :法线敏感度,建议
  • :深度敏感度,需按深度范围归一化。

完整 GLSL 实现(单 Pass)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// À-Trous 单 Pass — 外部循环控制 step(1, 2, 4, 8, 16)
uniform int u_Step; // 当前 Pass 步长
uniform float u_SigmaC; // 颜色 Phi
uniform float u_SigmaN; // 法线 Phi
uniform float u_SigmaZ; // 深度 Phi

// B3-Spline 核权重(展平的 5x5)
const float kernel[25] = float[25](
1./256., 1./64., 3./128., 1./64., 1./256.,
1./64., 1./16., 3./32., 1./16., 1./64.,
3./128., 3./32., 9./64., 3./32., 3./128.,
1./64., 1./16., 3./32., 1./16., 1./64.,
1./256., 1./64., 3./128., 1./64., 1./256.
);

const ivec2 offset[25] = ivec2[25](
ivec2(-2,-2), ivec2(-1,-2), ivec2(0,-2), ivec2(1,-2), ivec2(2,-2),
ivec2(-2,-1), ivec2(-1,-1), ivec2(0,-1), ivec2(1,-1), ivec2(2,-1),
ivec2(-2, 0), ivec2(-1, 0), ivec2(0, 0), ivec2(1, 0), ivec2(2, 0),
ivec2(-2, 1), ivec2(-1, 1), ivec2(0, 1), ivec2(1, 1), ivec2(2, 1),
ivec2(-2, 2), ivec2(-1, 2), ivec2(0, 2), ivec2(1, 2), ivec2(2, 2)
);

void main()
{
ivec2 coord = ivec2(gl_FragCoord.xy);

vec4 c_center = texelFetch(u_Color, coord, 0);
vec3 n_center = texelFetch(u_Normal, coord, 0).xyz;
float z_center = texelFetch(u_Depth, coord, 0).r;
float dzdx = dFdx(z_center); // 深度梯度,用于斜面归一化
float dzdy = dFdy(z_center);

vec4 sum = vec4(0.0);
float cumW = 0.0;

for (int i = 0; i < 25; i++)
{
ivec2 q = coord + offset[i] * u_Step;

// 颜色权重
vec4 c_q = texelFetch(u_Color, q, 0);
float dc2 = dot(c_center - c_q, c_center - c_q);
float c_w = exp(-dc2 / u_SigmaC);

// 法线权重
vec3 n_q = texelFetch(u_Normal, q, 0).xyz;
float dn2 = max(dot(n_center - n_q, n_center - n_q), 0.0);
float n_w = exp(-dn2 / u_SigmaN);

// 深度权重(梯度感知,抑制斜面上的误判)
float z_q = texelFetch(u_Depth, q, 0).r;
float dz = abs(z_center - z_q);
float zVar = abs(dot(vec2(dzdx, dzdy), vec2(offset[i]) * float(u_Step))) + 1e-4;
float z_w = exp(-dz / zVar / u_SigmaZ);

float w = c_w * n_w * z_w * kernel[i];
sum += c_q * w;
cumW += w;
}

fragColor = sum / max(cumW, 1e-6);
}

工程要点

  • 深度权重使用梯度感知归一化(zVar),防止斜面(grazing angle)上的假边缘。
  • 多 Pass 之间 Ping-Pong FBO,上一 Pass 输出作为下一 Pass 的颜色输入。
  • u_SigmaC 应随画面亮度动态调整(HDR 场景中高亮区域方差更大)。

2.3 SVGF(Spatiotemporal Variance-Guided Filtering)

论文:Schied et al., Spatiotemporal Variance-Guided Filtering, HPG 2017

SVGF 是目前实时光追降噪的工业标准基线,它在 À-Trous 的基础上增加了两个关键模块:时域积累方差引导

完整管线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
每帧输入:1 SPP 路径追踪结果 + G-Buffer(法线/深度/材质ID)

┌─────────────────────────────────────────┐
│ Step 1: 时域积累(Temporal Accumulation)│
│ - 重投影:将历史帧像素对齐到当前帧 │
│ - 有效性检测:法线/深度/ID 一致性 │
│ - EMA 混合:color_acc = α·cur + (1-α)·history │
│ - 同步积累方差:var_acc │
└─────────────────┬───────────────────────┘

┌─────────────────────────────────────────┐
│ Step 2: 方差空间滤波(3×3 高斯) │
│ - 对积累方差 var_acc 做空间平滑 │
│ - 得到局部方差估计 var_filtered │
└─────────────────┬───────────────────────┘

┌─────────────────────────────────────────┐
│ Step 3: À-Trous 空间滤波(5 Pass) │
│ - 颜色边缘权重:σ_c 由 var_filtered 驱动│
│ c_w = exp(-|ΔL|² / (α·var + ε)) │
│ - 法线/深度权重同前 │
└─────────────────────────────────────────┘

最终降噪输出

方差引导的关键公式

SVGF 中,颜色权重的 不再是常数,而由方差动态控制:

  • :时空积累得到的局部方差(经过空间平滑后更稳定)。
  • :调节灵敏度的超参数(论文建议 )。
  • :防止除零的小量(如 )。

直觉:高方差区域(噪点多)允许更大的颜色差异被视为”同类”,从而加强平滑;低方差区域(已收敛)则收紧阈值,保留细节。

时域积累(Temporal Accumulation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 重投影:从当前像素世界坐标反推历史帧 UV
vec2 motionVec = texture(u_MotionVector, uv).xy;
vec2 historyUV = uv - motionVec; // 运动向量指向历史位置

// 有效性检测(任一失败则丢弃历史帧,alpha = 1)
bool valid = true;
valid = valid && all(greaterThanEqual(historyUV, vec2(0.0)));
valid = valid && all(lessThanEqual(historyUV, vec2(1.0)));
valid = valid && (abs(z_cur - z_hist) < depthThreshold); // 深度一致
valid = valid && (dot(n_cur, n_hist) > normalThreshold); // 法线一致(如 0.9)
valid = valid && (matID_cur == matID_hist); // 材质/物体 ID 一致

float alpha = valid ? 0.1 : 1.0; // 历史帧权重(0.1 = 10帧指数平均)

vec4 color_acc = mix(color_history, color_cur, alpha);
// 方差的时域估计(Welford 在线算法 or 一阶矩/二阶矩法)
float mu1_acc = mix(mu1_history, L_cur, alpha);
float mu2_acc = mix(mu2_history, L_cur * L_cur, alpha);
float var_acc = max(mu2_acc - mu1_acc * mu1_acc, 0.0);

常见问题alpha = 0.1 意味着 EMA 等效约 10 帧,过小(如 0.01)导致 ghosting,过大(如 0.5)导致时域噪点残留。实践中可根据运动向量长度动态调整。


3. 时域抗锯齿与历史帧复用

3.1 TAA(Temporal Anti-Aliasing)原理

TAA 本质是将多帧 jitter 采样在时间轴上积累,等效于超采样(MSAA/SSAA)但无额外的帧内开销。

Jitter 策略:每帧在亚像素范围内偏移投影矩阵:

1
2
3
4
5
// Halton 低差异序列(推荐,比随机 jitter 更均匀)
float jx = (halton(frameIndex, 2) - 0.5f) / screenWidth;
float jy = (halton(frameIndex, 3) - 0.5f) / screenHeight;
projMatrix[2][0] += jx * 2.0f;
projMatrix[2][1] += jy * 2.0f;

Halton 序列的前 8 个样本(基数 2 和 3):

0 0.500 0.333
1 0.250 0.667
2 0.750 0.111
3 0.125 0.444
4 0.625 0.778
5 0.375 0.222
6 0.875 0.556
7 0.062 0.889

3.2 历史帧混合与 Resolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TAA Resolve Pass
vec2 velocity = texture(u_Velocity, uv).xy;
vec4 cur = texture(u_Current, uv);
vec4 hist = texture(u_History, uv - velocity);

// --- Variance Clipping(方差裁剪,抑制 ghosting)---
// 在 3x3 邻域内计算当前帧的均值和方差,将历史帧裁剪到置信区间内
vec4 mu = vec4(0.0);
vec4 sigma = vec4(0.0);
for (int dx = -1; dx <= 1; dx++)
for (int dy = -1; dy <= 1; dy++) {
vec4 s = texture(u_Current, uv + vec2(dx, dy) * texelSize);
mu += s;
sigma += s * s;
}
mu /= 9.0;
sigma = sqrt(max(sigma / 9.0 - mu * mu, 0.0));

// 将历史帧裁剪到 [μ - γσ, μ + γσ]
float gamma = 1.0; // 建议 1.0~2.0
hist = clamp(hist, mu - gamma * sigma, mu + gamma * sigma);

// EMA 混合
fragColor = mix(hist, cur, 0.1);

3.3 Ghost / Disocclusion 问题与对策

问题 原因 对策
Ghosting(鬼影) 历史帧数据已过时但仍混入 Variance Clipping / Color AABB Clamping
Disocclusion(去遮挡) 物体移开后露出背景,历史帧无有效数据 深度/法线检测 + 快速重置 alpha
Aliased History(历史锯齿) 双线性插值历史帧引入新锯齿 Catmull-Rom 5-tap 重采样
Flickering(闪烁) Variance Clipping 过激 动态调整 gamma,运动向量越大 gamma 越大

Catmull-Rom 5-tap 历史帧采样(比双线性更锐利,无新锯齿):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对历史帧使用 Catmull-Rom 重采样(5 次采样近似)
vec4 sampleCatmullRom(sampler2D tex, vec2 uv, vec2 texSize)
{
vec2 pos = uv * texSize - 0.5;
vec2 f = fract(pos);
vec2 tc = floor(pos) + 0.5;

// B-样条权重
vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));
vec2 w3 = f * f * (-0.5 + 0.5 * f);

vec2 w12 = w1 + w2;
vec2 tc12 = (tc + w2 / w12) / texSize;
vec2 tc0 = (tc - 1.0) / texSize;
vec2 tc3 = (tc + 2.0) / texSize;

return (texture(tex, vec2(tc12.x, tc0.y )) * w12.x
+ texture(tex, vec2(tc0.x, tc12.y)) * w0.x
+ texture(tex, vec2(tc12.x, tc12.y)) * w12.x
+ texture(tex, vec2(tc3.x, tc12.y)) * w3.x
+ texture(tex, vec2(tc12.x, tc3.y )) * w12.x) * (1.0 / (w12.x + w0.x + w3.x));
}

4. 基于学习的降噪

4.1 OIDN(Intel Open Image Denoise)

OIDN 是基于预训练 CNN 的离线/半实时降噪库,可在 CPU/GPU 上运行。

网络架构:改进的 U-Net,输入为 HDR 颜色 + 辅助 G-Buffer(法线、反射率)。

1
2
3
4
5
6
输入通道(可选组合):
color (R, G, B) ← 带噪声的 HDR 渲染结果(必须)
albedo (R, G, B) ← 漫反射率(无光照),可选但显著提升质量
normal (X, Y, Z) ← 世界空间法线,可选

输出:降噪后的 HDR 颜色

C++ 集成示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <OpenImageDenoise/oidn.hpp>

oidn::DeviceRef device = oidn::newDevice(oidn::DeviceType::Default);
device.commit();

oidn::FilterRef filter = device.newFilter("RT"); // 实时光追预设

filter.setImage("color", colorBuffer, oidn::Format::Float3, width, height);
filter.setImage("albedo", albedoBuffer, oidn::Format::Float3, width, height);
filter.setImage("normal", normalBuffer, oidn::Format::Float3, width, height);
filter.setImage("output", outputBuffer, oidn::Format::Float3, width, height);
filter.set("hdr", true); // HDR 模式
filter.set("cleanAux", false); // 辅助缓冲是否也有噪声
filter.commit();

filter.execute(); // 执行降噪

// 错误处理
const char* errorMessage;
if (device.getError(errorMessage) != oidn::Error::None)
std::cerr << "OIDN Error: " << errorMessage << std::endl;

工程注意

  • 输入颜色必须是线性 HDR(Gamma 空间会导致颜色偏移)。
  • cleanAux = false 意味着法线/反射率也需降噪(推荐单独先 denoise auxiliary buffers)。
  • 首次 execute 较慢(JIT 编译),生产环境建议预热。

4.2 NRD(NVIDIA Real-time Denoise)

NRD 是针对实时渲染设计的降噪框架,核心思想是将噪声数据拆解为低方差的辐照度估计 + 高频的材质反射率,分别降噪后重组。

信号分解

降噪只作用于 通过 G-Buffer 精确获取,因此降噪任务的信号方差更低。

主要降噪器

降噪器 适用信号 特点
REBLUR 漫反射 GI / 镜面反射 时空自适应模糊,主力方案
RELAX 漫反射 GI(Lumen 使用) 基于 SVGF 扩展,对漫反射效果更好
SIGMA 阴影(硬/软) 专用阴影降噪,避免漏光
REFERENCE 调试用 无降噪,直接输出积累均值

REBLUR 核心思路(简化)

1
2
3
4
5
6
1. 时域积累:基于运动向量重投影,追踪每像素的"有效历史帧数" hitT
2. 自适应模糊半径:radius ∝ 1 / sqrt(hitT)
- 历史帧少(刚遮挡) → 大模糊,快速收敛
- 历史帧多(稳定) → 小模糊,保留细节
3. 空间模糊:在屏幕空间用法线/粗糙度感知的椭圆核进行模糊
4. 后处理:Anti-firefly(去火萤),稳定化

NRD SDK 集成要点(Vulkan/D3D12)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 初始化 NRD
nrd::DenoiserCreationDesc desc = {};
desc.requestedDenoiserDescs = {{ nrd::Denoiser::REBLUR_DIFFUSE_SPECULAR }};
nrd::CreateDenoiser(desc, m_Denoiser);

// 2. 每帧设置参数
nrd::CommonSettings commonSettings = {};
commonSettings.motionVectorScale[0] = 1.0f / screenWidth;
commonSettings.motionVectorScale[1] = 1.0f / screenHeight;
commonSettings.frameIndex = m_FrameIndex;
// ... 填写 viewToClip, worldToView 等矩阵

// 3. 设置各降噪器参数
nrd::ReblurSettings reblurSettings = {};
reblurSettings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3;

// 4. 执行(生成 Dispatch List,调用你的 RHI 执行)
nrd::DispatchDesc dispatches[16];
uint32_t dispatchCount;
nrd::GetComputeDispatches(m_Denoiser, &commonSettings, &reblurSettings, 1, dispatches, dispatchCount);
// 遍历 dispatches 并执行 Compute Shader

4.3 神经网络降噪原理

训练数据构成

输入 说明
Noisy Image(1~16 SPP) 带噪声的低采样渲染
Reference Image(4096 SPP) Ground Truth
G-Buffer(法线/反射率/深度) 结构辅助信息

损失函数

简单 L1/L2 损失会偏好模糊,感知质量差。工业实践常用:

推理加速技巧

  • FP16 量化:权重和激活使用 16-bit 浮点,速度 2×,质量损失极小。
  • TensorRT 优化:算子融合 + Kernel 自动调优,RTX GPU 上可达实时(<2 ms)。
  • 滑动窗口推理:对 4K 分辨率分 Tile 推理,控制显存占用。
  • 知识蒸馏:用大模型训练小模型,以 90% 的质量换取 5× 的速度。

5. 离线渲染降噪

5.1 Monte Carlo 噪声特性

离线渲染(电影/产品级)的 Monte Carlo 积分噪声具有以下特点:

  • 高动态范围(HDR):直接光 firefly 可达正常像素亮度的 倍以上。
  • 结构性噪声:噪声分布受 BSDF、光源分布影响,不是均匀白噪声。
  • 路径长度相关:间接光照噪声远大于直接光照。

5.2 方差缩减技术(Variance Reduction)

这些技术在采样阶段减少噪声,是降噪的前置优化。

重要性采样(Importance Sampling)

按被积函数形状分配采样密度:

实践中的应用

  • BRDF 采样:按 GGX/Beckmann lobe 形状采样
  • 光源采样(Next Event Estimation):直接采样光源,避免大量无贡献路径。
  • MIS(Multiple Importance Sampling):结合 BRDF 和光源采样,取长补短:

低差异序列(Quasi-Monte Carlo)

用 Halton、Sobol 等低差异序列替代随机采样,同等采样数下方差更低:

1
2
3
4
5
6
7
// Sobol 序列(维度 0 和 1)
float sobol2D(uint index, uint scramble0, uint scramble1) {
// ... Owen scrambling for better high-dimensional distribution
}

// 实践:每像素用不同 scramble,避免全局相关性
uint scramble = xxhash(pixelX ^ (pixelY << 16) ^ frameIndex);

俄罗斯轮盘(Russian Roulette)

以概率 终止路径,存活路径权重放大 ,期望无偏但减少无效长路径:

1
2
3
4
// 在每次反弹后
float q = min(throughput.maxComponent(), 0.95f); // 最大分量作为存活概率
if (rand() > q) break; // 终止
throughput /= q; // 放大权重,保持无偏

5.3 OptiX AI Denoiser

NVIDIA OptiX 内置的 AI 降噪器,用于离线渲染后处理。

特点

  • 训练于海量电影级渲染数据。
  • 支持 AOV(Arbitrary Output Variables):法线、反射率、深度单独输入。
  • 支持时域模式(需提供运动向量),大幅减少闪烁。
  • 支持 HDR 输入,火萤抑制能力强。

OptiX 7 集成示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 创建降噪器
OptixDenoiser denoiser;
OptixDenoiserOptions options = {};
options.guideAlbedo = 1; // 使用反射率辅助
options.guideNormal = 1; // 使用法线辅助
OPTIX_CHECK(optixDenoiserCreate(optixContext,
OPTIX_DENOISER_MODEL_KIND_HDR, &options, &denoiser));

// 配置
OptixDenoiserSizes sizes;
OPTIX_CHECK(optixDenoiserComputeMemoryResources(denoiser, width, height, &sizes));

// 分配状态/临时缓冲
CUdeviceptr d_state, d_scratch;
cudaMalloc((void**)&d_state, sizes.stateSizeInBytes);
cudaMalloc((void**)&d_scratch, sizes.withoutOverlapScratchSizeInBytes);

OPTIX_CHECK(optixDenoiserSetup(denoiser, stream,
width, height, d_state, sizes.stateSizeInBytes,
d_scratch, sizes.withoutOverlapScratchSizeInBytes));

// 每帧执行
OptixDenoiserLayer layer = {};
layer.input.data = d_noisyColor;
layer.input.format = OPTIX_PIXEL_FORMAT_FLOAT3;
layer.input.width = width;
layer.input.height = height;
layer.input.rowStrideInBytes = width * sizeof(float3);
layer.output = layer.input;
layer.output.data = d_outputColor;

OptixDenoiserGuideLayer guideLayer = {};
guideLayer.albedo.data = d_albedo;
guideLayer.normal.data = d_normal;
// ... 填写 format/width/height/rowStride

OPTIX_CHECK(optixDenoiserInvoke(denoiser, stream,
&denoiserParams, d_state, sizes.stateSizeInBytes,
&guideLayer, &layer, 1,
0, 0, // tile offset
d_scratch, sizes.withoutOverlapScratchSizeInBytes));

6. 横向对比与选型指南

算法特性对比

算法 类型 帧延迟 时域稳定性 边缘质量 工程难度 适用场景
Bilateral Filter 空间 极低 ✗(单帧) ★★★ 轻量后处理、原型验证
À-Trous(单帧) 空间 ★★★★ 无历史帧场景(如截图工具)
SVGF 时空 ★★★ ★★★★ 实时光追基线
TAA 时域 ★★★★ ★★★ 抗锯齿标配,配合其他降噪使用
NRD (REBLUR) 时空 ★★★★★ ★★★★★ AAA 游戏实时光追
OIDN 学习型 高(数十ms) ★★(需配合TAA) ★★★★★ 离线渲染、工具链
OptiX Denoiser 学习型 ★★★★ ★★★★★ 离线渲染(NVIDIA GPU)

选型决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
你的目标场景是?
├── 实时渲染(<16 ms 帧预算)
│ ├── 是否有光追 GI/阴影?
│ │ ├── 是 → NRD(工业标准)或 SVGF(自研基线)
│ │ └── 否 → TAA 通常已足够
│ └── 显存极限(如移动端)?
│ └── 是 → À-Trous 单帧(最轻量)

└── 离线渲染 / 工具链
├── NVIDIA GPU?
│ ├── 是 → OptiX AI Denoiser
│ └── 否 → OIDN(跨平台 CPU/GPU)
└── 需要自定义训练?
└── 是 → 基于 U-Net 构建,参考 KPCN/OIDN 论文

常见调参经验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SVGF:
alpha (EMA) = 0.1 # 历史帧权重,鬼影/噪点 trade-off
sigmaL = 4.0 # 方差引导系数
sigmaN = 128.0 # 法线敏感度(点积空间)
sigmaZ = 1.0 # 深度敏感度

TAA:
blendFactor = 0.1 # 每帧混合比例
varianceGamma = 1.0 # Variance Clipping 范围(1.0~2.0)
jitterScale = 1.0 # Jitter 幅度(Halton,亚像素范围)

À-Trous:
c_phi = 1.0~4.0 # 颜色敏感度(HDR 场景需更大)
n_phi = 0.1~0.5 # 法线敏感度
z_phi = 0.5~2.0 # 深度敏感度
passes = 4~5 # 更多 Pass = 更大半径但更易过模糊

7. 参考文献

核心论文

论文 会议/年份 关键贡献
Tomasi & Manduchi, Bilateral Filtering for Gray and Color Images ICCV 1998 双边滤波原型
Dammertz et al., Edge-Avoiding À-Trous Wavelet Transform for Fast Global Illumination Filtering HPG 2010 À-Trous 降噪
Schied et al., Spatiotemporal Variance-Guided Filtering HPG 2017 SVGF
Chaitanya et al., Interactive Reconstruction of Monte Carlo Image Sequences using a Recurrent Denoising Autoencoder SIGGRAPH 2017 循环神经降噪
Bako et al., Kernel-Predicting Convolutional Networks for Denoising Monte Carlo Renderings SIGGRAPH 2017 KPCN
Schied et al., Gradient Estimation for Real-Time Adaptive Temporal Filtering SIGGRAPH Asia 2018 SVGF 梯度扩展
Liu et al., NRD: NVIDIA Real-time Denoiser GDC 2021 NRD 工业实现

学习资源

⚡ 速览层

用于快速回忆核心概念。每个算法一张卡片,深入内容见对应章节。


🧠 一句话理解噪声

渲染噪声 = Monte Carlo 积分的方差。每像素采样数 越少,方差越大。
降噪的本质 = 在保留边缘的前提下做低通滤波,等效增大


📇 算法速查卡

🔷 Bilateral Filter(双边滤波)

本质 高斯模糊 × 值域相似度权重,相似才参与平均
公式核心
解决什么 普通模糊糊掉边缘的问题
局限 单帧,无时域信息;大核性能差
适用 轻量后处理、原型验证
深入 2.1 节

🔷 À-Trous 小波变换

本质 带孔卷积 = 固定 25 次采样,步长指数增大,覆盖大半径
关键数字 5 Pass × 25 采样 = 等效 32px 半径,效率约普通模糊的 33×
权重构成
解决什么 在 À-Trous 大半径滤波中保留几何/纹理边缘
局限 单帧;无法自适应方差;σ 参数需手动调
适用 无历史帧场景、SVGF 的空间滤波模块
深入 2.2 节

🔷 SVGF(时空方差引导滤波)

本质 À-Trous + 时域积累 + 方差自动驱动滤波强度
管线 时域积累 → 方差空间平滑 → À-Trous(5 Pass)
关键公式 $w_c = \exp!\left(-\frac{
解决什么 À-Trous 单帧噪点残留 + σ 参数难调问题
局限 高速运动场景历史帧失效;间接光时域不稳定
适用 实时光追降噪基线(自研首选)
深入 2.3 节

🔷 TAA(时域抗锯齿)

本质 每帧亚像素 Jitter,历史帧 EMA 积累 = 时域超采样
关键操作 重投影(运动向量)→ Variance Clipping → EMA 混合
Jitter 推荐 Halton(2,3) 序列,8 帧循环
解决什么 锯齿 + 低采样噪声,零帧内开销
局限 运动/遮挡边缘 ghosting;需要正确的运动向量
适用 几乎所有实时渲染管线的标配组件
深入 3 节

🔷 NRD / REBLUR(NVIDIA 实时降噪)

本质 信号分解(反射率 × 辐照度)+ 自适应半径时空模糊
核心思路 只对低频辐照度 降噪,反射率 从 G-Buffer 精确重建
自适应半径 :历史少→大模糊;历史多→小模糊
解决什么 SVGF 在镜面/光泽材质和高速运动场景的质量不足
局限 集成复杂(需 Vulkan/D3D12);调参成本高
适用 AAA 游戏实时光追(工业标准)
深入 4.2 节

🔷 OIDN(Intel 开放图像降噪)

本质 预训练 U-Net CNN,输入噪声图 + G-Buffer,直接输出干净图
输入 color(必须)+ albedo + normal(可选但推荐)
要求 输入必须是线性 HDR,非 Gamma 空间
解决什么 离线渲染场景的一键高质量降噪,无需手调参数
局限 延迟数十 ms,时域稳定性差(需配合 TAA)
适用 离线渲染工具链、截图/烘焙工具
深入 4.1 节

🔷 OptiX AI Denoiser

本质 NVIDIA GPU 专用预训练降噪器,集成于 OptiX 渲染框架
优势 支持 AOV 辅助 + 时域模式(运动向量)+ 强 firefly 抑制
限制 仅 NVIDIA GPU;需 CUDA/OptiX 环境
适用 电影/产品级离线渲染(NVIDIA 生态)
深入 5.3 节

🗺️ 30 秒选型

1
2
3
4
5
6
7
8
实时渲染?
有光追 GI → NRD(工业)或 SVGF(自研)
纯光栅化 → TAA 足够
移动/极限显存 → À-Trous 单帧

离线渲染?
NVIDIA GPU → OptiX Denoiser
跨平台 → OIDN

🔑 三个最重要的概念

  1. Joint Bilateral:用法线/深度辅助判断边缘,比单纯颜色双边滤波准确得多。G-Buffer 是降噪质量的天花板。
  2. 方差引导(SVGF 核心):噪点多的地方大力模糊,已收敛的地方轻触,σ 不是常数而是实时估计的方差。
  3. 信号分解(NRD 核心):,反射率高频从 G-Buffer 精确获取,只对低频辐照度做降噪,任务难度大幅降低。

⚠️ 工程常见坑

症状 解法
σ 过小 降噪不足,噪点残留 增大 c_phi,或开启方差引导
σ 过大 过模糊,边缘丢失 减小 c_phi,增加法线/深度权重
TAA ghosting 运动物体有拖影 检查运动向量精度;加强 Variance Clipping
历史帧闪烁 画面抖动 提高历史帧权重(降低 blend alpha)
HDR firefly 亮点爆炸 À-Trous 前做 clamp;或用 NRD Anti-firefly
OIDN 颜色偏移 降噪后色调偏暖/偏冷 确认输入为线性空间,非 sRGB
深度边缘误判 斜面上出现假边缘 À-Trous 深度权重加梯度感知归一化