量子计算9 发表于 2023-1-7 13:21

《Unity Shader入门精要》笔记(三十七 完结)

本文为《Unity Shader入门精要》第十八章《基于物理的渲染》的第三节内容《一个更复杂的例子》。也是本系列的最后一篇笔记,原书第十九章和第二十章分别是《Unity 5更新了什么》和《还有更多内容吗》,这两章不涉及核心知识的讲解,所以将不会作为本系列笔记的内容,感兴趣的读者可自行查阅原书籍。
本文相关代码,详见:

原书代码,详见原作者github:
<hr/>本次笔记将结合一个更复杂的基于物理渲染的场景,来学习设置光照环境、放置反射探针、调整材质、线性空间这几个核心知识,场景案例可从原书资源仓库中查找。
在运行场景前,需要先将颜色空间调整为线性空间,在Edit-Project Settings-Player-Color Space中选择Linear,因为基于物理的渲染需要在线性空间下做计算。

1. 设置光照环境

1.1 环境光照

Unity中新建一个场景会包含一个默认的Skybox,本例使用SunsetSkyboxHDR来替换掉默认的Skybox。这里的Skybox使用了一个HDR格式的Cubemap,与之前制作Skybox时使用的纹理的不同,后面扩展内容将更加详细地介绍HDR的原理和应用,这里只需知晓HDR格式的Skybox可让场景中的物体的反射更加真实。


我们还可以设置场景使用的环境光照,这些环境光照可以对场景中的所有物体的表面产生影响。如上图的面板,我们可以设置环境光照的来源(Environment Lighting-Source选项),可配置是来源于Skybox还是渐变色,或是某个固定的颜色。环境光的强度可通过过拽Environment Lighting-Intensity Multiplier项的进度条来控制。

如果我们将场景中所有的光源都关闭,并把环境光照的强度设为0,可以看到场景中的物体仍能接收一些光照,如下右图:



1.2 环境反射

原因在于——反射,如果我们将光源设置面板的Environment Reflections-Intensity Multiplier设置为0,则物体将一片漆黑:



除了使用Skybox作为反射光照的反射源外,也可以设置自定义,拖拽一个自定义的Cubemap,但一般为了更逼真的渲染结果,通常不会这么做。

渲染实现上,即便场景中没有任何光源,Unity内部也会调用ForwardBase Pass(如果使用的是前向渲染路径的话),并使用反射的光照信息来填充光源信息,在进行基于物理的渲染计算。
如果场景中添加了其他反射探针(Reflection Probes),物体可能会使用其他反射源,而不是使用光源设置面板配置的默认反射源。当默认反射源是Skybox时,Unity会有场景使用的Skybox生成一个Cubemap,我们可通过Resolution选项来控制它每个面的分辨率。

1.3 直接光源

下面来设置场景使用的直接光源——平行光。在PBR中,为了让渲染效果更佳真实可信,需要保证平行光的方向和Skybox中的太阳(或其他光源)的位置一致,且平行光的颜色和Skybox呈现的光源的颜色一致。

1.4 烘焙

在平行光面板的烘焙选项中,我们可选择3种模式:

[*]Realtime
实时模式,场景中平行光影响的所有物体都会进行实时的光照计算,当光源或场景中其他物体的位置、旋转角度等发生变化时,场景中的光照结果也会随之变化,但这样会有较大的性能消耗。

[*]Baked
烘焙模式,Unity会在离线模式下将光源的光照效果烘焙到一张光照纹理(lightmap)中。这样就无需实时进行光照计算了,但缺点是:物体或光源发生旋转或位置改变时,光照的效果不会随之变化。

[*]Mix
混合模式,是实时模式和烘焙模式的结合,场景中静态物体(被标识为Static的物体)的光照会被烘焙到光照纹理中,但其他物体仍然做实时光照计算。

1.5 间接光照

