前言

DDGI (Dynamic Diffuse Global Illumination),名叫动态漫反射全局光照,这是由NVIDIA提出的一种全局光照技术,它结合光线追踪(Ray Tracing)辐照度探针(Irradiance Probes),用于解决动态场景中高质量、实时漫反射间接光的问题

为什么需要DDGI

DDGI 解决了传统探针的两大痛点:更新开销漏光

  • 传统探针
    • 大多需预计算,无法处理动态物体
    • 可见性问题导致严重穿墙,容易漏光,需手动放置大量屏蔽板

    下图天花板即是漏光

    下图阴影由门后的probe泄露

  • DDGI

    • 开销低:将3d纹理映射在2d纹理,每个探针的2d纹理仅有8*8和16*16的大小,每帧耗时几ms
    • Avoid leaks with visibility data + heuristics(使用可见性数据 + 启发式算法避免泄漏):借鉴VSM,计算像素到探针的距离,对比探针记录的平均距离和方差,利用切比雪夫不等式计算该像素被遮挡的概率。如果像素被墙挡住了,该探针的贡献度就会降为 0
    • Dynamic irradiance in 1-2M rays/frame(每帧 100万到200万 条光线)
    • Noiseless, incremental, infinite-bounce (无噪声、增量、无限反弹)
    • Forward, deferred, transparent, volumetric, etc.(支持前向、延迟、透明、体积光等渲染管线)
    • Eliminate surface parameterization/manual probe placement requirement(消除手动放置探针的要求)

但DDGI不是完美的,他只能计算漫反射,高光仍需SSR

DDGI算法

流程不复杂,但想一个一个全部实现是很复杂的,大致如下:

  • Ray Tracing & Shade:每个探针发射固定数量的均匀分布射线,使用上一次迭代结果shade:实现无限次反弹的全局光照

    通过八面体映射,将每个球面映射到2d贴图,得到一张irradiance、一张distance结果:

  • Blend:blend irradiance和depth到probe


    上图圆圈的黑体表示一个texel,黑体的黑色箭头是与texel关联的方向,该texel存储每个ray的加权平均,靠近texel的权重越大,反之权重越小

  • Border Fix:处理纹理边缘,防止采样时的分块感

  • Sample:基于3d坐标、可见性、normal 体积采样

实现流程

  • 定义尺寸

    尺寸参考nvidia官方

    • ProbeGridX x Y x Z 个探针

    • Ray Data Buffer :存储DXR计算的结果

    • 大小: ProbeCount * RaysPerProbe

    • Struct:float3 Radiance (颜色) 和 float Distance (距离)

    • Irradiance Texture:存储最终混合的GI

    • 每个探针大小:每个探针 6x6 像素,加上 1 像素边框(用于双线性插值),一共 8x8

      因为最终计算的需要双线性插值,到边缘是不对的(会采样到身旁的probe),需要在上下左右多增加1pixel,并使用Board寻址

    • :ProbeGrid.X * 8

    • :ProbeGrid.Y * ProbeGrid.Z * 8

    • Distance Texture:存probe到hit的distance数据 (R存distance的加权,G存distance平方的加权)

    • 每个探针大小:每个探针 14x14 像素,加边框共 16x16

    • :ProbeGrid.X * 16
    • :ProbeGrid.Y * ProbeGrid.Z * 16

Debug

  • 目的:确定探针在场景的位置

    先确定场景最大的BoundingBOX,在BoundingBOX中均分探针,得到BoundingBOX的起点(gridOrigin),及间隔(gridSpacing),随后在debug shader中计算每个探针的世界空间位置,在对应位置绘制圆球

  • 实现细节

    • 由于探针数量很多,这里使用20个面,60个索引的球体和实例化来减小debug下的性能消耗

    • gridOrigingridSpacingProbeGrid尺寸传递给shader,并结合SV_InstanceID计算世界空间坐标

    uint3 GetProbeGridCoord(uint probeIndex)
    {
        uint3 gridCoord;
        gridCoord.x = probeIndex % 16;
        gridCoord.y = (probeIndex / 16) % 4;
        gridCoord.z = probeIndex / (16 * 4);
        return gridCoord;
    }
    
    float3 GetProbeWorldPosition(uint probeIndex)
    {
        uint3 coord = GetProbeGridCoord(probeIndex);
        // 位置 = 起点 + 索引 * 步长
        return g_GridOrigin + (float3(coord) * g_GridSpacing);
    }
    
    o.positionWS = i.positionOS + o.probeCenterWS;
    
  • 效果

