流程主要就是昨天Games202上介绍的三个步骤,理清这三步怎么做的就可以了。下面分析的是Nvidia的实现代码。
Pass1 Reprojection
输入:Color,Emission,Albedo
输出 illumination,moments,historylength
(1)当前帧的illumination:
i l l u m i n a t i o n C u r r e n t = g C o l o r − g E m i s s i o n m a x ( g A l b e d o , e p s ) illuminationCurrent=\frac{gColor-gEmission}{max(gAlbedo,eps)} illuminationCurrent=max(gAlbedo,eps)gColor−gEmission
(2)取上一帧的illumination,moments,historylength
(2.1)如果上一帧对应位置无效则仅以当前帧信息计算
(2.2)如果有效:
1.计算historyLength
historyLength = historyLength + 1.0f
2.计算当前帧的momentCurrent
float2 momentCurrent;
momentCurrent.r = luminance(illumination);
momentCurrent.g = momentCurrent.r * momentCurrent.r;
插值得到moments结果
moments = lerp(prevMoments, momentCurrent, 1/historylength);
3.插值得到当前帧的illuminationCurrent
illumination=lerp(prevIllumination, illuminationCurrent, 1/historylength);
返回结果,
1.返回float historyLength
2.返回float3 moments
3.返回float4 Out_illumination
Out_illumination.xyz=illumination
Out_illumination.w=variance=moments.y - moments.x * moments.x
Pass2 Filter moments and illumination
当historyLength>4时不需要filter了直接将输入输出,如果<4需要filter
Filter过程:
对周围7x7区域加权即可。
权重计算公式如下:
float computeWeight(
float depthCenter, float depthP, float phiDepth,
float3 normalCenter, float3 normalP, float phiNormal,
float luminanceIllumCenter, float luminanceIllumP, float phiIllum)
{
const float weightNormal = pow(saturate(dot(normalCenter, normalP)), phiNormal);
const float weightZ = (phiDepth == 0) ? 0.0f : abs(depthCenter - depthP) / phiDepth;
const float weightLillum = abs(luminanceIllumCenter - luminanceIllumP) / phiIllum;
const float weightIllum = exp(0.0 - max(weightLillum, 0.0) - max(weightZ, 0.0)) * weightNormal;
return weightIllum;
}
总结下来就是如果法线相差,深度,illumination相差越大,filter的权重越小。
两种例外:
第一个,当两个采样点深度相差大但是位于同一表面上时p对中心点的权重应该更大,解决方法是除以phiDepth,它的计算方式是两者采样点之间的距离在深度的导数上的"投影",zCenterDerivat * length(float2(xx, yy))
第二个,两个采样点的illumination相近,原先的权重会很大,但是中心点illumination可能是由于噪声产生过亮导致的和另一采样点相近,所以权重应该更小。修正方法是,如果该点illumination方差过大,则权重应该降低。
由于Pass2中不含illumination方差信息(illumination方差信息是Pass2的输出),所以第二个例外没有考虑,在Pass3中才被考虑。
Pass3 最后filter
为了提高效率,使用a-trous wavelet transform进行filter
下面的步骤迭代多次,每次进行5x5的滤波,每次滤波间隔为
2
i
2^i
2i
1.将Pass2输出的方差在进行3*3的高斯模糊
float computeVarianceCenter(int2 ipos)
{
float sum = 0.f;
const float kernel[2][2] = {
{ 1.0 / 4.0, 1.0 / 8.0 },
{ 1.0 / 8.0, 1.0 / 16.0 }
};
const int radius = 1;
for (int yy = -radius; yy <= radius; yy++)
{
for (int xx = -radius; xx <= radius; xx++)
{
const int2 p = ipos + int2(xx, yy);
const float k = kernel[abs(xx)][abs(yy)];
sum += gIllumination.Load(int3(p, 0)).a * k;
}
}
return sum;
}
2.将步骤1的结果进行5x5的filter,方式和Pass2中filter的方式一样,不过这次需要考虑例外2。
步骤一将Pass2的方差进行了filter,用filter的结果计算:
σ l g 3 × 3 V a r ( l i ( c e n t e r ) ) + e p s \sigma_l\sqrt{g_{3\times 3}Var(l_i(center))}+eps σlg3×3Var(li(center))+eps
对应代码:
const float phiLIllumination = gPhiColor * sqrt(max(0.0, epsVariance + var.r));
Pass4 输出结果
float4 main(FullScreenPassVsOut vsOut) : SV_TARGET0
{
const int2 ipos = int2(vsOut.posH.xy);
return gAlbedo[ipos] * gIllumination[ipos] + gEmission[ipos];
}
Pass1代码
PS_OUT main(FullScreenPassVsOut vsOut)
{
const float4 posH = vsOut.posH;
const int2 ipos = posH.xy;
float3 illumination = demodulate(gColor[ipos].rgb - gEmission[ipos].rgb, gAlbedo[ipos].rgb);
if (isNaN(illumination.x) || isNaN(illumination.y) || isNaN(illumination.z))
{
illumination = float3(0, 0, 0);
}
float historyLength;
float4 prevIllumination;
float2 prevMoments;
bool success = loadPrevData(posH.xy, prevIllumination, prevMoments, historyLength);
historyLength = min(32.0f, success ? historyLength + 1.0f : 1.0f);
const float alpha = success ? max(gAlpha, 1.0 / historyLength) : 1.0;
const float alphaMoments = success ? max(gMomentsAlpha, 1.0 / historyLength) : 1.0;
float2 moments;
moments.r = luminance(illumination);
moments.g = moments.r * moments.r;
moments = lerp(prevMoments, moments, alphaMoments);
float variance = max(0.f, moments.g - moments.r * moments.r);
PS_OUT psOut;
psOut.OutIllumination = lerp(prevIllumination, float4(illumination, 0), alpha);
psOut.OutIllumination.a = variance;
psOut.OutMoments = moments;
psOut.OutHistoryLength = historyLength;
return psOut;
}
Pass3代码
float4 main(FullScreenPassVsOut vsOut) : SV_TARGET0
{
const int2 ipos = int2(vsOut.posH.xy);
const int2 screenSize = getTextureDims(gAlbedo, 0);
const float epsVariance = 1e-10;
const float kernelWeights[3] = { 1.0, 2.0 / 3.0, 1.0 / 6.0 };
const float4 illuminationCenter = gIllumination.Load(int3(ipos, 0));
const float lIlluminationCenter = luminance(illuminationCenter.rgb);
const float var = computeVarianceCenter(ipos);
const float historyLength = gHistoryLength[ipos].x;
const float2 zCenter = gLinearZAndNormal[ipos].xy;
if (zCenter.x < 0)
{
return illuminationCenter;
}
const float3 nCenter = oct_to_ndir_snorm(gLinearZAndNormal[ipos].zw);
const float phiLIllumination = gPhiColor * sqrt(max(0.0, epsVariance + var.r));
const float phiDepth = max(zCenter.y, 1e-8) * gStepSize;
float sumWIllumination = 1.0;
float4 sumIllumination = illuminationCenter;
for (int yy = -2; yy <= 2; yy++)
{
for (int xx = -2; xx <= 2; xx++)
{
const int2 p = ipos + int2(xx, yy) * gStepSize;
const bool inside = all(p >= int2(0,0)) && all(p < screenSize);
const float kernel = kernelWeights[abs(xx)] * kernelWeights[abs(yy)];
if (inside && (xx != 0 || yy != 0))
{
const float4 illuminationP = gIllumination.Load(int3(p, 0));
const float lIlluminationP = luminance(illuminationP.rgb);
const float zP = gLinearZAndNormal[p].x;
const float3 nP = oct_to_ndir_snorm(gLinearZAndNormal[p].zw);
const float2 w = computeWeight(
zCenter.x, zP, phiDepth * length(float2(xx, yy)),
nCenter, nP, gPhiNormal,
lIlluminationCenter, lIlluminationP, phiLIllumination);
const float wIllumination = w.x * kernel;
sumWIllumination += wIllumination;
sumIllumination += float4(wIllumination.xxx, wIllumination * wIllumination) * illuminationP;
}
}
}
float4 filteredIllumination = float4(sumIllumination / float4(sumWIllumination.xxx, sumWIllumination * sumWIllumination));
return filteredIllumination;
}