光线从光源直接打到物体表面后,发生反射,反射出的光线继续打到其他物体,此时其他物体接收到的这种光照就叫间接光照。

Unity中这些间接光照的强度是由GI系统计算得到的默认亮度值,在光源面板中,对应了Environment Reflections-Bounce一项:



当Bounces被设置为1,表示一条光线仅会和一个物体相交,不再被继续反射。即:场景中的物体只会受到直接光照的影响。

另外,间接光照还可能来自一些自发光的物体。

2. 放置反射探针

反射探针的工作原理和光照探针(Light Probes)类似,它允许我们在场景中的特定位置上对整个场景的环境反射进行采样,并把采样结果存储在每个探针上。

当游戏中包含反射效果的物体从这些探针经过时,Unity会把这些邻近探针存储的反射结果传递给物体使用的反射纹理。如果物体周围存在多个反射探针,Unity还会在这些反射结果之间进行差值,得到平滑渐变的反射效果。
实际上,Unity会在场景中放置一个默认的反射探针,用于存储场景中使用的Skybox的反射结果,作为场景的观景光照。

反射探针有3种类型:

[*]Baked
通过提前烘焙来得到该位置使用的Cubemap。游戏运行时,该Cubemap并不会发生变化。注意:这种类型的反射探针在烘焙时只处理那些静态物体(被标识Reflection Probe Static的物体)。

[*]Realtime
会实时更新当前的Cubemap,不受静态或动态物体的影响。缺点是需要花费更多的处理时间,性能消耗较大。

[*]Custom
既可以在编辑器中烘焙它,也可以使用一个自定义的Cubemap来作为反射映射,但自定义的Cubemap不会实时更新。

本案例中,场景中放了3个Bake类型的反射探针。有无反射探针的对比:



可以看到,有反射探针的盾牌看起来比无反射探针的盾牌多了一些夕阳的红色。

反射探针应该被放在那些具有明显反射现象的物体旁边,或是墙角等容易发生遮挡的物体周围。此外还需要为反射探针定义它们的影响区域。通常反射探针的影响区域会存在重叠,Unity会计算反射物体的包围盒与这些重叠区域的交叉部分,并据此来选择使用哪个反射探针。如果游戏运行平台使用的是SM 3.0及以上,Unity还会对这些相互重叠的反射探针进行混合,实现平滑的反射过渡效果。



反射探针的另一个好处——互相反射。这是传统的Cubemap无法达到的效果,这种效果的应用场景是两面互相面对面的镜子,只要反射光线没有被完全吸收,反射就会一直进行下去。这种效果需要追踪光线的反射轨迹,是传统反射方法无法实现的,Unity 5引入的GI系统可以让这种效果称为可能。

原书资源中Scene18_3_2就展现了这样的场景,两个金属反射的图像包含了两次互相反射的效果:



两个金属球的位置处各放置了一个反射探针,且每个金属球上的Mesh Renderer组件中的Reflection Probes设置为Simple,这样可以保证他们只会使用离他们最近的一个反射探针。默认情况下,反射探针只会步骤一次反射,也就是:左边金属球使用的反射只会步骤由右边金属球第一次反射过来的光线,但理想情况下,反射过来的光线会继续被左侧的金属球反射,并对右侧金属球造成影响。Unity允许我们在光源面板的Environment Reflection-Bounces选项中设置来回反射的次数。

使用反射探针需要更多的计算时间,其本质也是通过在它的位置上放置一个摄像机来渲染得到一个Cubemap。如果反弹次数设置得很大,或使用实时渲染,这些探针可能会造成性能瓶颈。

3. 调整材质

