3. Lumen - Radiance Cache
1 概述
Lumen 中的 radiance cache 是基于 world probe 实现的,与 DDGI 不同,radiance cache probe 仅用于补充世界空间的 radiance 信息,例如 screen probe、reflection 的光线样本都有可能采样到 radiance cache。
LumenRadianceCache::UpdateRadianceCaches
为radiance cache更新入口,被IrradianceFieldGather、ScreenProbe、TranslucencyVolumeLighting三处调用到,其中IrradianceFieldGather与ScreenProbe是为像素提供indirect diffuse的两种方式,本文仅讲述 ScreenProbe 部分的调用。
2 宏观逻辑
3 实现
3.1 层级分布
radiance cache 的 world probe 是基于 clipmap 的分布方式
3.1.1 Clipmap 介绍
Clipmap [1] 提出是为了解决mipmap的内存开销问题,一个非常大的mipmap实际渲染中,只会根据相机视角用到很小的部分。Clipmap的字面意思就是将原 mipmap 裁剪得到一个将会被使用的较小区域,物理显存中只会维持这个区域,根据视角加载原mipmap的相关数据。下面左图是mipmap执行clip操作的示意图,右图是clipmap的两部分,
- Clipmap Stack:层级大小超过clip size,这部分不同层的大小都是 clip size
- Clipmap Pyramid:层级大小低于clip size,这部分不同层的大小与原mipmap对应层级相同

在clipmap stack部分,虽然每一层级的大小都相同,但覆盖范围会越来越大。在视角移动加载数据时,clipmap使用环形寻址的方式来避免多次加载相同的数据,如下示意图,

3.1.2 World Probe 的 3D clipmap 结构
二维clipmap中的一个texel在3D clipmap中对应一个3D cell (probe),world probe 的3D clipmap结构是以相机为中心的包围盒区域,每个层级的probe数量相同,但覆盖范围不同,也就是层级越高的grid对应的3D区域越大。

