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

1. 前言

还记得自己念高中那会,朋友还会很兴奋的跟我说虚幻引擎开始支持基于物理的渲染了,当时是什么游戏,好像是孤岛危机2还是3,那个时候还觉得pbr是什么极其前沿的科技。
到现如今pbr相关的工具流已经相当的完善,pbr资产的流程已经高度融合到了各类游戏/数字孪生/数字人应用的方方面面。
毫不夸张的说,如果做个游戏不用pbr都拿不到投资人的钱;对于ta来说更加是必修课中的必修课,面试的时候问起pbr如果直接一问三不知的话,估计面试都可以当场结束了。。。

2. 概述

与传统的phong/blinn-phong光照模型相比,pbr主要是引入了三个新概念:

  • 微平面:主要是把物体的表面看做由无数微小的平面组成的,这些微小平面能够各自具备不同的特性,所以渲染出来的物体看起来信息量就会比传统光照模型大很多。一般来说,往往通过对应的纹理来存储微平面信息(按我个人理解,我们可以把纹理中的一个像素就对应一个微平面的特性)。
  • 能量守恒:在传统光照模型里面,由于默认光线从光源出发到物体表面再到人眼的过程中是不存在衰减的,有时候我们在调整specular的shiness时,就会出现高光的面积增大但是亮度完全不减的情况,从写实的角度来说容易让渲染看起来偏油腻。在learn-opengl的pbr的理论中,能量守恒不仅体现在随着光源距离的能量衰减上(一般unity拿点光源的位置比较方便,我看有的做定向光实现的pbr会不去考虑距离衰减),同时在物体表面处也会发生光线的折射分散从而导致能量衰减,折射的情况也与对应表面的特性相关,这也就对应了微平面的特性。
  • 基于物理的brdf:双向反射分布函数主要是描述入射光线和出现光线在对应物体表面位置的关系,就定义来说,phong也算是brdf,但是在修饰词来说,基于物理的brdf结合了微平面和能量守恒的特性,从而以一种比传统光照模型更复杂,但性能开销上仍能够接受的方式来描述基于物理的表面反射现象。

总的来说,微平面、能量守恒和基于物理的brdf是相互结合,互不可分的。

2.1 微平面

pbr的理论中,物体的表面是由很多的微平面组成的(至少也跟对应的贴图像素数一致),这些微平面都具有相应的特性:法线朝向,金属度,粗糙度,自遮蔽阴影等。而这些特性都是通过对应的pbr纹理来记录的。
在法线本身的粗糙度以外,还有一个描述全部微平面的统计粗糙度。在本节内后续提到的全部粗糙度定义,都是统计粗糙度的定义。
根据该粗糙度的不同,这些微平面的的法线方向上会由很明显的差异,粗糙度越低,微平面的法线分布就越规律,反之则越杂乱。因而光线的反射也会越规律,从而体现在光滑的表面表现上。
粗糙平面和光滑平面的对比:
在这里插入图片描述
法线分布情况:
在这里插入图片描述
光线的反射情况:
在这里插入图片描述
粗糙度由0.1到1.0的表现变化。
在这里插入图片描述

2.2 能量守恒

一方面,能量守恒体现在随着光源距离衰减,物体会逐渐变暗上。
另一方面,随着光线区域的分散,亮度也一样会下降,就像上面的图示一样。
对于接触到物体表面的光线,pbr理论将其分为反射部分(黄色)折射部分(红色)
在这里插入图片描述

反射部分体现的是镜面颜色(specular),折射部分由于有部分光线会再次离开物体表面,体现的是漫反射颜色(diffuse)。
与次表面散射不同的是,经典的pbr模型中,物体全部的物理性质都只由物体表面一层来决定,而不认为物体内部还存在其他的特性。
对于金属材质来说,金属度越高,反射光照的占比就越高,表现上就越是以镜面颜色为主。
对于非金属材质(也称电介质,Dielectrics),则是镜面颜色与漫反射颜色的平均混合,且随着非金属度的提高,往往以漫反射颜色颜色为主。