基于物理的渲染,并不意味着要模拟照片真实的效果,更多是为了不需要频繁地调整材质参数,在各种光照条件下得到令人满意的效果。
在Unity中,想要和全局光照、反射探针等内置功能良好地配合得到出色的渲染结果,就需要使用Unity内置的Standard Shader。本例中使用了更加复杂的纹理和模型,它们来自于Unity官方的实例项目Viking Village,这些材质可为我们只做自己的材质提供参考,如:场景中所有物体都使用了高光反射纹理(Specular Texture)、遮挡纹理(Occlussion Texture)、法线纹理(Normal Texture),一些材质还是用了细节纹理来提供更多的细节表现。

4. 线性空间

在使用基于物理的渲染时,我们应该使用线性空间(Linear Space)来得到更好的渲染效果。默认情况下Unity会使用伽马空间(Gamma Space),如果要使用线性空间的话,需要在Edit-Project Settings-Player-Other Settings-Color Space中选择Linear选项。

下图是线性空间和Gamma空间的效果对比,可以看出线性空间的渲染可以得到更加真实的效果(比如木桶上脏油的质感):



当线性空间的计算需要一些硬件支持,一些移动平台并不支持线性空间,这时职能退而求其次,选择伽马空间进行渲染和计算。

默认伽马空间进行渲染计算时,由于使用了非线性的输入数据,导致很多计算在非线性空间下进行的,这意味着得到的结果并不符合真实物理期望,此外输出时没有考虑显示器显示伽马的影响,会导致渲染出来的画面整体偏暗,和真实世界不像。

5. 答疑解惑

5.1 什么是全局光照

全局光照,指的是模拟光线是如何在场景中传播的,包括直接光照和间接光照。

在使用基于物理的着色技术时,当渲染表面上一点时,需要计算该点的半球范围内所有会反射到观察方向的入射光线的光照结果,这些入射光线就包含了直接光照和间接光照。

间接光照的计算非常耗时,传统方法是使用光线追踪,虽然它能得到非常出色的画面效果,常用在电影制作中,但它无法满足实时的要求。

Unity采用了Enlighten解决方案(已被集成在虚幻引擎中,用于很多3A大作的开发),使用实时+预计算的方法来模拟场景中的光照。实时光照用于计算直接光源对产经的影响,预计算光照包含常见的光照烘焙——把光源对场景中静态物体的光照效果提前烘焙到一张光照纹理中,然后把这张光照纹理直接贴在这些物体的表面,来得到光照效果。这些光照纹理存储了直接光照和由物体反射得到的间接光照,但它是静态的,无法再游戏运行时不断更新。

由于静态的光照烘焙无法在光照条件改变时更新物体的光照效果,Unity使用了预计算实时全局光照(Precomputed Realtime GI)技术动态地为场景实时更新复杂的光照结果。

该技术利用了一个事实:
一旦物体和光源的位置被固定了,这些物体对光线的反弹路径以及漫反射光照(假设漫反射光照在各个方向的部分是相同的)也是固定的,也就是:它和相机无关。

因此可以使用预计算方法把这些物体直接的关系提前计算出来。在实时运行时,只要光源位置不变,即便光源颜色、强度、物体材质属性(指漫反射和自发光相关的属性)发生变化,这些信息也是一直有效,无需实时更新。

预计算阶段,Enlighten会在由所有静态物体组成的场景上进行简化的“光线追踪”过程,此过程Enlighten会自动把场景分割成很多个子系统,为了得到场景中物体之间的关系。需要注意的是:这些预计算都是在静态物体上进行的,因此为了利用上述的预计算方法,场景中至少有一个物体需要被标识成Static(至少需要把Lightmap Static勾选上)。

一个例外:物体的高光反射是和相机的位置相关的。Unity的解决方案是使用反射探针,在实时运行时利用预计算得到的信息来计算光照信息,big把它们存储到额外的光照纹理、光照探针或Cubemap中,再和物体材质进行必要的光照计算,得到最后的渲染效果。

Unity的全局光照解决方案可大大提高一些基于PC/游戏主机等平台的大型游戏的画面质量,但如果在移动平台上使用,仍需要小心性能,一些低端手机不适合使用这种比较复杂的基于物理的渲染。

