1 阴影原理
光源照射到不透明物体上,会向该物体的后面投射阴影,如果阴影区域存在其他物体,这些物体不被光源照射的部分就需要渲染阴影。因此,我们可以将阴影的生成抽象出 2 个流程:物体投射阴影、物体接收阴影。
1.1 阴影相关开关
1)开启 Light 组件渲染阴影
- No Shadows:不渲染阴影
- Hard Shadows:硬阴影(阴影边缘较清晰)
- Soft Shadows:软阴影(阴影边缘较模糊)
2)开启投射阴影 / 接收阴影
- Cast Shadows:投射阴影,取值有 Off(关闭投射)、On(开启投射)、Two Sided(双面都可以投射阴影)、Shadows Only(只投射阴影,但物体不可见)
- Receive Shadows:接收阴影
注意:投射阴影和接收阴影开关可以独立控制,不相互依赖,因此,一个物体可以只投射阴影而不接收阴影,也可以只接收阴影而不投射阴影。
1.2 物体投射阴影
1)阴影映射纹理
物体的 MeshRenderer 组件开启了 Cast Shadows 后,Unity 会把该物体加入到光源的阴影映射纹理(Shadow Map)的计算中。阴影映射纹理是一张深度图,它记录了从光源位置能看到的顶点的位置和深度,即将相机放在光源位置渲染的深度纹理。
2)屏幕空间的阴影映射纹理
Unity 5 中,Unity 使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screenspace Shadow Map)。其原理是:先生成相机的深度纹理和灯光的阴影映射纹理,对于场景中任意一点,以其深度纹理的 x、y 值和阴影映射纹理的 z 值作为该点的屏幕空间阴影映射纹理。注意:Unity 并没有在所有平台上都使用了这种技术并,需要显卡支持 MRT,有些移动平台就不支持该特性。
3)ShadowCaster Pass
正常的渲染流程会调用 LightMode 标签为 ForwardBase 和 Additional 的 Pass,而深度映射纹理是一张深度图,不需要渲染顶点颜色,为节省性能,Unity 设计了 LightMode 为 ShadowCaster 的 Pass 专门用于渲染阴影纹理。如果当前 Shader 文件中没有 LightMode 为 ShadowCaster 的 Pass,就去 Fallback 指定的 Shader 中继续寻找,如果仍没有找到,就继续去 Fallback 里寻找,直到找到了 ShadowCaster Pass 或没有 Fallback,如果最后找到了,Unity 会使用该 Pass 更新光源的阴影映射纹理。
4)渲染阴影映射纹理
对于阴影映射纹理的渲染,Unity 封装得较好,用户只需要在 Shader 的 Fallback 中指定 Diffuse、Specular 或 VertexLit 等,如下:
Fallback "Specular"
内置的 Specular 中没有 ShadowCaster Pass,其 Fallback 为 VertexLit(Shader 见:Unity Editor 安装目录下的 Editor\Data\built-in-shaders-xxx\Shaders\DefaultResourcesExtra\Normal-VertexLit.shader 文件,built-in-shaders),VertexLit 中有 ShadowCaster Pass,如下:
// Pass to render object as a shadow caster
Pass {
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base v) {
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f i) : SV_Target {
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
1.3 物体接收阴影
对于模型的任意顶点,使用该点对应的阴影灰度值(可以从屏幕空间的阴影映射纹理中获取)乘以光照纹理,得到的就是该点最终要显示的纹理。阴影灰度值的计算如下:
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间顶点坐标
float3 normal: Normal; // 世界空间顶点法线向量
float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
SHADOW_COORDS(1) // 声明一个用于阴影纹理采样的uv坐标, 入参是下一个可用的插值寄存器的索引(此处下一个可用插值寄存器是TEXCOORD1, 其索引为1)
};
v2f vert(a2v v) {
v2f o;
...
TRANSFER_SHADOW(o); // 计算阴影纹理uv坐标
return o;
}
fixed4 frag(v2f i) : SV_Target {
...
// fixed shadow = SHADOW_ATTENUATION(i); // 计算阴影灰度
// return fixed4(ambient + (diffuse + specular) * shadow, 1);
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 计算衰减因子和阴影灰度的乘积
return fixed4(ambient + (diffuse + specular) * atten, 1);
}
说明:上述宏的定义在 AutoLight.cginc 文件中;这些宏中使用了上下文中部分变量进行相关计算(如:v2f 中的 pos),因此 pos 的名称不能改变。
2 阴影应用
Shadow.Shader
Shader "MyShader/Shadow" {
Properties {
_ModelColor ("Model Color", Color) = (1, 1, 1, 1) // 模型颜色
_Specular ("Specular Color", Color) = (1, 1, 1, 1) // 镜面反射颜色
_Gloss ("Gloss", Range(8.0, 256)) = 20 // 镜面反射光泽度
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _ModelColor; // 模型颜色
fixed4 _Specular; // 镜面反射颜色
float _Gloss; // 镜面反射光泽度
struct a2v {
float4 vertex : POSITION; // 模型空间顶点坐标
float3 normal: NORMAL; // 模型空间顶点法线向量
};
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间顶点坐标
float3 normal : Normal; // 世界空间顶点法线向量
float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
SHADOW_COORDS(1)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
o.normal = UnityObjectToWorldNormal(v.normal); // 将模型空间法线向量变换到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex); // 将模型空间顶点坐标变换到世界空间
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 normal = normalize(i.normal); // 世界空间法线向量
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 世界空间灯光向量
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // 世界空间观察向量
fixed3 halfDir = normalize(lightDir + viewDir); // 半向量
fixed3 albedo = _ModelColor; // 模型自身颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT * albedo; // 环境光
fixed3 diffuse = _LightColor0 * albedo * max(0, dot(normal, lightDir)); // 漫反射光
fixed3 specular = _LightColor0 * _Specular * pow(max(0, dot(normal, halfDir)), _Gloss); // 镜面反射光(Blinn Phong光照模型)
// fixed shadow = SHADOW_ATTENUATION(i); // 计算阴影灰度
// return fixed4(ambient + (diffuse + specular) * shadow, 1);
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 计算衰减因子和阴影灰度的乘积
return fixed4(ambient + (diffuse + specular) * atten, 1);
}
ENDCG
}
}
FallBack "Specular"
}
运行效果:
3 帧调试器查看阴影绘制过程
通过 Window → Analysis → Frame Debug 打开帧调试器,如下:
渲染对象主要有:Camera DepthTexture(相机深度纹理)、ShadowMap(阴影映射纹理)、Screenspace ShadowMap(屏幕空间阴影映射纹理)、TempBuffer(帧缓冲区)。
1)Camera DepthTexture
2)ShadowMap
3)Screenspace ShadowMap
4)TempBuffer