Unity通用渲染管线(URP)系列(六)——阴影遮罩(Shadow Masks)

2020-12-24 14:34:00 浏览数 (1)

目录

· 1 烘焙阴影

· 1.1 阴影遮罩距离

· 1.2 检测阴影遮罩

· 1.3 阴影遮罩数据

· 1.4 遮挡探针

· 1.5 LPPVs

· 1.6 MeshBall

· 2 混合阴影

· 2.1 如果可以就用烘焙

· 2.2 烘焙过渡

· 2.3 只有烘焙阴影

· 2.4始终使用阴影遮罩

· 3 多光源

· 3.1 阴影遮罩通道

· 3.2 选择适当的通道

本文主要内容: 烘焙静态阴影 实时光照里加入烘焙阴影 混合实时和烘焙阴影 支持4个阴影遮罩灯光

这是自定义可编程渲染管线的第六篇。使用阴影遮罩来烘焙阴影,并且将其加入到实时光的计算中。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。

本教程使用Unity 2019.2.21f1编写。

(近处为实时阴影,远处为烘焙阴影)

1 烘焙阴影

使用光照贴图的优点是我们不限于最大阴影距离。烘焙的阴影不会被剔除,但是它们也无法变化。理想情况下,我们可以使用最大阴影距离以下的实时阴影,并使用超出此范围的烘焙阴影。Unity的阴影遮罩的混合光照模式可以实现。

1.1 阴影遮罩距离

这次使用与上一教程相同的场景,但是减小了最大阴影距离,以使平台结构内部的一部分不会产生阴影。这让实时阴影的边界变得非常清晰。从单个光源开始吧。

(Mixed类型的灯光 最大距离为11)

将混合照明模式切换为Shadowmask。这将导致灯光数据无效,因此必须再次烘焙。

(mixed lighting的模式使用 Shadowmask)

有两种使用阴影蒙版混合照明的方法,可以通过project settings里的“Quality”进行配置。我们使用Distance Shadowmask模式。另一种模式就是阴影遮罩,稍后我们将介绍它。

(Shadow mask 模式设置为distance)

两种遮罩模式使用相同的烘焙照明数据。在这两种情况下,光照贴图最终都包含间接光照,与“Baked Indirect mixed lighting ”模式完全相同。所不同的是,现在还有一个烘焙的阴影遮罩贴图,你可以通过烘焙的光照贴图预览窗口进行检查。

(烘焙了间接光和阴影遮罩)

阴影遮罩贴图包含我们的单个混合定向光的阴影衰减,代表由对全局照明有贡献的所有静态对象投射的阴影。数据存储在红色通道中,因此贴图为黑色和红色。

就像烘焙的间接照明一样,烘焙的阴影在运行时无法更改。无论光线的强度或颜色如何变化,阴影都将保持有效。但是光线不应发生旋转,否则烘焙的阴影将无意义。另外,如果烘焙间接光照,则不应过多变化灯光。例如,在关闭灯后仍然保留间接照明,那显然是错误的。如果灯光变化很大,则可以将其间接系数设置为零,这样就不会烘焙任何间接灯光。

1.2 检测阴影遮罩

要使用阴影遮罩,我们的管线必须要先知道它的存在。因为所有关于阴影的事情都是Shadows类的工作。我们将使用着色器关键字来控制是否使用阴影遮罩。由于有两种模式,我们将引入另一个静态关键字数组,但它现在仅包含一个关键字:_SHADOW_MASK_DISTANCE。

添加一个布尔字段以追踪我们是否正在使用阴影遮罩们重新评估每帧,因此在Setup中将其初始化为false。

在“Render”末尾启用或禁用关键字。即使最终没有渲染任何实时阴影,也需要这样做,因为阴影遮罩不是实时的。

要知道是否需要阴影遮罩,我们需要检查是否有使用它的光。当最终得到有效的阴影投射光时,我们将在ReserveDirectionalShadows中进行此操作。

每盏灯光都包含有关其烘焙数据的信息。它存储在可以通过Light.bakingOutput属性检索的LightBakingOutput结构中。如果遇到其光照贴图烘焙类型设置为“mixed ”且其混合照明模式设置为“shadow mask”的光源,则说明我们正在使用阴影遮罩。

这将在需要时启用shader关键字。将其对应的多重编译指令添加到Lit着色器的CustomLit传递中。

1.3 阴影遮罩数据

