一.阴影的实现原理
1.1. Shadow Map
在 Unity 的实时渲染中,我们采用的是 Shadow Map 技术。
原理:计算光源的阴影映射纹理,记录光源的位置出发、能看到的场景中距离它最近的表面位置。简单理解就是先把摄像机的位置与光源重合,然后摄像机看不到的区域就是阴影。
前向渲染中,如果平行光开启了阴影(要注意需要手动开启,创建了一个新光源,默认是没有阴影的),Unity 就会为这个平行光计算阴影映射纹理。这张阴影映射纹理实质就是一张深度纹理,记录着从光源出发,距离光源最近的表面信息。
unity选择使用一个额外的pass来专门更新光源的阴影映射纹理,即LightMode标签被设为ShadowCaster的pass。这个pass的渲染目标不是帧缓存,而是阴影映射纹理(或者深度纹理)
1.2.屏幕空间的阴影映射技术(ScreenShadowMap)
原理:此技术根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图记录的表面深度大于阴影映射纹理中的深度值,说明表面是可见的。
方式:1.先调用LightMode为ShaderCaster的pass得到可投射阴影的光源的阴影映射纹理以及相机的深度纹理
2.然后根据光源的阴影映射纹理和相机的深度纹理得到屏幕空间的阴影图。
限制:阴影映射纹理本质上是一张深度图,这个技术原本是延迟渲染产生阴影的方法,所以显卡需要支持MRT。
1.3. 总结
1.如果想要一个物体接收其它的物体的阴影,就要在 shader 中对阴影映射纹理进行采样,把采样结果和光照结果相乘得到阴影效果。
2.如果想要一个物体向其它物体投射阴影,就要把该物体加入到阴影映射纹理之中,这一步骤是在 Shadow Pass 中实现的。
3.如果想要一个光源产生阴影效果,则需要手动选择阴影类型:No Shadows , Hard Shadows , Soft Shadows。Hard Shadows 相对于 Soft Shadows 计算量少一些,能满足大部分场景,边缘不平滑,锯齿明显。
二.不透明物体的阴影
2.1. 让物体投射阴影
unity中,让一个物体投射或者接收阴影,通过Mesh Render组件中的Cast Shadows和Receive Shadow属性来实现。
当 shader 中没有 ShadowCaster 的 Pass 时会去它的 Fallback 里面找,我们之前的 Fallback 为 Specular,Specular 中也没有这个 Pass, 但Specular 的回调Fallback 调用了VertexLit。想看源码的读者,可以在 Unity 官方下载 内置着色器 ,解压之后,在 builtin-shaders-xxx->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
#pragma multi_compile_instancing // allow instanced shadow pass for most of the shaders
#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
}
默认情况下,计算光源的阴影映射纹理会剔除掉物体的背面。可以将Cast Shadows设置为two sided来允许对物体的所有面都进行计算。.
2.2. 让物体接收阴影
步骤:
1.在顶点着色器的输出结构体添加内置宏SHADOW_COORDS,作用是声明一个用于对阴影纹理采样的坐标,参数是下一个可用的插值寄存器的索引值。
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
2.在顶点着色器返回之前添加另一个内置宏TRANSFER_SHADOW,作用是在顶点着色器计算上一步声明的阴影纹理坐标。
v2f vert(a2v v) {
v2f o;
...
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
3.在片元着色器计算阴影值,使用另一个内置宏SHADOW_ATTENUATION。
fixed shadow = SHADOW_ATTENUATION(i);
4.将得到的阴影值与漫反射颜色,高光反射颜色相乘。
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
注意点:这三个宏是天生的三个好基友,如果关闭了阴影,那么 SHADOW_COORDS,TRANSFER_SHADOW 会不起作用,而 SHADOW_ATTENUATION 的值为 1 。那么漫反射颜色和高光反射颜色不受 shadow 影响。而且这些宏会使用 v.vertex 和 a.pos 等变量来计算,所以 a2v 顶点坐标变量必须为 vertex,输入结构体 a2v 必须命名为 v ,且 v2f 中顶点位置坐标为 pos。
2.3. 完善的的光照衰减和阴影管理
在Base Pass中,平行光的衰减因子总是等于1,而在Additional Pass中,需要判断该Pass处理的光源类型,再使用内置变量和宏计算衰减因子。实际上,光照衰减和阴影对物体最终的渲染都是把光照衰减因子和阴影值以及光照结果相乘得到最终的渲染结果。
Unity 提供了一个内置宏UNITY_LIGHT_ATTENUATION来同时得到光照衰减因子和阴影值。
UNITY_LIGHT_ATTENUATION在AutoLight中的定义:
#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
完整代码:
Shader "Unity Shaders Book/Chapter 9/Attenuation And Shadow Use Build-in Functions" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
// Pass for ambient light & first pixel light (directional light)
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
// Apparently need to add this declaration
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
// Need these files to get built-in macros
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass {
// Pass for other pixel lights
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
// Apparently need to add this declaration
#pragma multi_compile_fwdadd
// Use the line below to add shadows for point and spot lights
// #pragma multi_compile_fwdadd_fullshadows
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
如果想在Additional Pass添加阴影效果,需要将#pragma multi_compile_fwdadd编译指令替换成#pragma multi_compile_fwdadd_fullshadows,使得这些额外的逐像素光源进行阴影计算。
三.透明物体的阴影
3.1透明度测试
透明度测试结合之前Unity shader学习---透明效果代码,把 Fallback 改为 VertexLit,
阴影部分相当于整个正方体的阴影,但镂空区域不应该有阴影。这是因为 VertexLit 中处理阴影的 Pass 并没有做透明度测试的计算。所以为了提供这样的一个 Pass ,我们可以更改 Fallback 为 "Transparent/Cutout/VertexLit" 。要注意的是,需要提供一个 _CutOff 的属性和 SHADOW_COORDS 的索引值。现在看一下效果:
3.2透明度混合
Shader "Unity Shaders Book/Chapter 9/Alpha Blend With Shadow" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
SHADOW_COORDS(3)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + diffuse * atten, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
// Or force to apply shadow
// FallBack "VertexLit"
}
四.完整的光照 shader
包含了法线纹理,多光源,光照衰减和阴影,基于 Blinn-Phong 的高光发射 shader。
Shader "Unity Shaders Book/Common/Bumped Specular" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_Specular ("Specular Color", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
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 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
TANGENT_SPACE_ROTATION;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
// Use the line below to add shadows for point and spot lights
// #pragma multi_compile_fwdadd_fullshadows
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}