PBR基础介绍(二)
本文将在unity中尝试复现上文提及的内容,主要参考了宋开心大佬的这篇
根据BRDF的渲染方程需要实现的主要部分:
- 直接光
- 直接光漫反射
- 直接光高光
- 法线分布函数 D
- 几何函数 G
- 菲涅尔函数 F
- 间接光
- IBL
- SH
实现之前
Unity的linear空间与Gamma空间
1 2 3 4 5 6 7
| // Shader中涉及到的参数如下 _MainTex("Texture", 2D) = "white" {} _Tint("Tint", Color) = (1 ,1 ,1 ,1) // 金属度 _Metallic("Metallic", Range(0, 1)) = 0 // 粗糙度 _Smoothness("Smoothness", Range(0, 1)) = 0.5
|
需要注意的是设置金属度/光滑度贴图的时候,如果在linear空间下贴图设定勾选了SRGB的选项(这个勾选本质上就是对颜色做了一次pow(color,2.2)的操作),那么需要在开头添加[Gamma]。一般来说PBR需要配合HDR进行开发。
1 2 3 4
| // 金属度 [Gamma]_Metallic("Metallic", Range(0, 1)) = 0 // 粗糙度 [Gamma]_Smoothness("Smoothness", Range(0, 1)) = 0.5
|
这里是两个空间下进行的步骤
linear颜色空间:
1)unity对输入颜色做逆gamma校正
2)shader对颜色进行计算并返回
3)unity对返回颜色做gamma校正
4)显示器对显卡输出的颜色做逆校正
5)人眼对显示器显示的图像做gamma校正
gamma颜色空间:
1)shader对输入颜色进行计算并返回
2)显示器对显卡输出的颜色做逆gamma校正
3)人眼对显示器显示的图像做gamma校正
Roughness和perceptualRoughness的关系
由Disney提出,在diffuse和specular中roughness使用的是不同的值,如下图所示,个人觉得并没有很清晰的解释另一个roughness是怎么来的,只是知道了roughness需要更明显的表现对表面的凸起的平滑。在Unity中计算关系如下
1 2
| perceptualRoughness = 1.0 - _Smoothness; roughness = perceptualRoughness * perceptualRoughness;
|
直接光
直接光漫反射
由渲染方程可得 Kd * Color / PI
1
| half3 diffuseColor = albedo * lightColor * NdotL / UNITY_PI;
|
得到的结果如下,上方是从Unity官方扒下来的直接光部分,下方是除了PI之后的表现效果。
可以看到得到的结果相当暗,但是在UnityStandardBRDF.cginc的注释中得到了答案。我理解的是这里因为是经验性的设置,为了统一和旧版本shader效果,保证整体不会太暗,漫反射和高光项同时乘了PI。
因此漫反射改为如下
1
| half3 diffuseColor = albedo * lightColor * NdotL;
|
在Unity中主要使用的是Disney Diffuse
1 2 3 4 5 6 7 8 9 10
| // Disney Diffuse half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness) { half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness; // Two schlick fresnel term half lightScatter = (1 + (fd90 - 1) * Pow5(1 - NdotL)); half viewScatter = (1 + (fd90 - 1) * Pow5(1 - NdotV));
return lightScatter * viewScatter; }
|
这里采用的是渲染方程中的实现方式,得到的效果如下第四排
直接光镜面反射
这边儿是重点,整个的形式如下,Unity中采用的是cook-torrance的公式
法线分布函数 D
按上一篇理论来说,这个函数本质上输出的是一个比值,统计学上是一个正态分布函数,计算的是微平面上的半程向量H和宏观平面半程向量一致的数量有多少。在Unity中采用的是Trowbridge-Reitz GGX。
1 2 3 4 5 6 7 8
| // NDF inline float TrowbridgeReitzGGX(float NdotH, float roughness) { float r = max(roughness, 0.003); float r2 = r * r; float d = (NdotH * NdotH) * (r2 - 1.0) + 1.0; return UNITY_INV_PI * r2 / (d * d); }
|
将实现的值输出,得到结果如下图第四排,当_Smoothness为0的时候没有高光的显示,当_Smoothness为1的时候球体上呈现出的是很小很亮的光斑。
几何函数 G
这个函数描述的是遮挡的比率。需要注意的是几何函数内是计算两个方向的,一个是光线入射方向,一个是光线出射方向。
写入函数如下:
1 2 3 4 5 6 7 8 9
| // Geometric inline float SchlickGeometricGGX(float NdotV, float NdotL, float roughness) { float k = (roughness + 1.0) * (roughness + 1.0) * 0.125; float lightTerm = NdotL / lerp(NdotL, 1.0, k); float viewTerm = NdotV / lerp(NdotV, 1.0, k);
return lightTerm * viewTerm; }
|
按照公式进行计算得到的结果如下
菲涅尔 F
菲涅尔主要描述的是反射光线对比光线被折射的部分所占的比率,这个比率会随着我们观察的角度不同而不同。当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度告诉我们被反射的光线所占的百分比。利用这个反射比率和能量守恒,我们可以直接得出光线被折射的部分以及光线剩余的能量。一般当观察方向是垂直的时候获得的是对应物体最基本的反射性,基本没有菲涅尔相关的影响,当掠射角增加,菲涅尔现象会明显很多。
通常会采用Fresnel-Schlick的方法:
在经过Unreal拟合优化之后,通过exp2函数提高了计算效率,变成了这样:
1 2 3 4 5 6 7 8
| half3 F0 = lerp(half3(0.04,0.04,0.04), albedo, _Metallic);
// Fresnel inline float3 Fresnel(float3 F0, float VdotH) { float power = (-5.55473 * VdotH - 6.98316) * VdotH; return F0 + (1 - F0) * exp2(power); }
|
最终菲涅尔效果如下:
综合整体的直接光效果后,下边面两排分别是Unity自带的standard,和手写的shader对比
环境光
环境光的公式如下
第一部分为间接光漫反射,第二部分为间接光镜面反射。
间接光漫反射
根据Learnopengl提到的方法,通常情况下是对间接光进行预处理,将经过处理后的数据存到一张新的贴图,贴图计算主要是通过卷积完成。这张贴图又叫辐照度图,在每个入射方向取平均值的结果。Unity中已经自动完成了这一步,是通过球谐函数进行编码的全局光照,Unity整个的执行过程为:将环境贴图进行预积分得到辐照度图→将辐照度图进行球谐函数编码存储。
在Unity中通过直接调用UnityCG.cginc中的ShadeSH9函数获取相应的光照信息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // 获取辐照度 half3 ambientContribution = ShadeSH9(float4(normalWorld, 1));
half3 ambient = 0.03 * albedo; // half3 iblDiffuse = max(half3(0, 0, 0), ambient.rgb + ambientContribution);
// 和直接光的系数不同需要重新计算 float3 Flast = FresnelSchlickRoughness(max(NdotV, 0.0), F0, roughtness); float kdLast = (1 - Flast) * (1 - _Metallic);
half3 iblDiffuseResult = iblDiffuse * kdLast * albedo;
// ***************
float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness) { return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); }
|
加上间接光之后效果如下图第二排:
间接光镜面反射
Unreal给出的方法是使用近似算法将高光反射的方程简化成右边。
镜面反射部分,使用的是对预处理的环境贴图进行LOD操作之后会生成有多层的一张贴图,通过对这张贴图进行三线性插值,得到的就是对应的mip层级数据,最后在根据这个数据对HDR进行解码。
1 2 3 4 5 6 7 8 9 10
| // 根据粗糙度获取mip的层级 half mipRoughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness); // 获取视线的反射 half3 reflectVec = reflect(-viewDir, normalWorld); // 获取mip层级 half mip = mipRoughness * UNITY_SPECCUBE_LOD_STEPS; // 三线性插值 half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectVec, mip); // 从HDR下解码 half3 iblSpecular = DecodeHDR(rgbm, unity_SpecCube0_HDR);
|
这样就得到了简化方程的左半部分,而右半部分在Unity中并没有采用LUT的方法,而是通过surfaceReduction的系数,以及一个在F0(specColor就是我们的F0)和grazingTerm之间进行插值的菲涅尔系数。Unity中的计算如下:
1 2 3 4 5
| half surfaceReduction = 1.0 / (roughtness * roughtness + 1.0); half oneMinusReflectivity = unity_ColorSpaceDielectricSpec.a - _Metallic * unity_ColorSpaceDielectricSpec.a; half grazingTerm = saturate(_Smoothness + (1.0 - oneMinusReflectivity));
half3 iblSpecularResult = iblSpecular * surfaceReduction * FresnelLerp(F0, grazingTerm, NdotV);
|
最后综合得到的效果如下:
参考:
- https://zhuanlan.zhihu.com/p/68025039
- https://zhuanlan.zhihu.com/p/141904960
- https://learnopengl-cn.github.io/07%20PBR/03%20IBL/01%20Diffuse%20irradiance/