查看原文
其他

程序丨Unity渲染基础系列教程(七):阴影

2017-07-17 崔嘉艺 Gad-腾讯游戏开发者平台

译者:崔嘉艺(milan21)

审校:王磊(未来的未来)


研究下Unity渲染阴影的方式。

投射平行光阴影。

接收平行光阴影。

添加对聚光灯和点光源的阴影的支持


这是关于渲染基础的系列教程的第七部分。这个系列教程的上一部分讲的是法线贴图。现在让我们一起看下阴影的实现。


系列回顾:

Unity 渲染教程(一):矩阵

Unity 渲染教程(二):着色器基础

Unity 渲染教程(三):使用多张纹理贴图

Unity 渲染教程(四):第一个光源

Unity 渲染教程(五):多个光源

Unity 渲染教程(六):凹凸度


这个系列教程是使用Unity5.4.0f3开发的。


在渲染的时候,物体能够投射阴影是一件好事情。


平行光阴影


虽然我们的光照着色器现在能够产生相当真实的结果,但是它孤立地评估每个表面片段。它假定来自每个光源的光线最终到达到每个表面片段。但只在这些光线不被其他东西阻挡的情况下,这个判断才是正确的。


一些光线被其他东西阻挡。


当一个物体位于光源和另外一个物体之间的时候,这个物体可以阻止部分或全部光线到达另外一个物体。照亮第一个物体的光线不再可用于照亮第二个物体。结果就是,第二个物体至少有部分区域未被照亮。不亮的区域位于第一个物体的阴影之中。为了描述这一点,我们经常说第一个物体在第二个物体上投射阴影。


在现实的情况中,在完全照明和完全阴影的空间之间存在一个过渡区域,这个过渡区域被称为半阴影区。半阴影区的存在,是因为所有光源都有一个体积。光源具有体积导致的结果就是,存在一些仅有部分光源可见的区域,这意味着它们被部分地遮蔽。光源越大,表面距离阴影投射提的距离越远,被遮蔽的区域也就越大。


带有半阴影区的阴影。


Unity不支持半阴影。Unity支持的是软阴影,但软阴影是一种阴影滤波技术,而不是半阴影的模拟。


启用阴影


如果场景没有阴影的话,很难看到对象之间的空间关系。为了说明这一点,我创建了一个简单的场景,在这个简单的场景厘米那有几个拉伸的立方体。我在这些立方体上方放了四排球。中间两行是浮动的球体,而最外面的两行通过圆柱体将球体与它们下方的立方体相连。


这些对象具有Unity的默认材质。整个场景有两个方向光,一个是默认的方向光,和一个略弱的黄色方向光。这些光源是在以前的教程中使用过的相同的光源。


目前,整个项目范围内的阴影都被禁用了。我们在之前的教程中设置过这一点。环境光的强度也设置为零,这使得我们更容易能看到阴影。


两个方向光,没有阴影没有环境光的效果。


阴影是项目级别的质量设置的一部分,通过编辑/项目设置/质量可以找到具体的设置项。 我们将在高质量等级中启用阴影。高质量等级意味着支持硬阴影和软阴影、使用高分辨率、稳定的适配投影、150的投影距离以及四个级联阴影贴图。


阴影的质量设置。


还要确保两个方向光源都设置为投射软阴影。它们的分辨率应该取决于质量设置。


对每个光源投射的阴影进行设置。


当两个方向光源投射阴影的时候,所有对象之间的空间关系变得更加清楚。整个场景也变得更加真实,看起来也更加有趣。


带有阴影的场景效果


阴影贴图


Unity是如何将这些阴影添加到场景中取得?标准着色器显然有一些方法来确定光源发出的光线是否被阻挡。


你可以通过在场景中投射一条射线,从射线是否能到达表面片段来判断一个点是否在阴影中。如果这条射线在到达表面片段之前命中了某物,那么光源往这个方向发出的光线就会被阻挡。这是物理引擎可以做到的,但是对于每个表面片段和每个光源都这样做是非常不切实际的。而且在得到这些结果之后,你必须将得到的结果通过某种方式传递到图形处理器之中。


有几种技术能够支持实时阴影。每种技术都有它自己的优点和缺点。Unity使用的是现在最常见的技术,即阴影贴图。这意味着Unity以纹理方式存储阴影信息。我们现在将研究下Unity的阴影具体是如何实现的。