在着色器端,我们需要知道是否使用了阴影遮罩,如果使用的话,烘焙的阴影是什么。让我们向Shadows添加ShadowMask结构,以使用bool 和float vector字段跟踪两者。命名boolean distance 以指示是否启用了distance shadow mask模式。然后将此结构作为字段添加到全局ShadowData结构中。

初始化阴影遮罩默认情况下在GetShadowData中不使用。

尽管使用阴影遮罩进行阴影遮挡,但它是场景的烘焙照明数据的一部分。因此,检索是GI的责任。因此,还要向GI结构中添加一个shadow mask字段,并将其初始化为在GetGI中不使用。

Unity通过unity_ShadowMask纹理和接下来的采样器状态使阴影遮罩贴图可用于着色器。定义GI中的那些以及其他光照贴图纹理和采样器状态。

然后添加一个使用光照贴图UV坐标对贴图进行采样的SampleBakedShadows函数。就像常规的光照贴图一样,这仅对光照贴图的几何有意义,因此需要在启用了LIGHTMAP_ON时才做操作。否则,就没有烘焙的阴影,衰减始终为1。

现在,我们可以调整GetGI,使其启用distance shadow mask模式,并在定义_SHADOW_MASK_DISTANCE的情况下对烘焙的阴影进行采样。注意,这会让distance布尔值成为编译时常数,因此其用法不会导致动态分支。

在循环灯光之前,Lighting可以在GetLighting中将阴影遮罩数据从GI复制到ShadowData。在这点上,我们还可以通过直接将阴影遮罩数据返回为最终的照明颜色来调试它。

它似乎有些问题,因为所有内容最终都变成白色。我们需要指示Unity将相关数据发送到GPU,就像我们在上一教程中对CameraRenderer.DrawVisibleGeometry中的光照贴图和探针所做的那样。在这种情况下,我们需要将PerObjectData.ShadowMask添加到每个对象的数据中。

(采样阴影遮罩)

为什么每次更改着色器代码时Unity都会烘焙灯光? 当我们更改元通道(meta pass)所包含的HLSL文件时,就会发生这种情况。您可以通过暂时禁用“Auto Generate”来防止不必要的烘焙操作。

1.4 遮挡探针

我们可以看到,阴影遮罩已正确应用于光照对象上了。但是还看到,动态对象并没有预期的阴影遮罩数据。因为他们使用的是光探针而不是光贴图。但是,Unity还将阴影遮罩数据烘焙到光探针中,我们将其称为遮挡探针(Occlusion Probes)。通过将unity_ProbesOcclusion向量添加到UnityInput中的UnityPerDraw缓冲区来访问此数据。将其放在世界变换参数和光照贴图UV变换向量之间。

现在,我们可以简单地在SampleBakedShadows中为动态对象返回该向量。

再次,我们需要通过启用PerObjectData.OcclusionProbe标志来指示Unity将数据发送到GPU。

(采样遮挡探针)

对于探针而言,未使用的阴影遮罩通道设置为白色,因此动态对象在完全照明时最终显示为白色,而在完全阴影时最终显示为青色,而不是红色和黑色。

尽管这足以使阴影遮罩通过探针的方式工作了,但它破坏了GPU实例化。遮挡数据可以自动获得实例,但是UnityInstancing仅在定义SHADOWS_SHADOWMASK时才执行此操作。因此,在包含UnityInstancing之前在Common中需要时定义它。这是我们唯一需要明确检查是否定义了_SHADOW_MASK_DISTANCE的其他地方。

1.5 LPPVs

LPPVs也可以与阴影遮罩一起使用。同样,我们必须通过设置一个标志来启用它,这次是PerObjectData.OcclusionProbeProxyVolume。

检索LPPV遮挡数据的方法与检索其光照数据的方法相同,除了我们必须调用SampleProbeOcclusion而不是SampleProbeVolumeSH4。它存储在相同的纹理中,并且需要相同的参数,唯一可能额不同是不需要法线向量。为此,将一个分支添加到SampleBakedShadows中,并为现在所需的世界位置添加一个Surface参数。

在GetGI中调用函数时添加新的Surface参数。

(采样LPPV 遮挡)

1.6 MeshBall

如果我们的MeshBall使用LPPV,则它已经支持阴影遮罩了。但是,当它自己对光探针进行插值时,我们需要在MeshBall.Update中添加遮挡探针的数据。这是通过对CalculateInterpolatedLightAndOcclusionProbes的最后一个参数使用临时Vector4数组并将其通过CopyProbeOcclusionArrayFrom方法传递到属性块来完成的。

