在计算阴影映射纹理时其中一种方法是:先把摄像机放置到光源的位置,然后按正常的渲染流程,即调用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分量得到),那么说明该点位于阴影中。但Unity使用的是屏幕空间的阴影映射技术。因此Unity会首先通过调用LightMode为ShadowCaster的Pass来得到可投影阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明虽然该表面可见的,但是却处于该光源的阴影中。通过这种方式,阴影图就包含了屏幕空间中所有的阴影区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间转换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结:
- 如果我们想要一个物体接收来自其他物体的阴影,就必须在shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。该效果在不使用unity内置shader的情况下需要手动编写代码实现。并在Unity的模型中的MeshReder组件中勾选ReceiveShadows:
- 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在shader中使用FallBack语句会自动实现其效果。在Unity中,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现的。如果使用了屏幕空间的投影映射技术(因为该技术需要MRT显卡支持,所以Unity对不支持该技术的移动平台不使用该技术),Unity还会使用这个Pass产生一张摄像机的深度纹理。对应的Unity模型的MeshRender组件的参数设置如下:
对于CastShadow参数说明如下:
Off:关闭向其他物体投射阴影。
On:开启向其他物体投射阴影,但会剔除物体背面光源的阴影映射纹理的计算。其结果是看不到该物体背面投射到其他物体表面的阴影。
Two Sided:允许对所有的面都计算阴影信息。
Shadows Only:仅计算阴影信息,该物体本身会不可见。
注意:勾选ReceiveShadows选项并不能自动接收到来自其他物体的阴影,这是因为使用Unity自带的shader进行了接收阴影的相关操作,而我们自己写的shader需要我们手动写代码来实现。
因此,不透明物体接收来自其他物体的阴影的计算代码如下:
Shader "Custom/Shadow"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
//#pragma multi_compile_fwdadd指令可以保证我们在shader中使用光照衰减等光照变量可以被正确赋值
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "AutoLight.cginc"
#include "Lighting.cginc"
float4 _Color;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
SHADOW_COORDS(2) //声明阴影坐标,参数2表示下一个可用的插值寄存器的索引值
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//顶点的模型空间下的坐标转换成世界空间下的坐标
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//模型空间下的法线向量转换成世界空间下的法线向量
o.worldNormal = UnityObjectToWorldNormal(v.normal);
TRANSFER_SHADOW(o); //计算上一步声明的阴影纹理坐标
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//获取直接空间下的单位法线向量
fixed3 worldNormal = normalize(i.worldNormal);
//获取世界空间下的光照方向的单位向量
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
//获取世界空间下的视角方向的单位向量
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//获取环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//计算漫反射
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
//高光反射计算公式
fixed3 halfDir = normalize(worldLightDir + worldViewDir);
fixed3 specular = _LightColor0.rgb * _Color.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
/*
该内置宏帮我们声明了变量atten(再次声明会报错),并存储光照衰减和阴影值相乘后的结果。
参数一:声明的变量
参数二:结构体v2f的值,传递使用的SHADOW_ATTENUATION。
使用语句fixed shadow = SHADOW_ATTENUATION(i)可得到其阴影纹理,与漫反射和高光反射的结果相乘即可得到阴影。
参数三:世界空间的坐标。
该参数会用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减。
*/
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}