通过“窗口/帧调试器”打开帧调试器,启用阴影,并查看渲染步骤中的层次结构。查看启用阴影的帧与不启用阴影的帧之间的差异。


启用阴影的帧与不启用阴影的帧对于渲染过程的差异


当禁用阴影的时候,所有的物体都会照常进行渲染。我们已经很熟悉这个过程了。但是当启用阴影的时候,渲染过程变得更加复杂。渲染阶段变多了,还多了非常多的绘制调用。所以渲染阴影是非常昂贵的!


渲染到深度贴图


当启用平行光阴影的渲染选项的时候,Unity开始在渲染过程加入一个深度渲染Pass。所得到的结果会放一张与屏幕分辨率相匹配的纹理贴图中去。这个深度渲染Pass会渲染整个场景,但是仅记录每个片段的深度信息。这是与图形处理器用来确定片段与先前渲染的片段的深度相对关系相同的信息。


这个数据对应于片段在裁剪空间中的Z坐标。裁剪空间是定义相机可以看到的区域的空间。深度信息存储为0到1范围内的值。当查看纹理贴图的时候,离相机比较近的纹理像素显示为比较暗的点。离相机比较近的纹理像素显示为比较亮的点。


在相机的近平面设置为5的时候的深度纹理贴图


什么是裁剪空间?


裁剪空间决定了相机可以看到的区域。当你在场景视图中选择主摄像机的时候,你将在主摄像机的前面看到一个金字塔线框,这个金字塔线框就是指示这个摄像机可以看到的区域。


近平面值很大的时候,相机的视图。


在裁剪空间中,这个金字塔是一个规则的立方体。模型-视图-投影矩阵用于将网格顶点转换到这个空间之中。它被称为裁剪空间,因为在这个金字塔之外的所有内容都会被剪裁掉,因为这个金字塔之外的所有内容都是不可见的。


这个信息实际上与阴影没有直接关系,但Unity会在稍后使用这个信息。


渲染到阴影贴图之中


Unity渲染的下一个内容是第一个光源的阴影贴图。稍后,它将渲染第二个光源的阴影贴图。


再一次,整个场景都被渲染了一遍,并且同样也是只有深度信息被存储在纹理贴图之中。然而,这一次是从光源的位置来渲染整个场景。最为特别的是,光源作为摄像机。这意味着深度值告诉我们的是在光线射到某物之前到底行进了有多远。这可以用来确定是否有东西被遮挡住!


会对法线贴图有什么影响么?


阴影贴图记录的是实际几何体的深度信息。法线贴图会给物体添加粗糙表面的错觉,而阴影贴图会忽略它们。因此,阴影不受法线贴图的影响。


因为我们使用的是平行光源,他们的相机是正交的。因此,没有透视投影,并且与光源相机的确切位置没有关系。Unity将定位光源相机的位置,以便光源相机看到正常相机视图中的所有对象。


两张阴影贴图,分别是四个视点下看到的结果。


事实上, Unity不只对每个光源都要渲染整个场景一次。而是对每个光源都要渲染整个场景四次!纹理贴图被分成四个象限,每个象限从不同的角度进行渲染。这是因为我们选择使用四个级联阴影。 如果你要切换到两个级联阴影,场景将对每个光源渲染整个场景两次。如果没有级联阴影的话,它只是对每个光源渲染整个场景一次。当我们观察阴影的质量的时候,我们将看到为什么Unity会这么做。


收集阴影


我们现在有从相机的视角得到的场景的深度信息。我们也有从每个光源的视角得到的场景的深度信息。当然,这些数据存储在不同的裁剪空间之中,但我们知道这些空间的相对位置和方向。所以我们可以从一个空间转换到另一个空间中去。这允许我们从两个视角来比较深度的测量。从概念上讲,我们有两个向量应该在同一点结束。如果他们这样确实在同一点结束的话,从相机和光源的视角都可以看到这一点,所以这一点应该被照亮。如果光源发出的射线在到达这一点之前结束,则意味着光源发出的射线被阻挡,也就意味着该点被遮蔽了。


当现场相机无法看到一个点的时候怎么办呢?


这些点隐藏在更靠近相机的其他点的后面。场景深度纹理只包含那些最靠近相机的点。因此,没有必要来浪费时间去评估这些隐藏的点上。



