Press "Enter" to skip to content

PBR中的金属度和粗糙度以及BRDF中的FDG项

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

因为一直以来对BRDF的了解仅仅是大概知道

 

导致很多细节的控制力有限

 

所以干脆抽一个周末,好好动手把直接光部分的算法都做一遍,加深理解

 

间接光的部分核心是球谐算法和IBL, 以后有机会在仔细搞一遍间接光的部分需要的算法

 

这篇文章因为太长,并且太琐碎写的我欲生欲死, 后面几度不想写了

 

所以如果有任何错误请告诉我, 我会及时跟进修改

 

老规矩 先放上效果图:

GIT地址:

https://github.com/lingzerg/LingzergDemo ​ github.com

概念 – PBR中的金属度, 粗糙度, 和 -F0

 

PBR中一个比较典型的实现-BRDF, 就是表达一束光照射在一个表面(微面元)上之后反射出去的结果

 

而微面元我觉得是体现在两个函数上: 法线分布函数 以及几何遮蔽函数

 

我们通过学习BRDF可以更加深入的理解PBR,对于工作中如何设计, 调整材质表现有很大帮助,

 

而在此之后, 还可以进一步学习其他不同的公式,例如 BSSRDF

 

BRDF有三个参数构成了整个运算的基础

 

金属度, 粗糙度, 反射率-F0

 

我想尽量不要用代码和公式描述这三个东西,第一部分可以让美术看懂

 

第二部分, 在这个三个参数的描述之后, 我们可以讨论下BRDF

 

以及FDG的具体运算方式

 

并且在unity中以GGX那套公式为基础实现一套完整的BRDF

 

至于微面元理论和辐射度量学可以单独再开一篇文章详细记录

 

金属度

 

金属度代表了有多少光子是直接被反射出去, 有多少光子被吸收后成了漫反射

 

金属度等于0的情况下, 光子会被完全吸收, 直接反射会变得非常弱, 只有漫反射

 

具体漫反射会有多弱? 我们会在F0的地方提到

 

金属度如果等于1的情况下, 所有的光子都会被反射出去, 会完全没有漫反射

 

并且当金属度等于1的情况下, base color就会成为反射率, 所以base color其实是不能乱设置的

 

我们常常可以看见一个表:

因为在高金属度的情况下F0和base color的这个关系, 所以高金属度的这个情况下,base color更加重要

 

那假如金属度等于0.5呢? 那幺反射率就是 F0 和 Basecolor的平均值

 

同理, 你的反射率就由这三个东西决定

 

F0, baseColor , 而金属度决定更靠近那个值

粗糙度

 

粗糙度代表表面的粗糙程度, 越粗糙的表面光的散射就会越厉害

还有一个额外的影响,就是越粗糙的表面可能会产生更多的遮蔽

 

所谓遮蔽是这个意思:

 

反射率-F0

 

F0就是反射率, 当我们90度直视一个表面的时候, 看到的光子回弹的比例

 

正如前面金属度提到的那样反射率基于F0和base color之间

 

所以base color设置多少就很重要了

 

在金属度等于1的情况,很容易分析, 漫反射等于0, 而F0就会等于base color

 

而在金属度等于0的情况下呢?

 

这时候F0就等于我们最开始设置的那个F0, 一般会设置为0.04, 甚至更高

 

大部分非金属的F0都很小, 例如0.04, 0.08

 

所以在UE中, 如果你给Specular设置成1, 那幺F0的默认值就会是0.08

 

设置成0, 就是0,默认Specular应该是0.5, 也就是说F00.04

unity中无法设置这个F0, 少了一个控制的维度, 可能需要我们手动添加, 后面我们会在代码中添加这个

 

PBR方程/BRDF函数

 

先看看光传输方程:

 

 

其实外面这个积分并不是BRDF的一部分

 

积分的意义在于计算这个角度上, 微面元反射出去全部的 irradiance, 毕竟还有环境光, 多光源

 

但是BRDF是定义 给定入射方向上的辐射照度如何影响给定出射方向上的辐射率

 

说白了就是

 

有位大佬指点我, 说的很有道理

 

BRDF可以看成两个函数

 

 

 

但是在大部分的实现中, 实际上是把这两个函数内联了

 

如果你看的有点晕,不要紧, 直接跟着下面实现一遍, 然后在扭回头在看这个公式

 

其次, (这里忘了…其次啥来着…, 想起来就补上, 想不起来这里就删了)

 

然后我们拆开看这个公式:

 

 

其实核心就是DGF的计算, 是光照方向这样不太方便看, 我后面改成

 

是反射方向, 也可以理解成视角,观察角,我们用

 

外面的 是LightColor, 后面的 就是光照方向点积法线

 

而Ks 就是fresnel,所以直接可以去掉了

 

我们把不太容易看明白的符号都换掉再看一眼:

 

 

kd, ks

 

 

DGF的公式我们先看一眼:

 

 

这一坨看起来确实很费劲, 如果你不想看也没问题, 我们后面一个一个实现一遍就好了

 

