0%

Reblur解析

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