对于每个光源而言的场景空间的阴影。Unity通过渲染覆盖整个视图的单个四边形来创建这些纹理。它使用Hidden /Internal-ScreenSpaceShadows这个着色器进行这个pass的渲染。从场景相机和光源的深度纹理贴图中进行每个片段的采样、进行比较、并将最终的阴影值渲染为屏幕空间的阴影贴图。被照亮的纹理像素设置为1,阴影贴图的纹理像素设置为0。此时,Unity还可以执行过滤,来创建那些柔和的阴影。


为什么Unity在渲染和收集阴影之间交替?


每个光源需要自己的屏幕空间的阴影贴图。但从光艳的角度渲染的阴影贴图可以重复使用。


对阴影贴图进行采样


最后,Unity完成了对阴影的渲染。现在场景被正常渲染,只是有一个变化。 光源的颜色会乘以存储在阴影贴图中的值。当这个光源应该被遮挡的时候,这会消除这个光源的影响。


每个被渲染的片段都会对阴影贴图进行采样。 最终会被其他物体隐藏的片段会放在后面进行绘制。 因此,这些片段可以最终接收到对它们进行隐藏的对象投射的阴影。当通过帧调试器单步调试的时候,你可以看到这一点。你还可以看到阴影出现在实际投射这些阴影的对象之前。当然,这些错误只有在渲染这一帧的过程中才会显示。一旦这一帧的渲染完成,所得到的图像就是正确的。


一个被部分渲染的帧,包含着奇怪的阴影。


阴影质量


当从光源的角度渲染整个场景的时候,光源的方向与场景相机的方向不匹配。因此,阴影贴图的纹理像素不与最终图像的纹理像素一一对应。阴影贴图的分辨率也与最终图像的分辨率不同。最终图像的分辨率由显示设置来决定。而阴影贴图的分辨率是由阴影质量设置来决定的。


当阴影贴图的纹理像素最终渲染的时候比最终图像的纹理像素大的时候,它们将变得非常明显。阴影的边缘将被出现走样。这在使用硬阴影的时候最为明显。


 硬阴影和软阴影的效果对比。


为了尽可能使这个走样更明显,让我们更改阴影质量设置,所以我们只在场景中得到硬阴影,分辨率为最低,并且没有级联。


低阴影质量设置下的阴影效果。


现在很明显,阴影是作为纹理贴图存储的。此外,阴影出现在他们不应该出现的地方。我们稍后会研究一下。


在阴影越靠近场景摄像机的地方,阴影的纹理像素就变得越大。这是因为阴影贴图目前覆盖场景摄像机可见的整个区域。我们可以通过在质量设置减少阴影覆盖的区域来提高相机附近的阴影质量。


距离减少为25的时候得到的阴影效果。


通过将阴影限制到接近场景摄像机的区域,我们可以使用相同的阴影贴图来覆盖更小的区域。结果就是,我们得到效果更好的阴影。但我们失去了在远离场景摄像机区域的阴影。阴影随着这些区域接近最大距离而消失。


在最理想的情况下,我们能够在接近场景摄像机的区域得到高质量的阴影,同时还能在远离场景摄像机的区域保持阴影。因为远处的阴影最终渲染到更小的屏幕区域,那些阴影可以使用较低分辨率的阴影贴图。这是通过级联阴影做的。启用级联阴影的时候,多个阴影贴图将渲染到同一纹理贴图之中。每个阴影贴图只用于一定距离的区域。


使用四个级联阴影的低精度阴影贴图所得到的渲染效果。


当使用四个级联阴影的时候,结果看起来好多了,即使我们仍然使用的是相同的纹理分辨率。我们只是更有效地使用了贴图中的纹理像素。缺点是我们现在必须再多渲染三次场景。


当渲染到屏幕空间的阴影贴图的时候,Unity会从正确的级联阴影中进行采样。你可以通过寻找阴影纹理尺寸的突然变化来找到一个级联阴影结束和另一个级联阴影开始的位置。


你可以通过质量设置控制级联阴影带所在的范围,因为级联阴影带所在的范围会作为阴影距离的一部分。你还可以通过更改渲染模式,而在场景视图中可视化级联阴影带所在的范围。使用杂项/级联阴影就可以除了渲染的物体之外, 还能渲染场景上方的级联阴影的颜色。


级联区域的示意图,调整后显示有三个级联区域。


我该如何更改场景视图的显示模式?


在场景视图窗口的左上角有一个下拉列表。默认情况下,它设置为“渲染”。


级联区域的形状取决于阴影投影质量设置。阴影投影质量设置的默认值为“稳定适配”。在这个模式下,可以根据渲染点到相机位置的距离来选择级联区域。另一个选项是“关闭适配”,那么级联区域的形状会使用相机的深度来决定。这会产生在相机的观察方向上的矩形区域带。


