本文重点内容:
只烘焙间接光 混合烘焙和实时光阴影 处理代码变更和导致的BUG 支持减法照明
这是关于渲染的系列教程的第17部分。上次,我们通过光照贴图增加了对静态照明的支持。现在,我们将烘焙和实时照明的功能相结合。
本教程是使用Unity 5.6.0制作的。
(混合烘焙和实时光)
1 烘焙间接光
光照贴图使我们可以提前计算光照。这减少了GPU实时执行的工作量,但以纹理内存为代价。除此之外,它还增加了间接照明。但是,正如我们上次看到的那样,存在局限性。首先,镜面照明无法烘焙。其次,烘焙的光仅通过光探头影响动态物体。第三,烘焙光不会投射实时阴影。
你可以在下面的屏幕截图中看到完全实时照明和完全烘焙照明之间的区别。这是上一教程中的场景,除了我使所有球体都动态化并重新定位了一些。其他一切都是静态的。使用正向渲染路径。
(全实时光和全烘焙光)
我没有调整光探针,由于静态几何体较少,因此,它们的位置变得没有意义。现在所产生的探针照明有点偏离,这在使用时更容易注意到。
1.1 混合模式
间接照明是烘焙照明有,而实时照明的没有的东西,因为它需要光照贴图。由于间接光照可以为场景增加很多真实感,因此如果我们将其与实时光照结合起来,那就更好了。
这当然是可以的,但这也意味着阴影会变得更加昂贵。它要求将Mixed Lighting下的的Lighting Mode设置为Baked Indirect。
(混合光照 烘焙间接光)
在上一教程中,我们已经切换到了这种模式,但是那时我们仅使用完全烘焙的灯光。结果,混合照明的模式没有任何变化。要使用混合照明,必须将光源的“Mode”设置为“Mixed”。
(主光源的混合模式)
将主定向光转换为混合光后,将发生两件事。首先,Unity将再次烘焙光照贴图。但这次,它仅存储间接光照,因此生成的光照贴图会比以前更暗。
(Fully baked vs indirect-only 光照贴图)
其次,所有东西都会被照亮,就好像主光源设置为实时一样,只是表现有所不同。光照贴图用于将间接光添加到静态对象,而不是球谐函数或探针。动态对象仍将光探针用作间接光。
(混合光照 实时定向光 烘焙间接光)
我们不必更改着色器来支持此操作,因为前向base pass已将光照数据和主方向光照结合在一起。与往常一样,附加的灯光会从附加 pass里获得。使用延迟渲染路径时,主光源也会通过pass获得灯光。
可以在运行时调整混合灯吗?
是的,因为它们用于实时照明。但是,它们的烘焙数据是静态的。因此,你只能在运行时对灯进行一些小的调整,就像稍微改变其强度一样。更剧烈的变化将使烘焙和实时照明不再同步变得显而易见。
1.2 升级着色器
刚开始,一切似乎工作正常。但是,事实上,阴影衰减对于定向光不再起作用。阴影会被切除,通过大幅度减小阴影距离很容易观察到。
(着色器淡出 标准VS我们的)
Unity长期以来一直使用混合照明模式,但实际上在Unity 5中是不起作用的。Unity 5.6中才添加了新的混合照明模式,这就是我们现在正在使用的模式。添加此新模式后,更改了UNITY_LIGHT_ATTENUATION宏后面的代码。在使用完全烘焙或实时照明时,我们没有注意到这一点,但是我们必须更新代码以与混合照明的新方法一起使用。由于这是最近的重大变化,因此需要排查错误。
要更改的第一件事是不再使用SHADOW_COORDS宏来定义阴影坐标的插值器。必须使用新的UNITY_SHADOW_COORDS宏。
同样,TRANSFER_SHADOW应该替换为UNITY_TRANSFER_SHADOW。
但是,这会产生编译器错误,因为该宏需要附加参数。从Unity 5.6开始,仅将方向阴影的屏幕空间坐标放入插值器中。现在可以在片段程序中计算点光源和聚光灯的阴影坐标。新功能是在某些情况下将光照贴图坐标用于阴影遮罩,我们将在后面介绍。为此,必须为宏提供来自第二个UV通道的数据,其中包含光照贴图坐标。
这再次产生编译器错误。发生这种情况时,是因为在某些情况下UNITY_SHADOW_COORDS错误地创建了一个插值器,即使实际上并不需要它,而TRANSFER_SHADOW也不会初始化它,从而导致错误。此错误的版本为5.6.0,最高版本为5.6.2和2017.1.0 Beta。
该错误通常不会引起注意,因为Unity的标准着色器使用UNITY_INITIALIZE_OUTPUT宏完全初始化其插值器结构。因为我们没有使用该宏,所以发现了该错误。要解决此问题,请使用UNITY_INITIALIZE_OUTPUT宏来初始化我们的插值器。这样,我们的代码将在有无bug的情况下进行编译。
UNITY_INITIALIZE_OUTPUT是做什么的?
它只是把变量赋零,强制转换为适当的类型。如果已经做过了的话,则不执行任何操作。
我不想使用此宏,而只依赖显式分配,因为它隐藏了像我们刚遇到的错误一样的错误。
1.3 我们自己淡出阴影
现在,我们正确地使用了新的宏,但是主光源的阴影仍然没有像应有的那样淡出。事实证明,当同时使用定向阴影和光照贴图时,UNITY_LIGHT_ATTENUATION不会执行此衰减,混合模式主定向光就是这种情况。所以,我们必须手动进行。
为什么在这种情况下不让阴影消失?
UNITY_LIGHT_ATTENUATION宏曾经独立存在,但是自Unity 5.6起,它被假定与Unity的标准全局照明功能一起使用。我们没有使用相同的方法,因此它无法为我们工作。
进行此更改的唯一真实线索是AutoLight中的注释,该注释为“出于性能原因处理GI功能深处的阴影”。随着着色器编译器随意移动代码,这不会告诉我们任何信息。如果有这种特殊情况的充分理由,则很难找到,因为Unity的着色器代码很复杂。所以我不知道。
对于我们的延迟光照着色器,已经有执行阴影淡出的代码。将相关的代码片段从MyDeferredShading复制到My Lighting中的新函数。唯一真正的区别是我们必须从视图向量和视图矩阵构造viewZ。只需要Z分量,因此不需要执行完整的矩阵乘法。
使用UNITY_LIGHT_ATTENUATION之后必须完成此手动淡出。
但仅当UNITY_LIGHT_ATTENUATION决定跳过衰减时。在UnityShadowLibrary包含文件中定义HANDLE_SHADOWS_BLENDING_IN_GI时就是这种情况。因此,仅当定义了HANDLE_SHADOWS_BLENDING_IN_GI时,FadeShadows才应执行操作。
最后,我们的阴影像应有的那样淡出了。
2 使用阴影遮罩
间接照明的混合模式光非常昂贵。它们需要的工作量与实时照明一样多,此外还需要间接照明的光照贴图。与完全烘焙的灯光相比,最重要的是添加了实时阴影。幸运的是,结合实时阴影,有一种方法仍然可以将阴影烘焙到光照贴图中。要启用此功能,请将混合照明模式更改为“Shadowmask”。
(Shadowmask模式)
在此模式下,间接光照和混合光照的阴影衰减都存储在光照贴图中。阴影存储在单独的贴图中,称为阴影遮罩。仅使用主定向光时,所有照亮的光源将在阴影遮罩中显示为红色。之所以为红色,是因为阴影信息存储在纹理的R通道中。实际上,由于地图具有四个通道,因此最多可以存储四个灯光的阴影。
(烘焙了强度和阴影遮罩)
Unity创建阴影遮罩后,静态对象投射的阴影将消失。仅光探针仍会考虑它们。动态对象的阴影不受影响。
(没有烘焙阴影)
2.1 采样阴影遮罩
为了取回烘焙过的阴影,我们必须对阴影遮罩进行采样。Unity的宏已经对点光源和聚光灯执行了此操作,但是我们也必须将其包括在FadeShadows函数中。为此,可以使用UnityShadowLibrary中的UnitySampleBakedOcclusion函数。它需要光照贴图的UV坐标和世界位置作为参数。
UnitySampleBakedOcclusion是什么样的?
它使用光照贴图坐标对阴影蒙版进行采样,然后选择适当的通道。unity_OcclusionMaskSelector变量是一个矢量,其单个分量设置为1,与当前正在着色的光匹配。
该功能还处理了光探测器代理卷(LPPVs)的衰减,但是我们尚不支持这些功能,因此我删除了该代码。这就是为什么函数具有世界位置参数的原因。
使用阴影遮罩时,UnitySampleBakedOcclusion为我们提供了烘焙的阴影衰减,在所有其他情况下,仅提供1。现在,我们必须将其与已有的衰减结合起来,然后淡化阴影。UnityMixRealtimeAndBakedShadows函数为我们完成了所有这些工作。
UnityMixRealtimeAndBakedShadows如何工作?
它也是UnityShadowLibrary中的函数。它还处理了光探针代理卷(LPPVs)和其他一些与我们无关的极端情况,因此我将其删除。
如果没有动态阴影,则结果是烘焙衰减。这意味着动态对象没有阴影,而光照贴图的对象没有烘焙阴影。
不使用阴影遮罩时,它会执行与以前相同的淡化处理。否则,这取决于我们是否要进行阴影混合,这会在后面介绍。现在,它只是在实时衰减和烘焙衰减之间进行插值。
(实时和阴影遮罩产生的叠加阴影)
现在,我们可以在静态对象上同时获取实时阴影和烘焙阴影,并且它们可以正确融合。实时阴影仍会超出阴影距离逐渐消失,但烘焙的阴影不会消失。
(只有实时阴影淡化)
2.2 添加阴影遮罩到G-Buffer
阴影遮罩现在可以用于正向渲染,但是在它与延迟渲染路径一起使用之前,我们需要做一些事情。具体来说,我们必须在需要时将阴影遮罩信息添加为附加的G-Buffer。因此,在定义SHADOWS_SHADOWMASK时,将另一个缓冲区添加到我们的FragmentOutput结构中。
这是我们的第五个G缓冲区,数量很多,但并非所有平台都支持它。只有在有足够的渲染目标可用时,Unity才支持阴影遮罩,我们也应该这样做。
只需要将采样的阴影遮罩数据存储在G缓冲区中,因为此时我们不使用特定的光源。可以为此使用UnityGetRawBakedOcclusions函数。除了不选择其中一个通道外,它的工作方式与UnitySampleBakedOcclusion相同。
要在没有光照贴图的情况下进行编译,请在光照贴图坐标不可用时将其替换为0。
2.3 使用阴影遮罩 G-Buffer
这足以使我们的着色器与默认的延迟照明着色器一起使用。但是要使其与我们的自定义着色器一起使用,必须调整MyDeferredShading。第一步是为额外的G缓冲区添加一个变量。
在CreateLight中,即使当前光源没有实时阴影,我们现在也必须在阴影遮罩的情况下淡化阴影。
要正确地包含烘焙的阴影,请再次使用UnityMixRealtimeAndBakedShadows代替我们以前的衰减计算。
现在,我们也可以使用自定义的延迟照明着色器获得正确的烘焙阴影。除非最终使用我们的优化分支,否则会跳过阴影混合。使用阴影遮罩时,无法使用该快捷方式。
2.4 Distance Shadowmask 模式
阴影遮罩模式可以为静态对象提供良好的烘焙阴影,而动态对象则无法从中受益。动态对象只能接收实时阴影和光探测器数据。如果我们希望动态对象具有良好的阴影,则静态对象也必须投射实时阴影。这就是“Distance Shadowmask”混合照明模式的用途。
(Distance Shadowmask 模式)
为什么我没有距离遮罩选项?
在Unity 2017中,你使用的阴影遮罩模式是通过质量设置控制的。
使用距离阴影遮罩模式时,所有内容都使用实时阴影。乍一看,它似乎与“Baked Indirect”模式完全相同。
(每个物体都是实时光阴影)
但是,它仍然需要一个阴影遮罩。在此模式下,超出阴影距离会使用烘焙的阴影和光探针。因此,这是最昂贵的模式,等于是阴影距离内使用“Baked Indirect ”,而超过该距离使用“Shadowmask ”。
(实时在附近,阴影遮罩和探针距离较远)
2.5 多灯光
由于阴影遮罩具有四个通道,因此可以一次支持多达四个重叠的光。例如,以下是屏幕快照,其中包含场景的光照贴图以及其他三个聚光灯。我降低了主光源的强度,因此更容易看到聚光灯。
(4个灯,都是混合模式)
主方向光的阴影仍存储在R通道中。你还可以看到G和B通道中存储的聚光灯的阴影。最后一个聚光灯的阴影存储在A通道中,该通道不可见。
当光量不重叠时,它们可以使用相同的通道来存储其阴影数据。因此,你以根据需要拥有任意数量的混合灯。但是必须确保最多四个光量最终相互重叠。如果有太多混合光影响同一区域,则有些光会退回到完全烘焙的模式。为了说明这一点,下面是添加了一个聚光灯后的带有光照贴图的屏幕截图。其中之一变成了烘焙光,你可以在强度图中清楚地看到它。
(5个互相叠加的光,1个变为全烘焙)
2.6 支持多个遮罩的定向灯
不幸的是,事实证明,只有在最多包含一种混合模式定向光的情况下,阴影遮罩才能正常工作。对于其他定向光,阴影衰减会出错。至少在使用前向渲染路径时会这样。而 延迟渲染则效果很好。
(两个定向光下不正确的淡化效果)
Unity的标准着色器也存在此问题,至少在版本5.6.2和2017.1.0f1之前。然而,这不是光映射引擎的问题。而是对用于UNITY_LIGHT_ATTENUATION的新方法的疏忽。Unity使用通过UNITY_SHADOW_COORDS定义的阴影插值器来存储定向阴影的屏幕空间坐标,或存储具有阴影蒙版的其他光源的光照图坐标。
使用阴影遮罩的定向光源也需要光照贴图坐标。对于前向base pass,会包括这些坐标,因为将在需要时定义LIGHTMAP_ON。但是,LIGHTMAP_ON从未在附加pass中定义。这意味着附加定向光源将没有可用的光照贴图坐标。实际上,在这种情况下,UNITY_LIGHT_ATTENUATION仅使用0,从而导致错误的光照贴图采样。
因此,不能依靠UNITY_LIGHT_ATTENUATION获得使用遮罩的其他定向光。我们可以很容易地确定什么时候是这种情况。所有这些都假定我们实际上正在使用屏幕空间定向阴影,而在某些平台上并非如此。
接下来,当我们具有其他遮罩的定向阴影时,还必须包括光照贴图坐标。
在可用的光照贴图坐标下,我们可以再次使用FadeShadows函数执行自己的衰减。
但是,这仍然是不正确的,因为我们正在向它提供错误的衰减数据。必须绕过UNITY_LIGHT_ATTENUATION并仅获得烘焙衰减,这时,可以通过SHADOW_ATTENUATION宏进行此操作。
(在两个定向光下 正确的衰减)
完全依靠UNITY_LIGHT_ATTENUATION是个好主意吗?
宏代码已稳定很长时间了。一直以来,都是与Unity自定义着色器的照明设置配合使用的最佳方法。这在Unity 5.6.0中发生了变化,当时新的方法被强制为旧的宏结构。
Unity在2017.3中再次更改了附加照明的方法,因此支持了定向照明,但这给我们的解决方法和未来的照明工作带来了麻烦。快速解决方案是禁用解决方法。
不幸的是,Unity的最新方法是一种黑客攻击,它对剪辑空间位置的W坐标引入了新的依赖关系-任何地方都是1。这不适用于具有LOD交叉淡入淡出的所有组合,因此一个错误被另一错误替代。当我介绍新的可编写脚本的渲染管线时,我可能不会依赖UNITY_LIGHT_ATTENUATION。
3 阴影减法
混合照明是不错的选择,但它不如完全烘焙的照明便宜。如果你以低性能的硬件为目标,那么混合照明是不可行的。可以使用烘焙的照明,但是你可能确实需要让动态对象在静态对象上投射阴影。在这种情况下,可以使用Subtractive 混合照明模式。
(Subtractive 模式)
切换到减法模式后,场景会变亮很多。发生这种情况是因为静态对象现在同时使用完全烘焙的光照贴图和直接光照。像往常一样,动态对象仍然使用光探针和直接照明。
(静态对象会受光两次)
减法模式仅适用于正向渲染。使用延迟渲染路径时,相关对象将像透明对象一样回退到前向。
3.1 减法灯光
减法模式的想法是,静态对象通过光照贴图进行照明,同时还将动态阴影纳入其中。这是通过降低阴影区域中的光照图强度来完成的。为此,着色器需要访问光照贴图和实时阴影。它还需要使用实时光源来确定必须将光照贴图调暗多少。这就是为什么在切换到此模式后我们得到双重照明。
减光照明是一个近似值,仅适用于单个定向光。因此,仅支持主定向光的阴影。同样,我们必须以某种方式知道动态阴影区域中的间接光照情况。由于我们使用的是完全烘焙的光照贴图,因此没有此信息。Unity使用统一的颜色来近似环境光,而不是仅使用间接光来包括其他光照图。这是实时阴影颜色,你可以在混合照明部分中进行调整。
在着色器中,我们知道应该在定义LIGHTMAP_ON,SHADOWS_SCREEN和LIGHTMAP_SHADOW_MIXING关键字时使用减光照明,而没有定义SHADOWS_SHADOWMASK。让我们定义SUBTRACTIVE_LIGHTING,以使其更易于使用。
在做其他事情之前,我们必须解决双重照明。这可以通过关闭动态光来完成,就像我们对延迟通道一样。
(只有烘焙光影响静态物体)
3.2 阴影烘焙光
要应用减影阴影,我们创建一个在需要时调整间接光的函数。它通常不执行任何操作。
在我们获取了光照贴图数据之后,必须调用此函数。
如果有减法照明,那么我们必须获取阴影衰减。可以简单地从CreateLight复制代码。
接下来,我们必须弄清楚如果使用实时照明,将接收多少光。我们假定此信息与光照图中的内容匹配。由于光照贴图仅包含散射光,因此可以计算出定向光的Lambert项。
为了获得阴影的光强度,我们必须将Lambert项乘以衰减。但是已经有了完全没有阴影的烘焙光。因此,我们估算阴影遮挡了多少光。
通过从烘焙光中减去此估算值,我们得到了调整后的光。
(减少后的光)
无论环境光线如何,这始终会产生纯黑色阴影。为了更好地匹配场景,我们可以使用我们的subtractive shadow color,通过unity_ShadowColor来使用。阴影区域不应比该颜色更暗,但它们可以更亮。因此,请充分利用计算出的光线和阴影颜色的最大值。
我们还必须考虑将阴影强度设置为低于1的可能性。要应用阴影强度,请基于_LightShadowData的X分量在阴影和非阴影光之间进行插值。
(阴影颜色)
因为我们场景的环境强度设置为零,所以默认的阴影颜色与场景不太匹配。但这很容易发现减法阴影,所以我没有对其进行调整。另外一个明显的事是,阴影颜色现在会覆盖所有烘焙的阴影,这是不应该发生的。它只应影响接收动态阴影的区域,而不能使烘焙阴影变亮。要强制执行此操作,请使用subtractive 照明和烘焙照明中的最小值。
(适当的减掉阴影)
现在,只要我们使用适当的阴影颜色,我们就可以获得正确的减色阴影。但是请记住,这只是一个近似值,不适用于多个光源。例如下面,其他烘焙的光将被错误地执行阴影。
(有其他光参与的情况下,错误的减色)
下一章,介绍全局光照,探针体积,LOD组。