【Unity3D】HDRP的IBL分析

前言

  基于图像光照(ImageBasedLighting)技术是用来实现全局光照的一种技术,本篇文章是来分析HDRP中IBL实现方法。

理论

渲染方程

  PBR的渲染方程为
  其中:


  BRDF
  遮蔽函数,所以BRDF可简写成

间接光照

  不同与入射光线离散的直接光照,间接光照是离散的,需要对整个半球面进行积分,采用蒙特卡洛积分法,用离散样本求值相加并除以样本总数。
  渲染方程可以先分解为两部分与,分别为Diffuse项和Specular项。

Diffuse间接光照

  可以将积分无关变量提取出来:。
  积分代码参照ImageBasedLighting.hlsl

//real4 IntegrateGGXAndDisneyDiffuseFGD(real NdotV, real roughness, uint sampleCount = 4096)
//.....
ImportanceSampleLambert(u, localToWorld, L, NdotL, weightOverPdf);

void ImportanceSampleLambert(real2   u,
                             real3x3 localToWorld,
                         out real3   L,
                         out real    NdotL,
                         out real    weightOverPdf)
{
    real3 N = localToWorld[2];

    L     = SampleHemisphereCosine(u.x, u.y, N);
    NdotL = saturate(dot(N, L));

    weightOverPdf = 1.0;
}

  u为Hammersley低差序列随机数,计算在球面上光的方向,并计算法线N·L,weightOverPdf是蒙特卡罗积分法的积分函数,weight是计算后的值,这里应该是,不过预计算阶段是没法取到c环境纹理的,所以用分割近似法,将积分拆成,光照项在运行时用球谐近似计算,此时weight是。
pdf是概率密度函数,,可以看到概率密度函数性质
,似乎积分没什么意义。
  不过HDRP用的Diffuse项是DisneyDiffuse,拓展为:

  所以积分的其实是DisneyDiffuse项

//ImportanceSampleLambert(u, localToWorld, L, NdotL, weightOverPdf);

if (NdotL > 0.0)
{
    real LdotV = dot(L, V);
    real disneyDiffuse = DisneyDiffuseNoPI(NdotV, NdotL, LdotV, RoughnessToPerceptualRoughness(roughness));

    acc.z += disneyDiffuse * weightOverPdf;
}

Specular间接光照

  依旧是分割近似求和法,先将光照部分分离积分

采用Trowbridge-Reitz GGX作为NDF:
其中
是半角向量

是粗糙度
Unity的V_SmithJointGGX用来近似GGX-Smith Joint

real GetSmithJointGGXPartLambdaV(real NdotV, real roughness)
{
    real a2 = Sq(roughness);
    return sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
}

real V_SmithJointGGX(real NdotL, real NdotV, real roughness, real partLambdaV)
{
    real a2 = Sq(roughness);

    real lambdaV = NdotL * partLambdaV;
    real lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);

    // Simplify visibility term: (2.0 * NdotL * NdotV) /  ((4.0 * NdotL * NdotV) * (lambda_v + lambda_l))
    return 0.5 / (lambdaV + lambdaL);
}

real V_SmithJointGGX(real NdotL, real NdotV, real roughness)
{
    real partLambdaV = GetSmithJointGGXPartLambdaV(NdotV, roughness);
    return V_SmithJointGGX(NdotL, NdotV, roughness, partLambdaV);
}

  根据siggraph 2012 course,迪斯尼提出来的模型,GGX的重要性采样pdf(概率密度函数)为,推导是:是微表面法线的概率分布函数,我们需要计算的是入射方向的概率分布,涉及到分布变换,要将乘以一个雅可比矩阵,最终得到的概率分布函数就是。
  利用蒙特卡洛积分法

  因为H是L和V的中间半角向量,所以
ImageBasedLighting.hlsl中ImportanceSampleGGX如下:

void ImportanceSampleGGX(real2 u, real3 V, real3x3 localToWorld, real roughness, real NdotV,
         out real3   L,
         out real    VdotH,
         out real    NdotL,
         out real    weightOverPdf)
{
    real NdotH;
    SampleGGXDir(u, V, localToWorld, roughness, L, NdotL, NdotH, VdotH);

    real Vis = V_SmithJointGGX(NdotL, NdotV, roughness);
    //F项放到外面乘
    weightOverPdf = 4.0 * Vis * NdotL * VdotH / NdotH;
}

  F项被放到外面乘,F项也可以分成两部分


  体现在下面是acc的x和y部分

