0%

1 概要

2 调用逻辑

文件 Reblur_DiffuseSpecular.hpp,在 nrd::InstanceImpl::Add_ReblurDiffuseSpecular 里创建好所有 render pass

每个 pass 的名字使用宏 PushPass("PostFix") 设置,最后得到 pass name = DENOISER_NAME - PostFix

AddDispatch 的第一个参数指定shader文件名,而这个shader文件只是充当一个组织文件,预定义一些宏,控制降噪走的分支, 实际的代码在其 include 的 shader 文件中。对于 REBLUR_DiffuseSpecular 会走 REBLUR_DIFFUSEREBLUR_SPECULAR 两个分支

reblur 配置 nrd::ReblurSettings m_ReblurSettings ,更新在 InstanceImpl::Update_Reblur

shader参数:

  • REBLUR_SHARED_CB_DATAInstanceImpl::AddSharedConstants_Reblur中更新
  • pass参数的更新在 InstanceImpl::Update_Reblur

3 渲染逻辑

image-20230820103719276-1694926428932-2

3.1 Classify tiles

识别需要降噪的tile

3.2 Pre-pass

准备 pre-blur 参数

3.2.1 depth-based bilateral weight

float2 wc :depth-based bilateral weight,使用左右两个像素viewZ0viewZ1与当前像素的viewZ的相对差异。对相对差异施加一个cut off,限制在[0, cut_off]范围内。当相对差异超过阈值 0.03 时,权重为 0;当相对差异<=0时,权重取 1;其余在 0~1 之间。

1
2
3
4
5
6
float2 viewZ01 = float2(viewZ0, viewZ1);
float2 x = abs(viewZ01 - viewZ) * rcp(max(abs(viewZ01), abs(viewZ))); // 当前像素与左右相邻像素的 viewZ 的相对差异
float cut_off = 0.03;
// 相当于把 x 限制在 [0, cut_off] 内,>= cut_off 为 0,<= 0 为 1
wc = saturate((x - cut_off) / (-cut_off)) = saturate(1 - x / cut_off);
wc *= 1.0 / max((wc.x + wc.y), 1e-15);

3.2.2 Checkboard模式处理

当为 RESOLUTION_HALF 时,checkerboard mode 为 CheckerboardMode::WHITE(2)、diffCheckerboard(1)、specCheckerboard(0);否则为 OFF(0),diffCheckerboard(2)、specCheckerboard(2)。当为半屏模式时,经过pre pass可以得到全屏结果。

  1. uint checkerboard :0/1 值,使用像素坐标与帧数得到,相邻像素交错,相邻帧交错

    1
    2
    3
    4
    5
    uint CheckerBoard( uint2 samplePos, uint frameIndex )
    {
    uint a = samplePos.x ^ samplePos.y;
    return ( a ^ frameIndex ) & 0x1;
    }

  2. int3 checkerboardPos :1/2 屏幕坐标(横坐标缩减一半)。x、z 取当前像素的左右相邻像素横坐标,y取当前像素坐标,最后横坐标右移一位

    1
    2
    int3 checkerboardPos = pixelPos.xyx + int3( -1, 0, 1 );
    checkerboardPos.xz >>= 1;
  3. checkboard 模式处理,如文件 NRDSettings.h 描述,当为半屏的 checkboard 模式时,noisy input在左半部分。hasData表示当前像素是否有有效数据,使用交错处理。

    1
    2
    3
    4
    5
    if(gDiffCheckerboard/*\gSpecCheckerboard*/ != 2){	// 半屏的交错模式
    hasData = checkerboard == gDiffCheckerboard/*\gSpecCheckerboard*/; // checkerboard 交错得到有效/无效数据
    pos.x >>= 1; // 1/2 屏幕
    }
    REBLUR_TYPE diff = gIn_Diff[pos]; /*REBLUR_TYPE spec = gIn_Spec[pos];*/

3.2.3 准备当前像素的数据

N(世界空间法线)、Nv(view space下的法线)、roughness、Vv (View space 下的view vector)、Xv (像素在view space下的position)

float4 rotator :每帧生成的向量,用于 Poisson 采样。REBLUR_PRE_BLUR_ROTATOR_MODENRD_FRAME ,这里 rotator 取的是 CPU 传来的。

3.2.4 执行 Pre-blur

diffuse 与 specular 有各自的 spatial filter,参考下一章节。

3.3 Temporal accumulation

3.3.1 Preload与数据准备

  1. 将 tile (GROUP_X + BORDER * 2) x (GROUP_Y + BORDER * 2) 的 normal hitdist 数据加载到 s_Normal_MinHitDist[GROUP_X+BORDER*2][GROUP_Y+BORDER*2] 中,减少后续重复访问 texture。

  2. 在当前像素的 (BORDER * 2) x (BORDER * 2) 区域计算Navg (averaged normal) 与hitDistForTracking (取最小)。

    注意:threadPos+BORDER 对应了当前像素在 s_Normal_MinHitDist 中的位置,因此循环中跳过了 (i== BORDER && j == BORDER)

    但是,Navg 的计算只包含了以当前像素为右下角的四个像素,而 hitDistForTracking 则遍历了以当前像素为中心的 3x3 区域。不懂这个设计

  3. 准备当前像素的数据 float3 N(世界空间法线),float materialIDXv(view space坐标), X(相机无平移变换时的世界空间坐标),roughness

3.3.2 变换

Temporal 处理时需要进行world、view、clip以及到上一帧的变换,比较特殊的一点,本阶段使用的变换将camera的平移都取消掉了。

常规的变换对应了 CommonSettings 中的 viewToClipMatrix()、worldToViewMatrix()等一系列矩阵,而传给shader的变换实际是 InstanceImpl 里的 m_ViewToClip()、m_WorldToView()、m_ViewToWorld()、m_WorldToClip()。将view space的平移整体取消,即 world 与 view 之间的变换的平移。而 world 到上一帧view 以及 view 到上一帧world 之间的变换采用相机的运动矢量,即相对偏移。

1
2
3
m_ViewToWorld.SetTranslation(ml::float3::Zero());
ml::float3 translationDelta = cameraPositionPrev - cameraPosition; // 指向上一帧的相机偏移
m_ViewToWorldPrev.SetTranslation(translationDelta);

上面一系列做法相当于永远将当前帧相机至于世界原点位置,因此 view 到 world 的变换只需要执行旋转变换。为了便于理解,后续描述也直接忽略相机平移。例如 Xv 是像素在view space下的坐标,而下面应用从view 到 world旋转得到的 X 称为像素的世界空间坐标。

1
float3 X = STL::Geometry::RotateVector(gViewToWorld, Xv);

因此,通过变换到三位空间得到的点,既是三维空间坐标,又是点到相机的向量。

3.3.3 计算视差

视差(parallax)是指比较两个观察方向(世界空间)的差距大小。观察方向是相机到着色点的方向,因此视差是针对某一着色点而言的。只有当相机发生了位置变化,才会产生视差。在相同的相机运动下,不同着色点具有不同的视差。因此计算视差要固定着色点,如下图所示,相机运动向量 \(\vec{c}\),运动前后的观察方向 \(\vec{v},\vec{v}_{prev}\),可以得到视差度量定义 \[ parallax = \tan\big(\arccos(\vec{v} \cdot \vec{v}_{prev})\big) \] image-20230821185416474

  • Xprev上一帧位置:处理animation带来的运动,计算着色点在上一帧的世界空间坐标 gIn_Mv应该是物体animation带来的motion vector(mv可能是世界空间下、也可能是uv空间下),测试例子里都是 0; gMvScale 取值 C++ 的 motionVectorScale,gIsWorldSpaceMotionEnabled = 0。

  • 世界空间下 (gIsWorldSpaceMotionEnabled ! = 0):Xpre += mv 得到上一帧的位置

    motionVectorScale = (1.0, 1.0, 0.0)

  • uv空间下:mv.xy 为 uv 的运动,uv.z 为深度的运动,(此时为 25D) motionVectorScale = (1.0/width, 1.0/height, 1.0)

    因此在上一帧的像素uv: smbPixelUv = pixelUv + mv.xy 与深度 (viewZ + mv.z)

    变换得到上一帧的世界坐标:Xprev = RotateVectorInverse(...) + gCameraDelta。RotateVectorInverse 只处理了相机旋转(应该是旋转矩阵的逆等于转置,避免求逆)。gCameraDelta 是指向上一帧的运动向量,而这里的变换都是以当前帧相机为世界原点,因此加上 gCameraDelta 最终得到上一帧的世界空间坐标。

    注意:Xprev 是着色点在上一帧的世界坐标,因此如果着色点没有动画,那么 Xprev 与着色点在当前帧的世界坐标应该一样

  • smbParallaxInPixels与上一帧之间的视差(像素为单位的距离)

    ComputeParallaxInPixels(Xprev - gCameraDelta, pixelUv, gWorldToClip, gRectSize )

    • gCameraDelta 即相机在世界空间指向上一帧的运动向量 prev - current。

      Xprev - gCameraDelta = Xprev + (-gCameraDelta):相当于保持相机位置不同,向着色点施加上一帧指向当前帧的相机平移运动

      再执行 gWorldToClip 变换得到当前相机下的 uv

    • 像素在上一帧的世界坐标 Xprev 施加了相机运动并变换到当前帧的uv,此时与 pixelUv 处于同一相机下,如上图右侧。因此可以计算二者之间的像素距离

3.3.4 历史数据重投影

时序累积是将当前帧与历史帧结合得到更稳定的结果。需要将当前帧重投影到上一帧,从历史数据中得到可靠的对应,并以一定权重将当前帧更新到历史帧中。为此,需要考虑:

  • disocclusion tracking:当前帧像素是否发生去遮挡,即新出现的像素。
  • accumulate speed:当前帧像素与历史帧像素结合的权重。

reblur的重投影结合了 surface motion based 与 virtual motion based(仅用于specular) 两种方法,下面先介绍surface motion

3.3.4.1 Surface motion based reprojection

(1)Disocclusion Tracking

前面已经通过重投影或者motion vector找到当前帧像素pixelUv对应上一帧像素smbPixelUv,通过比较当前帧与上一帧之间view z的差异是否超过阈值,如果超过则表示像素不匹配。此外,这个过程是基于Catmull-Rom filter与bilinear filter进行的,对于匹配的历史信息使用filter结果与当前帧进行混合。

  1. disocclusion threshold:输入 gDisocclusionThreshold,其在 C++ 上的数值计算如下

    1
    2
    float disocclusionThresholdBonus = (1.0f + m_JitterDelta) / float(rectH);
    float disocclusionThreshold = m_CommonSettings.disocclusionThreshold + disocclusionThresholdBonus; // 0.01 + 抖动值

    m_JitterDelta 是相邻两帧camera jitter的差值,camera jitter则是每帧的halton抖动值 [-0.5,0.5]

    先乘上 frustumSize 得到 disocclusionThresholdMulFrustumSize,基于NoV与视差调整阈值,视差越大,阈值越低

    1
    float smbDisocclusionThreshold = disocclusionThresholdMulFrustumSize / lerp(0.05 + 0.95 * NoV, 1.0, saturate(smbParallaxInPixels/30.0));

    上一帧uv是否在屏幕内,以及上一帧uv是否正面朝向。若不是,则 smbDisocclusionThreshold 取 -1,那么之后的occlusion判断都不通过

    • 是否正面朝向,通过上一帧与当前帧的法线夹角判断。因为当前帧肯定是正面朝向,如果法线夹角不超过一定值则视上一帧也是正面朝向。
      • 夹角cos阈值 frontFacing:lerp(cos(STL::Math::DegToRad(135.0)), cos(STL::Math::DegToRad(91.0)), saturate(2*smbParallaxInPixels-2))
      • 法线夹角采用当前帧/上一帧像素的bilinear区域法线均值:dot(prevNavg, Navg) > frontFacing;
  2. 在Catmull-Rom filter区域与bilinear filter区域进行occlusion判断,并生成occlusion weight。当catmull-rom区域不匹配时,则降为使用occlusion weight的bilinear区域。Catmull-Rom区域为4x4,中间2x2对应了bilinear区域,定义如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /*
    Gather => CatRom12 => Bilinear
    0x 0y 1x 1y 0y 1x
    0z 0w 1z 1w 0z 0w 1z 1w 0w 1z
    2x 2y 3x 3y 2x 2y 3x 3y 2y 3x
    2z 2w 3z 3w 2w 3z
    CatRom12 => Bilinear
    0x 1x
    0y 0z 1y 1z 0z 1y
    2x 2y 3x 3y 2y 3x
    2z 3z
    */
    struct CatmullRom
    {
    float2 origin;
    float2 weights[4];
    };
    • 通过GatherRed提取上一帧像素smbPixelUv的smbCatromFilter区域的view z数据。smbCatromGatherUv 为gather 0x的左上角,而 GatherRed 采样得到的是 bilinear 区域的4个像素,顺序如下

      image-20230829165239433

      通过 GatherRed 的以下偏移量取wzxy,正好对应上面 Gather 区域(注意首数字 0123 表明了屏幕坐标轴方向),有

      • (1, 1): smbViewZ0 = (0x, 0y, 0z, 0w)。取yzw,prevViewZ0 = (0y, 0z, 0w)

      • (3, 1): smbViewZ1 = (1x, 1y, 1z, 1w)。取xzw,prevViewZ1 = (1x, 1z, 1w)

      • (1, 3): smbViewZ2 = (2x, 2y, 2z, 2w)。取xyw,prevViewZ2 = (2x, 2y, 2w)

      • (3, 3): smbViewZ3 = (3x, 3y, 3z, 3w)。取xyz, prevViewZ3 = (3x, 3y, 3z)

    • 比较view z的差异判断Catmull-Rom区域的occlusion,并生成其中bilinear区域的occlusion weights,以及bilinear区域的可信度(用于之后的累积速度)。

      Xprev变换到上一帧的view space得到Xvprev.z(注意:这是当前帧像素 PixelUv 的着色点在上一帧坐标空间下的坐标)。如果差异绝对值超过 smbDisocclusionThreshold,则认为像素不匹配,取0;否则取1。最后可得到 smbOcclusion0、smbOcclusion1、smbOcclusion2、smbOcclusion3。

      • 计算bilinear区域的smbOcclusionWeights

        1
        float4 smbOcclusionWeights = GetBilinearCustomWeights(smbBilinearFilter, float4(smbOcclusion0.z, smbOcclusion1.y, smbOcclusion2.y, smbOcclusion3.x))
      • Catmull-Rom区域是否匹配,只有匹配时后续才会对历史信息进行 Catmull-Rom filter。

        1
        bool smbAllowCatRom = dot(smbOcclusion0 + smbOcclusion1 + smbOcclusion2 + smbOcclusion3, 1.0) > 11.5
      • bilinear区域的可信度 smbFootprintQuality:对 occlusion 进行 smbBilinearFilter 再开方得到,用以之后调整累积速度(越大越倾向于历史信息)。应对视角变化,对于可信度进行调整。例如相机斜对着表面(NoVPrev<1)变为正对着(NoV=1),那么历史信息相对当前帧可信度下降

        1
        2
        3
        4
        float sizeQuality = (NoVprev + 1e-3) / (NoV + 1e-3);
        sizeQuality *= sizeQuality;
        sizeQuality = lerp(0.1, 1.0, saturate(sizeQuality));
        smbFootprintQuality *= sizeQuality;
  3. 处理材质变化,如果材质ID不匹配,那么视为像素不匹配

    上一帧像素的bilinear区域得到的prevMaterialIDs,与当前像素materialID比较,得到 float4 materialCmps(相同为1,不同为0),bilinear区域的occlusion乘上 materialCmps 后再计算上述occlusion值,得到

    smbOcclusionWeightsWithMaterialID、smbAllowCatRomWithMaterialID、smbFootprintQualityWithMaterialID

  4. 上一帧像素 smbPixelUv 的 bilinear 区域 smbBilinearFilter

    使用 linear 采样法线得到上一帧 prevNavg,再转到当前帧世界坐标(gWorldPrevToWorld,本例为单位阵)

    收集 smbBilinearFilter 区域4个像素的 diffAccumSpeeds、specAccumSpeeds、prevMaterialIDs。

3.3 History fix

3.3.1 Preload与数据准备

与 temporal accumulation 阶段相同,将tile (GROUP_X + BORDER * 2) x (GROUP_Y + BORDER * 2) 加载到 float2 s_FrameNum[BUFFER_Y][BUFFER_X] 中,即累积的帧数(x为diffuse、y为specular)。本阶段的 BORDER 为 2。

提取当前像素数据:N(世界空间法线)、roughnessXv(view space坐标)、Nv(view space法线)

3.3.2 平滑累积帧数

从 preload 数据中获取当前像素的累积帧数 float2 frameNumUnclamped,当前像素在 preload 数据中的位置 int2 smemPos = threadPos + BORDER;

再使用 gHistoryFixFrameNum 进行归一化得到 normFrameNum

在当前像素的 (BORDER * 2 + 1) X (BORDER * 2 + 1) 区域,对归一化累积帧数超过当前像素的样本取平均,得到 normFrameNum。 权重为

1
float2 w = step(c, s);	// c 是当前像素归一化后的累计帧数,s是当前样本归一化后的累积帧数

最终得到

  • float2 scale = saturate(1.0 - normFrameNum);

    gHistoryFixFrameNum 作为累积帧数阈值,scale相当于累积帧数不足的比例,history fix会对scale超出一定阈值执行。

  • float2 frameNum = normFrameNum * gHistoryFixFrameNum;

4 Diffuse Denoise Pipeline

image-20230820112644427

5 Specular Denoise Pipeline

image-20230820112723742

5.1 Pre blur

5.1.1 一些默认参数

  1. 宏定义的默认参数 REBLUR_SPATIAL_MODE == REBLUR_PRE_BLUR REBLUR_PRE_BLUR_NON_LINEAR_ACCUM_SPEED 1/9 REBLUR_PRE_BLUR_FRACTION_SCALE 2.0
  2. CPU传入的可调参数 gSpecPrepassBlurRadius 默认 50.0 lobeAngleFraction 默认 0.15 roughnessFraction 默认 0.15 resolutionScale 默认 (1.0, 1.0) gPlaneDistSensitivity 默认 0.005 gMinRectDimMulUnproject : (float)ml::Min(rectW, rectH) * unproject

5.1.2 blur策略

  1. blur平面的选择:这里选用世界空间的blur平面,与 Reflected GGX-D 垂直,可以更多保留特征 image-20230820113124323image-20230820113146073

  2. blur半径的选择:由下图可以看出,反射物体离着色点越近,特征越明显,越远越模糊;此外越粗糙,specular lobe夹角越大,反射也会越模糊。因此反射距离越远、着色点越粗糙,对应越大的blur半径。

    也就是说,specular lobe与反射物体形成的锥形底面的直径越大,对应的blur半径越大。 image-20230820113322228image-20230820113351881

