前言:实时全局光照的困境与希望
在实时渲染领域,全局光照(Global Illumination, GI) 始终是衡量画面真实感的关键。经典的解决方案各有掣肘:
- 烘焙光照贴图:需要漫长的预处理,无法响应动态场景或动态光源。
- 屏幕空间方案(SSAO/SSGI):仅利用当前屏幕可见信息,边缘拉丝、漏光严重。
- 硬件光线追踪:对硬件要求苛刻,集成显卡和移动端难以实时运行,VR更成问题。
- 基于探针的方案(如DDGI):动态刷新困难,探针摆放需要大量手动工作。
Godot引擎在4.x版本中引入了一项全新的GI方案——SDFGI(Signed Distance Field Global Illumination)。它的使命是:一键开启,实时更新,全Vulkan设备支持,同时提供漫反射和镜面反射,甚至能驱动体积雾。不依赖光线追踪硬件,甚至连VR都不强制使用TAA。
其核心技术建立在Morgan McGuire的DDGI(Dynamic Diffuse Global Illumination)思想之上,但将探针更新机制改为有符号距离场球体追踪,从而摆脱了对硬件RT的依赖。这份解析基于Godot联合创始人Juan Linietsky的公开技术讲义,我将从零开始,一步步剥离SDFGI的每一个细节,介绍背后的图形学原理。
1. 设计哲学:目标与妥协
一个好的实时算法必先明确它的战场边界。SDFGI在设计之初就定下了严格的硬性目标和自觉牺牲。
1.1 硬性目标
- 一键启用:无需布置探针、SDF几何体、光照图UV等任何预处理。在Godot编辑器中,只需在Environment节点勾选SDFGI即可。
- 实时更新:光照变化(太阳移动、灯光开关)和几何变化(静态网格移动/旋转)能快速反映在间接光照中。近似实时,一般几帧内收敛。
- 良好质量:最大限度地消除漏光,提供可信的漫反射和镜面反射。目标是“够好”而非物理精确。
- 全Vulkan兼容:能在集成显卡(IGP)上运行,不依赖硬件光线追踪。Vulkan 1.0即可,无需扩展。
- VR友好:不强制要求TAA(时间抗锯齿),避免VR中的重影不适感。SDFGI的时间累积是可选的,且独立于TAA。
- 支持透明物体和体积雾:间接光也要成为体积光的来源。这意味着体积雾可以散射SDFGI计算的间接光。
1.2 妥协
为了达到上述目标,它明确放弃了一些特性:
- 高频GI细节缺失:小尺寸阴影和颜色变化无法被探针捕捉,必须由屏幕空间GI(SSIL)补偿。探针分辨率决定了间接光的频率上限。
- 动态物体不贡献GI:动态对象只能接收间接光,不会将自己表面的颜色反弹到环境中。未来可能为其增加简单的遮挡体支持。
- 必须使用级联(Cascades):随着距离增加,探针分辨率下降,细节丢失。这种层级结构是内存与精度的折衷。
- 小自发光物体产生噪点:因为探针数量有限,小的发射面很难被稳定采样,表现为时间上的闪烁。
了解了这些先决条件,我们就能明白后续每一步技术选择的原因:一切都为了在苛刻的限制下找到最优的性价比。
2. 全局光照的“记忆单元”——辐照度与遮蔽场
SDFGI的核心思想,是在空间中布置一张3D的“灯光记忆网”。这张网由成千上万个虚拟探针组成,每个探针记录着它所在位置从各个方向接收到的光照。当渲染某个表面时,就去周围找几个探针,插值得到间接光。
2.1 双场系统:Irradiance Field 与 Occlusion Field
为了防止插值造成“穿墙漏光”,SDFGI维护了两套场:
- 辐照度场(Irradiance Field):一个3D网格,每个节点是一个探针,存储该点来自球面各个方向的入射辐照度。注意,它存储的是辐照度(Irradiance),即单位面积接收的辐射通量,采样时可直接作为漫反射间接光。
- 遮蔽场(Occlusion Field):记录每个探针在各个方向上能看到的最近几何体的径向距离。作用相当于一个距离图,用来计算当前着色点与探针之间的可见性。它实际上存储的是该方向的深度均值
和平方均值 ,用于切比雪夫测试。
当我们在着色点
2.2 多级联(Cascades)覆盖全距离
为了在一个广袤世界中同时保证近处的精度和远处的覆盖范围,SDFGI借鉴了阴影贴图的级联思想,采用了最多8个级联。每个级联是一个立方体区域,边长以2的幂次扩大,例如第一个级联边长可能是16米,第二个是32米,以此类推,直到覆盖整个开放世界。这种指数增长保证了对数级的深度覆盖,非常适合大世界。
每个级联的内部结构是固定的:
- 距离场体素:
128³(存储16位浮点距离) - 辐照度与遮蔽探针:
17³(每个维度上17个探针,均匀分布在128³空间中,即每隔8个体素一个探针:,每个探针覆盖 8³的单元格区域)
图1 左侧展示了单一级联的解剖图:外框是128³体素距离场,内部17³个探针,以及级联之间重叠混合区域。当物体逐渐远离时,它会在两个相邻级联的过渡区域内同时读取两套探针并混合结果,从而平滑切换精度。混合因子通常由着色点距两个级联边界的距离决定。

