HPP_Graphics_5.1 PBR基础 BRDF介绍

Cook-Torrance

a). Unity中的PBR(Disney‘s Principled BRDF)

Unity中Standard Shader基本采用Disney’s Principled BRDF,但有些许不同。Disney’s Principled BRDF可具体看其他文章,如毛星云大佬的PBR白皮书,以下就写一些实现上的不同处和细节。

a.1). 直接光照漫反射BRDF

Unity中采用的漫反射BRDF不是Lambertian漫反射,而是Disney开发了的一种用于漫反射的新的经验模型。

Disney表示,Lambert漫反射模型在边缘上通常太暗,而通过尝试添加菲涅尔因子以使其在物理上更合理,但会导致其更暗。

思路方面,Disney使用了Schlick Fresnel近似,并修改掠射逆反射(grazing retroreflection response)以达到其特定值由粗糙度值确定,而不是简单为0。

5bedec7e9fd8594bb6ffcbff3aaeb9ad

Diffuse BRDF. 上图为Diffuse BRDF

为保证shader看起来和Legacy版本差不多亮 ,并且避免在ibl部分对非重要光源做特殊处理,Unity会把分母中的 $\pi$ 拿掉。同时也会在直接光照的镜面反射项上多乘上一个 $\pi$

a.2). 直接光照镜面反射BRDF

镜面反射即采用微表面BRDF,即

c1b2fc1ec5d1e5c6380ffacfb31cff28

  • D为微平面分布函数,主要负责镜面反射波峰(specular peak)的形状。

  • F为菲涅尔反射系数(Fresnel reflection coefficient)

  • G为几何衰减(geometric attenuation)/ 阴影项(shadowing factor)

a.2.1). 法线分布项(Specular D):GTR

Unity中采用法线分布项为GGX,这里采用GTR模型。

6fb3430619d35fecc4267b24f0edf6cd

其中,γ取2,即GGX

另外,在Disney Principled BRDF中,实际上有两个镜面反射波瓣(Specular lobe),并且都用GTR模型。其中γ=2的GRT代表基础底层材质,而γ=1的GRT则代表清漆层的反射。

a.2.2). 菲涅尔项(Specular F):Schlick Fresnel

071dcb665386a2a56f1cb84352dc60ce

由于原始的菲涅尔项表示过于复杂,人们会常用其他数值近似的方法。其中,应用地较为广泛的为Schlick Fresnel。本质上是考虑能量的反射和折射(即考虑菲涅尔就会有因为颜色的合理的能量损失,这也是为什么Kulla-Conty Approximation再为考虑颜色时,不乘上菲涅尔项的原因)

这里需要注意的有两点。

  1. $\theta_d$ 为半角向量h和视线v之间的夹角,而不是宏观法线n和视线v的夹角。$(i, h)$和$(i, n)$的区别其实就是宏观和微观。在微表面BRDF中,$D(h)$筛选出了沿$h$方向的normal。那此时菲涅尔项中应该使用的normal即为$h$

  2. 电介质(绝缘体)的$F_0$ 为float,金属的 $F_0$ 为float3。而最终用于菲涅尔项的 $F_0$ 常常会根据金属度在0.04(引擎中电介质默认的$F_0$)和albedo之间根据金属度Metallic插值。

a.2.3). 几何项/Shadowing-Masking(Specular G):Smith-GGX

几何项(Specular G)方面,对于主镜面波瓣(primary specular lobe),Disney参考了 Walter的近似方法,使用Smith GGX导出的G项,并将粗糙度参数进行重映射以减少光泽表面的极端增益,即将α 从[0, 1]重映射到[0.5, 1],α的值为(0.5 + roughness/2)^2。从而使几何项的粗糙度变化更加平滑,更便于美术人员的使用。

以下为Smith GGX的几何项的表达式:

另外,对于对清漆层进行处理的次级波瓣(secondary lobe),Disney没有使用Smith G推导,而是直接使用固定粗糙度为0.25的GGX的 G项,便可以得到合理且很好的视觉效果。

a.3). 间接光照漫反射

间接光照漫反射频率基本集中的低频,因此采用球谐函数取近似(Unity中采用前三阶)。

详见Games202 Real-time Environment Mapping

a.4). 间接光照镜面反射

间接光照镜面反射采用IBL,并通过prefiltering后采用模拟对Lighting的积分,通过Split sum对BRDF积分。(详见Games202 Real-time Environment Mapping)其中,mip和roughness之间的关系为:

1
2
3
4
5
float m = roughness*roughness;
const float fEps = 1.192092896e-07F;
float n = (2.0 / max(fEps, m * m)) - 2.0;
n /= 4;
roughness = pow( 2 / (n + 2), 0.25);

Unity中,则通过 $mip = r(1.7 - 0.7r)$ 来拟合。

http://jbit.net/~sparky/academic/mm_brdf.pdf

a.5). 各项比例

至此,我们已经可以把各项的表达式都写出来了。那么最后需要解决的就是各项之间的比例。

a.5.1). 直接光照

首先,我们考虑直接光照。直接光照中,漫反射和镜面反射的关键在于菲涅尔项。菲涅尔项本质上是考虑能量的反射和折射,而光线折射后会发生吸收和散射。而散射的尺度要是足够小,就变成了漫反射(尺度大,如大于一个像素区域时,散射变现为次表面散射,即SSS)。

镜面反射的比例已经在微表面BRDF中的F项中计算过了,因此漫反射的比例即为1 - F。同时,因为我们采用的Disney principled BRDF需要根据金属度在漫反射和镜面反射之间插值,因此漫反射项的比例为$(1 - F) \cdot (1-Metallic) $ 。

