前言
基于图像光照(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
蒙特卡洛积分