在确认阴影遮罩数据已正确发送到着色器之后,我们可以从GetLighting中删除其可视化调试。

2 混合阴影

现在我们有了可用的阴影遮罩,下一步是在没有实时阴影的情况下使用它,当某个片段最终超出最大阴影距离时就是这种情况。

2.1 如果可以就用烘焙

混合烘焙阴影和实时阴影会让GetDirectionalShadowAttenuation的工作更加复杂。首先隔离所有实时阴影采样代码,然后将其移至Shadows中的新GetCascadedShadow函数里。

然后添加一个新的GetBakedShadow函数,该函数返回给定阴影遮罩的烘焙阴影衰减。如果启用了遮罩的距离模式,则我们需要其阴影矢量的第一个分量,否则没有衰减可用,结果为1。

接下来,创建一个具有ShadowData,实时阴影和阴影强度参数的MixBakedAndRealtimeShadows函数。它只是将强度应用于阴影,除非有远距离阴影遮罩。如果有的话,请用烘焙的阴影替换实时阴影。

让GetDirectionalShadowAttenuation返回该函数的结果,而不是应用强度本身。

(渐变的烘焙阴影)

结果是我们现在始终使用阴影遮罩,因此我们可以看到它起作用了。但是,烘焙的阴影会像实时阴影一样随着距离逐渐消失。

2.2 烘焙过渡

要根据深度从实时阴影过渡到烘焙阴影,我们必须根据全局阴影强度在它们之间进行插值。但是,我们还必须应用光的阴影强度,这是在插值后必须执行的操作。因此,我们不能再将GetDirectionalShadowData中的这两种优势直接结合起来了。

在MixBakedAndRealtimeShadows中,根据全局强度在烘焙和实时之间执行插值,然后应用灯光的阴影强度。但是,当没有阴影遮罩时,就像我们之前所做的那样,仅将组合的强度应用于实时阴影。

(混合阴影)

结果是动态对象投射的阴影照常消失,而静态对象投射的阴影过渡到阴影遮罩。

2.3 只有烘焙阴影

当前,我们的方法仅在有实时阴影要渲染时才有效。如果没有,那么阴影遮罩也会消失。可以通过缩小场景视图直到所有内容都超出最大阴影距离来验证这一点。

(既没有直接阴影也没有烘焙阴影)

当有阴影遮罩但没有实时阴影时,我们也必须让显示正常。创建一个也具有强度参数的GetBakedShadow函数变量,以便我们可以方便地获得强度调节后的烘焙阴影。

接下来,在GetDirectionalShadowAttenuation中检查组合强度最终是否等于或小于零。如果是这样,则不是总是返回1,而是仅返回调制后的烘焙阴影,仍然跳过实时阴影采样。

除此之外,还需要更改Shadows.ReserveDirectionalShadows,以便它不会立即跳过没有实时阴影投射器的灯光。而是首先确定灯光是否使用阴影遮罩。之后,检查是否没有实时阴影投射器,在这种情况下,仅阴影强度是有关联的。

但是,当阴影强度大于零时,着色器将采样阴影贴图,即便那是不正确的。这时,我们可以通过取消阴影强度来完成这项工作。

然后,当我们跳过实时阴影时,将绝对强度传递给GetDirectionalShadowAttenuation中的GetBakedShadow。这样,它既可以在没有实时阴影投射器的情况下使用,也可以在我们超出最大阴影距离的情况下使用。

(只有烘焙阴影)

2.4始终使用阴影遮罩

还有另一种遮罩模式,简称为“Shadowmask”。它与距离模式完全相同,但Unity会为使用阴影遮罩的灯光省略静态阴影投射器。

(没有实时阴影投射器的静态几何体)

这个想法是因为阴影遮罩在任何地方都可以使用,所以我们也可以在任何地方使用它作为静态阴影。这意味着更少的实时阴影,从而使渲染速度更快,但代价是质量较低的静态阴影会出现在比较近的地方。

要支持此模式,请将_SHADOW_MASK_ALWAYS关键字添加为Shadows中阴影模板关键字数组的第一个元素。我们可以通过检查QualitySettings.shadowmaskMode属性来确定应在Render中启用哪些功能。

将关键字添加到我们的着色器中的multi-compile指令中。

并在决定定义SHADOWS_SHADOWMASK时在Common中进行检查。

