目录
· 1 聚光灯阴影
· 1.1 阴影混合
· 1.2 其他实时阴影
· 1.3 两个图集
· 1.4 渲染聚光灯阴影
· 1.5 没有Pancaking
· 1.6 采样聚光灯阴影
· 1.7 法线偏差
· 1.8 钳位采样
· 2 点光源阴影
· 2.1 一个灯光6个Tile
· 2.2 渲染点光源阴影
· 2.3 采样点光源阴影
· 2.4 画正确的表面
· 2.5 视场偏差
本文重点内容: 1、混合点光和聚光灯的烘焙和实时光阴影 2、添加第二个阴影图集 3、使用透视投影渲染和采集阴影 4、使用自定义的立方体贴图
这是有关创建定制脚本渲染管道的系列教程的第十部分。它增加了对点光源和聚光灯的实时阴影的支持。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2019.4.1f1制作。
(100%的实时阴影)
Unity升级 我已升级到Unity版本2019.4.1f1和Core RP Library版本7.3.1,因此某些编辑器UI视觉效果已更改。 除此之外,GetShadowCasterBounds现在对方向光返回true,即使在阴影范围内没有任何东西。这使得无法再进行corner-case的优化,但不需要修改代码。
1 聚光灯阴影
我们从支持聚光灯的实时阴影开始。我们将使用与方向光相同的方法,但要进行一些更改。我们还将使用统一平铺的阴影图集并以Unity提供的顺序填充阴影光,让实现尽可能简单。
1.1 阴影混合
第一步是使混合烘焙阴影和实时阴影成为可能。在Shadows中调整GetOtherShadowAttenuation,使其与GetDirectionalShadowAttenuation相似,不同之处在于它使用其他阴影数据并依赖于新的GetOtherShadow函数。新功能最初返回1,因为其他光源还没有实时阴影。
全局强度用于确定是否可以跳过采样实时阴影,比如超出了阴影距离,或者是在最大级联范围之外。但是,级联仅适用于定向阴影。它们对于其他光线没有意义,因为它们具有固定的位置,因此其阴影贴图不会随视点移动。话虽如此,但将所有阴影都以相同的方式淡出是个好主意,否则我们可能会遇到屏幕上某些没有方向性阴影但又有其他阴影的区域。因此,我们将对所有对象使用相同的全局阴影强度。
我们还需要处理的一个极端情况,就是当没有方向性阴影存在而又确实有其他阴影。发生这种情况时,不会有任何级联,因此它们不应影响全局阴影强度。而且我们仍然需要阴影距离的淡入值。因此,让我们移动代码以将级联计数和距离淡变从Shadows.RenderDirectionShadows设置为Shadows.Render,并在适当时将级联计数设置为零。
然后,我们需要确保在GetShadowData中进行级联循环后,全局强度没有错误地设置为零。
1.2 其他实时阴影
方向阴影具有自己的图集Map。而我们将对所有其他阴影光使用单独的图集,并分别进行计数。让我们设置最多使用其他十六个具有实时阴影的灯光。
这意味着我们最终可以使用启用了阴影但不适合图集的光源。哪些光不会产生阴影取决于它们在可见光列表中的位置。我们只是不会为没有贡献的灯光保留阴影,但是如果它们烘焙了阴影,我们仍然可以允许阴影。为此,请首先重构ReserveOtherShadows,以便在灯光没有阴影时立即返回。否则,它将检查阴影Mask通道(默认情况下使用-1),然后始终返回阴影强度和通道。
然后,在返回之前,检查增加的灯光计数是否会超过最大值,或者是否没有要渲染的阴影。如果是,则阴影强度和遮罩通道为负值,因此在适当时使用烘焙阴影。否则,继续增加光计数并设置平铺索引。
1.3 两个图集
由于方向阴影和其他阴影是分开的,因此我们可以对它们进行不同的配置。向其他阴影的ShadowSettings添加新的配置结构和字段,仅包含图集大小和滤镜,因为级联不适用。
(其他阴影设置)
在我们的Lit着色器的CustomLit Pass中添加一个多编译指令,以支持其他阴影的阴影过滤。
并向Shadows添加相应的关键字数组。
我们还需要跟踪其他阴影图集和矩阵的着色器属性标识符,以及一个用于保存矩阵的数组。
我们已经使用向量的XY分量将方向图集的图集大小发送到GPU。现在,我们还需要发送其他图集的大小,可以将其放入同一向量的ZW分量中。将其提升到一个字段,然后将全局矢量从RenderDirectionalShadows设置为Render。然后,RenderDirectionalShadows只需要分配给该字段的XY分量。
之后,复制RenderDirectionalShadows并将其重命名为RenderOtherShadows。对其进行更改,以使其使用正确的设置,图集,矩阵,并设置正确的尺寸分量。然后从中删除级联和剔除球代码。还可以删除对RenderDirectionalShadows的调用,但要保持循环。
现在,我们可以在需要时在RenderShadows中同时渲染定向阴影和其他阴影。如果没有其他阴影,则需要为它们提供虚拟纹理,就像定向阴影一样。我们可以简单地使用定向阴影图集作为虚拟对象。
并在Cleanup中发布其他阴影图集,在这种情况下,仅当我们确实为1的时候。
1.4 渲染聚光灯阴影
要渲染聚光灯的阴影,我们需要知道聚光灯的可见光索引,斜率比例偏差和法线偏差。因此,为这些字段创建一个ShadowedOtherLight结构,并为其添加一个数组字段,类似于我们追踪定向阴影的数据的方式。
返回之前,将相关数据复制到ReserveOtherShadows的末尾。
但是,现在,我们应该意识到,我们不能保证将正确的光照索引发送到Lighting中的ReserveOtherShadows,因为它会将自己的索引传递给其他光照。如果有阴影的方向光,索引将是错误的。我们通过在灯光设置方法中添加正确的可见光索引参数来解决此问题,并在保留阴影时使用该参数。为了保持一致性,我们还要对方向光进行此操作。
调整SetupLights,让它将可见光索引传递给设置方法。
回到Shadows,创建一个RenderSpotShadows方法,该方法与带有参数的RenderDirectionalShadows方法相同,不同之处在于它不会循环多个Block,没有级联,也没有剔除参数。在这种情况下,我们可以使用CullingResults.ComputeSpotShadowMatricesAndCullingPrimitives,其功能类似于ComputeDirectionalShadowMatricesAndCullingPrimitives,不同之处在于它仅将可见光索引,矩阵和拆分数据作为参数。
在RenderOtherShadows循环内调用此方法。
(3个聚光灯的阴影图集)
1.5 没有Pancaking
现在,使用与定向阴影相同的ShadowCaster Pass为聚光灯渲染阴影。这很好用,除了阴影平移仅对正交阴影投影有效,假定正投影阴影用于无限远的定向光。在有位置的聚光灯的情况下,阴影脚轮可能会部分落后于其位置。由于我们在这种情况下使用透视投影,因此将顶点clamping到近平面会严重扭曲此类阴影。因此,当不适合使用Pancaking时,我们应该关闭clamping。
我们可以通过全局着色器属性(我们将其命名为_ShadowPancaking)告诉着色器是否激活了pancaking。在阴影中追踪其标识符。
在渲染阴影RenderDirectionalShadows之前将其设置为1。
在RenderOtherShadows中设置为0。
然后将其作为布尔值添加到我们的Lit着色器的ShadowCaster通道中,并仅在适当的时候使用它进行clamp。
1.6 采样聚光灯阴影
要采样其他阴影,我们需要调整Shadows。首先定义另一个滤镜,然后将其他阴影的宏数最大化。然后添加其他阴影图集和其他阴影矩阵数组。
复制SampleDirectionalShadowAtlas和FilterDirectionalShadow,并重命名和调整它们以使其适用于其他阴影。请注意,对于此版本,我们需要使用图集大小向量的其他分量对。
现在,OtherShadowData结构也需要一个Tile索引。
这是由Light中的GetOtherShadowData进行设置的。
现在我们可以在GetOtherShadow中采样阴影贴图,而不是总是返回1。它的工作原理与GetCascadedShadow相似,只是没有第二个级联可以混合,并且它是透视投影,因此我们需要将变换位置的XYZ分量除以W分量 。另外,我们还没有功能性的法线偏差,因此我们现在将其乘以零。
(只有直接的聚光灯,有和没有实时阴影)
1.7 法线偏差
聚光灯会像定向光一样遭受阴影粉刺的困扰。但是由于透视投影的原因,纹理像素的大小也不固定,因此粉刺也不固定。离光越远,粉刺就越大。
(纹素随着灯光的距离增加)
纹素随着距灯光平面的距离呈线性增加,灯光平面是将世界散布在光的前面或后面的平面。因此,我们可以计算纹理像素大小,从而计算出距离1处的法线偏差,并将其发送到着色器,在此处将其缩放到适当的大小。
在世界空间中,与光平面的距离为1时,阴影图块的大小是弧度的spot角一半的切线的两倍。
(世界空间下,tile的大小推导)
这与透视投影匹配,因此距离1处的世界空间纹理像素大小等于2除以投影比例,为此,我们可以使用其矩阵的左上角值。我们可以像使用定向光一样使用它来计算法向偏差,不同之处在于,由于没有多个级联,我们可以立即将光的法向偏差纳入其中。在Shadows.RenderSpotShadows中设置阴影矩阵之前,执行此操作。
现在我们必须将偏差发送给着色器。我们稍后需要在每个Tile上发送更多数据,因此让我们添加_OtherShadowTiles向量数组着色器属性。将其标识符和数组添加到Shadows中,并将其与矩阵一起设置在RenderOtherShadows中。
使用索引和偏差创建一个新的SetOtherTileData方法。将偏差放在向量的最后一个分量中,然后将其存储在tile数据数组中。
一旦检测到偏差,请在RenderSpotShadows中调用它。
然后将另一个阴影tile数组添加到阴影缓冲区中,并使用它来缩放Shadows中的法向偏差。
(常量的法相偏差 设置为1)
现在,我们有一个法向偏差,仅在固定距离处才正确。为了根据距光平面的距离进行缩放,我们需要知道世界空间的光位置和聚光灯方向,因此将它们添加到OtherShadowData中。
让Light将值复制到其中。由于这些值来自灯光本身,而不是阴影数据,因此在GetOtherShadowData中将它们设置为零,然后在GetOtherLight中将它们复制。
我们通过在GetOtherShadow中获取表面到光矢量和点方向的点积来找到与平面的距离。用它来缩放法向偏差。
(每一处都是正确的法向偏差了)
1.8 钳位采样
我们为定向阴影配置了级联球体,以确保永远不会在适当的阴影Tile之外进行采样,但对其他阴影不能使用相同的方法。对于聚光灯,其Tile紧密贴合其圆锥体,因此法向偏差和滤镜大小会将采样推到圆锥体边缘接近Tile边缘或者边界之外。
(靠近边的tiles有错误的阴影)
解决该问题的最简单方法是手动钳位采样以使其停留在Tile范围内,就像每个Tile都是其自己的单独纹理一样。这样仍会在边缘附近拉伸阴影,但不会引入无效阴影。
调整SetOtherTileData方法,使其也可以基于通过新参数提供的偏移量和比例来计算和存储Tile边界。Tile的最小纹理坐标是缩放的偏移量,我们将其存储在数据向量的XY分量中。由于Tile是正方形,我们可以将Tile的比例存储在Z分量中,而W留在偏差上就足够了。我们还需要在两个维度上将边界缩小一半像素,以确保采样不会超出边缘。
在RenderSpotShadows中,将通过SetTileViewport找到的偏移量和拆分的倒数用作SetOtherTileData的新参数。
ConverToAtlasMatrix方法还使用拆分的逆函数,因此我们可以计算一次并将其传递给这两种方法。
然后,ConvertToAtlasMatrix不必自己执行除法。
这需要RenderDirectionalShadows来执行除法,该除法对于所有级联只需要执行一次。
要应用边界,请向SampleOtherShadowAtlas添加一个float3参数,并使用它来固定阴影Tile空间中的位置。FilterOtherShadows需要相同的参数,以便可以传递它。并且GetOtherShadow从Tile数据中检索它。
(不会再有阴影来自于错误的Tile)
2 点光源阴影
点光源的阴影的工作方式与聚光灯的阴影相同。区别在于点光源不限于圆锥体,因此我们需要将其阴影渲染到立方体贴图。这是通过分别渲染立方体的所有六个面的阴影来完成的。因此,出于实时阴影的目的,我们将点光源视为六个光源。它将在阴影图集中占据六个Tile。这意味着我们可以同时支持最多两个点光源的实时阴影,因为它们会占据16个可用Tile中的12个。如果少于六个Tile,则点光源将无法获得实时阴影。
2.1 一个灯光6个Tile
首先,我们需要知道在渲染阴影时正在处理点光源,因此请向ShadowedOtherLight添加一个布尔值以指示此点。
检查ReserveOtherShadows中是否有点光源。如果是,则包含此数字的新灯光计数将比当前计数大六倍,否则仅增加一倍。如果超过最大值,那么多出的光具有烘焙的阴影。如果图集中有足够的空间,则还应在返回的阴影数据的第三部分中存储是否为点光源,以方便在着色器中检测点光源。
2.2 渲染点光源阴影
调整RenderOtherShadows,以便在适当时在其循环中调用新的RenderPointShadows方法或现有的RenderSpotShadows方法。同样,随着点光源的计数增加,对于每种光源类型,迭代器将以正确的数量增加迭代器,而不仅仅是增加它。
新的RenderPointShadows方法是RenderSpotShadows的副本,但有两个区别。首先,它必须渲染六次而不是一次,才能遍历其六个Tile。其次,它必须使用ComputePointShadowMatricesAndCullingPrimitives代替ComputeSpotShadowMatricesAndCullingPrimitives。此方法在light索引之后需要两个额外的参数:CubemapFace索引和bias。我们为每个表面渲染一次,现在将偏差保持为零。
(2个点光源的阴影图集)
立方体贴图面的视场(FOV)始终为90°,因此距离1处的世界空间Tile大小始终为2。这意味着我们可以将偏差的计算结果提升到循环之外。我们也可以使用Tile比例来实现。
2.3 采样点光源阴影
想法是将点光阴影存储在立方体贴图中,我们的着色器对其进行采样。但是,我们将立方体贴图的面作为图块存储在图集中,因此我们不能使用标准立方体贴图采样。我们需要确定要从自己的结构中取样的合适的表面。为此,需要知道我们是否正在处理点光源以及表面到光的方向。将两者都添加到OtherShadowData。
在Light中设置这两个值。如果另一盏灯的阴影数据的第三部分等于1,则这是点光源。
接下来,在点光源的情况下,我们需要在GetOtherShadow中调整Tile索引和光平面。首先将它们转换为变量,这些变量最初是为聚光灯配置的。将tile索引设为float,因为我们将为其添加一个偏移量,该偏移量也被定义为float。
如果我们有一个点光源,那么必须改为使用适当的轴对齐平面。可以使用CubeMapFaceID函数通过将其否定的光方向传递给它来找到表面偏移。此函数是内部函数或在核心RP库中定义的函数,返回浮点数。立方体贴图面的顺序为 X,-X, Y,-Y, Z,-Z,与我们渲染它们的方式匹配。将偏移量添加到Tile索引中。
接下来,我们需要使用与表面方向匹配的光平面。为它们创建一个静态常量数组,并使用表面偏移对其进行索引。平面法线必须指向与面相反的方向,就像聚光灯方向指向灯光一样。
(只有点光源,有和没有实时阴影,没有偏差)
2.4 画正确的表面
现在,我们可以看到点光源的实时阴影, 使用的是零偏差,它们似乎也不会遭受阴影粉刺的困扰。不幸的是,现在光线从物体泄漏到另一侧非常靠近物体的表面。增加阴影偏差会使情况变得更糟,并且似乎还会在靠近其他表面的对象的阴影中切出孔。
(最大法向偏差为3)
发生这种情况是因为Unity为点光源渲染阴影的方式。它将它们上下颠倒,从而颠倒了三角形的缠绕顺序。通常,从光的角度绘制正面,但是现在可以绘制背面。这可以防止大多数粉刺,但会引起漏光。我们不能阻止翻转,但是可以通过对从ComputePointShadowMatricesAndCullingPrimitives中获得的视图矩阵进行取反来撤消翻转。让我们取反它的第二行。这第二次将图集中的所有内容颠倒过来,从而使所有内容恢复正常。因为该行的第一个成分始终为零,所以我们只需将其他三个成分取反即可。
(前面的阴影渲染,法向偏差为0和1)
比较阴影贴图时,会很明显地发现,这改变了渲染阴影的方式。
(阴影贴图的前和后)
注意那些MeshRenderer的阴影投射模式设置为双面的对象不会受到影响,因为它们的面都不会被剔除。例如,我用剪辑或透明材质使所有的球体都投射两面阴影,这样它们看起来更像实体。
(剪辑和透明材质的球体,两面都有阴影)
2.5 视场偏差
立方体贴图的面之间始终存在不连续性,因为纹理平面的方向突然改变了90°。常规的立方贴图采样可以在某种程度上隐藏它,因为它可以在面之间进行插值,但是我们从每个片元的单个Tile采样。我们得到了与聚光灯阴影Tile边缘相同的问题,但是现在它们没有被隐藏,因为没有Spot衰减。
在渲染阴影时,我们可以通过增加视野(简称FOV)来减少这些伪影,因此我们绝不采样超出Tile边缘。这就是ComputePointShadowMatricesAndCullingPrimitives的bias参数所针对的。我们通过在距光源1的距离处将瓦片大小设置为大于2来实现此目的。具体来说,我们在两边加上法向偏差和滤波器尺寸。然后,对应的一半FOV角的切线等于1加上偏差和滤镜大小。将其加倍,将其转换为度,减去90°,然后将其用于RenderPointShadows中的FOV偏置。
(增加世界空间的tile 大小)
(带有FOV偏差)
请注意,这种方法并不完美,因为通过增加Tile大小,纹理像素大小也会随之增加。因此,滤波的尺寸增加,法向偏差也增加,这意味着我们必须再次增加FOV。但是,差异通常很小,以至于我们可以忽略tile大小的增加而消失,除非结合使用大的法向偏置和滤波以及小的图集大小。
我们可以对聚光灯使用相同的方法吗? 可以,一点额外的工作可以不再需要使用Tile clamp。但是,ComputeSpotShadowMatricesAndCullingPrimitives没有FOV bias参数,因此我们必须创建自己的变体,这超出了本教程的范围。