【unity shader】PBR的理解和实现vol.2

PBR理解和实现vol.1中我们已经阐述了PBR相关的基本概念和公式。
下面我们直接开始实现环节。

PBR光照(点光源)

在这里插入图片描述
首先我们再次请出我们得反射率方程。
第一步,我们初始化radiance。

float3 Lo = float3(0.0, 0.0, 0.0);
//Lo出射radiance,Li入射radiacne,来自公式

基于光源距离的能量衰减

接下来是能量守恒的第一步,设置光源根据距离的衰减来计算入射radiance。
实际上只有点光源才能做基于光源距离的能量衰减,要是用定向光的话基本上就没办法这么考虑了。

float distance = length(_WorldSpaceLightPos0 - i.worldPos);
float attenuation = _LightPower / (distance * distance);

float3 radiance = _LightColor0 * attenuation;

之所以这里要去设置一个_LightPower 变量,是因为根据不同的场景下,场景中物件的尺寸,摆放位置,光源位置上都有差异。按照opengl里面的写法:
在这里插入图片描述
很有可能会出现在scene里看着很近,实际上distance相当大以至于radiance几乎没有的情况。
这里_LightPower 我给的值域是1-100。
贴个图展示下基于距离衰减的radiance的效果。可以看到离光源越近的物体,亮度就越大。
在这里插入图片描述
调整_LightPower ,注意最开始的值是1,可以看到球体基本是全黑的。最开始我还以为是有什么不可知的bug,在这里debug调到怀疑人生。
在这里插入图片描述

BRDF求解,合并radiance

之前我们已经在vol.1中说过D,F,G三个函数的实现了,这里我们直接就用他们的函数,写反射率方程对应的方法。

// 反射率方程-镜面反射部分
float NDF = DistributionGGX(N, H, _roughness);
float G   = GeometrySmith(N, V, L, _roughness); //四个参数说明是两次几何函数计算      
float3 F    = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);
//DFG部分
float3 numerator    = NDF * G * F; 
//分子部分
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; 
float3 specular = numerator / denominator;

基于F的结果,计算kd,ks。

float3 kS = F;
float3 kD = float3(1.0, 1.0, 1.0) - kS;
kD *= 1.0 - _metallic; 

然后我们向Lo中增加对应的Li radiance。

// 加上NdotL主要是入射方向差异会导致能量吸收率的差异
float NdotL = max(dot(N, L), 0.0);        
//注意这里的写法是opengl写法,由于unity shader的多光源模式难以在不改动管线的情况下,做逐个光源的radiance叠加,所以这里实际上只用了一个点光源
//实际上可以写成 Li = (kD * _Albedo / UNITY_PI +  specular) * radiance * NdotL;
// 然后 Lo += Li;
Lo += (kD * _Albedo / UNITY_PI +  specular) * radiance * NdotL;

那么我们得到了Lo radiance之后是不是可以直接兴高采烈直接输出了?
也不是的,连phong光照都考虑了环境光,难道不是IBL你就准备不考虑环境光了吗?
所以在PBR光照的情况下也得赋予一个固定的环境光值。

float3 ambient = float3(0.03, 0.03, 0.03) * _Albedo * _ao;
float3 color = ambient + Lo;

这边的color才是最终的颜色结果,记得要上gama校正。

PBR的特性表现

金属度变化
在这里插入图片描述
光照强度变化(注意和金属度变化的区别)
在这里插入图片描述
个人感觉主要是specular本身在非ibl的情况下,没有全局光照补充radiance,spcular在最终效果中占比较小,所以调整metalness会直接影响到最亮的diffuse光照,最终导致对效果的影响较大。
粗糙度变化
在这里插入图片描述
AO变化
AO的变化在正面看不太明显,由于是描述自遮蔽因素的,在背面看,乃至于在IBL中,其对全局光照的影响会更加的明显。
在这里插入图片描述
背面看AO变化
在这里插入图片描述
本来动图压图就厉害,这下更看不出来了哈哈。。。直接上截图吧
Ao为0.01和1的对比
在这里插入图片描述
在这里插入图片描述

PBR光照的尴尬

在非IBL的情况下,而且光源位置固定的时候,PBR和phong的视觉效果差别并不大。
当然这里是特地调过一下phong的albedo让他们看起来更像一些。
最上面一行的三个球体为blinn-phong光照。
在这里插入图片描述
在这里插入图片描述
phong经典的亮面增大而亮度不变。
在这里插入图片描述
pbr能量守恒的表现。
在这里插入图片描述

PBR IBL(点光源)

