[UFSH2025]《命运扳机》的光与影

[UFSH2025]《命运扳机》的光与影 | 杨彬 黄兆铭 萨罗斯网络科技 引擎研发工程师

1. 核心挑战:项目背景与技术选型

讲座首先阐述了项目(《命运扳机》)面临的独特挑战,这些挑战直接决定了其光照和阴影的技术选型。

  • 游戏类型与风格:

    • 6x6 大世界地图,风格为日式卡通(二次元)

    • 场景:包含室内室外战斗。

  • 核心冲突:

    • 高品质需求: 卡通渲染“很卷”,需要高品质美术资产。GI对室内氛围、阴影对室外层次感至关重要。

    • 多端与性能:

      • 多人射击游戏: 帧率和稳定性是重中之重

      • 多端互通: 需要跨越不同算力设备,实现渲染表现基本一致,以保证竞技公平性。

      • 包体大小: 大世界游戏资源量大,包体是一个硬性限制指标。

  • 最终技术战略:

    • 可伸缩框架 (Scalable Framework): 针对光照和阴影都设计了可伸缩的方案,以适配不同设备。

    • 离线方案 (Offline Solutions): 成为必要选项。将重度计算(如烘焙)放到预处理阶段,使低性能设备也能流畅运行。


2. 全局光照 (GI) 方案

本场分享的第一个重点是GI。项目组设计了一套“实时”与“离线”相结合的可伸缩GI框架。

2.1 基础GI方案:Lumen vs. Probe GI

项目根据画质等级,选用了两套基础GI技术:

  • 高画质方案:Lumen (UE5 实时GI)

    • 用于超高极致画质。

    • 提供实时的、高质量的全局光照和反射。

  • 中/低画质方案:Probe GI (离线烘焙)

    • 项目自研的基于Probe的离线方案。

    • 核心原理:

      1. 空间离散化: 在场景中布置Probe点。

      2. 离线烘焙: 预先计算每个Probe点的光照信息。

      3. 运行时采样: 通过采样并插值(Interpolation)周围Probe的数据,来重建任意点的GI。

    • 本项目实现细节:

      • 存储内容: Radiance (辐射度) 信息。

      • 压缩方式: 使用 二阶球谐 (Second-order Spherical Harmonics, SH2) 系数来存储。

      • 优势: 运行时还原GI只需要简单的线性运算,性能开销极低。

2.2 画质分级与可伸缩设计

基于上述两套基础方案,项目组定制了详细的GI画质分级策略:

核心思路:

  • Lumen 内部也分级: 通过调整Lumen自身参数(如Radiance Cache分辨率)来伸缩。

  • Probe GI 内部也分级: 通过调整数据精度(MIP Level)和加载距离来伸缩。


详细画质分级配置

A. 实时方案 (Lumen) Tiers

  • 极致 (Extreme) 画质:

    • 使用标准 Lumen
  • 超高 (Ultra) 画质:

    • 同样使用 Lumen

    • 性能优化: 使用了分辨率更低的 Screen Probe 去做 Radiance Cache,以此在效果和性能之间做平衡。

B. 离线方案 (Probe GI) Tiers

  • 高 (High) 画质:

    • 近距离: 采样 MIP0 级别的GI数据(高精度)。

    • 远距离: 采样 MIP1 级别的GI数据(低精度)。

    • 特点: GI 加载距离最远,能看到更远处的高精度GI效果。

  • 中 (Medium) 画质:

    • 近距离: 采样 MIP0

    • 远距离: 采样 MIP1

    • 与“高画质”的主要区别: GI 加载距离更近

  • 低 (Low) 画质:

    • 全部使用 MIP1 级别的GI数据(近处和远处都用低精度)。

    • 性能优化: 使用 “简单遮蔽信息” (Simple Occlusion) 来做 防漏光 (Anti-Light-Leak) 处理,这是一种低成本的防漏光方案。

(讲座预告:后续会详细讲解 MIP0/MIP1 的数据结构以及“简单遮蔽信息”的具体实现。)


