产生阴影的原理:光沿直线传播
即,从光源出发,看不到的地方都处于阴影中
Unity处理阴影的两种途径
传统的阴影映射
- 调用LightMode为ShadowCaster的Pass,获取光源的阴影映射纹理ShadowMap(包含可以被光源照亮的点的z值)
- 在正常渲染的Pass中,将顶点转换到光源空间中,获取该顶点在光源空间中的xy坐标,以及深度值z
- 使用顶点在光源空间中的xy坐标,对ShadowMap进行采样获得深度值Z
- 若z > Z,则该点处于阴影中
屏幕空间的阴影映射(Screenspace Shadow Map)
原本为延迟渲染中的方法,需要显卡支持MRT,否则使用传统的阴影映射处理
- 调用LightMode为ShadowCaster的Pass,获取光源的阴影映射纹理ShadowMap(可以被光源照亮的点的z值),和摄像机的深度纹理DepthMap(可以显示在屏幕上的点的z值)
- 使用ShadowMap和DepthMap计算获得屏幕空间的阴影图(显示在屏幕上的每一个像素是否可以被光源照亮的信息)
阴影的投射与接收
阴影的投射
若想让一个物体投射阴影给其他物体,需要将该物体添加到光源的阴影映射纹理ShadowMap的计算中(即shader中应包含LightMode为ShadowCaster的Pass,如果没有就会去FallBack中查找),否则该物体无法投射阴影
在Unity中,需要在Inspector中MeshRender组件的Lighting-CastShadows属性进行设置
注意:一些特殊情况下可能需要将CastShadows设置为“Two Sided”,因为默认情况下在计算光源阴影映射纹理时会剔除掉网格的背面,导致无法生成阴影
阴影的接收
若想让一个物体接收阴影,需要在着色时对光源的阴影映射纹理ShadowMap进行采样,并对光照结果进行相乘处理
在Unity中,需要勾选Inspector中MeshRender组件的Lighting-ReceiveShadows选项
ShadowCaster Pass
功能:将深度信息写入渲染目标中
渲染目标:光源的阴影映射纹理,或摄像机的深度纹理
所在内置着色器:VertexLit
使用:一般情况下不需要自己写,而是直接FallBack到含有该Pass的内置着色器
具体内容(提取于Unity内置着色器:builtin_shaders-2019.4.21f1.zip\DefaultResourcesExtra\Normal-VertexLit.Shader),大概了解一下:
// Pass to render object as a shadow caster
Pass {
Name "ShadowCaster"
Tags {
"LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#pragma multi_compile_shadowcaster
// allow instanced shadow pass for most of the shaders
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert( appdata_base v )
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag( v2f i ) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
其中appdata_base为内置结构体,包含顶点位置、顶点法线、第一组纹理坐标
阴影接收相关的内置宏
阴影计算“三剑客”
SHADOW_COORDS
作用:声明一个用于对阴影纹理采样的坐标
struct v2f {
// *为了宏正确执行,与SV_POSITION绑定的变量名必须为pos
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
// TEXCOORD0、TEXCOORD1已被使用,下一个可用的纹理坐标为TEXCOORD2
SHADOW_COORDS(2)
};
TRANSFER_SHADOW
作用:计算阴影纹理坐标
struct a2v {
// * 为了宏正确执行,顶点坐标必须命名为vertex
float4 vertex : POSITION;
float3 normal : NORMAL;
};
// *为了宏正确执行,参数a2v结构体必须命名为v
v2f vert(a2v v) {
v2f o;
// ...
// 计算阴影纹理坐标
TRANSFER_SHADOW(o);
return o;
}
SHADOW_ATTENUATION
作用:采样阴影纹理坐标
fixed4 frag(v2f i) : SV_Target {
// ...
// Base Pass中平行光的光照衰减因子为1
fixed4 atten = 1.0;
// 采样阴影纹理坐标
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}
为了说明以上3段代码中对一些变量命名的硬性要求,下面将展示AutoLight.cginc中的声明,主要关注代码中SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION的使用即可(提取自2019.4.21f1版本,与书中展示版本有多处不同):
// ----------------
// Shadow helpers
// ----------------
// 未指定类型、关闭阴影,则默认为平行光
#if !defined(POINT) && !defined(SPOT) && !defined(DIRECTIONAL) && !defined(POINT_COOKIE) && !defined(DIRECTIONAL_COOKIE)
#define DIRECTIONAL
#endif
// ---- Screen space direction light shadows helpers (any version)
#if defined (SHADOWS_SCREEN)
// 若不支持屏幕空间的阴影映射技术
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
// TRANSFER_SHADOW 计算阴影纹理坐标:将顶点从模型空间变换到光源空间,存储到_ShadowCoord中
// 因使用到了参数“v.vertex”,使用宏时,需要保证a2v结构体中的顶点坐标变量名为vertex,顶点着色器的输入结构体a2v名必须为v
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
// unitySampleShadow函数:使用_ShadowCoord进行纹理采样(阴影或深度纹理)
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
#if defined(SHADOWS_NATIVE)
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz);
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
return shadow;
#else
unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
}
// 若支持屏幕空间的阴影映射技术
#else
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
// TRANSFER_SHADOW 计算阴影纹理坐标:调用内置的ComputeScreenPos函数,并存储在_ShadowCoord中
// 因使用到了参数“a.pos”,使用宏时,需要保证v2f结构体中的顶点位置名命名为pos
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
// unitySampleShadow函数:使用_ShadowCoord进行纹理采样
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
#endif
// SHADOW_COORDS 声明一个用于对阴影纹理采样的坐标
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
// SHADOW_ATTENUATION 采样阴影纹理坐标
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif
光照衰减和阴影的统一管理
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
根据以上代码,光照衰减和阴影对最终渲染结果的影响本质上是相同的,都是将光照衰减因子和阴影值对光照结果做乘积处理
在前向渲染的ForwardBase Pass和ForwardAdd Pass中,光照衰减因子的值会根据光源类型而进行赋值;但是阴影值的计算却是相同的
Unity提供了另一个内置宏,来统一计算光照衰减因子和阴影值:UNITY_LIGHT_ATTENUATION
若使用它,就可以将ForwardBase Pass和ForwardAdd Pass的代码进行统一,不需要根据不同光源类型对光照衰减因子进行赋值,同时也省略(在宏中统一处理)了对阴影的计算
(若需要ForwardAdd Pass中的逐像素光源也可以产生阴影,只需要将ForwardAdd Pass中的#pragma multi_compile_fwdadd
使用#pragma multi_compile_fwdadd_fullshadows
替换)
fixed4 frag(v2f i) : SV_Target {
// ...
// UNITY_LIGHT_ATTENUATION 计算光照衰减因子、阴影信息
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
UNITY_LIGHT_ATTENUATION的3个参数
在AutoLight.cginc的声明中它们被分别命名为destName、input、worldPos
- destName:光照衰减因子和阴影值的乘积。宏执行完成后会将乘积返回到该参数
- input:结构体v2f。在宏执行时会将其传递给SHADOW_ATTENUATION来计算阴影值
- worldPos:世界空间坐标。在宏执行时会使用其计算光源空间坐标,再对光照衰减纹理采样得到光照衰减因子、对阴影映射纹理采样并进行阴影计算,获得阴影值
注:变量atten(destName)会在宏中声明,不需要在片元着色器中手动声明
由于书中强烈建议在AutoLight.cginc中查看UNITY_LIGHT_ATTENUATION的声明,于是把它贴出来方便以后查阅(提取于2019.4.21f1版):
(“\”为宏定义中常用的换行,方便阅读)
#ifdef POINT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
#endif
#ifdef SPOT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
inline fixed UnitySpotCookie(unityShadowCoord4 LightCoord)
{
return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
}
inline fixed UnitySpotAttenuate(unityShadowCoord3 LightCoord)
{
return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).r;
}
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1))
#else
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = input._LightCoord
#endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
#endif
#ifdef DIRECTIONAL
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif
#ifdef POINT_COOKIE
samplerCUBE_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz
# else
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = input._LightCoord
# endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * texCUBE(_LightTexture0, lightCoord).w * shadow;
#endif
#ifdef DIRECTIONAL_COOKIE
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xy
# else
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = input._LightCoord
# endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, lightCoord).w * shadow;
#endif
除去宏,基本都是在进行纹理采样和计算
部分纹理,可以打开Frame Debugger,根据纹理的变量名查看
半透明物体的阴影
因为涉及到透明度检测、透明度混合,半透明物体的阴影需要进行一些特殊处理
透明度检测物体的阴影投射与接收
不透明物体的ShadowCaster Pass可以通过FallBack VertexLit获得,透明度检测物体的ShadowCaster Pass也可以
FallBack "Transparent/Cutout/VertexLit"
Transparent目录下的ShadowCaster是一个具有透明度测试功能的ShadowCaster,不会对透明度检测物体上被镂空的部分生成阴影;但普通的ShadowCaster没有透明度检测功能,所以被镂空的部分依然会投射阴影
一些情况下,由于一些面被透明度检测舍弃,与它相对的面也作为背面被剔除,这时就会产生错误的投影
将该物体的Cast Shadows设置为“Two Sides”即可
阴影接收方面,透明度检测物体不需要进行特殊处理
透明度混合物体的阴影投射与接收
实际上Unity并没有为透明度混合的物体提供处理阴影的方法,所有内置的透明度混合shader都没有包含ShadowCaster Pass
因为透明度混合需要关闭深度写入,而正确生成阴影需要在每个光源空间下严格按照从后向前的顺序进行渲染,会极大的复杂化阴影的生成,并影响性能
书中提供了一个dirty trick,将透明度混合物体的FallBack修改为VertexLit来按照不透明物体投射阴影、接收阴影,效果如下:
可以看出,透明度混合物体可以向地面投射阴影,也可以接收来自墙面的阴影,但来自墙面的阴影没能穿透该物体投射到地面上,因为该物体的阴影本身就是按照不透明物体来计算的
总结
- 阴影产生的原理是光沿直线传播,所以需要一张从光源出发的深度纹理(阴影映射纹理)
- 判断顶点是否会被渲染,需要知道该点在摄像机空间下的深度值是否足够小
- 判断顶点是否会被照亮,需要知道该点在光源空间下的深度值是否足够小
- 对于不透明物体和透明度检测物体,ShadowCaster Pass可以通过FallBack到各自的VertexLit中获取
- 计算阴影:声明阴影纹理坐标、计算阴影纹理坐标、采样阴影纹理
- 将光照衰减和阴影计算统一的宏:UNITY_LIGHT_ATTENUATION(destName,input,worldPos)
插眼待验证
关于使用阴影相关宏时的命名需求,只有v2f的SV_POSITION在不为“pos”时会报错;a2v的POSITION、v2f vert(a2v v) {}的a2v,修改命名后无报错,是否有暗病尚未得知