“关闭适配”下得到的阴影效果。


这种配置允许更有效地使用阴影贴图,这会导致更高质量的阴影。但是,阴影的投影现在取决于相机的位置和方向。这样做的结果就是,当相机移动或旋转的时候,阴影贴图也会随着改变。如果你能看到阴影的纹理像素,你会注意到它们在移动。这种效应被称为shadow edge swimming,有时候可以非常明显的看到。这就是为什么默认选项是其他模式。


不选择“关闭适配”,阴影也会依赖于相机的位置?


阴影确实会依赖于相机的位置,但Unity可以对齐地图,以便当相机的位置变化的时候,阴影贴图的纹理像素看上去是不动的。 当然,级联区域会跟着移动,所以级联区域之间的转变点会随着相机的位置的变化而变化。但如果你根本就没有注意到级联区域的话,你也不会注意到级联区域之间的转变点移动。


阴影瑕疵


当我们使用低质量的硬阴影的时候,我们看到一些阴影出现在他们不应该出现的地方。不幸的是,不管质量设置如何,这种情况都可能发生。


阴影贴图中的每个纹理像素表示的是光线射到表面的点。然而,纹理像素不是单一的点。阴影贴图中的纹理像素最终会覆盖更大的区域。它们与光的方向对齐,而不是与表面对齐。这样导致的结果是,它们可能最终粘附在或是穿过诸如黑暗碎片的表面。当阴影贴图中的纹理像素的一部分最终从投射阴影的表面戳出时,这些表面看起来像是对自己投射了阴影。这被称为阴影瑕疵。


阴影贴图导致的瑕疵。


阴影瑕疵的另外一个来源是数值精度限制。当涉及非常小的距离的阴影的计算的时候,这些数值精度方面的限制会导致不正确的结果。


根本不使用偏移的时候,所导致的严重的瑕疵


防止这个问题的一种方法是通过在渲染阴影贴图的时候添加深度偏移。深度偏移被添加到从光到阴影投射表面的距离中,这样做会将阴影推入到表面中去。


偏移以后的阴影贴图。


默认情况下,阴影偏移是按每个光源进行配置,这个值被设置为0.05。


按每个光源进行配置的阴影属性。


比较低的偏移量可能回产生阴影偏移,但比较大的偏移量会引入另外一个问题。当阴影投射物体被推离光源的时候,它们的阴影也会被随之推开。因此,阴影将不会完美地与对象对齐。当使用比较低的偏移量的时候,这还不是那么糟糕。但比较大的偏移量可以使它看起来像是阴影从投射它们的对象断开连接一样。这种效果被称为peter panning。


比较大的偏移量会导致peter panning。


除了这个距离偏移意外,还有一个法线偏移。这是对阴影投射体的微调。这种偏移将阴影投射体的顶点沿着它们的法线向内推。这也减少了自阴影的出生,但将阴影投射体的顶点沿着它们的法线向内推也会使阴影更小,并可能导致在阴影中出现孔。


什么是最佳偏移设置?


根本就没有什么最佳偏移设置。不幸的是,你必须通过试验来找到适合你项目的最佳偏移设置。Unity的默认设置可能有效,但默认设置也可能产生不可接受的结果。不同的质量设置也可能产生不同的结果。


抗锯齿


你是否在质量设置中启用了抗锯齿?如果你启动了抗锯齿的话,那么你可能会发现阴影贴图这种技术存在的另外一个问题。那就是它们不能与标准的抗锯齿技术混合在一起使用。


当使用抗锯齿时候出现的锯齿情况


当你在质量设置中启用抗锯齿的时候,Unity将使用多重采样抗锯齿技术,也就是MSAA。多重采样抗锯齿技术通过沿着三角形边缘执行一些超采样来去除三角形边缘的混叠。 细节纹理贴图对多重采样抗锯齿技术没有什么影响。重要的是,当Unity渲染屏幕空间的阴影贴图时候,它会使用覆盖整个视图的单个四边形。 因此,在屏幕空间的阴影贴图中根本就没有三角形边缘,因此多重采样抗锯齿技术不会影响屏幕空间阴影贴图。多重采样抗锯齿技术只对最终图像起作用,但是阴影值是从屏幕空间的阴影贴图中直接获取的。当靠近较暗表面的一个比较亮的表面被遮蔽的时候,这就会变得非常明显。明暗几何之间的边缘是反锯齿的,但是阴影边缘不是。



