从零开始理解PBR( Physically Based Rendering)

**标题取大了!!!**这其实是一篇揉杂大佬们的总结在一起的笔记整理,主要是从数学和物理上去理解PBR基础理论。

PBR一开始是在电影工业里应用的离线渲染技术集合,是现代3D引擎实时渲染的大热方向,突破了以前实时渲染的塑料感。
相比Lambert、Phong、 Blinn-Phong这些经验光照模型,PBR是指使用基于物理原理和微平面理论建模的着色/光照模型,以及使用从现实中测量的表面参数来准确表示真实世界材质的渲染理念。
在这里插入图片描述

一、前置知识

1、球面坐标 SphericalCoordinate

由于光线主要是通过方向来表达,通常用球面坐标表达它们比用笛卡尔坐标系更方便。
如图,球面坐标中的向量用三个元素来指定:
在这里插入图片描述

r表示向量的长度
θ表示向量和Z轴的夹角
Φ表示向量在x-y平面上的投影和x轴的逆时针夹角。

2、 立体角 Solid Angle

立体角描述了从原点向一个球面区域张成的视野大小,可以看成是弧度的三维扩展。它可以为我们描述投射到单位球体上的一个截面的大小或者面积。投射到这个单位球体上的截面的面积就被称为立体角(Solid Angle),你可以把立体角想象成为一个带有体积的方向:
在这里插入图片描述
可以把自己想象成为一个站在单位球面的中心的观察者,向着投影的方向看。这个投影轮廓的大小就是立体角。立体角用ω表示。
在这里插入图片描述
我们知道弧度是度量二维角度的量,等于角度在单位圆上对应的弧长,单位圆的周长是2π,所以整个圆对应的弧度也是2π 。立体角则是度量三维角度的量,用符号Ω表示,单位为立体弧度(也叫球面度,Steradian,简写为sr),等于立体角在单位球上对应的区域的面积(实际上也就是在任意半径的球上的面积除以半径的平方ω= s/r2 ),单位球的表面积是4π ,所以整个球面的立体角也是4π 。
在这里插入图片描述
立体角ω有如下微分形式:
在这里插入图片描述
其中dA为面积微元。而面积微元dA在球面坐标系下可以写成:
在这里插入图片描述
因此:
在这里插入图片描述
3、投影面积 Foreshortened Area

投影面积描述了一个物体表面的微小区域在某个视线方向上的可见面积。
对于面积微元A,则沿着与法向夹角为θ方向的A的可见面积为:
在这里插入图片描述
在这里插入图片描述
4、辐射度量学基本参数表格

我们必须要稍微涉足一些辐射度量学(Radiometry)的内容。辐射度量学是一种用来度量电磁场辐射(包括可见光)的手段。有很多种辐射度量(radiometric quantities)可以用来测量曲面或者某个方向上的光,但是我们将只会讨论其中和反射率方程有关的一种。它被称为辐射率(Radiance),在这里用L来表示。辐射率被用来量化单一方向上发射来的光线的大小或者强度。由于辐射率是由许多物理变量集合而成的,一开始理解起来可能有些困难,因此我们首先关注一下这些物理量:
在这里插入图片描述
5、辐射通量/光通量 Radiant Flux

辐射通量(Radiant Flux,又译作光通量,辐射功率)描述的是在单位时间穿过截面的光能,或每单位时间的辐射能量,通常用Φ来表示,单位是W,瓦特。
在这里插入图片描述
其中的Q表示辐射能(Radiant energy),单位是J,焦耳。
光是由多种不同波长的能量所集合而成的,而每种波长则与一种特定的(可见的)颜色相关。因此一个光源所放射出来的能量可以被视作这个光源包含的所有各种波长的一个函数。波长介于390nm到700nm(纳米)的光被认为是处于可见光光谱中,也就是说它们是人眼可见的波长。在下面你可以看到一幅图片,里面展示了日光中不同波长的光所具有的能量:
在这里插入图片描述
辐射通量将会计算这个由不同波长构成的函数的总面积。直接将这种对不同波长的计量作为参数输入计算机图形有一些不切实际,因此我们通常不直接使用波长的强度而是使用三原色编码,也就是RGB(或者按通常的称呼:光色)来作为辐射通量表示的简化。这套编码确实会带来一些信息上的损失,但是这对于视觉效果上的影响基本可以忽略。

6、辐射强度/发光强度 Radiant Intensity

辐射强度(Radiant Intensity ,又译作发光强度)表示的是在单位球面上,一个光源向每单位立体角所投送的辐射通量。举例来说,假设一个全向光源向所有方向均匀的辐射能量,辐射强度就能帮我们计算出它在一个单位面积(立体角)内的能量大小:
在这里插入图片描述
对一个点(比如说点光源)来说,辐射强度表示每单位立体角的辐射通量,用符号I表示,单位 ,瓦特每球面度:
在这里插入图片描述
其中I表示辐射通量Φ除以立体角ω。

