1.首先了解一下PBR的物理理论:
光在照射到物体表面时,发生了反射(Reflection,镜面反射)和折射,而进入物体内的光一部分被再次散射出来,另一部分被吸收了(往往会转变为热量消耗掉)。散射出来的光有漫反射(diffsion),3S(SSS)等。
PS:在游戏当中,如果画面像素大于散射距离的话意味着这些次表面散射产生的距离可以被忽略,反之,我们就需要用特殊shader来模拟3S效果。
2.PBR: (直接光+间接光)
2.1直接光(含漫反射和镜面反射):
翻译成人话:
BRDF方程的配平系数:(不是很懂,两个点乘是和微平面有关)
公式推导过程: PBR反射方程推导
2.1.1 PBR的Lambert漫反射计算:
(Kd*c/π)光源颜色(lightDir·normal)
kd是漫反射比例,公式里面的F和Kd关系:kd=1-F
而Kd(漫反射比例),则是(1-F)(1-金属度),除了需要减掉F外,还要再乘一次(1-金属度)。这是 因为金属会更多的吸收折射光线导致漫反射消失,这是金属物质的特殊物理性质。
Diffuse= kd*BaseColor/PI; //费列尔值越大则漫反射占比越少,镜面光越强
注意:
下面是兰伯特漫反射计算:
PBR计算漫反射区别在于除以π,因为它把亮度除低了,就只能相应调高光源的亮度补回来。看似别扭,但是回头一想,光源的亮度,难道不就应该比周围的物品高上很多吗?因为即使是直射,也还是会有很多光线被散射到其他方向,只有少部分才正常投射到了人眼中,漫反射的性质就是如此,兰伯特定律不除π的做法其实才是错误的。
2.1.2 PBR的Disney漫反射计算:
Disney表示,Lambert漫反射模型在边缘上通常太暗,而通过尝试添加菲涅尔因子以使其在物理上更合理,但会导致其更暗。
思路方面,Disney使用了Schlick Fresnel近似,并修改掠射逆反射(grazing retroreflection response)以达到其特定值由粗糙度值确定,而不是简单为0。
Disney Diffuse漫反射模型的公式为:
以下为上述Disney Diffuse的Shader实现代码:
// [Burley 2012, "Physically-Based Shading at Disney"]
float3 Diffuse_Burley_Disney( float3 DiffuseColor, float Roughness, float NoV, float NoL, float VoH )
{
float FD90 = 0.5 + 2 * VoH * VoH * Roughness;
float FdV = 1 + (FD90 - 1) * Pow5( 1 - NoV );
float FdL = 1 + (FD90 - 1) * Pow5( 1 - NoL );
return DiffuseColor * ( (1 / PI) * FdV * FdL );
}
// [Burley 2012, "Physically-Based Shading at Disney"]
float3 Diffuse_Burley_Disney( float3 DiffuseColor, float Roughness, float NoV, float NoL, float VoH )
{
float FD90 = 0.5 + 2 * VoH * VoH * Roughness;
float FdV = 1 + (FD90 - 1) * Pow5( 1 - NoV );
float FdL = 1 + (FD90 - 1) * Pow5( 1 - NoL );
return DiffuseColor * ( (1 / PI) * FdV * FdL );
}
效果(可以看到Disney漫反射模型的确要比lambert更亮一些):
2.1.3 PBR的镜面反射计算:
(DFG/4(viewDir·normal)( lightDir· normal))光源颜色(lightDir·normal)
D(h)是法线分布函数:(采用了GGX模型的实现)
其中a为粗糙度Roughness,h为
描述的是微表面的法线方向与半角向量对齐的概率,如果对齐那么认为该反射光可以看 到,否则没有。可理解为粗糙度.仅有D项的效果:
Blinn-Phong高光经验模型就是一种非常简单的法线分布函数:
这部分主要是希望得到一个漂亮的高光效果。传统的Blinn-phong高光缺乏真实度,研究发现高光是带有拖尾的,GGX模型就是为了把这个拖尾模拟出来,虽然还不能完全模拟,但是比之前的模型已经好了很多。
黑色曲线表示MERL 铬金属(chrome)真实的高光曲线,红色曲线表示 GGX分布(α= 0.006),绿色曲线表示Beckmann分布(m = 0.013),蓝色曲线表示 Blinn Phong(n = 12000),其中,绿色曲线和蓝色曲线基本重合。可以发现,GGX相对于传统的模型,更接近真实了。
D(h)的代码:
//D项 NDF
inline float GGXTerm (float NdotH, float roughness)
{
float a2 = roughness * roughness;
float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
return UNITY_INV_PI * a2 / (d * d + 1e-7f);
}
//D项 NDF
inline float GGXTerm (float NdotH, float roughness)
{
float a2 = roughness * roughness;
float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
return UNITY_INV_PI * a2 / (d * d + 1e-7f);
}
G(I,v,h)是阴影—遮掩函数:
由Roughness调节,有两项,均套用同一公式如下图.理解:与法线分布不同点在于:这些光按照法线分布来讲本可以反射到观察者,但是由于以下两种遮蔽照成了屏蔽,所以要在法线分布中"减去"
(a=roughness)
跟材质的粗糙程度有关,可以理解为越粗超的材质表面越有可能发生自我遮蔽现象。 G项应该分为两部分相乘: 第一部分 是 GeometryObstruction 第二部分是Geometry Shadowing 即:
其中的K为直接光的K,即:
float GeometrySchlickGGX(float NdotV, float k)
{
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(float3 N, float3 V, float3 L, float k)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx1 = GeometrySchlickGGX(NdotV, k);
float ggx2 = GeometrySchlickGGX(NdotL, k);
return ggx1 * ggx2;
}
float GeometrySchlickGGX(float NdotV, float k)
{
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(float3 N, float3 V, float3 L, float k)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx1 = GeometrySchlickGGX(NdotV, k);
float ggx2 = GeometrySchlickGGX(NdotL, k);
return ggx1 * ggx2;
}
还有一种公式是:
F(I,h)是菲列尔反射函数:
/F项
half3 FresnelTerm(half3 F0,half cosA){
half t=Pow5(1-cosA);// ala Schlick interpoliation
return F0+(1-F0)*t;
}
/F项
half3 FresnelTerm(half3 F0,half cosA){
half t=Pow5(1-cosA);// ala Schlick interpoliation
return F0+(1-F0)*t;
}
n代表介质的折射率,非金属的F0数值较小,金属F0的数值较大。
法线和视线夹角越大(视线越接近水平),F的值也就越大,反射光的亮度也越高,这就是所有物体都具有的菲涅尔效应。
对于金属来讲,它的Albedo其实就是F0的颜色,对于塑料这种非金属来讲,它的F0就是unity_ColorSpaceDielectricSpec.rgb,这是一个Unity内部设置的默认值,非常暗,颜色值为float3(0.04, 0.04, 0.04),算是一个非金属的默认F0了。
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, Albedo, _Metallic);
PS:
性能更好的拟合版本,诸如UE4的Paper里,菲涅尔部分使用的公式:
通过把pow函数换成exp2,得到了更好一点的性能。
2.2.间接光(含漫反射和镜面反射):
间接光照可以分为漫反射间接光照和镜面反射间接光照,其中漫反射的间接光使用球谐光照来实现,而镜面反射的间接光照由CubeMap来实现。
2.2.1间接光漫反射:ShadeSH9函数+LightProbe
IBL(Image-Based Lighting 基于纹理的光照) 是所谓的动态全局光照(Gl)的正体,是用场景周围环境生成的cubemap。
漫反射部分就是最普通的环境贴图,因为并没有任何变量,直接搞出一张很糊的环境图,再通过normal从cubemap直接采样颜色值即可。
我们可以非常方便的取出漫反射信息,只需要在片元着色器中加一句:
half3 ambient_contrib = ShadeSH9(float4(i.normal,1));//注意输入为WorldSpace的法线
half3 ambient_contrib = ShadeSH9(float4(i.normal,1));//注意输入为WorldSpace的法线
而高光部分,则有粗糙度α这个变数,必须需要烘焙出多个粗糙度下的环境图。然而,不同α值下的烘焙出环境贴图,其实主要就是模糊程度的不同,所以生成这样一组图:
然后合并到一张cubemap的多个mipMap层级上,再利用cubeTexLod函数,根据其粗糙度选择特定层级的两个mipMap层级进行三线插值,就能得到需要的半球积分过的光照颜色值了(当然是近似的)。
float lod = getMipLevelFromRoughness(roughness);
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
float lod = getMipLevelFromRoughness(roughness);
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
然后通过光照正常计算一次BRDF就好了。
这样各个不同粗糙度和金属度的材质就都能从同一张环境贴图里获得需要的数据,并完成各自的渲染。