3. 关键术语小结

  • Probe GI: 一种离线GI烘焙技术,通过在空间中离散化采样点(Probe)来存储光照。

  • Radiance (辐射度): 存储的光照信息,比仅存储Irradiance(辐照度)包含更多方向性信息。

  • 二阶球谐 (SH2): 一种高效编码和解码低频光照(如Diffuse GI)的数学工具,性能开销小。

  • MIP0 / MIP1: 类似于贴图的MIPMAP,这里指代不同精度的GI烘焙数据。MIP0为高精度(可能数据量更大或Probe更密集),MIP1为低精度。

  • Radiance Cache (Lumen): Lumen中用于缓存和复用场景中Radiance信息的一种技术,降低Screen Probe的采样分辨率可以提升性能。

  • 简单遮蔽信息 (Simple Occlusion): 一种低成本的遮蔽数据,用于在低画质下防止光线从墙体等薄表面泄露。

3. GI 关键技术方案

本节深入探讨 Probe GI 方案中的四大关键技术:防漏光、Final Gather、反射,以及在半透明和体积雾上的应用。

3.1 防漏光 (Anti-Light-Leak)

核心问题: 渲染的像素(P点)采样到了错误的光照信息(例如,采样到墙体另一侧的Probe),导致漏光或漏影。

1. Lumen 方案的局限:

  • Lumen 的漏光可以通过调参缓解,但大世界场景无法找到一套通用参数

  • 项目方案: 将可调参数放入 Post Process Volume 中,由引擎实时插值解算,实现分区域的精细控制。

2. Probe GI 方案的防漏光(重点):

项目组的离线 Probe GI 方案参考了 DDGI (Dynamic Diffuse Global Illumination) 的思想,并进行了大量优化。

算法A:Relocation (探针重定位)

核心思想: 在离线烘焙时,就把 Probe 移动到“正确”的采样位置。

  • 情况1:Probe 陷在物体内部

    • 问题: 导致漏影(采样到被遮挡的GI)。

    • 解决方案: 必须将 Probe 推出墙外

    • 检测方法(离线): 从 Probe (O点) 向四周发射大量射线。

      • 如果射线方向与击中点法线的夹角小于90°,说明O点在Mesh内部(计数-1)。

      • 如果夹角大于90°,说明O点在Mesh外部(计数+1)。

      • 根据最终总计数的正负,判断 O 点是否在物体内部。

  • 情况2:Probe 在外部但远离表面

    • 问题: P点靠近墙面,但采样了远离墙面的 Probe,导致光照信息不准(衰减错误)。

    • 解决方案: 将 Probe 推向最近的墙边

    • 检测方法(离线): 从 Probe 发射射线,找到距离最近的相交点,将该点作为重定位后的新位置。

  • Relocation 的数据压缩:

    • 问题: 存储一个完整的偏移向量(方向+距离)数据量太大。

    • 解决方案: 离散化 (Discretization)

      1. 在 Probe 周围预定义N个固定的偏移点。

      2. 计算出理想的 Relocation 位置后,查找离它最近的那个预设点

      3. 最终只存储这个预设点的索引 (Index)(例如用 1 byte)。

算法B:Visibility Check (可见性检测)