2.3 基于物理的brdf

由于brdf是反射率方程的一部分,所以我们会先从反射率方程开始讲起。

2.3.1 反射率方程

在传统光照模型中,往往把光源试做一个点。所有的光线都从该点出发,到物体表面,再到摄像机,是一种点到点的光照模型。就算通过fwadd来做多光源,实际上也就是多个(有限个)点到点模型的叠加。
在pbr中,通过反射率方程,实现了一种从面到点的光照模型。
我们可以看到方程中这么大的一个积分符号,就是把一定范围内(在Ω这个半球的范围内)的光源都作为入射光线考虑进反射方程中,综合计算他们对出射光线的贡献。
在这里插入图片描述
接下来浅将一下PBR光学中引入的一系列用于约束能量守恒的概念。

辐射率 Radiance

其中LoLi,L是量化描述在某位置p的光线能量的变量,我们称之为辐射率Radiance
可以看出,Li(p,wi) 即位置p从入射方向wi上收到的光线能量,Lo(p,wo) 即点p在观察(出射)方向上反射的光线能量。
通过对某一个位置p的全部入射方向上的光线能量Li进行积分,再通过brdf计算每一个入射光线对出射光线的贡献,最终求解得到Lo

辐射通量 Radiant flux

辐射通量 Φ 没有直接出现在反射率方程中,但在教程里还是时常出现。
Φ 表示的是光源所输出的能量总和,对于点光源来说,Φ 就是其在全方向上放出的光线能量的总和。
我们说PBR的重要特性之一就是能量守恒,通过对光源增加响应的约束概念和条件,实现对能量(光线)的守恒计算。

立体角Solid Angle

立体角用 ω 来表示,虽然名字上叫“角”,但其在符号上跟角没有什么关系。
ω 表示的是投射到单位球体上某一点的一个截面的大小/面积,该投射面后续将用做光照的计算,投射面的面积为 ω
通过立体角的定义,PBR就摆脱了传统的点到点的光照计算模型,转入了面到点的模型。
在这里插入图片描述

辐射强度Radiant intensity

辐射强度用 I 表示。
在单位球面上,光源对每单位立体角所投送的部分辐射通量。
当然大伙最讨厌看到积分啊,导数啊之类的公式了,但也没有办法。这个公式假设的就是,光源对全部任何方向上的单位立体角都能够均匀地投送能量。
在这里插入图片描述

求解辐射率

在上述定义下,我们计算一个总能量(即辐射通量) Φ 的光源通过单位立体角 ω ,在单位面积 A 的平面上投射出的总能量 L 。之前提到过,PBR是面到点的光照计算模型,所以这里也是以立体角的单位面 ω,作为中间媒介,分成 光源到 ωω 到平面A两部分来计算光源投射到平面A的光照强度。
在这里插入图片描述

  • 第一部分,光源对该立体角(单位面)投射出了多少能量,上面刚刚提到:

在这里插入图片描述

  • 第二部分,这部分能量 I 与平面A发生接触,并最终被平面吸收的能量 L 。根据入射方向的不同,平面最终能够吸收到的能量也有差异,当入射方向和平面法线相同(或者说刚好相反)的时候,平面吸收到的能量最大。所以这里是跟入射方向与法线方向夹角的余弦值相关的导数。

在这里插入图片描述

在这里插入图片描述
根据第一步的公式补全后,就是完整的公式了。
在这里插入图片描述

2.3.2 求解半球体积内的积分

总之在上述的2.3.1节中,我们称述了反射率方程的物理意义。即我们通过积分的方法求解在特定某位置p上的半球范围内,全部的入射方向的光线的辐射率,并通过brdf调整对应入射方向的光线能量对出射方向的贡献,最终累加解出接受半球范围内全部入射光的店p,其在反射方向wo上的光线能量。
在这里插入图片描述
那么问题来了,在shader里面要怎么写微积分呢?
反射率方程里的这个美妙的符号显然是一段连续积分,有的人可能一看到微积分当场就要晕死过去,更何况而我们的计算机实际上处理的都是离散的数据,连续域的积分跟计算机可以说是八字不合。
我当年第一次看到连续积分的时候也是痛苦van分,那么有没有什么办法能够把连续积分转换成大家希望乐见的离散积分(累加)呢?

黎曼和Riemann sum

黎曼和是一个同样讨厌连续积分的德国老哥Bernhard Riemann提出的,这位老哥不幸的在18世纪就遇到需要用连续积分来解决的难题——如何计算不规则的土地的面积。
总的来说,黎曼和就是一种将连续积分转换成离散积分的方法,一开始是用于求解不规则地块的面积问题,总的来说就是通过把连续域(地块)划分成有限个规则矩形,通过这些规则矩形的面积累加,求解不规则地块的面积。
当划分成的规则矩形越多,最后累加得出来的数据就越接近地块的真实面积。
想具体了解数学姿势的兄弟们请走。
黎曼和-百度百科
通过规则矩形逼近积分结果的示意图。图片最下方的其实是矩形累计值和积分值的差值,可以看到随着划分的矩形数目越多,实际上差值是在不断缩小的。
在这里插入图片描述
这里我们引用一段learn-opengl里面的源码来说明黎曼和的应用方法。

//步长,根据steps值把连续的一段积分区域转换成离散的多次采样,
int steps = 100;
//sum用于记录radiance
float sum = 0.0f;
//位置p,法线方向N,出射方向Wo
vec3 P    = ...;
vec3 Wo   = ...;
vec3 N    = ...;
float dW  = 1.0f / steps;
for(int i = 0; i < steps; ++i)
{
    
    
	//这里通过不同的i值获取入射方向Wi,主要是体现在采样位置的区别上,从不同的采样位置获取的入射radiance是不同的
    vec3 Wi = getNextIncomingLightDir(i);
    //累加实现出射方向的radiance求解,Fr:BRDF
    sum += Fr(p, Wi, Wo) * L(p, Wi) * dot(N, Wi) * dW;
}

严格来说这是一段从二维空间降为到一维的方法,即这种方法能完成把一段连续的线拆分成在该线上的一系列离散点的集合。
若是需要把三维的半球压缩成二维平面上分布的离散点,后半部分的代码应为:

for(int x = 0;  x < steps; ++x){
    
    
	for(int y = 0; y < steps; ++y){
    
    
		//这里通过不同的x,y值获取入射方向Wi,主要是体现在采样位置的区别上,从不同的采样位置获取的入射radiance是不同的
	    vec3 Wi = getNextIncomingLightDir(x, y);
	    //累加实现出射方向的radiance求解,Fr:BRDF
	    sum += Fr(p, Wi, Wo) * L(p, Wi) * dot(N, Wi) * dW;
	}
}

2.3.3 BRDF的基本理论

我们再一次提到brdf的概念,即双向反射分布函数(Bidirectional Reflective Distribution Function)。接收一个入射方向Wi,一个出射方向Wo,平面法线以及表面平面特性的相关参数(金属度metalness,粗糙度roughness),最终返回一个当前入射方向的radiance对出射方向radiance的贡献,即0-1之间的值(在能量守恒关系下显然不可能出现小于0或者大于1关系的情况,除非是有自发光物体,但这里我们不考虑自发光问题)。

cook-torrance BRDF模型

这里我们使用的是cook-torrance BRDF模型(以下简称为ct-brdf)。就像我们在能量守恒一节中提到的,ct-brdf主要分为漫反射和镜面反射两部分:

Fr = kd * F-lambert + ks * F-cookTorrance

kd这边显然就是漫反射部分(diffuse),F-lambert说明这边使用的是lambert漫反射。

F-lambert = c/π

