本文重点
1、调查Unity是如何渲染阴影的 2、投射定向阴影 3、接受定向阴影 4、添加对聚光灯和点光源阴影的支持
(温馨提示:本系列知识是循序渐进的,推荐第一次阅读的同学从第一章看起,链接在文章底部)
这是有关渲染的系列教程的第七部分。上一部分介绍了法线贴图。现在我们来看看阴影。
本教程使用Unity 5.4.0f3制作的。
(渲染时,投射阴影是一件好事情)
1 定向阴影
到目前为止,虽然我们的照明着色器可产生相当逼真的结果,但它会独立评估每个表面片段。它假设来自每个光源的光线最终都会撞击每个片段。但这仅在那些光线未被阻挡的情况下才是正确的。
(一些光线被阻挡了)
当一个物体位于光源和另一个物体之间时,可能会阻止部分或全部光线到达该另一个物体。照亮第一个对象的光线不再可用于照亮第二个对象。结果,第二物体将保持至少部分不发光。未照亮的区域位于第一个对象的阴影中。为了描述这一点,我们经常说第一个物体在第二个物体上投下了阴影。
实际上,在完全照明和完全阴影的空间之间存在一个过渡区域,称为半影。存在是因为所有光源都有体积。结果,在某些区域中仅部分光源可见,这意味着它们被部分遮盖了。光源越大,并且表面离其阴影投射器越远,则该区域越大。
(Unity不支持半影。但支持柔和阴影,但这是一种阴影过滤技术,而不是半影模拟。)
1.1 启用阴影
没有阴影,很难看到对象之间的空间关系。为了说明这一点,我创建了一个带有几个拉伸立方体的简单场景。在这些立方体上方放置了四行球体。中间的行漂浮着球体,而外部的行通过圆柱体连接到它们下面的立方体。
这些对象具有Unity的默认白色材质。场景有两个方向光,即默认的方向光和稍弱的黄色光。这些光与以前的教程中使用的光相同。
当前,阴影在项目设置内被禁用。我们在之前的教程中做了。环境强度也设置为零,这使得更容易看到阴影。
(两个方向光,没有阴影,没有环境光)
阴影是项目设置中质量设置的一部分,可通过“Edit/ Project Settings / Quality”找到。我们将使它们处于高质量水平。这意味着要使用高分辨率,稳定的投影效果,150的距离和四个级联来同时支持硬阴影和软阴影。
(阴影质量设置)
确保两个光都设置为投射柔和的阴影。它们的分辨率应取决于质量设置。
(每个灯光的阴影设置)
使用两个定向光投射阴影时,所有对象之间的空间关系会变得更加清晰。整个场景看起来既逼真又有趣。
(场景带有阴影)
1.2 阴影贴图
Unity是如何将这些阴影添加到场景中呢?标准着色器显然具有某种方法来确定射线是否被阻挡。
通过将光线从场景投射到表面片段,你可以找出点是否在阴影中。如果该射线在到达片段之前撞击了某物,则它将被阻挡。这是物理引擎可以做的,但是对每个片段和每个光这样做是非常不切实际的。然后,你还必须以某种方式将结果发送到GPU。
有一些技术可以支持实时阴影, 但每个都有其优点和缺点。Unity使用当今最常见的技术,即阴影贴图。这意味着Unity以某种方式将阴影信息存储在纹理中。现在,我们将研究其工作原理。
通过“Window / Frame Debugger”打开帧调试器,启用它,然后查看渲染步骤的层次结构。查看不带阴影的帧和带阴影的帧之间的区别。
(渲染过程,不带阴影和带阴影)
禁用阴影时,将照常渲染所有对象。我们已经熟悉此过程。但是,启用阴影后,该过程将变得更加复杂。还有更多的渲染阶段,还有很多DrawCall。
阴影很耗!
1.3 渲染到深度纹理
启用方向阴影后,Unity开始进行渲染过程的深度 pass。将结果放入与屏幕分辨率匹配的纹理中。此过程渲染整个场景,但仅记录每个片段的深度信息。这与GPU用于确定片段结束于先前渲染的片段之上还是之下的信息相同。
此数据与片段空间中片段的Z坐标相对应。这是定义相机可以看到的区域的空间。深度信息最终存储为0-1范围内的值。查看纹理时,附近的纹素看起来很暗。纹素越远,它变得越轻。
(深度纹理,相机的近切面设置为5)
什么是剪辑空间?
它是确定相机所见的空间。在场景视图中选择主摄像机时,你将在其前面看到金字塔线框,该金字塔线框表示可以看到的内容。
(相机视角,具有较大的近平面值)
在剪辑空间中,此金字塔是规则的立方体。模型-视图-投影矩阵用于将网格顶点转换为该空间。之所以称为剪切空间,是因为所有不可见的东西都将被剪切掉。
该信息实际上与阴影无关,但是Unity将在以后的过程中使用它。
1.4 (渲染到阴影贴图)
Unity渲染的下一个东西是第一个光的阴影贴图。再过一会儿,它也会渲染第二个灯光的阴影贴图。
再次渲染整个场景,并再次仅将深度信息存储在纹理中。但是,这次是从光源的角度渲染场景,让光充当照相机。这意味着深度值告诉我们一束光线在撞击某物之前经过了多远。这可以用来确定是否有阴影。
(2个阴影贴图,每一个拥有4个视点)
事实证明Unity不只为每个光源渲染整个场景一次,而是每个灯光渲染场景四次!纹理被分为四个象限,每个象限是从不同的角度渲染的。发生这种情况是因为我们选择使用四个阴影级联。如果要切换到两个级联,则每个光源将渲染两次场景。并且没有级联,每个灯光仅渲染一次。当我们看阴影的质量时,我们将看到为什么Unity会这样做。
1.5 收集阴影
从摄像机的角度来看,我们可以获得场景的深度信息。从每种光源的角度来看,我们也有此信息。当然,这些数据存储在不同的剪辑空间中,但是我们知道这些空间的相对位置和方向。这样我们就可以从一个空间转换为另一个空间。这使我们可以从两个角度比较深度测量值。从概念上讲,我们有两个向量在同一点结束。如果他们确实到在同一点结束了,则相机和灯光都可以看到该点,因此它是亮的。如果光的矢量在到达该点之前结束,则该光被遮挡,这意味着该点已被阴影化。
场景摄像机看不到点怎么办?
说明这些点被隐藏在距离相机更近的其他点后面。场景的深度纹理仅包含最接近的点。没必要浪费时间去计算看不见的点上。
(屏幕空间下的阴影 逐灯光)
Unity通过渲染一个覆盖整个视图的四边形来创建这些纹理。它为此过程使用Hidden / Internal-ScreenSpaceShadows着色器。每个片段都从场景和灯光的深度纹理中采样,进行比较,并将最终阴影值渲染到屏幕空间阴影贴图。光纹理像素设置为1,阴影纹理像素设置为0。这时,Unity还可以执行过滤以创建柔和阴影。
为什么Unity在渲染和收集之间交替显示?
每个光源都需要自己的屏幕空间的阴影贴图。但是从光的角度渲染的阴影贴图可以重复使用。
1.6 采样阴影贴图
最后,Unity完成渲染阴影。现在,场景已正常渲染,只进行了一次更改。浅色乘以存储在其阴影贴图中的值。这样可以消除应遮挡的光线。
渲染的每个片段都会采样阴影贴图。最终会隐藏在后面绘制的其他对象后面的片段。因此,这些片段最终可能会接收到最终隐藏它们的对象的阴影。在逐步调试帧时,你会看到此信息。你还可以看到阴影在实际投射阴影的对象之前出现。当然,这些错误只会在渲染帧时出现。完成后,图像是正确的。
(细致渲染的帧,包含奇怪的阴影)
1.7 阴影质量
从灯光的角度渲染场景时,其方向与场景摄像机不匹配。因此,阴影贴图的纹理像素与最终图像的纹理像素不对齐。阴影贴图的分辨率最终也会有所不同。最终图像的分辨率取决于显示设置。阴影图的分辨率由阴影质量设置决定。
当阴影贴图的纹理最终渲染成大于最终图像的纹理时,它们将变得非常明显。阴影的边缘将被混合。使用硬阴影时,这一点非常显眼。
(硬阴影和软阴影)
为了使此效果尽可能明显,请更改阴影质量设置,以便仅以最低的分辨率获得硬阴影,且不存在级联。
(低质量阴影)
现在非常明显,因为阴影就是一张纹理。同样,一些不应该出现的地方也会出现阴影。我们稍后再研究。
阴影越靠近场景相机,其纹理像素就越大。这是因为阴影贴图当前覆盖了场景摄像机可见的整个区域。我们可以通过质量设置来减少阴影所覆盖的区域,从而提高接近相机的质量。
(阴影距离减少到25)
通过将阴影限制在靠近场景摄像机的区域,我们可以使用相同的阴影贴图覆盖更小的区域。结果,我们得到了更好的阴影。但是失去了更远的阴影。随着接近最大距离,阴影逐渐消失。
理想情况下,我们可以使高质量的阴影近在咫尺,同时也可以使阴影远离。由于距离较远的阴影最终渲染到较小的屏幕区域,因此可以使用较低分辨率的阴影贴图来弥补。这就是阴影级联所做的。启用后,多个阴影贴图将渲染到同一纹理中。每个地图都在一定距离内使用。
(低分辨率贴图,4个级联)
使用四个级联时,即使我们仍使用相同的纹理分辨率,结果看起来也会好得多。只是更加有效地使用了纹理像素。不利的一面是,我们现在必须将场景渲染三遍。
在渲染到屏幕空间阴影贴图时,Unity会从正确的级联中进行采样。通过查找阴影纹素大小的突然变化,你可以找到一个级联结束而另一个级联开始的位置。
可以通过质量设置来控制级联频段的范围,作为阴影距离的一部分。你还可以通过更改其Shading Mode在场景视图中可视化它们。使用“Miscellaneous / Shadow Cascades”,替代Shaded。这将在场景上渲染级联的颜色。
(级联区域,调整为显示三个频段)
如何更改场景视图的显示模式?
场景视图窗口的左上方有一个下拉列表。默认情况下,它设置为“Shaded”。
级联带的形状取决于Shadow Projection质量设置。默认值为“Stable Fit”。在此模式下,根据到相机位置的距离选择频段。另一个选项是“Close Fit”,它改用相机的深度。这会在相机的视线方向上产生矩形带。
(Close Fit)
此配置允许更有效地使用阴影纹理,从而产生更高质量的阴影。但是,阴影投影现在取决于位置和方向或相机。结果,当摄像机移动或旋转时,阴影图也会改变。如果你可以看到阴影纹素,你会注意到它们在移动。这种效果被称为阴影边缘游泳,并且可能非常明显。这就是为什么其他模式是默认模式。
(阴影游泳)
Stable Fit阴影是否也取决于相机的位置?
是的,但是Unity可以对齐贴图,以便在相机位置更改时,让纹素看起来静止不动。当然,级联带确实会移动,因此带之间的过渡点会发生变化。但是,如果你没有注意到这些频段,那么你也不会注意到它们在移动。
1.8 阴影尖刺(Shadow Acne)
当我们使用低质量的硬阴影时,我们会看到一些阴影出现在不应该出现的地方。而且,无论质量设置如何,都可能发生这种情况。
阴影图中的每个纹理像素代表光线照射到表面的点。但是,纹素不是单点。它们最终会覆盖更大的区域。它们与光的方向对齐,而不是与表面对齐。结果,它们最终可能会像深色碎片一样粘在,穿过和伸出表面。由于部分纹理像素最终从投射阴影的表面戳出来,因此该表面似乎会产生自身阴影。这被称为阴影尖刺。
(阴影贴图引起的尖刺)
阴影尖刺的另一个来源是数值精度限制。当涉及到非常小的距离时,这些限制可能导致错误的结果。
(严重的尖刺表现,不使用偏移)
防止此问题的一种方法是在渲染阴影贴图时添加深度偏移。此偏差会加到从光到阴影投射表面的距离,从而将阴影推入表面。
(偏移阴影贴图)
阴影偏移是针对每个光源配置的,默认情况下设置为0.05。
(单个光源的阴影设置)
低的偏移会产生阴影尖刺,但较大的偏移会带来另一个问题。当阴影物体被推离灯光时,它们的阴影也被推开。结果,阴影将无法与对象完美对齐。使用较小的偏移时,效果还不错。但是太大的偏移会使阴影看起来与投射它们的对象断开连接。这种效果被称为peter panning。
(大的偏移会导致peter panning)
除了这种距离偏移之外,还存在“Normal Bias”。这是对阴影投射器的微妙调整。此偏移将阴影投射器的顶点沿着其法线向内推。这可以减少自阴影,但也会使阴影变小,并可能导致在阴影中出现孔洞。
那么最佳偏置设置是什么?
没有最佳设置。你必须自己进行调校。Unity的默认设置可能会起作用,但是它们也会产生不可接受的结果。不同的质量设置也会产生不同的结果。
1.9 抗锯齿
你是否在质量设置中启用了抗锯齿功能?如果有,那么你可能已经发现了阴影贴图的另一个问题。它们没有与标准的抗锯齿方法混合使用。
(使用抗锯齿时的锯齿表现)
在质量设置中启用抗锯齿功能后,Unity将使用多重采样抗锯齿功能MSAA。通过沿三角形边缘进行一些超级采样,可以消除这些边缘上的混叠。细节在这里无关紧要。重要的是,当Unity渲染屏幕空间阴影贴图时,它使用覆盖整个视图的单个四边形进行渲染。结果,没有三角形边缘,因此MSAA不会影响屏幕空间阴影贴图。MSAA确实适用于最终图像,但是阴影值直接从屏幕空间阴影贴图中获取。当靠近较暗表面的较亮表面被阴影覆盖时,这变得非常明显。亮和暗几何之间的边缘被消除锯齿,而阴影边缘则没有。
(没有AA,MSAA4,FXAA)
依靠图像后处理的抗锯齿方法(例如FXAA)不会出现此问题,因为它们是在渲染整个场景之后应用的。
这是否意味着我无法将MSAA与定向阴影结合使用?
可以,但是你会遇到上述问题。在某些情况下,它可能不会引起注意。例如,当所有表面颜色大致相同时,失真将很微小。当然你仍然会获得锯齿状的阴影边缘。
2 投射阴影
现在我们知道Unity如何为定向光创建阴影,是时候将其支持添加到我们自己的着色器中了。目前,“My First Lighting”既没有投射也没有阴影。
先处理阴影。在示例场景中更改了球体和圆柱体,让它们使用我们的材质。现在它们不再投阴影了。
(我们的材质,没有阴影投下来)
我们知道Unity多次渲染场景以获得定向阴影。对于每个阴影贴图级联,一次用于深度pass,一次用于光。屏幕空间阴影贴图是一种屏幕空间效果,与我们无关。
为了支持所有相关的pass,我们必须向它的着色器添加一个pass,其照明模式设置为ShadowCaster。因为我们只对深度值感兴趣,所以它将比其他pass操作简单得多。
给影子程序一个自己的包含文件,名为My Shadows.cginc。它们很简单。顶点程序像往常一样将位置从对象空间转换为剪切空间,并且不执行其他任何操作。片段程序实际上不需要执行任何操作,因此只需返回零即可。GPU会为我们记录深度值。
这就已经足以定向投射阴影了。
(投射阴影)
2.1 偏差
我们还必须支持阴影偏差。在深度遍历期间,偏差为零,但是在渲染阴影贴图时,偏差对应的灯光设置。通过在剪辑空间中将深度偏差应用于顶点着色器中的位置,可以实现此目的。
为了支持深度偏差,我们可以使用UnityCG中定义的UnityApplyLinearShadowBias函数。
UnityApplyLinearShadowBias如何工作?
它将增加剪辑空间中的Z坐标。使它复杂化的是它正在使用齐次坐标。必须补偿透视投影,以使偏移量不会随着距相机的距离而变化。还必须确保结果不会超出范围。
为了也支持法向偏差,我们必须基于法线向量移动顶点位置。因此,需要将法线添加到顶点数据中。然后,我们可以使用UnityClipSpaceShadowCasterPos函数应用偏差。此功能也在UnityCG中定义。
UnityClipSpaceShadowCasterPos如何工作?
它将位置转换为世界空间,应用法线偏差,然后转换为剪辑空间。确切的偏移量取决于法线和光照方向之间的角度以及阴影纹理像素大小。
UnityObjectToClipPos函数仅执行模型-视图-投影矩阵乘法,使用立体渲染时需要注意。
现在,我们的着色器是功能齐全的阴影投射器了。
3 接受阴影
第二部分是接收阴影。现在,把测试场景中的所有对象都换成我们的材质。
(全部使用我们自己的材质之后,阴影不再被接收了)
首先让我们只关注主方向光的阴影。由于此光包含在基本通道中,因此我们必须对其进行调整。
当主定向光投射阴影时,Unity将查找启用了SHADOWS_SCREEN关键字的着色器变体。因此,我们必须创建基本pass的两个变体,一个带有此关键字,另一个不带有此关键字。这与VERTEXLIGHT_ON关键字的作用相同。
现在,该pass具有两个多重编译指令,每个指令用于一个关键字。结果,有四个可能的变体。一个没有关键字,每个关键字一个,两个关键字都有一个。
添加了多编译指示后,着色器编译器将报错_ShadowCoord不存在。发生这种情况是因为在播放阴影时,UNITY_LIGHT_ATTENUATION宏的行为有所不同。要快速解决此问题,请打开My Lighting.cginc文件,并在有阴影时将衰减设置为1。
3.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坐标。
(翻转阴影)
3.2 使用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的宏对顶点数据和插值器结构进行了假设。首先,假设顶点位置命名为vertex,而我们将其命名为position。其次,假定内插器位置命名为pos,但我们将其命名为position。
我们老实一点,也采用这些名称。不管如何,它们仅在少数几个地方使用,因此我们不必进行太多更改。
我们的影子应该再次起作用,这次是在Unity支持的平台上。
这些宏是什么样的?
最终使用哪些宏版本取决于启用了哪些着色器关键字以及支持的功能。定义SHADOWS_SCREEN后,你将得到以下代码。
请注意,仅当同时定义了UNITY_NO_SCREENSPACE_SHADOWS和SHADOWS_NATIVE时,才使用阴影坐标的Z分量。
tex2Dproj函数的作用与tex2D相同,但是它还负责XY / W划分。查看已编译的代码时,你可以看到此信息。
3.3 多阴影
现在,主要的定向光正在投射阴影,但是第二个定向光仍然没有。那是因为我们还没有在附加pass中定义SHADOWS_SCREEN。向其中添加多编译语句,但是SHADOWS_SCREEN仅适用于定向光。要获得正确的关键字组合,请将现有的多编译语句更改为还包含阴影的语句。
这会将四个其他关键字添加到混合中,以支持不同的光源类型。
(投射阴影的两个定向光)
4 聚光灯阴影
现在,我们已经处理了定向光,让我们继续关注聚光灯。禁用定向光,并向场景添加一些带有阴影的聚光灯。惊喜!多亏了Unity的宏,聚光灯阴影直接可以工作了。
(2个聚光灯的阴影)
查看帧调试器时,你会发现Unity对聚光灯阴影的工作较少。没有单独的深度pass,也没有屏幕空间阴影传递。仅渲染阴影贴图。
(使用聚光灯阴影渲染)
阴影贴图与定向光的作用相同。它们是深度图,是从灯光的角度渲染的。但是,定向光和聚光灯之间存在很大差异。聚光灯具有实际位置,并且光线不平行。因此,聚光灯的摄像机具有透视图。结果,这些灯不能支持阴影级联。
(近平面的阴影贴图设置为4)
尽管相机设置不同,但是两种光源的阴影投射代码相同。仅对定向阴影支持法线偏差,对于其他光源,将其设置为零。
4.1 采样阴影贴图
由于聚光灯不使用屏幕空间阴影,因此采样代码必须不同。但是Unity的宏对我们隐藏了这种差异。
聚光灯下的宏长什么样?
通过将顶点位置转换为世界空间,然后从顶点位置转换为光源的阴影空间,可以找到阴影坐标。
我们只需对屏幕空间阴影贴图进行采样即可找到定向阴影。在创建该地图时,Unity会进行阴影过滤,因此我们不必为此担心。但是,聚光灯不使用屏幕空间阴影。因此,如果要使用柔和阴影,则必须在片段程序中进行过滤。
然后,SHADOW_ATTENUATION宏使用UnitySampleShadowmap函数对阴影贴图进行采样。此功能在AutoLight包含的UnityShadowLibrary中定义。使用硬阴影时,该函数对阴影贴图采样一次。使用柔和阴影时,它将对地图进行四次采样并取平均值。结果不如用于屏幕空间阴影的过滤效果好,但速度要快得多。
(聚光灯阴影 硬VS软)
UnitySampleShadowmap是什么样的?
此功能有两种版本,一种用于聚光灯,另一种用于点灯。这是聚光灯之一。
_ShadowOffsets包含平均用于创建柔和阴影的四个样本的偏移量。在下面的代码中,我仅显示了这四个示例中的第一个。
5 点光源阴影
现在尝试点光源。当为点光源启用阴影时,会遇到编译错误。显然,UnityDecodeCubeShadowDepth是未定义的。发生此错误是因为UnityShadowLibrary依赖UnityCG,但未明确包含它。因此,我们必须确保首先包含UnityCG。为此,我们可以在将“AutoLight”添加到“My Lighting”之前,先添加UnityPBSLighting。
它可以编译,但是灯光范围内的所有对象最终都变成黑色。阴影贴图有问题。
(错误的阴影贴图)
当你通过帧调试器检查阴影贴图时,你会发现每个灯光不是渲染一张,而是渲染六张贴图。发生这种情况是因为点光源向各个方向发光。结果,阴影贴图必须是立方体贴图。通过在相机指向六个不同方向的情况下渲染场景来创建立方体贴图,每个立方体的每个面一次。因此,点光源的阴影非常昂贵。
5.1 投射阴影
不幸的是,Unit不使用深度立方体贴图。显然,没有足够的平台支持它们。因此,我们不能依靠“My Shadows”中片段的深度值。取而代之的是,我们必须输出片段的距离作为片段程序的结果。
渲染点光源阴影贴图时,Unity将使用定义的SHADOWS_CUBE关键字查找阴影投射器变体。SHADOWS_DEPTH关键字用于定向和聚光灯阴影。为了支持这一点,在我们的通道中添加一个特殊的针对影子投射器的多编译指令。
这将添加我们需要的变体。
因为点光源需要这种不同的方法,所以让我们为它们创建一套单独的程序功能。
为了弄清楚一个片段到灯的距离,我们必须构造一个从灯到片段的世界空间向量。可以通过在每个顶点上创建这些向量并进行插值来实现。这需要一个附加的内插器。
在片段程序中,我们取光向量的长度,并对其加上偏差。然后,将其除以光线的范围以使其适合0~1范围。_LightPositionRange.w变量包含其范围的倒数,因此我们必须乘以该值。结果作为浮点值输出。
UnityEncodeCubeShadowDepth是做什么的?
Unity更喜欢使用浮点立方体贴图。在可能的情况下,此功能不执行任何操作。如果无法做到这一点,Unity将对值进行编码,以便将其存储在8位RGBA纹理的四个通道中。
(正确的阴影贴图)
5.2 采样阴影贴图
现在我们的阴影贴图正确了,将出现点光阴影。Unity的宏负责这些贴图的采样。
(点光源阴影)
点光源,宏看起来是什么样的?
在这种情况下,构造与投射阴影时相同的光矢量。然后,使用此向量对阴影立方体贴图进行采样。请注意,内插器仅需要三个组件,而不是四个。这次我们不传递齐次坐标。
在这种情况下,UnitySampleShadowmap会采样立方体贴图而不是2D纹理。
与聚光灯阴影一样,阴影贴图对硬阴影采样一次,对软阴影采样四次。最大的区别是Unity不支持对阴影立方体贴图进行过滤。结果,阴影的边缘更加粗糙。因此,点光阴影既昂贵,锯齿又强。
(点光源的阴影 硬VS软)
如何制作柔和的灯笼阴影?
使用一个或多个阴影聚光灯。如果附近没有其他阴影投射对象,则可以将未阴影的光线与cookie一起使用。这既适用于聚光灯也适用于点光源,并且渲染起来便宜很多。
下一章 介绍反射。