八面体映射

  • 目的:DDGI通过八面体映射将三维信息映射到二维坐标,从而减少内存消耗

  • 什么是八面体映射

    八面体映射(Octahedral Mapping)的过程是将一个球体投影到一个正八面体上,然后像切橘子皮一样把它摊平在一个正方形平面内

  • 为什么是八面体映射

    • 极高的均匀度:相比 UV 布局,八面体映射在整个正方形区域内的像素分布非常均匀
    • 完美的硬件过滤:在 2D 纹理中,八面体映射可以通过简单的Border Padding来处理纹理环绕,当着色器在 2D 纹理上进行双线性采样时,即使跨越了八面体的边缘,通过 Padding 补全的像素也能保证插值结果在数学上是连续的
    • 计算开销极低:只需要几行基础的代数运算(绝对值、加减法),不需要复杂的三角函数(如 sin, cos, atan2
  • 实现八面体映射
    float2 SignNotZero(float2 v)
    {
      return float2((v.x >= 0.0) ? +1.0 : -1.0, (v.y >= 0.0) ? +1.0 : -1.0);
    }
    
    // 3D dir normalize to [-1, 1]
    float2 OctEncode(float3 n)
    {
      n /= (abs(n.x) + abs(n.y) + abs(n.z));
      float2 result = n.xy;
      if (n.z < 0.0)
      {
          result = (1.0 - abs(result.yx)) * SignNotZero(result);
      }
      return result;
    }
    
    // [-1, 1] to normalized 3D dir
    float3 OctDecode(float2 f)
    {
      float3 n = float3(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));
      float t = saturate(-n.z);
      n.xy += (n.xy >= 0.0) ? -t : t;
      return normalize(n);
    }
    
  • 计算二维图上的坐标:光得到八面体uv坐标还不行,还没有确定指定probe在irradiance上的位置
    1. 归一化八面体uv
      float2 normalizedOctUV = octUV * 0.5 + 0.5;
      
    2. 确定probe的行列位置

      probe是 3D 的,但纹理是 2D 的。需要将一维的 probeIndex 展平成二维布局,从而找到probe在2D里是第几列第几行

      uint probeX = probeIndex % (uint)gridDims.x;
      uint probeY = probeIndex / (uint)gridDims.x;
      
    3. 计算probe块内的起始坐标

      由于2d图内的probe块有效信息是6x6,但上下左右需要多增加一个pixel,防止双线性插值得到的不准确的信息,因此(probeRes + 2.0)得到probe块的尺寸,+ 1.0避开最边缘的1pixel

      // probeRes = 6
      float2 probeTopLeft = float2(probeX, probeY) * (probeRes + 2.0) + 1.0;
      
    4. 根据起始坐标、八面体uv定位当前probe块计算的pixel
      float2 pixelPos = probeTopLeft + normalizedOctUV * probeRes;
      
    5. 转换到全局 UV 空间
      float2 atlasSize = float2(gridDims.x, gridDims.y * gridDims.z) * (probeRes + 2.0);
      pixelPos / atlasSize
      
  • 简单的测试

    简单的计算一下,让2d RT 图中每个坐标的颜色值都不同,并在debug中根据八面体的uv坐标、probe index计算2d图中的uv坐标,采样2d图

    下图第一张为2d图,第二张是采样2d图的效果

DXR BLAS TLAS

  • 目的:众所周知光追是一个非常耗时得过程,为了优化光追的性能消耗,DX12特意推出了DXR,用于加速快线追踪,其中的核心便是BLAS、TLAS

  • 什么是BLAS TLAS

    • BLAS (Bottom-Level Acceleration Structure):底层加速结构。一种加速结构,类似BVH
    • 存储位置:由于build BLAS非常耗时,只构建一次,因此一般存储于每个mesh
    • 存储数据:vertex、index
    • TLAS(Top-Level Acceleration Structure):顶层加速结构。TLAS基于BLAS build
    • 存储位置:由于每一帧都可以快速 build TLAS,因此存储于每个渲染instance
    • 存储数据:BLAS 的引用、world matrix、InstanceID、掩码(Mask)
  • 如何运作
    1. ray先与 TLAS 的包围盒求交,确定ray可能hit的instance
    2. 如果hit了某个instance,ray会通过该instance的“逆变换矩阵”转换到局部空间,然后与对应的 BLAS 求交,确定撞击到的三角形
  • 总结