a.5.2). 间接光照

在间接光照中,与直接光照不同的地方在于在求间接光照的镜面反射时,我们对BRDF求了积分(Split sum)。因此,我们菲涅尔的不再是微表面的菲涅尔,而是使用宏观法线的菲涅尔。即,$\theta_d$ 为法线$n$和视线$v$之间的夹角

071dcb665386a2a56f1cb84352dc60ce

这里$F_0$ 与albedo之间lerp使用Roughness,而不是Metallic。(一种经验化的做法,方法来自:https://seblagarde.wordpress.com/2011/08/17/hello-world/)

附上最终Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
Shader "PBR/DisneyPBR"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Roughness ("Roughness", Range(0, 1.0)) = 0.3
_Metallic ("Metallic", Range(0.0, 1.0)) = 0.2
_IBLlut ("IBL Lut", 2D) = "while" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }

CGINCLUDE
#include "UnityCG.cginc"
#include "UnityStandardBRDF.cginc"
#include "UnityStandardUtils.cginc"

half DI_DisneyDiffuse(half NdotV, half NdotL, half NdotH, half perceptualRoughness) {
half F90 = 0.5 + 2 * perceptualRoughness * NdotH * NdotH;
half lightScatter = 1 + (F90 - 1) * Pow5(1 - NdotL);
half viewScatter = 1 + (F90 - 1) * Pow5(1 - NdotV);

return lightScatter * viewScatter;
}

half SmithJointGGX(half NdotV, half NdotL, half perceptualRoughness) {
half a = 0.5 + perceptualRoughness/2;
a *= a;
half a2 = a * a;

half lightGGX = 2 * NdotL / (NdotL + sqrt(a2 + (NdotL - a2 * NdotL) * NdotL));
half viewGGX = 2 * NdotV / (NdotV + sqrt(a2 + (NdotV - a2 * NdotV) * NdotV));

return lightGGX * viewGGX;
}

half GTR2(half NdotH, half perceptualRoughness) {
half a2 = perceptualRoughness * perceptualRoughness;
half cos2 = NdotH * NdotH;
half denom = (1 + (a2 - 1) * cos2);

return a2 * UNITY_INV_PI / (denom * denom);
}

half3 F_schlick(half3 F0, half HdotV) {
return F0 + (1 - F0) * Pow5(1 - HdotV);
}

half3 IBL_LightSample(float3 dir, half perceptualRoughness) {
float mip_roughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);
half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS;

half4 hdr_col = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, dir, mip);
float3 ldr_col = DecodeHDR(hdr_col, unity_SpecCube0_HDR);

return ldr_col;
}

float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * Pow5(1.0 - cosTheta);
}

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
sampler2D _IBLlut;
float4 _MainTex_ST;
float _Roughness;
float _Metallic;

v2f vert (a2v v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal = normalize(i.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 worldHalfDir = normalize(worldLightDir + worldViewDir);

float NdotV = max(dot(worldNormal, worldViewDir), 0.0001);
float NdotL = max(dot(worldNormal, worldLightDir), 0.0001);
float NdotH = max(dot(worldNormal, worldHalfDir), 0.0001);
float HdotV = max(dot(worldHalfDir, worldViewDir), 0.0001);

fixed4 albedo = tex2D(_MainTex, i.uv);

float roughness = lerp(0.004, 0.9, _Roughness);

//DI
// DisneyDiffuse没有乘上Pi
fixed3 DisneyDiffuse = albedo.rgb * DI_DisneyDiffuse(NdotV, NdotL, NdotH, roughness); // * UNITY_INV_PI

fixed3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, _Metallic);

float D = GTR2(NdotH, roughness);
float3 F = F_schlick(F0, HdotV);
float G = SmithJointGGX(NdotV, NdotL, roughness);

half3 SpeBRDF = F * D * G / (4 * NdotL * NdotV);

fixed3 LightColor = _LightColor0;

half kd = OneMinusReflectivityFromMetallic(_Metallic);

fixed3 Ambient = albedo * UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 Diffuse = (1-F) * kd * LightColor * DisneyDiffuse * NdotL;
fixed3 Specular = LightColor * SpeBRDF * NdotL * UNITY_PI; // 乘上Pi和Diffuse等比例变化;

// Environment Map

half3 ambient_contrib = ShadeSH9(half4(worldNormal, 1));

float3 iblLight = IBL_LightSample(reflect(-worldViewDir, worldNormal), roughness);

float2 envLut = tex2D(_IBLlut, float2(lerp(0, 0.99, NdotV), roughness)).rg;

float3 F0_Roughness = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, roughness);
float3 Flast = fresnelSchlickRoughness(max(NdotV, 0.001), F0, roughness);
float kdLast = (1 - Flast) * (1 - _Metallic);

float3 iblDiffuse = (ambient_contrib + Ambient) * albedo * kdLast;
float3 iblSpec = (iblLight * (Flast * envLut.r + envLut.g));

return fixed4(Diffuse + Specular + iblDiffuse + iblSpec, 1);
// return fixed4(iblDiffuse, 1);
}
ENDCG
Pass
{
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}

a.6). 结果

DisneyBRDF

Custom PBR. 同参数下,与Standard Shader的对比(左侧为Custom PBR)

为获得更长的拖尾,将GTR的γ取3以区别Standard。

可以看到,实现的Shader基本与Unity的Standard Shader一致。但因为法线分布使用了GTR,高光拖尾更长,更柔和。