🌐项目总览§ 01

该项目为 Unity URP 实现了一套完整的 屏幕空间全局光照(SSGI) 系统, 展示了生产级的 Renderer Feature 编写规范,同时涵盖以下技术点:

🔑 项目架构核心
整套 SSGI 管线由 1 个 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 路径跳过
💡 Forward 适配思路
SSGI 本质依赖 GBuffer。Deferred 模式直接复用现有 GBuffer; Forward 模式则通过 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(不透明物体后、透明物体前):

Pass 0
Copy Direct Lighting
复制主颜色 Buffer 或环境光作为 SSGI 底色,确保间接光单独叠加
Pass 1
SSGI Ray Marching
核心光线步进,利用 Depth + GBuffer 计算带噪声的原始间接漫射光
Pass 2
Temporal Reprojection
时域重投影:运动向量重映射 + 遮挡剔除 + AABB 钳制的首次混合
Pass 3
Edge-Avoiding Spatial Denoise
A-Trous 小波保边模糊,以法线/深度差异权重保护几何体边缘
Pass 4
Temporal Stabilization
空域降噪后的二次时域稳定:再次 AABB 钳制 + 速度敏感混合权重
Pass 5
Copy History Depth
将当前帧深度写入历史深度纹理,供下帧 Pass 2 重投影使用
Pass 6
Combine & Upscale GI
将降噪后的间接光叠加到主颜色 Buffer,降采样时同步做上采样
Pass 7
Camera Motion Vectors(Editor)
仅 Scene View 使用,修正运动向量以保证时域效果调试正确
Pass 8
Poisson Disk Recurrent Denoise
泊松盘采样 + 切线空间权重(ReBLUR 启发),高质量表面贴合模糊
Pass 9
Blit Color Texture
管线末端的标准拷贝 Pass,将结果写入最终渲染目标

完整渲染流程

P0Copy Lighting
P1Ray March
P2Temporal Reproj.
P3/8Spatial Denoise
P4Temporal Stable
P5Save History
P6Combine

🔭光线步进算法§ 04

SSGI.hlsl 中的 RayMarching() 实现了一种 不依赖 Hi-Z 加速结构的纯线性步进算法,通过以下手段保证性能与质量的平衡:

① 自适应分段步长

步进次数 i 被划分为三段,近密远疏:

阶段条件步长厚度容差
近距离 Smalli ≤ MAX_SMALL_STEPSMALL_STEP_SIZEMARCHING_THICKNESS_SMALL_STEP
中距离 MediumMAX_SMALL_STEP < i ≤ MAX_MEDIUM_STEPMEDIUM_STEP_SIZEMARCHING_THICKNESS_MEDIUM_STEP
远距离 Largei > MAX_MEDIUM_STEPSTEP_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、强调厚度精度的场景
⚠️ 混合方案建议
可采用 粗糙阶段用 Hi-Z 快速跳跃定位区域,精细阶段切换为线性步进 + 背面深度判定, 兼顾远距离加速与近距离厚度精度。

🔬降噪管线 (T-S-T)§ 05

SSGI 的光线步进结果本身噪声极高,系统采用经典的 T-S-T(Temporal → Spatial → Temporal)三阶降噪管线:

$$\text{Noisy GI} \xrightarrow[\text{Disocclusion, AABB}]{\text{Temporal Reprojection (P2)}} \text{Stable GI} \xrightarrow[\text{Normal/Depth Aware}]{\text{Spatial Denoise (P3/8)}} \text{Smooth GI} \xrightarrow[\text{Velocity Blend}]{\text{Temporal Stabilization (P4)}} \text{Final GI}$$

Pass 2:时域重投影(核心混合)

这是将上一帧 GI 引入当前帧的关键步骤,输入:原始噪声 GI + 运动向量 + 历史深度/GI。

核心逻辑

  1. 运动补偿:通过 _MotionVectorTexture 将当前像素重投影回上一帧 UV
  2. 遮挡剔除(Disocclusion):对比当前与历史深度/法线,差异超阈值则丢弃历史权重
  3. 加权累积:根据 _TemporalIntensity 和累积帧数做加权平均
  4. 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), 以较低采样数模拟大半径模糊:

$$w = w_{gaussian} \cdot \exp\!\left(-\frac{1 - \mathbf{n}_c \cdot \mathbf{n}_s}{\sigma_n}\right) \cdot \exp\!\left(-\frac{|d_c - d_s|}{\sigma_d}\right)$$

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:时域稳定(精细后处理)

在空域降噪之后执行,作为最后一道防线:

💡 Pass 2 vs Pass 4 区别
Pass 2首次、主要、有遮挡检测的混合,决定时域效果核心质量; Pass 4精细、后续、钳制为主的稳定,弥补空域降噪可能引入的缺陷。

📁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 ProbesAPV
数据存储稀疏离散点(3D 网格)3D 纹理(Texture3D)体积
数据密度固定,手动放置自适应,可依场景复杂度调整
内存效率空旷区域仍占固定内存基于稀疏体素八叉树,按需存储
运行时采样CPU/GPU 插值计算GPU 3D 纹理直接采样,延迟低
球谐阶数主要 L0/L1支持 L2 高阶 SH,方向精度更高

APV 在 SSGI 中的作用:Ray Miss Fallback

当光线步进未能找到屏幕上的几何体交点(射向天空或远处空旷区域)时, SSGIFallback.hlsl 的分级回退策略:

  1. 若启用 APV → 调用 SSGISampleProbeVolumePixel 获取高质量烘焙间接光
  2. 若无 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
🔑 APV 的工程价值
APV 为 SSGI 提供了高质量、内存高效的烘焙 GI 数据, 作为屏幕空间计算结果的"背景"与"补充",消除了光线穿帮时常见的突兀黑色区域。

🚀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 写入
⚠️ 迁移挑战
CS 迁移需要手动管理 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 全局光照系统无缝衔接。


📌 参考资源