核心思想: 运行时动态判断 P 点与 Probe 之间是否有遮挡,被挡住的 Probe 不应被采样。

  • DDGI/VSM 方案:

    • 原理: 基于切比雪夫不等式 (Chebyshev's Inequality),类似方差阴影(VSM)。

    • 离线存储: 烘焙时,Probe 需存储到最近表面的距离(Depth)和距离的平方(Depth²)

    • 运行时: 根据 P 点和 Probe 的相对位置,以及 Probe 存储的 Depth/Depth² 算出方差,套用不等式估算可见性概率。

    • 数据压缩: 可使用八面体映射 (Octahedral Mapping) 来控制存储的遮挡方向数量。

  • 《命运扳机》的改进:混合遮蔽 (Hybrid Occlusion)

    • 问题: 完整的 DDGI 可见性数据量依然巨大。

    • 关键洞察: 场景中大部分 Probe 周围的几何体很简单(例如只靠近一面墙)。

    • 解决方案:

      1. Simple Occlusion (简单遮蔽):

        • 用于绝大多数 Probe。

        • 仅用 1 个方向 + 1 个距离 就能描述其遮挡信息。

        • 数据量极大压缩 这是实现包体大小可控的关键。

      2. Complex Occlusion (复杂遮蔽):

        • 仅用于少数几何复杂区域(如角落)。

        • 使用完整的 DDGI 可见性数据,保证效果。

    • 结果: 实现了视觉无损的数据压缩,使这套算法得以在项目中实际落地。

算法C:Bias (偏移)

核心思想: 在采样 GI 前,将 P 点的世界坐标沿法线 (Normal Bias)视线 (View Bias) 方向偏移一点,避免自遮挡。

  • 问题: 偏移后的点 (A点) 可能移到墙体内部,采样到错误信息 ( "干坏事" )。

  • 解决方案: 验证偏移的有效性

    1. 将偏移后的 A 点投影回屏幕空间。

    2. 比较 A 点的深度 Depth(A)Depth Buffer 在该像素的深度 Depth(Buffer)

    3. 如果 Depth(A) > Depth(Buffer),说明 A 点在可见表面之后(即在墙内),是“坏点”。

    4. 算法: 使用二分查找 (Binary Search) 快速搜寻一个更小但有效的偏移量。


3.2 Final Gather (FG)

核心思想: 运行时,根据 P 点周围所有 Probe 的信息(SH2系数)和可见性,混合计算出P点的最终GI。

  • 基本原理:

    1. 目标: 求解渲染方程中的球面光照 (来自GI)。

    2. 方法: 是一个球面函数,可以用球谐 (SH) 重建:

      • 是P点的球谐系数。

      • 是球谐基函数(取决于方向 )。

    3. P点的 是由其周围所有可见 Probe 的 加权平均得到的。

    4. 权重 (Weight):这个权重函数非常综合,前面所有的防漏光算法(Relocation, Visibility)最终都作用在这里,用于决定每个 Probe 的贡献大小。

  • 方向性采样:

    • Diffuse GI: 使用P点的法线 (Normal) 方向去计算基函数

    • Rough Reflection / Subsurface: 使用反射方向或其他特定方向去计算,以获得不同的光照效果。

  • 性能优化 (重点):

    • 问题: Final Gather 是一个全屏 Shading Pass,开销较大。

    • 方案1:降分辨率 (Downsampling)

      • GI Pass 在 1/2 分辨率下渲染。
    • 方案2:上采样 (Upsampling)

      • 关键洞察: GI 是低频信息,不需要昂贵的降噪器。

      • 算法: 棋盘格渲染 (Checkerboard Rendering) + 时间累积 (Temporal Reprojection)

      • 逐帧逻辑(以N+1帧为例):

        1. 在一个 2x2 的像素块中,只计算1个像素(如左下角)的 FG 结果。

        2. 对于另外 3个 像素:

          a. 邻居复用: 检查当前帧已计算的邻居(如左下角),如果深度和法线接近,直接复用其结果。

          b. 历史帧复用: 如果邻居复用失败,则将该像素重投影 (Reprojection)上一帧 (N帧) 的结果中。

          c. 最差情况: 如果重投影也失败,直接复用当前帧已计算的那个邻居的结果(例如左下角)。

      • 效果: 因为GI是低频的,这种简单的复用没有明显视觉问题

      • 性能提升: 在 4070 显卡上,提速约 75%


3.3 反射 (Reflections)

  • 问题: 室内金属物体如果只采样天光,表现会很差。

  • 解决方案: 混合反射 (Hybrid Reflections)

    1. 粗糙反射 (Rough):

      • 使用 Probe GI 数据

      • 采样方向:使用反射向量 (Reflection Vector) 去查询 SH 数据。

      • 效果: 修正了粗糙表面的GI颜色和亮度。

    2. 光滑反射 (Smooth):

      • Probe GI 信息频率太低,不适用于光滑表面。

      • 混合使用 SSR (Screen Space Reflections) 和经典的反射探针 (Reflection Probes / Cubemaps)

    3. 最终混合:

      • 根据材质粗糙度 (Roughness)像素到反射探针的距离,来混合 Probe GI、SSR 和反射探针三者的结果。

3.4 体积雾 (Volumetrics) 与半透明 (Translucency)

  • Lumen 方案: 需要一个额外的 Transluency Volume 来存储和追踪半透明物体的 GI,消耗额外显存

  • Probe GI 方案的优势:

    • GI数据已存在于空间中(即离散的 Probe 数据),无需额外显存

    • 半透明物体: 可以在 Translucency Base Pass 中直接采样 Probe GI。

    • 体积雾: 在体积光渲染(Volume Marching)的每个 Cell 中,根据视线和位置去采样 Probe GI 作为散射光照。

    • 效果: 避免了室内体积雾被天光错误照亮的漏光问题,得到相对正确的效果。

4. GI 数据生产与压缩

本节讨论 Probe GI 所需的数据、如何进行高效压缩,以及离线烘焙的完整管线。

4.1 数据定义与压缩

  • 所需数据: 每个Probe需要存储 Radiance(光照)Occlusion(遮蔽)Offset(防漏光偏移)

  • 核心问题: 原始数据量巨大(示例中15GB),无法承受。

  • 压缩方案 1:Simple Occlusion

    • (如前所述)使用简化的遮蔽信息(如1个方向+距离)来代替完整的遮蔽数据,极大压缩了Occlusion的数据量。
  • 压缩方案 2:GI MIPMAPs (空间层级)

    • 核心假设:

      1. GI 本身是低频信息

      2. 动态物体在大部分情况下不需要高精度的防漏光信息。

    • 技术实现:

      • Brick (数据块):4x4x4 个 Probe 合并为一个管理对象,称为一个 Brick

      • MIPMAP: 通过 Brick 的设计,实现了不同尺度的GI数据,构成了GI的 MIPMAP

        • MIP0: 高精度,Probe 密度高(例如1个Brick覆盖小范围)。

        • MIP1: 低精度,Probe 密度低(1个MIP1数据块覆盖了MIP0的 64 倍空间体积)。

    • MIP Level 的选择策略:

      • 传统 MIPMAP: 仅基于与摄像机的距离。(远处用MIP1,近处用MIP0)。

      • 项目改进: 同时基于场景复杂度

        • 关键优化: 即使在近距离,如果是空旷区域(复杂度低),也强制使用 MIP1 的GI数据。
    • 压缩成果:

      • 显存节省: 最低画质(全MIP1)相比高画质(含MIP0)可节省 90% 以上的显存。

      • 包体压缩: 原始 15GB 的资源量被压缩到了 231MB

      • 视觉效果: MIP1 的GI效果会更“平”(模糊),但对于二次元画风来说,这种变化并不突兀,可以接受。


4.2 离线烘焙管线 (Baking Pipeline)

Probe GI 像 Lumen 一样,需要对场景进行预处理。

1. 范围与对象过滤

  • Detail Volume: 由美术师在房屋等重要资产周围手动放置的包围盒。

    • 作用1: 框定玩家主要活动范围,此范围内的 Probe 使用 MIP0 数据。

    • 作用2: 极大加速后续烘焙流程(只处理重点区域)。(注:也可用 Navigation Mesh 替代)

  • 物体过滤 (Object Filtering):

    • 在烘焙前剔除特定物体,例如:

      • 对GI贡献不大的超大物体。

      • 不适合烘焙光照的物体。

      • 为保证竞技公平性而隐藏的物体(如 Detail Mode: High 的草丛)。

2. 材质导出 (Material Export)

  • 挑战: 虚幻的离线烘焙器 Lightmass 对UE的现代材质系统(如RVT、分层材质)支持不充分。

  • 解决方案 A:RVT (运行时虚拟纹理) 地形

    • 问题: Lightmass 无法直接烘焙带 RVT 特性的地形。

    • 方案: “预渲染”RVT结果。

      1. 从地形正上方(-Z方向)进行一次 Scene Capture,抓取一张应用了RVT的地形顶视图纹理。

      2. 调整地形 Mesh 的 UV,使其在 Lightmass 中能正确采样到这张“预渲染”好的RVT纹理。

  • 解决方案 B:分层材质 (Layered Material)

    • 问题: 游戏中使用顶点的2U作为材质混合参数,但 Lightmass 导出时默认只使用基础UV,导致导出的纹理错误。

    • 方案: 重写 Lightmass 的材质导出模块

      1. 为 Static Mesh 生成一个 Dynamic Mesh 专门用于烘焙器。

      2. 关键技巧: 将该 Dynamic Mesh 的顶点坐标 (Position) 设置为它的 Lightmap UV

      3. 保持其他顶点属性不变。

      4. 效果: 在导出材质时,运行时逻辑能生成正确的纹理数据

3. 烘焙器 (Lightmass) 定制

  • 工具: 使用虚幻官方的 Unreal Lightmass,因为它足够稳定。

  • 定制 1: 增加了自定义烘焙通道,用于计算 IrradianceOcclusionRelocation 数据。

  • 定制 2:全阶段分布式烘焙 (Full-Stage Distributed Baking)

    • 问题: 原版 Lightmass 的分布式计算只支持最后最耗时的阶段,且更适用于小场景。对于大世界,烘焙的每个阶段计算压力都很重

    • 方案: 改进了 Lightmass 的分布式系统,使其支持所有阶段的分布式计算。


5. GI 运行时管理

5.1 数据流式加载 (Streaming)

  • 挑战: 大世界项目,不可能一次性加载所有GI数据。

  • 数据划分 (Offline):

    • 类比 World Partition:World Partition 系统一样,GI数据也根据网格单元 (Grid Cell) 进行划分。

    • GI Data Actor: 使用一个 Actor 来记录每个 Cell 空间中所有的GI数据。

    • (注:这与 UE 5.5 中的实验性功能 World Partition Static Lighting 思路相似。)

  • 数据更新 (Runtime):

    • GI Volume: 在GPU显存中只保留摄像机周围有限范围的GI数据(称为 GI Volume)。

    • 环形更新 (Ring Update):

      1. 当相机移动超过一个 Brick 的距离时,触发更新。

      2. 计算 GI Volume 中哪些数据已过时(需要释放),哪些是新进入的(需要上传)。

      3. 同时判断新进入区域的 MIP Level

    • 分帧上传 (Per-Frame Upload Limit):

      • 目的: 避免某一帧上传数据量过大导致卡顿。

      • 策略: 每一帧只上传固定大小的数据。

      • 效果: 视觉无损,因为需要更新的数据总是在 GI Volume边缘

5.2 GPU 数据结构与采样

  • 挑战: 如何在GPU上高效存储和访问这些流式加载的、离散的 Brick 数据?

  • 解决方案: 两级数据结构。

1. 资源池 (Resource Pool)

  • 定义: 一块(或多块)有限的、固定大小的显存空间(类似于内存池或大 Texture Array)。

  • 作用: 存储实际的GI数据(Irradiance, Occlusion等)。Brick 数据被动态换入换出到这个池中。

2. 层次化索引 (Hierarchical Index)

  • 定义: 一个 3D 纹理(或 Buffer),其空间结构与摄像机周围的 GI Volume 一一对应

  • 作用: 它不存储实际数据,而是存储一个地址或索引,该地址指向这个 Brick 的实际数据在 Resource Pool 中的位置。

  • “层次化”: 索引的编码方式可以指向不同MIP层级(MIP0或MIP1)的数据。

  • 更新: Ring Update 过程 Compute Shader 负责计算并更新这个 Hierarchical Index

3. 运行时采样 (Shader - Final Gather)

  • 核心: 间接访存 (Indirect Access)

  • 流程:

    1. 着色器根据 Probe 的世界坐标,计算出它在 GI Volume 中的位置。

    2. 使用这个位置去采样 Hierarchical Index,获得一个地址(指针)。

    3. 使用这个地址去 Resource Pool间接访问 (fetch) 实际的GI数据。


6. 性能与总结

  • 低画质 (4070):

    • GI 仅需 0.2ms GPU 时间。

    • Resource PoolHierarchical Index 总显存占用仅 1.44MB (因为全部使用 MIP1)。

  • 画质对比:

    • 中画质 (Probe GI): 相比低画质有更高层次感(MIP0 细节)。

    • 高画质 (Probe GI): 相比中画质有更高质量的反射。

    • 超高 (Lumen) vs 极致 (Lumen): 极致画质下的 Lumen 更准确,因为它能 Trace Mesh SDF

  • 设备适配: 展示了四种不同配置的机型(对应低、中、高、超高四档默认画质)的GI渲染时间,证明了该方案的高可伸缩性

7. 阴影方案:背景与目标

讲座的第二部分转为介绍项目中的静态阴影烘焙方案

  • 方案名称: MMH Shadow

  • 核心定义: 一套使用 MMH 压缩算法大世界静态阴影烘焙方案

  • 技术定位: 作为 CSM (Cascaded Shadow Maps)VSM (Virtual Shadow Maps) 的替代方案,用于静态场景

  • 方案特点:

    • 适用范围: 仅针对方向光 (Directional Light)

    • 前提: 光源和场景必须是静态的

    • 兼容性: 方案本身依然能支持动态物体(接收静态阴影)。

    • 核心目标: 优化性能,具体针对的就是 ShadowDepths Pass。

8. 核心问题:为什么需要压缩?

  • UE 动态阴影的痛点:

    • ShadowDepths Pass(即渲染 ShadowMap)每帧都会执行

    • 即使有 VSM 的缓存机制,在摄像机移动或场景变化时,开销依然很高。

  • 简单烘焙的局限:

    • 既然光源和场景是静态的,理论上可以把 ShadowMap 离线烘焙下来。

    • 致命问题:包体大小。 一张 4096x4096 的标准 ShadowMap(单通道)就需要 64MB。对于几公里的大世界,这种存储开销是绝对无法接受的

  • 解决方案: 必须使用高效的压缩算法


9. 核心算法:阴影压缩技术演进

方案基于三篇论文,核心思想是将高分辨率的 ShadowMap 压缩成一个四叉树 (Quadtree)。该方案的“黑盒”输入是一个双层阴影贴图 (Dual Shadow Map),输出是一个可序列化的、无损压缩的四叉树。

9.1 算法 1:MH (Multi-resolution Hierarchy)

  • 核心概念:Dual Shadow Map / 双层阴影贴图

    • 传统的 ShadowMap 只存储最近点的深度(遮挡物正面,A点)。

    • 洞察: 遮挡物背面的深度(B点)也是一个有效的遮挡深度。任何在 [A, B] 之间的深度都处于阴影中。

    • 存储: 一个深度的区间 (Depth Interval) [Front_Depth, Back_Depth],而不是单一深度值。

  • 压缩原理:四叉树压缩 (Quadtree Compression)

    • 自底向上 (Bottom-up) 构建四叉树。

    • 核心思路: 尝试找到一个父节点,其存储的深度区间能够尽可能多地代表其四个子节点

    • 示例: 假设 4 个子节点的深度分别是 [1], [2], [2], [3]。压缩算法可能会选择 [2] 作为父节点,此时父节点代表了两个子节点,另外两个子节点 [1][3] 则保留。这样,4个节点就被压缩成了3个节点(父、子、子)。

  • 采样(还原):

    • 自顶向下 (Top-down) 遍历四叉树。

    • 从根节点出发,根据要采样的像素坐标,向下遍历,直到叶子节点,得到最终的深度区间。

  • 压缩率: 极高。论文数据显示 4K 贴图能压缩到原始大小的 0.64%

9.2 算法 2:MMH (Merged Multi-resolution Hierarchy)

  • 核心改进: 在 MH 的基础上,增加了子树合并 (Merge) 功能。

  • 合并条件:

    1. 两颗子树具有完全相同的拓扑结构

    2. 两颗子树的深度区间相匹配

  • 关键优化:存储相对深度 (Relative Depth)

    • 问题: 即便结构相同,两颗子树的绝对深度也几乎不可能匹配(例如,一个在近处,一个在远处)。

    • 解决方案: 节点不存储绝对深度,而是存储相对于其父节点的相对深度

    • 效果: [父=10, 子=11, 子=12][父=50, 子=51, 子=52],它们的相对深度都是 [+1, +2],因此可以合并。

  • 采样变化:

    • 还原时需要累加深度:Final_Depth = Self.Relative_Depth + Parent.Depth
  • 压缩率: 相比 MH,数据量又压缩了约 1/3

9.3 算法 3:DAG-MH (Directed Acyclic Graph MH)

  • 核心改进: 数据结构分离。这是《命运扳机》项目当前正在使用的算法。

  • 原理: 将 MH 的数据拆分为两种结构,并分别进行压缩。

    1. 数据结构 A:数组 (Array)

      • 内容: 存储所有不重复的深度区间 (Depth Interval)
    2. 数据结构 B:四叉树 (Quadtree)

      • 内容: 节点不再存储深度,而是存储一个指向 数组A 的下标 (Index)
  • 进一步优化:

    • 四叉树 (B): 同样可以使用 MMH 算法进行压缩(合并结构相同的子树)。

      • 优势: 此时压缩的是整数(下标),而不是浮点数(深度),理论上更容易找到匹配项,压缩率更高。
    • 数组 (A): 使用调色板压缩 (Palette Compression) 算法进行压缩。

      • 原理: 寻找一条最优直线,使得直线上的点能近似代表数组中的深度区间。
  • 采样流程(最复杂):

    1. 采样四叉树 (B),得到一个(相对)数组下标

    2. 累加得到绝对下标后,用该下标去采样调色板(压缩后的数组A)

    3. 从调色板中还原出最终的深度区间

  • 压缩率: 相比 MMH,数据量又压缩了约 1/3

10. 离线烘焙 (Offline) 实现细节

10.1 烘焙流程与数据划分

  • 烘焙工具: 依然基于虚幻的 Unreal Lightmass

  • 烘焙范围:

    1. 美术师在场景中放置 Importance Volume 来框定烘焙范围。

    2. 将这个 Volume 投影到光源空间,形成一个2D矩形。

  • 数据切分 (Tiling):

    • 将该2D矩形划分为 4096x4096Tile (瓦片)

    • 每个 Tile 对应一张 Dual Shadow Map (DS-Map)

  • 数据生成:

    • 以 Tile 为单位,逐像素向场景发射射线(光线追踪),获取每个像素的深度区间 (Depth Interval) [Front, Back]
  • 数据采样(反向):

    • 运行时,将世界坐标点转换到光源空间,再转换到 Tile 空间,即可采样对应的深度区间。

10.2 数据存储 (World Partition)

  • 核心策略: 基于 World Partition 系统进行数据管理。

  • 存储单位:

    1. 每个 WP Cell 放置一个自定义 Actor

    2. 当该 Cell 被流式加载时,Actor 也会被加载。

    3. 压缩后的 Tile 数据存储在该 Actor 的一个 Component 中。

  • 核心技术问题:2D Tile 3D Actor 的映射

    • 问题: Tile 数据是2D的,无法仅凭 (x, y) 坐标确定它属于哪个3D空间中的 Cell Actor。

    • 解决方案: 在离线处理时,对 Tile 数据进行一次采样,得到一个深度值,从而反算出该 Tile 对应的世界坐标(贴在物体表面的位置)。根据这个世界坐标,即可确定它属于哪个 Cell。

10.3 关键优化与细节

  • 精度选择:1cm/pixel

    • 权衡: 2cm/pixel 能大幅减少数据量,但 1cm/pixel 产生的阴影瑕疵 (Artifacts) 更少。项目最终选择 1cm/pixel 以保证质量。
  • 烘焙数据大小的进一步压缩(重点):

    • 观察 1:树冠(高频信息)压缩率极低。

      • 策略: 单独对树(LOD)进行降分辨率烘焙

      • 原因: 树冠内部无法进入,且烘焙精度的降低在视觉上不明显。

    • 观察 2:屋顶、围墙(大块连续平面)

      • 策略: 使用 DAG-MH 算法 对这类结构有效压缩。
    • 最终成果: 压缩率从 0.55% 进一步降低到 0.24%


11. 运行时 (Runtime) 实现

本节讲述 MH Shadow 数据如何在运行时被高效加载、解压和渲染。

11.1 数据流送 (Streaming)

  • 加载: 当 WP 加载 Component 时,会创建 Scene Proxy,其中包含了压缩的 Tile 数据。

  • GPU 数据结构:

    • Memory Pool (资源池): 一个固定大小的 GPU Buffer。用于存储摄像机 Upload Range 范围内的压缩 Tile 数据

    • Index Buffer (索引): 另一个 Buffer,其长度等于 Tile 数量,用于存储每个 Tile 在 Memory Pool 中的起始地址(指针)

  • 问题: 简单加载的显存开销巨大(例如800米范围就需要 400MB)。

11.2 核心方案:Virtual Clipmap

  • 解决方案: 借鉴 VSM (Virtual Shadow Maps) 思路,实现了一个自定义的、轻量级的 Virtual Clipmap

  • Clipmap: 阴影被分为多级,每级分辨率相同,但覆盖范围逐级增大。

  • Virtual (虚拟化):

    • 将 Clipmap 进一步划分为 128x128 像素的分页 (Page)

    • 引擎只在显存中保留(缓存)解压后的、当前帧可见的 Page。

    • Physical Texture (物理页池): 用于存储这些解压后的 Page。

  • 为何不复用 VSM?

    • 解耦合: 方便后续升级和维护。

    • 轻量级: 项目只需要一个轻量级的 Virtual Clipmap 功能。

11.3 Virtual Clipmap 渲染流程

  1. Feedback Pass (反馈): 读取 SceneDepthZ ,分析当前帧哪些 Page 是可见的

  2. Async Readback (异步回读): CPU 异步获取这个可见性列表。

  3. CPU Analysis (CPU分析):

    • 分析需要 Request(请求加载)哪些新 Page。

    • 向物理页池申请空闲页。

    • 释放(Evict)不再使用的旧 Page。

  4. Request Pages Pass (页面请求 - 核心):

    • 执行解压缩,将 Tile 数据还原为深度图,并写入 Physical Texture
  5. Shadow Projection (阴影投影):

    • 正常渲染 Pass,采样 Physical Texture 来生成最终的阴影遮罩 (ShadowMask)。

11.4 解压缩策略 (重点)

  • 问题: 在快速转动视角时,阴影出现明显的加载延迟和“刷”出来的现象。

  • 原因: CPU 解压太慢(每帧只能解压1页),而中远距离的 Clipmap 之前依赖 CPU 解压。

  • 分级解压策略:

    • 近距离 (Level 0): GPU 解压

      • 数据已在 GPU 的 Memory Pool 中,解压极快,每帧可处理大量 Page。
    • 远距离 (Far Clipmaps): CPU 解压

      • 数据不在 GPU,CPU 解压后上传。依然很慢(每帧1页),但远处不明显。
    • 中距离 (Mid Clipmaps) - 优化后:

      1. 压缩的 Tile 数据动态上传到一个临时 GPU Buffer

      2. 在 GPU 端执行解压缩

      • 结果: 性能较好,只需少量临时显存,完美解决了视角转动时的阴影延迟问题

11.5 软阴影 (Soft Shadows)

  • 实现: 有了 Clipmap(本质是一张 ShadowMap),可以套用任意软阴影算法。

  • 选型: 为贴近 VSM 的效果,项目选用了 SMRT(Shadow Map Ray Tracing) 算法。

11.6 动态物体 (Dynamic Objects)

  • 角色的阴影: 使用 PerObject Shadow,该技术独立于CSM/VSM,可直接复用。

  • 其他动态物体(含 Nanite):

    • 目标: 避免同时开启 CSM/VSM(双倍显存开销)和 Nanite 的高固有开销。

    • 解决方案: 将动态物体绘制(Rasterize)到我们的 Virtual Clipmap 中。就像VSM对Non-Nanite物体做的那样。

    • 流程:

      1. 构建一个FProjectedShadowInfo。

      2. 收集Clipmap范围内FMeshBatch。

        • 将Nanite当作Fallback Mesh处理。
      3. Submit Culling Pass:

        • 一个线程处理一个Primitive。
        • 包围盒跟每级Clipmap判断是否需要绘制。
        • 如果需要(覆盖了脏页)则InstanceCount加一。
      4. Submit Raster Pass:

        • 只在 InstanceCount > 0 的 Clipmap Page 上绘制动态物体。
    • 优势: 大部分情况下,InstanceCount 为 0(因为阴影已被缓存)。


12. 总结与未来工作

12.1 性能与效果

  • 性能对比 (vs VSM):

    • ShadowDepths Pass(核心开销):

      • VSM: 0.58 ms

      • MHM Shadow: 0.06 ms (这 0.06ms 几乎全是动态物体的开销)

    • 总开销: 显著优于 VSM,并且优化了渲染线程(RT)和RHI线程(Draw Call 大幅减少)。

  • 视觉效果:

    • MMH Shadow 提供了比低、中、高画质(CSM)精度更高的阴影。

    • 在某些区域,MMH Shadow 的精度甚至高于 VSM (Ultra) 的表现。

12.2 未来工作

  • 阴影 (Shadows):

    • 尝试使用神经网络进行压缩。

    • 实现 Time-of-Day (TOD) 动态烘焙

    • 支持 Local Lights(点光源、聚光灯)。

    • 更高效率。

  • 全局光照 (GI):

    • 尝试使用神经网络压缩 GI 数据或实现 TOD GI。

    • 完善远景 GI 细节。

    • 可破坏的动态物体实现动态 GI 效果。