这里面NDF的: 就是粗糙度, 一般在实际开发中, 为了让使用者更舒服, 粗糙度会等于 输入粗糙度的平方

 

然后我们在解释几个基本概念

 

微面元与辐射度量学

 

这玩意展开讲真的要再开一篇文章

 

我今年有一半的时间都在跟灯光打交道,所以让我们把这些概念的详细描述放到另一篇文章里好了

 

简单的说辐射度量学和光传输方程最大的关联在于:

 

 

radiance是每单位角的辐射通量密度, 就是这个

 

而irradiance是辐照度,求的就是这个东西

 

PBR那个公式L 则是整个半球的辐照度积分

 

微面元理论 则是说我们入射的 radiance 照射对象是一个概念上的表面

 

微面元最大的相关项是D和G, 在后面也会讲到

 

如果你想简单的理解, 辐射度量学就是入射光 , 和 反射结果

 

微面元就是照射的表面

 

大概就是这样

 

DOT – 点积

 

点积是代表两个向量的相似程度, 或者说两个向量的夹角大小, 或者说两个向量夹角的cos

 

如果两个向量完全重合, 点积等于1

 

如果两个向量夹角等于90度 则点积等于0, 大于90度则等于负数, 我们会归一化,所以在向量运算的时候 往往忽略这个情况.

有没有发现这个恰好可以描述光 视角 表面法线三者之间的关系?

 

例如光如果垂直于法线照射表面, 那幺点积则等于1 , 而光越倾斜, 则点积值越小

 

当等于大于90度 则灯光照射不到这个表面了

 

而视角也是同样的道理, 点积广泛用于渲染中来计算两个向量之间的关系

 

知道上面的内容后, 我们来用手轮一个基于pbr的直接光shader

 

我们在Unity里, 创建一个新的unlit的shader , 删除多余的部分, 开始试着实现一下

 

Shader "Unlit/MyBRDF"
{
    Properties
    {
        _Color ("Base Color", Color) = (1,1,1,1)
        [Gamma] _Metallic ("Metallic", Range(0,1)) = 0.5
        _Roughness ("Roughness", Range(0,1)) = 0.5
        _BaseF0 ("BaseF0",Range(0,1)) = 0.04
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityStandardBRDF.cginc" 
            
            #define PI 3.14159274f
            struct VertexInput
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
                
            };
            struct Interpolators
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD1;
                fixed3 normal : TEXCOORD2;
				
                
            };
            fixed4 _Color;
            fixed _Metallic,_Roughness,_BaseF0;
            Interpolators vert (VertexInput v)
            {
                Interpolators i;
                i.vertex = UnityObjectToClipPos(v.vertex);
				i.worldPos = mul(unity_ObjectToWorld, v.vertex);
                
                i.normal = UnityObjectToWorldNormal(v.normal);
                i.normal = normalize(i.normal);
                return i;
            }
            fixed4 frag (Interpolators i) : SV_Target
            {
                
                return 0;
            }
            ENDCG
        }
    }
}

 

然后我们先实现最基本的漫反射

 

 

我们先把外面的 写好放边上, 等下DGF部分最后的结果也要乘以这个

 

我们定义一个最后的输出FinalColor

 

接着我们需要计算kd, 而KD和F相关, 所以我们要先算出F

 

F的公式:

 

 

菲涅尔反射是一个光学效应, 当你的视角越贴近湖面, 反射就会越强

 

而当你垂直于湖面的时候菲涅尔就等于F0

 

所以这里用 来作为视角和法线的倾斜权重, ,任何时刻F均大于F0

 

根据公式增加一个F的方法:

 

fixed3 fresnelSchlick(float cosTheta, fixed3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

 

在frag里增加代码:

 

fixed3 F0 = _BaseF0;
F0 = lerp(F0, _Color.rgb, _Metallic);
fixed3 F = fresnelSchlick(VdotH, F0);

 

然后把NdotL单独给一个变量, 代码如下:

 

fixed4 frag (Interpolators i) : SV_Target
{
    fixed4 FinalColor = 0;
    float3 lightColor = _LightColor0.rgb;
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
    
    float3 normal = normalize(i.normal);
    float VdotH = max(saturate(dot(viewDir, halfVector)), 0.000001);
    float NdotL = max(saturate(dot(normal, lightDir)), 0.000001);
    fixed3 F0 = _BaseF0;
    F0 = lerp(F0, _Color.rgb, _Metallic);
    fixed3 F = fresnelSchlick(VdotH, F0);
    
    fixed kd = (1-F)*(1-_Metallic);
    float3 diffuse = _Color/PI * kd;
    FinalColor.rgb = diffuse * lightColor * NdotL;                
    FinalColor.a;
    return FinalColor;
}

 

然后你就可以看到场景里的球变成了这样:

感觉漫反射弱了很多? 因为unity很神奇的给结果乘了个PI,

 

但是这样其实是不对的, 等于unity中光照等于1的时候 实际上和ue的3.14一样

 

我们现在结果上乘一个π , 让结果看起来正确,

 

等最后完整着色器后我们可以根据这个仔细调试下

 

return FinalColor * PI;

 

乘了π之后,果然结果就正确多了:

接着我们实现DGF中的D

 

公式如下:

 

 

同时D,项目和视角相关, 所以我们还需要view ,以及半角向量, 我们在程序中增加两个变量:

 

float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); float3 halfVector = normalize(lightDir + viewDir); 

 

