返回「计算机、信息技术与工程」

URP的BRDF计算的疑惑,与论文中的公式不一样?

更多
Markdown 结构化数据
本文目录 3 个章节

URP的BRDF计算的疑惑,与论文中的公式不一样?

note · 我的 UWA 问答 本文收录我在 UWA 问答发表的公开回答。问题内容归原提问者;回答保留当时语境,技术结论可能受 Unity 版本、平台和项目条件限制。

问题

最近在看URP shader的时候发现个问题,不知道是不是我理解的不对

首先贴代码

先是URP的Lighting.hlsl里初始化BRDF的部分

inline void InitializeBRDFData(half3 albedo, half metallic, half3 specular, half smoothness, half alpha, out BRDFData outBRDFData)

{

#ifdef _SPECULAR_SETUP

    half reflectivity = ReflectivitySpecular(specular);

    half oneMinusReflectivity = 1.0 - reflectivity;

    outBRDFData.diffuse = albedo * (half3(1.0h, 1.0h, 1.0h) - specular);

    outBRDFData.specular = specular;

#else

    half oneMinusReflectivity = OneMinusReflectivityMetallic(metallic);

    half reflectivity = 1.0 - oneMinusReflectivity;

    outBRDFData.diffuse = albedo * oneMinusReflectivity;

    outBRDFData.specular = lerp(kDieletricSpec.rgb, albedo, metallic);

#endif

    outBRDFData.grazingTerm = saturate(smoothness + reflectivity);

    outBRDFData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);

    outBRDFData.roughness = max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN);

    outBRDFData.roughness2 = outBRDFData.roughness * outBRDFData.roughness;

    outBRDFData.normalizationTerm = outBRDFData.roughness * 4.0h + 2.0h;

    outBRDFData.roughness2MinusOne = outBRDFData.roughness2 - 1.0h;

#ifdef _ALPHAPREMULTIPLY_ON

    outBRDFData.diffuse *= alpha;

    alpha = alpha * oneMinusReflectivity + reflectivity;

#endif

}

按照我的理解

outBRDFData.perceptualRoughness 是我们说的粗糙度

outBRDFData.roughness 是粗糙度的平方

outBRDFData.roughness2 是粗糙度的4次方

再看URP的Lighting.hlsl里计算BRDF的部分

// Based on Minimalist CookTorrance BRDF

// Implementation is slightly different from original derivation: http://www.thetenthplanet.de/archives/255

//

// * NDF [Modified] GGX

// * Modified Kelemen and Szirmay-Kalos for Visibility term

// * Fresnel approximated with 1/LdotH

half3 DirectBDRF(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS)

{

#ifndef _SPECULARHIGHLIGHTS_OFF

    float3 halfDir = SafeNormalize(float3(lightDirectionWS) + float3(viewDirectionWS));

    float NoH = saturate(dot(normalWS, halfDir));

    half LoH = saturate(dot(lightDirectionWS, halfDir));

    // GGX Distribution multiplied by combined approximation of Visibility and Fresnel

    // BRDFspec = (D * V * F) / 4.0

    // D = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2

    // V * F = 1.0 / ( LoH^2 * (roughness + 0.5) )

    // See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course

    // https://community.arm.com/events/1155

    // Final BRDFspec = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 * (LoH^2 * (roughness + 0.5) * 4.0)

    // We further optimize a few light invariant terms

    // brdfData.normalizationTerm = (roughness + 0.5) * 4.0 rewritten as roughness * 4.0 + 2.0 to a fit a MAD.

    float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f;

    half LoH2 = LoH * LoH;

    half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm);

    // On platforms where half actually means something, the denominator has a risk of overflow

    // clamp below was added specifically to "fix" that, but dx compiler (we convert bytecode to metal/gles)

    // sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...))

#if defined (SHADER_API_MOBILE) || defined (SHADER_API_SWITCH)

    specularTerm = specularTerm - HALF_MIN;

    specularTerm = clamp(specularTerm, 0.0, 100.0); // Prevent FP16 overflow on mobiles

#endif

    half3 color = specularTerm * brdfData.specular + brdfData.diffuse;

    return color;

#else

    return brdfData.diffuse;

#endif

}

这里最终的公式是BRDFspec = roughness2 / ( NoH2 *(roughness**2 - 1) + 1 )*2 (LoH^2 (roughness + 0.5) 4.0)

然后看unity 2015年的论文,也就是代码里写的那个地址https://community.arm.com/events/1155

BRDF.png

代码里的roughness实际上是图里roughness的平方,为了防止混淆,下面用roughness(c)和roughness(n)来表示

roughness(c)=roughness(n)^2

看到这里我产生了疑问,论文里roughness(n) + 0.5这一步,对应到代码里,应该是roughness(c)^0.5+0.5才对,而URP代码里直接用了roughness(c)+0.5。

我想问下是我理解的不对,还是URP的shader写错了。

  • 提问者:金喆
  • 提问时间:2021-04-22 00:13:51

我的回答

讲一下不太成熟的看法哈。我这边的结论是:

  • 用平方(roughness(n)^2)加0.5是合理的;
  • 用一次方或者是平方差别不大。

SIGGRAPH 报告中最初V*F函数如下:

image.png

实际使用的V*F函数是对上述函数的一个近似,使用图像相近的函数进行优化:

image.png

笔者对取一次放和取平方的函数图像进行了绘制如下:

image.png

image.png

两个图像差别不大,而从走势上看,取平方似乎更最接近最初的 VF (Modified KSK and Schlick Fresnel depend on LH)

image.png

笔者修改了Lighting.hlsl的代码,使用一次方: outBRDFData.normalizationTerm = outBRDFData.perceptualRoughness* 4.0h + 2.0h;

将使用roughness(n) + 0.5 和使用roughness(n)^2 + 0.5 的材质绘制在同一个屏幕上如下,两者几乎没有差别:

image.png

根据图形学第一定律:如果它看起来是对的,那么它就是对的,所以理解成两个都对应该也可以。那么,是不是在差距不大的情况下就不必深究了。

  • 回答时间:2021-04-23 15:15:26
  • 最后更新:2021-04-23 15:24:43
  • 采纳状态:已采纳

来源与关系