Unity可编程渲染管线系列(九)烘焙阴影(混合光照)

2020-08-17 16:01:15 浏览数 (1)

本文重点: 1、淡入淡出实时阴影 2、应用阴影遮罩和阴影探针 3、每个物体使用4个烘焙阴影 4、在普通和距离 阴影遮罩中进行选择 5、支持减法照明

这是涵盖Unity的可编写脚本的渲染管道的系列教程的第九部分。它涉及将实时照明与烘焙阴影结合在一起,在减法照明的情况下,将烘焙照明与实时阴影结合起来。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。

本教程使用Unity 2018.3.0f2制作。

(烘焙阴影和实时阴影一起工作)

1 阴影淡入淡出

带有阴影的实时照明渲染起来很昂贵。烘焙的照明便宜得多,但它不包含镜面反射,并且在运行时无法更改。Unity支持第三种方法,该方法将实时照明与烘焙阴影结合在一起。但是仍然会使用一些实时阴影,因此必须以某种方式混合使用这两种类型的阴影。 

烘焙阴影不受阴影距离的影响,但实时阴影受阴影距离的影响。为了减少实时阴影的突然消失,我们首先添加支持以使它们在接近阴影距离时逐渐消失。

(实时阴影会根据阴影距离逐渐收紧)

1.1 淡出范围

淡出阴影的最简单方法是从片段到相机的距离中减去阴影距离,加1,然后使结果饱和:c-s 1。最终值是零,直到小于阴影距离一个单位,此后,它达到阴影距离时线性增加到1。然后,实时阴影消失,依靠烘焙的阴影。

但是我们不必总是在一个单位范围内淡入淡出。可以通过将两个距离除以任意一个正的淡入淡出范围:(c-s)/ r 1。

将配置选项添加到MyPipelineAsset中,以设置渐变范围,并具有合理的限制(如0.01~2),默认值为1。将其添加到构造函数参数中的阴影距离之后。

MyPipeline不需要跟踪实际的淡入度范围。我们可以将淡入淡出功能重写为

,因此我们可以将两个值传递给着色器,并且可以使用一条乘加指令执行。将 1/r 放入全局阴影数据的Y分量,并将 1-(s/r)放入其Z分量。将全局阴影数据移动到一个字段,并立即在构造方法中设置其Y值。

(阴影淡化范围设置为1)

从现在开始,我们将在“Render”中设置全局阴影数据,并在其中计算其Z分量,我们可以依靠它的Y值。

从RenderCascadedShadows和RenderShadows中都删除全局阴影数据代码,但在后一种情况下,我们仍然需要将tile比例放在其X组件中。

1.2 固定阴影裁剪

因为我们更改了全局阴影数据,所以阴影不再被修剪到阴影距离处。要解决此问题,请先从Lit.hlsl中删除DistanceToCameraSqr函数。将其替换为基于全局阴影数据计算阴影混合因子的函数。

当该值达到1时,将不再使用实时阴影,因此我们可以跳过对它们的采样。创建一个方便的功能来对其进行检查,并在ShadowAttenuation和CascadedShadowAttenuation中使用它。

现在,当超出了阴影距离阴影再次被裁剪。

1.3 衰减

淡化实时阴影只是混合实时阴影和烘焙阴影在没有可用的烘焙阴影时的一种特殊情况。我们将在一个新的MixRealtimeAndBakedShadowAttenuation函数中进行此操作,该函数最初仅具有用于实时阴影衰减和世界位置的参数。不存在烘焙阴影的衰减为1,因此可以在实时阴影和基于插值器的阴影之间进行插值。

阴影衰减为0或1,沿边缘进行一点过滤。实际上,我们最终得到的就是简单地为t的lerp(0,1,t)或始终为1的lerp(1,1,t)。可以通过将插值器添加到实时阴影衰减中来获得相同的结果,并且 饱和(saturate)结果,计算起来便宜一些。