5.1.3 blur radius计算

  1. float hitDist :基于roughess与viewZ 进行一定缩放,roughness 越小 scale 越大。scale 定义如下

    1
    2
    3
    4
    5
    float _REBLUR_GetHitDistanceNormalization(float viewZ, float4 hitDistParams, float roughness = 1.0)
    {
    return (hitDistParams.x + abs(viewZ) * hitDistParams.y) * lerp(1.0, hitDistParams.z,
    saturate(exp2(hitDistParams.w * roughness * roughness)));
    }

    其中 gHitDistParams 定义如下

    1
    2
    3
    4
    5
    6
    7
    struct HitDistanceParameters
    {
    float A = 3.0f; // 来自 hitDistScale * meterToUnitsMultiplier,默认为 3.0 * 1.0。大概是米到单位值的变换
    float B = 0.1f; // (> 0) - viewZ based linear scale (1 m - 10 cm, 10 m - 1 m, 100 m - 10 m)
    float C = 20.0f; // (>= 1) - roughness based scale, use values > 1 to get bigger hit distance for low roughness
    float D = -25.0f; // (<= 0) - absolute value should be big enough to collapse "exp2(D * roughness ^ 2)" to "~0" for roughness = 1
    };

  2. lobeRadius :反射lobe形成的锥形底面的半径

    • Specular lobe的主方向 float4 Dv:(GGX Dominant Direction, lerp factor) 见附录 NoD 法线与 Dv 的夹角余弦

    • 计算lobe夹角正切 float lobeTanHalfAngle :根据roughness估算specular lobe的半角正切 [1](page 72)

    • 估算lobe半径大小

      1
      float lobeRadius = hitDist * NoD * lobeTanHalfAngle;    // 没看出有什么精确的几何变换,更像是一个近似模型

    转换为像素单位: 世界空间半径 / 一个像素对应的世界空间大小。像素对应的世界空间大小,与像素的深度有关,即投影到 viewZ 处的截面

    1
    2
    3
    4
    5
    6
    minBlurRadius = lobeRaidus / PixelRadiusToWorld(gUnproject, gOrthoMode, 1.0, viewZ + hitDist * Dv.w);
    // 将屏幕空间以像素为单位的半径投影回视锥体 viewZ 处的截面上的几何半径
    float PixelRadiusToWorld(float unproject, float orthoMode, float pixelRadius, float viewZ)
    {
    return pixelRadius * unproject * lerp( viewZ, 1.0, abs( orthoMode ) );
    }

    • gUnproject:1.0f / (0.5f * rectH * project[1]); project[1] 计算的是 \(\frac{1}{\tan\alpha_1}\) image-20230917160953598image-20230917161201304

      视锥体在 viewZ 处的截面高度为 \(2 * viewZ * \tan\alpha_1\),因此与屏幕高度的比值为 \[ \begin{align} & 2 * viewZ * \tan\alpha_1 * \frac{1}{rectH} = 2 * viewZ * \frac{1}{project[1]} * \frac{1}{rectH} \\ & = viewZ * \frac{1}{project[1] * 0.5 * rectH} \end{align} \] 因此屏幕空间像素乘上以上比值,可以投影回viewZ处,得到几何半径。

  3. blurRadius :使用一个 hitDistFactor 与 specular magic curve 缩放输入radius参数。不理解原理 :question: :confused:

    • float hitDistFactor = hitDist * NoD / frustumSize 并 clamp 到 0~1

      • float frustumSize:(2 / aspect) * viewZ,视锥体在 viewZ 处截面的高度 :confused: :question:

        1
        2
        3
        4
        float GetFrustumSize(float minRectDimMulUnproject, float orthoMode, float viewZ)
        {
        return minRectDimMulUnproject * lerp(viewZ, 1.0, abs(orthoMode));
        }

        gMinRectDimMulUnproject : (float)ml::Min(rectW, rectH) * unproject

        前述已经讲过 viewZ * unproject 为视锥体在 viewZ 处的截面高度与屏幕高度的比值,因此 frustumSize 描述的是 viewZ 处截面的高度

    1
    2
    3
    float blurRadius = gSpecPrepassBlurRadius;
    blurRadius *= hitDistFactor * smc;
    blurRadius = min( blurRadius, minBlurRadius );

5.1.4 权重参数计算

一些使用到的默认参数

1
2
3
4
5
float specNonLinearAccumSpeed = REBLUR_PRE_BLUR_NON_LINEAR_ACCUM_SPEED = 1/9;
gPlaneDistSensitivity = 0.005; // 用于调整几何权重影响,越大表示增大几何权重
gLobeAngleFraction = 0.15; // 用于调整法线权重影响,越大表示增大法线权重
gRoughnessFraction = 0.15; // 用于调整粗糙度权重影响,越大表示增大粗糙度权重
fractionScale = REBLUR_PRE_BLUR_FRACTION_SCALE = 2.0;