特性 BLAS TLAS
存储内容 vertex、index instance id、world matrix、BLAS 的引用、Mask
更新频率 极低(底层mesh改变才更新) 极高(每帧更新,处理位移/旋转)
内存占用 较大(取决于模型复杂度) 较小(取决于场景instance个数)
构建开销 昂贵 廉价

Ray Tracing

  • 目的:记录与射线相交的pixel颜色、距离

  • 算法

    • 确定probe

    • 生成均匀分布的射线

    使用DXR自带的"raygeneration"shader,其中自带的TraceRay可以发出射线

    生成射线的方式使用 low discrepancy spherical Fibonacci,且每帧旋转避免走样

    • 判断射线是否相交

    • 相交:重心坐标插值,计算需要的Ray Data(radiance、distance),返回Ray Data

      注意:需要处理hit back face的情况,normal需要反向

      先考虑直接光,shadow、间接光后续加入

    • 不相交:返回无效信息(radiance为0、distance为一个特别大的值)

    • 将Ray Trace结果写入GBuffer

  • 如果将radiance、distance可视化为射线,射线本身的颜色表示radiance,射线长短表示distance;如果想看每个得到的Ray Tracing是否合理正确,可以实现一个类似unity probe的功能,不难得到如下结果:

Irradiance Map

  • 目的:为了后续采样probe且尽可能优化性能,需要将所有probe的Ray Data存储在一张Irradiance Map上,每个probe的Ray Data大小占6*6pixel(漫反射是低频信息)

  • 算法

    • 将3维探针组平铺到2维

    • Dispatch 的 Z大小是有限制的,X/Y **:通常最大支持 **65,535,但Z最大支持64

    • 易于计算probeIndex

    • 计算八面体uv

    • 由于每个probe的Ray Data大小有效空间只有6x6,左右上下各自多出的1pixel用于防止边缘双线性插值的错误结果,需要计算有效(只包含6x6)的oct(八面体)uv

    • 基于oct uv计算球面方向probeDirection

    • 计算irradiance

    • 既然是计算irradiance,那肯定是计算半球上的积分,又因为Ray Tracing的结果是稀疏的,需要进行平滑否则会有噪点,所以需要每个射线与probeDirection的权重,最后加权平均

      公式:E(probeDirection) = \int_Ω L(rayDir) * max(0, dot(probeDirection, rayDir)) drayDir

    • TAA

    因为irradiance计算在compute shader,可以无需额外pass执行时间滤波

  • 效果

Distance Map

  • 目的:与Irradiance map一样,只是存储的是Distance
  • 算法:大部分与Irradiance map相同,不同在于weight计算