(实时阴影逐渐消失)

2 阴影遮罩

要烘焙阴影,请将Unity的混合照明模式设置为Shadowmask。同时禁用实时全局照明,这样我们就可以专注于阴影。最初,我们将仅使用主方向灯,应将其设置为“Mixed ”模式。

(烘焙 阴影遮罩)

可以使用两种方式使用Shadowmask模式:常规Shadowmask或Distance Shadowmask。我们现在将使用常规模式,这是在项目设置下找到的质量设置。

(Shadowmask 模式)

现在,在检查烘焙的光照贴图时,你可以从右上角的下拉菜单中选择“Baked Shadowmask”。仅使用单个定向光时,生成的贴图为黑色和红色。没有阴影的片段为红色,因为红色通道用于存储阴影衰减。除此之外,常规光照贴图还包含烘焙的间接照明,就像“Baked Indirect mixed lighting”模式一样。

(平面的烘焙阴影遮罩)

现在已经烘焙了静态阴影,渲染实时阴影贴图时不再包括静态几何。由于我们尚未使用烘焙的阴影,因此它们已消失。

(没有静态阴影,只有实时)

2.1 检测阴影遮罩

要使用烘焙阴影,我们必须首先知道它们的存在。是否使用阴影遮罩会因光线而异,因此我们必须在MyPipeline.ConfigureLights中进行检查。如果存在阴影,我们将启用_SHADOWMASK着色器关键字。

对于每个可见光,我们可以通过从灯光对象获取烘焙输出来检查它是如何烘焙的。如果混合烘焙类型,则将灯光的混合烘焙模式设置为阴影遮罩时,将使用阴影遮罩。

在我们的着色器中为关键字添加一个多编译指令。

2.2 采样烘焙阴影

可以通过unity_ShadowMask纹理手柄及其关联的采样器状态使用该阴影遮罩。将它们添加到Lit.hlsl。

阴影遮罩使用与光照贴图相同的纹理坐标。创建一个函数来获取烘焙的阴影,例如将输入和表面作为参数的GlobalIllumination。我们目前尚未使用该位置,但稍后会使用。默认值为返回1,表示没有烘焙的阴影。需要做的其他事情取决于我们是渲染静态对象还是动态对象。结果是float4,因为阴影遮罩样本可能包含多达四个光的阴影衰减。

对于静态片段,如果有阴影遮罩,我们将对其进行采样,这就是结果。

在实时阴影衰减参数之后,在LitPassFragment中检索烘焙的阴影,并将它们传递给MixRealtimeAndBakedShadowAttenuation的两个调用。

将相应的参数添加到MixRealtimeAndBakedShadowAttenuation。由于我们仅支持主光源,因此我们需要的烘焙阴影衰减存储在烘焙阴影的第一个通道中。如果有遮罩,请返回它而不是衰减的实时阴影衰减。

这会导致所有阴影消失,因为我们还没有告诉Unity应该将阴影数据发送到GPU。这是通过启用渲染器配置的RendererConfiguration.PerObjectShadowMask标志来完成的。

(只有烘焙阴影)

现在出现了烘焙的阴影。要将它们与实时阴影混合,请返回两个衰减中的最小值。

(混合实时阴影和烘焙阴影)

请注意,尽管在运行模式下烘焙的阴影无法更改,但可以调节灯光本身。更改灯光的方向会产生明显错误的结果,因为只有实时阴影会随之变化。但是可以改变灯光的颜色和强度,而不会使烘焙的阴影无效。但是,如果烘烤间接照明,则光线不应改变太多。例如,红光与蓝色间接照明的不一致将是显而易见的,但强度的轻微不一致将不会。

2.3 阴影探针

因为动态对象没有光照贴图,所以它们也无法采样阴影遮罩纹理。但是,就像常规烘焙的照明一样,Unity也会在光探测器中烘焙阴影衰减。因此,光探针还可以用作阴影探针。通过启用RendererConfiguration.PerObjectOcclusionProbe标志,我们可以告诉Unity将这些数据发送到GPU。