5.2 什么是伽马校正

Unity默认使用伽马空间,在伽马空间下进行渲染会导致很多非线性空间下的计算,从而引入一些误差。而吧伽马空间转换到线性空间,就需要进行伽马校正。

“伽马”一词来源于伽马曲线,其表达式如下:
Lout = Linγ

人们使用伽马曲线对拍摄的图像进行伽马编码(gamma encoding)。摄像机的原理可简化为:把进入到镜头内的光线亮度编码成图像中的像素,如果采集到亮度是0,像素就是0;亮度是1,像素就是1;亮度是0.5,像素就是0.5。如果只用8位空间来存储像素的每个通道,意味着0~1区间可以对应256种不同的亮度。但后来人们发现,人眼有一个有趣的特性:对光的灵敏度在不同亮度上不一样。在正常光照条件下,人眼对较暗区域的变化更加明暗。



一个生动的例子:一个屋子的光照由一盏灯增加到两盏灯,人对这种亮度变化的感知要远远强于从101盏灯增加到102盏灯的变化。

基于此,为了更了充分利用像素的存储,人们用更多的空间来存储更多暗部区域。摄像设备如果用了8位空间来存储照片,会使用约为0.45的编码伽马来对输入的亮度进行编码,得到一张编码后的图像,因此图像中0.5像素值对应的亮度并不是0.5,而大约为0.22。因为:0.5 ≈ 0.220.45。

伽马编码使得我们可以充分利用图像的存储空间,但当把图片放到显示器里显示时,我们应该对图像在进行一次解码操作,使得屏幕输出的亮度和捕捉到的亮度是符合线性的。这时人们发现了一个巧合——CRT显示器本身几乎已经自动做了这个界面操作。

这类设备的显示机制是,使用一个电压轰击屏幕上的一种图层,这个图层可以发亮,人们就可以看到图像。但CRT显示器有一个特性,它的输入电压和显示出来的亮度关系不是线性的,显示器的这个伽马曲线称为显示伽马(display gamma),非常巧合,CRT的显示伽马值大约就是编码伽马的倒数,这种特性正好补偿了图像捕捉设备的伽马曲线。

虽然现在CRT设备很少了,但人们仍在硬件上做了调整来提供这种兼容,随后sRGB颜色空间标准提出后,推荐显示器的显示伽马值为2.2,并配合0.45的编码伽马,就可以保证最后伽马曲线之间可以相互抵消(2.2 X 0.45 ≈ 1),绝大多数摄像机、PC和打印机都是用了这个sRGB标准。



游戏界长期以来都忽略了伽马矫正的问题,由于编码伽马和显示伽马的存在,我们一不小心就可能在非线性空间下进行计算,或是使得输出的图像是非线性的。

对于输出来说,直接输出渲染结果而不进行任何处理,在经过显示器的显示伽马处理后,会导致图像整体偏暗,出现失真的情况。

本书资源Scene_18_4_2_a显示了伽马对光照的影响,场景中放置了一个球体,环境光照设置为全黑,再把平行光的方向设置为从上方直接射到球体表面,球体使用的材质是内置的漫反射材质。
下面是球体在伽马空间和线性空间的对比,可以看出伽马空间下的渲染结果整体偏暗:


上图看起来可能会感觉伽马空间更加正确,然而实际此时屏幕输出的亮度和球面的光照结果并不是线性的。假设球面上有一点A,它的法线和光照方向成60度角,还有点B,它的法线和光线方向成90度,那么在Shader中计算漫反射光照时,我们会得出A的输出是(0.5, 0.5, 0.5),B的输出是(1.0, 1.0, 1.0)。上左图没有进行伽马矫正,因此由于显示器存在显示伽马就引入了非线性关系,也就是说A点的亮度并不是B亮度的一半,而是约为1/4;上右图使用了线性空间,Unity会在把像素写入颜色缓冲前进行一次伽马矫正,来抵消屏幕的显示伽马的作用,此时得到的屏幕亮度才是真正像素成正比的。

伽马的存在还会对混合造成影响。本书资源Scene_18_4_2_b中演示了3个互相重叠的圆,他们的材质均为简单的透明混合材质,并使用了一个边界模糊的圆作为输入纹理。它们在伽马空间和线性空间的混合效果对比:


在上左图中,可以看到绿色和红色的混合边界处出现了不正常的蓝色渐变,而上右图中从绿色到红色的渐变才是正确的混合结果。此外,上左图交叉的边界似乎都变暗了,这是因为混合后进行输出时,显示伽马导致接缝处颜色变暗。

实际上渲染中非线性输入最有可能的来源就是纹理。为了利用存储空间,大多数图像文件都进行了提前的校正,即:已经使用了一个编码伽马对像素值编码。而在使用多级渐远纹理(mipmaps)时也需要注意,如果纹理存储在非线性空间中,多级渐远纹理就会在非线性空间里计算,由于它的计算是一种线性计算(采样,对某个方形区域内的像素取平均值),这样会得到错误的结果。正确的做法是:把非线性的纹理转换到线性空间后再计算多级渐远纹理。

在游戏渲染中,我们需要保证所有的输入都被转化到线性空间下,并在线性空间下进行各种光照计算,最后在输出前通过一个编码伽马进行伽马矫正后再输出到颜色缓冲中。
Unity的颜色空间设置可以满足我们的需求,

[*]当选择伽马空间时
实际是“放任模式”,不会对Shader的输入进行任何处理,也不会对输出像素进行任何处理,这意味着最终输出的像素会经过显示器的显示伽马转换后得到非预期的亮度,通常表现为整个场景会比较昏暗;

[*]当选择线性空间时
Unity会把输入纹理设置为sRGB模式,这种模式下硬件在对纹理进行采样时会自动将其转换到线性空间中,且GPU在Shader写入颜色缓冲前自动进行伽马校正或是保持线性在后面进行伽马校正(取决于当前的渲染配置)。


[*]如果开启了HDR,渲染会使用一个浮点精度的缓冲,这些缓冲有足够的精度不需要进行任何伽马校正,此时所有的混合和屏幕后处理都是在线性空间下进行的。当渲染完成要写入显示设备的后备缓冲区时,再进行一次最后的伽马校正。
[*]如果没有使用HDR,Unity会把缓冲设置成sRGB格式,这种格式的缓冲像普通的纹理一样,在写入缓冲前进行伽马校正,在读取缓冲时需要再进行一次解码操作。如果此时开启了混合,每次混合式硬件会首先把之前的颜色缓冲中存储的颜色转换回线性空间中,然后再与当前的颜色进行混合,然后再进行伽马校正,最后把校正后的混合结果写入颜色缓冲中。注意:透明通道是不会参与伽马校正的。


Unity的线性空间并不是所有平台都支持的,此时需要自己在Shader中进行伽马校正。对非线性输入纹理的校正代码通常如下:
float3 diffuseColor = pow(tex2D(diffTex, texCoord), 2.2);
在最后的输出前,对输出像素值的校正代码通常如下:
fragColor.rgb = pow(fragColor.rgb, 1.0 / 2.2);
return fragColor;

手动对输出像素进行伽马校正会在使用混合时出现问题。因为校正会导致写入颜色缓冲内的颜色是非线性的,这样混合就发生在非线性空间中。
一种解决方法:
在中间计算时不要对输出颜色进行伽马校正,但在最后需要进行一次屏幕后处理操作来对最后的颜色进行伽马校正,即:保证伽马校正发生在渲染的最后一步,但这可能会造成一定的性能损耗。

5.3 什么是HDR