7、辐射率/光亮度 Radiance

辐射率(Radiance,又译作光亮度,用符号L表示),表示物体表面沿某一方向的明亮程度,它等于每单位投影面积和单位立体角上的辐射通量,单位是 ,瓦特每球面度每平方米。在光学中,光源的辐射率,是描述非点光源时光源单位面积强度的物理量,定义为在指定方向上的单位立体角和垂直此方向的单位面积上的辐射通量。光亮度L也可以理解为发光程度I在表面dA上的积分。
一种直观的辐射率的理解方法是:将辐射率理解为物体表面的微面元所接收的来自于某方向光源的单位面积的光通量,因此截面选用垂直于该方向的截面,其面积按阴影面积技术计算。
辐射率的微分形式:
在这里插入图片描述
其中:Φ是辐射通量,单位瓦特(W);Ω是立体角,单位球面度(sr)。
另外需要注意的是,辐射率使用物体表面沿目标方向上的投影面积,而不是面积。
在这里插入图片描述
辐射率是辐射度量学上表示一个区域平面上光线总量的物理量,它受到入射(Incident)(或者来射)光线与平面法线间的夹角θ的余弦值cos⁡θ的影响:当直接辐射到平面上的程度越低时,光线就越弱,而当光线完全垂直于平面时强度最高。

8、如果我们把立体角ω和面积A看作是无穷小的,那么我们就能用辐射率来表示单束光线穿过空间中的一个点的通量

辐射率方程很有用,因为它把大部分我们感兴趣的物理量都包含了进去。如果我们把立体角ω和面积A看作是无穷小的,那么我们就能用辐射率来表示单束光线穿过空间中的一个点的通量。这就使我们可以计算得出作用于单个(片段)点上的单束光线的辐射率,我们实际上把立体角ω转变为方向向量ω然后把面A转换为点p。这样我们就能直接在我们的着色器中使用辐射率来计算单束光线对每个片段的作用了。

9、辐照度/辉度 Irradiance

当涉及到辐射率时,我们通常关心的是所有投射到点p上的光线的总和,而这个和就称为辐照度(Irradiance,又译作辉度,辐射照度,用符号E表示),指入射表面的辐射通量,即单位时间内到达单位面积的辐射通量,或到达单位面积的辐射通量,也就是辐射通量对于面积的密度。
用符号E表示,单位 ,瓦特每平方米。
我们需要计算的就不只是是单一的一个方向上的入射光,而是一个以点p为球心的半球领域Ω内所有方向上的入射光。一个半球领域(Hemisphere)可以描述为以平面法线n为轴所环绕的半个球体:
在这里插入图片描述
为了计算某些面积的值,或者像是在半球领域的问题中计算某一个体积的时候我们会需要用到一种称为积分(Integral)的数学手段∫,它的运算包含了半球领域Ω内所有入射方向上的dωi 。积分运算的值等于一个函数曲线的面积,它的计算结果要么是解析解要么就是数值解。由于渲染方程和反射率方程都没有解析解,我们将会用离散的方法来求得这个积分的数值解。这个问题就转化为,在半球领域Ω中按一定的步长将反射率方程分散求解,然后再按照步长大小将所得到的结果平均化。这种方法被称为黎曼和(Riemann sum) ,我们可以用下面的代码粗略的演示一下:

int steps = 100;
float sum = 0.0f;
vec3 P = ...;
vec3 Wo = ...;
vec3 N = ...;
float dW = 1.0f / steps;
for(int i = 0; i < steps; ++i)
{
	vec3 Wi = getNextIncomingLightDir(i);
	sum += Fr(p, Wi, Wo) * L(p, Wi) * dot(N, Wi) * dW;
}

通过利用dW来对所有离散部分进行缩放,其和最后就等于积分函数的总面积或者总体积。这个用来对每个离散步长进行缩放的dW可以认为就是反射率方程中的dωi 。在数学上,用来计算积分的dωi 表示的是一个连续的符号,而我们使用的dW在代码中和它并没有直接的联系(因为它代表的是黎曼和中的离散步长),这样说是为了可以帮助你理解。请牢记,使用离散步长得到的是函数总面积的一个近似值。细心的读者可能已经注意到了,我们可以通过增加离散部分的数量来提高黎曼和的准确度(Accuracy)。
辐照度可以写成辐射率(Radiance)在入射光所形成的半球上的积分:
在这里插入图片描述
其中,Ω是入射光所形成的半球。L(ω)是沿ω方向的光亮度。

二、 PBR的范畴(Scope of PBR)