通过float4 unity_ProbesOcclusion可以使用它,它是UnityPerDraw缓冲区的一部分。

尽管此数据是通过插值式光探测器提供的,但其用途与阴影遮罩完全相同,但适用于动态对象。因此,在适当的时候将其返回BakedShadows中。

(烘焙阴影也通过光探针实现)

GPU实例化也可以与unity_ProbesOcclusion一起使用,但是它依赖于定义的SHADOWS_SHADOWMASK,这不会自动发生。在包含UnityInstancing.hlsl之前,我们必须自己完成此操作。仅应在必要时执行此操作,因此仅对于使用阴影遮罩的动态对象。

2.4 LPPV 阴影

阴影探针也可以与光探针代理卷一起使用。再次,我们必须通过RendererConfiguration.PerObjectOcclusionProbeProxyVolume标志显式启用它。

BakedShadows可以使用与SampleLightProbes完全相同的方法,不同的是,它需要调用SampleProbeOcclusion函数而不是没有常规矢量参数的SampleProbeVolumeSH4。

(通过LVVP烘焙阴影)

2.5 多光源

阴影遮罩纹理具有四个通道,因此最多可以支持四个灯光。每个片段都是如此,但是通过将同一通道重用于多个灯光,它可以支持任意数量的灯光。唯一的限制是,影响同一张贴图片段的光不超过四个。如果太多的光影响同一区域,则有些光将被迫退回到完全烘焙(fully baked)。

因为我们仅支持主光源,所以其他光源最终都使用相同的烘焙阴影,即使它们是实时光源也是如此。例如,将两个混合模式聚光灯添加到场景中,再添加一个实时点光源。确保聚光灯投射阴影。点光源无法投射阴影,因为我们不支持它,但最终仍会受到主光源的烘焙阴影的影响。

(四盏灯全部受主要烘焙阴影的影响)

检查阴影遮罩会发现聚光灯已在R和G通道中烘焙。有可能在A通道中也烘焙了一个光,但是在预览窗口中看不到它。

(三个灯光的烘焙阴影)

每个光源在贴图中都有其自己的通道。我们可以通过获取烘焙阴影的点积和将适当通道设置为1的遮罩的点积来选择正确的遮罩。将这些遮罩发送到着色器,为此我们将创建遮挡遮罩数组。向MyPipeline添加一个着色器标识符和向量数组。

有四个可能的遮罩,我们可以在静态数组中预定义它们。但是也有可能某些灯光不使用阴影遮罩。通过将第一个遮罩分量设置为-1来标识。使这种情况成为数组的第一个元素,因此其长度为5。

在ConfigureLights中,根据烘焙输出的遮挡遮罩通道为每个可见光设置遮挡遮罩。如果光线不使用阴影遮罩,则通道为-1,因此在检索预定义的遮罩时,请添加1。

在“Render”中设置遮挡遮罩数组以及其他可见光数据。

在Lit.hlsl中,将数组添加到灯光缓冲区。

向MixRealtimeAndBakedShadowAttenuation添加一个轻量索引参数。然后,我们可以获取遮挡遮罩,提取相关的烘焙阴影衰减,并检查光线是否完全具有烘焙阴影。仅当我们拥有有效的烘焙数据时,才将实时阴影和烘焙阴影混合在一起。

在LitPassFragment中添加所需的light index参数。

(烘焙的阴影会影响正确的灯光)

2.6 距离阴影遮罩

使用常规阴影遮罩模式时,只有动态对象才能投射实时阴影。这样可以消除大量实时阴影,并用阴影模板样本和插值探针数据替换它们。尽管渲染成本可能较低,但是与所有内容都使用实时阴影时相比,结果质量较低。另一方面,烘焙的阴影不限于阴影距离。距离阴影遮罩模式在消除前者的同时利用了后者。所有阴影都是实时的,而烘焙阴影的使用超出阴影距离。因此,此模式比仅使用实时阴影更为昂贵,而不是更便宜。