采样Irradiance

  • 目的:将GI加入光照计算

  • 算法

    • 计算postionWS在探针网格中的坐标

    • 与周围八个probe(2x2x2)做trilinear

    • 为了方便计算将八个probe index[0-7]从一维变换到三维,对应三维空间下的probe index[(0,0,0),(1,1,1)]

      offset = int3(i, i >> 1, i >> 2) \& int3(1, 1, 1)

    • 计算每个probe的positionWS in 探针网格

      float3 biasPositionWS = positionWS + surfaceBias;
      
      float3 gridCoord = GetGridCoord(biasPositionWS, gridOrigin, gridSpacing);
      int3 baseProbeCoords = floor(gridCoord);
      float3 alpha = frac(gridCoord);
      
    • Warp Shading(Backface weight)

      由于采样点附近的8个probe,有些probe可能不该给采样点有贡献,如墙壁背后(会导致漏光),因此需要剔除与normal 角度相差大于180的

      公式:(dot(DirPointToProbe,NormalSurface)+1.0)×0.5

      完全正面——点积为 1,权重最大;侧面——点积为 0,依然保留一部分权重(约 0.5);完全背面——点积为 -1,权重降为 0

    • 计算trilinear weight

      int3 offset = int3(i, i >> 1, i >> 2) & int3(1, 1, 1);
      float3 trilinear = lerp(1.0 - alpha, alpha, (float3)offset);
      float trilinearWeight = trilinear.x * trilinear.y * trilinear.z;
      
      • offset:offset = (0, 0, 0) 代表左下前方的探针,offset = (1, 1, 1) 代表右上后方的探针
      • alpha:alpha越接近0,越偏向offset0的部分,反正越偏向offset1的部分
      • trilinear:之所以是lerp(1.0 - alpha, alpha, (float3)offset),当offset为0时,lerp得到1.0 - alpha,alpha越接近0,offset0的权重越大;当offset为1时,lerp得到alpha,alpha越接近0,offset0的权重越小,offset1权重越大
      • trilinearWeight:当前probe对周围八个probe的权重
    • 计算oct uv

      使用normalWS作为probe dir

    • 映射probe position WS->irradiance atlasPos

      uint2 atlasPos = uint2(adjCoords.x, adjCoords.y + adjCoords.z * gridDimensions.y);
      float2 octantCoordsIrr = OctEncode(normalWS);
      uv = (octantCoordsIrr * 0.5f + 0.5f) * (DDGI_PROBE_NUM_TEXELS - 2.f) + 1.0f;
      finalUV = (float2(atlasPos * DDGI_PROBE_NUM_TEXELS) + uv) * irradianceTexSize.zw;
      
    • 采样irradiance,加权平均
      float3 probeColor = SampleTexture2D_LOD(irradianceTexIndex, finalUV, linearClampSampler, 0).rgb;
      probeColor = pow(probeColor, exponent);
      
      sumIrradiance += probeColor * weight;
      sumWeight += weight;
      
      float3 finalIrradiance = sumIrradiance / sumWeight;
      finalIrradiance *= finalIrradiance;
      finalIrradiance *= TWO_PI;
      

切雪比夫计算遮挡

如上图所示,红色圆环表示probe,灰色表示geometry,绿色表示probe发出的光线,其中,probe存储\sum r、\sum r^2

最终计算sample时,用probe存储的距离信息,来计算方差σ

关于\frac{1}{n},可以提前到 ray trace时,相加r最后平均

LIMITATIONS & CONTINUING WORK

  • Low-frequency
    • 问题
    • DDGI 只能模拟变化平缓、大面积覆盖的间接光照,无法捕捉锐利的阴影或微小的反光细节

    • 解决

    • 这意味着AO、高光反射DDGI无法模拟,需要用屏幕空间ao、屏幕空间反射来实现,
  • Thin corridors & sharp corners

    • 问题
    • Thin corridors(狭窄的走廊):导致两个墙壁间没有任何probe,导致这里的光照看着不连续,很奇怪
    • sharp corners(尖锐死角):Shading Point周围8 个探针有可能分布在墙体的不同侧 。虽然Chebyshev Test可以剔除墙后的探针,但如果墙角过于尖锐,可能导致周围所有探针都被判定为不可见,从而产生黑块或光照断层

    • 解决

    • 对于Thin corridors & sharp corners切换SSGI shade,而SSGI用于大范围的光照
  • Probes just inside walls

    • 问题
    • 由于Grid是均匀分布的,某些probe可能会处于几何体内,从而导致Shadow Leak
      • 采集偏黑:probe处于几何体内,会导致ray trace hit的面都是背面,计算存储的irradiance偏黑
      • 插值出错:若插值位于几何体内的probe,会插值到错误的irradiance
    • 解决
    • Probe Relocation
      • 通过检测背面击中率找出几何体内的probe,将这些探针顺着射线反向推出墙体
    • Probe Classification
      • Probe Relocation不能让所有几何体内的probe都能推出墙体,对于这些probe,标上标记,后续shade、sample时完全跳过
  • Convergence rate (faster → more rays → lower hysteresis)
    • 问题
    • 由于 DDGI 使用了时间滤波,如果光照发生剧变,DDGI通常需要数帧时间才能更新完成 ,而不是瞬时的
    • 解决
    • 更多的ray:如果单帧的ray足够多,结果就越准确,就不再需要更多的时间滤波平滑画面

优化