寒霜(Frostbite)引擎在SIGGRAPH 2014的分享《Moving Frostbite to PBR》中提出,基于物理的渲染的范畴,由三部分组成:
基于物理的材质(Material)
基于物理的光照(Lighting)
基于物理适配的摄像机(Camera)

在这里插入图片描述
完整的这三者,才是真正完整的基于物理的渲染系统。而很多同学一提到PBR,就说PBR就是镜面反射采用微平面Cook-Torrance模型,其实是不太严谨。

三、PBR基础理论

在这里插入图片描述
微平面理论(Microfacet Theory)
微平面理论是将物体表面建模成做无数微观尺度上有随机朝向的理想镜面反射的小平面(microfacet)的理论。在实际的PBR 工作流中,这种物体表面的不规则性用粗糙度贴图或者高光度贴图来表示。
能量守恒(Energy Conservation)
射光线的能量永远不能超过入射光线的能量。随着粗糙度的上升镜面反射区域的面积会增加,作为平衡,镜面反射区域的平均亮度则会下降。
菲涅尔反射(Fresnel Reflectance)
光线以不同角度入射会有不同的反射率。相同的入射角度,不同的物质也会有不同的反射率。万物皆有菲涅尔反射。F0是即 0 度角入射的菲涅尔反射值。大多数非金属的F0范围是0.02-0.04,大多数金属的F0范围是0.7-1.0。
线性空间(Linear Space)
光照计算必须在线性空间完成,shader 中输入的gamma空间的贴图比如漫反射贴图需要被转成线性空间,在具体操作时需要根据不同引擎和渲染器的不同做不同的操作。而描述物体表面属性的贴图如粗糙度,高光贴图,金属贴图等必须保证是线性空间。
色调映射(Tone Mapping)
也称色调复制(tone reproduction),是将宽范围的照明级别拟合到屏幕有限色域内的过程。因为基于HDR渲染出来的亮度值会超过显示器能够显示最大亮度,所以需要使用色调映射,将光照结果从HDR转换为显示器能够正常显示的LDR。
物质的光学特性(Substance Optical Properties)
现实世界中有不同类型的物质可分为三大类:绝缘体(Insulators),半导体(semi-conductors)和导体(conductors)。在渲染和游戏领域,我们一般只对其中的两个感兴趣:导体(金属)和绝缘体(电解质,非金属)。其中非金属具有单色/灰色镜面反射颜色。而金属具有彩色的镜面反射颜色。即非金属的F0是一个float。而金属的F0是一个float3,如下图。
在这里插入图片描述
1、除了PBR的基础理论,光与非光学平坦表面的交互对理解微平面理论(Microfacet Theory)至关重要。

所有的PBR技术都基于微平面理论。这项理论认为,达到微观尺度之后任何平面都可以用被称为微平面(Microfacets)的细小镜面来进行描绘。根据平面粗糙程度的不同,这些细小镜面的取向排列可以相当不一致:
在这里插入图片描述
光在与非光学平坦表面(Non-Optically-Flat Surfaces)的交互时,非光学平坦表面表现得像一个微小的光学平面表面的大集合。表面上的每个点都会以略微不同的方向对入射光反射,而最终的表面外观是许多具有不同表面取向的点的聚合结果。
在这里插入图片描述
产生的效果就是:一个平面越是粗糙,这个平面上的微平面的排列就越混乱。这些微小镜面这样无序取向排列的影响就是,当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散(Scatter)开来,进而产生出分布范围更广泛的镜面反射。而与之相反的是,对于一个光滑的平面,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射:
在这里插入图片描述
在微观尺度下,没有任何平面是完全光滑的。然而由于这些微平面已经微小到无法逐像素的继续对其进行区分,因此我们只有假设一个粗糙度(Roughness)参数,然后用统计学的方法来概略的估算微平面的粗糙程度。我们可以基于一个平面的粗糙度来计算出某个向量的方向与微平面平均取向方向一致的概率。这个向量便是位于光线向量l和视线向量v之间的中间向量(Halfway Vector)。
在这里插入图片描述
微平面的取向方向与中间向量的方向越是一致,镜面反射的效果就越是强烈越是锐利。然后再加上一个介于0到1之间的粗糙度参数,这样我们就能概略的估算微平面的取向情况了:
在这里插入图片描述
我们可以看到,较高的粗糙度值显示出来的镜面反射的轮廓要更大一些。与之相反地,较小的粗糙值显示出的镜面反射轮廓则更小更锐利。

2、微平面近似法使用了这样一种形式的能量守恒(Energy Conservation):出射光线的能量永远不能超过入射光线的能量(发光面除外)。

出于着色的目的,我们通常会去用统计方法处理这种微观几何现象,并将表面视为在每个点处在多个方向上反射(和折射)光。为了遵守能量守恒定律,我们需要对漫反射光和镜面反射光之间做出明确的区分。当一束光线碰撞到一个表面的时候,它就会分离成一个折射部分和一个反射部分。反射部分就是会直接反射开来而不会进入平面的那部分光线,这就是我们所说的镜面光照。而折射部分就是余下的会进入表面并被吸收的那部分光线,这也就是我们所说的漫反射光照。
在这里插入图片描述
这里还有一些细节需要处理,因为当光线接触到一个表面的时候折射光是不会立即就被吸收的。通过物理学我们可以得知,光线实际上可以被认为是一束没有耗尽就不停向前运动的能量,而光束是通过碰撞的方式来消耗能量。每一种材料都是由无数微小的粒子所组成,这些粒子都能如下图所示一样与光线发生碰撞。这些粒子在每次的碰撞中都可以吸收光线所携带的一部分或者是全部的能量而后转变成为热量。
一般来说,并非所有能量都会被全部吸收,而光线也会继续沿着(基本上)随机的方向发散,然后再和其他的粒子碰撞直至能量完全耗尽或者再次离开这个表面。而光线脱离物体表面后将会协同构成该表面的(漫反射)颜色。不过我们常用的反射方程(The Reflectance Equation)中我们进行了简化,假设对平面上的每一点所有的折射光都会被完全吸收而不会散开。而有一些被称为次表面散射(Subsurface Scattering)技术的着色器技术将这个问题考虑了进去,它们显著的提升了一些诸如皮肤,大理石或者蜡质这样材质的视觉效果,不过伴随而来的则是性能下降代价。
漫反射和次表面散射其实是相同物理现象,本质都是折射光的次表面散射的结果。唯一的区别是相对于观察尺度的散射距离。散射距离相较于像素来说微不足道,次表面散射便可以近似为漫反射。也就是说,光的折射现象,建模为漫反射还是次表面散射,取决于观察的尺度,如下图。
在这里插入图片描述
图在左上角,像素(带有红色边框的绿色圆形)大于光线离开表面之前所经过的距离。 在这种情况下,可以假设出射光从入口点(右上)射出,可以当做漫反射,用局部着色模型处理。 在底部,像素小于散射距离; 如果需要更真实的着色效果,则不能忽略这些距离的存在,需当做次表面散射现象进行处理。
对于金属(Metallic)表面,当讨论到反射与折射的时候还有一个细节需要注意。金属表面对光的反应与非金属材料还有电介质(Dielectrics)材料表面相比是不同的。它们遵从的反射与折射原理是相同的,但是所有的折射光都会被直接吸收而不会散开,只留下反射光或者说镜面反射光。亦即是说,金属表面不会显示出漫反射颜色。由于金属与电介质之间存在这样明显的区别,因此它们两者在PBR渲染管线中被区别处理。
对于金属,折射光会立刻被吸收 - 能量被自由电子立即吸收。
在这里插入图片描述
对于非金属(也称为电介质或绝缘体),一旦光在其内部折射,就表现为常规的参与介质,表现出吸收和散射两种行为。
在这里插入图片描述
反射光与折射光之间的这个区别使我们得到了另一条关于能量守恒的经验结论:反射光与折射光它们二者之间是互斥的关系。无论何种光线,其被材质表面所反射的能量将无法再被材质吸收。因此,诸如折射光这样的余下的进入表面之中的能量正好就是我们计算完反射之后余下的能量。
我们按照能量守恒的关系,首先计算镜面反射部分,它的值等于入射光线被反射的能量所占的百分比。然后折射光部分就可以直接由镜面反射部分计算得出:

float kS = calculateSpecularComponent(...); // 反射/镜面部分
float kD = 1.0 - ks; // 折射/漫反射部分

这样我们就能在遵守能量守恒定律的前提下知道入射光线的反射部分与折射部分所占的总量了。按照这种方法折射/漫反射与反射/镜面反射所占的份额都不会超过1.0,如此就能保证它们的能量总和永远不会超过入射光线的能量。

3、渲染方程(The Rendering Equation)
在这里插入图片描述
渲染方程作为渲染领域中的重要理论,其描述了光能在场景中的流动,是渲染中不可感知方面的最抽象的正式表示。根据光学的物理学原理,渲染方程在理论上给出了一个完美的结果,而各种各样的渲染技术,只是这个理想结果的一个近似。
渲染方程的物理基础是能量守恒定律。在一个特定的位置和方向,出射光 Lo 是自发光 Le 与反射光线之和,反射光线本身是各个方向的入射光 Li 之和乘以表面反射率及入射角。
这个方程经过交叉点将出射光线与入射光线联系在一起,它代表了场景中全部的’光线传输。所有更加完善的算法都可以看作是这个方程的特殊形式的解。
某一点p的渲染方程,可以表示为:
在这里插入图片描述
在这里插入图片描述
而在实时渲染中,我们常用的反射方程(The Reflectance Equation),则是渲染方程的简化的版本,或者说是一个特例:
在这里插入图片描述
在这里插入图片描述

4、应用基于物理的BRDF。

在渲染方程中我们只有Fr暂未介绍。接下来说说什么是BRDF。
我们可以将给一个表面着色的过程,理解为给定入射的光线数量和方向,计算出指定方向的出射光亮度(radiance)。在计算机图形学领域,BRDF (Bidirectional Reflectance Distribution Function,译作双向反射分布函数 )是一个用来描述表面如何反射光线的方程。顾名思义,BRDF就是一个描述光如何从给定的两个方向(入射光方向l和出射方向v)在表面进行反射的函数。
BRDF的精确定义是出射辐射率的微分(differential outgoing radiance)和入射辐照度的微分(differential incoming irradiance)之比:
在这里插入图片描述
要理解这个方程的含义,可以想象一个表面被一个来自围绕着角度l的微立体角的入射光照亮,而这个光照效果由表面的辉度dE来决定。
表面会反射此入射光到很多不同的方向,在给定的任意出射方向v,光亮度dLo与辐照度dE成一个比例。而两者之间的这个取决于l和v的比例,就是BRDF。
一个最常见的疑问是,BRDF为什么要取这样的定义。BRDF为什么被定义为辐射率(radiance)和辐照度(irradiance)之比,而不是radiance和radiance之比,或者irradiance和irradiance之比呢?
首先,我们分别重温它们的定义:
辐照度(Irradiance,又译作辉度,辐射照度),表示单位时间内到达单位面积的辐射通量,也就是辐射通量对于面积的密度,通常用符号E表示,单位 瓦特每平方米。
辐射率(Radiance,又译作光亮度),表示每单位立体角每单位投影面积的辐射通量,通常用符号L表示,单位是,瓦特每球面度每平方米。
那么,关于这个问题,我们可以这样理解:
因为照射到入射点的不同方向的光,都可能从指定的反射方向出射,所以当考虑入射时,需要对面积进行积分。而辐照度irradiance正好表示单位时间内到达单位面积的辐射通量。所以BRDF函数,选取入射时的辐照度Irradiance,和出射时的辐射率Radiance,可以简单明了地描述出入射光线经过某个表面反射后如何在各个出射方向上分布。而直观来说,BRDF的值给定了入射方向和出射方向能量的相对量。

5、BRDF的非微分形式

这里的讨论仅限于非区域光源,如点光源或方向光源。在这种情况下,BRDF定义可以用非微分形式表示:
在这里插入图片描述
其中:
EL是光源在垂直于光的方向向量L平面测量的辐照度(irradiance)。
Lo(v)是在视图矢量v的方向上产生的出射辐射率(radiance)。

要理解这个方程的含义,可以想象一个表面被一个来自围绕着角度l的微立体角的入射光照亮,而这个光照效果由表面的辉度dE来决定。
表面会反射此入射光到很多不同的方向,在给定的任意出射方向v,光亮度dLo与辐照度dE成一个比例。而两者之间的这个取决于l和v的比例,就是BRDF。
在这里插入图片描述
6、 BRDF与着色方程

根据上文所了解了BRDF的定义,现在,就很容易得到BRDF是如何用n个非区域光来拟合一般的着色方程的:
在这里插入图片描述
其中k是每个光源的索引。使用⊗符号(分段向量乘法),是因为BRDF和辉度(irradiance)都是RGB向量。考虑到入射和出射方向都拥有两个自由度(通常参数化是使用两个角度:相对于表面法线的仰角θ和关于法线的旋转角度φ),一般情况下,BRDF是拥有四个标量变量的函数。
另外,各向同性BRDFs(Isotropic BRDFs)是一个重要的特殊情况。这样的BRDF在输入和输入方向围绕表面法线变化(保持相同的相对夹角)时保持不变。所以,各向同性BRDF是关于三个标量的函数。
BRDF的可逆性源自于亥姆霍兹光路可逆性(Helmholtz Recoprpcity Rule)。
BRDF的可逆性即,交换入射光与反射光,并不会改变BRDF的值:
在这里插入图片描述
BRDF需要遵循能量守恒定律( 严格上来说,同样采用ωi和ωo作为输入参数的 Blinn-Phong光照模型也被认为是一个BRDF。然而由于Blinn-Phong模型并没有遵循能量守恒定律,因此它不被认为是基于物理的渲染)。能量守恒定律指出:入射光的能量与出射光能量总能量应该相等。能量守恒方程如下:
在这里插入图片描述
由此可知:
在这里插入图片描述
因此BRDF必须满足如下的积分不等式,也就是能量守恒性质:
在这里插入图片描述
7、主流BRDF模型–Microfacet Cook-Torrance BRDF
游戏业界目前最主流的基于物理的镜面反射BRDF模型是基于微平面理论(microfacet theory)的Microfacet Cook-Torrance BRDF。
Cook-Torrance BRDF兼有漫反射和镜面反射两个部分:
在这里插入图片描述
这里的kd是早先提到过的入射光线中被折射部分的能量所占的比率,而ks是被反射部分的比率。BRDF的左侧表示的是漫反射部分,这里用flambert来表示。它被称为Lambertian漫反射,这和我们之前在漫反射着色中使用的常数因子类似,用如下的公式来表示:
在这里插入图片描述
c表示表面颜色(漫反射光线所占的比例,回想一下漫反射表面纹理)。 除以π是为了对漫反射光进行标准化,因为前面含有BRDF的积分方程是受π影响的,因为我们假设漫反射在所有方向上的强度都是相同的,而BRDF要求在半球内的积分值为1。可以用下面的公式理解。
在这里插入图片描述
BRDF的镜面反射部分要稍微更高级一些,它的形式如下所示:
在这里插入图片描述
Cook-Torrance BRDF的镜面反射部分包含三个函数,此外分母部分还有一个标准化因子 。字母D,F与G分别代表着一种类型的函数,各个函数分别用来近似的计算出表面反射特性的一个特定部分。三个函数分别为法线分布函数(Normal Distribution Function),菲涅尔方程(Fresnel Rquation)和几何函数(Geometry Function)
法线分布函数:估算在受到表面粗糙度的影响下,取向方向与中间向量一致的微平面的数量。这是用来估算微平面的主要函数。
几何函数:描述了微平面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。
菲涅尔方程:菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率。
以上的每一种函数都是用来估算相应的物理参数的,而且你会发现用来实现相应物理机制的每种函数都有不止一种形式。它们有的非常真实,有的则性能高效。你可以按照自己的需求任意选择自己想要的函数的实现方法。
BRDF的镜面反射大多数(仍然有些材质是无法用微平面理论来描述的)是建立在微平面理论的假设上的,当光和这些微平面相交时,光线会被分割成两个方向–反射方向和折射方向,这里我们只考虑反射方向,因为折射方向我们在漫反射中考虑过了。
假设表面法线为n,这些微平面的法线为m(并不都等于n)。因此不同的微平面会把同一入射方向的光线反射到不同的方向上。而我们在计算BRDF时,入射方向I和观察方向v都会被给定,这意味着只有一部分微平面反射的光线才会进入到我们的眼睛中,这部分微平面恰好把光线反射到v上,即它们的法线m等于I和v的一半,也就是我们一直看到的半角度矢量h(half-angle vector,也成 half vector)。
在这里插入图片描述
那些m=h的微平面恰好把入射光从I反射到v上,只有这部分的微平面才可以添加到BRDF计算中。这种物理现象我们可以用法线分布函数来描述。
法线分布函数D,或者说镜面分布,从统计学上近似的表示了与某些(中间)向量h取向一致的微平面的比率。举例来说,假设给定向量h,如果我们的微平面中有35%与向量h取向一致,则正态分布函数或者说NDF将会返回0.35。目前有很多种NDF都可以从统计学上来估算微平面的总体取向度,只要给定一些粗糙度的参数以及Trowbridge-Reitz GGX模型( 业界较为主流的法线分布函数,因为具有更好的高光长尾):
在这里插入图片描述在这里插入图片描述
在这里h表示用来与平面上微平面做比较用的中间向量,而a表示表面粗糙度。
如果我们把h当成是不同粗糙度参数下,平面法向量和光线方向向量之间的中间向量的话,我们可以得到如下图示的效果:
在这里插入图片描述
当粗糙度很低(也就是说表面很光滑)的时候,与中间向量取向一致的微平面会高度集中在一个很小的半径范围内。由于这种集中性,NDF最终会生成一个非常明亮的斑点。但是当表面比较粗糙的时候,微平面的取向方向会更加的随机。你将会发现与h向量取向一致的微平面分布在一个大得多的半径范围内,但是同时较低的集中性也会让我们的最终效果显得更加灰暗。
使用GLSL代码编写的Trowbridge-Reitz GGX正态分布函数是下面这个样子的:

float D_GGX_TR(vec3 N, vec3 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;
}