(Distance shadowmask 模式)

两种荫罩模式的烘焙数据都相同。唯一的区别是渲染实时阴影时包含哪些对象,以及着色器如何组合烘焙和实时阴影。因此,我们需要另一个着色器变体,这次是通过_DISTANCE_SHADOWMASK关键字控制的。这是一种替代的阴影遮罩模式,因此将其添加到与_SHADOWMASK相同的多编译指令中。

现在,当定义了_SHADOWMASK或_DISTANCE_SHADOWMASK时,我们必须定义SHADOWS_SHADOWMASK。

BakedShadows中的条件编译也是如此。

但是在MixRealtimeAndBakedShadowAttenuation中,我们必须为每种模式做一些不同的事情。在常规阴影遮罩模式下,我们将实时衰减和烘焙阴影的衰减降至最低。但是对于距离阴影遮罩模式,我们必须根据插值器从实时过渡到烘焙阴影衰减。

最后,在MyPipeline中,我们必须基于QualitySettings.shadowmaskMode属性启用正确的关键字。

(Distance shadowmask 模式)

2.7 烘焙点光源阴影

虽然我们不支持点光源的实时阴影,但此限制不适用于烘焙的阴影。至少在使用常规阴影遮罩模式的情况下,可以出现混合模式点光的烘焙阴影。因为距离阴影遮罩模式从实时过渡到烘焙阴影,所以达到阴影距离的点光源阴影最终会小时,但烘焙阴影可以超出该距离。

(部分缺少点光阴影)

由于烘焙阴影总比没有阴影要好,因此在距离阴影遮罩模式下,我们始终将烘焙阴影用于点光源。为了让着色器支持,着色器必须能够检测到点光源。我们目前没有使用visibleLightSpotDirections向量的第四个分量,因此在点光源的情况下将其设置为1,而不是添加另一个数组。由于矢量的其余部分最终仍未使用,因此我们只需在ConfigureLights中使用Vector4.one。

在Lit.hlsl中,让MixRealtimeAndBakedShadowAttenuation在点光源的情况下返回烘焙的衰减,但仅在距离阴影遮罩模式下。

(始终烘焙点光阴影)

不需要对主??光源进行此检查,因此通过添加一个可选的boolean参数来优化此效果,该参数指示是否要混合主光源的阴影。

在LitPassFragment中的主光源上工作时启用优化。

3 减法照明

第三种混合照明模式:减法。这是一个预算选项,仅支持主方向灯的混合照明。选择此模式后,将显示另一个选项以设置实时阴影颜色,稍后我们将使用它。

(Subtractive 混合光照模式)

启用减光照明后,主光源将完全烘焙。光照贴图用于静态对象,但动态对象仍会实时照明并投射实时阴影。所有其他混合模式光源也是如此,但只能混合主光源的阴影。一开始我们的着色器将光照贴图和实时光照都应用于静态对象,会它们变得太亮。

(主光源会两次应用于静态对象)

3.1 固定主光源

我们需要用于混合照明的另一个着色器变体。这次,我们将使用_SUBTRACTIVE_LIGHTING关键字。将其添加到多编译指令。

检测减色照明并设置关键字的工作方式与其他阴影遮罩模式相同。这些模式是互斥的,因此我们可以在MyPipeline.ConfigureLights中单独检查它们。

在Lit.hlsl中,当使用减色照明时,对于静态对象,我们必须跳过LitPassFragment中的实时主光源。

(对于静态物体 只使用烘焙的主光源)

3.2 阴影化烘焙光

