前半部分的基于圖檔的實時陰影技術是百人計劃的前半部分總結,後面的Unity中的實作過程是《入門精要》中的實作。
1 基于圖檔的實時陰影技術
這裡的“基于圖檔”指陰影生成一張圖檔。
1.1 平面投影陰影
他并不是一個基于圖檔的解決方案,但思路值得借鑒。
缺點:隻能投影到平面;投影物體必須在光線和平面之間——意味着這種方法曲面/折面就不可行了,那麼需要解決這個問題——在光源地方放一個Camera,生成一個深度圖,記錄距離最近表面的深度資訊:
這個就是陰影映射的核心思想。
1.2 陰影映射 Shadow Map
上面的圖中,右側有一個光照到整個場景,右圖就是一個深度圖,記錄深度值(0-1的顔色記錄下來每個像素的位置遠近和深度關系)。
陰影映射的大概思路:
1.首先在光源位置生成光源空間下的深度圖(一張Shadowmap);
2.然後從真正的Camera視角去渲染整個場景的物體,每次渲染都需要先把目前片元的深度zp(螢幕空間)與Shadowmap(光源空間下)記錄的深度值zs作比較(深度測試),如果zp > zs,意味着該片元在陰影裡。
2 Unity中的螢幕空間陰影映射
前向渲染想要獲得shadowmap,會在光源處同樣進行一次Base Pass和Additional Pass來得到深度值,但這樣消耗太大了!是以Unity提供了一種新的思路——螢幕空間陰影映射,以此實作Shadow Map。
Unity中實作Shadow Map主要有兩個特點,
- 額外提供了一個LightMode為ShadowCaster的Pass;
- 是采樣的方式不同——螢幕空間陰影映射
過程大概是,
- 渲染螢幕空間深度圖;
- 光源方向渲染一個shadowmap——用的是調用ShadowCaster的Pass的方式;
- 在螢幕空間做一次陰影收集,把光源下的shadowmap和錄影機下的深度圖做判斷之後,得到一個新的深度圖——螢幕空間陰影紋理;
- 最後uv直接采樣3中得到的螢幕空間陰影紋理。
2.1 物體投射陰影
如果想要一個物體投射陰影,意味着它将要參與shadowmap的計算中去。我們知道,一個Pass是在Shader中定義的,那麼Shader與材質緊緊關聯,如果一個物體的Shader中開啟了ShadowCaster這個Pass,那麼這個物體就可以參與到光源方向渲染shadowmap記錄的資訊中去,意味着這個物體的資訊已經可以參與其他物體計算陰影的過程中去了。
在Unity中的實作
讓我們在Unity中實際看看這個過程是如何實作的。
Unity中每個物體的lighting元件都會有Cast Shadows和Receive Shadows兩個可選項。
當物體Cast Shadows為On,意味着目前物體處于可投射陰影的狀态。此時Unity就會為目前物體執行LightMode為ShadowCaster的Pass。
檢視ShadowCaster Pass
預設的前向渲染路徑都是隻有Base Pass和Additional Pass,但Shaderlab的Fallback語義我們通常會定義:
Fallback "Specular"
查找Unity内置的Vertex.Lit:
// 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
}
}
}
上述代碼的目的就是,把目前的深度資訊寫入渲染目标中。
2.2 物體接收陰影
想要一個物體接受陰影,就是物體在進行光照計算後繪制的顔色除了光照計算的結果值,還需要考慮陰影的顔色,也就是還需要加上“采樣陰影映射紋理”這一步,再把采樣的結果和光照計算結果相乘,才算是考慮了陰影效果。我們舉個例子:
如下圖所示,左圖是最終的效果圖,右邊是螢幕空間陰影紋理。這裡的平面相當于“開啟接受陰影”的物體,那麼在渲染得到平面上每個片元的顔色時,就還需要多家一步用每個片元對應位置去采樣右邊的陰影紋理的步驟,光照結果*采樣結果就能得到最終的值(值總是非0即1的)。
Base Pass中處理陰影投射
讓一個物體接受陰影就沒有實作陰影投射那麼簡單了,如下情況:
右邊的高一點Cube的陰影并沒有投射到左邊的小Cube上,上述效果對于一個不透明物體來說,肯定是錯誤的。
如何糾正呢?
上述場景中的兩個Cube我挂的Shader都是沒考慮陰影的前向渲染Shader,這裡我們要加上Unity内置的宏,SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION,計算出采樣的陰影值後與diffuse和specular值相乘即可,三個宏也都已經展現在下面的代碼中了:
Shader "Unity Shaders Book/Chapter 9/Shadow"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8, 256)) = 20.0
}
SubShader {
// Pass for ambient light & directional light
Pass {
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc" //for shadow--
//need to add this declaration
#pragma multi_compile_fwdbase
//properties
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;
TRANSFER_SHADOW(o); //計算上一步中聲明的陰影紋理坐标
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldViewDir + worldLightDir);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//attenuation of directional light
fixed atten = 1.0;
//根據紋理坐标采樣
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4 (ambient + (diffuse + specular) * atten * shadow, 1.0);
}
ENDCG
}
Pass {
Tags {"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "Autolight.cginc"
#pragma multi_compile_fwdadd
//properties
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;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else //is pointlight
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldViewDir + worldLightDir);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//attenuation of directional light
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
//1.Change point from world to lightspace, add-> "Autolight.cginc"
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
//2.sample
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4 (ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
最終的效果為,現在就正确啦:
Additonal Pass中處理陰影投射
在前向渲染學習過程中我們了解到,Base Pass是處理平行光的,Additional Pass是處理點光源等等的,當我們把場景中的平行光換成點光源時,沒有對Additional Pass加上陰影處理,效果将會是這樣:
右邊的Cube陰影又一次”穿過了“左邊的Cube,這又是一種錯誤的效果。
如何解決呢?
這裡有用到了一個新的宏——UNITY_LIGHT_ATTENUATION
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
如果要用它,兩個Pass要一起用上,也就是Base Pass就不需要再單獨處理陰影了,也不需要單獨在Additional Pass中單獨處理光照衰減量atten。
這個在AutoLight.cginc中定義的宏,給出了一些SPOT、DIRECTIONAL、POINT_COOKIE、DIRECTIONAL_COOKIE,也就是針對不同光源類型、是否啟用cookie等情況聲明了對應版本的UNITY_LIGHT_ATTENUATION。
最後整個代碼:
Shader "Unity Shaders Book/Chapter 9/Shadow"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8, 256)) = 20.0
}
SubShader {
// Pass for ambient light & directional light
Pass {
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc" //for shadow--
//need to add this declaration
#pragma multi_compile_fwdbase
//properties
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;
TRANSFER_SHADOW(o); //計算上一步中聲明的陰影紋理坐标
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldViewDir + worldLightDir);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// //attenuation of directional light
// fixed atten = 1.0;
//根據紋理坐标采樣
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4 (ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass {
Tags {"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
#pragma multi_compile_fwdadd_fullshadows
//properties
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;
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else //is pointlight
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldViewDir + worldLightDir);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// //attenuation of directional light
// #ifdef USING_DIRECTIONAL_LIGHT
// fixed atten = 1.0;
// #else
// //1.Change point from world to lightspace, add-> "Autolight.cginc"
// float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
// //2.sample
// fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
// #endif
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4 (ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
最後的效果: