初始PBR
最近刚接触pbr不久,我搜寻了许多文章进行阅读并了解后发现,pbr涉及到的知识点繁琐且不容易理解,所以想在博客上给自己记录并总结一下,方便以后回顾并加深记忆。
这里首先借用知乎上的大佬毛星云关于pbr所涉及到的知识要点:
下面就整理下自己的知识点
一. PBR的基本介绍
1.1 PBR的概念
PBR是Physically based rendering的缩写,直译的意思就是基于物理的渲染,即利用真实世界的规律和理论,使用数学和物理的相关知识和公式推导出一系列渲染的方程,近似的模拟真实世界。这里为什么说近似呢,因为在真实世界里,人眼的像素级别是以亿为单位的,而目前人类所使用的硬件水平和知识水平暂时还达不到这种高度,所以只能最大程度的模拟真实世界
1.2 PBR涉及的参数
在Unity3d里有一个最标准的内置的pbr着色器(Standard Shader),里面包含着许多重要的参数,下面列举一些比较重要的参数:
1.2.1 Albedo
定义了物体整体的颜色,也可以使用纹理进行采样,非金属材质的亮度范围通常在50-243,而金属材质的亮度一般在186-255之间
1.2.2 Metallic
定义了物体像金属的程度,该值同样可以用纹理进行采样,对应的是纹理的R通道值,如果该值为0,则表示该物体为一个绝缘体,如果该值为1,则表示该物体完全是一个金属材质
1.2.3 Smoothness
定义了物体表面的光滑程度,该属性非常的重要,因为在后面涉及到光照计算时,该值要作为参数进行计算。该值作为Metallic的附属属性,如果在定义Metallic时使用了纹理进行取值,那么该纹理的A通道值则会作为Smoothness的值
另外还有许多参数,诸如法线贴图、高度图、遮蔽图等属性,这些暂时就不一一细说,下面给出Unity提供的校准表格,分别是金属工作流和高光反射工作流的校准表格。
二. PBR的基础理论和推导
要先满足PBR,我们首先要满足一下三个条件:
- 基于微平面模型(Be based on the microfacet surface model)
- 能量守恒(Be energy conserving)
- 使用基于物理的BRDF(Use a physically based BRDF)
下面就一一细说以上三个条件
2.1 微平面(Microfacet)
大部分的PBR技术都是基于微平面理论。微平面理论认为,在微观上,所有材质表面都是由很多不同的朝向不一的微小平面组成,有的材质光滑而有的材质粗糙,如图左是较为粗糙的,图右是较为光滑的。
这样的结果会导致什么呢?当光线射入这些平面时,通常会发生镜面反射,而较为粗糙的平面的反射光的朝向会更加的乱序,光滑的平面会较为的平齐。如图所示
从微观角度上讲,没有任何平面是绝对的光滑的,而微平面已经无法进行逐像素的细分,所以我们一般使用上面提到的参数Smoothness粗糙度,用统计学的方式来估算平面的粗糙程度。
下面要引入一个重要的参数,这个参数是光照向量 l 和视线方向 v 之间的中间向量,我们一般称它为半角向量(Halfway Vector),用 h 来表示,下面是基于Unity的计算公式:
//lightdir 为光照方向 viewdir 为视角方向 worldpos 为计算的在世界的顶点坐标
fixed3 lightdir = normalize(UnityWorldSpaceLightDir(worldpos));
fixed3 viewdir = normalize(UnityWorldSpaceViewDir(worldpos));
fixed3 halfdir = normalize(lightdir + viewdir);
2.2 能量守恒(Energy Conservation)
在基于微平面理论下,我们采用近似的能量守恒,即出射光总能量不能大于入射光总能量(自发光材质除外)。为了进一步了解该定律,我们首先要知道,当一束光照射下来时,光路具体的变化。
当光照射到物体表面时,通常会发生反射和折射两个部分。反射就是我们常说的镜面反射,即不经过物体内部,直接从表面反射出去,而从常理来说,一般光滑的物体漫反射较少,高光反射比较多,比如金属一类的材质。反之则是塑料一类的粗糙物体,漫反射较多,高光反射比较少。而折射即指光线进入了物体的内部,而该部分的光线会跟物体内部的粒子碰撞,产生损耗,下面又分几种情况。
第一种情况是光线经过碰撞后完全消耗掉,即在内部消失。
第二种情况是经过多次碰撞后又从物体表面折射出,我们一般称这个光学现象叫次表面散射,这种应用一般用于皮肤、大理石、蜡像等材质渲染。
第三种情况是经过多次碰撞后从物体的另一个表面折射出,我们一般称这个光学现象叫透射。
不过这几种情况在这里不多做讨论,等以后深入了解了在记录。
回到能量守恒这个话题,通常PBR会简化折射部分,将平面上的折射光都视为完全吸收而不会散开,即不存在次表面散射。所以我们可以先计算镜面反射部分,此部分等于入射光线被反射的能量所占的百分比。而折射部分可以由镜面反射部分计算得出。
float kS = calculateSpecularComponent(...); // 反射/镜面部分
float kD = 1.0 - kS; // 折射/漫反射部分
所以我们可以看出,镜面反射部分和漫反射部分的和肯定不会超过1.0,从而近似地达到能量守恒。
★2.3 反射方程(Reflectance Equation)
该章节非常的重要,所以标注了一个星星。
渲染方程(Render Equation)是用来模拟光的视觉效果最好的模型。而PBR的渲染方程是用以抽象地描述PBR光照计算过程的特化版本的渲染方程,被称为反射方程。
PBR的反射方程可以抽象成下面的公式:
一眼看去,好像很复杂,其实不然。我们把该公式拆分,并一一阐述,就一目了然了。
首先,我们先把公式下的参数了解清楚,Lo为出射光的辐射率,一般光的测量便是用辐射度量来计算,而p便是该点的辐射率,ωo指的是视角方向,所以该式子计算的便是该p点在所有视线方向ωo上被反射出来的综合,即总辐照度。
下面式子里分为三个部分,第一个部分为反射函数,ωi一般指的是光照方向,该函数为重点,待会儿我们将细细拆分。第二个部分是入射光的辐射度,该部分一般可以由光源处获得。最后一个部分是一个点乘计算,即受到入射光与平面法线夹角影响。
最后我们在把以该点法线为基准的一个半球,做一个积分运算,因为有多个方向的入射光,所以我们需要计算所有相关的光照,即如图所示:
★2.4 双向反射分布函数(BRDF)
在反射率方程里,我们提到了三大关键的部分。而其中,BRDF反射函数部分是最为关键的一部分,它是一个使用入射光方向ωi作为输入参数的函数,输出参数为出射光ωo,表面法线为n,参数a表示的是微平面的粗糙度。
首先我们先理解该函数的意义,该函数的意义是计算每一束不同的光线对不透明的物体最终的反射光的贡献量。而BRDF也是基于之前提到的微平面理论和能量守恒定律,BRDF函数有多种模拟表面光照的算法,具体在一开始给出的图里可以看到,下面以Cook-Torrance BRDF为例,基本在大部分的实时渲染上都是使用该算法。
首先该算法分为镜面反射项和漫反射项两个部分,具体公式如下:
其中Kd是入射光中被折射的比例,Ks是另外一部分被镜面反射的入射光。在等式左边部分的Flambert表示的是漫反射部分,与我们之前学习的普通光照模型漫反射类似,但它是一个恒定的算式:
其中C代表的是Alebedo或表面颜色,而除于π是为了后面的积分做准备,当然我们记得在以前兰伯特模型里,后面还需要乘上表面法线和光照的点积,其实并没有省略,只是我们把该点积移出了反射方程后面的位置,即之前讲到的n · Wi。
BRDF另外一部分为高光反射项,它具体的公式为:
该式子由三个函数DFG和一个标准化因子构成。而D、F、G的函数意义则是充分模拟了特定部分的表面反射属性,下面就来一一介绍。
- D(Normal Distribution Function,NDF): 法线分布函数,估算在受到粗糙度影响下,微平面的法线与中间向量方向一致的比率。
- F (Fresnel equation): 菲涅尔方程,描述的是在不同的表面角下表面反射的光线所占的比率
- G (Geometry function): 几何函数,也可以称为阴影-遮掩函数,含义是计算当一些微平面相对粗糙,导致遮挡住其他微平面所反射的光线
★2.4.1 D (Normal Distribution Function,NDF)
法线分布函数,从统计学上近似的表示了微平面法线与中间向量h取向一致的微平面的比率。对于该函数已经衍生出许多不同的计算方法和模型,我们这里选用的是Trowbridge-Reitz GGX(GGXTR) 模型,具体公式如下:
简单介绍下这里的参数,首先n为表面的法线,h为上文曾提到的中间向量,a为表面的粗糙度。
对于该函数,通俗的来说,我们可以这样理解。我们在宏观上先把平面的法线定义为n,在微观上我们先把微平面上的法线定义为m,然后把光照方向l和视角方向v的中间向量定义为h即上文所说的计算。因为我们都知道,镜面反射是入射角和反射角相同的反射,因此,当中间向量h和微平面的法线m一致的时候,就会发生镜面反射。所以该函数实际上可以改成,当给定光的向量l和视角方向v都给定的时候,与他们计算得到的中间向量h一样的微平面法线m有多少?下面给出更加直观的图:
在Unity,该模型的代码如下:
float DistributionGGX(fixed3 N, fixed3 H, float a)
{
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);
denom = PI * denom * denom;
return nom / denom;
}
★2.4.2 F(Fresnel equation)
菲涅尔方程定义的是在不同的观察方向上,表面上被反射的光除于被折射的光的比例。举一个现实例子,一般我们在水面上的时候,低头看水面,这个时候视角方向几乎是90°,所以这个时候光线的折射占比非常大,而反射非常少。反之,我们看远处的水面时,有很多时候都看不到底面,都是一层高光的反射,这个时候便是反射占比大,而折射占比少。
对于菲涅尔方程,我们使用的是Fresnel-Schlick来近似:
其中,F0表示的表面基础反射率,需要注意的是,Fresnel-Schlick算法仅仅是为了电介质表面定义的算法,所以如果我们需要计算金属表面时,需要预先计算F0,下图是一些基础反射率:
另外对于金属来说,它的基础反射率一般是带有色彩的,所以我们一般会这样处理:
vec3 F0 = vec3(0.04);
F0 = mix(F0, surfaceColor.rgb, metalness);
最后在Unity中,我们可以这样定义Fresnel Schlick
//cosTheta为表面法向量n与观察方向v的点乘的结果
fixed3 fresnelSchlick(float cosTheta, fixed3 F0)
{
return F0 + (1.0 - F0) * pow(1.0 - cosTheta,5.0);
}
★2.4.3 G(Geometry function)
几何函数则是拿来模拟微平面间的相互遮挡导致光线能量减少的现象。
在函数里,我们使用了粗糙度作为参数,这也意味着物体表面越粗糙,微平面产生自阴影的概率越高,该几何函数我们选用了由GGX和Schlick-Beckmann组合而成的模拟函数Schlick-GGX :
其中参数k是使用粗糙度a计算得来的,我们根据不同的光照给出了不同的k值,对于直接光照,k的取值如下:
对于IBL光照(Image Based Lighting),即环境光照,k的取值如下:
为了有效地模拟几何体,我们需要同时考虑两个视角,视线方向(几何遮挡)跟光线方向(几何阴影),我们可以用Smith函数将两部分放到一起:
其中v表示视线向量,Gsub(n,v,k) 表示视线方向的几何遮挡;l 表示光线向量,Gsub(n,l,k) 表示光线方向的几何阴影。
在Unity里,该几何函数的代码如下:
float GeometrySchlickGGX(float NdotV, float k)
{
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(vec3 N, fixed3 V, fixed3 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;
}
★2.4.4 Cook-Torrance反射方程(Cook-Torrance reflectance equation)
最后,我们把上述讲到的漫反射项和高光反射项(包括重要的DFG)合并起来,可以得到最终的反射方程:
又因为在菲涅尔项F代表了光的比率,所以该式子是隐含了因子Ks,所以我们把它去掉,最终的式子即为:
该方程完整的定义了一个基于物理的渲染模型,也就是我们说的PBR。
引用: