作者:光阳果 链接:https://zhuanlan.zhihu.com/p/272553650 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
约定
Lambert Lighting
Half-Lambert Lighting
Phong
Blin-Phong Lighting
Wrap Lighting
Gouraud Lighting
Banded Lighting
Minnaert Lighting
Oren-Nayar Lighting
BackLight or CheapSSS 背光|简易次表面散射
Anisotropic Lighting
PBR
PBR Anistropy基于物理的各项异性高光
ClearCoat清漆
Cloth布料
约定
L 光照方向
N 法线方向
V 视角方向
T 切线方向
H 半角向量
这些向量 为世界空间下的向量 且是 单位向量
Lambert Lighting
兰伯特反射(Lambert)是最常见的一种漫反射,它与视角无关,即从不同的方向观察并不会改变渲染结果。

Half-Lambert Lighting
Half-Lamber是Valve公司提出来的算法,其为了解决Lambert公式在灰面太暗的问题。这是一个经验公式,没啥依据可言。

比原Lambert亮了许多
既然是经验公式,那么我也瞎写一个

同样更亮了,
既然我能瞎写,也有大佬在我之前就瞎写了,且起了个名字,叫DiffuseWrap
_WrapValue设置成0.8

如果不小心将_WrapValue调成了1.4你还能看到光"透射"的效果,没错近似的次表面散射就是用这种方法做的,只不过要把视角计算进去!

同样地可以开脑洞,把代码改成这种调调效果

一顿狂调

Phong
Phong模型用来模拟高光效果,它认为 从物体表面反射出的光线中包括 粗糙表面的漫反射与光滑表面高光 两部分。
Diffuse

Specular


注意Pong公式 pow(VR,_SpecularPowerValue)*_SpecularScaleValue 使用了一个pow函数,其实我刚开始看到的时候也纳闷为什么要用pow函数,后来想通了,其作用就是控制函数曲线调节数值,调节PowerValue 与 PowerScale可以调节曲线数值,和波函数 y=A(Wx+B)+C 中的 AWBC一个作用。
推荐一个数学公式画图网站: https://www.desmos.com/calculator
红色是pow函数,蓝色是sin函数
Blin-Phong Lighting
Jim Blinn在Phong的基础上进行了改进,引入了半角向量,即视角与光线中间的方向。其得到的高光结果比Phong更"光滑"。
对比一样,相同参数的效果




Wrap Lighting
正常情况下,当表面的法线对于光源方向垂直的时候,Lambert漫反射提供的照明度是0。而环绕光照修改漫反射函数,使得光照环绕在物体的周围,越过那些正常时会变黑变暗的点。


Gouraud Lighting
简单来说Gouraud就是在VertexShader而不是在FragmentShader中计算光照的Phong模型。
在顶点中计算光照后,再在Fragment中插值。
Gouraud 高光

Phong 高光

在顶点中计算光照的可以节省一些性能,但是效果没那么好。
Gouraud 漫反射+高光