分别是没有使用抗锯齿技术、使用了多重采样抗锯齿技术和使用了快速近似抗锯齿技术的效果对比图。


依赖图像后处理的抗锯齿方法(比如快速近似抗锯齿技术)就没有这个问题,因为这些抗锯齿方法是在整个场景被渲染以后才进行处理的。


这是否意味着我不能混合使用多重采样抗锯齿技术与平行光阴影?


你可以混合使用多重采样抗锯齿技术与平行光阴影,但你会遇到上述问题。在某些情况下,这些问题可能不明显。让我们举个简单的例子来说明一下,当所有表面的颜色大致相同的时候,阴影的瑕疵将是很微小的当然,你仍然会得到有锯齿的阴影边缘。


投射阴影


现在我们知道Unity如何为平行光光创建阴影了,现在是时候为我们自己的着色器添加对平行光阴影的支持了。目前,我的第一个光照着色器既不投射也不接收阴影。


让我们先处理投射阴影的问题。我改变了示例场景中的球体和圆柱体,以便他们可以使用我们的材质。所以现在示例场景中的球体和圆柱体不再投射阴影。


使用了我们的材质以后,示例场景中的球体和圆柱体不再投射阴影。


我们知道Unity会为平行光阴影多次渲染场景。其中一次渲染场景是为了深度pass,并且还会为每个光源每个级联阴影贴图渲染场景一次。屏幕空间的阴影贴图是一个屏幕空间效果,不会涉及我们。


为了支持所有相关的pass,我们必须向我们的着色器添加一个pass,在这个pass中光照模式设置为ShadowCaster。因为我们只对深度值感兴趣,所以它会比我们的其他pass更加简单。



让我们把阴影程序它们自己的导入文件命名为My Shadows.cginc。这个导入文件很简单。顶点程序像往常一样将位置从对象空间转换为裁剪空间,不做任何其他操作。而片段程序实际上不需要做任何事情,所以只是返回零。图形处理器会为我们记录深度值。



这已经足够投射平行光阴影了。


投射平行光阴影。


偏移


我们还必须支持阴影的偏移。在深度pass渲染期间,这个阴影的偏移为零,但是当渲染阴影贴图的时候,阴影的偏移会相对光源进行设置。我们可以通过应用深度偏差到顶点着色器中顶点在裁剪空间的位置来实现这一点。


为了支持阴影的深度偏移,我们可以使用在UnityCG中定义的UnityApplyLinearShadowBias函数。



UnityApplyLinearShadowBias是如何工作的?


这个函数是增加裁剪空间中的Z坐标的值。 让这个事情变复杂的原因是它正在使用的是齐次坐标。它必须对透视投影进行补偿,以便偏移不随着渲染的位置离相机的距离变化而变化。它还必须确保结果不会超出范围。



为了支持阴影的法线偏差,我们必须根据法线向量来移动顶点的位置。所以我们必须在顶点数据中加上法线数据。然后我们可以使用UnityClipSpaceShadowCasterPos函数来应用这个法线偏差。这个函数也是在UnityCG中定义的。



UnityClipSpaceShadowCasterPos是如何工作的?


这个函数将顶点的位置转换到世界空间中去,然后应用正常偏置,再转换到裁剪空间中去。精确的偏移取决于法线和光线方向之间的角度,以及阴影纹理像素的尺寸。



UnityObjectToClipPos函数只是执行模型-视图-投影矩阵乘法,当使用立体渲染时需要注意下。



我们的着色器现在是一个功能齐全的阴影投射体了。


接收阴影


这个教程的第二部分是接收阴影。测试场景中的所有对象现在都在使用我们的材质。


测试场景中的所有对象现在都在使用我们的材质,所以不会接受阴影。


让我们首先关注主方向光的阴影。因为这个光源包括在基本base pass里面,我们必须对它进行调整。


当主方向光投射阴影的时候,Unity将寻找启用SHADOWS_SCREEN关键字的着色器的变体。因此,我们必须为我们的base pass创建两个变体,一个启用了SHADOWS_SCREEN关键字,一个没有启用SHADOWS_SCREEN关键字。 这与VERTEXLIGHT_ON关键字的工作原理相同。



这个pass现在有两个多编译指令,每个指令对应一个关键字。因此,一共有四种可能的变体。 一个变体是没有使用关键字,然后是两个分别对应一个关键字的变体,还有一个两个关键字都启用的变体。



