第十七章 基于物理的渲染(3)
一个更加复杂的例子
我们将讲解一个更加复杂的、基于物理渲染的场景
1.设置光照环境
我们首先要为场景设置光照环境。在默认情况下,Unity 5中一个新创建的场景会包含一个默认的Skybox。在本例中我们使用一个自定义的Skybox来代替默认值。做法是,打开Window->Lighting,在Scene标签下把本例使用的SunsetSkyboxHDR拖拽到Skybox选项中,如下图所示:
本例中的Skybox使用了一个HDR格式的Cubemap,这与我们之前制作Skybox时使用的纹理不同。这需要解释HDR(High Dynamic Range)的相关知识。我们会在后面更加详细的介绍HDR的原理和应用。但在这里,我们需要只知道,使用HDR格式的Skybox可以让场景中的物体的反射更加真实,有利于我们得到更加可信的的光照效果。
我们还可以设置场景中使用的环境光照,这些环境光照可以对场景中的所有的物体表面产生影响。在上图所示的设置面板中,我们可以选择光照的来源(Ambient Source选项),是来自与场景使用的Skybox,还是使用渐变值,亦或是某个固定的颜色。我们还可以设置环境光照的强度(Ambient Intensity参数),如果想要场景中的所有物体不接受任何环境光照,可以把该值设为0。在使用了Standard Shader的前提下,如果我们关闭场景中所有的光源,并把环境光照的强度设置为0,场景中的一些物体仍然可以接受一些光照,如下图的左图所示:
那么,这部分光照是从哪里来的呢?答案就是反射。默认的反射源(Reflection Source选项)是场景使用的Skybox。如果我们不想让场景中的物体接收任何默认的反射光照,可以把反射源设置为自定义(即Custom),并把自定义的Cubemap保留为空即可(另一种方式是直接把场景使用的Skybox设置为空),如上图右图所示。但为了得到更加逼真的渲染结果,我们通常是不会这样做的。在渲染实现上,即便场景中没有任何光源,Unity在内部仍然会调用ForwardBase Pass(假设使用的是前向渲染路径的话),并使用反射的光照信息来填充光源信息,再进行基于物理的渲染计算。读者可以通过帧调试器(Frame Debugger)来查看渲染过程。需要注意的是,这里设置的反射源是默认的反射源,如果我们在场景中添加了其他的反射探针(Reflection Probes),物体可能会使用其他反射源。当默认反射源是Skybox时,Unity会由场景使用的Skybox生成一个Cubemap,我们可以通过Resolution选项来控制它每个面的分辨率。
除了Standard Shader外,Unity还引入了一个重要的流水线——实时全局光照(Global Illumination,GI)流水线。使用GI,场景中的物体不仅可以受直接光照的影响,还可以受间接光照的影响。直接光指的是那些直接把光照射到物体表面的光源,在本书之前的章节中,我们使用的都是直接光照来渲染场景中的物体。但在现实生活中,物体还会受间接光照的影响。例如,想象一个红色墙壁旁边放置了一个球体,尽管墙壁本身不发光,但球体靠近墙的一面仍会有少许的红色,这是由于红色墙壁把一些间接光照投射到了球体上。在Unity中,间接光照指的就是那些被场景中其它物体反弹的光,这些间接光照会受反弹光的表面颜色的影响(例如之前例子中的红色的墙壁),这些表面会在反弹光线时把自身表面的颜色添加到反射光的计算中。在Unity 5中,我们可以使用这些直接光照和间接光照来创建更加真实的视觉效果。
下面,我们首先设置场景使用的直接光照——一个平行光。在PBR(Physically Based Rendering)中,想要让渲染效果更加真实可信,我们需要保证平行光的方向进而Skybox中的太阳或其他光源的位置一致,是的物体产生的光照信息可以与Skybox互相吻合。有时我们可能会使用一张耀斑纹理(Flare Texture)来模拟太阳等光源,此时我们同样需要确保平行光的颜色和耀斑纹理的位置相一致。与之类似的还有平行光的颜色,我们应该尽量让平行光的颜色和场景环境相匹配。我们还在Skybox的材质面板上调整天空的旋转角及曝光度,来调整场景的背景。
在平行光面板的烘焙选项(即Baking)中,我们选择了Realtime模式,这意味着场景中受平行光影响的所有物体都会进行实时的光照计算,当光源或场景中其他物体的位置、旋转角度等发生变化时,场景中的光照结果也会随之发生变化。然而。实时光照往往需要较大的性能消耗,对于移动平台这样这样资源比较紧缺的平台,我们可以选择Baked模式,此时Unity会把该光源的光照效果烘焙到一张光照纹理(lightmap)中,这样我们就不用实时为物体计算复杂的光照,而只需要进行纹理采样来得到光照结果。选择烘焙模式的缺点在于,如果场景中的物体发生了移动,但是它的阴影等光照效果并不会发生变化。烘焙选项中的Mix模式则允许我们混合使用实时模式和烘焙模式,它会把场景中的静态物体(即那些被标识为Static的物体)的光照烘焙到光照纹理中,但仍然会对动态物体产生实时光照。
Unity 5引入了实时间接光照的功能,在这个系统下,场景中的直接光照会在场景中各个物体之间来回反射,产生间接光照。正如我们之前讲到的,间接光照可以让那些没有直接被光源照亮的物体同样可以接受到一定的光照信息,这些光照是由它周围的物体反射到它表面上的。当一条光线从光源被发射出来后,它会与场景中的一些物体相交,第一个和光线相交的物体受到的光照即为直接光照。当得到直接光照在该物体上的光照结果后,该物体还会继续反射该光线,从而对其他物体产生间接光照。此后与该光线相交的物体,就会受到间接光照的影响,同时它们也会继续反射。当经过多次反射后,该光线最后完全消失。这些间接光照的强度是由GI系统计算得到的默认亮度值。下图所示的光源面板中的Bounce Intensity参数可以让我们调节这些间接光照的强度。
当我们把它设置为0时,意味着一条光线仅会和一个物体相交,不再被继续反射。也就是说场景中的物体只会受到直接光照的影响。下图显示了Bounce Intensity分别是0和8时,场景的渲染结果,注意其中阴影部分的细节。除了上述调整单个光源的间接光照强度,我们也可以对整个场景的间接光照强度进行调整。这是按照前面的光照面板来实现的。在光照面板的Scene标签页下,我们可以调整Genereal GI参数块中的Bounce Boost参数来控制场景中反射的间接光照强度,它会和单个光源的Bounce Intensity参数来一起控制间接光照的反射强度。除此之外,把Indirect Intensity参数调大同样可以增大间接光照的强度。需要注意的是,间接光照还有可能来自一些自发光的物体。
2.放置反射探针
回忆我们在前面讲到的环境映射,在实时渲染中,我们经常会使用Cubemap来模拟物体的反射效果。例如在赛车游戏中,我们需要对车身或车窗使用反射映射技术来模拟它们的反光材质。然而,如果我们永远使用同一个Cubemap,那么当赛车周围的场景发生较大的变化时,就很容易出现“穿帮镜头”,因为车身或车窗的环境反射并没有随着环境变化而发生变化。一种解决办法是可以在脚本中控制何时生成从当前位置观察到的Cubemap,而Unity 5为我们提供了一种更加方便的途径,即使用发射探针(Reflection Probes)。反射探针的工作原理和光照探针(Light Probes)类似,它允许我们在场景的特定位置上对整个场景的环境反射进行采样,并把采样的结果存储在每个探针上。当游戏中包含反射效果的物体从这些探针附近经过时,Unity会把从这些临近探针存储的反射结果传递给物体使用的反射纹理。如果物体周围存在多个反射探针,Unity还会在这些反射结果之间进行插值,来得到平滑渐变的反射效果。实际上,Unity会在场景中放置一个默认的反射探针,这个反射探针存储了对场景使用的Skybox的反射结果,来作为场景的环境光照。如果我们需要让场景中的物体包含额外的反射效果,就需要放置更多的反射探针。
反射探针同样有3种类型:Baked,这种类型的反射探针是通过提前烘焙来得到该位置使用的Cubemap的,在游戏运行时反射探针中存储的Cubemap并不会发生变化。需要注意的是,这种类型的的反射探针在烘焙时同样只会处理那些静态物体(即那些被标识为Reflection Probe Static的物体);Realtime,这种类型则会实时更新当前的Cubemap,并且不受是静态物体还是动态物体的影响。当然这种类型的探针需要花费更多的处理时间,因此,在使用时应当非常小心它们的性能。幸运的是,Unity允许我们从脚本中通过触发来精确控制反射探针的更新;最后一种类型是Custom,这种类型的探针既可以让我们从编辑器中烘焙它,也可以让我们使用一个自定义的Cubemap来作为反射映射,但自定义的Cubemap不会被实时更新。
我们在本节使用的场景中放置了3个反射探针,它们的类型都是Baked(前提是我们把场景中的物体标识成了Static)。使用反射探针前后的对比效果如下图所示。
需要注意的是,在放置反射探针时,我们选取的位置并不是随意的。通常来说,反射探针应该被放置在那些具有明显反射现象的物体旁边,或是一些墙角等容易发生遮挡的物体周围。在本例使用的场景中,木屋内的盾牌具有比较明显的反射效果,而盾牌本身又被木屋遮挡,因此其中一个反射探针的位置就在盾牌附近。当我们放置好探针后,我们还需要为它们定义每个探针的影响区域,当反射物体进入到这个区域后,反射探针就会对这个物体产生影响。通常情况下,反射探针的影响区域之间会有所重叠,例如,本例中盾牌附近的反射探针和另外两个(一个在木屋前方,一个在木屋后方)的影响区域都有所重叠。此时,Unity会计算反射物体的包围盒与这些重叠区域的交叉部分,并据此来选择使用的反射映射。如果当前的目标平台使用的是SM3.0及以上的话,Unity还可以允许我们在这些相互重叠的反射探针之间进行混合,来实现平缓的反射过渡效果。
使用Unity内置的反射探针的另一个好处是,我们可以模拟互相反射(interreflections)。我们曾在前面讲过使用传统的Cubemap方法无法模拟互相反射的效果。例如,假设场景中有两面互相面对面的镜子,在理想情况下,它们不仅会反射自己对面的那面镜子,也会反射那面镜子里反射的图像。只要反射光线没有被完全吸收,反射就会一直进行下去。要想实现这种效果,就需要追中光线的反射轨迹,这是传统的反射方法无法实现的。Unity 5引入的GI系统让这种效果变成了可能。如下图所示,我们可以在图中看到,两个金属反射的图像包含了两次互相反射的效果。
在上图所示的场景中,我们在每个金属球的位置处放置了一个反射探针,并把每个金属球上的Mesh Renderer组件中的Reflection Probes设置为Simple,这样保证它们只会使用离它们最近的一个反射探针。默认情况下,反射探针只会捕捉一次反射,也就是说,左边金属球使用的反射探针只会捕捉由右边的金属球第一次反射过来的光线。但在理想情况下,反射过来的光线会继续被左边的金属球反射,并对右边的金属球产生影响。Unity允许我们控制物体之间这样来回反射的次数,我们可以改变Reflection Bounces参数来实现。在上图所示的场景中,我们把该值设为了2。
然而,正如本节一开始所提到的,使用反射探针往往会需要更多的计算时间。这些探针实际上也是通过在它的位置上放置一个摄像机,来渲染得到一个Cubemap。如果我们把反弹的次数设置的很大,或是使用实时渲染,那么这些探针很可能会造成性能瓶颈。
3.调整材质
要得到真实可信的渲染效果,我们需要为场景中的物体指定合适的材质。需要再次提醒读者的是,基于物理的渲染并不意味着一定要模拟照片的真实效果。基于物理的渲染更多好处在于,可以让我们的场景在各种光照条件下都能得到令人满意的效果,同时不需要频繁的调整材质参数。
在Unity中,要想和全局光照、反射探针等内置功能良好地配合来得到出色渲染结果,就需要使用Unity内置的Standard Shader。我们已经在前面学习了如何针对不同类别的物体来调整它们使用的材质属性。本例中,我们使用了更复杂的纹理和模型。例如场景中所有的物体都使用了高光反射纹理(Specular Texture)、遮挡文理(Occlusion Texture)、法线纹理(Normal Texture),一些材质还使用了细节纹理来提供更多的细节表现。
4.线性空间
在使用基于物理的渲染方法渲染整个场景时,我们应该使用线性空间(Linear Space)来得到最好的渲染效果。默认情况下,Unity会使用伽玛空间(Gamma Space),如果要使用线性空间的话,我们需要在Edit->Project Settings->Player->Other Settings->Color Space中选择Linear选项。下图显示了分别在线性空间和伽玛空间下的渲染结果。
从上图可以看出,使用线性空间可以得到更加真实的效果。但它的缺点在于,需要一些硬件支持来实现线性计算,但一些移动平台对它的支持并不那么好。这种情况下我们只能退而求其次,选择伽马空间进行渲染和计算。
那么,线性空间、伽马空间到底是什么意思呢?为什么线性空间可以得到更加真实的效果呢?这就需要介绍伽玛校正(Gamma Correction)的相关内容了。实际上,当我们在默认的伽玛空间下进行渲染计算时,由于使用了非线性的输入数据,导致很多计算都是在非线性空间下进行的,这意味着我们得到的结果并不符合真实的物理期望。除此之外,由于输出时没有考虑显示器的显示伽玛影响,会导致渲染出来的画面整体偏暗,总是和真实世界不像。
尽管在Unity中我们可以通过之前所说的步骤直接选择在线性空间进行渲染,Unity会在背后为我们照顾好一切,但理解伽玛校正的原理对我们理解渲染计算有很大帮助。