这里的各种权重设计都是基于平常见到的权重之上,再增加一个调控参数设计。对于每项权重,实现上先计算好其所需参数,再最后计算得到权重,这样实现更能够利用MAD指令。最后每项权重都会乘上各自的调控参数,而调控参数都与最后的权重呈反比,细节可查看 4.1.2.6 小节。

  1. float2 geometryWeightParams :用于计算基于平面距离的权重

    1
    2
    3
    4
    5
    6
    7
    8
    float2 GetGeometryWeightParams(float planeDistSensitivity, float frustumSize, float3 Xv, float3 Nv, 
    float nonLinearAccumSpeed)
    {
    float relaxation = lerp(1.0, 0.25, nonLinearAccumSpeed);
    float a = relaxation / (planeDistSensitivity * frustumSize);
    float b = -dot(Nv, Xv) * a;
    return float2(a, b);
    }

    \[ \begin{align} a_g &= \frac{lerp(1.0, \space 0.25,\space 1/9)}{g_{sensitivity} * frustumSize}\\ g_{params} &= \begin{pmatrix}a_g, & -a_g \cdot (N_v \cdot X_v)\end{pmatrix} \end{align} \tag{1}\label{geometry-weight-params} \]

  2. float normalWeightParams :用于计算基于法线夹角的权重。

    从下式可以看出,这里的设计从specular对法线朝向的敏感度出发,specular lobe angle越小,权重对法线夹角越敏感。

    1
    2
    3
    4
    5
    6
    7
    8
    float lobeAngleFractionScale = gLobeAngleFraction * fractionScale;	// fraction 参数
    float GetNormalWeightParams(float nonLinearAccumSpeed, float fraction, float roughness = 1.0)
    {
    float angle = STL::ImportanceSampling::GetSpecularLobeHalfAngle(roughness);
    angle *= lerp(saturate(fraction), 1.0, nonLinearAccumSpeed); // TODO: use as "percentOfVolume" instead?

    return 1.0 / max(angle, REBLUR_NORMAL_ULP); // REBLUR_NORMAL_ULP (2.0 / 255.0)
    }

    \[ \begin{align} a_n &= 1.0 / \Big( halfAngle * lerp(g_{lobeAngleF} * 2.0,\space 1.0,\space 1/9) \Big)\\ n_{params} &= a_n \end{align} \tag{2}\label{normal-weight-params} \]

  3. float2 hitDistanceWeightParams :基于反射距离差异的权重

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    float2 GetHitDistanceWeightParams(float hitDist, float nonLinearAccumSpeed, float roughness = 1.0)
    {
    // IMPORTANT: since this weight is exponential, 3% can lead to leaks from bright objects in reflections.
    // Even 1% is not enough in some cases, but using a lower value makes things even more fragile
    float smc = GetSpecMagicCurve2(roughness);
    float norm = lerp(NRD_EPS, 1.0, min(nonLinearAccumSpeed, smc)); // NRD_EPS 1e-6
    float a = 1.0 / norm;
    float b = hitDist * a;
    return float2(a, -b);
    }

    \[ \begin{align} a_h &= 1.0 / \Big( lerp(1\times 10^{-6}, \space 1.0, \space \min(1/9, smc) \Big)\\ h_{params} &= \begin{pmatrix} a_h, & -a_h\cdot hitDist \end{pmatrix} \end{align} \tag{3}\label{hit-weight-params} \]

    根据 \(\eqref{hit-weight}\) 可知,超参 \(a_h\) 越大,基于反射距离的权重越小

  4. float2 roughnessWeightParams :用于计算基于roughness差异的计算

    1
    2
    3
    4
    5
    6
    7
    float roughnessFractionScale = gRoughnessFraction * fractionScale;	// fraction 参数
    float2 GetRoughnessWeightParams(float roughness, float fraction)
    {
    float a = rcp(lerp(0.01, 1.0, saturate(roughness * fraction)));
    float b = roughness * a;
    return float2(a, -b);
    }

    \[ \begin{align} a_r &= 1.0 / \Big(lerp(0.01, \space 1.0,\space roughness * g_{roughness} * 2.0)\Big)\\ r_{params} &= \begin{pmatrix} a_r, & -a_r \cdot roughness \end{pmatrix} \end{align} \tag{4}\label{roughness-weight-params} \]

5.1.5 Sampling Space

  1. TB坐标系轴

    1
    float2x3 TvBv = GetKernelBasis(Dv.xyz, Nv, NoD, roughness, specNonLinearAccumSpeed);

  2. blurRadius 转为世界空间下的 worldRadius,对TB进行缩放 TvBv[0] *= worldRadius; TvBv[1] *= worldRadius;

5.1.6 Poisson Sampling

  1. 当前样本的 uv 偏移量:泊松样本生成二维向量,再乘上 blurRadius * texelSize

    1
    float2 uv = pixelUv + STL::Geometry::RotateVector( rotator, offset.xy ) * gInvScreenSize * blurRadius;

    offset:(x, y) 是单位圆盘内的点坐标,z 是点距圆心的距离

  2. 如果是checkboard模式(gSpecCheckerboard != 2),当前采样样本是否有有效数据,如果没有则向左或向右偏移一个,表示取相邻像素数据

  3. 采样当前样本的 viewZ 与 spec,以及得到当前样本在view space的坐标 Xvs

  4. 计算当前样本的权重,为下面几项的乘积

    • 比较当前样本与像素的材质ID,材质ID不同,返回权重 0,相同返回 1。

    • gaussian权重:当前样本在单位泊松盘中距圆心的距离 offset.z,代入权重exp(-0.66 * r * r)

    • 使用计算好的权重参数计算 combinded weight:包括基于平面距离的权重、基于法线夹角的权重、基于粗糙度差异的权重,三项乘积

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      float GetCombinedWeight(
      float2 geometryWeightParams, float3 Nv, float3 Xvs,
      float normalWeightParams, float3 N, float4 Ns, // Ns = (sample normal, sample roughness), N 当前像素的世界空间法线
      float2 roughnessWeightParams = 0
      ) {
      float3 a = float3(geometryWeightParams.x, normalWeightParams, roughnessWeightParams.x);
      float3 b = float3(geometryWeightParams.y, 0.0, roughnessWeightParams.y);

      float3 t;
      t.x = dot(Nv, Xvs);
      t.y = STL::Math::AcosApprox(saturate(dot(N, Ns.xyz)));
      t.z = Ns.w;

      float3 w = _ComputeWeight(t, a, b);
      return w.x * w.y * w.z;
      }
      // _ComputeWeight
      #define _ComputeNonExponentialWeight(x, px, py) \
      STL::Math::SmoothStep(0.999, 0.001, abs((x) * (px) + (py)))

      根据 \(\eqref{geometry-weight-params}\)\(\eqref{normal-weight-params}\)\(\eqref{roughness-weight-params}\) 可简化上述代码 \[ \begin{align} a &= \begin{pmatrix} a_g, & a_n, & a_r \end{pmatrix} \\ b &= \begin{pmatrix} -a_g \cdot (N_v \cdot X_v), & 0, & -a_r\cdot roughness \end{pmatrix} \\ t &= \begin{pmatrix} N_v \cdot X_{vs}, & \arccos(N\cdot N_s), & r_s\end{pmatrix} \\ X = t * a + b &= \begin{pmatrix} a_g \cdot (N_v \cdot X_{vs} - N_v\cdot X_v), &a_n \cdot \arccos(N\cdot N_s), &a_r\cdot (r_s - roughness)\end{pmatrix} \end{align} \tag{5} \label{combined-weight} \] STL::Math::SmoothStep 的定义如下

      1
      2
      3
      4
      #define _SmoothStep01(x) 							(x * x * (3.0 - 2.0 * x))	// 相比y=x,在0,1两端更平缓
      #define _LinearStep(a, b, x) saturate((x - a) / (b - a))
      float3 SmoothStep01(float3 x) { return _SmoothStep01(saturate(x)); }
      float3 SmoothStep(float3 a, float3 b, float3 x) { x = _LinearStep(a, b, x); return _SmoothStep01(x); }

      \[ x = saturate\left(\frac{x-a}{b-a}\right) \\ w=\_SmoothStep01(x) = x^2 \cdot (3 - 2x) \]

      _LinearStep 中 a<b,则得到的是反比关系。因此 \(X\) 由 0.001 ~ 0.999 递增,w 由 1 到 0递减。当 \(X<=0.001\) 时,w = 1;当 \(X >= 0.999\) 时,w = 0

      _SmoothStep01 得到的是在0,1两端更平缓的效果,图像如下所示

      image-20230820201305538

      \(\eqref{combined-weight}\) 式计算的每项权重的意义:

      • \(a_g \cdot (N_v \cdot X_{vs} - N_v\cdot X_v)\) :小括号里是样本到当前像素平面的距离,基于平面距离的几何权重,距离越小对应权重越大

      • \(a_n \cdot \arccos(N\cdot N_s)\):样本法线与当前像素法线的夹角,夹角越小对应权重越大

      • \(a_r\cdot (r_s - roughness)\):样本粗糙度与当前像素粗糙度的差值(后面有取绝对值),差值越小对应权重越大

      因此这些权重的设计都是基于平常所见到的设计,但在此之上还有复杂超参设计,即 \(a_g,a_n,a_r\)

    • 基于反射距离的权重:使用一个最小权重到1.0之间的插值,lerp(minHitDistWeight, 1.0, ...)

      • 最小权重:float minHitDistWeight

        1
        float minHitDistWeight = REBLUR_HIT_DIST_MIN_WEIGHT * fractionScale;	// REBLUR_HIT_DIST_MIN_WEIGHT 0.1
      • 基于反射距离差异的权重作为插值权重

        1
        2
        3
        4
        5
        6
        #define _ComputeExponentialWeight(x, px, py) \
        ExpApprox(-NRD_EXP_WEIGHT_DEFAULT_SCALE * abs((x) * (px) + (py))) // NRD_EXP_WEIGHT_DEFAULT_SCALE 3.0
        float GetHitDistanceWeight(float2 params, float hitDist) // 样本的反射距离
        {
        return _ComputeExponentialWeight(hitDist, params.x, params.y);
        }

        代入 \(\eqref{hit-weight-params}\) 定义的 params,有 \[ \exp\Big(-3 \cdot abs(s_{hitDist} \cdot a_h - a_h \cdot hitDist)\Big) = \exp\Big(-3 \cdot abs\big(a_h\cdot (s_{hitDist}-hitDist)\big)\Big) \tag{6} \label{hit-weight} \]

    • 处理样本距反射物距离影响,距离越近,反射越shaper。但为什么roughness越大,权重越大:question::confused:

      1
      2
      3
      4
      float d = length(Xvs - Xv); // 当前样本到像素的距离
      float h = ExtractHitDist(s) * hitDistScale; // roughness weight will handle the rest,当前样本的反射距离
      float t = h / (hitDist + d); // hitDist 为当前像素的反射距离
      w *= lerp(saturate(t), 1.0, STL::Math::LinearStep(0.5, 1.0, roughness));

  5. 样本speular的加权平均 如果checkboard模式最终带来无效数据,则使用当前像素的左右相邻像素、进行bilateral filter

5.2 Temporal Accumulation

5.2.1 估算沿运动方向的 curvature

5.2.1.1 着色点的运动方向

将相机的运动转为着色点的运动 Xprev - gCameraDelta(将上一帧的着色点施加上一帧相机到当前帧的运动),再变换到当前帧 screen uv空间,得到运动后的着色点的 motionUv。因此,uv空间的运动方向 cameraMotion2d 计算如下

1
cameraMotion2d = normalize((motionUV-pixelUV) * gRectSize) * gInvRectSize;	// 转为像素单位标准化,再转为uv单位

注意:这里的 cameraMotion2d 不只是相机的运动矢量,如果着色点具有动画,由于 Xprev 是动画前的世界坐标,因此还有动画带来的运动

接下来选择两个运动方向上的视差点,基于这两点以及着色点的法线、位置以及观察方向来计算曲率:

  • 在运动方向上走一个单位得到一个低 parallax 点

    float2 uv = pixelUv + cameraMotion2d * 0.99;

  • 在运动方向上走前述计算的视差距离个单位

    float2 uvHigh = pixelUv + cameraMotion2d * smbParallaxInPixels;

5.2.1.2 Low parallax

像素的bilinear区域以及描述定义如下

image-20230822111025809

Bilinear f的定义与计算如下,origin 是 bilinear 2x2 区域起始坐标(像素单位),weights是距 origin 像素中心的偏移量

1
2
3
4
5
6
7
8
9
10
11
12
struct Bilinear
{
float2 origin; // Bilinar 2x2 区域起点
float2 weights; // 插值权重
};
Bilinear GetBilinearFilter(float2 uv, float2 texSize) {
float2 t = uv * texSize - 0.5;
Bilinear result;
result.origin = floor(t);
result.weights = t - result.origin;
return result;
}
  1. 当前像素的bilinear区域考虑了小的视差,所位于的bilinear区域大多在Preload的数据 s_Normal_MinHitDist 中。存储位置计算如下

    1
    2
    3
    // threadPos+BORDER为当前像素在tile数据中的位置,int2(f.origin)-pixelPos为bilinear区域相对于当前像素的偏移量
    int2 pos = threadPos + BORDER + int2(f.origin) - pixelPos;
    pos = clamp(pos, 0, int2(BUFFER_X, BUFFER_Y) - 2);
  2. Bilinear filter后的法线 n,2x2区域的4个像素的法线为 n00 (pos + (0, 0))、n10 (pos + (1, 0))、n01 (pos + (0, 1))、n11 (pos + (1, 1)) bilinear计算如下,两个水平方向的插值,再加上一个垂直方向的插值

    1
    lerp(lerp(s00, s10, f.weights.x), lerp(s01, s11, f.weights.x), f.weights.y);

5.2.1.3 High parallax

与 low parallax 计算normal不同的是 uvHigh 的视差较大,因此其bilinear区域大多不在preload tile数据内,从贴图中采样并执行 bilinear filter,得到 nHigh

同时bilinear filter uvHigh 的 view space深度得到zHigh。计算 zHigh 与当前着色点 viewZ 之间的相对误差

1
float zError = abs(zHigh - viewZ) * rcp(max(zHigh, viewZ));

如果相对误差 zError < 0.1,则选择high parallax的 uvHigh 与 nHigh;否则,选择 low parallax 的 uv 与 n。

5.2.1.4 计算 curvature

使用当前像素的 X(世界坐标)、N(法线)、Navg 以及所选parallax 点的 v(世界空间观察方向)、n(世界空间法线)计算这两点曲率 :question:

5.2.5 累积速度更新

本例中 gSpecMaterialMask = 0,因此 specOcclusionWeights 采用前面计算的不带材质比较的 smbOcclusionWeights;specHistoryConfidence 采用 smbFootprintQuality,即smbPixelUv的bilinear区域的可信度。

历史帧smbPixelUv的bilinear区域的累积速度 specAccumSpeeds,使用specOcclusionWeights得到加权平均specAccumSpeed。根据bilinear区域的可信度调整历史信息的累积帧数:如果 confidence = 1,累积帧数不变;如果confidence < 1,累积帧数减小,则混合权重更倾向于当前帧。

1
2
3
// specAccumSpeed *= ((specAccumSpeed * confidence + 1) / (1 + specAccumSpeed));
specAccumSpeed *= lerp(specHistoryConfidence, 1.0, 1.0 / ( 1.0 + specAccumSpeed)); // +1 避免除 0
specAccumSpeed = min(specAccumSpeed, gMaxAccumulatedFrameNum); // fast history下 gMaxAccumulatedFrameNum = 5,否则 30

注意:累积速度在保存时除以最大累积帧数 REBLUR_MAX_ACCUM_FRAME_NUM(63),在提取时再乘上最大累积帧数。因此计算过程中,表示的是历史帧信息的累积帧数

5.2.6 Virtual motion based reprojection

image-20230903135344354

对于反射的重投影,反射的世界具有自己的运动,例如反射点具有动画,而着色点与相机是静止的,这时着色点的反射也发生了运动。而反射点的运动常常使用虚拟反射点来追踪,如上图所示的镜面反射的虚拟反射点可以通过在着色点处,沿着相机到着色点的方向延长反射距离得到,计算如下

1
2
float3 Xvirtual = X - V * hitDist;
float2 pixelUvVirtualPrev = GetScreenUv(gWorldToClipPrev, Xvirtual);

但这种方法只对镜面反射有效,而实际的glossy反射,虚拟反射点会更加接近表面。

  1. 计算虚拟反射点 Xvirtual

    • 对preload阶段得到的当前像素2x2区域最小反射距离进行一定scale hitDistForTracking *= hitDistScale

    • 计算着色点处的GGX Dominant Direction float4 D,D.w 是 n 到 r(镜面反射方向) 的插值,roughness增大会导致specular lobe主方向偏向法线。

    • 虚拟反射点计算如下:question:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      float ApplyThinLensEquation( float NoV, float hitDist, float curvature )
      { // https://www.geeksforgeeks.org/sign-convention-for-spherical-mirrors/
      float hitDistFocused = hitDist / (2.0 * curvature * hitDist * NoV + 1.0);
      return hitDistFocused;
      }
      float3 GetXvirtual(float NoV, float hitDist, float curvature, float3 X, float3 Xprev, float3 V, float dominantFactor)
      {
      float hitDistFocused = ApplyThinLensEquation(NoV, hitDist, curvature);
      float closenessToSurface = saturate(abs(hitDistFocused) / (hitDist + NRD_EPS));

      return lerp(Xprev, X, closenessToSurface * dominantFactor) - V * hitDistFocused * dominantFactor;
      }

      Xvritual 变换到上一帧screen uv空间,得到 vmbPixelUv

    • 使用虚拟反射点与着色点在上一帧的像素距离 vmbPixelsTraveled 表达virtual motion

      1
      2
      float2 vmbDelta = vmbPixelUv - smbPixelUv;
      float vmbPixelsTraveled = length(vmbDelta * gRectSize);
    • 根据virtual motion对curvature进行调整,再重新计算上述虚拟反射点相关变量,例如 Xvirtual、vmbPixelUv、vmbDelta、vmbPixelsTraveled。

      1
      2
      float curvatureCorrection = float(vmbPixelsTraveled < 3.0 * smbParallaxInPixels + gInvRectSize.x);
      curvature *= curvatureCorrection;
  2. occlusion判断:虚拟反射点在上一帧的vmbPixelUv,对应bilinear区域vmbBilinearFilter ,计算该区域的occlusion。

    • 获取上一帧数据:

      • vmbViewZs:vmbBilinearFilter区域的view z
      • vmbVv:vmbPixelUv像素取view z = 1得到的view space坐标,指向vmbPixelUv像素并且z=1的view space下的向量
      • Nvprev:当前帧像素的 Navg转到上一帧view space
    • 使用到着色点平面的距离的差异来评估occlusion

      • 上一帧bilinear区域到着色点平面的距离计算如下

        1
        2
        3
        float4 NoX = (Nvprev.x * vmbVv.x + Nvprev.y * vmbVv.y) * (gOrthoMode == 0 ? vmbViewZs : gOrthoMode) + Nvprev.z * vmbVv.z * vmbViewZs;
        // gOrthoMode = 0
        NoX = vmbViewZs * dot(Nvprev, vmbVv);

        由于 vmbVv 是 vmbPixelUv像素在 view z = 1 的点,通过变换可以得到 vmbViewZs * vmbVv 则是bilinear区域的四个点(这里忽略了像素点的不同)。因此NoX计算的是bilinear区域4个点到着色点平面法线的距离(这里的平面方程常数项为0)

      • 着色点在上一帧的平面方程常数项:float NoXreal = dot(Navg, X - gCameraDelta);

        由于 Navg 位于世界空间,因此到上一帧世界空间不需要变换

        X - gCameraDelta:着色点世界坐标变换到上一帧的世界空间。

        • 注意,正常情况下是不需要此变换的,因为世界空间是绝对的,但由于前面所述,这里的变换都取消掉了当前帧的相机平移,也就是以当前帧相机为原点。

        • 简单推导:gCameraDelta是指向上一帧的相机运动向量,X是当前帧着色点的世界坐标,又是(着色点->相机)的向量。因此 X-gCameraDelta为(上一帧相机->着色点)的向量。

        • dot(Navg, x-gCameraDelta) = ||Navg|| * ||x-gCameraDelta|| * cos = ||x-gCameraDelta|| * cos

          相当于将 Navg与X转到上一帧的view space 下,再求平面方程常数项。保持与 NoX 的坐标空间一致。

      • float4 vmbPlaneDist = abs(NoX-NoXreal):计算的是虚拟反射点在上一帧对应的bilinear区域到着色点平面的距离。

    • float4 vmbOcclusion = step(vmbPlaneDist, disocclusionThresholdMulFrustumSize);

      之后就和surface motion计算类似

      • float4 vmbOcclusionWeights:bilinear权重施加vmbOcclusion 得到

      • 是否可以使用 catrom filter:

        1
        2
        bool vmbAllowCatRom = dot( vmbOcclusion, 1.0 ) > 3.5 && REBLUR_USE_CATROM_FOR_VIRTUAL_MOTION_IN_TA;
        vmbAllowCatRom = vmbAllowCatRom && specAllowCatRom;
      • bilinear可信度 vmbFootprintQuality: 对 vmbOcclusion 执行 bilinear filter 再开方得到

5.2.6.1 累积速度更新

使用 vmbBilinearGatherUv 获取上一帧 vmbBilinearFilter 区域的累积速度,使用vmbOcclusionWeights加权平均得到 vmbSpecAccumSpeed,再使用 vmbBilinearFilter 区域的可信度调整累积速度

1
2
// vmbSpecAccumSpeed *= ((vmbSpecAccumSpeed * confidence + 1) / (1 + vmbSpecAccumSpeed));
vmbSpecAccumSpeed *= lerp(vmbFootprintQuality, 1.0, 1.0 / (1.0 + vmbSpecAccumSpeed));

与surface motion不同的是,最大帧数进行了如下限制,本例中 gResponsiveAccumulationRoughnessThreshold=0,因此无作用

1
2
3
4
5
6
7
8
9
10
float GetResponsiveAccumulationAmount(float roughness)	
{ // 当 roughness 大于阈值时为0,roughness越小,返回值越大。但 gResponsiveAccumulationRoughnessThreshold 为 0,返回值恒为0
float amount = 1.0 - (roughness + NRD_EPS) / (gResponsiveAccumulationRoughnessThreshold + NRD_EPS);
return STL::Math::SmoothStep01(amount);
}
float responsiveAccumulationAmount = GetResponsiveAccumulationAmount(roughness);
responsiveAccumulationAmount = lerp(1.0, GetSpecMagicCurve(roughness), responsiveAccumulationAmount);

float vmbMaxFrameNum = gMaxAccumulatedFrameNum * responsiveAccumulationAmount;
vmbSpecAccumSpeed = min(vmbSpecAccumSpeed, vmbMaxFrameNum);

5.2.6.2 Surface 与 Virtual 之间的混合权重

virtualHistoryAmount,用于混合 virtual-motion based 与 surface-motion based重投影。

  • 初始为 GGX-D 的 lerpFactor

    1
    float virtualHistoryAmount = IsInScreen(vmbPixelUv) * D.w;
  • virtual motion的bilinear区域可信度 vmbFootprintQuality

    1
    virtualHistoryAmount *= saturate(vmbFootprintQuality / 0.5);
  • normal:根据着色点法线 N与虚拟反射点法线vmbN、virtual motion走过的弧度 angle 得到 virtualHistoryNormalBasedConfidence

    1
    virtualHistoryAmount *= lerp(1.0 - saturate(vmbPixelsTraveled), 1.0, virtualHistoryNormalBasedConfidence)
  • back-facing: 虚拟反射点法线 vmbN 与着色点的平均法线 Navg。

    1
    virtualHistoryAmount *= float(dot(vmbN, Navg) > 0.0)
  • roughness: virtualHistoryRoughnessBasedConfidence

    1
    virtualHistoryAmount *= lerp(1.0 - saturate(vmbPixelsTraveled), 1.0, virtualHistoryRoughnessBasedConfidence)

  • 在virtual motion方向上的少量像素累积roughness权重 wr

    1
    2
    virtualHistoryAmount *= 0.1 + wr * 0.9;
    virtualHistoryRoughnessBasedConfidence *= wr;

5.2.7 混合

5.2.7.1 历史数据filter

specAllowCatRom == true时,使用 catrom filter;否则使用 specOcclusionWeights 进行bilinear filter,得到着色点在上一帧的specular历史filter结果 smbSpecHistory, smbSpecFastHistory

同理,当 vmbAllowCatRom == true时,使用 catrom filter;否则使用 vmbOcclusionWeights 进行bilinear filter,得到虚拟反射点在上一帧的 specular 历史filter结果vmbSpecHistory, vmbSpecFastHistory

SpecHistory 是 float4(radiance, dist),SpecFastHistory 是 float2(luma, hitDistForTrackingPrev)

5.2.7.2 virtual history confidence

用于历史radiance clamp,以及控制累积速度

  • virtual parallax difference:使用 SpecFastHistory 的 hitDist 得到虚拟反射点 XvirtualPrev,变换到上一帧的sreen uv空间得到 vmbPixelUvPrev。得到与 vmbPixelUv的像素距离 deltaParallaxInPixels

    1
    2
    // lobeRadiusInPixels 着色点处反射lobe半径
    float virtualHistoryConfidence = STL::Math::SmoothStep(lobeRadiusInPixels + 0.25, 0.0, deltaParallaxInPixels);
  • 在virtual motion方向上的少量像素累积roughness权重 wr、normal权重 w1

    1
    virtualHistoryConfidence *= isInScreen ? w1 : 1.0;

5.2.7.3 累积速度与混合权重更新

vmbSpecAccumSpeed *= virtualHistoryConfidence;

surface motion based:smbSpecAccumSpeed

由 virtualHistoryAmount 混合得到最终的 specAccumSpeed

1
2
virtualHistoryAmount *= saturate(vmbSpecAccumSpeed / (smbSpecAccumSpeed + NRD_EPS)); // gNonReferenceAccumulation = 1
specAccumSpeed = lerp( smbSpecAccumSpeed, vmbSpecAccumSpeed, virtualHistoryAmount );

5.2.7.4 Specular 混合

  • 混合速度选用 float specNonLinearAccumSpeed = 1.0 / (1.0 + specAccumSpeed);

    当checkboard模式下,当前无有效数据时,specNonLinearAccumSpeed *= lerp(1.0 - gCheckerboardResolveAccumSpeed, 1.0, specNonLinearAccumSpeed);

  • 混合surface motion 与 virtual motion的历史specular:specHistory = lerp(smbSpecHistory, vmbSpecHistory, virtualHistoryAmount);

  • 混合历史specular与当前specular 得到最终累积结果 specResult

    1
    2
    3
    4
    5
    6
    7
    8
    specResult = MixHistoryAndCurrent(specHistory, spec, specNonLinearAccumSpeed, roughnessModified);
    float4 MixHistoryAndCurrent(float4 history, float4 current, float f, float roughness = 1.0)
    {
    float4 r;
    r.xyz = lerp(history.xyz, current.xyz, f);
    r.w = lerp(history.w, current.w, max(f, GetMinAllowedLimitForHitDistNonLinearAccumSpeed(roughness)));
    return r;
    }
  • specular混合结果的 hit dist 与 lum矫正(Anti-firefly suppressor)

    1
    2
    3
    // REBLUR_FIREFLY_SUPPRESSOR_RADIUS_SCALE 0.1, gBlurRadius 15
    float specAntifireflyFactor = specAccumSpeed * gBlurRadius * REBLUR_FIREFLY_SUPPRESSOR_RADIUS_SCALE * smc;
    specAntifireflyFactor /= 1.0 + specAntifireflyFactor;

    virtual motion 与 surface motion 的混合结果 与 最终时序累积结果之间的混合

  • fast history混合

  • 计算误差 GetColorErrorForAdaptiveRadiusScale,之后blur基于此调整blur radius

5.3 History Fix

5.3.1 Preload

从 fast history 加载 lum 到 float s_SpecLuma[BUFFER_Y][BUFFER_X]

5.3.2 History Reconstruction

当累积帧数相对于 gHistoryFixFrameNum 小于 (1-REBLUR_HISTORY_FIX_THRESHOLD_1) 比例时 (0.111),执行 history reconstruction。

  1. 采样步长 scale.y(像素单位):与前面计算相同,得到specular lobe的 lobeRadius(世界空间),再转到屏幕空间得到 minBlurRadius

    1
    2
    // gHistoryFixStrideBetweenSamples = 14.0,frameNum 是 gHistoryFixFrameNum 范围内的平滑帧数
    scale.y = min(gHistoryFixStrideBetweenSamples / (2.0 + frameNum.y), minBlurRadius / 2.0);
  2. 权重参数:normal、geometry、roughness 与之前计算相同

  3. 在 [-2, 2] x [-2, 2] 区域进行加权平均specular得到 spec

    • 每个样本为 float2 uv = pixelUv + float2(i, j) * gInvRectSize * scale.y;
    • 使用权重参数计算每个样本的权重
  4. 在以当前像素为中心的 (BORDER * 2 + 1) x (BORDER * 2 + 1) 区域计算 luma 一阶矩specM1、二阶矩specM2

    每个样本的luma数据都在shared data s_SpecLuma

  5. 如果开启了 antiFirefly ,则在 [-4, 4] x [-4, 4] 区域计算 luma 的一阶矩 m1、二阶矩 m2,限制filter结果spec的luma specLuma

    clamp(specLuma, m1-sigma, m1+sigma),sigma为标准差

  6. 使用 specM1、specM2以及当前像素的lum specCenter 对 specLuma 进行 clamp 得到 specLumaClamped

    1
    2
    3
    float specMin = min(specM1 - specSigma, specCenter);
    float specMax = max(specM1 + specSigma, specCenter);
    float specLumaClamped = clamp(specLuma, specMin, specMax);
  7. specLumaClamped 到 specLuma 之间的插值得到最终的 specLuma,插值权重由累积帧数决定

1
2
specLuma = lerp(specLumaClamped, specLuma, 
1.0 / (1.0 + float(gMaxFastAccumulatedFrameNum < gMaxAccumulatedFrameNum) * frameNumUnclamped.y));

对filter结果ChangeLuma得到最终输出 spec = ChangeLuma(spec, specLuma);

Appendix

1 GGX Dominant Direction

Off-Specular 现象:通常都认为 BRDF lobe 是以镜面反射方向为中心,但由于光源方向与 shadow-masking 项,在 roughness 增大时,BRDF lobe 会朝着法线方向偏移,称为 Off-specular peak。如下图所示 [1](page 69)

image-20230820113637853

为了模拟这种变化,引入一个参数 lerpFactor 来得到法线到镜面反射方向之间的dominant direction,即

1
lerp(N, R, lerpFactor);

这个 lerpFactor 使用 roughness、NoV来建模。在reblur的实现中, float4 GetSpecularDominantDirection 返回 (dominant direction, lerpFactor)

Reference

[1] https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf

条款一:模板类型推导

对于下面一段模板伪代码:

1
2
template<typename T>
void f(ParamType param); // ParamType

ParamType 是模板形参类型,常带有修辞词(const)或引用声明。接下来将要谈论的几种情况:

  • 值传递:ParamTypeT
  • 引用传递:
    • 左值引用:ParamTypeT&T const&
    • 万能引用:ParamTypeT&&
  • 指针传递:ParamTypeT*

类型推导规则

  1. 无论是上述哪种情况,引用类型的实参都会被当作非引用类型处理

    这一点与普通函数调用类似,实参是否为引用类型不会影响到形参是否为引用

  2. 值传递模板类型:实参的 const/volatile 会被去掉

    这一点与普通函数调用类似,值传递形参只是实参的副本,const/volatile 修饰对于值传递没有意义。

  3. 引用传递模板类型:

      1. 左值引用:遵循第1点,实参自带的引用类型会被忽略。
      • 1 const引用模板类型:实参的const会被忽略
      • 2 非const引用模板类型:实参的const会被模板类型保留,这一点与普通函数调用不同
      1. 万能引用:会根据实参属于左值还是右值推导模板类型
      • 1 左值实参:模板类型会被解释为左值引用,剩下的推导过程与 3.a.1 相同
      • 2 右值实参:模板类型会被解释为右值引用。右值实参没有const而言了(因为是右值)
  4. 指针模板类型:与左值引用推导规则类型

    1. 会取消掉实参的指针类型

示例

1 Summary

本文介绍屏幕空间反射中最重要的一步——基于 hierarchy depth buffer(HZB) 的 ray march算法,主要参考了 [1] 中的实现。

ray march算法的目的是为了根据屏幕空间深度得到光线在屏幕上的交点,相比于世界空间的光追,这是一种开销很低但仅局限于屏幕空间的做法,也就是若光线交于相机看不到的表面,那么屏幕空间ray march则无法得到求交结果。而 hierarchy depth buffer 则提供了四叉树的加速结构,每一级mipmap都取上一级四个深度中的closest depth。假如光线在 closest depth 的表面之上(距相机更近),那么可以光线也不会与更精细层级相交,因此可以快速行进不会相交的区域。

2 hierarchy depth buffer

hierarchy depth buffer(HZB) 是构建depth buffer的mipmap形式,但与API接口提供的mipmap构建方式(取均值)不同,hierarchy ray march需要每一层级取前一层级四个深度中的closest depth,具体取最小或最大由depth实现决定。本文采用 \([0,1]\) 的depth,越大距相机越远,因此取最小深度。

3 Construct Ray

对于屏幕空间,往往已知光线起点像素坐标 fragCoord,以及基于需要采样的光线方向 rayDir (位于世界/相机空间,本文采样相机空间)。ray march 过程在屏幕空间进行,同时为了方便在不同mip层级行进,需要得到screen uv space(即 \([0,1]\times [0,1]\times [0,1]\))下的光线起点,以及屏幕空间的光线方向。

(fragCoord, depth) 变换到view space得到 viewRayPos,沿光线方向行进一个单位得到 nextRayPos = viewRayPos + rayDir,最后将二者都变换到screen uv space,得到 originnextRayScreenPos,光线方向 direction = nextRayScreenPos - origin,注意不需要normalize,因为这并不是三维空间的方向。

4 Ray March

给定光线起点 origin 与光线方向 direction,都位于 screen uv space。在光线行进过程中,会反复执行一下步骤:

  • 先行进 linear step,即在当前层级行进一个像素。执行基于深度的求交判断,有:

    • 如果光线从当前像素在表面上(更靠近相机)掠过,表示无交点,那么去更高层级(表示更大步长)。

    • 如果光线与当前像素相交,表示可能相交,那么去更低层级(表示更小步长)

4.1 Linear March

行进一个步长是希望沿着光线方向行进到下一个像素,同时下个像素内执行一个偏移,如图 Fig-1 所示。

image-20230812143337171

Fig-1 一次Linear March的示意图

行进步长 marchStep 可设置为

1
vec2 marchStep = mix(vec2(0.0), vec2(1.0), greaterThanEqual(direction.xy, vec2(0.0)));

像素内部偏移量 uvOffset 以像素大小为单位,可设置为

1
2
vec2 uvOffset = 0.005 * exp2(baseLevel) * (1.0 / screenResolution);	// baseLevel 表示 ray march 执行的最低层级
uvOffset = mix(-uvOffset, uvOffset, greaterThanEqual(direction.xy, vec2(0.0)));

假设当前光线位置 currentRayCoord 以及当前层级分辨率 currentMipResolution,linear march的执行如下

1
2
3
4
5
6
vec2 LinearMarch(vec2 currentRayCoord, vec2 currentMipResolutionInv)	// currentMipResolutionInv 为倒数
{
vec2 newRayCoord = floor(currentRayCoord) + marchStep;
newRayUV = newRayCoord * currentMipResolutionInv + uvOffset;
return newRayUV;
}

在进行求交之前,光线需要先行进一步,避免与自身所在像素自相交,行进到下一个像素的boundary plane处,有

1
2
3
4
5
6
7
8
9
void InitRay(vec3 origin, vec3 direction, vec3 directionInv, vec2 currentRayCoord, 
vec2 currentMipResolutionInv, out vec3 position, out float t)
{
vec2 xyPlane = LinearMarch(currentRayCoord, currentMipResolutionInv);
// o + d * t = p' => t = (p' - o) * (1 / d)
vec2 tXy = xyPlane * directionInv.xy - origin.xy * directionInv.xy;
t = min(tXy.x, tXy.y);
position = origin + t * direction;
}

4.2 Intersection

每次迭代都需要进行判断是否与将要行进到的像素有交点,以此来决定下一步应该如何执行。如何判断光线与将要行进像素是否相交?

假设光线当前位置为 positton,将要行进位置为 new position,如图 Fig-1 所示。new position 处的 (x, y, surfaceZ) (surfaceZ为该处像素的深度) 形成了一个bounding box的一半,即相邻的三个面组成的boundary,可以看到 Fig-1 中,俯视近平面所展示的 x/y plane 形成的黄色 boundary plane。从侧面看近平面如图 Fig-2 所示,相机位于上方,两条黄色 boundary line交点处即为 new position。

image-20230812194326749
Fig-2 从侧面看近平面

对于一个像素而言,其内部深度都是相同的,光线是否会与即将行进的像素相交,通过查看两个端点 position与new position即可:

  • 光线当前位置 position 与表面的关系有:position.z < surfaceZ,光线在表面之上;否则,光线在表面之下,可能与像素相交
  • 求光线与new position形成的boundary plane (x, y, surfaceZ)的最近交点,如果没有与z plane相交,则表示光线可以从new position上方掠过

因此,对于光线当前位置 position 的两种情况而言:

  • position.z < surfaceZ:光线在表面之上,不会与position所在像素相交。
      1. 光线与 z plane 相交,表示光线与下一个像素相交
      1. 否则,即与 x/y plane 相交,表示光线与下一个像素也不相交
    1. 否则,光线在表面之下,与position所在像素相交

为了能够使用 hierarchy depth buffer 加快速度,应该有:

  • 如果本次迭代的行进有可能相交,则去更细粒度层级,即使用更小行进步长。例如上述情况 a,光线可以行进到 z plane 交点位置;c,光线位置不变,等待更细粒度层级测试
  • 如果不会相交,则去更粗粒度层级,即使用更大行进步长。例如上述情况 b,光线可以行进到 x/y plane 的交点位置。

上述迭代一直进行到没有更细粒度层级可用时,表示已找到可能交点;否则,达到迭代次数上限后,表示无法找到交点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// surfaceZ: depth at currentRayCoord.xy
bool AdvanceRay(vec3 origin, vec3 direction, vec2 currentRayCoord, float surfaceZ. vec2 currentMipResolutionInv, inout vec3 position, inout float t)
{
// new position 处的 boundary plane
vec2 xyPlane = LinearMarch(currentRayCoord, currentMipResolutionInv);
vec3 boundaryPlane = vec3(xyPlane, surfaceZ);
// o + d * t = p' => t = (p' - o) * (1 / d)
vec3 tXyz = boundaryPlane * directionInv - origin * directionInv;
// 特殊情况,// Prevent using z plane when shooting out of the depth buffer.
tXyz.z = direction.z > 0 ? tXyz.z : SSR_FLOAT_MAX;
float tMin = min(min(tXyz.x, tXyz.y), tXyz.z);
bool isAboveSurface = position.z < surfaceZ;
bool skipTile = tMin != tXyz.z && isAboveSurface; // 是否为情况 b
t = isAboveSurface ? tMin : t;
position = origin + t * direction;
return skipTile;
}

每次迭代后,调整层级

1
2
3
4
bool skipTile = AdvanceRay(...);
currentLevel += skipTile ? 1 : -1;
currentMipResolution *= skipTile ? 0.5 : 2.0;
currentMipResolutionInv *= skipTile ? 2.0 : 0.5;

4.2.1 特殊情况 :confused: :question:

上面描述都是以光线方向背离相机为例,当光线方向朝向相机时,需要特殊注意:

  • position.z >= surfaceZ:与前述过程没有不同
  • position.z < surfaceZ:此时不能使用 z plane 的交点,因为得到的

4.2.2 线性查找

前面讲到,迭代到没有更细粒度层级时,表示找到交点,退出迭代。但如果是超出最大层级时呢?一种做法是,可以看作无法找到交点,但如果想要查找更充分,此时可以降为在最大层级线性查找过程,迭代后层级调整实现修改如下

1
2
3
4
5
6
7
8
bool skipTile = AdvanceRay(...);
int prevLevel = currentLevel;
currentLevel += skipTile ? 1 : -1;
currentLevel = min(currentLevel, LevelCount - 1);
if (currentLevel != prevLevel) { // 只有层级发生改变时
currentMipResolution *= skipTile ? 0.5 : 2.0;
currentMipResolutionInv *= skipTile ? 2.0 : 0.5;
}

4.2.3 交点是否有效

前述讲的求交测试都是基于深度信息进行的,如果光线走到了像素背后,则表示相交。但实际上,像素占据了表面一定面积,而该小部分表面并非与近平面平行或者并非是平坦的,因此所在表面存在一定厚度,有可能光线从表面背后穿过去,只有在像素厚度范围内的光线,才算是相交。因此,在得到交点后,还需要进行验证是否有效。

提取交点 position 处的深度值 surfaceZ,分别将 position.zsurfaceZ 变换到线性空间(相机/世界空间),得到 rayDepthhitDepth,有

1
2
3
4
5
6
bool TestThickness(float rayZ, float hitZ)
{
float rayDepth = Linearize(rayZ);
float hitDepth = Linearize(hitZ);
return abs(rayDepth - hitDepth) < (relativeThickness * max(abs(hitDepth), 1e-5));
}

4.3 Put All Together

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
vec3 HierarchyRayMarch(vec3 origin, vec3 direction, int maxIteration, 
float relativeThickness, out bool isValidHit)
{
// 一些参数初始化
const vec3 directionInv = 1.0 / direction;
const int baseLevel = 0; // 0 means full resolution depth
int currentLevel = baseLevel;
vec2 currentMipResolution = GetMipResolution(screenResolution, currentLevel);
vec2 currentMipResolutionInv = 1.0 / currentMipResolution;
// 设置 Linear March 步长
vec2 marchStep = mix(vec2(0.0), vec2(1.0), greaterThanEqual(direction.xy, vec2(0.0)));
vec2 uvOffset = 0.005 * exp2(baseLevel) * (1.0 / screenResolution); // baseLevel 表示 ray march 执行的最低层级
uvOffset = mix(-uvOffset, uvOffset, greaterThanEqual(direction.xy, vec2(0.0)));
vec3 position;
float t;
// 光线先行进一个单位,避免与自身像素相交
InitRay(origin, direction, currentMipResolution * origin.xy, currentMipResolutionInv, position, t);
int i = 0;
while (i < maxIteration && currentLevel >= baseLevel && InsideScreen(position.xy)) {
vec2 currentRayCoord = currentMipResolution * position.xy;
float surfaceZ = texelFetch(hiDepthTexArray[currentLevel], ivec2(currentRayCoord), 0).r;
bool skipTile = AdvanceRay(origin, direction, currentRayCoord, surfaceZ,
currentMipResolutionInv, position, t);
int prevLevel = currentLevel;
currentLevel += skipTile ? 1 : -1;
currentLevel = min(currentLevel, LevelCount - 1);
if (currentLevel != prevLevel) { // 只有层级发生改变时
currentMipResolution *= skipTile ? 0.5 : 2.0;
currentMipResolutionInv *= skipTile ? 2.0 : 0.5;
}
++i;
}

// 验证交点是否有效
isValidHit = false;
if (currentLevel < baseLevel && InsideScreen(position.xy)) {
vec2 currentRayCoord = screenResolution * position.xy;
float hitZ = texelFetch(hiDepthTexArray[0], ivec2(currentRayCoord), 0).r;
isValidHit = TestThickness(position.z, hitZ);
}

return position;
}

5 Reflection

最后简要介绍下屏幕空间反射的反射部分。屏幕空间反射希望得到金属材质的着色点的反射信息,首先需要在着色点处根据材质信息,重要性采样得到反射光线的方向。以着色点为起点、反射光线的方向组成的光线,在屏幕空间执行ray march,找到的交点即为反射点。接下来,SSR直接将上一帧反射点的颜色(不包含反射)作为反射点向着色点发出的radiance。注意:这是一种简单的近似,并非PBR过程,因为屏幕上的颜色实际上应该是屏幕像素向相机发出的radiance,这里直接近似作为其向着色点的radiance。 

Reference

[1] https://github.com/GPUOpen-Effects/FidelityFX-SSSR

1 Summary

传统的光栅化管线着色(forward/deferred)是基于屏幕空间像素的着色过程,由于光栅化流水线作业的限制,shading rate完全由分辨率、几何复杂度、帧率控制。而本文提出的texel shading管线将着色转化为texture space的任务,并将着色结果cache在texture space,最终的着色通过采样cache完成。

基于texture space的着色将shading rate与分辨率、几何复杂度、帧率解耦,可以每像素调整shading rate,变得更加可控。同时texture space下对于时空复用类算法更加友好。概况地讲,texture space shading(TSS)的提出主要有如下动机:

  1. 可控的表面像素采样率。对于远处高光的小物体,相机的运动会导致非常不稳定的效果。例如,从正对着表面运动到grazing视角,表面变得更加高频,而采样率不足。TSS的着色受相机视角影响较小。
  2. 更加准确的帧间复用。不同的观察位置、角度下,相邻帧之间的表面像素的不匹配程度较高,例如可能处于不同的mip level。传统光栅化管线在时序上的复用难以考虑这一点,而TSS着色结果cache在texture space,时序连续性较高。
  3. 更加准确的空间复用。屏幕空间的像素之间暴露出incoherent性质,这对于空间复用很不友好。而texture space下,相邻像素往往满足世界空间的coherent。

2 Pipeline设计

texel shading的前提是使用某种参数化方式将场景的mesh统一到唯一的texture space下,即每一个surface point都具有唯一的texture坐标。这种参数化方式在TSS技术中也是一大问题,本文未做这方面的详细调研。texel shading管线共包含三个阶段:

  1. Shade Queuing:使用一个geometry pass来标记需要着色的texel tiles(8x8),每个texel tile作为着色任务。再新增着色任务前会先查询cache中是否已存在。
  2. Shade Evaluation:compute shader中提取着色任务需要的顶点属性,并生成着色结果到cache texture中。
  3. Shade Gathering:再执行一次geometry pass,从cache texture中收集着色结果,得到最终屏幕着色。

2.1 预处理

为了能够在compute shader中得到几何信息,还需要在预处理阶段生成triangle index texture,即texture space下纹理坐标到triangle ID的映射。

实现设计:对整个texture space分辨率进行一次光栅化即可得到triangle index texture。

2.2 Shade Queuing

本阶段通过geometry pass为每个像素生成texel单位的shade任务,并选择合适的mip level。通过一张cache texture来缓存每个8x8 tile的状态,例如最近更新的帧数,可以控制tile的更新频率。8x8 tile的shade任务生成算法如下:

image-20230703182339279

算法:geometry pass生成的texel任务(i, j, L)转为 8x8 tile任务(i/8. j/8, L-3)。一个L-3级别上的8x8 tile可以计算得到一个L的texel。

这里可以看出控制shading rate的方式有:

  • tile的更新频率阈值(多少帧更新一次)
  • texel的mip level越大,意味着越少数量的texel,一个texel可以覆盖多个像素,这适用于低频区域。

实现设计

  • geometry pass需要对带纹理坐标的几何进行光栅化,得到每个像素的纹理坐标、以及重心坐标。
  • cache texture:从算法实现上看,cache texture应该是N-3层级的贴图,给定shade texel(i, j, L),其在cache中的坐标为(i/8, j/8, L-3)。cache texture的texel是对应tile的状态信息:
    • 上一次更新的帧数
    • bias配置:提高mip level来减少shade texel
  • 如果在本阶段的geometry pass中计算的mip level向上取整,一个8x8 tile的着色可以在Shade Gathering阶段做到三线性插值。

2.3 Shade Evaluation

本阶段执行于compute shader,每个thread group执行一个8x8 tile。通过texel的纹理坐标从triangle index texture得到当前triangle ID,并根据此ID得到triangle数据。使用texel的重心坐标进行插值得到texel的顶点数据。之后便可以按照传统渲染的方式对texel着色。

2.4 Shade Gathering

此时,cache中已经有了屏幕着色需要的所有数据。再执行一次geometry pass得到对应level的texel着色。

1 Summary

本文基于object-space/texture-space shading管线,提出一种面向multi-viewers的云渲染的cache系统设计。对于effect中与视角无关的部分,通过转换为texture-space下的渲染任务,并且设计effect-based cache系统,使得不同viewers共享同一份effect中间数据。提出的effect-based cache系统共包含定义在物体表面的On-Surface Cache(OSC)与定义在grid上的World-Space Cache(WSC)两种策略,统一使用hash table的形式进行存储管理,但二者具有不同的hash key设计,包含实际数据的cache entry由具体的effect定义。作者为两种cache策略分别给出相应的具体effect应用,同时提出通过为不同的effect使用不同的bias配置来为低频effect选择低分辨率,减少内存开销。

2 Background

3 Pipeline设计

管线设计如图 Fig-1,首先为每个viewer执行visibility pass来决定那些cache entries会被使用到;然后对所有viewers的cache request进行整合更新,这一步可以执行在不同的GPU上;之后viewers再通过采样cache来着色帧;最后是后处理,将生成的图像插入到output stream。

image-20230703130901243

Fig-1 Cloud Native Rendering Pipeline

想要被多个viewer复用,被cache的计算只能是视角无关的,同时作者提出基于effect的cache设计,可以为不同类型的渲染计算进行调整。经过对现代游戏引擎中的效果分析,作者提出两种类型的cache:on-surface cache,共享表面点上的计算;world-space cache,共享世界空间数据。

3.1 On-Surface Caches

On-Surface Cache(OSC)采用类似于[1]的管线。首先预处理阶段需要为mesh生成唯一表示的纹理空间。作者通过将一定数量的相连接的面片划分成island单位,每个island有独立的texture space,可以生成triangle lookup texture,即每个texel的triangle id。

对于cache texture的实现,尽管目前硬件支持virtual texture,但64KB的page大小对于本系统过大,因此作者采用软件实现的cache系统。本文采用的cache entity大小为8x8 texel block,texel的实际大小由具体effect定义。cache的访问采用hash形式,为此作者提出设计用于访问所有virtual textures的单个hash map。hash map中存储的是指向cache entity(实际数据)的hash entity,由如下部分组成:

  • island index:32bit,用来标识island,island下是独立的texture space
  • resolution:4bit,应该是mipmap level
  • texel block coordinates:2x12bit,island下的8x8 texel block的纹理坐标。 因此可以最多表示 32kx32k的纹理空间(\(2^{12}*8=2^{15}=32k\)

OSC pipeline主要由如图Fig-2中的五个阶段组成:

  1. Visibility:决定哪些cache entity会被用到以及去重cache request
  2. Cache Update Queuing:填充着色任务队列
  3. Triangle Lookup Rendering:
  4. Effect Update:cache更新
  5. Compositing:整合cache数据以及剩余的渲染计算生成最终渲染结果

image-20230704143603816
Fig-2 OSC Pipeline

3.1.1 Visibility

为每个viewer执行visibility pass生成所请求的cache entity,此外可以根据屏幕空间梯度以及每个effect的先验知识来调整cache entity的bias,提高目标mip level。例如空间低频的effect可以渲染的目标分辨率更低。

为每个texel block配置一个64bit mask,来标识block内哪些像素需要更新。对于每个surface point可能需要1、4、8个texel blocks,分别用于point、bilinear、trilinear采样。对于每个texel block都会查询hash table来判断是否已经map,如果没有map,则map一个空block。同时,每帧重置texel mask。通过这种方式,可以去除重复请求。

不是很清楚mip level如何工作的?似乎是降低的是texture space的分辨率,例如level 0的分辨率32kx32k,level 1的分辨率则为16kx16k。

unbiased情况是通过屏幕空间梯度计算的mip level达到pixel与texel一一对应;biased情况是手动为某些低频effect增大mip level,多个pixel对应一个texel。

3.1.2 Cache Update Queuing

本阶段的更新以texel block为单位进行,为每个效果创建一个cache update queue,用于存储texel block的更新请求。

3.2 World-Space Caches

Reference

[1] K. E. Hillesland and J. C. Yang. 2016. Texel Shading. In Proceedings of the 37th Annual Conference of the European Association for Computer Graphics: Short Papers (Lisbon, Portugal) (EG ’16). Eurographics Association, Goslar, DEU, 73–76.

1 Summary

传统的光栅化管线(forward或者deferred)执行于屏幕空间,表面采样率或者像素密度与相机距离、视角相关。同时,GPU光栅化管线的流水线作业使得shading rate完全由分辨率、几何复杂度、帧率控制。而texture space shading(TSS)提出在texture space着色,使得shading rate变得更加可控。同时texture space下的texel着色具有更优的spatial coherent以及temporal coherent,这对于时空复用类算法更加友好。

概况地讲,TSS的提出主要有如下动机:

  1. 可控的表面像素采样率。对于远处高光的小物体,相机的运动会导致非常不稳定的效果。例如,从正对着表面运动到grazing视角,表面变得更加高频,而采样率不足。TSS的着色受相机视角影响较小。
  2. 更加准确的帧间复用。不同的观察位置、角度下,相邻帧之间的表面像素的不匹配程度较高,例如可能处于不同的mip level。传统光栅化管线在时序上的复用难以考虑这一点,而TSS着色结果cache在texture space,时序连续性较高。
  3. 更加准确的空间复用。屏幕空间的像素之间暴露出incoherent性质,这对于空间复用很不友好。而texture space下,相邻像素往往满足世界空间的coherent。

2 Category

本文按照 [1] 给出的分类方法描述TSS的相关工作,以及设想中的大致实现思路。

image-20230628191733254

Reference

[1] Neff, T., Mueller, J.H., Steinberger, M. and Schmalstieg, D. (2022), Meshlets and How to Shade Them: A Study on Texture-Space Shading. Computer Graphics Forum, 41: 277-287. https://doi.org/10.1111/cgf.14474

1 Summary

本文提出了一种基于light probe的实时动态diffuse GI(DDGI) 算法。该算法的probe在世界空间下摆放,其数据完全实时生成,不需要预计算,因此支持具有动态光源与动态几何的场景。

DDGI算法主要包含了probe数据更新与probe-based shading:

  1. probe数据更新:为场景中的每个probe生成 \(n\) 条probe-update光线,光线与表面相交记为surfel,最终每个probe可以得到存储求交信息的surfel buffer。使用probe-base shading方法进行surfel shading,包含了sufel上的直接光照与从surfel周围probes中得到的间接光照。将surfel shading以给定滞后系数blend到probe中。

  2. probe-base shading:作者为surfel shading与最终着色提出了统一的probe-base shading,一共包含了两部分:直接光照pass生成的直接光照信息,注意作者为了简化面光源的模拟过程,使用一次反射的间接光作为面光源的直接光照,而其在直接光照过程中只会生成自发光信息;从probe数据中采样得到的间接光照信息。

本文提出的probe-based shading可以使用上一帧的光照信息与probe数据,实现了将多次反射的开销分摊到多帧。但这也会在visibility信息快速变化时,带来time-lag的异常现象。

2 DDGI Probe Overview

[1] 类似,probe中同样存储球面分布的数据,包括几何信息和辐射度量数据,存储在两张oc-map中:

  • 距最近几何的距离及其平方,存储在16x16 oc-map中,数据格式为RG16F
  • diffuse irradiance存储在8x8 oc-map中,数据格式为R11G11B10F

不同的是,在 [1] 中使用texture array打包所有probe的oc-map;而本文采用将所有probe的每一张oc-map打包进一张贴图集,并使用一个像素的边界隔开,同时增加额外的padding来确保4x4对齐。其次本文的probe数据是实时计算得到的,因此能够实现真正动态全局光照。在每一帧,除了插值probe深度信息以适应场景几何的变化外,还能够高效地将更新的光线追踪照明混合到probe地图集。

3 DDGI probe更新

每一帧都按照如下几步更新probe数据:

  1. 为场景中的 \(m\) 个active probe都生成并发出 \(n\) 条primary rays,并将光线追踪得到的 \(n\times m\) 求交信息存储到surfel buffer中
  2. 使用统一的着色方法对surfel buffer计算直接光照与间接光照
  3. 使用每个相交surfel得到的shading、距离及其平方结果以blending方式更新到 \(m\) 个active probe的oc-map texel

与probe相交的surfel着色计算依赖于前一帧的光照与probe数据,这样做有两个目的:这种方式可以将多次反射的计算分摊到多帧;以blending方式更新probe数据可以使得几何上与辐射度量上在随着时间的尖锐过渡变得平滑。相反,这种方式会在visibility剧烈变化的情况下,由于间接光照更新的延迟,表现出明显的变化。

3.1 放置probe

probe在场景中的放置方式与 [1] 相同,在轴向均匀的三维grid的每个顶点上放置一个probe,对于每轴需要不同空间离散化的场景,我们可以设置scale参数来独立地缩放网格在一个轴上的间距。空间中每一点都与一个包含该点的grid相关联,即对应了一个带有8个probe的cage,如下图所示。而着色过程则需要在cage的probe中进行查询。为了能够充分采样局部光照的变化,需要尽量满足每个点都能有cage的8个probe,避免某些probe位于墙内而无效。为此,作者建议使用grid分辨率以及缩放参数来使得每个类似房间的空间中产生至少一个完整的cage。文中给出,在人类尺度的场景中,1~2米的grid间隔效果最好。

image-20230224010007700

cage的8个probe [1]

3.2 生成与追踪Probe-Update光线

为了捕获场景中几何与光照的变化,作者选择 \(m\) 个active probes,对每个active probe均匀采样 \(n\) 个球面方向,采样方式采用stochastically-rotated Fibonacci spiral pattern与 [1] 类似。一个probe中心与这 \(n\) 个方向可以生成 \(n\) 条光线,作者将 \(m\) 个probe的光线以thread-coherent方式排布,在一个batch内发出。经测试,这种组织方式要优于直接在Vulkan或DirectX的实时光追管线中的primary shader发出光线。

球面方向采样方式stochastically-rotated Fibonacci spiral pattern [3]以及光线组织方式需要看实现代码

此外,作者还测试了active probes选取场景中部分probe(相机周围给定半径的范围内)以及光线采样数量随相机距离增大而减少,但实验结果表明,虽然这些优化会带来性能收益,但优化参数依赖于场景,不同场景需要配置不同的参数,否则会带来artifact。在本文的实验中,作者统一采用了每帧更新probe、每个probe都为active以及为所有probe采样相同数量的光线。

3.3 Secondary Surfel Shading

对于probe更新和最终渲染中的间接光,作者采用统一的着色模型。具体来说,计算全局光照的两个阶段:更新 \(m\times n\) Probe-Update光线采样的surfels着色;最终输出图像中相机下的像素着色。这两个阶段使用相同的着色例程,结合直接光照pass和使用probe数据的间接光照pass。也就是说,surfel的shading包含其直接光照与间接光照:

  • surfel的直接光照:从直接光照pass生成直接光照数据中获取,当然surfel不一定可见
  • surfel的间接光照:从surfel附近的probes中进行采样插值得到间接光照信息,即probe-based shading

而probe更新与最终渲染之间的差异在于shading queries,着色例程需要着色位置、法线以及观察方向作为输入。对于基于probe光追的surfel shading更新,需要光线求交得到的surfel位置与法线,以及probe中心到surfel的观察方向。

3.4 Probe Surfel Upates

在surfel shading之后,可以得到 \(m\times n\) surfel points中的每一个surfel的更新着色值、probe中心到surfel的距离及其平方,这些值需要更新到probe数据中。作者给出了alpha-blending的更新方式,设置 \(\alpha\) 为滞后参数,用来控制更新至覆盖前一帧值得速度,那么更新速度为 \(1-\alpha\),文中采用 \(\alpha\in[0.85,0.98]\),有以下更新过程, \[ newIrradiance[texelDir]=lerp(oldIrradiance[texelDir], \\ \sum_{ProbeRays}(\max(0,texelDir\cdot rayDir)*rayRadiance),\alpha) \] 上式中可以看出,filtered irradiance是采用moment-based filtered shadow query:confused: 直接计算得到的,避免了prefilter高分辨的入射radiance map。这种平滑的入射irradiance被用于计算diffuse间接光照,同时,也可以选择维护一个更高频的irradiance map用于glossy和specular的间接光。

4 Shading with DDGI Probes

4.1 直接光照

点光源与方向光的直接光照采用variance shadow map(VSM)的deferred render [2] 来计算。

对于面积光源的直接光照,作者使用一次反射的间接光照管线计算。间接光照管线中,一次反射的间接光是计算场景中直接光的一次反射,多次反射的间接光则是基于前一次反射的间接光计算得到。而对于面积光源的一次反射间接光是基于区域的自发光,而不是面积光源下的直接光照,因此计算得到的是面积光源的直接光照。图4.1 的最后一行给出了面积光源特殊处理的直观感受,直接光照结果只有面积光源区域的自发光,而没有照明场景中其他物体。

面积光源的特殊处理避免了对面积光源的直接光照的传统近似方式,例如在面积光源上采样多个点光源计算来近似面积光源的直接光照。除此之外,对面积光源的计算统一为本文提出的probe-base shading技术,可以得到平滑的面积光阴影与反射。

image-20230226115136810 image-20230226115205828

图 4.1 第一列只有直接光照,第二列有完整的全局光照

4.2 Diffuse间接光

为了弥补probe位置的入射irradiance没有考虑着色点周围的局部遮挡,作者使用SSAO得到的遮挡系数调制漫反射的出射radiance。但在更新probe过程中不会包含局部遮挡,间接光忽略该项的影响要远远小于直接可见表面的光照。

本文对间接漫反射的插值和采样技术结合了光线追踪与shadow map的想法,提高对动态几何和光源的鲁棒性,与 [1] 中不同。具体来说,在得到包含着色点的cage上的8个probe的索引后,为每个irradiance probe根据其相对于着色点的位置与方向计算插值权重。如图4.2中surfel X的着色过程的权重包括:使用表面法线 n 采样eight-probe cage的每个probe;使用X到P的方向 dir,为每个probe P施加back-face权重;P中存储的(进行过filter)平均距离 r;为了避免visibility边界采样问题,作者会将X朝着法线方向施加一个偏移量。

image-20230226143129489

图4.2 surfel X的着色。

图4.3说明了这些加权阶段对最终渲染结果的影响,从最初的传统probe渲染开始,逐步加入权重项,强调每个权重如何消除artifacts:

  • 背面剔除位于着色点切平面下方的probe,设置一个soft threshold,当着色点法线与其到probe的方向之间的点乘小于该阈值时,则剔除。
  • 人类视觉系统对于黑暗区域的低强度照明的敏感性,作者降低了少于5% irradiance范围的贡献
  • VSM中描述的mean- and variance-biased Chebyshev interpolants :confused:
  • 对着色点应用与着色点法线和其到probe方向成正比的bias偏移,避免visibility函数边界处不连续阴影的问题
  • 使用上述权重与bias,根据着色点与probe中心的距离,执行三线性插值

image-20230226145015593

图4.3 封闭房间场景下的irradiance插值与采样。d~f逐渐加入权重

技术细节:之前的工作使用 \(128\times 128 \times 6\) 的高精度cube map存储深度信息,而在应用本文的权重准则后,可以在不引入数值误差问题的情况下,降低至 \(16 \times 16\) 的medium精度。

4.3 多次反射的全局光照

多次反射的计算是一个递归过程,每一次反射的间接光都是基于前一次反射的radiance buffer。本文的间接光计算是分销在多帧的,因此会带来time-lag artifact,这一点对于静态相机下的静态场景最明显,而对于视角、光源和几何是动态的场景,这种延迟在间接光反射是不易察觉的。

Reference

[1] Morgan McGuire, Mike Mara, Derek Nowrouzezahrai, and David Luebke. 2017. Real-time global illumination using precomputed light field probes. In Proceedings of the 21st ACM SIGGRAPH Symposium on Interactive 3D Graphics and Games (I3D '17). Association for Computing Machinery, New York, NY, USA, Article 2, 1–11. https://doi.org/10.1145/3023368.3023378

[2] Thaler, Jonathan. (2011). Deferred Rendering. URL: https://www.researchgate.net/profile/Jonathan_Thaler2/publication/323357208_Deferred_Rendering/links/5a8fce31aca272140560aaad/Deferred-Rendering.pdf

[3] Ricardo Marques, Christian Bouville, Mickaël Ribardière, Luis Paulo Santos, Kadi Bouatouch. Spherical Fibonacci Point Sets for Illumination Integrals. Computer Graphics Forum, 2013, 32 (8), pp.134-143. ⟨10.1111/cgf.12190⟩. ⟨hal-01143347⟩

1 Summary

这篇论文提出了一种基于light probe的全局光照方法,其中包括预计算过程中probe数据的生成方式、基于probe数据的光线追踪算法以及实时渲染中的着色过程。

在预计算过程中,对场景的每个离散采样点生成以该位置为中心的probe数据,每个probe包含了球面分布的两组数据:probe中心收到每一方向上最近点的radiance、该最近点的法线以及该最近距离(radial distance);为了处理diffuse间接光,对probe中心收到的radiance进行prefilter得到的irradiance、最近距离及其平方。

同时,probe数据采用了具有分段线性投影特性的octahedral map存储,作者提出了在基于此map的具有2-level层级加速的高效光线追踪算法。为了得到确定的求交结果,需要在着色点周围的八个probe中选择进行求交测试,为此,作者提出最小化octahedral map访问次数的选取策略。

在实时渲染的着色过程中,直接光照选择采用传统的deferred shading。对于间接光照,作者将BRDF拆分为Lambertian项和glossy项,并为每一项采样一条光线计算间接光照:前者由于其在分布空间变化平缓的特性,而选择采样irradiance map,并可以使用较大的filter进行降噪;而后者在其分空间变化强烈,采样radiance map,并使用较小的filter降噪。而对于降噪filter过程,除了应用传统的空间与时序的降噪,作者还提出使用probe中的几何信息来构建geometry-aware的权重,来避免light/shadow-leaking问题。

作者源码 [3]

2 Light Field Probe Structure

Light Field(光场)描述了空间中一点的radiance在出射方向上的分布,假设点 \(\mathbf{x}\in \mathcal{R}^3\),以及该点的一个出射方向 \(\omega\in \mathcal{S}^2\) (应该指上半球),那么Light Field可使用 \(\mathcal{L}(\mathbf{x},\omega)\) 表示。不可能对场景中每一点都定义Light Field,作者通过将场景的包围盒均分为grid来将空间离散化,对每个grid的一个采样点定义一个light field。

在每个离散采样点 \(\mathbf{x}'\)(每个grid的一个采样点)上存储一个球形light field probe数据,用以记录点 \(\mathbf{x'}\) 的每个方向 \(\omega\) 的数据,包括:

  • \(\mathcal{L}(\mathbf{x'},\omega)\):点 \(\mathbf{x}'\) 在方向 \(\omega\) 的入射radiance。light field是在球面上的定义,而probe像素有一定大小,可以看作在球面上一片的radiance,因此文中称为spherical light field slice radiance。
  • 在方向 \(\omega\) 上距点 \(\mathbf{x'}\) 最近的点 \(\mathbf{x''}\) 的表面法线:\(\vec{\mathbf{n_{x''}}}\)
  • \(\mathbf{x'}\)\(\mathbf{x''}\) 之间的radial distance \(r_{\mathbf{x'}\leftrightarrow\mathbf{x''}}\)

2.1 Probe 数据生成方式

上述Probe数据中的几何信息,例如 \(\vec{\mathbf{n_{x''}}}\)\(r_{\mathbf{x}'\rightarrow\mathbf{x''}}\),作者使用光栅化管线生成。而spherical light field slice radiance \(\mathcal{L}(\mathbf{x'},\omega)\) 需要光线模拟过程来生成:可以通过光栅化过程现有的着色技术(deferred renderer with shadow map)来计算;或者使用离线path tracer;作者选择迭代应用本文提出的着色算法来生成,具体做法:对于一次反射的直接光照 \(\mathcal{L}(\mathbf{x'},\omega)\) 只包含场景中光源的发光,重复应用着色算法得到具有额外反射的 \(\mathcal{L}(\mathbf{x'},\omega)\)

2.2 实现细节

一个probe本质上是以其中心点 \(\mathbf{x'}\) 为球心的球面上所有方向的信息,类似于全向相机的渲染(Omnidirectional),其数据组织为cube map的形式,每个像素的数据为其对应方向 \(\omega\) 的probe数据。本文提出的算法中不直接使用cube map,而是在生成probe数据过程中,将cube map重采样到octahedral map(oc-map)中 [1] [2] [5.1],之后的算法就不再需要cube map。八面体参数化是将球面映射到一个方形平面上,相比于cube map,oc-map具有优势:失真度更低;同等质量水平下,存储开销与带宽降低4倍;分段线性的投影能够应用于高效的ray marching。

每个probe的oc-map数据作为2D texture array的一个layer存储,一个oc-map像素大小为32字节,其存储格式为:

  • HDR radiance \(\mathcal{L}(\mathbf{x'},\omega)\):R11G11B10F (32 bits/pixel)
  • 法线 \(\vec{\mathbf{n_{x''}}}\):RG8 (16 bits/pixel)
  • radial distance \(r_{\mathbf{x}'\rightarrow\mathbf{x''}}\):R16F (16 bits/pixel)

irradiance probe:很多着色过程得益于prefilter light field radiance \(\mathcal{L}(\mathbf{x'},\omega)\),因此将prefilter结果预存储到一个cosine-weighted incident irradiance probe \(\mathbf{E}(\mathbf{x'},\vec{\mathbf{n}})\),即点 \(\mathbf{x'}\) 处的法线方向 \(\vec{\mathbf{n}}\) 上收到的radiance积分。例如,Lambertian项描述的漫反射项。作者发现irradiance probe的oc-map的分辨率高于 \(128\times 128\) 后就不会再有精确度方面的收益了。irradiance probe的数据存储格式和上述基本相同,只是将radiance替换为irradiance,法线替换为radial distance的平方,之后用于降噪过程的切比雪夫测试。

radiance probe需要的分辨率取决于渲染分辨率和场景中的最低粗糙度,避免完美镜面反射的走样。为此需要满足八面体纹素对着的立体角不大于probe中心位置的像素对着的立体角。文中建议1024x1024的分辨率。

场景中不同区域在屏幕上的像素比例不同,例如离相机远的区域占用像素更少、粗糙度越低对应的立体角越小。如果probe分辨率不足,会导致八面体纹素对应的立体角大于像素对应的立体角,像素采样到的radiance信息超出了像素范围,即radiance采样率不足。

3 Light Field Probe Ray Tracing

已知光线方程 \(\mathbf{p}(t)=\mathbf{r}_o+t\cdot\mathbf{r}_d\),求解最近交点 \(\mathbf{p}(t')\equiv\mathbf{p}'\)。Light field probe中的几何信息radial distance和法线用于解决incoherent世界空间求交,radiance probe和irradiance probe包含了所需的辐射量,交点处的出射radiance \(\mathcal{L}(\mathbf{p}',-\mathbf{r}_d)\)和入射irradiance \(\mathcal{L}(\mathbf{p}',\vec{\mathbf{n}}\equiv\vec{\mathbf{r}_d})\)

作者提出的Light Field ray trace过程如下图:先选择一个候选probe,执行single-probe ray trace,当得到miss或hit结果时,则求交结束。当无法得到求交确定结果时,选择下一个候选probe继续执行single-probe ray trace。

如果得到不确定的结果,则表示本次选择的probe缺少光线传播区域的信息,例如从probe视角来看,光线传播区域被遮挡,因此无法确定是miss或hit。

image-20230201162511726

image-20230202104825030

作者提出的 light field ray tracing算法主要包括两方面:选择候选probe的启发式策略;probe的oc-map中的光线求交。

3.1 Single-Probe光线求交

球面的八面体投影性质:将三维空间的线段映射为oc-map中的多段线(最多4段线),其中每段线分布在oc-map的不同面;probe的三维球体被以probe中心为原点的坐标系分割成8个平面,分别对应了oc-map的8个面,而每段线的端点为三维空间的线段与三个坐标轴对齐面的交点在oc-map的投影。

probe的光线求交:首先计算出光线在oc-map投影的几组端点;然后沿着线段进行ray-march,使用当前位置与probe中心的radial distance \(t_0\) 以及当前位置在oc-map中的对应纹素存储的radial distance \(t_1\) 做求交测试,

  • 如果 \(t_0>t_1\) 表示光线交于一个表面(hit)或者从一个表面后面通过(unknown,光线行进区域在probe中被遮挡)
  • 否则miss

probe的光线求交与SSR思想类似,不同的是:

  • 光线在depth buffer中的投影是一条直线,而在oc-map中的投影是几段线
  • 对于光线行进位置处的求交测试:SSR使用相机视角的depth buffer在行进位置处的深度值与相机到行进位置距离,而本文的probe求交则使用oc-map在行进位置处的radial distance与probe中心到行进位置的radial distance

对于oc-map的一个面内ray-march可以使用hierarchy算法加速 [4]。上述求交测试中,当 \(t_0 < t_1\) 可以确定miss,但 \(t_0 > t_1\) 有可能hit,也有可能从后面通过(unknown)。为了得到确定结果,需要确定求交平面即oc-map的对应texel在三维空间的范围。oc-map的每个纹素描述的是距离probe中心 \(r_{\mathbf{p}'\leftrightarrow \mathbf{x}''}\) 并且法线为 \(\vec{\mathbf{n_{x}}''}\) 的一个小patch(surfel)。基于法线和表示精度,作者使用一个球形体素包围该patch。只有当前行进点与该体素相交时,才表示与光线与patch表面相交。法线也可以用作排除掉相对于光线方向的背面的交点。下图为光线求交过程的可视化,光线在如图oc-map投影得到三个线段

image-20230202213146200

two-level层级结构的求交过程:黄色与绿色分别为粗粒度与细粒度的ray-march测试

3.2 Probe选取策略

对于任意光线起点 \(\mathbf{r}_o\) 都会被其周围8个probe所描述立方体包裹,如下图中编号所示。light field ray tracing首先选择中心距光线最近的probe进行求交。根据空间局部性原理,这8个probe往往具有相似的visibility信息,这一启发策略在深度高复杂高和probe密度低的情况下会失效。而首先选择最近的probe是因为光线在该probe的oc-map中的投影最短,这样会带来ray-march过程中访问texture的次数较少;同时,最近的probe更可能观察到光线行进区域而不被遮挡。

image-20230203133555805

图3.2 光线起点周围的8个probe

当single-probe hierarchy ray march不能确定求交结果时,需要再选择一个probe执行求交过程。首先将光线起点移动到上一个确定求交结果的行进位置,\(\mathbf{r}'_{d}=\mathbf{r}_o+t'\mathbf{r}_d\) 。如果新的光线起点进入了一个新的立方体范围,那么再次选择一个距离光线最近的probe。如果新的光线起点还在原立方体范围内,从未进行求交测试的probe中选择一个距离上一个probe最远的probe,这种策略同样是基于空间局部性原理。为此,作者设计了简单高效位运算,假设light field ray tracing始于距光线最近的probe \(i\),那么下一次选择 \(i'=(i+3)\space mod\space 8\),选择顺序如上图的有向曲线所示。

图3.3使用像素颜色表示了该像素得到求交结果的probe编号,同样的颜色表示同一个probe。可以看出越近的像素,其遮挡信息越相近。注意:probe的选取策略只会影响性能,不会影响求交结果。

image-20230203182557809

图3.3 probe选取可视化。像素颜色表示该像素发出的glossy ray在颜色所表示编号的probe得到求交结果

3.3 性能说明

由于文中提出的probe选取策略会得到光线在所选probe的oc-map中的投影线段最短,而投影线段的缩短减少光线行进的步数,也就是减少oc-map的访问以及求交测试。这可以带来便利的内存-性能权衡:增大probe密度不仅能够提高准确度,也会由于缩短光线在oc-map的投影长度而提高性能。

在实现上,作者通过重新组织glsl中的branch,降低线程动态分支的开销。light field ray tracing使用two-level mipmap实现的层级加速,第2层分辨率是上一层的 \(1/16^2\)

4 Shading with Light Field Probes

原始的着色算法:光栅化直接可见表面后(deferred shading)在pixel shader中使用shadow map计算直接光照;然后在同一pixel shader中进行蒙特卡洛积分来估计间接光:基于重要性采样,发出 \(n\) 个随机光线,每条光线对light field进行采样得到对应入射方向上的incident radiance。最后乘上cosine-weighted BRDF并使用采样分布和采样数 \(n\) 归一化后得到积分估计。

上述原始算法可以捕获到静态和动态物体的间接效果,同时可以使用该算法再次渲染probe来逐步填充light field probe的在任意次反射之后的radiance分布。但该方法有两个局限:

  • 更高阶间接光照效果的视点偏差:当从light field probe中采样radiance时,得到的是采样点到probe中心的radiance,与着色点是有一定差距的。

    在使用上一章节描述的光追算法,得到采样光线的反射点后,可将probe中心点到反射点的方向进行转换得到对应的oc-map的采样坐标,采样radiance map得到的是probe中心收到反射点的入射radiance。

  • 性能:原始算法中需要很多光线数量来降低噪声,无法应用到实时渲染中

但更高阶的光照效果的失真影响远远小于第一次反射,作者更关注性能问题。作者对原始着色算法进行近似与优化,使其更适用于现代实时渲染管线。

4.1 Spatial-Temporal Radiance Denoising

实时渲染下能够支持的光线数量非常有限,应对噪声的通常做法是使用时序-空间上的降噪,即SVGF的思想。针对BRDF,作者将其分解为两项:描述diffuse的Lambertian项和描述specular的glossy项。间接光照的render pass为每一项采样(cosine-weighting)一条光线,并将结果写入带噪声的Lambertian和glossy入射radiance buffers。之后再在单独的denoise pass进行双边过滤(cross-bilateral)和时序重投影降噪。

对于Lambertian项,均匀分布在法线方向的上半球范围,同样采样率水平下,噪声更大,但变化较小。因此可以直接采用存储积分后的irradiance,同时由于其变化较为平缓的特点,可以使用更大的filter,文中采用半径为12像素、步长为2像素的空间滤波与98%置信度的时序滤波。glossy项虽然噪声较小,但变化较为剧烈,因此使用更小的filter,采用半径为3像素的空间滤波与75%置信度的时序滤波,如 图4.1 所示。

image-20230213234756136

图4.1 使用一个采样光线的glossy indirect buffer。左边为降噪前,右边为降噪后

4.2 Irradiance with (Pre-)Filtered Visibility

前文所述的预计算过程,通过蒙特卡洛计算radiance积分估计,得到probe中心位置的irradiance oc-map(128X128)中,采样坐标为法线方向转换得到oc-map坐标。在运行时,作者通过在如图3.2的八个probe之间使用三线性插值来估计着色点处收到的irradiance。但该方法存在的缺陷:插值得到的irradiance会透过irradiance probe立方体网格体积内的物体,从而造成漏光或者漏阴影,如图4.3所示。具体而言,在立方体网格体积内的着色点与一个或多个probe之间的可见性假设是无效的。

为此作者提出了一种geometry-aware方法来避免这些不合理现象。在预计算cosine-filtered irradiance map时,采用cosine-power lobe的滤波方式,对radial distance和其二次方进行滤波得到的filter map,这中滤波方式避免了距离的离散值带来的锯齿,同时又不造成过度模糊,如图4.2所示。在运行时,结合基础的三线性插值的权重与额外的两项得到geometry-aware权重:

  • 着色点表面法线与其到probe的方向之间的clamp dot
  • local radial distance分布的切比雪夫可见性检验,方法与VSM[5]原理类似,分布方差为 \(\sigma^2=E[r]^2-E[r^2]\)

最后累加结果使用8个probe权重之和归一化。

image-20230214001658030

图4.2 每一列为irradiance probe的一组数据。行从上到下依次为:irradiance;过滤的radial distance;过滤的二次距离

image-20230214233524659

图4.3 传统irradiance map的漏光缺陷,文中提出的irradiance map解决后的效果

5 Appendix

5.1 八面体参数化

cube map的八面体参数化过程:先将球面投影到正八面体上,然后再投影到 \(z = 0\) 平面上,并将 \(z < 0\) 的部分进行翻转,最终形成一个二维正方形。如下图所示

image-20230301210633868

八面体参数化过程

Reference

[1] Cigolle, Zina H., et al. "A survey of efficient representations for independent unit vectors." Journal of Computer Graphics Techniques 3.2 (2014).

[2] https://zhuanlan.zhihu.com/p/384232048

[3] https://github.com/Global-Illuminati/Precomputed-Light-Field-Probes

[4] McGuire M, Mara M. Efficient GPU screen-space ray tracing[J]. Journal of Computer Graphics Techniques (JCGT), 2014, 3(4): 73-85.

[5] William Donnelly and Andrew Lauritzen. 2006. Variance shadow maps. In Proceedings of the 2006 symposium on Interactive 3D graphics and games (I3D '06). Association for Computing Machinery, New York, NY, USA, 161–165.

1 Summary

Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting [1]

解决 many-lights 下的直接光照的重要性采样问题。

2 Background

本论文将光源视为场景中的发光几何体,因此 many-lights 下的渲染方程为:着色点 \(y\) 在方向 \(\vec{\omega}\) 所反射的 radiance \(L\) 是对所有发光表面 \(A\) 的积分: \[ L(y,\vec{\omega}) = \int_A \rho(y,\vec{yx}\leftrightarrow\vec{\omega})L_e(x\rightarrow y)G(x\leftrightarrow y)V(x\leftrightarrow y) \space dA_x \tag{1} \label{render equation} \] 其中,\(\vec{\omega}\) 是观察方向,与相机相关;\(\rho\) 与着色点 \(y\) 的材质有关,论文中指 BSDF;\(L_e\) 是光源表面一点 \(x\) 到着色点 \(y\) 方向上发出的 radiance;\(G\) 几何项,包含了 inverse squared distance 和反射方向与入射方向的余弦项。

对渲染方程的表示进行一些符号简化,被积函数简化为 \(f(x)\),积分微元 \(dx\),有渲染方程的简化形式: \[ L=\int_A f(x)\space dx,\quad where \space f(x)\equiv\rho(x)\cdot L_e(x)\cdot G(x)\cdot V(x) \tag{2} \label{simple render equation} \]

  • Importance Sampling(IS)

使用蒙特卡洛重要性采样方法估计 \(\eqref{simple render equation}\) 式积分,从 source PDF \(p(x_i)\) 中进行 \(N\) 次采样,得到 \(N\) 个样本,\(\eqref{simple render equation}\) 积分的估计量为: \[ L \approx \langle L \rangle_{IS}^N =\frac{1}{N}\sum\limits_{i=1}^N \frac{f(x_i)}{p(x_i)} \tag{3} \label{IS MC} \] 其中如果在 \(f(x)\) 非零时,\(p(x)\) 都为正,IS 则为无偏估计;并且可以通过选取与 \(f(x)\) 相关的 \(p(x)\) 形式,来降低方差。

由于 \(f(x)\) 的复杂性(包含多项),直接采样与 \(f(x)\) 成比例(相关)的分布是不可实行的,而通常采样与 \(f(x)\) 中单项成比例的分布,例如 \(\rho\) BSDF、\(L_e\) 等。因此,定义 \(M\) 个采样策略 \(p_s\) (概率分布),MIS 从每个采样策略 \(s\) 中采样出 \(N_s\) 个样本,再将这些样本组合到一个 weighted-estimator 中,如下: \[ L\approx \langle L\rangle_{MIS}^{M,N}=\sum\limits_{s=1}^M\frac{1}{N_s}\sum\limits_{i=1}^{N_s}w_s(x_i)\frac{f(x_i)}{p(x_i)} \tag{4}\label{MIS MC} \] 其中,只要权重 \(w_s\) 可以组成一个归一化整体,MIS 就是无偏估计,即 \(\sum^M_{s=1}w_s(x)=1\)。一种流行且较好的启发式选择为 \(\Large w_s(x)=\frac{N_s p_s(x)}{\sum_j N_jp_j(x)}\),等价于采样 \(M\) 个独立采样策略的混合分布。

2.1 Resample Importance Sampling(RIS)

\(\eqref{MIS MC}\) 可以看出,MIS 是对着色项的线性组合的采样,而另一种可选的采样策略是对其中一些项的乘积进行近似成比例地采样,这就是 RIS 实现的策略。RIS 从 source PDF \(p\) 中生成 \(M\geq 1\) 个候选样本 \(\boldsymbol{x}={x_1,\cdots,x_M}\)\(p\) 选择次佳但易于采样地分布,如 $p L_e $。然后从这个候选池中使用以下离散概率分布进行随机选取 \(z\in\{1,\cdots,M\}\)\[ p(z\mid \boldsymbol{x})=\frac{\mathrm{w}(x_z)}{\sum^M_{i=1}\mathrm{w}(x_i)} \quad with \quad \mathrm{w}(x)=\frac{\hat{p}(x)}{p(x)} \tag{5}\label{discrete PDF} \] 其中,\(p(x)\) 为易于采样的 source PDF;\(\hat{p}(x)\) 为期望得到的 target PDF,例如无法直接采样的分布 \(\hat{p}\propto \rho\cdot L_e \cdot G\)。对于 1-sample RIS 估计量,因此有 \(y\equiv x_z\),有 \[ L\approx \langle L \rangle^{1,M}_{RIS} = \frac{f(y)}{\hat{p}(y)}\cdot\left(\frac{1}{M}\sum\limits^M_{j=1}\mathrm{w}(x_j)\right) \tag{6} \label{1-sample RIS} \]

\(\eqref{1-sample RIS}\) 的理解:RIS 一次采样相当于得到一个 target PDF \(\hat{p}\) 的样本 \(y\),这个分布是可以采用多项乘积的形式。回顾重要性采样 IS,要使得被积函数对应的估计量 \(\langle L\rangle\) 无偏,需要估计量的形式为 \(f(y)/p_y(y)\),注意这里的 \(p_y(y)\)\(y\) 的真实 PDF,而不是 target PDF。因此需要乘上括号部分,来近似 \(y\) 的真实 PDF,但这种近似并不总是无偏的,会在某些情况下引入偏差,之后部分会有具体分析。

多次执行 RIS,然后取平均,可以得到一个 N-sample RIS 估计量: \[ L \approx\langle L\rangle^{M,N}_{RIS}=\frac{1}{N}\sum\limits^N_{i=1}\left(\frac{f(y_i)}{\hat{p}(y_i)}\cdot \left(\frac{1}{M}\sum^M_{j=1}\mathrm{w}(x_{ij})\right)\right) \tag{7} \label{N-sample RIS} \]\(M,N\geq 1\) 并且 \(f\) 非零时,\(p,\hat{p}\) 都为正,RIS 则为无偏估计。理论上 \(M\)\(N\) 存在与方差有关的最优比,但实际中,这个比值难以事先预知。本文中采用简单形式 \(N=1\),即 1-sample RIS。有算法流程:

  1. 预设 source PDF \(p\),文中实验将其设为在面光源上的面积采样分布;target PDF \(\hat{p}\),文中实验设置为 unshadowed path contribution 部分,即 \(\rho \cdot L_e\cdot G\),不包含 visibility 项

  2. 设置初始为空的权重列表 \(\textup{W}\),初始为 \(0\) 的权重累积和 \(\textup{w}_{sum}\),初始为空的候选样本列表 \(\boldsymbol{x}\)

  3. 对 source PDF \(p\) 进行 \(M\) 次采样:

    • \(i\) 次采样的随机样本为 \(x_i\),放入候选样本列表 \(\boldsymbol{x}\)

    • 计算样本 \(x_i\) 的权重 \(\mathrm{w}_i=\Large \frac{\hat{p}(x_i)}{p(x_i)}\),并放入权重列表 \(\mathrm{W}\)

    • 累积权重和 \(\mathrm{w}_{sum}=\mathrm{w}_{sum}+\mathrm{w}_i\)

  4. 对候选样本权重进行归一化:\(\mathrm{w}_i= \Large \frac{\mathrm{w}_i}{\mathrm{w}_{sum}}\)

  5. 将归一化权重作为候选样本的概率分布,采样候选样本列表得到最终选出的样本 \(y=x_z\)

  6. 返回选出的样本 \(y=x_z\) 以及本次 RIS 的候选样本的权重累积和 \(\textup{w}_{sum}\)

假设像素 \(q\) 对应 target PDF \(\hat{p}(q)\),以及其渲染方程积分 \(f_q\),1-sample RIS 算法伪代码如下:

image-20220717215808514

算法 1

\(\eqref{1-sample RIS}\) 式的计算需要候选样本数量 \(M\),这是算法输入,是已知的;\(\mathrm{w}_{sum}\),算法输出;\(\hat{p}(y)\)\(y\) 是算法选出的样本,\(\hat{p}\) 是预设 target PDF,本文使用的是代入样本到被积函数后的颜色转为亮度。

2.2 Weighted Reservoir Sampling(WRS)

WRS 是一类从一个 stream \(\{x_1,x_2,\cdots,x_M\}\) 中通过一遍操作来随机采样 \(N\) 个元素的算法,该 stream 数据中每个元素 \(x_i\) 都关联一个权重 \(\textup{w}(x_i)\)\(x_i\) 被选中的概率为 \[ P_i = \frac{\mathrm{w}(x_i)}{\sum^M_{j=1}\mathrm{w}(x_j)} \tag{8} \label{reservoir CDF} \] Reservoir sampling 对每个元素只进行一次操作,仅需要 \(N\) 个元素保留在内存中,并且 stream 的长度 \(M\) 不必事先预知。Reservoir sampling 算法,分为有放回采样与无放回采样,由于蒙特卡洛积分积分的采样通常是相互独立的,因此本论文只考虑较为简单的有放回采样算法。

Reservoir sampling 按照 input stream 的次序来处理其中的元素,保存一个具有 \(N\) 个样本的 reservoir。在任何时刻,reservoir sampling 保持采样自 desired PDF 的 reservoir 中的样本的概率分布仍然是 desired PDF。即当处理一个来自 stream 的新元素时,更新 reservoir 来保持其中的样本分布的不变性,更新操作为:

在处理了 m 个样本之后,样本 \(x_i\) 在 reservoir 出现的概率为 \(\textup{w}(x_i)/\sum^m_{j=1}\textup{w}(x_j)\)。处理下一个新元素 \(x_{m+1}\) 的更新规则为,使用下一个样本 \(x_{m+1}\) 以以下概率随机替换 reservoir 中样本 \(x_i\)\[ \frac{\mathrm{w}(x_{m+1})}{\sum^{m+1}_{j=1}\mathrm{w}(x_j)} \] 因此任意之前的样本 \(x_i\) 在 reservoir 内的概率(即已在 reservoir 中,且不会被下一个样本替换掉的概率)为: \[ \frac{\mathrm{w}(x_i)}{\sum^m_{j=1}\mathrm{w}(x_j)}\left(1-\frac{\mathrm{w}(x_{m+1})}{\sum^{m+1}_{j=1}\mathrm{w}(x_j)}\right)=\frac{\mathrm{w}(x_i)}{\sum^{m+1}_{j=1}\mathrm{w}(x_j)} \] > 可以看出 N 个样本的 reservoir 接收到一个新元素的更新规则是,逐个样本执行更新规则

观察 \(\eqref{reservoir CDF}\) 中给出的 reservoir CDF 可知,在处理过一个新的元素后 reservoir 中元素的概率分布依然不变。N=1 时的 WRS 算法如下:

image-20220717215902454

3 Streaming RIS With Spatiotemporal Reuse

RIS 算法对多个 source PDF 的样本进行重采样,重采样的概率分布结合了被积函数的(unshadowed) target PDF 以及源分布,而 target PDF 可以近似为渲染方程 \(\eqref{simple render equation}\) 的被积函数每一项对应分布的乘积形式,这种乘积形式要比 MIS 的线性组合形式更接近被积函数的真实分布。因此,RIS 最终选出的样本可近似看作一次 MIS 采样(包含多种采样策略)的样本,且理论上更优。

WRS 算法能够高效地维护一个 reservoir,在保持 reservoir 中原有样本所服从分布不变的情况下,不断纳入新的随机样本,使得 reservoir 样本不断逼近目标估计量的期望值。

理论上,对 RIS 应用 WRS 可以得到一个使 RIS 样本不断逼近期望值的高效算法,当然由于本文的 target PDF 选取 unshadowed path contribution,最终会由于 visibility 无法继续提高 RIS 样本。下面介绍如何应用 WRS 算法,从而得到 Streaming RIS 算法。

3.1 Streaming RIS Using Reservoir Sampling

RIS 算法为每个像素 \(q\) 独立地生成候选样本,再在候选样本中以 target PDF \(\hat{p}_q\) 重新采样,选出最终的样本。对 RIS 可以直接应用 WRS 算法,RIS 选出的样本及其对应的权重,可以直接用于 WRS 算法更新其 reservoir。论文中,通过均匀采样 emitter 的面积,并且使用无阴影的路径的贡献 \(\hat{p}(x)=\rho(x)\cdot L_e(x)\cdot G(x)\) 作为 target distribution,仅对 reservoir 中的 \(N\) 个样本进行 trace shadow ray。Streaming RIS 算法如下,

image-20220717215920480

算法 3

这里每次更新 reservoir :候选样本 \(x_i\) 及其权重 \(\eqref{discrete PDF}\) 中的 \(\Large \textup{w}_{i}=\frac{\hat{p}(x_i)}{p(x_i)}\)

3.2 Spatiotemporal Reuse

3.2.1 复用 reservoir

Reservoir 样本累积了目前见过的所有样本信息,如果可以基于某些策略(如接下来的Spatiotemporal),选择与当前像素样本分布相似的某些像素的 reservoir,再与当前像素的 reservoir 进行结合复用,就能大幅提高当前像素的采样率。

一种易想到的简单复用方式使用 2-pass 算法:第一个 pass 先为每一个像素生成其候选样本;第二个 pass 再结合自身与其周围像素的候选样本,进行重采样。但这种方式带来的内存开销非常大,需要存储每个像素的候选样本,即 input stream (如前述算法中 \(M\) 个)。

为了规避内存开销问题,论文使用了 Reservoir sampling 的一个重要性质,该性质可以在不访问其他 reservoir 的 input stream 的条件下,结合多个 reservoir。如果多个 reservoir 中样本分布相似,同样可以将多个 reservoir 输入到一个新的 reservoir 中,这个新的 reservoir 汇集了所有输入 reservoir 的样本信息,并且由于相似的样本分布,而得到采样率/质量更高的样本。

以两个 reservoir 为例:每个 reservoir 当前的状态包含当前选择的样本 \(y\) 以及其 sum weight \(\textup{w}_{sum}\),作为一个新的 reservoir 的输入,重采样出一个结合两个输入 reservoir 的新样本和新权重。这种方式只需要访问 reservoir 的当前状态,避免了存储 input stream 的开销,但结果在数学形式上等同于在两个 reservoir 的 input streams 重采样。

复用 reservoir 只是与当前 reservoir 的样本分布相似,但不完全相同,因此需要根据样本分布调整权重。假设 \(q\) 为当前像素,\(q'\) 为复用像素,论文中将新的 reservoir 中的样本权重使用 \(\hat{p}_q(r.y)/\hat{p}_{q'}(r.y)\) 进行调整,采用 算法 3 中的符号得到最终新的权重为 \(\hat{p}_q(r.y)\cdot r.W \cdot r.M\)。对于任意数量的 reservoir 都可以以这种方式结合,下面是 \(k\) 个 reservoir 结合的算法,只有 O(k) 的复杂度:

image-20220717215937128

算法 4

这里新的 reservoir 更新:输入 reservoir 样本,其对应权重根据 \(\eqref{discrete PDF}\) 理论上应为 \(\Large \frac{\hat{p}_q(r.y)}{p_q(r.y)}\),但 \(y\) 的真实分布是未知的,这里使用 \(\hat{p}_q(r.y)\cdot r.\textup{W}\) 来近似,\(r.M\) 则是为了对不同候选样本数量的 reservoir 的加权。在复用 reservoir 样本时,新的 reservoir 的 source PDF 就是输入 reservoir 样本的分布,即 \(p_y(y)\)

下面介绍,选用那些像素的 reservoir 来增强当前像素。

3.2.2 Spatial Reuse

空间复用基于一种观察:相邻像素的 target PDFs 通常存在显著联系,通常具有相似的 Geometry 和 BSDF。因此可以结合相邻像素的 RIS 候选样本来增加自身的 RIS 候选样本数量。

空间复用相邻像素的 reservoir 的步骤如下:

  1. 使用 算法 3 RIS(q) 为每个像素生成 \(M\) 个候选样本,并将 reservoir 的结果存储在 image-sized buffer 中。
  2. 每个像素选取 \(k\) 个邻居像素,并使用 算法 4 结合 \(k\) 个邻居像素的 reservoirs 以及自身的 reservoir。

每像素的复杂度为 \(O(k+M)\),但产生了 \(k\cdot M\) 个候选样本。Spatial Reuse 可以重复多次执行,其中每次都是上个 reuse pass 的输出。对于执行了 \(n\) 次空间复用迭代,假设每次迭代都复用不同的相邻像素,那么时间复杂度为 \(O(kn+M)\),但产生了 \(k^n\cdot M\) 个候选样本。

注意,虽然空间复用可以大幅增多候选样本,即采样率,进而提高图像质量,但这种提升不会随着迭代次数增加而一直增加下去。直至迭代次数多到相邻像素的像素信息被全部利用到,图像质量不再提升。

3.2.3 Temporal Reuse

上一帧的像素同样可以提供可复用的候选样本。当完成绘制一帧时,将当前帧的像素的 reservoir 结果存入历史帧信息中。在下一帧时利用 motion vector 找到像素的对应关系,复用对应像素的历史 reservoir。

3.2.4 Visibility Reuse

不幸的是,即使有无限数量的候选样本,RIS 也无法达到无噪声的图像质量。尽管理论上,\(M\rightarrow \infty\) 时,reservoir 样本的分布趋向于 target PDF \(\hat{p}\),但 \(\hat{p}\) 并没有完美采样渲染方程的被积函数。因为,在实际中,\(\hat{p}\) 通常设置为了无阴影的 path contribution,当 \(M\) 不断增大时,由于 visibility 项噪声会越来越明显,并且 visibility 噪声会表现的非常严重。论文为了解决这一问题,同样对 visibility 进行复用。

在进行 spatial reuse 或者 temporal reuse 之前,先对每个像素的 reservoir 选择的样本 \(y\) 进行 visibility 评估。如果 \(y\) 被遮挡,则丢弃该 reservoir,即被遮挡的样本不会传递给其相邻像素。visibility 评估需要对当前像素向复用的 reservoir 样本发射 shadow ray。

3.2.5 完整算法描述

将上述过程全部考虑进来,得到完整的算法流程:

  1. 首先为每个像素生成 \(M\) 个 light candidates,进行重采样,选出最终的样本
  2. 对选出的样本进行 visibility 测试,从中丢弃掉被遮挡的样本
  3. 结合每个像素的 reservoir 和上一帧的对应像素的 reservoir
  4. 执行 \(n\) 次空间复用迭代,利用附近 \(k\) 个像素的 reservoirs

算法伪代码如下所示:

image-20220717215954040

4 Eliminating Bias in Multi-Distribution RIS

虽然上一部分引入复用样本,提升了图片质量,但由于每个像素都具有不同的积分区域和 target PDF,复用会引入潜在的偏差,导致蒙特卡洛积分成为有偏估计。下面通过分析 RIS 权重,推导出偏差的源头。这里假设所有的候选样本都来自于同一源分布 \(p\)

4.1 分析 RIS 权重

\(\eqref{1-sample RIS}\) 式的 1-sample RIS 可以重写为: \[ \langle L\rangle^{1,M}_{RIS}=f(y)\cdot\left(\frac{1}{\hat{p}(y)}\frac{1}{M}\sum\limits^M_{j=1}\mathrm{w}(x_j)\right)=f(y)\mathrm{W}(\boldsymbol{x},z) \tag{9}\label{regroup 1-sample RIS} \] 其中 \(\boldsymbol{x}=\{x_1,x_2,\cdots,x_M\}\),source PDF 的 \(M\) 次独立采样;\(\textup{W}\) 是 RIS 选出的样本 \(y\equiv x_z\) 对应的权重,挑选过程是随机,可知是个随机变量。

理解 \(y\)\((\boldsymbol{x},z)\) 以及 \(\textup{W}(\boldsymbol{x},z)\):首先 \((\boldsymbol{x},z)\) 表示从 \(M\) 次独立采样得到的 \(M\) 个样本中,随机挑选出第 \(z\) 个。因此 \(\boldsymbol{x}\)\(M\) 个独立同分布于 source PDF 的 \(M\) 维随机变量,有 \(p(\boldsymbol{x})=\prod\limits^M_{i=1}p_i(x_i)\)\(z\) 服从一个离散分布,有 \(\eqref{discrete PDF}\) 描述的条件形式。其次 \(y\) 是 RIS 算法得到的样本,虽然采样过程已知,但 \(y\) 的真实概率密度没有解析形式。但根据 RIS 算法,\(y\)\((\boldsymbol{x},z)\) 有关,但对于一个样本 \(y\),会有很多种 \((\boldsymbol{x},z)\) 的组合,例如 \(x_1=y\)\((x_2,\cdots,x_M)\) 随机选择,同理也可以有 \(x_2=y\),因为 \(\boldsymbol{x}\)\(M\) 次独立采样,每次采样都有可能得到样本 \(y\)

对于 \(\textup{W}\),虽然文中公式形式为 \(\textup{W}(\boldsymbol{x},z)\),但实际意义应该是,RIS 采样得到样本 \(y\) 时对应的权重,即 \(x_z=y\) 的权重。换句话说,给定样本 \(y\) 时的权重,权重更精确的形式是: \[ \mathop{\textup{W}}\limits_{x_z=y}(\boldsymbol{x},z)=\frac{1}{\hat{p}(y)}\frac{1}{M}\sum\limits^M_{j=1}\textup{w}(x_j) \tag{10} \label{RIS weight} \] 即在 \(x_z=y\) 的条件下的权重。

首先猜想如何使得 RIS 估计量是个无偏估计?虽然 \(y\) 的真实分布是未知的,但根据 \(\eqref{IS MC}\) 式的重要性采样理论,这里的 \(\textup{W}(\boldsymbol{x},z)\) 需要充当 \(1/p(y)\) 的作用才能得到无偏估计,注意这里的 \(p(y)\) 指的是 \(y\) 的真实 PDF,而不是 source PDF,为了区分,我们重写为 \(p_y(y)\)。 如 \(\eqref{RIS weight}\) 式描述,对于给定的 RIS 样本 \(y\),可能有很多种 \((\boldsymbol{x},z)\),因此 \(\textup{W}(\boldsymbol{x},z)\) 是一个随机变量,要想使得 RIS 无偏,则需要 \(\textup{W}(\boldsymbol{x},z)\) 的期望值等于 \(1/p_y(y)\)

算法 4 中将多个 reservoir 整合为一个新的 reservoir 时,每个 reservoir 的权重根据 \(\eqref{discrete PDF}\) 理论上应为 \(\Large \frac{\hat{p}(y)}{p_y(y)}\),而算法 4 使用的近似为 \(\hat{p}(y)\cdot \textup{W}\),这里则是用了 \(\textup{W}\) 作为 \(1/p_y(y)\) 的估计量。额外的 \(M\) 则是为了对不同候选样本数量的 reservoir 进行的加权。

4.2 Biased RIS

我们将第 \(i\) 次采样所服从分布的概率密度写为 \(p_i(x_i)\),候选样本的联合概率密度有 \[ p(\boldsymbol{x})=\prod\limits^M_{i=1}p_i(x_i) \] \(\eqref{discrete PDF}\) 式中候选样本的概率密度重写为 \[ p(z|\boldsymbol{x})=\frac{\textup{w}_z(x_z)}{\sum^M_{i=1}\textup{w}_i(x_i)} \quad where \space \space \textup{w}_i(x_i)=\frac{\hat{p}(x_i)}{p_i(x_i)} \] 候选样本与选取的样本索引 \(z\) 的联合概率密度为 \[ p(\boldsymbol{x},z)=p(\boldsymbol{x})\cdot p(z\mid \boldsymbol{x})=\left(\prod\limits^M_{i=1}p_i(x_i)\right)\cdot \frac{\mathrm{w}_z(x_z)}{\sum^M_{i=1}\mathrm{w}_i(x_i)} \tag{11} \label{joint PDF} \] 对于给定样本值 \(y\),可能有非常多的 \((\boldsymbol{x},z)\) 组合,对于采样出值为 \(y\) 的样本 \(x_i\) 要满足 \(p_i(y)=p(\boldsymbol{x},i)>0\),因此所有可能情况有: \[ Z(y)=\{i\mid 1\leq i\leq M \wedge p_i(y)>0 \} \tag{12} \label{all z} \] 因此最终输出 \(y\) 的 PDF 可以由 \(\eqref{joint PDF}\) 的联合概率密度和 \(\eqref{all z}\) 的所有可能取值得到 \[ p_y(y)=\sum\limits_{i\in Z(y)}\underbrace{\int\cdots\int}_{M-1} p(\boldsymbol{x}^{i\rightarrow y},i) \space \underbrace{dx_1\cdots dx_M}_{M-1} \tag{13} \label{y PDF} \] $p(^{iy},i) $ 是参照 \(\eqref{joint PDF}\) 代入 \(z=i,x_i=y\) 得到的 \((x_1,\cdots,x_{i-1},y,x_{i+1},\cdots,x_M)\) 的联合概率密度,对每种可能取值下的联合概率密度求 \(y\) 的边缘概率密度 \(p_i(y)\),并求和得到 \(p_y(y)\)

验证 RIS 是否无偏,需要知道 \(\eqref{RIS weight}\) 中描述的权重的期望是否为 \(1/p_y(y)\),求解其期望如下: \[ \mathop{\mathbb{E}}\limits_{x_z=y}\left[\textup{W}(\boldsymbol{x},z)\right]=\frac{1}{p_y(y)}\frac{|Z(y)|}{M} \tag{14} \label{weight expectation} \]\(\eqref{all z}\) 可知,只有在所有候选样本分布都不为 0 时,上式的权重期望才为 \(1/p_y(y)\),RIS 才是无偏的

如果部分积分区域上的 source PDF 为 0,则有 \(\large\frac{|Z(y)|}{M} < 1\), 此时,重要性采样的 inverse PDF 会偏小,积分结果会偏小,带来能量损失。

4.3 Unbiased RIS

上一节给出了 RIS 有偏的情况,即会有部分样本的 source PDF 为 0。这是由于 visibility 项导致的,我们在复用其他像素的 reservoir 时,并没有考虑这些 reservoir 样本是否对当前像素可见。同样,在执行 算法 3 streaming RIS 时,会根据 shadow ray 丢弃掉部分不可见候选样本,但在复用阶段,这些被丢弃的样本可能对当前像素是可见的,这就损失了部分能量。

因此若想得到无偏 RIS 则需要对 \(\eqref{RIS weight}\)\(\eqref{weight expectation}\) 的候选样本的总数 \(M\) 进行调整,调整为当前像素可见的候选样本总数,设置 \(m(x_z)=\frac{1}{M}\),新的权重及其期望形式为 \[ \begin{align} \mathop{\textup{W}}\limits_{x_z=y}(\boldsymbol{x},z)&=\frac{1}{\hat{p}(x_z)}\left[m(x_z)\sum\limits^M_{j=1}\textup{w}(x_j)\right] \\ \mathop{\mathbb{E}}\limits_{x_z=y}\left[\textup{W}(\boldsymbol{x},z)\right]&=\frac{1}{p_y(y)}\sum\limits_{i\in Z(y)}m(x_z) \end{align} \] 因此当 \(\sum\limits_{i\in Z(y)}m(x_z)=1\) 时,RIS 为无偏估计。如何保证这一条件?

4.4 A Practical Algorithm for Unbiased Reuse

本文使用光源分布作为 source PDF,候选样本以及选出的 reservoir 样本都是光源面上一点。并且对于直接光照采样,只会采样着色点可见光源,即 \(\eqref{weight expectation}\) 中使用的候选样本分布都不为 0,因此对于初始候选样本重采样的过程是无偏的。但对于reservoir 重用过程,新的 reservoir 的 source PDF 变成了,每个 reservoir 样本的分布,这个是未知的,因此重用过程的 source PDF 可能互不相同。重用过程引入 bias 的原因是:尽管复用 reservoir 对其像素可见,但对于当前像素可能不可见,这就导致了新 reservoir 的部分 source PDF 为 0,为 \(\eqref{weight expectation}\) 引入了 bias。

论文中提出一种无偏 RIS 算法,该算法基于一种观察:尽管复用 reservoir 样本 PDF (即新 reservoir 的 source PDF)不可知,但当样本对当前像素不可见时,其 PDF 为 0。因此可以在复用 reservoir 样本之前,先进行 visibility 测试,如果不可见,设置其 PDF 为 0,即丢弃掉该样本,也就是 visibility reuse。相比有偏算法,无偏算法开销很大,因为需要对每个复用 reservoir 样本发射一条 shadow ray。

5 Design and Implementation

作者为有偏与无偏算法选用不同的参数配置,以达到两种算法的开销近似。

\(M=32\) 个初始候选样本的生成:根据三角面片发光的 power 重要性采样选择一个三角形,然后在三角形上均匀采样一个点 \(x\),即 source PDF \(p(X)\propto L_e(x)\)。如果有环境光贴图,那么 25% 的候选样本由重要性采样环境光贴图生成。重要性采样发光三角形与环境光贴图都是离散采样,采用 alias table [2]。同时,作者也测试了通过预计算为场景的发光三角形生成 VPL 集合,虽然能够提高性能,但有 visual artifact。

Target PDF:在重采样步骤中,需要 target PDF 对选出的样本进行加权,作者使用 unshadowed path contribution 作为 target PDF,即 \(\hat{p}\propto \rho\cdot L_e\cdot G\)。作者为场景所有几何使用统一的材质,Lambertian diffuse 和 dielectric GGX microfacet 。而对于复杂的材质模型,为每个候选样本计算会更耗时。

Neighbor Selection:对于 Spatial Reuse,在 30-pixel 半径范围内采样 \(k\) 个像素进行复用,无偏选用 \(k=3\),有偏选用 \(k=5\)。对于 Temporal Reuse,则使用 motion vector 找到历史帧对应的像素。

Biased RIS 降低偏差:对于有偏算法,重用来自不同几何/材质的像素会增加偏差,因此作者使用一种简单的启发式策略拒绝这部分像素。比较重用像素与当前像素的到相机距离、normal 夹角,拒绝超过预设阈值的像素(10% 当前像素的深度和 25°)。

Evaluated Sample Count:对于 算法 5 ,每个像素存储 \(N\) 个 reservoir,无偏算法采用 \(N=1\),有偏算法采用 \(N=4\)

Reservoir storage and temporal weighting:每个像素只存储选出的样本 \(y\) 及其权重 \(W\),候选样本的数量 \(M\)。对于 temporal reuse,对当前像素产生贡献的候选样本数量会无限制增加,因为每一帧都结合了历史帧的 reservoir。在重采样过程中,这样会导致 temporal sample 加权高度不成比例,作者将历史帧的候选样本数量限制在当前帧 \(20\times M\) 内,这样就限制了时序信息的影响。

Reference

[1] Benedikt Bitterli, Chris Wyman, Matt Pharr, Peter Shirley, Aaron Lefohn, and Wojciech Jarosz. 2020. Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting. ACM Trans. Graph. 39, 4, Article 148 (August 2020), 17 pages. DOI:https://doi.org/10.1145/3386569.3392481

[2] Alastair J Walker. 1974. New fast method for generating discrete random numbers with arbitrary frequency distributions. Electronics Letters 10, 8 (1974), 127–128.

Appendix

1. 重要性采样为何要到 Resample Importance Sampling

蒙特卡洛积分估计理论上是进行越多的采样次数,越接近 groud-truth,但实际中要限制开销,只能以有限的采样次数来进行积分估计。因此,采样分布如果能够更倾向于采样被积函数较重要的区域,则会更快达到收敛,这就是重要性采样 (IS) 的思想。\(\eqref{IS MC}\) 式描述的重要性采样(IS)是蒙特卡洛积分的最简单的无偏估计,因为 IS 只采样了一种分布,例如 cosine-weighted sample hemisphere 就是根据渲染方程 cosine 项的特征得到的采样方法。

但通常渲染方程被积函数包含多项,如 \(\eqref{simple render equation}\) 中的 \(\rho,L_e,G\) 等。于是又有了,\(\eqref{MIS MC}\) 式描述的多重重要性采样,MIS 使用了多种采样策略,每种采样策略都进行了一次重要性采样,最后将多次重要性采样结合在一起。虽说,MIS 考虑了多种分布,但其估计量形式相当于是对多种分布的线性组合,而对于渲染方程而言,其被积函数是多项的乘积形式,因此不能更好地采样被积函数的重要性区域。

对于多种分布的乘积形式,是无法直接采样的。但可以使用 Resample Importance Sampling(RIS) \(\eqref{1-sample RIS}\) 来近似成比例地采样多种分布乘积形式。

2. RIS 权重期望

\[ \begin{align} \mathop{\mathbb{E}}\limits_{x_z=y}\left[\textup{W}(\boldsymbol{x},z)\right]&=\underbrace{\int \cdots \int}_M\frac{ \textup{W}(\boldsymbol{x},z,x_z=y)\cdot p(\boldsymbol{x},z,x_z=y)}{p_z(x_z=y) \space } \space dx_1\cdots dx_Mdz \\ &\overset{\eqref{y PDF}}{=}\sum\limits_{i\in Z(y)}\underbrace{\int\cdots\int}_{M-1} \frac{ \textup{W}(\boldsymbol{x}^{i\rightarrow y},i)\cdot p(\boldsymbol{x}^{i\rightarrow y},i)}{p_y(y) } \space dx_1\cdots dx_M \\ &= \sum\limits_{i\in Z(y)} \frac{\underbrace{\int\cdots\int}_{M-1} \textup{W}(\boldsymbol{x}^{i\rightarrow y},i)\cdot p(\boldsymbol{x}^{i\rightarrow y},i)\space dx_1\cdots dx_M}{p_y(y)} \\ &= \frac{1}{p_y(y)}\cdot \sum\limits_{i\in Z(y)} \underbrace{\int\cdots\int}_{M-1} \textup{W}(\boldsymbol{x}^{i\rightarrow y},i)\cdot p(\boldsymbol{x}^{i\rightarrow y},i)\space dx_1\cdots dx_M \\ &\overset{代入 \eqref{RIS weight},\eqref{joint PDF}}{=} \frac{1}{p_y(y)}\sum\limits_{i\in Z(y)}\int\cdots\int \left(\frac{1}{\hat{p}(x_i)}\frac{1}{M}\sum\limits^M_{j=1}\textup{w}(x_j)\right) \left(\prod\limits^M_{j=1}p_j(x_j)\right) \frac{\textup{w}_i(x_i)}{\sum^M_{j=1}\textup{w}_j(x_j)} \space dx_1\cdots dx_M \\ &= \frac{1}{p_y(y)}\sum\limits_{i\in Z(y)}\int\cdots\int \left(\frac{1}{\hat{p}(x_i)}\frac{1}{M}\right) \left(\prod\limits^M_{j=1}p_j(x_j)\right) \textup{w}_i(x_i) \space dx_1\cdots dx_M \\ &= \frac{1}{p_y(y)}\sum\limits_{i\in Z(y)}\left(\frac{p_i(x_i)}{\hat{p}(x_i)}\frac{\textup{w}_i(x_i)}{M}\underbrace{\int\cdots\int \prod\limits^M_{j\neq i}p_j(x_j) \space dx_1\cdots dx_M}_1\right) \\ &=\frac{1}{p_y(y)}\sum\limits_{i\in Z(y)}\left(\frac{p_i(x_i)}{\hat{p}(x_i)}\frac{\textup{w}_i(x_i)}{M}\right) \\ &= \frac{1}{p_y(y)}\sum\limits_{i\in Z(y)}\frac{1}{M} \\ &= \frac{1}{p_y(y)}\frac{|Z(y)|}{M} \end{align} \]

1 Summary

Glossy Probe Reprojection for Interactive Global Illumination [8]

这篇论文提出了一种基于 light probe 思想的实时全局光照算法,高效解决带有 glossy 路径的全局光照问题。light probe 思想在场景中放置多个 light probe,每个 light probe 包含了该位置的间接光照信息,这些信息通过预计算过程存入 light probe,并用于在实时渲染过程中计算着色点的全局光照。本论文提出自适应的 probe 参数化,通过评估不同角度、不同位置的几何信息,自适应不同情况所需要的 probe 分辨率,用以缓解 glossy 全局光照带来的高内存消耗问题。在实时渲染中提出基于 specular path perturbation 的间接光反射点估计方法,高效 reproject glossy path 信息到实时的 novel view space,进而收集到实时全局光照信息。同时论文改善了 occlusion boundaries 附近出现的硬边界问题,通过预计算过程中降低几何 roughness 来预计算 probe data,以收集到更大范围的全局光照信息,在实时渲染中设计 filter 重构原 roughness,达到硬边界的平滑过渡。

2 Background

2.1 Light Probe

3 Approach

3.1 Probe 预计算

Probe 中存储实时渲染过程中需要使用的全局光照信息,通过 path-tracer 计算得到。glossy BRDF 需要较多的采样才能得到平滑的着色,如果在整个场景中的每个 probe 的所有方向都收集同样多样本的信息,会使得 probe 数据存储消耗过大。因此作者提出 adaptive probe resolution,即根据不同位置、不同视角观察到的几何信息来评估该位置需要的 probe resolution,高分辨率对应高样本数,低分辨率对应低样本数。先介绍一下本论文使用的 probe data 存储的数据

3.1.1 Per Probe Data

预计算过程中,为每个 Probe 生成数据,包含三张贴图,如下图所示:

image-20220717214054549image-20220717214112108image-20220717214125605

    1. 可见的三角形信息(三角形 ID、重心坐标)
    1. probe 空间的环境贴图(只存储 glossy BRDF 计算的颜色值)
    1. 将每个面视为镜面,记录完美反射过程中的几何信息(反射点、及其材质 ID)

这三张贴图都是 360 度贴图,将 probe 作为相机,贴图中每个 texel 存储的是不同角度看向场景得到的信息,即 probe view 下的渲染信息。

3.1.2 Probe Parameterization

对于有限的内存消耗,probe 数量是有限的,这篇论文提出的 adaptive probe resolution 是为了将更多地 probe resolution 分配到需要高采样的区域(因为同样的 fov 下,probe 数据的分辨率越高,包含采样的样本越多)。作者设计了一种 probe 需求的评估策略,评估结果为一个方向看到的场景所需的 probe resolution 参数。根据评估得来的 probe resolution 不断调整 probe 对应 3D grid 的等划分网格,最终达到网格大小与 probe resolution 相匹配,即 probe resolution 越大,网格越大。对网格中像素对应的场景角度,使用 path tracer 计算 probe 离线数据。

如何确定什么情况需要较高的 probe resolution 或较低的 probe resolution?

    1. 因为光滑表面频率信息较高,因此越光滑的表面需要越高的分辨率,反之粗糙的材质频率较低,需要较少的样本
    1. 远距离表面或者 grazing 角度看到的表面需要较高的分辨率
    1. 高频几何(法线)信息在 reflected radiance 上表现出很大的变化,因此需要较高的分辨率。

根据上述要求,论文使用 lat-long (相当于球坐标) 对 probe 分辨率参数化。在 probe view space 中,计算每个角度对应的 probe resolution 参数,并存储到 adaptive resolution map 中。lat-long 参数化易于确定 adaptive resolution map 的每个像素对应的 ray direction,方便 probe data预计算过程的 ray-tracing。

3.1.2.1 Adaptive Resolution Map 计算

一个 Adaptive resolution map 的元素表示 probe 每个方向对应的 probe resolution 参数。计算该 Map 前先使用以 probe 出发的 ray casting 构建 4 个 256x128 的 buffers,(i) 材质 smoothness \(\large m_{mat}\),(ii) 深度值 \(\large m_{depth}\),(iii) 法线 \(\large m_{norm}\),(iv) facing angle \(\large m_{face}\) (入射光方向与表面法线点乘)。

Adaptive resolution map 的元素计算:

\[ m=m_{mat}(m_{size}+m_{complexity}) \] 可以看出

  • \(m_{mat}\)​ 满足了 a),越光滑 \(m_{mat}\)​ 越大,得到的分辨率参数 \(m\)​ 也越大;

  • \(m_{size}\)​​​​ 考虑了 b) 中距离与角度的要求,其计算为 \[ m_{size}=\frac{m^2_{depth}}{m_{face}}\cos(\theta_{long}) \]

    depth 越大,表面面积相对越小,对应较大的 \(m\)​;\(m_{face}\)​​ 越小,表示入射光与 normal 夹角越大,越 grazing,对应较大的 \(m\)

  • 要求 c) 需要对局部几何复杂度分析,作者使用频率分析代替: \[ m_{complexity}=\frac{1}{N^2}\sum\limits^{N-1}_{p=0}\sum\limits^{N-1}_{q=0}w_{p,q}||\mathbf{b}_{p,q}*m_{norm}||_1 \]

    \(w_{p,q} =||[p,q]||^k\) 权重用来确保高频对几何复杂度贡献更高,论文中取 \(N=16,k=5\)

\(m_{size}\)\(m_{complexity}\) 使用均值经过归一化。

上述 Adaptive Resolution Map 只是计算了 probe 在不同方向上的分辨率参数,但这个参数只是相对分辨率,反应不同方向上分辨率的大小关系,而不是真正的分辨率。接下来需要根据这个相对分辨率参数,进行对 probe 真正分辨率的参数化。

3.1.2.2 Adaptive Parameterization

本小节,作者使用 tensorial quasi-harmonic maps [3],将 adaptive resolution map 转为 adapted resolution。转换过程如下:

  1. 将每个 proble 对应的 3D grid 进行离散等划分,得到均分的四边形网格 quad mesh (应该是 3D grid 的六面划分成 四边形网格),并且 adaptive resolution map 中每一个元素对应一个 quad mesh。
  2. 根据 adaptive resolution map ,求解 quasi-harmonic equation ,得到 forward flow map。
  3. forward flow map 指示如何调整 quad mesh 的顶点,不断对 quad mesh 变形,最终达到与 apaptive resolution map 相一致的参数化。probe 对应的 3D grid 最终变形为如图 Fig. 1.2.2(h) 所示。

至此,变形的 quad mesh 达到与 adaptive resolution map 描述一致的 adaptive resolution,即 adaptive resolution map 较大的元素对应较大的网格,较小的元素对应较小的网格。下面就需要计算不同分辨率的 probe data,即对网格的每个像素进行 path-tracing 计算 probe data。但由于变形,这里的像素位置已经不能确定 path-tracing 的光线方向,即像素的 view vector。作者将 forward flow map 进行求逆,得到 inverse flow map,这样就可以确定像素真正的 view vector。

image-20220717214426137

Fig. 1.2.2

全局光照包含两个组成成分 glossy 和 diffuse:glossy 成分存储在 probe 数据中,在计算 probe 时,path-tracer 计算 glossy BRDF;Diffuse 成分与 view 视角无关(view-independent),因此存储在传统的 lightmap 中。

3.1.3 Geometric Information

上一小节完成了 probe 中包含的全局光照信息的预计算,但 probe 贴图数据中包含的是 360° 视角的全局信息,在实时渲染中,需要确定当前着色点需要哪些 probe 数据的样本。对于此,作者使用 specular path perturbation 的方法,这个在之后章节阐述,但该方法需要几何面的导数信息。

因此本小节预计算场景几何信息,估计场景中每个顶点的主曲率,以此将 mesh 近似为具有解析表达式形式的抛物面,这样的抛物面可以高效计算出其高阶导,用于实时渲染中收集 probe 样本的 glossy light 信息。过程如下:

  1. 计算来的主曲率可以以曲率方向为坐标轴将物体表面局部表示为具有解析表达式的抛物面 Fig 1.3(c),该解析表达式方便后续高效计算其高阶导。

image-20220717214455780image-20220717214514069

Fig 1.3

至此,预计算部分已经结束,下面开始实时渲染。

3.2 Rendering Global Illumination

上述预计算的数据都是 probe view 下的光照信息,实时渲染中则需要将 probe view 重投影为 novel view 下所需要的全局光照信息。全局光照信息包含 diffuse 与 glossy 光照信息,对于 diffuse 全局光照,直接采样预计算的 lightmap。而 glossy 成分则需要对 light probe 重投影到 novel view。

glossy 光线路径的全局光照渲染概要:

  1. 需要将预计算的 light probe 进行重投影(reprojection)。为此,先光栅化一个 G-buffer,其中包含 position、normal、表面曲率和材质(ID和roughness)。

  2. 使用 ray-caster,发射光线到 specular pixels(应该指把像素看作 specular),再经过完美镜面反射。ray data 中存储反射光的 hit position 和对应的材质 IDs。

  3. 之后基于像素附近收集的 probes 信息来计算像素的 glossy 光照信息。

    • 选择距离着色点最近的 probe(最终方案为最近的 8 个),确定 probe data 中与当前像素关联的 sample:计算 probe 经过镜面反射最终到达 ray data 中的 hit position 的反射点,probe data 中该反射点对应的 sample 即包含了着色像素的 glossy GI 信息。(因为 camera -> shading point -> hit position -> specular reflected position -> probe 正好形成了一条带有间接光照的光线路径,前三个是一个镜面反射路径(从 ray data 中获取),后三个是一个镜面反射路径(由选定的 probe 位置计算得到))。

      如图 Fig 2(a) 所示,光线路径为 \(\mathbf{p}\rightarrow \mathbf{x} \rightarrow \mathbf{q} \rightarrow \mathbf{x'} \rightarrow \mathbf{p'}\)​,其中 \(\mathbf{x}\) 为像素对应的着色点,\(\mathbf{q}\) 为经过镜面反射的 hit position(存储在 ray data 中),\(\mathbf{p'}\) 为​挑选的 probe,\(\mathbf{x'}\) 为由 \(\mathbf{p'}\)\(\mathbf{q}\)​ 作为镜面反射路径端点计算得到。

    • 基于一种设计的评估策略,在反射点对应的 probe sample 附近搜索选择最优的 probe sample,即为最终用来计算着色点 GI 的 sample。

image-20220717214548051

Fig 2

由上述可知,想要收集 probe samples 信息,必须先知道反射点 \(\mathbf{x'}\) 的位置。虽然可以再次使用 ray-caster,但为了高效,作者引入 specular path perturbation 方法。

3.2.1 On-the-fly Reflection Position Estimation

当相机由 \(\mathbf{p}\)​​​ 移动到 \(\mathbf{p'}\)​​​ 时,作者基于 specular path perturbation 理论来确定 \(\mathbf{x}\)​​​ 移动到的位置 \(\mathbf{x'}\)​​​。这样的移动描述的其实是镜面反射路径\(\mathcal{S}_1\)​​​(\(p\rightarrow x\rightarrow q\)​​​) 到 \(\mathcal{S}_2\)​​​(\(p'\rightarrow x'\rightarrow q\)​​​​) 的 specular motion。其中反射路径 \(\mathcal{S}_1\)​​​​ 在 G-buffer 光栅化后已经确定,那么只有 \(\mathbf{x'}\)​​​ 是未知的。

至此,已经计算出 \(p\rightarrow x \rightarrow q \rightarrow x' \rightarrow p'\) 光线路径中 \(x'\) 的位置,作者对于得到的 \(x'\) 做了额外的处理,只保留和 \(x\) 具有相同材质的 \(x'\) 点。下面可以使用 \(x'\) 来收集 probe data 中的 probe samples,从而得到 probe data 中存储的 glossy GI 信息。

3.2.2 Gathering View-dependent Color

结合 specular path perturbation 和预计算中表面曲率估计的方法,只是得到了 novel view 到 probe view 的 specular motion。为了得到更加鲁棒的结果,需要在 \(x'\) 对应的 probe sample 的区域中搜索,选取最优的 probe sample。

\(p\rightarrow x \rightarrow q \rightarrow x' \rightarrow p'\) 光线路径中,\(p'\rightarrow x'\rightarrow q\) 为镜面反射的路径,但 probe data 中存储的是 glossy GI,glossy BRDF 是一个 lobe,光线路径对应的是一个区域,只要最终能到达 \(q\) 即为所找 probe sample。因此,\(x'\) 对应的样本附近区域也属于该镜面反射对应的 glossy 路径。因此可以在这个区域的 probe samples 中选取最优的 sample。注意:probe data 中的每个 sample 携带的都是其 probe 对应的 3D grid 区域的 glossy BRDF 的全局光照信息,若要使得像素的全局光照信息更加平滑,应收集不同区域 (即不同 probe) 的全局光照信息。

probe sample 搜索算法:使用 two-level 的 grid 搜索算法,coarse-level 在较大的区域内使用较大的 step-size 进行搜索,得到 coarse-level 的最优 sample。然后以这个最优 sample 为中心,使用较小的 step-size 进行 fine-level 搜索。

评估 probe sample 的优劣:接下来使用 Fig 2(b) 中的符号,probe view 下的 reflector position \(r_p\)、normal \(n_p\) 和 reflected position \(R_p\);novel view 下对应的 \(r_v\)\(n_v\)\(R_v\)​ 。设计 energy function 来评估 probe sample 优劣的四个标准:

  • novel view sample(\(R_v\)) 和 probe view samples(\(R_p\)) 更倾向于位于同一表面。如果二者的材质 ID 不同,则惩罚总 energy,即乘上 \(\large s_a=10\). 因此只有在其他样本不可用时,才会选用材质不匹配的样本。

  • \(R_v\)\(R_p\) 距离近更好,对此引入 \(s_b=||R_v-R_p||\).

  • 相近的表面法线 \(n_v\)​ 和 \(n_p\)​ 可以确保一致的光照。对此引入 \(s_c=1-(n_v\cdot n_p)\)

  • 样本应该具有相近的 reflected ray,对此引入: \[ s_d=1-\frac{(R_v-r_v)\cdot (R_p-r_p)}{||R_v-r_v||\cdot||R_p-r_p||} \]

结合以上标准,设计 energy function 为:

\[ \varepsilon=s_a\cdot \big(min(s_b,1)+min(s_c,1)+min(s_d,1)\big) \tag{1} \label{energy function} \] 由于预计算 probe data 过程中应用了 apaptive parameterization,因此在搜索 sample 邻域中不能按常规使用固定大小的 grid(即搜索算法中的步长大小单位不应该固定),因为这样会在压缩区域中丢失细节,或在放大区域中不充分搜索。对于此,作者对 step size 进行一次 scale,即步长(grid 数量)要乘以

\[ |\frac{\partial f^{-1}_h}{\partial x}|^{-1} \] 其中 \(\large f^{-1}_h\)​​ 是预计算过程中生成的 inverse flow field。在作者实验中,coarse-level 搜索区域大小为 7X7 samples,步长为 4 texels;fine-level 搜索区域大小为 3X3 samples,步长为 2 texels。(步长都是 scale 前的)。

至此已经完成在 probe data 中选取最优 probe sample 的方法。下面将 probe sample 中的 glossy GI 信息加入到像素的最终着色中。

如果简单的只收集一个 probe 的 probe sample 中的 glossy GI,相机移动过程中会出现 pop-up 不平滑现象。作者使用距离 novel view 最近(即距相机最近)的 8 个 probe,每个 probe 选取一个最优 probe sample,最终以以下方式整合: \[ C=\frac{1}{Z}\sum\limits^8_{i=1} t_i\cdot exp(-\phi\varepsilon_i)\cdot c_i \tag{2} \label{the sum of neighboor probes} \] 其中 \(t_i\)​ 是 trilinear weight;\(\phi\) 是 falloff 因子,论文中设为 8;\(Z\) 是归一化系数,确保加权和为一个 unity;\(c_i\) 从 probe data 中得到,当 sample 没有 color-blend 时,在加载 \(c_i\)​​ 过程中使用 bilinear 插值。对于没有找到有效 sample 的像素,对上一帧的 probe 进行 reprojection。

3.3 Two-step Convolution For Accurate Warping of Glossy Probes

上述描述的都是完美镜面反射的着色点,即 \(p\rightarrow x\rightarrow q\) 为镜面反射路径,但实际中着色点更多的还是 glossy 材质。对于 glossy 材质的着色点,其对应的 hit position \(q\) 会变为一个区域,即 glossy lobe 对应的区域。任何可以到达该区域的光线都可以复用该区域内对应的 probe samples。

对于一种特殊情况,两个相邻着色点的 BRDF lobe 的所有 probe samples 正好分别落在了 occlusion 边界两侧,这样就形成了明显的硬边界。图 Fig 3 形象描述了出现这种情况的原理,Fig 3(e) 中两个像素对应的着色点的 BRDF lobe 包含了黄色和蓝色两个平面的信息,这是 ground truth Fig 3(a) 对应的 BRDF lobe 积分情况;简单的 reprojection 收集的 probe samples 正好落在了遮挡的一侧 Fig 3(f),使得模糊的边缘变得 sharpen Fig 3(d);如果仅进行一次简单的 post-process filter,会导致 overblur,使得表面视觉 roughness 增大 Fig 3(g),注意着色像素的颜色(roughness 增大,导致像素包含了更大区域的信息,最终颜色过于平滑)。

image-20220717214753044

Fig 3

本章节设计新的 filter,减小 Naive post-process filter 造成的 overblur。作者使用 Gaussian 分布拟合 BRDF,利用 Gaussian 分布卷积中的可分离特性,将 BRDF 包含的卷积进行分离为两步。在预计算过程中,减小表面 roughness,设计实时 image filter 重构原 roughness

这种聚集 occlusion 边界一侧的现象可以理解为采样不足导致的,本章节提出在不提高采样数量的情况下,在 probe 预计算阶段降低几何表面的 roughness,这样会使得反射的光线更发散,probe sampe 更容易收集到周围着色点信息。在实时渲染过程中,再使用设计的 filter 重构原 roughness,得到原 roughness 下的周围着色点信息。

3.3.1 Filter Footprint Estimation

为了使用 Gaussian 分布可分离特性,首先需要使用 Gaussian 分布拟合 BRDF,为此需要估计其 footprint(指不同参数变化下 BRDF 的变化足迹)。为了估计 BRDF footprint,作者使用了一个简单的设定,如 Fig 3.1(a) 所示:相机通过看向各向同性均匀 roughness 的反射平面(\(\mathbf{x}\)​​​​​​​​)来观察 reflected point,其中反射平面法向量与 view direction 平行,并且与相机和 reflected point 的共线并行。基于这种设定,以固定的 fov 评估每一个点的 GGX BRDF(微表面法线模型) [6],如 Fig 3.1(b) 所示,中间区域最亮应该是直射区域,越往外侧越暗,GGX BRDF 的 specular lobe 反射到相机的越来越少。

影响 BRDF footprint 的参数有评估点的 roughness \(\rho\)​ 、相机到反射平面的距离 \(d_c\)​、反射平面到反射点的距离 \(d_r\)​​,给定这些参数,可以拟合一个协方差矩阵为 \(\sum_{\rho}\)​ 的 Gaussian 分布 \(\mathcal{G}_{\rho}(\mathbf{x}:\rho,d_c,d_r)\)​ 。协方差矩阵 \(\sum_{\rho}\)​ 可以通过选定一组 \([\rho, d_c,d_r]\)​ 参数,采样 BRDF footprint 来确定。​例如 Fig 3.1(c) 展示的是一条扫描线(相机移动轨迹)下的 BRDF 随 \(\mathbf{x}\)​ 变化的曲线。作者通过预计算不同参数组合下的协方差表格,供实时渲染时查表。该制表包含 32x32x32 bins,roughness 由 \(0\)\(0.5\) 、距离由 \(0.01\)\(10m\)。并且采样过程选取 power sampling,在 \(d_r<0.5m\)​ 降低协方差,防止这种情况下的拟合 overestimated 协方差。

image-20220717214823052

Fig 3.1

上述简单设定是 view direction 与反射平面垂直的情况,当二者不垂直时,即反射平面为一个倾斜平面,会使得 BRDF footprint 会由于远小近大而沿着倾斜方向缩小。在渲染时,通过将协方差乘上 view direction 和 surface normal 的点乘来进行补偿。

使用 Fig 3.1(a) 中的设置在预计算过程中拟合出 \([\rho, d_c,d_r]\) 参数不同组合下的 BRDF footprint 的 Gaussian 分布,然后在实时渲染中,根据当前着色点的 \([\rho, d_c,d_r]\) 查表获取该点的 BRDF footprint。但实时渲染中着色点、相机、反射平面多数不是 Fig 3.1(a) 的简单设定,需要通过对协方差参数进行 scale 补偿,scale 系数为 view direction 和 surface normal 的点乘。

假设 \(\mathcal{G}_{\rho}\) 是反射平面的 BRDF,在 glossy light probes 预计算中获取;\(\mathcal{G}_I\) 是 runtime image filter。我们可以在预计算中降低反射平面的 roughness,得到具有对应新 roughness \(\rho'\) 的协方差矩阵 \(\sum_{\rho'}\)\(\mathcal{G}_{\rho'}\). 使用 Gaussian 分布的性质:

\[\sum(\mathcal{G}_1*\mathcal{G}_2)=\sum(\mathcal{G}_1)+\sum(\mathcal{G}_2)\]​​, 其中 \(\sum\) 是卷积的求和操作,其余为协方差矩阵

现在需要求得一个 image filter \(\mathcal{G}_I\) 将降低过 roughness \(\rho '\)\(\mathcal{G}_{\rho'}\) 重构为原 roughness 的 \(\mathcal{G}_{\rho}\)​. 利用上述性质,可以得到

\[ \sum_I=\sum_{\rho}-\sum_{\rho'} \] 从而得到 \(\mathcal{G}_I\) (因为这里所有的 Gaussian 分布都采用均值为 0)。在实现中,作者使用 \(\rho '=\rho/2\)​ 作为预计算中光线路径中的第一个 glossy 顶点的 roughness 参数。

至此,已经完成对 image-space filter footprint 的估计。下面是如何应用 image filter。

3.3.2 Gloss Filtering

使用几何数据来估计公式 \(\eqref{the sum of neighboor probes}\) 中的 \(C\)\[ \hat{C}(\mathbf{x})=\frac{1}{Z(\mathbf{x})}\sum\limits_{\mathbf{x}_i\in\mathcal{N}(\mathbf{x})}\mathcal{G}_I(\mathbf{x}_i-\mathbf{x})w_r(\mathbf{x,x}_i)\space\varepsilon^{-1}(\mathbf{x}_i)C(\mathbf{x}_i) \tag{3} \label{glossy filtering} \] 其中,\(\mathcal{N}(\mathbf{x})\)​​​​ 表示 \(\mathbf{x}\)​​​​ 处的 filter footprint;\(\large \varepsilon\)​​​​ 是 energy function \(\eqref{energy function}\)​​​​,其逆函数作为置信度来限制向匹配良好的像素的传播;range weight:

\[ w_r(\mathbf{x,x}_i)=\mathbb{1}_{\mathbf{n(x)\cdot n}(\mathbf{x}_i)>\alpha_\mathbf{n}}\cdot \mathbb{1}_{|d(\mathbf{x})-d(\mathbf{x}_i)|<\alpha_d}\cdot \mathbb{1}_{m(\mathbf{x})=m(\mathbf{x}_i)} \] 作为 cross-bilateral 项,防止不连续的 normal 和 depth 或者不同反射材质 IDs 之间的 filtering. 实现中,作者设置 \(\alpha_{\mathbf{n}}=0.8\)​ 和 \(\alpha_d=0.2\)​;\(Z\)​​ is the normalizing partition function, ensuring filter weights sum to unity。

求和的区域应该是 glossy BRDF lobe 对应的区域,对这个区域进行 filter

3.3.3 Efficient Filter Approximation

4 Remain Questions

Reference

[1] Morgan McGuire, Mike Mara, Derek Nowrouzezahrai, and David Luebke. 2017b. Realtime global illumination using precomputed light field probes. In Proceedings of the Symposium on Interactive 3D Graphics and Games. ACM, 2

[2] Nasir Ahmed, T Raj Natarajan, and Kamisetty R Rao. 1974. Discrete cosine transform. IEEE Trans. Comput. 100, 1 (1974), 90–93.

[3] Rhaleb Zayer, Christian Rossl, and H-P Seidel. 2005. Discrete tensorial quasi-harmonic maps. In International Conference on Shape Modeling and Applications. IEEE, 276–285.

[4] Mark Meyer, Mathieu Desbrun, Peter Schröder, and Alan H Barr. 2003. Discrete differential-geometry operators for triangulated 2-manifolds. In Visualization and Mathematics III. Springer, 35–57.

[5] Perturbation methods for interactive specular reflections. IEEE Transactions on Visualization and Computer Graphics 6, 3 (2000), 253–264.

[6] Bruce Walter, Stephen R Marschner, Hongsong Li, and Kenneth E Torrance. 2007. Microfacet Models for Refraction through Rough Surfaces. Rendering Techniques 2007 2007, 18th.

[7] Eduardo SL Gastal and Manuel M Oliveira. 2011. Domain transform for edge-aware image and video processing. ACM Transactions on Graphics (TOG) (2011), 1–12.

[8] Simon Rodriguez, Thomas Leimkühler, Siddhant Prakash, Chris Wyman, Peter Shirley, and George Drettakis. 2020. Glossy probe reprojection for interactive global illumination. ACM Trans. Graph. 39, 6, Article 237 (December 2020), 16 pages. DOI:https://doi.org/10.1145/3414685.3417823