在添加多重编译的预编译指令以后,着色器的编译器将警告一个不存在的_ShadowCoord关键字。发生这种情况是因为UNITY_LIGHT_ATTENUATION宏在使用阴影进行渲染的时候的运作方式不同。要快速解决这个问题,请打开My Lighting.cginc文件,当我们有阴影的时候,只要将衰减度设置为1就行了。



对阴影进行采样


为了得到阴影,我们必须对屏幕空间的阴影贴图进行采样。为了做到这一点,我们需要知道屏幕空间的纹理坐标。像其他纹理的坐标一样,我们将屏幕空间的纹理坐标从顶点着色器传递到片段着色器。所以我们需要在支持阴影的时候使用一个额外的插值器。我们将从裁剪空间的位置信息开始传递,所以我们需要一个float4类型。



我们可以通过_ShadowMapTexture来访问屏幕空间的阴影。它是在适当的时候在AutoLight中进行定义的。一种比较简单的方法就是直接使用这个片段的裁剪空间的XY坐标来对该纹理进行采样。



对阴影进行采样。



我们现在对阴影进行采样,但使用的是裁剪空间的坐标,而不是使用屏幕空间的坐标。我们确实得到了阴影,但是得到的阴影最终被压缩到屏幕中心的一个微小的区域。我们必须拉伸得到的阴影以覆盖整个窗口。


我现在场景中的阴影是颠倒的?


这是由于API所造成的差异。我们会尽快处理。


在裁剪空间中,所有可见的XY坐标都落在-1到1这个范围内,而屏幕空间的坐标范围为0到1这个范围内。第一步通过使用XY坐标来解决这个问题。接下来,我们还必须偏移坐标,使它们在屏幕的左下角的坐标值为零。因为我们要对透视变换进行处理,我们需要偏移坐标多少取决于它们距离相机有多远。在这种情况下,在二等分之前,偏移的大小等于齐次坐标的第四个分量。 



在左下角的阴影


投影仍然不正确,因为我们使用的是齐次坐标。我们必须通过将X分量和Y分量除以W分量来转换为屏幕空间的坐标。



不正确的转换。


结果出现了一点变形。阴影被拉伸和出现了弯曲。这是因为我们在插值之前进行了除法操作。这种做法是不正确的,坐标应该在除法之前独立进行插值。因此,我们必须将除法移动到片段着色器中去。



插值是如何影响除法的?


这最好用一个例子来进行说明。假设我们在XW坐标对(0,1)和(1,4)之间进行插值。不管我们如何做,X / W从0开始到¼结束。但是在这些点之间的点会发生什么呢?


如果我们在内插值之前进行除法,那么我们最终在0和¼两个点之间的中点是⅛


如果我们在内插值之后进行除法,那么在中间点的地方,我们得到的坐标是(0.5,2.5),这会导致除法0.5 / 2.5,结果是⅕,而不是⅛。因此在这种情况下,插值不是线性的。


不同的方法,不同的结果


正确采样后的阴影


此时,你的阴影将正确显示,或者是上下颠倒。如果它们上下颠倒的话,则意味着你的图形API –也就是 Direct3D –屏幕空间Y坐标从0到1是沿着向下的方向而不是向上的方向。要与此同步,请翻转顶点的Y坐标。



翻转的阴影。


使用Unity的代码


Unity的导入文件提供了一组函数和宏来帮助我们对阴影进行采样。这组函数和宏负责处理API的差异和平台的限制。举个简单的例子来说,我们可以使用UnityCG中的ComputeScreenPos函数。



ComputeScreenPos函数是什么样子的?


这个函数会执行我们刚才所做的相同的计算。当Y坐标需要翻转的时候,_ProjectParams.x变量的值为-1。此外,它使在用Direct3D9的时候会对纹理进行对齐。在进行单通道立体渲染时还会有一些特殊的逻辑。



AutoLight导入文件定义了三个有用的宏。它们分别是SHADOW_COORDS,TRANSFER_SHADOW和SHADOW_ATTENUATION。 当启用阴影的时候,这些宏会执行我们刚刚所执行过的相同的工作。当没有阴影的时候这些宏什么都不会做。


SHADOW_COORDS在需要的时候定义了阴影坐标的内插值器。我使用_ShadowCoord这个名字,这是之前编译器报警过的一个关键字。



TRANSFER_SHADOW在顶点程序中填充这些坐标。