就是粗糙度, 但是我们要让输入的粗糙度先做一个平方操作, 这样的目的是为了让滑条和粗糙度的映射关系等于下面这个曲线:

这样美术在拉动粗糙度的滑动条时, 得到的值会偏小, 更容易控制高光

 

然后为了方便,我们单独建一个方法, 在下面调用,

 

方法的内容就是公式的内容:

 

fixed DistributionGGX(fixed3 NdotH, fixed a) {
    fixed a2 = a*a;
    fixed denom = (NdotH*NdotH * (a2-1)+1);
    denom = PI * denom * denom;
    return a2/denom;
}

 

接着我们输出一下D看下效果:

 

fixed D = DistributionGGX(NdotH,roughness);
return D;

嗯 稍微有点大不过不要紧, 我们后面会增加几何遮蔽项, 以及配平的除数

 

几何遮蔽 – G

 

先放公式:

 

 

 

 

也就是说几何遮蔽等于要调用两次 函数,

 

一次计算灯光的遮蔽情况, 一次计算视角的遮蔽情况,最后乘到一起

 

几何遮蔽描述的是微面元中两个物理情况:

就是说在微面元上, 不仅要光能照到, 并且眼睛也要能看到

 

所以我们要求两次G, 然后把他们乘起来, 几何遮蔽会减弱我们看到的灯光效果

 

所以老规矩, 我们先创建一个函数:

 

fixed SchlickGGX(float cosTheta, fixed k) {
    return cosTheta/(cosTheta* (1-k)+k);
}

 

然后我们先计算

 

因为是直接光, 所以我们直接用直接光的公式:

 

 

fixed k_dir = pow((squareRoughness+1),2)/8;

 

接着我们计算两次G项 并乘到一起, 然后输出到颜色看下效果:

 

fixed ggx1 = SchlickGGX(NdotL,k_dir);
fixed ggx2 = SchlickGGX(NdotV,k_dir);
fixed G = ggx1 * ggx2;
return G;

 

结果如下:

现在我们已经有了FDG, 我们先输出一下FDG的乘积看下效果:

 

return fixed4(F*G*D,1);

这时候会发现高光变小了, 这是因为G项的遮蔽作用

 

然后我们把最后的配平参数写上:

 

FDG /= 4*NdotV*NdotL;

 

这个配平参数的推导, 推荐大家看这篇文章中镜面反射如何推导的部分:

TC130:彻底看懂PBR/BRDF方程 ​ zhuanlan.zhihu.com

实在过于繁琐, 我这里就不展开讲解了

 

如果你此时输出FDG, 会发现FDG非常小, 这依旧是由于unity那个缩放的PI 导致的 TC130:彻底看懂PBR/BRDF方程 如果你此时输出FDG, 会发现FDG非常小, 这依旧是由于unity那个缩放的PI 导致的

 

我们直接把漫反射和高光项加到一起, 然后乘以外部的乘数

 

FinalColor.rgb = diffuse;
FinalColor.rgb += +FDG;
                
FinalColor.rgb *=  lightColor * NdotL * PI;
FinalColor.a = 1;
return FinalColor;

 

最后输出看下结果:

然后我们可以看到, 如果在F0 = 0.04的情况下 和unity的standard材质高光还是不同, 我实际测试, 如果把F0换成:

那幺unity的F0 显然不等于0.04了

 

我实测大概是0.15左右, 除了π 这里也是个让人哭笑不得的地方

 

而最后和unity的standard还是有一些出入

 

主要是我是严格按公式里的方式算的, 比如ue的F用的是一个近似算法:

 

float3 F = F0 + (1 - F0) * exp2((-5.55473 * vh - 6.98316) * vh);

 

unity肯定也会有一定的改动, 所以略有出入并不要紧,并且我觉得我实现的效果更接近公式的效果,毕竟unity是一个需要结果乘π的引擎…

 

我们也可以通过拉动参数调整

 

我也推荐你试试去掉结尾的π, 然后把光强拉到3.14试试

 

我在shader中加一个开关方便你测试

到这里就全部结束了, 我写的几度崩溃, 因为太长了…

 

相信看到这里的你也是个猛士

 

谢谢你的阅读, 并且如果你有任何建议或者吐槽欢迎留言

 

对于理论我主要参考的文章是这篇:

https://learnopengl.com/PBR/Theory ​ learnopengl.com

中文还有一篇很不错的分析 也推荐大家看下:

Be First to Comment

发表评论

邮箱地址不会被公开。 必填项已用*标注