Phong 漫反射+高光
Banded Lighting
这个方法更像一个Trick而不是一个光照模型。基本原理就是对NdotL做条状化处理,卡通渲染效果的二分卡边原理与此类似。
整点颜色渐变
三个颜色渐变
Minnaert Lighting
Minnaert光照主要是用来模拟月球光照的,因为月球上没有空气,所以没有大气散射象限,只有光照到的部分是亮的,其他的部分都是暗的。可以看下图感受一下:
这是月球的白天,阿波罗号
在月球背面的嫦娥号
Oren-Nayar Lighting
此模型用来描述光在粗糙表面的反射情况,相比于Lambert模型,它考虑了 粗糙度参数,常用来模拟比较粗糙的表面。比如游戏风之旅人的沙漠就是在Oren-Nayar的模型上做的改进。(https://www.gdcvault.com/play/1017742/Sand-Rendering-in)
原Oren-Nayar模型公式,公式中光线方向与反射方向是用球坐标系表示的,
这是一个转化后便于写代码的版本:
注意arccos 是arc cos,即反余弦函数
参考:
https://en.wikipedia.org/wiki/Oren%E2%80%93Nayar_reflectance_model
https://lonalwah.wordpress.com/2013/11/29/oren-nayar-reflectance-diffuse-getting-rough/
狂调参数
加一张Roughness贴图
hh:D

BackLight
先看图片:


概念:
反射:入射光与反射光在表面的同一侧,且入射点与反射点相同

次表面散射:入射光与反射光在表面的同一侧,且入射点与反射点不同

透射:入射光与反射光在表面的不同侧,即光线投过了物体

时常有人把透射与次表面散射弄混,因为这两种现象看起来那么像,且真实光照总是包括这三种 "射"的情况。
为了实现这种背面透光的效果,我们可以假定,在主光的反方向还有一个 补光,用这个补光去做一些运算的骚操作即可模拟 背面透光的效果,比如说 沿补光 偏移 反法线 ,等价于,沿主光偏移法线 最后在取反。

(https://www.alanzucconi.com/2017/08/30/fast-subsurface-scattering-2/)
将N_Shift 当作N去进行光照模型的运算,比如带入Phong模型中。

将这个背光效果与前面介绍的Phong 和 Wrap 模型结合一下:

Anisotropic Lighting
各向异性,Anisotropy,通俗上讲就是在各个方向上所体现出来的性质都不一样。比如在光照下的头发。

可以明显看到一条 "高光带",而不是 "点状" 高光。为了模拟这种 带状高光,我们在光照方程中使用发丝切线T替代法线N,进行光照计算。为了使高光可以沿着头发移动,需要沿法线的方向偏移切线。为了使高光有点上下偏移使用了一个拉升的噪音贴图。


参考:
http://web.engr.oregonstate.edu/~mjb/cs519/Projects/Papers/HairRendering.pdfweb.engr.oregonstate.edu/~mjb/cs519/Projects/Papers/HairRendering.pdf
先计算高光:

用真实的物理公式去模拟这种各项异性是非常复杂的,所以都只是视觉上近似模拟。
产生带状的高光是因为使用了sin函数,而sin函数里的变量范围是 [0,1],所以只会产生一条高光带
可以看一下sin函数的效果:

既然sin函数可以,那么cos函数也一定行,因为它们只是相位差了一个 PI/2:
使用cos函数,效果一样,且比使用sin更省性能(因为要开方)

如果想要多条高光带,就像这位飘逸的姐姐

再加几个高光带,可以直接这么写:
在调这个效果的时候,_AnisotropicPowerValue _AnisotropicPowerScale 以及 _StretchedNoise的uv也要一起调,

Diffuse 用前面的Wraplight
高光带有断裂是因为我的_StretchedNoiseTex贴图不连续,整个连续的就行,
调个猛男色,:D

理论
PBR(Physically Based Shading)是基于与现实世界的物理原理所创建的一种渲染技术,相比于传统的基于经验的模型(Phong Blin-Phong等)更具有物理准确性。基于物理的光照模型必须满足一下三个条件:
1.基于微平面(Microfacet)的表面模型。
2.能量守恒。
3.基于物理的BRDF。
微表面模型
微观尺度上,物体表面有无数的为表面构成。用粗糙度去衡量一个表面的粗糙程度,越粗糙则说明微表面的法线方向越不一致,反射的光越分散,反之则越集中。

较高的粗糙度值显示出来的镜面反射的轮廓要更大一些。与之相反地,较小的粗糙值显示出的镜面反射轮廓则更小更锐利。
能量守恒
能量守恒定律是自然界普遍的基本定律之一。一般表述为:能量既不会凭空产生,也不会凭空消失,它只会从一种形式转化为另一种形式,或者从一个物体转移到其它物体,而能量的总量保持不变。
将能量守恒定律应用到光照模型中:出射光线的能量永远不能超过入射光线的能量。可以用渲染方程来描述:

在PBR中考虑简化的形式 即反射方程:
再看这张图:
从上图可以看出随着粗糙度的上升镜面反射区域的面积会增加,作为平衡,镜面反射区域的平均亮度则会下降。
按照能量守恒的关系,首先计算镜面反射部分,它的值等于入射光线被反射的能量所占的百分比。然后漫反射部分就可以直接由镜面反射部分计算得出:
而在实时渲染中,会有两种光对物体表面造成影响,分别是直接光和间接光,那么与两种反射组合,会有四中光照反射情况。
基于物理的BRDF
双向反射分布函数(Bidirectional Reflectance Distribution Function)描述的是入射光和反射光关系。在各种brdf模型中,Cook-Torrance效果更真实应用最广泛。
其中Ks是镜面反射所占百分比,Kd是漫反射所占百分比 Kd=1-Ks
将Cook-Torrance带入反射方程中可得:
其中F菲涅尔描述了光被反射的比例,代表了反射方程的ks,两者可以合并,所以最终的反射方程为:
直接光 漫反射+镜面反射
由于直接光通常是 平行光 或者 点光源 都是有限的数量,因此直接将它们的光照结果相加 即是最终的积分结构。我们目前只考虑一个平行光,因此直接光的计算结构 即使积分结果。而间接光从四面八方射到物体表面上来,数量是无限的,因此要用积分计算出所有间接光的计算结果。
直接光的漫反射比较简单:
间接光需要考虑DFG三项
D Distribution of normal function
法线分布函数描述的是微表面的法线方向与半角向量对齐的概率,如果对齐那么认为该反射光可以看到,否则没有。
D的Trowbridge-Reitz GGX公式:
仅有D项
F Frenel function
如果你站在湖边,低头看脚下的水,你会发现水是透明的,反射不是特别强烈;如果你看远处的湖面,你会发现水并不是透明的,但反射非常强烈。这就是“菲涅尔效应”。简单的讲,就是视线垂直于表面时,反射较弱,而当视线非垂直表面时,夹角越小,反射越明显。如果你看向一个圆球,那圆球中心的反射较弱,靠近边缘较强。
F的Schlick公式:
n代表介质的折射率,1为空气的折射率,以下为真实材质测量的F0数值表
非金属的F0数值较小,金属F0的数值较大,出于简化计算的原因,通过金属度在一个预设的F0和自身颜色之间经行插值。
仅有F项:
从背面观察:
G Geometry function
几何项体现了微表面的自我遮蔽现象,即入射光线或者反射光线会被自身凹凸不平的表面遮蔽。一般而言,这一项也跟材质的粗糙程度有关,可以理解为越粗超的材质表面越有可能发生自我遮蔽现象。

G的Schlick-GGX公式:

k在直接光与间接光中分别为:

如前面的示意图所示 Smith认为 G项分应该分为为两部分: 第一部分 是 Geometry Obstruction 第二部分是Geometry Shadowing 即:

仅有G项:

直接光的最后效果:

间接光 漫反射+镜面反射
间接光是用IBL(Image Based Lighting)来模拟的。与直接光相比,IBL把周围环境整体视为一个大光源,IBL 通常使用(取自现实世界或从3D场景生成的)环境立方体贴图 (Cubemap) ,我们可以将立方体贴图的每个像素视为光源,在渲染方程中直接使用它。这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。
表示环境或场景辐照度的一种方式是(预处理过的)环境立方体贴图,给定这样的立方体贴图,可以将立方体贴图的每个纹素视为一个光源。使用一个方向向量 wi 对此立方体贴图进行采样,就可以获取该方向上的场景辐照度。
给定方向向量 wi ,获取此方向上场景辐射度的方法就简化为:
虽然这么简化了,但是如果要求解反射方程的话,我们依然需要计算球面积分,这对于实时渲染来说太昂贵了。解决这个问题,我们可以通过预计算一些结果并存储起来,然后在运行时利用这些中间结果计算最终值。首先将反射方程拆分一下。

通过将积分分成两部分,可以分开研究漫反射和镜面反射部分。
间接光漫反射部分:

间接光镜面反射部分:
漫反射部分
颜色 c 、折射率 kd 和 π 在整个积分是常数,不依赖于任何积分变量。基于此,可以将常数项移出漫反射积分:

这给了我们一个只依赖于 wi 的积分(假设 p 位于环境贴图的中心)。有了这些知识,我们就可以计算或预计算一个新的立方体贴图,它在每个采样方向——也就是纹素——中存储漫反射积分的结果,这些结果是通过卷积计算出来的。
卷积的特性是,对数据集中的一个条目做一些计算时,要考虑到数据集中的所有其他条目。这里的数据集就是场景的辐射度或环境贴图。因此,要对立方体贴图中的每个采样方向做计算,我们都会考虑半球 Ω 上的所有其他采样方向。
为了对环境贴图进行卷积,我们通过对半球 Ω 上的大量方向进行离散采样并对其辐射度取平均值,来计算每个输出采样方向 wo 的积分。以wo方向所在的半球,采样方向 wi 。

这个预计算的立方体贴图,在每个采样方向 wo 上存储其积分结果,可以理解为场景中所有能够击中面向 wo 的表面的间接漫反射光的预计算总和。这样的立方体贴图被称为辐照度贴图,因为经过卷积计算的立方体贴图能让我们从任何方向有效地直接采样场景(预计算好的)辐照度。

对于一点P 它所受间接光来自整个CubeMap (以CubeMap中的每个像素为一个光源) ,假设P点的法线N为光线的出射方向,那么可以离线计算出P点所受环境光照的影响。给定任何方向向量 wi ,我们可以对预计算的辐照度图采样以获取方向 wi 的总漫反射辐照度。为了确定片段上间接漫反射光的数量(辐照度),我们获取以表面法线为中心的半球的总辐照度。获取场景辐照度的方法就简化为:
生成辐照度贴图
实时进行积分计算太消耗,可以先离线计算出来。然而,理论上在半球上采样方向是无限的 ,因此不可能从 半球上 的所有方向采样环境光照。不过我们可以对有限数量的方向采样以近似求解,在半球内均匀间隔或随机取方向可以获得一个近似精确的辐照度值,从而离散地计算积分结果。
伪代码:
LightProbe 与 球协函数
辐照度贴图是固定的,如果游戏中有多个场景,比如室内室外 那么肯定是不能为每一个场景都单独弄一个辐照度贴图的,会消耗大量内存。游戏引擎中使用LightProbe放置在场景中,去近似采样并存储以该LightProbe为中心的 "辐照度贴图",但不是单独存一张贴图,而是用球协函数存储相应的信息,最终存储的是球协函数的参数,通常是 9个float,这比单独存一张贴图节省多了。
在Unity 中放置LightProbe,并且烘培后,使用ShadeSH9函数即可获取当前的点的"辐照度"。

仅显示球协函数的光照:

HDR图可以在这里下载: https://hdrihaven.com/hdris/

仅显示间接光漫反射:

骚操作一下:
将上面的HDR图直接在PS里做一个高斯模糊,然后再去采样

得到下面结果:

会比较暗,这是因为在PS做模糊的时候,将HDR从32位转为了16位,因此手动把精度加回来(不准确,但是可以理解这个过程)
和球协的有点区别,但是明显这张图比球协的更准确地反映了 环境光 的 "黄"

球协函数原理参考:
https://zhuanlan.zhihu.com/p/50208005
https://pdfs.semanticscholar.org/83d9/28031e78f15d9813061b53d25a4e0274c751.pdf
https://basesandframes.files.wordpress.com/2016/05/spherical-harmonic-lighting-gdc-2003.pdf
http://web.cs.wpi.edu/~emmanuel/courses/cs563/S05/talks/mark_w5_spherical_harmonic_lighting.pdf
镜面反射部分

将镜面反射的公式近似地拆成两部分(EpicGames SplitSum)
间接光镜面积分第一部分
对于高光部分而言,如果材质表面越光滑,则该点出射光大部分贡献来源于入射光的镜面反射,很小一部分来源于各个方向的漫反射。所以,这次我们在计算积分的时候,需要把粗糙度也考虑进去。如果粗糙度越低,那么积分的贡献范围就越集中,相当于卷积核的面积越小,大权重集中在核中心,卷积结果尺寸越大,产生越清晰的反射效果。反之亦然。
于是可以根据材质的粗糙度,映射到某个粗糙度区间,然后把计算结果保存到不同的LOD等级中。这张具有不同LOD等级的立方体贴图也称为pre-filtered environment map。
后续在运行时只需要根据粗糙等级做一次采样就可以得到高光积分的第一部分。
仅显示ReflectionProbe信息,即 间接光镜面积分第一部分 结果
ReflectionProbe
Reflection Probe 是六个相机分别捕捉它所在位置的6个方向的场景信息,最后储存在一张Cubemap贴图里。
在场景中放置ReflectionProbe Bake之后才会有信息。
当用一个未烘培过的ReflectionProbe去靠近物体(仅显示ReflectionProbe信息),明显发现物体变黑了,因为这个未烘培过的ReflectionProbe没有信息,而unity_SpecCube0是最近的一个ReflectionProbe。
间接光镜面积分第二部分
经过推导后可得如下:
计算这个积分有两种办法,第一种是BRDF积分图 第二种是 函数拟合
BrdfLut
假设每个方向的入射光都是白色的(L(p,x)=1.0 ),就可以在给定粗糙度、光线 ωi 法线 n 夹角 n⋅ωi 的情况下,预计算 BRDF 的响应结果。以X轴为法线与入射光的夹角(dot(N,L)),以Y轴为粗糙度,将计算的结果存储在一张 2D 贴图上(Lut),该贴图称为BRDF积分贴图。积分的结果分别储存在 贴图的 RG通道中。使用的时候可以直接采样该贴图即可。
仅显示Specular PartTwo
上面代码中的菲涅尔系数计算和前面的有两点不同,第一点是这里没有用于计算微片元朝向的D函数,计算菲涅尔系数使用的是真正的nv而不是vh,第二点是考虑了粗糙度。使用nv是由于环境光来自半球内围绕法线N的所有方向,因此无法和直接光照中的法线分布函数D一样使用单个半角向量来确定微平面分布,所以在此我们只能使用法线和视线的夹角(即nv)来计算菲涅尔效果。
数值拟合
PartTwo的结果出了可以预计算出来外,还可以用函数拟合来实时计算。
用函数拟合的效果比BrdfLut的 效果更好,且更节省性能。
关于BrdfLut的生产参考:
https://learnopengl.com/PBR/IBL/Specular-IBL
数值拟合的参考:
https://blog.selfshadow.com/publications/s2013-shading-course/lazarov/s2013_pbs_black_ops_2_slides_v2.pptx
公式推导参考:
https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf
最终效果:

与Unity默认的对比 BaseColor=(255,199,88) Metallic=0.95 Roughness=0.21 参数一致 用屏幕后处理统一做 ToneMapping

可以看出Unity 的效果偏亮一点,因为Unity在实现PBR算法的时候 直接光漫反射 与 间接光漫反射 都没有除以PI(为了与老版本效果兼容),而我们的算法是标准的PBR算法,在表现金属上会有更细腻的亚光效果。
完整的PBR效果:(BaseColor + Metallic +Roughness + Normal + AO)

完整的PBR代码链接:
https://github.com/ipud2/Unity-Basic-Shader/blob/master/PBR_BetterThanUnity.shader
HDR 与 Gamma
HDR
在PBR中做光照运算时,最终输出的颜色值时常超过1,而超过1的部分在显示器中显示就会 “泛白过爆”,为了解决这个问题,将HDR(High Dynamic Range)的颜色值转换到LDR(Low Dynamic Range)
的算法叫做 ToneMapping(色调映射)。在各种ToneMapping算法中ACESTonemapping效果与性能兼优。
Gamma
人眼对 低亮度 的颜色变化 感知强,对 高亮度 的颜色变化 感知弱
为了提高图片的显示辨识度,将自然界线性的颜色储存在非线性的空间(Gamma)
在渲染中先将图片转化为 线性空间(Linear),再进行颜色加减乘除操作才是正确的,最后再转为非线性(Gamma)的颜色输出
在Unity中如果设置颜色空间为Gamma,那么在进行PBR计算之前需要用代码手动转到Linear,并且最终输出到显示器的颜色时候 需用代码手动转到Gamma空间
在Unity中如果设置颜色空间为Linear,那么Unity会帮我们把图片自动转到Linear空间,并且最终输出到显示器的颜色时候 自动转到Gamma空间
总结

TODO原理:
Physically Based Rendering in Filamentgoogle.github.io/filament/Filament.html#materialsystem/anisotropicmodel/anisotropicspecularbrdf

结果
代码:
ipud2/Unity-Basic-Shadergithub.com/ipud2/Unity-Basic-Shader/blob/master/%E5%9F%BA%E4%BA%8E%E7%89%A9%E7%90%86%E7%9A%84%E9%AB%98%E5%85%89Brdf/PBRAnistropicSpecular.shader

传统的PBR可以很好地模拟出各项同性的单层表面的光照模型,但是对于多层材质就很难模拟出。比如常见的清漆效果(ClearCoat)。

以下图片是一个对比:

为了模拟这种清漆效果,可以再原PBR的基础上加一个二级高光,但要保证能量守恒,那么能量守恒公式可以这么描述:

对于清漆材质,物体的表面分为两部分,上面的一部分是ClearCoatLayer 下面的部分是BaseLayer,一束光入射到表面,一部分光直接反射出去(ClearCoat高光,且忽略这部分光的漫反射),另一部分进入BaseLayer,再分为漫反射与GGX高光反射出去。

那么现在要解决的问题就是:有多少光进入了BaseLayer? 为了解决这个问题,引入了两个参数,ClearCoatRoughness用来描述清漆表面的粗糙度以及ClearCoatIntensity用来描述清漆反射的强度。清漆表面的菲尼尔反射部分属于ClearCoatLayer的能量,剩下的进入BaseLayer。用公式描述:

ClearCoat高光用GGX来描述,因为GGX中有Fresnel部分就不用额外在额外乘以菲尼尔项了。公式可以简化为:

对于直接光部分的代码
对于间接光部分的代码
最后结果:

完整代码连接:
https://github.com/ipud2/Unity-Basic-Shader/blob/master/%E5%85%89%E7%85%A7%E6%A8%A1%E5%9E%8B/ClearCoat.shadergithub.com/ipud2/Unity-Basic-Shader/blob/master/%E5%85%89%E7%85%A7%E6%A8%A1%E5%9E%8B/ClearCoat.shader
TODO原理:
结果:

完整代码连接:
https://github.com/ipud2/Unity-Basic-Shader/blob/master/%E5%85%89%E7%85%A7%E6%A8%A1%E5%9E%8B/Cloth.shadergithub.com/ipud2/Unity-Basic-Shader/blob/master/%E5%85%89%E7%85%A7%E6%A8%A1%E5%9E%8B/Cloth.shader