SHADOW_ATTENUATION使用坐标对片段程序中的阴影贴图进行采样。



实际上,UNITY_LIGHT_ATTENUATION宏已经使用了SHADOW_ATTENUATION。这就是为什么我们之前会有编译器错误。 所以我们可以使用那个宏就足够了。唯一的变化是我们必须使用内插值器作为它的第二个参数,而在我们刚才的处理中我们使用的是零。



在重写我们的代码以继续使用这些宏之后,我们得到一个新的编译错误。这是因为Unity的宏不幸地对顶点数据和内插值器结构做出了假设。首先,它假设顶点位置被命名为顶点,而我们把它命名为位置。第二,它假定内插值器中位置变量被命名为pos,但是我们将其命名为position。


让我们面对现实,并采纳这些名字。它们只在几个地方使用,所以我们不必改变太多。



我们的阴影应该可以再次正常工作了,这一次我们的阴影可以在Unity支持的许多平台上运行起来。


这些宏是什么样子的?


你最终使用哪些宏取决于启用哪些着色器关键字,以及支持哪些功能。当定义SHADOWS_SCREEN的时候,最终会得到以下代码。



请注意,阴影坐标的Z分量仅在定义UNITY_NO_SCREENSPACE_SHADOWS和SHADOWS_NATIVE的时候使用。


tex2Dproj函数的功能与tex2D函数的功能相同,但是它也负责XY / W的除法操作。当看编译的代码的时候你可以看看这个函数。


使用多个阴影


主平行光现在投射阴影,但是第二个平行光现在仍然不投射阴影。这是因为我们还没有在加法pass中定义SHADOWS_SCREEN。我们可以向它添加一个多重编译语句,但SHADOWS_SCREEN只适用于平行光。 要获得正确的关键字组合,请将现有的多重编译语句更改为同时包括阴影的多重编译语句。



这将在组合中添加四个额外的关键字,以支持不同的光源类型



两个投射阴影的平行光。


聚光灯造成的阴影


现在我们已经处理号了平行光所投射的阴影,让我们开始处理聚光灯所投射的阴影。禁用平行光并添加一些带阴影的聚光灯到场景中。哇,惊喜! 由于Unity的宏,聚光灯的阴影现在已经开始工作了。


两个带有阴影的聚光灯。


当通过帧调试器查看的时候,你会看到Unity对聚光灯的阴影只做了很少的处理。没有单独的深度pass,并且没有屏幕空间的阴影pass。仅仅是渲染阴影贴图。


对带有阴影的聚光灯进行渲染。


聚光灯的阴影贴图的工作原理与平行光的阴影贴图的工作原理相同。它们是从光源的角度渲染的深度贴图。然而,平行光和聚光灯之间存在很大的差异。聚光灯具有实际位置,并且其光线不平行。所以聚光灯的相机看到的是一个透视视图,不能任意的旋转。因此,这些光源不能支持级联阴影。


近平面设为4的阴影贴图。


虽然相机的设置不同,但是两种光源类型的阴影投射代码是相同的。法线偏移这个设置仅支持方向光的阴影,对于其他光源,法线偏移只是简单的设置为零。


对阴影贴图进行采样


因为聚光灯不使用屏幕空间的阴影,所以对阴影贴图的采样代码必须不同。但是Unity的宏隐藏了与平行光对对阴影贴图的采样代码之间的区别。


对于聚光灯而言,这些宏到底是什么样子的?


通过将顶点位置转换到世界空间来找到阴影的坐标,并从那里将阴影的坐标转换到光源的阴影空间。



我们通过简单地采样屏幕空间的阴影贴图找到了平行光的阴影。Unity在创建这个贴图的时候会考虑阴影过滤,所以我们不需要担心。但是,聚光灯不使用屏幕空间的阴影。因此,如果我们想再次使用软阴影,那么我们必须在片段程序中进行过滤。


然后SHADOW_ATTENUATION宏使用UnitySampleShadowmap函数对阴影贴图进行采样。这个函数在UnityShadowLibrary中进行定义,包括了AutoLight。当使用硬阴影的时候,这个函数对阴影贴图采样一次。当使用软阴影的时候,这个函数对贴图采样四次并对结果值求平均。所得到的结果不如用于屏幕空间的阴影过滤,但是这种方法快得多。


聚光灯造成的硬阴影和软阴影的效果对比


UnitySampleShadowmap函数是什么样子的?


