【Unity3D】HDRP的IBL分析

前言

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

理论

渲染方程

  PBR的渲染方程为\int_{\Omega}(k_d\frac{c}{\pi}+ks\frac{DGF}{4cos\theta_icos\theta_o})L_icos\theta_id\omega_i
  其中:
cos\theta_iN·L
cos\theta_oN·V
  BRDFf_r=\frac{DGF}{4cos\theta_icos\theta_o}
  遮蔽函数V(v, l)=\frac{G(l, v, h)}{4cos\theta_icos\theta_o},所以BRDF可简写成fr=DVF

间接光照

  不同与入射光线离散的直接光照,间接光照是离散的,需要对整个半球面进行积分,采用蒙特卡洛积分法,用离散样本求值相加并除以样本总数。
  渲染方程可以先分解为两部分\int_{\Omega}k_d\frac{c}{\pi}L_icos\theta_id\omega_i\int_{\Omega}ks\frac{DGF}{4cos\theta_icos\theta_o}L_icos\theta_id\omega_i,分别为Diffuse项和Specular项。

Diffuse间接光照

  可以将积分无关变量提取出来:k_d\frac{c}{\pi}\int_{\Omega}L_icos\theta_id\omega_i
  积分代码参照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是计算后的值,这里应该是L_icos\theta_i,不过预计算阶段是没法取到c环境纹理的,所以用分割近似法,将积分拆成k_dc\int_{\Omega}\frac{1}{\pi}cos\theta_id\omega_i\int_{\Omega}L_id\omega_i,光照项在运行时用球谐近似计算,此时weight是\int_{\Omega}\frac{c}{\pi}cos\theta_id\omega_i
pdf是概率密度函数,\frac{N·L}{\pi},可以看到概率密度函数性质\int_\Omega\frac{N·L}{\pi}d\omega=\frac{1}{\pi}\int_0^{\frac{\pi}{2}}cos\theta sin\theta d\theta\int_0^{2\pi}d\phi=1
weightOverPdf=\frac{\frac{1}{\pi} N·L}{\frac{N·L}{\pi}}=1,似乎积分没什么意义。
  不过HDRP用的Diffuse项是DisneyDiffuse,\frac{c}{\pi}拓展为:\frac{c}{\pi}(1+(F_{D90}-1)(1-cos\theta_l)^5)(F_{D90}-1)(1-cos\theta_v)^5)
F_{D90}=0.5+2*roughness*cos^2\theta_d
  所以积分的其实是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间接光照

  依旧是分割近似求和法,先将光照部分分离积分\int_{\Omega}f_rL_icos\theta_id\omega_i\Rightarrow\int_{\Omega}L_id\omega_i*\int_{\Omega}f_rcos\theta_id\omega_i

采用Trowbridge-Reitz GGX作为NDF:D(\omega_h)=\frac{\alpha^2}{\pi(cos\theta_h^2(\alpha^2-1)+1)^2}
其中
\omega_h是半角向量
cos\theta_hN·H
\alpha是粗糙度
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);
}

\frac{G_2(l, v)}{4 |n·l| |n·v|}\Rightarrow \frac{0.5}{\mu_o\sqrt{\alpha^2+\mu_i(\mu_i-\alpha^2\mu_i)}+\mu_i\sqrt{alpha^2+\mu_o(\mu_o-\alpha^2\mu_o)}}

  根据siggraph 2012 course,迪斯尼提出来的模型,GGX的重要性采样pdf(概率密度函数)为pdf_L=\frac{D(H)(N·H)}{4(L·H)},推导是:pdf_m=D(m)(n·m)是微表面法线m的概率分布函数,我们需要计算的是入射方向i的概率分布,涉及到分布变换,要将pdf_m乘以一个雅可比矩阵,最终得到的概率分布函数就是pdf_L
  利用蒙特卡洛积分法\frac{1}{N}\Sigma_{i=0}^N\frac{BRDF*(N·L)}{pdf}
\frac{f_r*cos\theta_i}{pdf}=\frac{F(H)(L·H) * 4(N·L) V(v, L)}{N·H}
  因为H是L和V的中间半角向量,所以(L·H)=(V·H)
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项也可以分成两部分
\int_{\Omega}f_rcos\theta_id\omega_i=\int_{\Omega}\frac{f_r}{F}Fcos\theta_id\omega_i=\int_{\Omega}\frac{f_r}{F}[F_0+(1-F_0)(1-cos\theta_v)^5]cos\theta_id\omega_i=
F_0\int_{\Omega}\frac{f_r}{F}cos\theta_id\omega_i+(1-F_0)\int_{\Omega}\frac{f_r}{F}(1-cos\theta_v)^5cos\theta_id\omega_i
  体现在下面是acc的x和y部分

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

if (NdotL > 0.0)
{
    // Integral{BSDF * <N,L> dw} =
    // Integral{(F0 + (1 - F0) * (1 - <V,H>)^5) * (BSDF / F) * <N,L> dw} =
    // (1 - F0) * Integral{(1 - <V,H>)^5 * (BSDF / F) * <N,L> dw} + F0 * Integral{(BSDF / F) * <N,L> 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 * <N,L> dw} =
    // Integral{(F0 + (1 - F0) * (1 - <V,H>)^5) * (BSDF / F) * <N,L> dw} =
    // (1 - F0) * Integral{(1 - <V,H>)^5 * (BSDF / F) * <N,L> dw} + F0 * Integral{(BSDF / F) * <N,L> 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
蒙特卡洛积分

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,289评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,968评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,336评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,718评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,734评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,240评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,631评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,599评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,139评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,166评论 3 345
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,286评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,917评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,604评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,075评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,205评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,814评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,351评论 2 365

推荐阅读更多精彩内容