1.阴影是如何实现的

       当一个光源发射的一条光线遇到一个不透明物体时,这条光线就不可以再继续照亮其他物体(这里不考虑光线反射)。因此,这个物体就会向它旁边的物体投射阴影,阴影区域的产生是因为光线无法到达这些区域。

       在实时渲染中,我们最常使用的是一种名为Shadow Map的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是哪些摄像机看不到的地方。

      在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。

       那么,在计算阴影映射纹理时,我们如何判断距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按正常的渲染流程,即调用Base Pass和Additional Pass来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而Base Pass和Additional Pass中往往涉及很多复杂的光照模型计算。因此,Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback指定的Unity Shader中继续寻找,如果仍然没有找到,该物体就无法想其他物体投射阴影(但它仍然可以接收其他物体的阴影)。当找到了一个LightMode为ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。

       在传统的阴影映射纹理的实现中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后,我们使用xy分量对阴影映射纹纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由z分量得到),那么说明该点位于阴影中。但在Unity5中,Unity使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有的平台Unity都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持MRT,而有些移动平台不支持这种特性。

       当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。

总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。

  • 如果我们想要一个屋里接收来自其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
  • 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现的。如果使用了屏幕空间的投影映射技术,Unity还会使用这个Pass产生一张摄像机的深度纹理。

2.不透明物体的阴影

   1.让物体投射阴影

           在Unity中,我们可以选择是否让一个物体投射或接收阴影。通过设置Mesh Renderer组件中的Cast Shadows和Receive Shadows属性来实现。Cast Shadows可以被设置为开启(On)或关闭(Off)。如果开启了Cast Shadows属性,那么Unity就回吧该物体加入到光源到阴影映射纹理到计算中,从而让那个其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。正如之前所说,这个过程时通过为该物体执行LightMode为ShadowCaster的Pass来实现的。Receive Shadows则可以选择是否让物体接受来自其他物体的阴影。如果没有开启Receive Shadows。那么当我们调用Unity内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接收阴影的功能,就不会在内部为我们计算阴影。

       在Unity内置的SpecularShader中,它的FallBack调用了VertexLit,它会继续回调,并最终回调到了内置的VertexLit。打开这个Shader我们可以看到LightMode为ShadowCaster的pass:

unity ui轮廓阴影 unity阴影条状_变量名

2.让物体接收阴影

unity ui轮廓阴影 unity阴影条状_变量名_02

unity ui轮廓阴影 unity阴影条状_阴影效果_03

unity ui轮廓阴影 unity阴影条状_阴影效果_04

unity ui轮廓阴影 unity阴影条状_深度图_05

上面的代码看起很多,很复杂,实际上只是Unity为了处理不同光源类型、不同平台而定义了多个版本的宏。在前向渲染中,宏SHADO_COORDS实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量。而TRANSFER_SHADOW的实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了UNITY_NO_SCREENSPACE_SHADOWS来得到 ),TRANSFER_SHADOW会调用内置的ComputeScreenPos函数来计算_ShadowCoord:如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW会把顶点坐标从模型空间变换到光源空间后存储到_ShadowCoord中。然后,SHADOW_ATTENUATION负责使用_ShadowCoord对相关纹理进行采样,得到阴影信息。 

        注意到,上面内置代码的最后定义了在关闭阴影时的处理代码。可以看出,在关闭了阴影后,SHADOW_COORDS和TRANSFER_SHADOW实际没有任何作用,而SHADOW_ATTENUATION会直接等同于数值1。

        需要注意的是,由于这些宏中会使用上下文变量来计算相关计算,例如 TRANSFER_SHADOW会使用v.vertex或a.pos来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:a2f结构体中的顶点坐标变量名必须是vertex,顶点着色器的输出结构体v2f必须命名为v,且v2f中的顶点位置变量必须命名为pos。

3.透明物体的阴影

       我们从一开始就强调,想要在Unity里让物体能够向其他物体投射阴影,一定要在它使用的Unity Shader中提供一个LightMode为ShadowCaster的Pass。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们需要小心设置这些物体的Fallback。

       透明度测试的处理比较简单,但如果我们仍然直接使用VertexLit、Diffuse、Specular等作为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而VertexLit中阴影投射纹理并没有进行这样的操作。

   

unity ui轮廓阴影 unity阴影条状_阴影效果_06

unity ui轮廓阴影 unity阴影条状_阴影效果_07

这种写法在镂空区域的处理上会有问题,我们需要把FallBack设置为Transparent/Cutout/VertexLit。但需要注意的是,由于Transparent/Cutout/VertexLit中计算透明度测试时,使用了名为_Cutoff的属性来进行透明度测试,因此,这要求我们的Shader中也必须提供为名_Cutoff的属性。否则,同样无法得到正确的阴影结果。

       但是,这样的记过人有问题,我们需要将正方体Mesh Renderer组件中的Cast Shadows属性设置为Two Sided,强制Unity在计算阴影映射纹理时计算所有面的深度信息。