本文重点:
支持实时光全局光照 自发光对全局光照的贡献动画化 和LPPVs一起生效(light probe proxy volumes) 使用LOD组和全局光照相结合 不同级别的LOD之间淡入淡出
这是关于渲染的系列教程的第18部分。第17部分中总结了烘焙的全局照明之后,我们将继续支持实时GI。之后,我们还将支持光探针代理体积(LPPVs)和LOD组的淡入淡出。
从现在开始,本教程系列使用Unity 2017.1.0f3制作的。它不适用于旧版本,因为我们最终会使用新的着色器功能。
(静态LOD组和实时GI的组合)
1 实时全局光照
得益于光探针的原理,烘焙光对于静态几何体非常友好,对于动态几何体也非常适用。但是,它不能处理动态光。混合模式下的光源可以进行一些实时调整,但是太多的物体因为烘焙的间接光源,需要保持不变是显而易见的。因此,当你有户外场景时,太阳必须保持不变。它不能像现实生活中那样穿越天空,因为那样需要逐渐改变GI。
为了使间接照明能够与移动的太阳等物体一起工作,Unity使用Enlighten系统可以计算实时全局照明。除了在运行时计算光照贴图和探针之外,它的工作方式类似于烘焙间接照明。
找出间接光需要了解光如何在静态表面之间反射。问题是哪些表面可能会受到其他表面的影响,以及受到何种程度的影响。弄清这些关系是很复杂的工作,不能实时完成。因此,该数据由编辑器处理并存储以供运行时使用。然后,Enlighten使用它来计算实时光照贴图和探针数据。即使这样,它也只适用于低分辨率的光照贴图。
1.1 开启实时全局光照
可以独立于烘焙照明而启用实时全局照明。可以同时都没有,任意一个或两个都处于活动状态。通过“Lighting”窗口的“Realtime Lighting”部分中的复选框启用该功能。
(实时和烘焙的GI都启用)
要查看实时GI的实际效果,请将测试场景中的主光源模式设置为实时。由于我们没有其他光源,因此即使已启用,它也可以有效地关闭烘焙光源。
(主光源设置为实时)
确保场景中的所有对象都使用我们的白色材质。与上次一样,这些球都是动态的,其他所有都是静态几何体。
(只有动态物体接受实时GI)
事实证明,只有动态对象才能从实时GI中受益。静态对象变暗了。那是因为光探针会自动包含实时GI。静态对象必须采样实时光照贴图,该实时光照贴图与烘焙的光照贴图不同。我们的着色器尚未执行此操作。
1.2 烘焙实时GI
在编辑模式下,Unity已经生成了实时光照贴图,因此你始终可以看到实时GI贡献。在编辑和播放模式之间切换时,这些贴图不会保留,但最终会保持不变。你可以通过“Lighting”窗口的“Object maps”选项卡检查实时光照贴图,并选择静态贴图对象。选择“Realtime Intensity”可视化以查看实时光照贴图数据。
(实时光照贴图,已选择顶部)
尽管实时光照贴图已经烘焙,并且可能看起来正确,但是我们的meta pass实际上使用了错误的坐标。实时GI具有自己的光照贴图坐标,最终可能与静态光照贴图的坐标不同。Unity根据光照贴图和对象设置自动生成这些坐标。它们存储在第三个网格UV通道中。因此,将此数据添加到“My Lightmapping”中的VertexData。
现在,MyLightmappingVertexProgram必须使用第二或第三UV集,以及静态或动态光照贴图的比例和偏移。可以依靠UnityMetaVertexPosition函数来使用正确的数据。
UnityMetaVertexPosition是什么样的?
除了使用通过unity_MetaVertexControl提供的标志来确定要使用的坐标集和光照贴图外,它的工作与我们以前的操作相同。
请注意,meta pass用于烘焙和实时光照贴图。因此,当使用实时GI时,它也将包含在构建中。
1.3 采样实时光照贴图
要实际采样实时光照贴图,我们还必须将第三个UV集添加到“My Lighting”中的VertexData。
使用实时光照贴图时,我们必须将其光照贴图坐标添加到插值器中。标准着色器将两个光照贴图坐标集组合在一个内插器中(与其他一些数据复用),但是我们可以为这两者使用单独的内插器。我们知道定义DYNAMICLIGHTMAP_ON关键字时就有动态的灯光数据。它是multi_compile_fwdbase编译器指令的关键字列表的一部分。
除了可以通过unity_DynamicLightmapST使用的动态光照贴图的缩放和偏移之外,还可以像静态光照贴图坐标一样填充坐标。
在我们的CreateIndirectLight函数中完成对实时光照贴图的采样。复制#if defined(LIGHTMAP_ON)代码块并进行一些更改。首先,新块基于DYNAMICLIGHTMAP_ON关键字。另外,它应该使用DecodeRealtimeLightmap而不是DecodeLightmap,因为实时贴图使用不同的颜色格式。由于此数据可能会添加到烘焙的照明中,因此请不要立即将其分配给indirectLight.diffuse,而应使用最后添加到其中的中间变量。最后,仅在不使用烘焙光照图或实时光照图的情况下,才应采样球谐函数。
(实时GI 应用在所有物体上了)
现在,我们的着色器将使用实时光照贴图。最初,当使用距离阴影遮罩模式时,它的外观可能与带有混合光的烘焙照明相同。在播放模式下关闭灯光时,区别变得明显。
(禁用混合光后,仍会保留间接光)
禁用混合光后,其间接光将保留。相比之下,实时光的间接贡献会消失并重新出现。但是,可能需要一小段时间才能完全解决新情况。Enlighten需要逐步调整光照贴图和探针。这种情况发生的速度取决于场景的复杂性和“Realtime Global Illumination CPU ” CPU质量等级设置。
(在实时GI模式下 开关实时光)
所有实时照明均有助于实时GI。但是,它的典型用法是仅使用主方向光,代表太阳在天空中移动时的太阳。它完全适用于定向光。但点光源和聚光灯也可以工作,只是没有阴影。因此,当使用阴影点光源或聚光灯时,你可能会得到不正确的间接照明。
(实时聚光灯和不带阴影的间接光)
如果要从实时GI中排除实时光,可以通过将其间接强度(Indirect Multiplier)设置为零来实现。
1.4 自发光
实时GI也可以用于发光的静态对象。这使得可以通过匹配的实时间接光来改变其发射。我们来试一下。在场景中添加一个静态球体,并为其提供一种材质,该材质使用具有黑色反照率和白色自发光颜色的着色器。最初,我们只能通过静态光照贴图看到自发光的间接影响。
(带有自发光的球体的烘焙GI)
为了将自发光烘焙到静态光照贴图中,必须在着色器GUI中设置材质的全局照明标记。正如我们始终将标志设置为BakedEmissive一样,光最终在烘焙的光照贴图中结束。当发射光恒定时这很好,不应该允许对其进行动画处理。
为了同时支持烘烤照明和实时照明,必须使其可配置。为此,可以通过MaterialEditor.LightmapEmissionProperty方法在MyLightingShaderGUI中添加一个选项。它的单个参数是属性的缩进级别。
每当自发光属性发生变化时,还必须停止覆盖这些标志。实际上,这要复杂得多。标志选项之一是EmissiveIsBlack,它表示可以跳过自发光的计算。始终为新材质设置此标志。为了使间接发射起作用,无论我们选择实时还是烘焙的时候,还需要确保此标志未设置。可以通过始终屏蔽flags值的EmissiveIsBlack位来实现。
(实时GI,带有自发光球体)
烘焙GI和实时GI的视觉区别在于,实时光照贴图通常具有比烘焙GI更低的分辨率。因此,当自发光属性没有变化并且无论如何都在使用烘焙的GI时,请确保使用更高的分辨率。
EmissiveIsBlack的目的是什么?
这是一项优化,可以跳过部分GI烘焙过程。但是,它仅在发光颜色确实为黑色时才依赖设置标志。由于标志是由着色器GUI设置的,因此这是在通过检查器编辑材质时确定的。至少,这就是Unity的标准着色器的工作方式。因此,如果以后通过脚本或动画系统更改发光颜色,则不会调整该标志。这是导致许多人不了解为什么自发光属性动画化,不会影响实时GI的原因。结果是,如果要在运行时更改自发光颜色,则通常不将自发光颜色设置为纯黑色。
我们没有使用这种方法,而是使用LightmapEmissionProperty,它还提供了完全关闭GI进行自发光的选项。因此,该选择对于用户是明确的,没有任何隐藏的行为。不使用自发光的话, 确保其GI设置为“None”。
1.5 动画自发光属性
用于自发光的实时GI仅适用于静态对象。当对象是静态的时,其材质的emission属性可以设置为动画,并由全局照明系统拾取。我们用一个在白色和黑色的emission颜色之间振荡的简单组件来尝试一下。
将此组件添加到我们的自发光球体上。在播放模式下,其发光将进行动画处理,但间接光尚未受到影响。必须通知实时GI系统它有工作要做。这可以通过调用适当的网格渲染器的Renderer.UpdateGIMaterials方法来完成。
(带有动画自发光球体的实时GI)
调用UpdateGIMaterials会触发对象自发光属性的完整更新,并使用meta pass 对其进行渲染。当自发光比纯色更复杂时,例如在使用纹理的情况下,这是必需的。如果纯色足够了的话,我们可以通过使用渲染器和发光色调用DynamicGI.SetEmissive来走个捷径。这比使用meta pass渲染对象要快,因此请在可能的时候使用它。
2 光探针代理体积(LPPVs)
烘焙GI和实时GI都通过光探针应用于动态对象。对象的位置用于内插值光探针数据,然后用于应用GI。这适用于比较小的对象,但对于较大的对象而言过于粗糙。
例如,在测试场景中添加一个很长的立方体,以使其适应变化的光照条件。它使用我们的白色材质。由于它是一个立方体,因此最终只能使用一个点来确定其GI贡献。对其进行定位,以使该点最终变为阴影,整个立方体会变暗,这显然是错误的。要使其变的更加明显,请使烘焙的主光源,因此所有照明均来自烘焙的和实时GI数据。
(大的动态物体,使用较差的光源)
为了使光探针能够在这种情况下工作,可以使用光探针代理体积或简称LPPV。通过向着色器提供一个插值探针值的网格(而不是单个值)来工作。这需要具有线性过滤的浮点3D纹理,这会把它限制为现代的显卡。除此之外,还要确保在图形层设置中启用了LPPV支持。
2.1 将LPPV添加到对象
LPPV可以通过多种方式设置,最直接的方法就是将其用在使用它的对象的组件。可以通过“Component/ Rendering / Light Probe Proxy Volume”添加它。
(LPPV 组件)
LPPV通过在运行时在光探测器之间进行插值来工作,就像它们是常规动态对象的网格一样。内插的值被缓存,并以“Refresh Mode”控制它们的更新时间。默认值为“Automatic”,这意味着更新会在动态GI更改以及探针组移动时发生。“Bounding Box Mode”控制体积的放置方式。“Automatic Local”表示它适合与其连接的对象的边界框。这些默认设置适用于我们的立方体,因此我们将保留它们。
要使我们的立方体实际使用LPPV,必须将其网格渲染器的“Light Probes”模式设置为“Use Proxy Volume”。默认行为是使用对象本身的LPPV组件,但是你也可以强制其使用另一个体积。
(使用代理体积代替常规探针)
自动分辨率模式不适用于我们的拉长的立方体。因此,将“Resolution Mode”设置为“Custom”,并确保在立方体角处有采样点,并且在其长边上有多个采样点。选择对象后,可以看到这些采样点。
(自定义探针分辨率以适合拉长的立方体)
2.2 采样代理体
立方体变成黑色,因为我们的着色器尚不支持LPPV采样。为了使其工作,我们必须在CreateIndirectLight函数中调整球谐函数代码。使用LPPV时,UNITY_LIGHT_PROBE_PROXY_VOLUME被定义为1。在这种情况下,我们什么也不做,看看会发生什么。
(没有球谐函数影响了)
事实证明,对于不使用LPPV的动态对象,所有球形谐波都被禁用。这是因为UNITY_LIGHT_PROBE_PROXY_VOLUME是在项目范围内定义的,而不是在每个对象实例中定义的。单个对象是否使用LPPV由unity_ProbeVolumeParams的X组件指示,该组件在UnityShaderVariables中定义。如果将其设置为1,则我们有一个LPPV,否则我们应该使用规则的球谐函数。
要采样体积,可以使用SHEvalLinearL0L1_SampleProbeVolume函数而不是ShadeSH9。此功能在UnityCG中定义,并且需要将世界位置作为附加参数。
SHEvalLinearL0L1_SampleProbeVolume如何工作?
顾名思义,该函数仅包括前两个球谐频带L0和L1。Unity LPPV不使用第三频段。因此,我们得到的照明质量较低,但是我们在多个世界空间样本之间进行插值,而不是使用单个点。下面是代码。
(LPPV采样,在gamma空间中太暗)
现在,我们的着色器可以在需要时对LPPV进行采样,但是结果太暗了。至少在gamma色彩空间中工作时就是这种情况。这是因为球谐数据存储在线性空间中。因此,可能需要进行颜色转换。
(采样LPPV,现在有正确的颜色了)
3 LOD组件
当对象最终仅覆盖应用程序窗口的一小部分时,你不需要高度详细的网格即可对其进行渲染。可以根据对象的视图大小使用不同的网格。这称为细节级别(level of detail),或简称LOD。Unity允许我们通过LOD Group组件执行此操作。
3.1 创建一个LOD层次
这个想法是在不同的LOD使用同一版本的同一个网格。最高级别– LOD 0 –具有最多的顶点,子对象,动画,复杂的材质等。附加的级别变得越来越简单,渲染起来也更便宜。理想情况下,设计相邻的LOD级别,以便当Unity从一个切换到另一个时,不容易分辨出它们之间的区别。否则,突然的变化将是显而易见的。但是在研究这项技术时,我们将使用明显不同的网格。
创建一个空的游戏对象,并给它两个子节点。第一个是标准球体,第二个是标准立方体,其比例尺统一设置为0.75。结果看起来像预期的那样,并且重叠了球体和立方体。
(球体和立方体看起来像一个物体)
通过Component/ Rendering / LOD Group将LOD组组件添加到父对象。你将获得具有默认设置的LOD组,该组具有三个LOD级别。百分比指的是对象的边界框所覆盖的窗口的垂直部分。因此,默认设置是在垂直尺寸下降到窗口高度的60%时切换到LOD 1,在减小到30%时切换到LOD 2。当达到10%时,根本不会渲染。你可以通过拖动LOD框的边缘来更改这些阈值。
(LOD组 组件)
阈值由LOD偏差修改,该偏差由组件的检视器提供。这是默认设置为2的质量设置,这意味着将阈值有效地减半。也可以设置最大LOD级别,从而导致跳过最高级别。
为了使其正常工作,你必须告诉组件每个LOD级别使用哪些对象。通过选择LOD块并将对象添加到其“Renderers ”列表中,可以完成此操作。尽管你可以在场景中添加任何对象,但是请确保添加其子对象。将球形用于LOD 0,将立方体用于LOD1。我们将LOD 2保留为空,因此实际上只有两个LOD级别。如果需要,可以通过右键单击上下文菜单删除和插入LOD级别。
(使用球体子节点 当做LOD0)
配置LOD级别后,你可以通过移动摄像机来查看它们的运行情况。如果对象最终足够大,它将使用球体,否则将使用立方体或根本不会渲染。
(LOD 转换效果)
3.2 烘焙GUI和LOD组
因为LOD组的呈现方式取决于其视图大小,所以它们自然是动态的。但其实,你仍然可以将它们设为静态。对整个对象层次结构执行此操作,因此对根及其两个子级都进行此操作。然后将主光设置为烘焙再查看会发生什么。
(使用烘焙光)
烘焙静态光照贴图时似乎使用了LOD 0。即使当LOD组切换到立方体或剔除时,最终总是看到球体的阴影。但请注意,立方体也使用静态光照贴图。所以它是不是没有使用光探针?调整光探头组试试。
(烘焙光,没有光探针)
禁用探针组会使立方体变暗。这意味着它们不再接收间接光。发生这种情况是因为在烘焙过程中确定间接光时使用了LOD 0。要找到其他LOD级别的间接光,Unity最好的办法是依靠烘焙的光探针。因此,即使我们在运行时不需要光探针数据,也需要它来为立方体释放间接光。
3.3 实时光GUI和LOD组
仅使用实时GI时,方法类似,不同之处在于,我们的立方体现在在运行时使用光探针。可以通过选择球体或立方体来验证这一点。选择立方体后,可以看到显示使用了哪些光探针的小控件。球体不显示它们,因为它使用了动态光照贴图。
(实时光GI LOD1 使用探针)
同时使用烘焙GI和实时GI会变得更加复杂。在这种情况下,立方体应对烘焙的GI使用光照贴图,对实时GI使用光探针。不幸的是,这是不可能的,因为不能同时使用光照贴图和球谐函数。这是一个或另一个的选择。由于光照图数据可用于立方体,因此Unity最终使用了该数据。最后,该立方体不受实时GI的影响。
(LOD 1仅使用低强度主光源的烘焙照明)
一个重要的细节是LOD级别的烘焙和渲染是完全独立的。他们不需要使用相同的设置。如果实时GI最终比烘焙的GI更重要,则可以通过确保立方体不是光照贴图静态的同时使球保持静态来强制立方体使用光探针。
(LOD1 强制使用光探针)
3.4 LOD不同级别之间的淡入淡出
LOD组的缺点是,当LOD级别更改时,它在视觉上很明显。几何突然出现,消失或改变形状。可以通过将相邻LOD级别之间的交叉淡入淡出来缓解这种情况,这可以通过将组的“Fade Mode”设置为“Cross Fade”来实现。Unity还为SpeedTree对象使用了另一种淡入模式,我们将不使用它。
启用“Cross Fade”后,每个LOD级别都会显示“Fade Transition Width”字段,该字段控制其块的哪一部分用于淡入淡出。例如,当设置为0.5时,LOD范围的一半将用于淡入下一级。或者,可以对衰落进行动画处理,在这种情况下,在LOD级别之间转换大约需要半秒钟。
(Cross-fade的transition width设置为0.5)
启用“Cross Fade”后,当组在它们之间转换时,会同时渲染两个LOD级别。
3.5 支持交叉淡化
默认情况下,Unity的标准着色器不支持交叉淡化。需要复制标准着色器,并为LOD_FADE_CROSSFADE关键字添加一个多编译指令。我们也需要添加该指令以支持My First Lighting Shader的交叉渐变。将指令添加到除meta pass之外的所有pass中。
我们将使用抖动在LOD级别之间进行转换。该方法适用于正向和延迟渲染以及阴影。
在创建半透明阴影时,我们已经使用了抖动处理。它需要片段的屏幕空间坐标,这迫使我们对顶点和片段程序使用不同的插值器结构。因此,让我们在“My Lighting”中也复制Interpolators结构,将其中一个重命名为InterpolatorsVertex。
当我们需要淡入淡出时,片段程序的插值器必须包含vpos,否则我们保持通常的位置。
可以在片段程序开始时使用UnityApplyDitherCrossFade函数执行交叉淡化。
UnityApplyDitherCrossFade如何工作?
该功能在UnityCG中定义。它的方法与我们在“第12章 半透明阴影”中使用的抖动相似,不同之处在于,整个对象的抖动级别是统一的。因此,不需要在抖动级别之间进行混合。它使用存储在4×64 2D纹理中而不是4×4×16 3D纹理中的16个抖动级别。
unity_LODFade变量在UnityShaderVariables中定义。它的Y分量以十六个步骤包含对象的淡入量。
(通过抖动进行交叉淡化)
交叉淡化现在适用于几何体了。为了使它也适用于阴影,我们必须调整“My Shadows”。首先,在进行交叉淡入淡出时必须使用vpos。其次,我们还必须在片段程序开始时使用UnityApplyDitherCrossFade。
(交叉淡化几何图形和阴影)
由于立方体和球体相交,因此在它们之间相互淡入淡出时会产生一些奇怪的自阴影。方便地看到阴影之间的交叉渐变有效,但是在为实际游戏创建LOD几何图形时,必须注意此类失真现象。
下一章,介绍GPU实例化。