回到上面说的只有m=h的微平面才可以进行BRDF计算中,然而,并不是所有这些微平面算入计算中,因为它们其中一部分会在入射方向I上被其他微平面挡住,他们不会接受到光照从而形成阴影(shadowing)。我们用菲涅尔方程来描述这一现象。
在这里插入图片描述
菲涅尔反射(Fresnel Reflectance)或者菲涅尔效果(Fresnel Effect),即当光入射到折射率不同的两个材质的分界面时,一部分光会被反射,而我们所看到的光线会根据我们的观察角度以不同强度反射的现象。
菲涅尔反射能够真实地模拟真实世界中的反射。在真实世界中,除了金属之外,其它物质均有不同程度的菲涅尔反射效果。
关于菲涅尔反射,一个很好的例子是一池清水。从水池上笔直看下去(也就是与法线成零度角的方向)的话,我们能够一直看到池底。而如果从接近平行于水面的方向看去的话,水池表面的高光反射会变得非常强以至于你看不到池底。
在这里插入图片描述
根据菲涅尔反射,若你看向一个圆球,那么圆球中心的反射会较弱,而靠近边缘是反射会较强。另外需注意,这种关系也受折射率影响。
在这里插入图片描述
对于菲涅尔(Fresnel)项,业界方案一般都采用Schlick的Fresnel近似,因为计算成本低廉,而且精度足够:
在这里插入图片描述
F0表示平面的基础反射率,它是利用所谓折射指数(Indices of Refraction)或者说IOR计算得出的。
菲涅尔方程还存在一些细微的问题。其中一个问题是Fresnel-Schlick近似仅仅对电介质或者说非金属表面有定义。对于导体(Conductor)表面(金属),使用它们的折射指数计算基础折射率并不能得出正确的结果,这样我们就需要使用一种不同的菲涅尔方程来对导体表面进行计算。由于这样很不方便,所以我们预先计算出平面对于法向入射(F0)的反应(处于0度角,好像直接看向表面一样)然后基于相应观察角的Fresnel-Schlick近似对这个值进行插值,用这种方法来进行进一步的估算。这样我们就能对金属和非金属材质使用同一个公式了
在这里插入图片描述
通过预先计算电介质与导体的F0值,我们可以对两种类型的表面使用相同的Fresnel-Schlick近似,但是如果是金属表面的话就需要对基础反射率添加色彩。我们一般是按下面这个样子来实现的:

	vec3 F0 = vec3(0.04);
	F0 = mix(F0, surfaceColor.rgb, metalness);

我们为大多数电介质表面定义了一个近似的基础反射率。F0取最常见的电解质表面的平均值,这又是一个近似值。不过对于大多数电介质表面而言使用0.04作为基础反射率已经足够好了,而且可以在不需要输入额外表面参数的情况下得到物理可信的结果。然后,基于金属表面特性,我们要么使用电介质的基础反射率要么就使用F0来作为表面颜色。因为金属表面会吸收所有折射光线而没有漫反射,所以我们可以直接使用表面颜色纹理来作为它们的基础反射率。
Fresnel Schlick近似可以用代码表示为:

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
	return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

其中cosTheta是表面法向量n与观察方向v的点乘的结果。

还有一部分m=h的微平面在它们的反射方向v上被其他微平面挡住了,因此这部分反射光线也不会被看到(masking)。几何函数可以描述这一现象。
在这里插入图片描述
目前较为常用的是其中最为简单的形式,分离遮蔽阴影(Separable Masking and Shadowing Function)。
该形式将几何项G分为两个独立的部分:光线方向(light)和视线方向(view ),并对两者用相同的分布函数来描述。根据这种思想,结合法线分布函数(NDF)与Smith几何阴影函数,于是有了以下新的Smith几何项:

Smith-GGX
Smith-Beckmann
Smith-Schlick
Schlick-Beckmann
Schlick-GGX

Schlick-GGX( 使用的几何函数是GGX与Schlick-Beckmann近似的结合体)
在这里插入图片描述
这里的k是α基于几何函数是针对直接光照还是针对IBL光照的重映射(Remapping) :
在这里插入图片描述
注意,根据你的引擎把粗糙度转化为α的方式不同,得到α的值也有可能不同。
为了有效的估算几何部分,需要将观察方向(几何遮蔽(Geometry Obstruction))和光线方向向量(几何阴影(Geometry Shadowing))都考虑进去。我们可以使用史密斯法(Smith’s method)来把两者都纳入其中:
在这里插入图片描述
使用Smith’s method与Schlick-GGX作为Gsub可以得到如下所示不同粗糙度的视觉效果:
在这里插入图片描述
几何函数是一个值域为[0.0, 1.0]的乘数,其中白色或者说1.0表示没有微平面阴影,而黑色或者说0.0则表示微平面彻底被遮蔽。
使用GLSL编写的几何函数代码如下:

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

	return nom / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 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;
}

还有一种多次表面反射的,其中一些最终还是可见的,但是这在目前的微平面理论中一般不考虑的。
在这里插入图片描述

四、Cook-Torrance反射率方程

随着Cook-Torrance BRDF中所有元素都介绍完毕,我们现在可以将基于物理的BRDF纳入到最终的反射率方程当中去了:
在这里插入图片描述
这个方程现在完整的描述了一个基于物理的渲染模型,它现在可以认为就是我们一般意义上理解的基于物理的渲染,也就是PBR。