减光照明的想法是,即使使用完全烘焙的光照贴图,实时阴影和烘焙阴影也会混合在一起。这意味着我们必须调整烘焙的照明。我们将在新的SubtractiveLighting函数中执行此操作,该函数将表面和采样的全局照明作为参数。如果需要,请在GlobalIllumination中调用它以修改其结果。

SubtractiveLighting必须以某种方式找出烘焙的照明样本是否有阴影。由于烘焙的照明仅是漫反射的,因此首先要计算主光源的漫反射照明,就像实时照明一样。如果将其用作结果,我们将最终得到无阴影的仅漫射主光。

SubtractiveLighting必须以某种方式找出烘焙的照明样本是否有阴影。由于烘焙的照明仅是漫反射的,因此首先要计算主光源的漫反射照明,就像实时照明一样。如果将其用作结果,我们将最终得到无阴影的仅漫射主光。

还要计算淡入淡出的实时阴影衰减。那是由动态物体引起的阴影,这些物体不是烘焙照明的一部分。通过将实时漫射照明比例缩小一倍,然后减去实时阴影衰减,我们可以猜测如果烘焙了这些对象,那么会有多少烘焙的照明阴影。如果没有烘焙的阴影,没有间接照明或任何其他烘焙的光,那将是正确的。所以,这不是完美的,但是我们能做到的最好的了。最后,通过从烘焙的照明中减去猜测来找到最终的照明。结果需要进行饱和处理的,因为不正确的猜测可能会产生负光。

(减法照明)

3.3 阴影颜色

初始结果看起来可以接受,但仅当黑色阴影正确时才可以。将环境照明的强度乘数设置为1表示我们的猜测是错误的。

(猜测值太暗了)

我们无法在着色器中改善猜测,但是我们可以做的是限制减去的光量。这就是阴影颜色设置的目的。可以通过RenderSettings.subtractiveShadowColor检索它,并且当我们通过_SubtractiveShadowColor着色器属性检测到减色照明模式时,应该在ConfigureLights中对其进行设置。

将颜色添加到阴影缓冲区。

在SubtractiveLighting中,采用减去的光照和阴影颜色中的最大值,以限制移除的光量。但这可能使烘焙的照明变亮,它应该永远不会发生。因此,最终结果是烘焙和减去照明的最小值。

(颜色化的阴影)

如果环境照明大部分是均匀的并且与阴影颜色匹配,则可以产生合理的结果。默认阴影颜色是Unity标准环境照明设置的不错选择。

3.4 阴影强度

如果降低主光的阴影强度,则应将减去的照明效果降低相同的量。因为这也会影响阴影颜色,所以我们应该在应用颜色之后通过基于阴影强度在烘焙光和减光之间进行插值来应用阴影强度。

但这会将阴影强度两次应用于实时阴影,因为CascadedShadowAttenuation也将其应用。因此,让该函数可以忽略阴影强度。

仅在SubtractiveLighting中不应应用该强度。

3.5 阴影探针

减法照明现在可以正确地用于静态对象,但是动态对象仅接收实时阴影。再一次,我们可以依靠阴影探测器。首先,在使用减色照明时,还为动态对象定义SHADOWS_SHADOWMASK,因此GPU实例化仍然起作用。

在相同情况下,在BakedShadows中对阴影探针进行采样。

在MixRealtimeAndBakedShadowAttenuation中,如果我们具有减色照明并且正在处理动态对象,则必须像常规阴影遮罩模式一样混合阴影,但仅对主光源进行混合。因此,我们始终使用烘焙阴影的第一个通道。

(使用阴影探针)

这在存在主光源的情况下有效,但是即使使用减色照明,也有可能我们不渲染主光源。当忽略实时阴影时会发生这种情况,因为没有一个最终会落在阴影距离之内。在那种情况下,我们可以直接返回烘焙阴影衰减就足够了。但是因为没有单独的主光源,所以我们必须检查是否正在处理第一个光源索引。

下一节,介绍LOD。

0 人点赞