给ShadowMask结构一个单独的布尔字段,以指示是否应始终使用阴影遮罩。

然后在适当的时候在GetGI中设置它及其阴影数据。

使用任何一种模式时,两个版本的GetBakedShadow都应选择遮罩。

最后,当阴影遮罩始终处于活动状态时,MixBakedAndRealtimeShadows现在必须使用其他方法。首先,必须通过全局强度来调制实时阴影,以便根据深度对其进行淡入淡出。然后,将烘焙阴影和实时阴影合为一体,并取其最小值。之后,将光的阴影强度应用于合并的阴影。

(静态烘焙阴影和动态实时阴影的混合)

3 多光源

因为阴影遮罩贴图具有四个通道,所以它最多可以支持四个混合光。烘焙时,最重要的灯获取红色通道,第二个灯获取绿色通道,依此类推。让我们通过复制单向光,旋转一点并降低其强度来进行尝试,以便新光最终使用绿色通道。

如果混合灯超过四个,会发生什么? Unity会将前四个以外的所有混合模式光源转换为完全烘焙的光源。这里假设所有灯光都是定向的,这是我们当前支持的唯一灯光类型。其他类型的光的影响范围有限,这可能使同一个通道可以使用一个以上的光。

第二盏灯的实时阴影可以按预期工作,但最终会使用第一盏灯的遮罩烘焙阴影,这显然是错误的。使用始终阴影遮罩模式时,这很容易观察到。

3.1 阴影遮罩通道

检查烘焙的阴影遮罩贴图可发现阴影已正确烘焙。仅第一个灯点亮的区域是红色,仅第二个灯点亮的区域是绿色,而两者都点亮的区域是黄色。这最多可用于四盏灯,由于未显示Alpha通道,所以在预览中看不到第四盏灯。

(2盏灯的烘焙阴影)

两种光源都使用相同的烘焙阴影,因为我们始终使用红色通道。为了完成这项工作,我们必须将灯光的通道索引发送到GPU。我们不能依赖灯光顺序,因为它会在运行时变化,因为灯光可以更改甚至禁用。

可以通过LightBakingOutput.occlusionMaskChannel字段在Shadows.ReserveDirectionalShadows中检索灯光的遮罩通道索引。在向GPU发送4D向量时,我们可以将其存储在返回的向量的第四通道中,将返回类型更改为Vector4。当光线不使用阴影遮罩时,我们通过将其索引设置为-1来表示。

3.2 选择适当的通道

在着色器大小上,将阴影遮罩通道作为附加整数字段添加到“Shadows”中定义的DirectionalShadowData结构。

GI然后必须在GetDirectionalShadowData中设置通道。

将通道参数添加到两个版本的GetBakedShadow中,并使用它返回适当的阴影遮罩数据。但是,只有在光线使用阴影遮罩时才这样做,因此通道至少要大于等于0。

点积是否比索引通道更好? 是的,但是着色器编译器会为我们解决这个问题。它将使用该通道为矢量的静态缓冲区建立索引,并将适当的分量设置为1,然后将其用于执行带掩码的点积以对其进行过滤。我们也可以将点积发送到GPU来跳过查找步骤,但这将需要发送一个额外的向量数组,无论如何都必须对其进行索引。

调整MixBakedAndRealtimeShadows,使其沿着所需的阴影遮罩通道传递。

最后,在GetDirectionalShadowAttenuation中添加所需的通道参数。

(两盏等用各自的通道)

减法混合照明模式如何? 减光照明是仅使用单个光照贴图将烘焙的照明和阴影相结合的替代方法。这样的想法是,你可以完全烘焙光,但也可以将其用于实时照明。然后,计算该光的实时漫射照明,采样实时阴影,并使用该值来确定要对多少漫射光进行阴影处理,然后从漫射GI中减去该阴影。

它仅适用于无法改变的单个定向光。否则的话, 所有间接照明或任何其他烘焙的光都会产生不正确的结果,但可以通过可配置的阴影颜色(应与场景的平均间接GI颜色匹配)来限制变暗,从而减轻这种情况。

在本系列中,将不包括对减法模式的支持。如果你有足够的空间放置阴影遮罩贴图,则阴影遮罩模式将始终优于减法。如果没有,则考虑完全烘焙,这会允许更复杂的灯光设置。

本文翻译自 Jasper Flick的系列教程

原文地址:

https://catlikecoding.com/unity/tutorials

0 人点赞