IBL的部分我在vol.1中没有太多的讲,实际上IBL就是在主光源的前提下,实现了环境光贴图作为全局副光源的radiance输入,最后都是合并到radiance计算中的,就是再跑一次反射率方程的流程。
所以就是说,在IBL的模式下,反射率方程的计算是使用两次:

  • 一次是通过主光源计算获取主光源的radiance
  • 第二次是通过采样环境光HDR图来获取全局环境的radiance

然后将他们合并。其中会涉及一些优化性的概念,由于unity本身能够通过UNITY_SAMPLE_TEXCUBE_LOD这个函数来做HDR图的采样,并且还自动分了LOD,省去了我们做mipmap的功夫。但是像LUT这样的概念,在后面IBL的部分我也会稍作补充说一下自己的理解。

与pbr lighting的差异

ibl与PBR lighting最大的区别在于ambient部分,对于主光源的计算部分则保持一致。

//pbr lighting的ambient,默认设置一个亮度较低的albedo
float3 ambient = float3(0.03, 0.03, 0.03) * _Albedo * _ao;
//pbr ibl的ambient,通过UNITY_SAMPLE_TEXCUBE_LOD采样环境光贴图,作为全局irradiance
float3 diffuseIrradiance = sRGB2Lin(UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, N, _RoughnessLod).rgb);
float3 specularIrradiance = sRGB2Lin(UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, R, _roughness * _RoughnessLod).rgb);
//从LUT图中获取brdf的烘焙结果
float2 brdf_ibl = tex2D(_LUT_Tex, float2(max(dot(N, V), 0.0), _roughness));
specularIrradiance = specularIrradiance * (F * brdf_ibl.x + brdf_ibl.y);

float3 ambient = (kD * diffuseIrradiance + specularIrradiance) * _Albedo * _ao;

UNITY_SAMPLE_TEXCUBE_LOD这个函数在我们设置好hdr天空盒之后,就能够通过unity_SpecCube0这个默认变量直接获取,作为irradiance使用。
此外,unity还贴心地分好了mipmap(在导入hdr图的时候就已经能够自动计算了),共计11个等级,我们看到_RoughnessLod这个参数,实际上就能够获取对应等级的hdr mipmap。
在这里插入图片描述
UNITY_SAMPLE_TEXCUBE_LOD第二个参数是采样方向,就不多做解释了。
由于是从图像里面采样radiance,我们这里再做一个srgb到线性空间的转换。
所以不要再说什么unity搞pbr不方便了,贼好使!这不比opengl方便多了。。
UNITY_SAMPLE_TEXCUBE_LOD采样的效果,变化的是_RoughnessLod参数
在这里插入图片描述

关于LUT图

LUT主要是提前把brdf的结果提前烘焙到了该贴图上,从而能够避免在计算时再去做繁琐的brdf求解工作,直接计算纹理坐标,然后从LUT贴图上直接去取brdf的结果即可。
首先我们来看d,f,g三个函数的参数。N和V固定去拿就行,H跟N和V有关,_roughness也固定的。主要是L,像环境光这种半球内计算的,如何去拿到L呢?逐个逐个去累加显然是很浪费效率的,所以我们需要预先把BRDF结果烘焙到LUT贴图里面。

float NDF = DistributionGGX(N, H, _roughness);
float G   = GeometrySmith(N, V, L, _roughness);      
float3 F    = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);

此外,我们再看公式,这里一共有4个参数:n, Wo,Wi, _roughness。既然要预计算brdf积分的结果,那么这四个会变化的变量就必须得作为图片的x,y轴坐标。
在LUT图中,x轴是NdotWi参数,y轴是_roughness参数。Wo则默认其是通过反射的方式获取,将其变换为Wi。
在这里插入图片描述

在这里插入图片描述
与pbr lighting的效果对比(最右列三个为pbr ibl),可以见到暗部部分也出现了丰富的场景细节
在这里插入图片描述
在这里插入图片描述
通过调整参数,能做到跟默认pbr材质效果接近(最右为默认pbr材质)
在这里插入图片描述

PBR IBL(定向光)

跟点光源的最大差别就在于,无法获取对应的光源距离,应该说即便获取了也没有真正的物理意义,从而做不了基于光源距离的能量衰减。
需要调整的地方一个是LightMode,点光源换成定向光这里必须要调整。
此外就是涉及,光线方向,能量距离衰减这部分的代码要调整。
这些都是hlsl的基操了,就不再多做讲解了。

float3 Lo = float3(0.0, 0.0, 0.0);

float3 L = normalize(_WorldSpaceLightPos0);
float3 H = normalize(V + L);
float3 radiance = _LightColor0 ;

效果如下,经过相关参数的调整,也能做到跟默认材质接近
在这里插入图片描述

使用PBR材质的效果对比

来源:免费pbr
左边是IBL,右边是phong
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/misaka12807/article/details/130496763