这个函数有两个版本,一个版本用于聚光灯,一个版本用于点光源。这里是聚光灯的那个版本。


_ShadowOffsets包含了用于创建软阴影所需要的四个采样的偏移量。在下面的代码中,我只显示了这四个采样中的第一个。



点光源造成的阴影


现在让我们尝试对点光源进行处理。当为点光源启用阴影的时候,将遇到编译错误。显然,UnityDecodeCubeShadowDepth是未定义的。这个错误之所以发生,是因为UnityShadowLibrary依赖于UnityCG,但没有显式的导入它。因此,我们必须确保首先导入UnityCG。我们可以通过在My Lighting.中导入AutoLight和UnityPBSLighting来做到这一点。



它可以成功编译,但在光源的范围内的所有对象都是黑色的。阴影贴图有一些问题。


不正确的阴影贴图。


当你通过帧调试器检查阴影贴图的话,你会发现,不是一个,而是为每个光源渲染六个贴图。这是因为点光源往所有方向发射光线。因此,阴影贴图必须是立体贴图。通过使用相机指向六个不同方向渲染场景来创建立方体贴图,每次渲染立方体的一个面。因此,点光源的阴影的代价是非常昂贵的。


投射阴影


不幸的是,Unity不使用深度立方体贴图。显然,没有足够的平台支持深度立方体贴图。所以我们不能依赖于My Shadows中的片段的深度值。相反,我们必须输出片段的距离来作为片段程序的结果。


当渲染点光源的光阴影贴图的时候,Unity会查找定义了SHADOWS_CUBE关键字的阴影投射体的变体。SHADOWS_DEPTH关键字用于平行光和聚光灯的阴影。为了支持这一点,添加一个特殊的多重编译指令为阴影投射体到我们的pass中。



这增加了我们需要的变量。



在这个场景中使用了2个关键词。

SHADOWS_DEPTH

SHADOWS_CUBE


因为点光源需要这样一种不同的方法,让我们为它们创建一组单独的程序函数。



要找出片段程序与光源之间的距离,我们必须构建从光源到片段的世界空间向量。我们可以通过为每个顶点创建这些向量,并对它们进行插值来实现。这需要一个附加的内插值器。



在片段程序中,我们取光源的矢量的长度并向其添加偏移。然后我们把它除以光源的范围,以把它们适配在0到1的范围内。_LightPositionRange.w变量包含其范围的倒数,因此我们必须乘以此值。得到的结果会作为浮点值输出。



UnityEncodeCubeShadowDepth负责做什么?


Unity喜欢使用浮点数立方贴图。当可以使用浮点数立方贴图的时候,这个函数不执行任何操作。当不可以使用浮点数立方贴图的时候,Unity将对值进行编码,以便将其存储在8位RGBA纹理贴图的四个通道中。



正确的阴影贴图。


对阴影贴图进行采样


现在我们的阴影贴图是正确的,点光源的阴影出现在场景中了。Unity的宏用于处理这些贴图的采样。


带有阴影的点光源。


点光源的宏是什么样子的?


在这种情况下,构造与投射阴影时相同的光线矢量。然后使用该向量对阴影立方体贴图进行采样。注意,内插值器仅需要三个分量,而不是四个。这次我们不传递齐次坐标。



在这种情况下,UnitySampleShadowmap会对立方体贴图而不是二欸纹理进行采样。



与聚光灯的阴影一样,点光源的阴影贴图对于硬阴影只采样一次,而对于软阴影则采样四次。最大的区别是Unity不支持对阴影立方体贴图进行过滤。这样的结果就是,阴影的边缘更加粗糙。所以点光影的阴影是昂贵的和有锯齿的。


点光影的硬阴影和软阴影效果对比图


我该如何营造漂亮的灯笼阴影?


使用一个或多个带有阴影的聚光灯。如果附近没有其他阴影投射对象的话,则可以使用带有Cookie的不带有阴影效果的光源。这适用于聚光灯和点光源,并且对渲染而言更加的廉价。


这个系列教程的下一部分是关于反射


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。


今日推荐


游戏空间矩阵变化的深度探索

日本游戏到底厉害在哪?CEDEC游戏技术精选


添加小编微信,可享双重福利

1.加入GAD程序猿交流基地

获取行业干货资讯,观看大牛分享直播

2.领取60G独家程序资料库,地址在小编朋友圈

包括腾讯内部分享、文章教程、视频教程等全套资料

 

↓长按添加小编GAD苏苏↓

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存