Self-Shadow Bias & Backface Hit

  • Self-Shadow Bias
    • 问题:当在物体surface处查询Probe时,variance in the visibility estimate(可见性估计的方差)在分布均值附近最高——换句话说,就在surface,若方差越高会导致切雪比夫越大,从而使得遮挡率越高

    • 解决

    • 通过手动偏移 shade point position,使得r增大,从而计算的遮挡率降低

    • 公式:


      其中:

      n为normal

      ω_0为view dir

      如果probe grid是均匀的,MIN_DISTANCE_BETWEEN_PROBES为1;反之,MIN_DISTANCE_BETWEEN_PROBES取三个维度的min

      TunableShadowBias:一般为0.3(论文中提到该值大多数情况表现都比较好)

  • Backface Hit

    • 为了进一步减少漏光,击中背面的探针更新记录值的irradiance为0,且distance缩短 80\%缩短distance可以确保探针将背面视为被阴影遮挡,从而不照亮它们,但为什么不将distance设为0呢?
    • 设为0会使得切比雪夫权重趋向于 0,当权重归一化时,该值可能会被推得更高
    • probes that see some backfaces but are not stuck in walls (due to modeling idiosyncrasies) could have overly skewed average depths

Perception-based Exponential Encoding

  • 问题

    如果irradiance probe的收敛很慢,场景中剧烈的光照变化会导致明显的滞后,尤其是从亮到暗

  • 解决

    • 为irradiance应用基于感知的指数gamma编码:该编码以感知线性插值方式,加速由亮到暗的收敛

    由于人眼对暗部的感知更为敏感,直接在分量空间做线性混合会导致在变暗过程中能量下降过慢,感觉有残影;使用 x^{1/5} 编码后,能量在混合过程中的下降曲线会更符合人眼感知的线性度

    实验得出,gamma为5效果是最好的

    甚至可以减少低频闪烁,这是由fireflies(由于更新光线击中小且亮的irradiance,引起diffuse GI的明亮闪光)引起

    netIrradiance = pow(netIrradiance, 1.0f / g_DDGIEncodingGamma);
    
    for (int i = 0; i < 8; ++i):
        float3 probeColor = SampleTexture2D_LOD(irradianceTexIndex, finalUV, linearClampSampler, 0).rgb;
        probeColor = pow(probeColor, gamma * 0.5f);
    probeColor = Pow2(probeColor);
    

Fast Convergence Heuristics

  • 问题

    目前hysteresis是一个固定值,但对应不同的光照变化,固定的hysteresis不能满足需求

    比如,hysteresis很高0.97,GI效果很平滑没问题,如果灯光熄灭,场景亮度需要快速从亮到暗,但由于hysteresis很高,大多数数据是从历史帧获取的,导致收敛很慢;如果hysteresis很低,虽然收敛很快,但不好抑制噪点,且会影响可见性,导致漏光

  • 解决

    需要一个动态的hysteresis满足多种需求

    低阈值会检测量值超过最大值 25% 的变化,将滞后系数(hysteresis)降低 0.15。高阈值检测量值超过 80% 的变化,将滞后系数直接降低至 0.0,即完全不信任历史帧

    还可以针对光照或几何变化调整滞后系数:

    • 小型光照变化(手电筒打开):将辐照度滞后系数降低 15%,持续四帧
    • 大型光照变化(时间剧变):将辐照度滞后系数降低 50%,持续 10 帧
    • 大型物体变化(天花板塌陷):将辐照度滞后系数降低 50% 并持续 10 帧,同时将可见性滞后系数降低 50% 并持续 7 帧
    float significantChangeThreshold = 0.25f;
    float newDistributionChangeThreshold = 0.25f;
    
    float changeMagnitude = DDGIMaxComponent(netIrradiance - historyIrradiance);
    if (abs(changeMagnitude) > significantChangeThreshold)
    {
    hysteresis = max(0, hysteresis - 0.15f);
    }
    if (abs(changeMagnitude) > newDistributionChangeThreshold)
    {
    hysteresis = 0.f;
    }
    

    如果包含TAA,hysteresis可以更低