c表示表面颜色,一般来说由漫反射贴图提供,除以π主要是因为需要对该数值进行标准化,抵消在半球积分中带来的π的影响。
总之,在漫反射部分,计算方式和效果上跟传统光照模型还是比较类似的,毕竟传统光照模型就是物体在及吸收了一部分光的同时,又反射出对应的光,才形成的颜色。
ks是镜面反射部分(specular),ct-brdf的镜面反射部分相对复杂:

F-cootTorrance = DFG/(4(Wi * n)(Wo * n))

Cook-Torrance BRDF的镜面反射部分包含三个函数,此外分母部分还有一个标准化因子 。字母D,F与G分别代表着一种类型的函数,与learn-opengl中一致,我们这里采用Epic Games在Unreal Engine 4中所使用的函数,其中D使用Trowbridge-Reitz GGX,F使用Fresnel-Schlick近似(Fresnel-Schlick Approximation),而G使用Smith’s Schlick-GGX。

法线分布函数D

D是法线分布函数(NDF,normal distribution function),主要受物体表面粗糙程度的影响,表示微平面法向量与半向量朝向一致的概率。
在公式上,法线分布函数为:
在这里插入图片描述
表现上来说,表面越粗糙(roughness值越高),高光就越分散
在这里插入图片描述

HLSL代码如下:

float DistributionGGX(float3 N, float3 H, float roughness)
{
    
    
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 =  NdotH * NdotH;

    float nom = a2;
    float denom = (NdotH2  * (a2 - 1.0) + 1.0);
    float denom2 = UNITY_PI * denom * denom;

    //return denom2;
    return nom / denom2;
}
几何函数G

几何函数(Geometry Function),主要描述微平面之间相互遮蔽的概率,注意光线在入射和出射时都有可能会被遮蔽,所以一般是进行两次几何函数的计算并相乘。
在这里插入图片描述
公式上,几何函数:
在这里插入图片描述
k值是跟粗糙度相关的参数,但在直接光照和IBL上有区别:
在这里插入图片描述
HLSL代码如下:

float GeometrySchlickGGX(float NdotV, float roughness)
{
    
    
     float r = (roughness + 1.0);
     float k = (r*r) / 8.0;

     float nom   = NdotV;
     float denom = NdotV * (1.0 - k) + k;

     return nom / denom;
 }

 float GeometrySmith(float3 N, float3 V, float3 L, float roughness)
 {
    
    
     float NdotV = max(dot(N, V), 0.0);
     float NdotL = max(dot(N, L), 0.0);
     float ggx2 = GeometrySchlickGGX(NdotV, roughness);
     float ggx1 = GeometrySchlickGGX(NdotL, roughness);

     return ggx1 * ggx2;
 }
菲涅尔函数F

菲涅尔函数(Freh-nel Function)主要描述被反射的光线对比光线被折射的部分所占的比率,主要根据观察角度变化。
公式上:
在这里插入图片描述
由于这里采用的Schlick菲涅尔函数只能描述非金属,所以为了保证通用性,需要基于albedo和metalness值做差值来求解F0。
HLSL代码如下:

float3 fresnelSchlick(float cosTheta, float3 F0)
{
    
    
	//cosTheta是半向量和观察方向的点乘
     return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
 }

//默认F0值根据物体材质特性会有较大变化
float3 F0 = float3(0.04, 0.04, 0.04); 
F0 = lerp(F0, _Albedo, _metallic);

2.3.4 完整的反射率方程

有了先前的全部定义后,我们获得了完整的反射率方程。
在这里插入图片描述
由于我个人不喜欢去翻看那些长篇累牍的技术文章,后续我将在vol.2中更新unity shader(直接光照/IBL,点光源/平行光)的具体实现。
先放一张封面图,可以看到PBR能量守恒的特性,比如说随着光源距离变化带来的亮度衰减,这种表现最上面部分的三个bling-phong光照的球体是没有的。
在这里插入图片描述

猜你喜欢

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