图1 右侧直观展示了三个级联的嵌套关系和过渡混合区域,由于级联跟随相机移动(类似于CSM阴影),相机总是位于中心级联的内部,保证了近处最高精度。
2.3 探针的存储格式:5×5八面体映射
每个探针需要记录球面上各个方向的入射光,常见的做法是小立方体贴图或球谐函数。SDFGI选择的是5×5像素的八面体映射。八面体映射能将球面均匀地参数化到一个正方形,具体方法:将单位球面上的方向映射到一个八面体,然后展开到 [-1,1]² 的正方形,再映射到 [0,1]² 的纹理坐标。
八面体映射推导:
给定球面方向
- 计算
范数分母: - 映射到二维:
- 处理下半球:如果
,则额外将 变换到 以外的区域,具体表现为 ,最终得到一个在 内的表示。 - 缩放至
: texcoord = (u*0.5+0.5, v*0.5+0.5)
下面用示意图展示从三维方向到八面体,再到2D纹理的过程:
这种映射的优点是球面面积均匀性较好,且5×5的网格可以很好地覆盖所有方向,每个像素对应一个立体角大致相等的方向。25个方向采样可以满足漫反射探针的精度需求。
为什么用八面体而非立方体贴图?因为它在存储上更紧凑且无接缝问题,并且易于在Compute Shader中遍历。5×5像素正好构成25个方向采样,等价于一个极低分辨率的立方体贴图(2×2 face),但更适合直接作为Compute Shader的25线程工作组处理。25个线程可以完美覆盖整个探针的更新计算。
这25个方向的光照打包成一个大纹理数组,一个级联的所有探针(17³ = 4913个)平铺在数组的一层中。每个5×5小块周围留1像素边框,用于双线性插值采样。注意,边框像素通常与相邻探针的边框共享,实现无缝滤波。如此紧凑的安排为后续的高效更新埋下伏笔。
在内存中,一个辐照度探针占用 5*5*4字节(RGBA8) ≈ 100字节。一个级联的辐照度探针数组约 4913 * 100 ≈ 480 KB。遮蔽探针需要存储两个通道(均值和平方均值),每个像素8字节,一个级联占用约 4913 * 5*5*8 ≈ 960 KB。加上距离场等,单个级联内存占用约几MB,8级联控制在几十MB内,非常适合低端硬件。
3. 数据生成管道:从几何到光照
SDFGI的数据生成分为五个阶段,均在GPU上执行,利用Compute Shader和光栅化管线:
- 高精度实体位场生成
- 距离场生成(Jump Flood)
- 遮蔽探针生成
- 光照缓冲与光照纹理建立
- 球体追踪填充辐照度探针
3.1 超采样实体位场(Oversampled Solid Bitfield)
如果直接在128³体素上渲染几何体,薄墙会因分辨率不足而消失,导致漏光或者需要巨大的偏差。SDFGI的解决方案是4倍超采样,即用512³的子体素精度表达几何体。
- 渲染时,不写入颜色,只利用光栅化覆盖信息。使用空帧缓冲,通过片段着色器或直接利用保守光栅化,调用
imageStoreAtomicOr将覆盖的位填充到512³的“实体位”网格中。这个网格用uint32类型纹理,每个uint包含一个4³子立方体的64个体素状态(因为),因此总内存为 (512/4)^3 * 4字节 = 128^3 * 4 ≈ 8 MB,实际上因为有些压缩,最终约16MB。这种位打包极大节省了带宽。 - 同时,在更粗的
64³网格中存储该区域的平均反照率和自发光颜色。理由:光照查询时不需要512³的颜色细节,64³足以提供平滑的表面颜色。它存储为RGBA8纹理,每个体素代表(512/64)^3 = 8^3个子体素的平均色。
位打包细节:
每个 uint32 包含64位(4³),每个位代表一个子体素是否被几何体覆盖。位顺序一般按Z/Y/X递进。例如,对于一个体素坐标
于是,我们得到了一个在亚体素精度上表达几何体的二进制体素场,为生成精确距离场打下基础。
下图是位打包的示意图:一个 uint32 存储一个 4×4×4 子立方体的64个位置占用信息。
渲染技巧
为了高效填充位场,Godot使用了几何着色器或多视图渲染:对每个三角形,根据其朝向选择投影到哪个2D平面(即选择对应的轴对齐投影),然后光栅化到 512² 的2D切片,再用原子操作写入3D位。这样可以一次性覆盖一个方向上的所有切片,效率很高。
3.2 Jump Flood 距离场
拥有一个高精度实体位场后,需要将其转换为有符号距离场(存储每个空体素到最近实体表面的距离)。这里采用Jump Flood 算法,它是一种基于传播的并行算法,特别适合GPU。
算法核心思想:
每个体素存储一个指向“最近实体子体素”的坐标(称为种子)。初始只有实体子体素养心提供有效种子。然后通过多次迭代,步长从网格边长的一半开始,每次减半,每个体素检查步长距离处的邻居的种子,如果更近就采纳。经过对数次迭代后,每个体素都收敛到全局最近实体的种子。
具体步骤(128³网格):
初始化种子:
1
2
3
4
5
6
7ivec3 seed;
if (hasSolidBit) {
// 实体子体素养心坐标,子体素大小为 1/4,所以实体坐标为 (x*4 + 1.5, y*4 + 1.5, z*4 + 1.5)
seed = ivec3(x*4 + 1.5, y*4 + 1.5, z*4 + 1.5);
} else {
seed = ivec3(INT_MAX, INT_MAX, INT_MAX); // 无穷远
}Jump Flood 迭代:
步长step从64开始,每次除以2,直到1。对于每个体素,查询周围偏移量为 的邻居。通常取采样模式为6轴(±x, ±y, ±z)或更复杂的星型模式。这里给出一个简化但有效的伪代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21int step = 64;
while (step > 0) {
ivec3 best_seed = mySeed;
float best_dist = length(vec3(mySeed - currentCoord));
// 采样所有可能方向
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -1; dz <= 1; dz++) {
ivec3 nCoord = currentCoord + ivec3(dx, dy, dz) * step;
ivec3 nSeed = loadSeed(nCoord);
float dist = length(vec3(nSeed - currentCoord));
if (dist < best_dist) {
best_dist = dist;
best_seed = nSeed;
}
}
}
}
mySeed = best_seed;
step >>= 1;
}实际实现会优化采样模式,只检查那些可能提供更近种子的方向,但概念上如此。
转换为距离:
最终,每个体素的种子是一个亚体素坐标,计算欧几里得距离:1
float distance = length(vec3(mySeed - currentCoord)) * voxelSize;距离值存储为16位浮点数。
由于种子来自512³精度位场,得到的距离场能够表达薄至1/4体素的几何体,极大提高了遮蔽探针和球体追踪的精度。需要注意的是,这里生成的是无符号距离场(只存在于空体素),但通过一定的偏置和遮蔽探针,仍然能有效工作。
3.3 遮蔽探针:亚体素精确追踪
遮蔽探针需要知道“从该探针向某个方向看,多远能撞到几何体”。最大距离设为28个体素(约为相邻探针间距的
- SDF初筛:查SDF距离场(
128³),如果当前位置的距离值> 28,则直接取28为遮蔽距离,因为已超出有效检测范围。 - DDA精追踪:否则,在
512³实体位场中进行光栅化式的追踪。从探针中心出发,沿方向前进,步长自适应: - 先用SDF给出的距离大步前进,因为SDF保证不会穿墙。
- 当SDF值小于一个阈值(比如0.5体素)时,切换为精细的DDA步进,每次步进一个子体素(1/4体素),检查所在体素的64位掩码中对应位是否为1。
DDA位检查具体实现:
对于当前子体素坐标 subPos,计算包含它的父体素索引 gridCoord 和子体素在64位内的索引 bitIndex。然后读取 solidBits[gridCoord],检查 (solidBits >> bitIndex) & 1。一旦发现被设置,记录精确深度
为了减少噪声,完成所有方向追踪后,对该探针的5×5遮蔽纹理进行3×3的高斯模糊(或简单均值滤波),平滑相邻方向的深度,抑制切比雪夫测试中的突变。
3.4 光照缓冲与光照纹理
光照数据并非直接存在探针里。取而代之,我们构建一个光缓冲区:一个1D数组(StructuredBuffer),包含所有与实体相邻的空体素坐标(即表面下一层的体素)。这些空体素恰好是“可以看到表面”的位置,它们将会被着色以形成出射光。对应的3D光照纹理(RG32U,实际存储为RGBE编码的颜色+各向异性权重)也一并生成,覆盖整个级联区域,但只有缓冲区列表中的那些体素会被计算。
这样将光照计算的范围缩小到仅包括表面附近真正重要的区域,避免了在整个128³空间内计算光照。对于空旷场景,这能节省95%以上的计算量。
RGBE9995与各向异性编码:
颜色以共享指数形式存储,9位红色、9位绿色、9位蓝色、5位指数,共32位。各向异性权重占用另一个32位纹理的5位,编码了8个方向的大致概率分布(可能用一组离散方向索引),用于后续探针更精确的方向性采样。
3.5 球体追踪填充辐照度探针
这是整个流程的核心计算,也是SDFGI区别于传统DDGI的关键。我们用Compute Shader为每一个辐照度探针计算其5×5的球面入射光。
工作组分配:每个探针对应一个工作组(25个线程),每个线程处理八面体的一个像素方向。线程组共享内存用于最后的后处理。
每个线程的处理流程(伪代码详细描述):
GLSL
1 | |
法线计算:利用SDF梯度,中心差分近似:
1 | |
各向异性:光照纹理中的5位各向异性索引一个预定义的方向集合,权重用于在半球模糊之前调制入射光,使得在粗糙度极低时仍能保留一定的方向性。
半球模糊:近似漫反射积分 normal_estimate 可以使用探针对应方向,或者更简单的取八面体中心方向。
时间混合因子
跨级联追踪的过程对于清晰反射尤为关键,下图为射线跨级联追踪示意图:
4. 渲染时的间接光采样
当实际渲染场景几何时,每个像素需要计算出间接漫反射光照。我们使用三线性探针混合 + 切比雪夫遮蔽测试。
4.1 寻找周围8个探针
对于世界坐标点
GLSL
1 | |
最终漫反射间接光为
背向权重 max(0.005, dot(dirToProbe, wsNormal)) 使得如果表面背对探针方向,该探针贡献极小,防止背面不合理的照明。
4.2 切比雪夫不等式防漏光原理
遮蔽探针存储了每个方向深度的均值
这里我们将可见性权重近似为这个概率上界。当着色点
为了增加过渡对比度,对计算的权重
平方操作使衰减更快,减去偏置可进一步收紧半影,防止过度柔化。默认bias取值约0.1~0.2。
另外,加入 varianceBias 和 distanceBias 防止除零和数值不稳定:varianceBias 保证方差不为零,distanceBias 轻微偏移均值以补偿SDF精度误差。
4.3 级联间混合
如果 blend = smoothstep(0.9, 1.0, dist_norm) 线性插值。有时还需要考虑更高一级联。混合因子由着色点到两个级联有效边界的相对距离确定。为了保证一致性,还要注意权重归一化。
多级联采样会使着色器开销略有增加,但确保了远近光照的平滑过渡,不会出现明显的环状边界。
5. 性能优化
如果每一帧都对所有级联的所有切片重新生成距离场、遮蔽探针和辐照度探针,性能将无法接受。SDFGI的精髓在于一套精密的增量更新机制。
5.1 级联滚动(Cascade Scrolling)
级联体跟随相机移动。当相机移动导致级联体边界在某个方向上移动超过一个体素时,一条新的切片(3D slab)进入,对应旧区域的另一条切片滚出。由于相机通常不会沿对角线疾速移动,每个时刻往往只有一个轴向的切片更新,因此我们只需重新生成那些新进入的切片区域,而非整个128³域。这被称为“滚动”更新。
例如,相机沿X轴正方向移动,导致最左侧的一片体素被丢弃,最右侧出现一片新体素需要计算。那么我们只需重新光栅化右侧新切片,并合并旧数据。
下图展示了交界处指针合并的逻辑:
5.1.1 距离场合并
对于新旧区域交界的切片,我们要将新生成的区域与存量数据进行无缝合并:
- 旧区域保留着Jump Flood最终迭代的种子指针(指向最近实体子体素),新切片也产生了自己的指针。
- 对于交界附近的每个体素,检查对面区域对应位置的种子指针距离,如果更近,则替换自己的种子。具体做法:在切片边界,新旧区域各保存一份完整的种子纹理。对每个体素,如果
dist(newSeed) < dist(oldSeed),则用新种子替换。 - 之后,只需在这些交界区域重新运行少数几次Jump Flood迭代(因为跳跃传播会快速扩散新种子),通常只需额外几次步长较小的迭代,就能让交界体素收敛到正确的最近实体。正是因为跳洪水算法局部性弱,这种局部修补十分高效。
伪代码:
1 | |
5.1.2 遮蔽探针合并
- 完全落入新区域的探针,全部重新计算(8个方向像素全追踪)。
- 边界上的探针,仅重新追踪朝向新切片方向的那部分八面体像素(例如向右偏移的方向),朝向旧区域的方向保持原值不变。具体实现:对每个八面体方向像素,计算其方向向量,如果与新区域有正点积,则重新追踪;否则保留原值。
- 然后,对这些边界探针执行一次轻量的模糊,融合新旧数据。
这种方向性裁剪极大减少了遮蔽探针的更新工作。
5.1.3 光缓冲区与光照纹理更新
光缓冲区(需要着色的空体素列表)在距离场合并时同步重构:删除出界体素,加入新入界体素。这个操作仅是一个数组重排,开销很低。光照纹理对应的区域被标记为脏,后续球体追踪光照计算时,将只处理标记为脏的光照体素和依赖它们的探针。
5.2 射线缓存
由于多数场景几何是静态的,球体追踪的命中信息(命中级联、命中光照纹理坐标、命中法线)可以跨帧缓存。每个辐照度探针的每个方向像素存储一个缓存结构。每帧只对失效缓存进行重追踪。缓存失效条件:
- 命中点在当前级联内,且该级联发生了滚动影响到该区域(新切片影响了该体素)。
- 命中点在上层级联,且射线穿出当前级联的边界侧面因滚动而改变了(即射线方向对应的出口侧面进入新数据)。
这样,静态区域内的大多数射线直接复用上一帧的命中,大幅降低了追踪负载。动态灯光或天空变化时,虽然缓存仍有效,但需要通过脏标记重新计算光照颜色(见下节),不影响命中位置。
5.3 光照脏区域缓存
除了射线命中缓存,还有一套粗粒度(比如 8³ 块)的3D脏位纹理。当某个光缓冲区体素因为灯光变化、新几何或天空变化需要重新计算时,它被标记为脏。在辐照度探针更新步骤中,对于每个方向像素,如果其命中点所在光照体素的脏位被设置,则强制重算该方向光照。未脏的命中点直接复用缓存光照。
此外,静态光源(如烘焙灯)的光照可以完全分离到另一张静态光照纹理中,它们只在几何体变化时重新计算脏区域,其他时间完全跳过。这几乎将静态光照的维护成本降为零。
5.4 视锥外降频更新
远离相机视锥体的探针可大幅降低更新频率。例如,根据探针与视锥体的距离,设置跳过因子:视锥内每帧更新,近距离外区域每2帧更新,更远区域每4帧或8帧更新一次。由于这些探针主要影响远处间接光,而远处间接光本身时间累积就慢,降频根本看不出差异。由于相机居中,视锥外探针数量很大,此优化可节省大量GPU时间。
5.5 小结
通过这些策略,SDFGI的重负载计算量被压缩到近似于“仅仅处理相机周围一个薄壳的新内容”。当相机以正常速度移动时,每帧额外开销极低;突然跳跃时会有短暂重计算高峰,但很快恢复。这使得实时GI在低端硬件(如Steam Deck、AMD APU)上也能够流畅运行。
6. 用SDF做反射:从镜面到漫反射
通常GI只解决漫反射,但SDFGI直接将反射也纳入了同样的框架,覆盖全粗糙度范围。这是SDFGI的一大亮点——在探针数据基础上,几乎无额外成本地提供三种反射层级。

6.1 清晰反射(Sharp Reflections)
对屏幕上的每个像素(通常在延迟渲染的反射pass中):
- 从深度缓存重建世界坐标。
- 计算反射向量
。 - 找到包含该点的最小级联(即最精细级联),开始追踪。
- 利用该级联的距离场进行球体追踪,沿
方向步进,直到命中表面。 - 在命中点采样光照纹理,获取出射光,即为清晰反射颜色。还可结合材质粗糙度做微量模糊。
级联跳变问题解决:单个级联追踪时,当像素恰好在级联边界,反射内容可能因分辨率突变而pop。SDFGI在追踪清晰反射时,会在当前级联和上一级联(更粗一级)同时执行球体追踪,取两者中较小的命中距离,并用两个距离的差作为混合权重,融合两者颜色。具体混合:
1 | |
若粗级联命中更近,则更多采用粗级联;否则采用细级联。这极大地平滑了级联过渡。
6.2 中等粗糙反射(Medium Roughness)
回顾3.5节,辐照度探针在工作组屏障之前,有一个“未滤波”的5×5辐射率数据(即方向辐射率,未经半球积分)。系统会将其存入另一个纹理数组——反射探针场,并进行时间累积平均(不加半球模糊)。它存留了方向性,因此能产生比辐照度更清晰的反射。
对于中等粗糙度材质,采样该反射探针场:取反射向量方向,在该方向对应的探针中按八探针方法插值获取辐射率。由于反射场的时间累积,它相当于在多帧内对出射方向分布进行了平均,自然实现了可变粗糙度的效果。粗糙度约0.2~0.5时效果最佳。
6.3 完全粗糙反射(Rough Reflections)
也就是纯粹的漫反射镜面:直接用反射向量去采样辐照度场(而不是反射场)。因为辐照度场已经进行了漫反射半球积分,其存储的辐照度值已经相当于在所有入射方向上按余弦加权平均,因此无论取哪个方向采样(只要用反射向量索引探针的八面体),返回的值都是该位置的近似漫反射光。这提供了几乎免费的完全粗糙反射。传统光线追踪需要成百上千条光线逼近粗糙反射,而SDFGI利用探针将这一积分提前完成了。
三个粗糙度级别通过线性插值融合:粗糙度0使用清晰追踪,粗糙度0.5混合清晰追踪和反射场,粗糙度1使用辐照度场。覆盖了从镜面到Lambert的全频谱。配合屏幕空间反射(SSR)填补小细节,能产生非常令人信服的反射效果。
7. 局限与未来展望
7.1 当前的主要局限
- 动态物体不参与光能传递:只能接收光,不能在环境中产生二次照明。房间里移动一个亮红色箱子并不会把墙壁染红。这限制了动态光照的真实感。
- 小发射体噪声:由于探针的5×5分辨率有限,手电筒、火苗等小光源可能在GI中出现闪烁或颗粒感,因为探针可能未能稳定采样到该小发射体。
- 网格分辨率限制:体素化会丢失细小几何体(如铁索、植被叶片),这些丢失的几何体无法遮挡或反弹光线,导致间接光不准确。
- 陡峭角度的背面漏光:虽然切比雪夫权重能极大抑制漏光,但在某些极端观察角度或探针恰好位于薄墙附近时,可能出现轻微的渗色。这需要通过bias调整和屏幕空间遮蔽来掩盖。
7.2 未来的改进方向
Juan Linietsky 在讲义中提到了若干改进计划:
- 个体SDF与卡片:类似Lumen的方式,为动态物体生成局部距离场,注入到探针更新射线中,让动态物体也能参与GI(至少作为遮挡或光源)。
- 动态遮挡盒子:支持门、移动墙体等作为简单的光线遮挡物,允许动态场景对GI做出遮挡反应。这可以用轴对齐包围盒近似,不需要完整SDF。
- 探针自适应偏移:当动态物体靠近探针时,探针位置可临时偏移,避免突然变暗;或采用淡入淡出遮挡强度,防止光照突变。
- 级联剔除动态体:为了性能,在上层级联中完全关闭动态物体的影响。
这些改进将使SDFGI更接近一个完备的实时GI解决方案,同时保持对低端硬件的友好性。Godot社区也在积极贡献相关PR。
8. 实践建议:调参与诊断
如果你在Godot项目中使用SDFGI(在Environment节点中勾选SDFGI),下面几个参数值得深入理解:
- Cascade Count / Cell Size:Cell Size 决定最精细级联的体素大小(米)。较小的值意味着近处精度更高,但覆盖范围变小,需要更多级联维持远距离覆盖。建议根据项目尺度调整:室内场景可设0.5
1m,室外可设24m。级联总数越大,远处间接光越好,但内存和更新时间增加。 - Bias / Chebyshev Bias:漏光时增加,但过大会导致应该照亮的区域变暗(探针被错误地判定为遮挡)。
Min Occupied Voxel参数也很关键,可避免细小物体产生漏光。 - Energy / Normal Bias:控制间接光强度和方向性。
Normal Bias影响探针插值时表面法线的影响程度。 - Temporal Blending 系数:控制探针收敛速度,增大可更快响应灯光变化,但可能引入瞬态噪点。
- Reflection Max Distance / Resolution:影响清晰反射追踪距离和精度。降低可提升性能。
- Use Lightmap with SDFGI:可与LightmapGI混合,Lightmap提供静态高精度GI,SDFGI处理动态物体和光源,互不干扰。
诊断技巧:
- 打开 Debug Draw -> SDFGI Probes 可视化探针颜色和遮蔽。若墙壁后方探针亮色,说明漏光。
- 查看 SDFGI Cascade 范围,了解当前级联覆盖区。
- 如果出现块状闪烁,可能是更新频率不足,可尝试减少
Min Occupied Voxel或增加更新预算。 - 在低端硬件上,可适当减少级联数,关闭清晰反射,或降低探针更新速率。
通过这些调参,你可以在不同硬件和场景规模下获得最佳的SDFGI表现。
9. 结语
SDFGI是面向真实世界实时渲染约束的一次精彩设计。它勇敢地放弃了硬件光线追踪的“标准答案”,转而用体素化+距离场+球体追踪重新实现了DDGI的核心流程。25线程的八面体探针、亚体素位场、切比雪夫遮挡、增量级联合并……这些细节组成了一套高效、易用、跨硬件的全局光照系统。
理解SDFGI不仅能让我们用好Godot,更能深刻体会到现代实时渲染中数据结构与增量更新的重要性。这套方案的很多思想,如超采样位场、Jump Flood传播、射线缓存,都可以迁移到其他GI或物理模拟系统中。