HDR是High Dynamic Range的缩写,即:高动态范围,与之相对的是低动态范围(Low Dynamic Range,LDR)。通俗讲,动态范围是最高和最低的亮度值之间的比值。真实世界中,一个场景中最亮和最暗区域的范围可以非常大,如:太阳发出的光可能要比场景中某个影子上的点的亮度要高出几万倍,这个范围远远超过图像或显示器能够显示的范围。

通常在显示设备使用的颜色缓冲中每个通道的精度为8位,一位置我们只能用这256中不同的亮度来表示真实世界中的所有亮度,这一过程会存在一定的精度损失。HDR使用远高于8位的精度(如:32位)来记录亮度信息,尽管最后需要把信息转换到显示设备使用的LDR内,但中间的计算却是更加真实可信的。

Nvidia曾总结HDR进行渲染的动机:让亮的物体可以真的非常亮,暗的物体可以真的非常暗,同时又可以看到两者之间的细节。

使用HDR来存储的图像被称为高动态范围图像(HDRI)。HDR可以更加真实地反映亮暗的对比,还对光照叠加也有非常重要的作用。如果场景中有多个光源或是光源强度很大,那么一个物体经过多次光照渲染叠加后最终得到的光照亮度很可能超过1。如果没有开启HDR,超过1的部分全部会截取到1,使得场景丢失了很多亮部区域的细节。但如果开启了HDR,就可以保留这些超过范围的光照结果,尽管最后会被转换到LDR进行显示,但我们可以使用色调映射(tonemapping)技术来控制这个转换的过程,从而允许最大限度地保留需要的亮度细节。

HDR的使用允许我们在屏幕后处理中拥有更多的控制权,如:使用HDR和Bloom效果。

HDR优点:

[*]不会丢失高亮度区域的颜色值
[*]提供更真实的光照效果
[*]为一些屏幕后处理提供了更多的控制能力
HDR缺点:

[*]浮点缓冲来存储高精度图像,需要更大的显存空间,渲染速度会变慢
[*]一些硬件不支持HDR
[*]使用了HDR,无法再利用硬件的抗锯齿功能(如果在Unity中同时打开硬件的抗锯齿和摄像机的HDR,Unity会发出警告,此时可以使用基于屏幕后处理的抗锯齿操作来弥补)

在Camera组件面板中打开HDR选项即可,此时场景会被渲染到一个HDR的图像缓冲中,这个缓冲的精度范围可以远远超过0~1,最后再使用一个色调映射的屏幕后处理脚本把HDR图像转换到LDR图像中进行显示。

5.4 PBS适合什么样的游戏

再次强调:
PBS并不意味着游戏画面需要追求和照片一样真实的效果。

PBS优点:

[*]只需要一个万能的Shader就可以渲染相当一大部分类型的材质,无需每种材质写一个特定的Shader;
[*]保证在各种光照条件下,材质可以自然地和光源进行交互,无需反复调整材质参数;
PBS缺点:

[*]需要更复杂的光照配合,如:光照探针、反射探针等;
[*]需要开启HDR和一些必不可少的屏幕特效,如:抗锯齿、Bloom、色调映射,性能消耗相当大;
[*]对美工人员同样是个挑战,美术资源的制作过程和传统的Shader有很大不同,普通的法线纹理+高光反射纹理的组合不再使用,需要创建更细腻复杂的纹理集,包括:金属值纹理、高光反射纹理、粗糙度纹理、遮挡纹理。有些还需要使用额外的细节纹理给材质添加更多的细节表面。
[*]除使用图片扫描的传统辅助方法外,这些纹理的制作通常需要更专业的工具来绘制,如:Allegorithmic Substance Painter、Quixel Suite。

以上是本系列笔记的所有内容,感兴趣的小伙伴可以阅读原书学习更细节的知识!完结撒花!

<hr/>
写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页: [1]
查看完整版本: 《Unity Shader入门精要》笔记(三十七 完结)