Probe-position Adjustment

  • 问题

    探针可见性信息可以防止被遮挡探针产生漏光和漏影,但也会导致某些探针处于完全遮挡状态,导致错误的渲染效果,尤其是DDGI是动态的,probe会跟着相机移动,不能预先手动摆放probe

  • 解决

    nvidia提出一种在静态几何体周围迭代地移动探针,以最大限度地增加有用探针的数量并生成良好的观察点的方法

    具体思路:

    • probe发射光线,记录光线是否打中背面

    • 使用backFaceCount记录打中背面的次数,并统计probe到背面的最近距离closestBackfaceDist、到正面的最近距离closestFrontfaceDist、到正面的最远距离farthestFrontfaceDist

    • 计算打中背面的次数,是否大于设定的阈值PROBE_BACKFACE_THRESHOLD,大于则认定该probe在墙内

    • 大于,则偏移当前probe到最近背面(将probe推向距离最近的面)

    • 将probe推向距离最近的面后,还需要将probe在向外推一些,和其他物体留一些距离,这个距离设定为PROBE_MIN_FRONTFACE_DISTANCE

    • 若probe到正面的最近距离closestFrontfaceDist小于PROBE_MIN_FRONTFACE_DISTANCE,则继续将probe向外推

      需要计算probe到最近正面的dir和到最远正面的dir,当这两个dir的夹角大于90°时,才允许移动(避免在狭窄的夹缝里,远离a面的同时,又撞进b面)

    • 若probe到正面的最近距离closestFrontfaceDist大于PROBE_MIN_FRONTFACE_DISTANCE,则继续将probe向正面推

Probe States

  • 问题

    即使对probe position进行了调整,某些probe依旧不会对irradiance有所贡献,这部分计算是无效的,应该跳过,问题在于如何高效且稳定地跳过

  • 解决

    nvidia使用了状态机的思想,引入了一套稳健的探针状态,以避免对这些探针进行光线追踪或更新,从而在保持相同视觉效果的同时提升性能

    将不应更新的探针与必须更新的探针区分开来,并增加了一个额外的中间状态,用于识别刚刚出现的探针(场景初始化、probe移动),并调整Hysteresis


    其中,\alpha 是当前帧的hysteresis;\alpha' 是场景的默认hysteresis

    • 对于静态几何体内部的probe设为“off” (never trace or update)

    • 处于静态几何体外部的probe也不是每帧都要更新,当probe附近一个probeSpacing内没有任何静态几何体,这种probe设为“Sleeping

    只有当probeSpacing内有静态几何体时,才唤醒,设为“Awake”

  • 算法

    1. 对于未初始化的probe,光线追踪需要五帧,用于确定最优的位置和初始化状态。在这个pass最后,所有先前未初始化的probe设为“Newly Vigilant,” “Off,” or “Sleeping”
    • 这一步可以直接使用静态物体的AABB来快速加速,因为可以直接根据AABB来调整probe,而无需依靠distance和backface的数据

      通过这种方法,许多探针可以快速设为“Newly Vigilant”,但光线追踪仍是需要的,来判定哪些探针应设为“Off”

    1. 所有动态物体的 AABB(轴对齐包围盒)扩大一个探针网格单元宽度加上自阴影偏移量,作为保守估计,位于动态物体扩大后的 AABB 内的所有“Sleep”探针设为“Newly Awake”

    2. (可选)。对于“Newly Vigilant”、“Newly Awake”状态的probe,使用大量光线并将hysteresis设为0,在一帧内对这些probes快速收敛,然后设为 “Vigilant”和“Awake"

    3. 如果Trace rays的对象是“Vigilant”、“Awake”的probe,则使用场景默认的hysteresis更新他们的数据

      如果省略第三步,这一步同样适合于“Newly Vigilant”、“Newly Awake”probe

  • 注意

    • 对于参与介质(Participating media,如雾),虽然它不是静态几何体,但仍需要设为“Awake”
  • 收益

    使用probe状态机可以带了30-50%的性能收益,probe可以发射更多的光线,且可以使用更低的hysteresis以快速收敛

    其中,“Baseline”为不使用探针休眠的耗时,“Equal Quality”、“Time Saved”为百分比和时间的节省,"Better Quality"为匹配Baseline的前提下,增加活跃探针的光线数量带来的画质提升

GPGPU

  • 目的

    通过使用通用 GPGPU,可以实现更快的更新,将传入的着色采样光线命中点存储在共享内存(Shared Memory/LDS),以便所有线程并行读取

Reference

DDGI: DYNAMIC DIFFUSE GLOBAL ILLUMINATION WITH RAY-TRACED IRRADIANCE FIELDS

Deep G-Buffers for Stable Global Illumination Approximation

Scaling Probe-Based Real-Time Dynamic Global Illumination for Production

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


他们曾如此骄傲的活过,贯彻始终