我们是通过光来看到世界的,光线赋予了斑斓的世界,同样的在虚拟世界中也需要绘制出光,但我们要面对一些问题:
- 1.现实中光发射出来打到物体上,被物体吸收部分其余的反射出去,最终进入人的眼睛,那么如何在虚拟世界中模拟光的原理。
- 2.光和不同物质的反应,反弹?吸收?折射?
- 3.如何写shader
目录
目录
简化Visibility to Light :ShadowMap
PCF-PercentageCloserFilter-PCSS -根据物体距离光源的远近,确定阴影的质量。
一、Lighting
rendering equation
1986年,James Kajiya提出了物体渲染方程(Rendering Equation),世界上所有物体的渲染逻辑都可以用这一方程来解释。
在一个点去观察一个物体表面任一点X,求点x射入眼睛的光:
1.物体自发光,首先需要有一个观察角,也就是眼睛所对应的那条橙色线,即人的observe观察角。如果点x会自我发光,那么观察角方向一定会看到点X射出的光。
2.反射,当点X被周边多个光源贡献时,进行积分,将所有球面上入射角射入的光投影在表面上(θ是法向与入射的夹角,如太阳光照在赤道上几乎为90度,能量几乎全部接收因此会热),之后通过scattering function(brdf)来得出在当前入射角和出射角情况下,有多少能量反射出去。
最后将自发光与反射的能量相加,即得出射入到眼睛的光的能量。
出射光能量 = 物体自发光 + 反射
渲染难点(challenge)
Visibility to Lights -> 光的可见性,Shadow
Light Source Complexity ->光的复杂性
方向光源,点光源、锥体光源、面光源,不同的光源如何影响物体,如何得到准确的入射irradiance?
Do integral -> 如何快速算出光和材质之间的积分结果。
虽然我们有了渲染方程,但其涉及了积分计算,十分复杂耗时,在工程实践中如何进行使用呢,如何去做shading?
光会不断地进行bounce,且场景中的物体在吸收光后自身就是光源
光会不断的bounce,不同物体之间的反射相互影响光照,每次计算出的output都会作为下一次的input,这是一个无限递归的过程,无法按照这个逻辑去实现,因为计算量爆炸。
总结
1.如何在一点去得到入射的irradiance
2.得到光源后,做shading计算,也就是半球上的lighting和材质brdf的积分运算十分expensive
3.为了准确的计算irradiance,我们需要判断所有光源的贡献,由于场景中的所有物体均为光源,导致每一次计算的output会是下一次的input,从而导致无限迭代,这又扰乱了第一个challenge使其变得更加麻烦。
二、Rendering in the engine
简化lighting
我们将光分解成两部分:
对于我们的主光dominant light来说,大部分情况下我们使用的是方向光,点光源和锥体光使用情况较少。将主光外的光叫ambient,ambient环境光是一个常数,用它来代替半球主光外的irradiance的平均值。
让某些金属材质能够产生镜面效果,呈现出环境的样子。这就需要我们将环境数据存储到一张纹理中(Cubemap),便于环境采样,也就是环境贴图(environment map)。
将一个点的半球面的光场分布模拟成了一个常数代替的环境光,加上一个主光(方向光),将环境光中的高频信息用环境贴图表达,从而将brdf中的specular项表达出来。
简化Material : Blinn-Phong
物体之所以能被我们观察,是因为人眼接收到了从物体来的光。
这其实也就是局部光照模型的基础,blinn-phong模型是一个根据经验得出来的模型,它使用Ambient(环境光照)、Diffuse(漫反射)以及Specular(高光)来描述光照shading计算。
1. 镜面反射:高光部分(完全反射),光线的镜面反射。
2. 漫反射:漫反射(均匀反射),粗糙物体表面向各个方向反射光线。(视线与法线的夹角)
3. 环境光:间接光照,物体某些点不会被光线直接照到,但我们仍然能够看到颜色,这部分区域是通过周围环境的漫反射获得的光照。通常会使用一个固定常量。
光叠加原理,即所有的光最后均可linear的叠加在一起,使用简单易用。
1.blinn-phong模型不遵守能量守恒,其能量可能会溢出(射入<射出)入射能量小于通过方程后算出的反射能量,假设进入能量为100反射出为101,看着数值相差不大,但如果我们使用ray tracing的话,光线不断bounce的情况下会使得能量不断增加,场景越来越亮。
2.blinn-phong模型有一种强烈的塑料感,而不是真实感,无法用其表达复杂模型的细节。
简化Visibility to Light :ShadowMap
shadow其实很简单,就是场景中的点是否可以被光源所看到,前提是光源为简单的点光源,方向光源,spot光,而不是复杂的多光源情况。
其实在二三十年前就有了很多关于shadow的算法,即使渲染阴影有很多处理方式,但游戏引擎中最常见的处理方式是ShadowMap。
- 在光源处放一个Camera,从光源的Camera看向场景生成一张场景物体深度图(ShadowMap)。
- 当我们进行shading计算时,在vertex shader中将world顶点给transform到Light Space中,将三维点(PX,PY,PZ)变换为二维坐标(PX,PY)和深度PZ,并将转换后的数据传到fragment shader中
- 然后在fragment shader中,我们首先找到closetDepth (也就是通过(PX,PY) 在shadowmap上查询对应的深度信息),接着再找currentDepth (也就是 Pz),接着如果PZ>closetDepth ,则认为此片元处于阴影中
- ShadowMap问题:自遮挡、可以尝试加一个bias使它的容差大一点,但是这样会导致物体与阴影之间出现”断层“的人脚浮空现象。
三、 基于预计算的全局光照
AAA
全局光照 = 直接光照(光线直接照射)+间接光照(物体之间反射)
直接光照包括:光源的直接光照;动态物体提供实时的环境光计算
间接光照包块:光源在物体之间的光线反射;以及环境光照(比如天空)。
在上述的简易光照方案中,我们使用ambient作为一个均匀的间接光照。这种处理方式很容易让画面出现平面感,因为现实中各个方向的间接光照是不同的。(所有的物体都采用相同的反射光,会使效果显得平面化,解决这些就需要对所有点进行采样,但是数据量很大。)
实时计算全局光照太过困难,因此我们想要预先计算全局光照的结果,以内存换时间。
我们知道对于场景中,其实大部分物体都是静止不动的,比如假设场景中90%的物体是静止不动的,而且我们也设置好了每个关卡中的太阳光源角度,这些其实是可以通过预计算来提前算出来的,也就是我们用空间换时间。
假设我们拥有GI,此时对于间接光需要面临两个挑战:
1.数据存储量大。实际上是一个球面空间的采样,相当于将球面像地图一样展开,但是如果我们存这样的数据的话,数据量会十分的大,因为场景中每个像素点都要存一个。
2.即使存下了整个场景的光照,如何将一个材质上的diffuse面和球面光照快速的积分出来结果。
虽然我们用蒙特卡洛积分去进行采样,但我们在绘制时候,如果每一个fragment上都进行这么一个球状的光照函数采样再累加的这么一个卷积运算,简直要疯掉了。
傅里叶变换的应用
球面是一个无穷维的连续信号,对于这种信号只能采样表达,它实际上是一个无限的向量。
1. 一张图片可以转换为一张傅里叶频率谱,如果在这个频域中只截取其中的一小部分,就可以形成对信号粗糙的表达,也就是高度压缩,这个粗糙表达可以让我们大概看出是什么东西。
2. 当一张图片与另外一张图片做积分(卷积)运算,不需要每个像素做运算,只需要投影到频域空间两张频谱进行一次卷积运算,就可以实现效果,最后再进行逆傅里叶变换将频谱变为图片即可。
Spherical Harmonics
SH是一系列基函数,系列中的每个函数都是2维函数,并且每个二维函数都是定义在球面上的,定义在球面上可以理解为方向的函数。
- 它是一系列的基函数,可以以傅立叶变换为参考,与里面不同频率的cos和sin函数类似,只是全都是二维函数,与一维的傅里叶一样,SH也存在不同频率的函数,但不同频率的函数个数也不同,频率越高所含有的基函数越多。
- 因为它是定义在球面上的,球面上会有不同的值,由于在球面上两个角度 和就可以确定一个方向了,因此可以理解为是对方向的函数,通过两个角度变量从而知道这一方向对应在球面上的值.
一般来说我们取很低的阶来展现低频信息,如图在场景中取一点,在这个点进来的光(也就是我们进行球面采样)我们将球按照地图一样展开。接着我们对其用SH进行压缩,在这里我们只取L0和L1两阶,一共4个基函数。
我们可以看到重建出来的图虽然十分模糊,但是我们大致知道哪些地方有光,而且整个数据是连续,如果想知道某方向的光强是多少只需要进行一次线性的vector计算即可,大部分时间其实用来表示环境光的,因为环境光本来就是低频的,用SH正合适。
以n=1时为例,此时球谐函数由4个基函数组成(记录4个系数),每个点的光照信息由RGB三个数据组成,因此一共需要记录3*4=12个数据。
虽然有4个参数,但其权重是不一样的,比如1阶L0,相当于对整个环境广场做了一次加权平均使用的是HDR的处理方式,对于2阶L2的话使用的是LDR的处理方式。
这样每个点只需要32个bit就可以存每个点上的光场,不但空间很少,而且由于把光用SH去表达,那么计算diffuse和specular很简单,就是两个向量的点积。
LightMap
光照贴图是预先计算场景中表面亮度的过程。它将计算出的信息存储在图表或光照贴图中供以后使用。光照贴图允许您以相对较低的计算成本添加全局光照、阴影和环境光照。
早期quake时代,卡马克大神因为shadow计算不过来从而使用了light map,后来随着shadow的技术不断地更新,工业界也就抛弃了light map,但后来由于需要全局光照,发现light map的思想十分有用,就再次开始使用light map了。
将场景中的光照先烘焙到一张图上去,这张图叫做lightmap atlas(航海图、地图),将很多的几何给放在一张图上。
如何将如此多的几何放在一张图上?
1.首先需要对这个世界进行几何减化。
我们不能使用非常清晰的geometry,因为有一个步骤叫做parameterization,这一步会 将三维空间内的复杂几何投影到二维空间上,如果是一个方块或者圆的话,对他们这种简单形状进行参数化比较简单,但如果是一个复杂几何,拥有成千上万个三角形的话,对其进行参数化比较复杂,所以需要对几何进行简化。
2.在参数空间进行分配,将所有的几何分配到atlas中。
希望在同样的体积或者面积里分配的texture resolution上的texture精度基本上类似。如图,其意思就是如果我们把不同resolution上的texel(格子)标上不同的颜色,当把求重建回3D空间时格子们的大小比较均匀。
LightMap的优点:
- 在real-time中效率高,因为成本低
- 由于是离线baking,当将空间分解之后,会产生很多细节或subtle的效果。
LightMap的缺点:
- 时间非常长的预计算时间。
- 只能处理static物体和static光源。
- 由于采用的是空间换时间的策略,在real-time时lightmap会占用几十到几百兆的存储空间
LightMap思想:
- bake可以空间换时间;空间光照可以烘焙成图片,那很多计算可以实现可以管理。
Light Probe
那么除了lightmap这种方法还有没有更加straight一点的方法呢,lightmap中我们需要将场景中的复杂几何参数化给放置到atlas中去,参数化的算法写起来十分恶心,动不动就一堆BUG,有没有更加直接一点的方法呢?
有,我们可以在空间中放置一堆采样点,因为每个点上的光照其实就是一个球面采样,我们称这些采样点为light probe。
对于每个 probe,我们采样其所在位置的光场,当我们的角色在场景中进行移动时,根据相邻几个probe的插值求出角色所在位置的光照。
reflection probe
我们在设置light probe时候一般会设置的比较密集,但是我们会使用压缩算法去压缩精度,因为我们需要的是只是光照信息,如果我们做diffuse的话取低阶就可以。但是在游戏中存在着一些闪亮像镜子一样会反射场景的物体,而这种物体反射的场景是会变化,所以我们需要设置reflection probe。
reflection probe的数量不会特别多,但是它不像light probe一样,reflection probe我们会取高精度,因为反射对高频十分敏感,我们需要反射这个Probes周围的环境,所以取高精度。
因此如果我们采用light probe + reflection probe的方法,会得到一个不错的GI效果。
light probe + reflection probe优点:
- real-time时效率高,速度快(比起lightmap存的数据少,而且采用了各probe之间插值处理的方法)
- 能处理静态物体和动态物体
- 在real-time中并非实时更新,而是隔几帧更新一下
light probe + reflection probe缺点:
- 比不上lightmap,无法实现软阴影等效果,因为精度低
四、基于物理的材质
微平面理论
一个表面上其实是有无数个微表面,而光其实是在这些微表面上进行反射打出去的,一个金属表面是粗糙还是光滑,实际上是和平面上的法向聚集度相关的:
- 如果微表面们的法向朝向比较相似,其表现为金属材质较为清晰的反射(反射方向大致一致,完全一致则为镜子)
- 如果微表面们的法向朝向几乎不相同,其表现为金属材质较为粗糙的反射(反射方向为四面八方)
微平面理论的BRDF反射模型
基于微表面理论的基础上引入了一个反射模型,现如今大家用的最多的这类模型叫GGX。
我们知道当光打到一个物体上时,只会发生两种情况:
- 部分光打到物体表面最后被反射,反射能量的多少取决于表面上法向的分布,也就是roughness(粗糙度),roughness越高,随机性越强,而roughness越低,随机性就地,大部分法向相似,也就越接近于镜面反射。
- 部分光被吸收进入物体中,金属物体的电子可以捕获光子,而非金属没有能力捕获光子,因此进入的光会在内部进行几次弹射后,最后以一个随即方向射出去,因此一束光打入物体在经历几次折射后最后发生漫反射现象。
因此PBR模型其实可以分为两个PART:
- lambert的漫反射(diffuse)部分,如果将球面所有的漫反射部分积分起来的话结果为 c/pi,c的大小取决于多少能量可以进来。
- specular,反射的这一部分引入了著名的cook-Torrance模型,可以看到它的公式中有DFG这三项,每一项代表了一种光学现象。
D(Normal Distribution) 表示法线分布
左下角是GGX模型和Phong模型对比,我们可以发现GGX模型的变化足够快并且底部足够平滑,也就是高频部分突出,低频部分变化慢不像Phong一样快速衰减。
将GGX模型比作音箱来理解就是,高音够脆,低音够沉,范围广,这样的表现力才强。
表达法向分布的随机度,roughness越高,随机度越高,也就越发散。接下来根据roughness来计算G。
G (Geometric attenuation term) 微表面几何内部的自遮挡
在基于微表面理论的基础上,我们认为表面上的法线形成了一个roughness的分布方程,根据这个roughness的分布方程我们可以用积分的方法大致估算出每个角度上大致有多少光被挡住,伪代码中的GGX函数的目的是为了计算出阻挡性,而G_Smith是在将光的阻挡性和视线的阻挡性计算出之后,将两个阻挡性相乘,从而知道有多少光从入射表面到弹到眼睛时被挡住了。
举个例子,一束光以100%的能量射入到表面上,根据NDF方程(roughness分布方程),我们知道30%的光被遮挡了,因此此时的光的能量是70%,这些光又是无数个光子,这些光子开始朝眼睛运动,由于分布时各向同性的,因此70%的能量中又有30%被遮挡了,最后到达眼睛的能量就是70% X 70% = 49%。
F(Fresnel)菲尼尔现象
什么是菲涅尔现象?
当你的眼睛视线足够靠近表面的切线方向时,反射系数会急剧增加,从而会产生一种倒影的效果。
视线与平面的夹角约大,人眼接收到的反射约弱。反射效果越弱,水体看起来越透明,像空气;反射效果越强,水体看起来越光滑,像镜子。
如图我们可以知道,摄像头在固定位置,而他所照出来的图片在近处看感觉清澈见底,在远处看则像是一面镜子反射出了山,这就是因为在同一位置对不同位置进行观察时,视线与平面的夹角不同,观察近处时与平面夹角大,观察远处时夹角小。
因此,当你的视线与水平面接近时,此时就会产生很强的镜面效果。
这样Cook-Torrance模型就可以通过roughness以及fresnel这两个参数来很好的模拟出符合物理规则的材质。
MERL BRDF
ok,现在我们有了表面上的法线朝向分布情况,引入了roughness参数,并且根据分布方程和roughness计算出了各个角度的阻挡性,又引入了fresnel从而模拟fresnel现象,已经能够很好表达物理世界的效果,但是现实世界的物体仍然很复杂,想要通过艺术家手动调参的方式来实现仍然有一定难度。
MERL BRDF数据库是对大量现实中的物体进行采样,提供个各种材质对应的BRDF参数,从而提供了不同材质的roughness参数和菲尼尔参数,也表达了diffuse应该是多少。
Disney BRDF规则
在最早期时候,艺术家对于cook-torrance的使用并不成熟,导致了很多的问题,比如能量不守恒的话,光线bounce几次之后lightmap的计算就会爆炸了。
这时候Disney的一位大神提出了几条规则:
- 物理材质的每个参数需要符合直觉且容易让艺术家明白,不可以很抽象
- 要尽量使用较少的参数
- 参数要尽量在0~1之间
- 需要一些特殊效果时参数可以超出0~1这个区间
- 所有参数的组合应该是合理的,不会出现诡异的结果
主流PBR材质模型
基于上述的原则,现代游戏引擎中常用的PBR模型有以下两种:
1 Specular Glossiness(SG模型)
SG模型的特点是几乎没有什么参数,它的参数被图代替了,相比参数可以精确到像素来调整,由于他太灵活了,导致其中specular项的RGB通道artist不太容易设置好,一旦出错会导致菲涅尔项炸掉,从而使得材质整体看起来十分的假。
其中:
- Diffuse处理漫反射部分,有RGB这三个通道
- Specular处理菲尼尔项,也有RGB这三个通道,当表达像黄金,铜这种带颜色的金属时,对于不同的色彩不同的角度所表达的效果是不一样的,因此菲涅尔项也有RGB三个通道。
- Glossiness控制材质的光滑程度,也就是这片区域是粗糙还是光滑。
Metallic Roughness模型
MR材质相比SG材质,使用金属度(metalic)来关联diffuse和菲尼尔部分,这样就避免了两个参数冲突问题。本质上MR材质还是使用SG材质,只不过对参数做了一些限制,设置了base_color,roughness,metallic。
- 如果一个物体的金属度非常低,则代表为非金属,那么此时的base_color无法用在diffuse和specular中进行计算。
- 而如果金属度十分高,则代表这个物体是金属,那么你的base_color会被大量抽走并用在diffuse和specular的计算中。
MR就是这个意思,相当于在SG的基础上为了避免菲涅尔项炸掉,根据metallic值来判断是金属还是非金属
但MR模型也不是完美的,当非金属与金属过渡时容易产生白边。
五、Image-Based Lighting(IBL)
SH函数只可以大致的表示明暗变化,无法完美的展现出细节和凹凸感,并且在游戏中由于主角是一直在运动的,当光打到主角正面时,我们希望暗部也能展示出细节感,因此提出了IBL。
BRDF的材质模型分为diffuse和specular两部分:
1.Diffuse部分的环境光
diffuse部分本质上就是一个着色点对球面光照进行积分的操作,我们知道天空盒/天空球是无限大的,因此天空盒/天空球的环境光来说,光的强度大小都一样因为光来自于无穷远处,也就是说,不管我们的模型多么大,我们计算模型上某一个具体点的时候我们认为该点处于天空球中心。
IBL的思路就是提前计算每一个法线n所对应的l(n),将结果保存在一张立方体贴图(漫反射辐照度图)中,这里可以用一个trick,直接根据BRDF的区域大小采取相对应的滤波核然后对天空盒进行模糊,这样每次计算的时候只需要用具体的法线n去寻找图上存的对应的l(n)即可,用空间来换时间。
2.Specualr环境光
对于specular部分我们如果可以像diffuse部分一样生成irradiance map也是一件很好的事情,但是对于diffuse来说我们的变量只有法线朝向这一个变量。
两个函数乘积的积分 = 两个函数独立积分的乘积,因此我们将整个公式拆分为lighting项和brdf项:
六、Shadow
shadow map
Shadow的通常处理方式是Shadow Map,Shadow Map原理:在光源处放置一台摄像机,生成一张深度图(Shadow Map),在渲染接收阴影的物体时,将像素点转换到光源空间中,与Shadow Map的深度对比。若位置处于ShadowMap记录深度前方,则不处于阴影中;反之则处于阴影中。
但Shadow Map最大的问题是精度不足。当我们的视线范围教大时,所需的阴影精度可能较低;但当我们的视野范围缩小时,就需要较高精度的阴影,而我们的Shadow的采样区域和分辨率是相对固定的。并且在距离人眼比较近的位置,阴影更加清晰,远离人眼的阴影相对模糊。
Cascade shadow map
根据距离的不同采取不同精度的shadow map,从我们的眼睛出发的一个frustum,我们通过距离将frustum分成若干个子frustum,至于如何分看个人喜好,不同frustum内的shadow精度也不同。
但是cascade shadow有一个问题,就是由于分成了不同层级采用了不同的采样精度,因此不同层级之间的交界处需要进行插值处理,否则就会产生一个很生硬的边界。
优点:
- 很快,因为用空间换时间,但它消耗的存储空间十分大。
- 效果比较好。
缺点:
- 近处的shadow质量不会特别高。
- shadows没有颜色
- 透明的物体会显示不透明的阴影。
RealisticShadow-SoftShadow
PCF-PercentageCloserFilter-PCSS -根据物体距离光源的远近,确定阴影的质量。
PercentageCloserSoftShadow-
Variance Soft Shadow Map
上个时代的3A标配选择
光照:Lightmap + Lightprobe。都会有解决不同的问题
材质:PBR(SG、MR) + IBL(背光、环境光)
Shadow:CascadeShadow+VSSM
七、前沿技术
Shader Model
- Compute shader
- Mesh shader
- Ray-tracing shader
Real-Time Ray-Tracing
上面讲到的GI算法,其实都不是真实的实时光照处理,它们都有一定的预计算或者很多非常规假设。但在新一代硬件支持下,实时光线追踪的处理方式出现了,虽然现在还没有能够大规模普及,但这项技术已经在突破的边缘。
Real-Time Global Illumination
Virtual Shadow Maps
Virtual Shadow Maps和Virtual Texture原理很像。Virtual Texture是将游戏中需要用到的所有纹理Pack到一张纹理中,需要使用时就加载调用,不需要时就进行卸载。
Virtual Shadow Maps首先计算哪些地方需要Shadow Map,然后在一个完整虚拟的Shadow Map中去分配空间,每小块得生成Shadow Maps。在计算Shadow时,反向去取小格数据。这种处理方式可以更有效利用存储空间。
Uber Shader
通过宏定义不同情况下的Shader组合,在编译时生成大量独立的Shader代码,这就是所谓Uber Shader(类似Unity中的Shader变体概念)。这样的好处是,当Shader发生变化时,只需修改组合Shader后重新编译。