//IntegrateGGXAndDisneyDiffuseFGD
ImportanceSampleGGX(u, V, localToWorld, roughness, NdotV,
                    L, VdotH, NdotL, weightOverPdf);

if (NdotL > 0.0)
{
    // Integral{BSDF *  dw} =
    // Integral{(F0 + (1 - F0) * (1 - )^5) * (BSDF / F) *  dw} =
    // (1 - F0) * Integral{(1 - )^5 * (BSDF / F) *  dw} + F0 * Integral{(BSDF / F) *  dw}=
    // (1 - F0) * x + F0 * y = lerp(x, y, F0)

    acc.x += weightOverPdf * pow(1 - VdotH, 5);
    acc.y += weightOverPdf;
}

  最后还要除以总体采样数

acc /= sampleCount;

  调用生成LUT图的shader是preIntegratedFGD_GGXDisneyDiffuse.shader,将自身的屏幕空间uv作为NdotV和PerceptualRoughness传入:

float4 Frag(Varyings input) : SV_Target
{
    float2 coordLUT = RemapHalfTexelCoordTo01(input.texCoord, FGDTEXTURE_RESOLUTION);

    float NdotV = coordLUT.x * coordLUT.x;
    float perceptualRoughness = coordLUT.y;

    float4 preFGD = IntegrateGGXAndDisneyDiffuseFGD(NdotV, PerceptualRoughnessToRoughness(perceptualRoughness));

    return float4(preFGD.xyz, 1.0);
}

  HDRP的PreIntegratedFGD.cs脚本负责生成这张LUT图的调用。
  解码调用放在PreIntegratedFGD.hlsl中:

void GetPreIntegratedFGDGGXAndDisneyDiffuse(float NdotV, float perceptualRoughness, float3 fresnel0, out float3 specularFGD, out float diffuseFGD, out float reflectivity)
{
    // We want the LUT to contain the entire [0, 1] range, without losing half a texel at each side.
    float2 coordLUT = Remap01ToHalfTexelCoord(float2(sqrt(NdotV), perceptualRoughness), FGDTEXTURE_RESOLUTION);

    float3 preFGD = SAMPLE_TEXTURE2D_LOD(_PreIntegratedFGD_GGXDisneyDiffuse, s_linear_clamp_sampler, coordLUT, 0).xyz;

    // Pre-integrate GGX FGD
    // Integral{BSDF *  dw} =
    // Integral{(F0 + (1 - F0) * (1 - )^5) * (BSDF / F) *  dw} =
    // (1 - F0) * Integral{(1 - )^5 * (BSDF / F) *  dw} + F0 * Integral{(BSDF / F) *  dw}=
    // (1 - F0) * x + F0 * y = lerp(x, y, F0)
    specularFGD = lerp(preFGD.xxx, preFGD.yyy, fresnel0);

    // Pre integrate DisneyDiffuse FGD:
    // z = DisneyDiffuse
    // Remap from the [0, 1] to the [0.5, 1.5] range.
    diffuseFGD = preFGD.z + 0.5;

    reflectivity = preFGD.y;
}

  这个函数调用写在GetPreLightData中,这个函数在多处被声明:

//Lit.hlsl
GetPreIntegratedFGDGGXAndDisneyDiffuse(clampedNdotV, preLightData.iblPerceptualRoughness, bsdfData.fresnel0, 
      preLightData.specularFGD, preLightData.diffuseFGD, specularReflectivity);

  preLightData.specularFGD被用于采样Cubemap后相乘算出环境光:

//Fabric.hlsl EvaluateBSDF_Env
//EvaluateBSDF_Env函数在LightLoop中被调用

float4 preLD = SampleEnv(lightLoopContext, lightData.envIndex, 
    R, iblMipLevel, lightData.rangeCompressionFactorCompensation, sliceIndex);
float3 envLighting = preLightData.specularFGD * preLD.rgb;

  preLightData.diffuseFGD被用在ModifyBakedDiffuseLighting函数中:

//Fabric.hlsl ModifyBakedDiffuseLighting

builtinData.bakeDiffuseLighting *= preLightData.diffuseFGD * bsdfData.diffuseColor;

  builtinData.bakeDiffuseLighting在乘之前是从球谐中采样出来的值:

//BuiltinUtilities.hlsl InitBuiltinData
builtinData.bakeDiffuseLighting = SampleBakedGI(posInput.positionWS, normalWS, texCoord1.xy, texCoord2.xy);

ref:
几何函数相关总结
LearnOpengl
蒙特卡洛积分

你可能感兴趣的:(【Unity3D】HDRP的IBL分析)