unity Shader之——unity的阴影

为了让场景看起来更加真实,具有深度信息,我们通常希望光源可以把一些物体的阴影投射到其他物体上,下面介绍如何让一个物体向其他物体投射阴影,以及如何让一个物体接收来自其他物体的阴影。

一、阴影是如何实现的?

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

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

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

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

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

(1)如果我们想要一个物体接收来自其他物体的阴影,就必须在shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。

(2)如果想要一个物体向其他物体投射阴影,就必须把物体加入到光源的阴影映射纹理的计算中,从而让其他物体对阴影映射纹理的采样时可得到该物体的相关信息,在unity中这个过程是通过为物体执行lightMode为shadowCaster的Pass来实现的,如果使用了屏幕空间的阴影投射技术,unity还会使用这个pass产生一张相机的深度纹理。

二、不透明物体的阴影

1. 让物体投射阴影

通过设置物体的mesh renderer组件中的cast shadows和receive shadows属性来实现物体投射或接收阴影。

cast shadows可以被设置为开启或关闭,如果开启,那么unity会把物体加入到光源的阴影映射纹理计算当中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息,这个过程是通过shadowcaster的pass实现的(把深度信息写入渲染目标中)。receive shadows则可以选择是否让物体接收来自其他物体的阴影,如果没有开启,当我们调用unity内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接收阴影的功能,就不会再内部为我们计算阴影。

如果没有shadowcaster的pass块,但仍然物体可以投射阴影,是因为Fallback里面的回调,一般会层层调用了内置的VertexLit  shader,这里面就有这个pass块。如果把fallback注释掉,就发现物体不在平面投射阴影了。虽然可以自定义shadowcaster的pass,让我们更加灵活的控制阴影的产生。但这个pass的功能通常是在多个unity shader间通用的,因此直接fallback是一个更加方便的用法。

一般计算光源的阴影映射纹理时时会剔除掉物体的背面,但由于内置的平面来说,他只有一个面,有时候平面在光源空间下没有任何证明,因此就不会添加到阴影映射纹理中,我们可以将cast shadows设置为Two Sided来允许对物体的所有面都计算阴影信息。

2. 让物体接收阴影

为了让物体接收阴影,

  (1)先在shader包含一个内置文件 #include "AutoLight.cginc",下面计算阴影时所用的宏东升在这个文件中声明的。

(2)在顶点着色器的输出结构体v2f中添加了一个内置宏SHADOW_COORDS(X),这个宏的作用很简单,就是声明一个用于对阴影纹理采样的坐标,宏的参数需要是下一个可用的插值寄存器的索引值。

(3)在顶点着色器返回之前添加另一个内置宏TRANSFER_SHADOW(o),这个宏用于顶点着色器中计算上一步声明的阴影纹理坐标。

(4)最后在片元着色器中计算阴影值,这同样使用了一个内置宏 SHADOW_ATTENUATION。

总结:在前向渲染中,宏SHADOW_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,顶点着色器输入结构体a2v必须命名为v,且v2f中顶点坐标变量必须命名为pos。

(5)最后需要把阴影值shadow和漫反射以及高光反射颜色相乘即可。

(6)要计算光照衰减和阴影同时计算,用内置的UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos)宏处理,接收三个参数,会将光照衰减和阴影值相乘后的结果存储到第一个参数。第二个参数结构体是v2f,这个参数会传递给之前的SHADOW_ATTENUATION,用来计算阴影值。第三个参数是世界空间的坐标,用于计算光源空间下的坐标,再对光照衰减纹理采样得到光照衰减。如果希望在Additional Pass添加阴影效果,就需要使用#pragma_multi_compile_fwdadd_fullshadows变异指令代替#pragma_multi_compile_fwdadd,这样unity也会为这些额外的逐像素光源计算阴影,并传递给shader。

三、透明度物体的阴影

透明或半透明物体的实现通常会使用透明度测试或透明度混合,小心设置这些物体的fallback,透明度测试需要在片元着色器中舍弃某些片元,而VertexLit中的阴影投射纹理并没有进行这样的操作,pass中并没有任何透明度测试的计算,因此他会把整个物体的深度信息渲染到深度图和阴影映射纹理中。所以想得到经过透明度测试后的阴影效果,需要提供一个有透明度测试功能的shadowCaster Pass,可以自行编写,也可以把Fallback设置为Transparent/Cutout/VertexLit,它的shadowCaster Pass计算了透明度测试,它使用了名为_Cutoff是属性来进行透明度测试,因此我们的shader中也必须提供名为_Cutoff的属性,否则无法得到正确的阴影结果。

与透明度测试的物体相比,想要为透明度混合的物体添加阴影比较复杂,事实上所有内置的透明度混合的unity shader,如Transparent/VertexLit等,都没有包含阴影投射的Pass,这意味着这些半透明的物体不会参与深度图和阴影映射纹理的计算,不投影也不接收。unity会这样处理半透明物体也是有原因的,由于透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成,想要为这些半透明的物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影的处理变得非常复杂,也会影响性能。当然可以使用一些dirty trick来强制为半透明物体生成阴影,fallb设置为Vertexlit、Diffuse这些不透明物体使用的unity shader,然后开启阴影投射合计接收属性,就产生阴影了,但这样其实是不正确的。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/100395363

最新文章