第八章 更复杂的光照(4)

@

Unity的阴影

为了让场景看起来更加真实,具有深度信息,我们通常希望光源可以把一些物体的阴影投射在其它物体上。在本节,我们就来学习如何在Unity中让一个物体向其他物体投射阴影,以及如何让一个物体接收来自其它物体的阴影。

1. 阴影是如何产生的

我们首先可以考虑真实生活中阴影是如何产生的。当一个光源发射的一条光线遇到一个不透明物体时,这条光线就不可以继续照亮其他物体(这里不考虑光线发射)。因此这个物体就会向其他物体投射阴影,那些阴影区域的产生是因为光线无法到达这些区域。
在实时渲染中,我们最常使用是一种名为Shadow Map的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方,而Unity使用的就是这样的技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
那么,在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源位置上,然后按正常的渲染流程,即调用Base Pass和Additional Pass来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为实际上我们仅仅需要深度信息而已,而Base Pass和Additional Pass往往会涉及很多复杂的光照模型计算。因此Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。Unity首先把摄像机放置到光源位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的UnityShader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback指定的Unity Shader中继续寻找,如果仍然没有找到,该物体就无法向其它物体投射阴影(但它仍然可以接收来自其它物体的阴影)。当找到一个LightMode为ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。
在传统的阴影映射纹理的实现中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后我们使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由z分量得到),那么说明该点位于阴影中。但在Unity5中,Unity使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screenspace Shadow Map)。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有平台的Unity都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持MRT,而有些移动平台不支持这种特性。
当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理的深度值,就说明该表面虽然是可见的,但却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其它物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结一下,一个物体接收来自其它物体的阴影,以及它向其它物体投射阴影是两个过程。
●如果我们想要一个物体接收来自其它物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后光照结果相乘来产生阴影效果。
●如果我们想要一个物体向其它物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其它物体对阴影映射纹理采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现的。如果使用了屏幕空间的投影映射技术,Unity还会使用这个Pass产生一张摄像机的深度纹理。在下面的章节中,我们会学习如何在Unity中实现上面两个过程。

2. 不透明物体的阴影

为了让场景中可以产生阴影,我们首先需要让平行光可以收集阴影信息。这需要在光源的Light组件中开启阴影,如下图所示。在本例中,我们选择了软阴影(Soft Shadows)。
在这里插入图片描述

2.1 让物体投射阴影

在Unity中,我们可以选择是否让一个物体投射或接收阴影。这是通过设置Mesh Renderer组件中的Cast Shadows和Receive Shadows属性来实现的。如下图所示:
在这里插入图片描述
Cast Shadows可以被设置为开启(On)或关闭(Off)。如果开启了Cast Shadows属性,那么Unity就会把该物体加入光源的阴影映射纹理的计算中,从而让其它物体在对阴影映射纹理采样时可以得到该物体的相关信息。正如之前所说,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass实现的。Receive Shadows则可以选择是否让物体接收来自其它物体的阴影。如果没有开启Receive Shadows,那么当我们调用Unity的内置宏和变量计算阴影(在后面我们会看到如何实现)时,这些宏通过判断该物体没有开启接收阴影的功能,就不会在内部为我们计算阴影。
我们把正方体和两个平面的Cast Shadows和Receive Shadows都设为开启状态,可以得到下图的效果:
在这里插入图片描述
从上图可以发现,尽管我们没有对正方体使用的Chapter9-ForwardRendering进行任何更改,但正方体仍然可以向下面的平面投射阴影。一些读者可能会有疑问,之前不是说Unity要用LightMode为ShadowCaster的Pass来渲染阴影映射纹理和深度图的吗?但是Chapter9-ForwardRendering中并没有这样一个Pass啊。没错,我们在Chapter-ForwardRendering的SubShader只定义了两个Pass——一个Base Pass,一个Additional Pass。那为什么它还可以投射阴影呢?实际上,秘密就在于其中的Fallback语义。

Fallback"Specular"

我们为Fallback指定了一个用于回调的Unity Shader,即内置的Specular。虽然Specular本身也没有包含这样一个Pass,但是由于它的Fallback调用了VertexLit,它会继续回调,并最终回调到内置的VertexLit。我们可以在Unity内置的着色器里找到它:builtin-shaders-xxx->DefaultResourcesExtra->NormalVertexLit.shader。打开它,我们可以看到传说中的LightMode为ShadowCaster的Pass了:

//Pass to render object as a shadow caster
Pass{
Name"ShadowCaster"
Tags{"LightMode"="ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct v2f{
V2F_SHADOW_CASRER;
};
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f i):SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}

上面的代码非常短,尽管有一些宏和指令是我们之前没有遇到的,但它们的用处实际上就是为了把深度信息写入渲染目标中。在Unity5中,这个Pass的渲染目标可以是光源的阴影映射纹理,或是摄像机的深度纹理。
如果我们把Fallback注释掉,就会发现正方体不会再向平面投射阴影了。当然,我们可以不依赖Fallback,而自行在SubShader中定义自己的LightMode为ShadowCaster的Pass。这种自定义的Pass可以让我们更加灵活的控制阴影的产生。但由于这个Pass的功能是可以在多个Unity Shader间通用的,因此直接Fallback是一个更加方便的用法。在之前的章节中,我们有时也在Fallback中使用内置的Diffuse,虽然Diffuse本身也没有包含这样的一个Pass,但是由于它的Fallback调用了VertexLit,因此Unity最终还是会找到一个LightMode为ShadowCaster的Pass,从而可以让物体产生阴影。在下小结中我们可以看到LightMode为ShadowCaster的Pass对产生阴影的重要性。
上图中还有一个有意思的现象,就是右侧的平面并没有向最下面的平面投射阴影,尽管它的Cast Shadows已经被开启了。
在默认情况下,我们在计算光源的阴影映射纹理时会剔除掉物体的背面,但对于内置平面来说,它只有一个面,因此在本例中计算阴影映射纹理时,由于右侧的平面在光源下没有任何正面(front face),因此就不会添加到阴影映射纹理中。我们可以将Cast Shadows设置为Two Sided来允许对物体的所有面都计算阴影信息。下图给出了当把右侧平面的Cast Shadows设置为Two Sided后的结果。
在这里插入图片描述
在本例中,最下面的平面之所以可以接收阴影是因为它使用了内置的Standard Shader,而这个内置的Shader进行了接收阴影的相关操作。但由于正方体使用的Chapter-9-ForwardRendering并没有对阴影进行任何处理,因此它不会显示出右侧平面投射来的阴影,在下一节中我们将学习如何让正方体也接收阴影。

posted @ 2019-12-08 10:37  御坂御坂001  阅读(287)  评论(0编辑  收藏  举报