world probe 的分布记录在 FRadianceCacheState
的下面成员中,UpdateRadianceCacheState
负责 world probe 的层级分布的更新。
1 |
|
clipmap的层级数量、每一层级的grid数量以及每一层级的覆盖范围,都有对应命令行调整
cmd | 类型 | 说明 |
---|---|---|
r.Lumen.ScreenProbeGather.RadianceCache.NumClipmaps | int32 | 一共多少层级 |
r.Lumen.ScreenProbeGather.RadianceCache.ClipmapWorldExtent | float | 0级覆盖的世界范围的一半,立方体区域 |
r.Lumen.ScreenProbeGather.RadianceCache.ClipmapDistributionBase | float | 0级之后的世界范围指数增长的base,即 extent * pow(level) |
r.Lumen.ScreenProbeGather.RadianceCache.GridResolution | int32 | 每一层的probe数量,每个维度probe数量相同 |
下面是基于这些配置参数对层级分布的更新,主要是以相机位置为中心构建clipmap空间。一个clipmap空间,坐标单位是CellSize,原点为 clipmap 起点,中心点为相机所位于cell的中心。
1 |
|
相机位置 NewViewOrigin,各成员计算为
- Center = SnappedOrigin = floor(NewViewOrigin / CellSize) * CellSize,相机位置对齐到 CellSize 的世界坐标
- Extent:clipmap 包围盒范围的一半
- CornerTranslatedWorldSpace:SnappedOrigin - 0.5 * CellSize - Extent + PreViewTranslation
clipmap 的起始位置,再偏移 0.5 * CellSize 到 cell 中心点,也就是以起点为原点的坐标系的偏移量 - ProbeTMin:用于光追 TMin,CellLength * ProbeTMinScale (半透明情况下ProbeTMinScale 为 1,否则为 0.1)
3.1.3 shader 中的 clipmap 变换
3.1.3.1 ProbeCoord <-> ProbeWorldPosition
有了clipmap,在 shader 可以将一个世界坐标变换为指定层级的clipmap空间下 probe coord,以及将 probe coord 变换到世界空间下,例如
1 |
|
3.1.3.2 查找给定WorldPosition
对于一个被 level i 覆盖的世界坐标,肯定会被后续大于 i 层级覆盖,那么到底采样哪一层级?优先选择最低层级,因此在仅给定世界坐标来获取probe coord以及层级时,从最低层级到最高层级依次遍历,直至找到覆盖该坐标的层级为止。
1 |
|
在实际实现中,相邻两个层级的边界上由于层级跳变,效果上会出现明显的边界感。因此在层级边缘会设置一定范围的fade区域,当采样到fade区域时,会引入一个抖动值 ClipmapDitherRandom
。fade 区域定义为距clipmap边界的grid单位,默认 GLumenTranslucencyVolumeRadianceCacheClipmapFadeSize(4)
,上述中的 InvClipmapFadeSizeForMark
为倒数:
- 当不在fade区域时,EdgeFade >= 1,EdgeFade > ClipmapDitherRandom 恒为 true
- 当在fade区域时,EdgeFade < 1,最终在上下相邻层级中随机采样一个
3.2 构建probe更新列表
3.2.1 indirect 数据更新
DDGI 的world probe负责插值得到某一点的irradiance,同时在计算光线radiance时还会采样上一帧的irradiance来近似无限反弹的结果,这意味着像素会采样周围probe得到其接收的间接 irradiance,而次级反射点同样需要采样其周围world probe。在 Lumen 里,无限反弹由 surface cache 的 radiosity 部分完成,radiance cache probe 的 radiance 来采样 surface cache 得到的,已经具有无限反弹的结果,因此只需要更新被使用到的 probe。
在标记使用状态之前,先将 RadianceProbeIndirectionTexture
中的每个probe状态重置为 INVALID_PROBE_INDEX
。RadianceProbeIndirectionTexture 是一个 3D texture,记录了所有clipmap层级的每个probe的使用状态。每个层级的probe数量相同,在 x 维度逐层级并排排列在贴图中,假如 probe 数量为 48x48x48,有层级0 [047, 047, 047],层级 1 [4895, 047, 047]。
ScreenGatherMarkUsedProbes
表示被screen probe使用到的 world probe,主要实现在 shader MarkRadianceProbesUsedByScreenProbesCS
中,遍历每个screen probe:
通过变换得到 screen probe 的 WorldPosition
获取覆盖screen probe的最小clipmap层级
uint ClipmapIndex = GetRadianceProbeClipmapForMark(WorldPosition, 0);
层级有效,在 indirection texture 将包裹screen probe的 8 个 world probes 标记为使用状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14void MarkPositionUsedInIndirectionTexture(float3 WorldPosition, uint ClipmapIndex)
{
float3 ProbeCoordFloat = GetRadianceProbeCoordFloatForMark(WorldPosition, ClipmapIndex);
int3 BottomCornerProbeCoord = floor(ProbeCoordFloat - 0.5f);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(0, 0, 0), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(0, 0, 1), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(0, 1, 0), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(0, 1, 1), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(1, 0, 0), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(1, 0, 1), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(1, 1, 0), ClipmapIndex);
MarkProbeIndirectionTextureCoord(BottomCornerProbeCoord + int3(1, 1, 1), ClipmapIndex);
}结果记录到
RadianceProbeIndirectionTexture
中,对应位置设置为 USED_PROBE_INDEX。1
2int3 IndirectionTextureCoord = ProbeCoord + int3(ClipmapIndex * RadianceProbeClipmapResolutionForMark, 0, 0);
RWRadianceProbeIndirectionTexture[IndirectionTextureCoord] = USED_PROBE_INDEX;
3.2.2 Probe重用、分配与释放
整个clipmap的probe数量比较多,但只会更新其中一部分,如果按照clipmap分布为所有probe分配数据会非常浪费。因此使用buffer来紧凑管理需要分配数据存储的probe列表。RWProbeFreeList
存放空闲的probe数据索引,可使用时从中申请以及释放时归还,其中的 ProbeIndex 唯一标识probe的数据位置,包括:
- RWProbeLastUsedFrame[ProbeIndex] 记录probe上次使用的帧数
- 光追结果 RadianceProbeAtlasTexture, DepthProbeAtlasTexture 等等
3.2.2.1 上一帧 Probe 重用与释放
如前所述,clipmap以相机为中心,会随着相机的移动而移动,往往上一帧的clipmap与当前帧有重叠,因此可以重用这部分probe。主要实现在 shader UpdateCacheForUsedProbesCS
中,遍历每个LastFrameProbe:
- 当前遍历的 ClipmapIndex 以及 LastFrameProbeCoord可以从线程ID中得到
- LastFrameProbeCoord -> ProbeTranslatedWorldPosition -> ProbeCoord 变换到当前帧clipmap下
- 从 LastFrameRadianceProbeIndirectionTexture 中得到
LastFrameProbeIndex
(probe唯一ID,分配得到的,非变换得到) - 使用上一帧的clipmap变换,得到probe世界坐标
ProbeTranslatedWorldPosition
,再变换到当前帧的clipmap中,得到ProbeCoord
。ProbeCoord有效,表示上一帧probe也在当前帧clipmap中,继续执行重用逻辑
- 从 LastFrameRadianceProbeIndirectionTexture 中得到
- 按照 probe 在当前帧是否被使用到以及其中数据是否过旧来更新重用状态
- 从 RadianceProbeIndirectionTexture 得到当前帧probe的
ProbeUsedMarker
- probe上次更新帧数
LastUsedFrameNumber = RWProbeLastUsedFrame[LastFrameProbeIndex]
- probe 被当前帧使用或者
(FrameNumber - LastUsedFrameNumber)
在阈值范围内- 标记为重用 bReused = true
- 将 LastFrameProbeIndex 分配给当前帧probe,更新到 RadianceProbeIndirectionTexture 中
- 被当前帧使用,则更新
RWProbeLastUsedFrame[LastFrameProbeIndex] = FrameNumber
- 从 RadianceProbeIndirectionTexture 得到当前帧probe的
- 如果上一帧probe没有被重用,则释放probe索引 LastFrameProbeIndex,归还到 RWProbeFreeList 中
3.2.2.2 Probe 分配索引、优先级与开销预算评估
处理完上一帧probe,接下来需要为当前帧被使用到的probe分配数据索引。这部分在 shader AllocateUsedProbesCS
中,遍历 RadianceProbeIndirectionTexture 的每个texel的ProbeUsedMarker:
- 当前probe被使用到,还未被分配数据索引 (ProbeUsedMarker == USED_PROBE_INDEX)。从 ProbeFreeList 中分配一个ProbeIndex
- 否则表示,当前probe为重用probe,已有数据索引,即 ProbeIndex=ProbeUsedMarker
- 每帧trace更新probe的开销是有限的,因此需要评估probe的更新优先级以及trace开销
- 更新优先级,基于 LastTracedFrameIndex, LastUsedFrameIndex, ClipmapIndex 来划分PriorityBucketIndex,bucket越靠前优先级越高。
GetPriorityBucketIndex
- 如果probe从未trace过,返回 bucket 0
- 否则,根据“clipmap层级越高重要性越低”、”经历越久帧未trace,优先级越高“来确定 UpdateImportance,最终得到 BucketIndex
- ProbeTraceCost:probe的光线样本分辨率不同,根据probe到相机的距离具有不同的trace cost
- 如果低于 SupersampleDistanceFromCameraSq 为16
- 如果低于 DownsampleDistanceFromCameraSq 为 4
- 更新优先级,基于 LastTracedFrameIndex, LastUsedFrameIndex, ClipmapIndex 来划分PriorityBucketIndex,bucket越靠前优先级越高。
- 累积 trace cost,
RWPriorityHistogram[PriorityBucketIndex] += ProbeTraceCost
这里通过probe的上次trace帧、上次使用帧以及层级,将probe映射到bucket中,得到了一个不严格的优先级列表 RWPriorityHistogram
。
3.2.3 为预算内的probe构建 trace data
3.2.3.1 为probe创建trace data
首先基于probe的trace cost优先级队列 RWPriorityHistogram
,确定更新预算内的bucket。在 shader SelectMaxPriorityBucketCS
中,按照bucket从前往后累积 trace cost,直至达到预设 NumTracesBudget,即查找到最后一个更新的bucket MaxUpdateBucketIndex
,以及记录最后一个bucket剩余多少trace cost MaxTracesFromMaxUpdateBucket
。
在 shader AllocateProbeTracesCS
中,遍历 RadianceProbeIndirectionTexture 每个texel,对于分配ProbeIndex的probe:
- 以同样的方式得到 probe 的 ProbeTraceCost, PriorityBucketIndex
- 位于更新开销内的probe (
PriorityBucketIndex <= MaxUpdateBucketIndex
)- 如果 probe 位于最后一个bucket中MaxUpdateBucketIndex,还需要判断是否已消耗完最后一个bucket的剩余预算,已消耗完则不进行trace
- 为能够 trace 的probe分配 TraceIndex:
- 累积 ProbeTraceCost 到 ProbesToUpdateTraceCost[0]
- 构建 trace data
RWProbeTraceData[TraceIndex] = float4(ProbeWorldPosition, asfloat((ClipmapIndex << 24) | ProbeIndex));
- 更新trace帧
RWProbeLastTracedFrame[ProbeIndex] = FrameNumber;
按照预算开销,构建了要执行trace的probe列表 RWProbeTraceData,其中每个元素打包了 ProbeWorldPosition, ClipmapIndex, ProbeIndex。
3.2.3.2 probe 偏移与PDF估计
避免probe距物体表面过近,会为每个需要trace的probe计算一个偏移量,实现在 shader ComputeProbeWorldOffsetsCS
中。主要思想是,通过查询 SDF 可以得知 probe 位置到最近表面的距离,若距离小于阈值,则:
- 每个group负责一个probe trace data
- 将一个cell划分为 4x4x4 voxels, 在不同的 voxels 方向上对于 probe 位置施加一个偏移量,再次查询SDF最近距离
- 在所有查询结果中,取偏最近距离最大的一个偏移量
world probe 在不同的方向上重要性不同,后面会根据重要性来执行超采样逻辑,即具有更高分辨率的光线样本。重要性的其中一方面是需要PDF估计,这里使用screen probe的数据来评估world probe pdf。具体做法在 shaderScatterScreenProbeBRDFToRadianceProbesCS
中,通过将 screen probe 的BRDF扩散到其周围的8个world probe实现:
- 每个group负责一个screen probe
- 根据 screen probe 的 WorldPosition 可以查询到其周围8个world probe。得到最小的 BottomCornerProbeCoord,再加上
int3(GroupThreadId.x & 0x1, (GroupThreadId.x & 0x2) >> 1, (GroupThreadId.x & 0x4) >> 2)
,就可以定位其中一个probe - 通过
RadianceProbeIndirectionTexture
查找到 ProbeIndex - ProbeIndex 可以找到 probe 的SH数据位置,将screen probe的SH系数
BRDFProbabilityDensityFunctionSH
累加到RWRadianceProbeSH_PDF
3.3 构建用于更新probe的trace tile
3.3.1 创建 probe trace tile 列表
前面讲到 world probe 会根据重要性来执行超采样而达到更高分辨率的光线样本,主要是通过对probe trace tile进行不同的层级划分实现的,实现在 shader GenerateProbeTraceTilesCS
:
- 每个 group 负责一个probe。BaseTraceTileResolution 为 ProbeResolution(默认为 32) 的 1/16,即 2x2
- 确定 probe trace tile 的层级数量
NumLevels
:- 如果ProbesToUpdateTraceCost超出预算的2倍,取 1
- 否则根据probe到相机的距离
DistanceFromCameraSq
:SupersampleDistanceFromCameraSq 取 3,DownsampleDistanceFromCameraSq 取 2
- BaseTraceTile 每个 texel 都表示一个立体角区域,划分则是为其中的立体角区域再细分一个tile,
- NumLevels > 1 并且 TraceTileCoord 方向上PDF高于阈值,为该方向细分 1 个 L1 trace tile (2x2),添加到
PendingTraceTileList
,待后续进一步划分- TraceTileList 的每个元素打包了 uint2:
- x: 低16位,TraceTileCoord * 2 + uint2(0/1, 0/1);高16位,Level=1
- y: ProbeTraceIndex
- TraceTileList 的每个元素打包了 uint2:
- 否则,表示不需要再细分,则添加一个 L0 trace tile 到最终结果
CompletedTraceTileList
- NumLevels > 1 并且 TraceTileCoord 方向上PDF高于阈值,为该方向细分 1 个 L1 trace tile (2x2),添加到
- 遍历 PendingTraceTileList,继续划分NumLevels 3, 2的 trace tile 如果 PDF 高于超采样阈值,则L1划分4个L2,L2划分4个L3
- 划分 NumLevels > 2 的 trace tile
- 划分 L1
- 如果TraceTileCoord 方向上 PDF 高于
SupersampleTileBRDFThreshold
,添加 4 个 L2 tiles 到PendingTraceTileList
- 否则,添加1个 L1 tile 到最终结果
CompletedTraceTileList
- 如果TraceTileCoord 方向上 PDF 高于
- 收集 L2:将 PendingTraceTileList 中的 L2 tile 加入到结果
CompletedTraceTileList
中
- 划分 L1
- 划分 NumLevels > 1 的 trace tile
- 将 PendingTraceTileList 中的 L1 tile 加入到结果
CompletedTraceTileList
中
- 将 PendingTraceTileList 中的 L1 tile 加入到结果
- 划分 NumLevels > 2 的 trace tile
- 按照 probe 次序,将将CompletedTraceTileList的所有trace tile写入RWProbeTraceTileData,每个元素 uint2
- x:低 16位,TraceTileCoord * 2 + uint2(0/1, 0/1);高16位,level
- y:ProbeTraceIndex
上述,针对probe做了以trace tile为单位的划分,每个trace tile表示一定分辨率。假如每个trace tile具有 8x8 分辨率,而一个 probe 最少有 2x2 个 trace tile,即最小分辨率为 16x16;最多有 8x8 个 trace tile,即最大分辨率为 64x64。但注意一个probe的trace tile划分不是均匀,例如一个PDF较高的方向上可能划分更多trace tile。
3.3.2 trace tile 排序
trace tile的每个texel都对应了球面上一个立体角范围,可以在该立体角范围内采样光线,执行光追得到radiance。为了光追效率,还会额外做一次trace tile排序,将方向相近的trace tile相邻存放。具体做法是:将光线方向范围划分为一定数量的bin,再把trace tile划分到对应的bin中,最后按照bin顺序排列trace tile。实现在 shader SortProbeTraceTilesCS
中:
- 只有一个group,一个线程负责一个trace tile,能够得到 TraceTileCoord, TraceTileLevel, ProbeTraceIndex
- 确定 trace tile 所位于层级的probe分辨率
TraceResolution=(RadianceProbeResolution/2) << TraceTileLevel
- RadianceProbeResolution 默认为 32,对应了 trace tile 默认 8x8 的分辨率
- trace tile 在所处层级分辨率下的texel
ProbeTexelCoord=TraceTileCoord * RADIANCE_CACHE_TRACE_TILE_SIZE_2D(8)
。这是在给定球面分辨率下,表示trace tile方向的texel坐标 - 给定指定bin数量,投影trace tile到bin中:
DirectionalBin=ProbeTexelCoord * NUM_DIRECTION_BINS_2D(8) / TraceResolution
。把整个球面分辨率划分为8份- 记录每个bin内的trace tile数量。DirectionalBin转为一维索引 FinalBinIndex,
SharedNumTraceTileBins[FinalBinIndex] +=1
- 按照Bin的顺序排列trace tile
- 统计当前 TraceTile 所属bin之前所有Bin的trace tile数量
SortedTraceTileOffset
,以及累积所属bin已存放trace tile数量 - 写入 RWProbeTraceTileData[SortedTraceTileOffset]
- 统计当前 TraceTile 所属bin之前所有Bin的trace tile数量
3.4 执行光追更新probe
3.4.1 为每个trace tile执行光追
下面就是为每个trace tile执行光追,生成每个texel方向上的radiance,实现在shader LumenRadianceCacheHardwareRayTracingCS
中:
一个group负责一个trace tile,一个线程负责一个光线样本,每个trace tile 8x8 分辨率
线程索引得到trace tile内的texel坐标
TexelCoord
,执行TraceRadianceCacheProbeTexel
:TraceTileIndex -> ProbeTraceIndex -> TraceData -> ProbeIndex,可以获取到probe的所有相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct FRadianceCache
{
uint2 TraceTileCoord;
uint TraceTileLevel;
uint ProbeTraceIndex;
float3 ProbeWorldCenter;
uint ClipmapIndex;
uint ProbeIndex;
uint ProbeResolution;
float ProbeTMin;
uint ProbeAtlasResolutionModuloMask;
uint ProbeAtlasResolutionDivideShift;
bool bFarField;
};构造光线
Ray.Direction
:将局部 TexelCoord 变换到球面上的 ProbeTexelCoord,通过 spherical mapping 可得到texel中心点的方向Ray.Origin
:probe的世界坐标Ray.TMin
:ProbeTMin,0.1 * cell_sizeRay.TMax
:限制到near field内
追踪并采样surface cache
TraceAndCalculateRayTracedLightingFromSurfaceCache
追踪结果按照trace tile排列,存放在 RWTraceRadianceTexture, RWTraceHitTexture
上面的追踪同样在 far field 内执行一次
3.4.2 Resolve到统一分辨率下
如前所述的trace tile划分,为probe分配了不同的分辨率,最终需要 resolve 到统一分辨率下。实现在 shader SplatRadianceCacheIntoAtlasCS
中:
- 一个group负责一个trace tile
- 获取trace tile基本信息,TraceTileIndex -> ProbeTraceIndex -> TraceData -> ProbeIndex,得到 FRadianceCache
3.5 采样
Radiance Cache 本质上只提供稍远距离的radiance信息(可能是 radiance cache 分辨率不足提供近光会有比较明显的artifact吧,例如反射过于模糊?),这个由 probeTMin 控制。反射、screen probe 会首先尝试 trace probeTMin 范围内,如果没有近距离交点且被radiance cache覆盖,则会采样 radiance cache 得到 radiance 信息。shader 函数 SampleRadianceCacheAndApply
提供了采样radiance cache的功能。
References
[1] Christopher C. Tanner, Christopher J. Migdal, and Michael T. Jones. 1998. The clipmap: a virtual mipmap. In Proceedings of the 25th annual conference on Computer graphics and interactive techniques (SIGGRAPH ‘98). Association for Computing Machinery, New York, NY, USA, 151–158.