在实时渲染中,最常使用的是Shadow Map技术,它会首先把摄像机的位置放在和光源重合的地方,场景中该光源的阴影区域就是那些摄像机看不到的地方。
而在前向渲染中,如果场景中平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadowmap),这张阴影映射纹理的本质就是一张深度图,它记录了从该光源的位置出发,能看到的场景中距离它最近的表面位置(深度信息)
在Unity Shader中实现时,需要选择一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass的LightMode标签被设为ShadowCaster,这个Pass的目标不是帧缓存,而是阴影映射纹理。
而如果没有ShadowCaster的话,Unity会自动按Fallback指定的Unity Shader中寻找到LightMode为Shadow Caster的Pass。如果还是没有的话则该物体就无法向其他物体投射阴影。
首先 调用LightMode为ShadowCaster的Pass(即阴影Pass)来得到可投影阴影的 光源阴影映射纹理 以及 摄像机的深度纹理。
然后根据 光源阴影纹理 与 摄像机的深度纹理 得到 屏幕空间的阴影图
如果 摄相机的深度图 中记录的表面深度大于转换到 阴影纹理映射纹理 中的深度值,就表明该表面虽然可见,但却处于该光源的阴影之中。
通过这样的方式,阴影图中就包含了屏幕空间中所有有阴影的区域。
如果我们想得到一个物体接收来自其他物体的投影,只需要在Shader中对 阴影图 进行采样。
由于 阴影图 是 屏幕空间 下的,因此需要吧表面坐标从 模型空间 转换到 屏幕空间 中,然后使用这个坐标对 阴影图 进行采样即可。
总结一下,一个物体接受来自其他物体的投影,以及它向其他物体投射阴影,这是两个过程。
如果我们想要一个物体接受来自其他物体的阴影,那就必须在Shader中对 阴影映射纹理(包括屏幕空间下的阴影图) 进行采样,把采样的结果
与最后的光照结果相乘得到阴影结果。
如果我没想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的 阴影映射纹理 的计算中,从而让其他物体在对阴影映射纹理采样时
可以得到该物体的相关信息。
实践
Cast Shadows的选项有4个,分别是On, Off, TwoSided, Shadow Only,分别代表了开启,关闭,允许对物体的所有面计算阴影信息,只计算阴影信息。这整个过程都是通过LightMode设置为ShadowCaster的Pass来实现的。
下面的Receive Shadows则是设置是否让该物体接收来自其他物体的阴影。
让物体接收阴影
1. 首先需要在Base Pass中包含新的内置文件 #include "AutoLight.cginc"
这是因为计算阴影的所有宏都是在这个文件里声明的。
2. 在v2f里添加一个内置宏SHDOW_COORDS(2)
它的作用是声明一个用于对阴影纹理采样的坐标。这个宏的参数需要是下一个可用插值寄存器的索引值,本例中是2。
3. 在顶点着色器vert返回前添加一个内置宏TRANSFER_SHADOW
这个宏用来在顶点着色器中计算上一步中声明的阴影纹理坐标。
4. 在片元着色器frag中计算阴影值,使用了内置宏SHADOW_ATTENUATION
以上三个内置宏(SHADOW_COORDS, TRANSFER_SHADOW, SHADOW_ATTENUATION) 就是计算阴影的三个主要部分。可以在AutoLight。cginc中找到它们的声明。
SHADOW_COORDS 声明了一个名为_ShadowCoord的阴影纹理坐标变量。
TRANSFER_SHADOW 会根据平台的不同而有所差异,如果当前平台可以使用屏幕空间的阴影映射技术(SCREENSPACE_SHADOWS),则会调用内置的ComputeScreenPos函数计算_ShadowCoord; 如果不支持则会使用传统的阴影映射技术,TRANSFER_SHADOW会把顶点坐标从模型空间转换到光源空间后存储到_ShadowCoord中。
SHADOW_ATTENUATION 负责使用_ShadowCoord对对应的纹理进行采,得到阴影信息。
由于这些宏会使用上下文的变量进行计算,所以在命名变量时要保证与宏中使用的变量名相匹配,所以在a2v结构体中的顶点坐标变量必须为vertex,frag中的输入结构体a2v必须为v,且v2f中的顶点位置变量必须命名为pos。