屏幕空间全局光照
Screen Space Global Illumination
基于开源项目 jiaozi158 / UnitySSGIURP 的完整技术笔记 · 涵盖光线步进、T-S-T 降噪管线与 URP Render Feature 架构
🌐项目总览§ 01
该项目为 Unity URP 实现了一套完整的 屏幕空间全局光照(SSGI) 系统, 展示了生产级的 Renderer Feature 编写规范,同时涵盖以下技术点:
- 光线步进算法:自适应步长 + 二分搜索 + 背面深度厚度判定
- T-S-T 降噪管线:时域重投影 → 空域双边滤波 → 时域稳定
- SVGF 原理应用:Spatiotemporal Variance-Guided Filtering
- Unity 6 Render Graph API:RenderGraph / RTHandle / RecordRenderGraph 编写模式
- MRT GBuffer 流程:Forward 下手动生成 GBuffer 的 MRT 方法
ScriptableRendererFeature(C#)和 10 个 Shader Pass(HLSL)构成,
C# 负责调度与资源管理,HLSL 负责具体 GPU 计算。
⚙️Renderer Feature 架构§ 02
ScreenSpaceGlobalIlluminationURP.cs 是整个系统的调度中枢,负责判断渲染路径、
注入 Pass、管理历史纹理、传递 Uniform 参数。
四大内部 Pass
| Pass 类名 | RenderPassEvent | 核心职责 |
|---|---|---|
| PreRenderSSGIPass | BeforeRenderingPrePasses | 修复 Scene View 中运动向量每帧未更新的编辑器 Bug,修正视图投影矩阵 |
| SSGIPass(核心) | BeforeRenderingTransparents | 调度全部 Pass 0-9:光线步进、时域累积、空域降噪、最终混合 |
| BackfaceDataPass | AfterRenderingOpaques | 以 CullMode.Front 渲染背面深度(可选颜色),为厚度判定提供数据 |
| ForwardGBufferPass (值得借鉴 MRT 写法) | AfterRenderingOpaques | Forward 渲染路径下,利用 MRT 手动生成 _GBuffer0/1/2;Deferred 路径跳过 |
ForwardGBufferPass 按需注入,
避免了 Deferred 下的重复渲染,做到了"按需生成"的渲染路径适配。
历史数据管理 CameraHistoryData
多相机场景下(Game View + Scene View + 反射相机),每个相机需独立维护自己的历史纹理,
否则会相互污染导致时域效果闪烁。项目使用数组 + GetHashCode 追踪相机身份:
// 历史纹理结构体 (每相机独立)
struct CameraHistoryData {
RTHandle historyDepthHandle; // 上一帧深度,用于遮挡剔除
RTHandle historyIndirectDiffuseHandle; // 上一帧 GI 结果
RTHandle accumulateHistorySampleHandle; // 累积采样帧数
Matrix4x4 prevCamInvVPMatrix; // 上一帧 VP 逆矩阵,用于反推世界坐标
}
// 相机哈希查找
int camID = camera.GetHashCode();
var histData = cameraHistoryData.FirstOrDefault(d => d.cameraId == camID);
分辨率与时域强度也会动态联动:分辨率越低,越依赖时域累积补充细节;分辨率高时适当降低权重,减少拖影。
float temporalIntensity = Mathf.Lerp(
denoiseIntensity + 0.02f, // 低分辨率:增强时域
denoiseIntensity - 0.04f, // 高分辨率:减弱拖影
resolutionScale
);
🎞️Shader Pass 0–9§ 03
所有 Pass 均运行于 BeforeRenderingTransparents(不透明物体后、透明物体前):
完整渲染流程
🔭光线步进算法§ 04
SSGI.hlsl 中的 RayMarching() 实现了一种
不依赖 Hi-Z 加速结构的纯线性步进算法,通过以下手段保证性能与质量的平衡:
① 自适应分段步长
步进次数 i 被划分为三段,近密远疏:
| 阶段 | 条件 | 步长 | 厚度容差 |
|---|---|---|---|
| 近距离 Small | i ≤ MAX_SMALL_STEP | SMALL_STEP_SIZE | MARCHING_THICKNESS_SMALL_STEP |
| 中距离 Medium | MAX_SMALL_STEP < i ≤ MAX_MEDIUM_STEP | MEDIUM_STEP_SIZE | MARCHING_THICKNESS_MEDIUM_STEP |
| 远距离 Large | i > MAX_MEDIUM_STEP | STEP_SIZE(+指数增长) | MARCHING_THICKNESS |
在未命中状态下,步长还会 指数级增长(每步乘 1.1),加速跨越空旷区域:
// 未命中时:指数加速(来自 voxagon SSPT 博客)
else if (!startBinarySearch)
{
currStepSize += currStepSize * 0.1;
marchingThickness += _Thickness_Increment;
}
每步还会叠加 dither 抖动偏移,将固定步长的条纹伪影(Banding)转化为高频噪声,
便于后续降噪消除:
rayPositionWS += (currStepSize + currStepSize * dither) * ray.direction;
② 二分搜索交点细化
当光线符号 Sign 发生翻转(由"物体前"进入"物体后")时,进入二分搜索模式:
if (startBinarySearch)
{
// 步长减半;方向取决于当前是在物体前还是后
currStepSize *= (FastSign(currStepSize) == Sign) ? 0.5 : -0.5;
}
光线在表面附近来回振荡并收敛,极大提升了交点精度,弥补了大步长带来的精度损失。
③ 背面深度精确厚度判定
无背面纹理(默认)
- ⚠️假设固定物体厚度
marchingThickness - ⚠️薄物体(告示牌、树叶)易漏光
- ✅无额外渲染开销
// 判定击中:在表面后方且未超厚度
hitSuccessful = (depthDiff <= 0.0)
&& (depthDiff >= -marchingThickness)
&& !isSky;
启用 _BACKFACE_TEXTURES
- ✅采样
_CameraBackDepthTexture - ✅精确计算物体真实厚度区间
- ⚠️需额外 BackfaceDataPass 渲染开销
// 判定击中:光线处于正面与背面深度之间
sceneBackDepth = max(sceneBackDepth, sceneDepth + thickness);
hitSuccessful = (depthDiff <= 0.0)
&& (hitDepth <= sceneBackDepth)
&& !isSky;
④ Hi-Z vs 线性步进 对比
| 维度 | Hi-Z 层级步进 | 本项目:自适应线性步进 |
|---|---|---|
| 算法复杂度 | $O(\log N)$,可跳过大片空白 | $O(N)$,逐步探测 |
| 空域跳跃 | 极佳,天空/空旷区域近乎瞬过 | 依赖指数加速,仍需多次采样 |
| 物体厚度精度 | 困难(需额外 Hi-Z 背面金字塔,显存×2) | ✅ 背面深度 + 二分搜索,高精度 |
| 实现复杂度 | 需额外 Compute Pass 生成 Min-Depth Pyramid | 直读 _CameraDepthTexture,无预处理 |
| 适用场景 | 长距离反射、大型开阔场景 | 中近距离 GI、强调厚度精度的场景 |
🔬降噪管线 (T-S-T)§ 05
SSGI 的光线步进结果本身噪声极高,系统采用经典的 T-S-T(Temporal → Spatial → Temporal)三阶降噪管线:
Pass 2:时域重投影(核心混合)
这是将上一帧 GI 引入当前帧的关键步骤,输入:原始噪声 GI + 运动向量 + 历史深度/GI。
核心逻辑
- 运动补偿:通过
_MotionVectorTexture将当前像素重投影回上一帧 UV - 遮挡剔除(Disocclusion):对比当前与历史深度/法线,差异超阈值则丢弃历史权重
- 加权累积:根据
_TemporalIntensity和累积帧数做加权平均 - AABB 钳制(可选):激进降噪模式下,钳制历史颜色在邻域色彩包围盒内
// AABB 邻域钳制核心(AdjustColorBox)
// 采样 3x3 邻域,计算颜色包围盒
half3 boxMin, boxMax;
AdjustColorBox(currentUV, out boxMin, out boxMax);
// 将历史颜色钳制在包围盒范围内,防止鬼影
prevColor = clamp(prevColor, boxMin, boxMax);
Pass 3/8:空域降噪
Pass 3 · A-Trous Wavelet 保边模糊
基于小波变换的保边滤波,使用 动态扩张步长(Dilation), 以较低采样数模拟大半径模糊:
- 法线权重:中心法线 vs 邻域法线夹角越大,权重越低
- 深度权重:线性深度差异越大,权重越低(保护几何边缘)
- 若命中距离
hitDistance < 1.0,自动降低模糊强度以保留近处细节
Pass 8 · Poisson Disk Recurrent Denoise
使用泊松盘采样点 + 切线空间加权(ReBLUR 启发),采样沿法线曲面分布, 适合处理具有复杂法线变化的表面,提供更高质量的平滑效果。
// C# 侧:每帧旋转泊松盘,消除固定图案 Artifacts
m_SSGIMaterial.SetVector(_ReBlurBlurRotator,
EvaluateRotator(k_BlurRands[frameCount % 32]));
多级降噪的 Ping-Pong 执行
// Aggressive Denoise:Pass 8 执行两次 Ping-Pong
if (denoiserAlgorithm == Aggressive) {
Blitter.BlitCameraTexture(cmd, diffuse, intermediateDiffuse, mat, pass: 8);
Blitter.BlitCameraTexture(cmd, intermediateDiffuse, diffuse, mat, pass: 8);
}
// Second Denoiser Pass:Pass 3 → Pass 4
if (secondDenoiserPass) {
Blitter.BlitCameraTexture(cmd, diffuse, intermediateDiffuse, mat, pass: 3);
Blitter.BlitCameraTexture(cmd, intermediateDiffuse, diffuse, mat, pass: 4);
}
Pass 4:时域稳定(精细后处理)
在空域降噪之后执行,作为最后一道防线:
- 再次 AABB/Variance 钳制:消除萤火虫(Fireflies)和颜色溢出
-
速度敏感混合:运动越快的像素历史权重越低(减拖影),静止像素权重高(最大化稳定)
$$\alpha = \text{lerp}(\alpha_{base}, \alpha_{low}, \text{velocity} \cdot k)$$
📁HLSL 文件职责§ 06
| 文件 | 职责定位 | 核心内容 |
|---|---|---|
| SSGI.hlsl | 执行者:光线步进逻辑 | RayMarching() 函数;自适应步长;二分搜索;背面深度碰撞;命中颜色采样 |
| SSGIUtilities.hlsl | 工具箱:基础设施 | Ray/RayHit 结构体;ConvertLinearEyeDepth;HitSurfaceDataFromGBuffer;SSGISampleProbeVolumePixel;GenerateRandomValue |
| SSGIConfig.hlsl | 配置层:常量映射 | #define 映射 C# Uniform 变量;MAX_STEP / STEP_SIZE / MARCHING_THICKNESS;MAX_ACCUM_FRAME_NUM = 8;MAX_REPROJECTION_DISTANCE |
| SSGIDenoise.hlsl | 降噪层:时域+空域 | Temporal Reprojection;AdjustColorBox (AABB Clamp);A-Trous Spatial;Temporal Stabilization;Poisson Disk |
| SSGIFallback.hlsl | 回退层:光线未命中 | BoxProjectedDirection(盒体投影校正);SampleReflectionProbes;APV 采样回退;SH 球谐回退 |
| SSGIInput.hlsl | 接口层:数据声明 | 所有纹理声明(GBuffer / 历史帧 / 运动向量);Uniform 变量;SH 探针;兼容旧版 Unity |
💡APV 自适应探针体积§ 07
Adaptive Probe Volumes (APV) 是 Unity URP/HDRP 中的高级实时间接光照系统, 解决传统 Light Probe 在精度与内存效率上的不足。
| 特性 | 传统 Light Probes | APV |
|---|---|---|
| 数据存储 | 稀疏离散点(3D 网格) | 3D 纹理(Texture3D)体积 |
| 数据密度 | 固定,手动放置 | 自适应,可依场景复杂度调整 |
| 内存效率 | 空旷区域仍占固定内存 | 基于稀疏体素八叉树,按需存储 |
| 运行时采样 | CPU/GPU 插值计算 | GPU 3D 纹理直接采样,延迟低 |
| 球谐阶数 | 主要 L0/L1 | 支持 L2 高阶 SH,方向精度更高 |
APV 在 SSGI 中的作用:Ray Miss Fallback
当光线步进未能找到屏幕上的几何体交点(射向天空或远处空旷区域)时,
SSGIFallback.hlsl 的分级回退策略:
- 若启用 APV → 调用
SSGISampleProbeVolumePixel获取高质量烘焙间接光 - 若无 APV → 回退到传统 SH 球谐函数
SSGIEvaluateAmbientProbeSRGB
#if defined(PROBE_VOLUMES_L1) || defined(PROBE_VOLUMES_L2)
half3 ambientLighting = SSGISampleProbeVolumePixel(
positionWS, reflectVector, viewDirectionWS,
normalizedScreenSpaceUV, probeOcclusion);
#else
half3 ambientLighting = SSGIEvaluateAmbientProbeSRGB(normalWS);
#endif
🚀Compute Shader 优化方向§ 08
将计算密集型 Pass 迁移到 Compute Shader 可更好地利用 GPU 并行性与 Shared Memory(LDS), 以下是各 Pass 的优化潜力排序:
| Pass | 优化潜力 | CS 优化优势 |
|---|---|---|
| P1 Ray Marching | 🔥🔥🔥 最高 | 每像素独立计算,完美并行;LDS 缓存 GBuffer/深度,减少纹理读取延迟 |
| P3/P8 Spatial Denoise | 🔥🔥🔥 最高 | Tile-based 双边滤波:将 Tile 边界像素载入 Shared Memory,避免重复采样,高效处理边界重叠 |
| P2 Temporal Reproj. | 🔥🔥 高 | 邻域 AABB 计算可以 Tile 化,将小块区域整体载入 LDS 后并行钳制 |
| P4 Temporal Stable | 🔥 中 | 与 P2/P3 同管线,减少 Render State 切换开销,统一 Compute 调度链 |
| P5 Copy History | ⚡ 辅助 | 整管线 CS 化后纳入统一数据复制流,保持管线连贯性;可同步做格式转换 |
推荐 CS 管线拆分方案
// CS 1:核心 GI 生成
DispatchCS(RayMarchingCS, screenW/8, screenH/8, 1);
// CS 2:完整降噪链(同一 CS 多次 Dispatch)
DispatchCS(DenoiseCS, ..., kernel: "TemporalReprojection"); // Pass 2
DispatchCS(DenoiseCS, ..., kernel: "SpatialDenoise_H"); // Pass 3 水平
DispatchCS(DenoiseCS, ..., kernel: "SpatialDenoise_V"); // Pass 3 垂直
DispatchCS(DenoiseCS, ..., kernel: "TemporalStabilization"); // Pass 4
// 最终混合(Pass 6)仍可用传统 Blit + Blend 或 CS UAV 写入
RWTexture2D 绑定与 Dispatch 同步。
Unity 6+ 推荐通过 Render Graph 调度 CS 任务,
用 builder.UseTexture / builder.UseRWTexture 声明资源依赖。
📚值得借鉴的工程实践§ 09
1. 双 API 兼容性(旧版 URP + Render Graph)
大量 #if UNITY_6000_0_OR_NEWER 宏区分代码路径,
旧版用 ScriptableRenderPass + Execute,
新版用 RecordRenderGraph + builder.SetRenderFunc,
是学习 URP 迁移至 Render Graph 的标准范例。
2. 精确时域数据管理(解决多相机污染)
CameraHistoryData 数组 + GetHashCode 追踪相机身份,
完美解决 Game View / Scene View / 反射相机历史帧相互污染的问题。
3. 灵活渲染路径适配(Forward GBuffer 按需生成)
SSGI 依赖 GBuffer,延迟模式直接复用,前向模式注入 ForwardGBufferPass 按需生成,
避免了 Deferred 下的重复渲染浪费。
4. 强大 T-S-T 降噪管线
Temporal Reprojection (P2) → Spatial Denoise (P3/8) → Temporal Stabilization (P4), 配合 AABB 钳制、遮挡剔除、速度敏感混合, 实现了生产级的屏幕空间 GI 噪声抑制。
5. 优雅的环境光分级回退策略
SSGIFallback.hlsl 的分级设计:
反射探针 Box Projection 校正 → APV 高阶烘焙 GI → 传统 SH 球谐,
保证了 SSGI 在任何配置下都能与 Unity 全局光照系统无缝衔接。
- GitHub: jiaozi158 / UnitySSGIURP
- ShaderToy: Edge-Avoiding À-Trous Wavelet Transform
- Voxagon: Screen Space Path Tracing – Diffuse
- GPU Pro 7 · Horizon Zero Dawn 体积云(相关降噪思路来源)