五、上面说了一堆巴拉巴拉的,总结下

光强度是单位立体角的辐射能量
辐射度是单位面积流出的光通量
辐照度是单位面积流入的光通量

它们的关系:
光强度=光亮度单位面积入射角余弦
辐照度=光亮度入射角余弦单位立体角
辐射度=光亮度入射角余弦单位立体角(但方向不同)

BRDF规定的是辐照度和光亮度的关系:
光亮度=BRDF*辐照度

但用到编程中,乱七八糟的概念就靠边站了,只需要知道:
某点显示在屏幕上的颜色=光源颜色材质颜色反射系数*你规定的函数
“你规定的函数”指的是和法线、光源方向、视点方向等与空间有关量的函数

BRDF主要有两类:镜面反射器和漫反射器

BRDF用非光线追踪算法就能实现(逐像素计算而不是逐顶点计算,具体到编程就是在片元着色器中进行向量的规范化和亮度计算,而不是顶点着色器),但只能计算光从物体表面直接反射进“人眼”产生的亮度,不能计算光在物体间反射或在(半)透明物体中的折射对亮度的影响。

常用的真实图像渲染方法有:光线追踪方法和辐射度方法。
光线追踪方法适合处理物体间的镜面反射
辐射度方法适合处理物体间的漫反射

光线追踪方法模拟物理光在空间中的传播(考虑反射和折射),从投影平面的一个像素点发出一条或多条光线进入场景,遇到物体表面就提取表面的材质信息,沿反射光线继续前进(若是透明物体,再分出一条折射光线),这样递归下去直到光线到达光源、场景之外(环境)或光亮度减小到规定的临界值,将过程中所有颜色信息相加就是显示在这一点的颜色。

六、fragment shader 代码

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

// material parameters
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

uniform vec3 camPos;

const float PI = 3.14159265359;
// ----------------------------------------------------------------------------
float DistributionGGX(vec3 N, vec3 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);
    denom = PI * denom * denom;

    return nom / max(denom, 0.001); // prevent divide by zero for roughness=0.0 and NdotH=1.0
}
// ----------------------------------------------------------------------------
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(vec3 N, vec3 V, vec3 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;
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// ----------------------------------------------------------------------------
void main()
{        
    vec3 N = normalize(Normal);
    vec3 V = normalize(camPos - WorldPos);

    // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0
    // of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    // reflectance equation
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i)
    {
        // calculate per-light radiance
        vec3 L = normalize(lightPositions[i] - WorldPos);
        vec3 H = normalize(V + L);
        float distance = length(lightPositions[i] - WorldPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lightColors[i] * attenuation;

        // Cook-Torrance BRDF
        float NDF = DistributionGGX(N, H, roughness);   
        float G   = GeometrySmith(N, V, L, roughness);      
        vec3 F    = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);
           
        vec3 nominator    = NDF * G * F;
        float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        vec3 specular = nominator / max(denominator, 0.001); // prevent divide by zero for NdotV=0.0 or NdotL=0.0
        
        // kS is equal to Fresnel
        vec3 kS = F;
        // for energy conservation, the diffuse and specular light can't
        // be above 1.0 (unless the surface emits light); to preserve this
        // relationship the diffuse component (kD) should equal 1.0 - kS.
        vec3 kD = vec3(1.0) - kS;
        // multiply kD by the inverse metalness such that only non-metals
        // have diffuse lighting, or a linear blend if partly metal (pure metals
        // have no diffuse light).
        kD *= 1.0 - metallic;      

        // scale light by NdotL
        float NdotL = max(dot(N, L), 0.0);        

        // add to outgoing radiance Lo
         // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply  by kS again                                                                                                                     
        Lo += (kD * albedo / PI + specular) * radiance * NdotL; 
    }  
    
    // ambient lighting (note that the next IBL tutorial will replace
    // this ambient lighting with environment lighting).
    vec3 ambient = vec3(0.03) * albedo * ao;

    vec3 color = ambient + Lo;

    // HDR tonemapping
    color = color / (color + vec3(1.0));
    // gamma correct
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

在这里插入图片描述

参考:
https://learnopengl-cn.github.io/07%20PBR/01%20Theory/
https://zhuanlan.zhihu.com/p/53086060
https://zhuanlan.zhihu.com/p/28059221
https://www.zhihu.com/question/20286038/answer/17348649
冯乐乐的《Unity shader 入门精要》
https://blog.csdn.net/Neil3D/article/details/83783638
https://zhuanlan.zhihu.com/p/91111385

发布了7 篇原创文章 · 获赞 2 · 访问量 1200

猜你喜欢

转载自blog.csdn.net/xyxsuoer/article/details/103049091