Unity Shader - 陰影(平面陰影&球體陰影)
陰影效果:
上面兩幅圖分別為:平面陰影和球體陰影的效果
平面陰影簡述:
平面陰影是一種比較特殊的情形。在這種情形裡,我們只考慮物體的陰影投射到平面上的情形,所以有一套相對比較簡單的專用演算法。
首先考慮最簡單的情況,如何計算一個平行光的投影。平行光在我們的計算中其實就是一個方向向量,是陰影的投射方向,而平面是陰影要影響的目標物體。我們需要知道到目標物體的Object Space矩陣,在目標物體的空間內將投影物體的頂點進行重新計算,計算其沿光線方向,在陰影接受平面上的位置,這個位置關係可以通過三角形相似來計算
平面陰影實現原理:
1.向shader傳入陰影接收平面(通常是地面)的世界空間到模型空間的轉換矩陣和模型空間到世界空間的轉換矩陣; 2.在陰影接收平面空間下,根據燈光方向計算目標物體的頂點在平面上的投影座標,最終得到陰影;可以根據相似三角形,計算出AC,即目標物體的頂點沿著光照方向到達陰影接收平面的距離;
首先,每一個投射平面陰影的物體都需要下面這麼一個指令碼來告訴它陰影接受物體的資訊,
具體來說就是到其Object Space空間的矩陣,以及從平面的Object Space返回的矩陣。
//C#程式碼:放在需要顯示陰影的物件上
using UnityEngine; using System.Collections; [ExecuteInEditMode] public class PlaneShadowCaster : MonoBehaviour { public Transform reciever; //陰影接收平面(通常是地面) void Update() { GetComponent<Renderer>().sharedMaterial.SetMatrix("_World2Ground", reciever.GetComponent<Renderer>().worldToLocalMatrix); GetComponent<Renderer>().sharedMaterial.SetMatrix("_Ground2World", reciever.GetComponent<Renderer>().localToWorldMatrix); } }
// shader,放在需要顯示陰影的物件上
// shader,放在需要顯示陰影的物件上
Shader "Custom/PlanarShadow" {
Properties {
_Instensity ("Shininess", Range (2, 4)) = 2.0 //光照強度
}
SubShader {
//對物體本身做一個簡單的光照計算
pass
{
Tags{"LightMode"="ForwardBase"}
Material{Diffuse(1,1,1,1)}
Lighting On
}
//計算陰影
Pass
{
Tags{"LightMode"="ForwardBase"}
Blend DstColor SrcColor
Offset -1, -1 //使陰影在平面之上
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4x4 _World2Ground; //陰影接收平面(世界空間到模型空間的轉換矩陣)
float4x4 _Ground2World; //陰影接收平面(模型空間到世界空間的轉換矩陣)
float _Instensity;
struct v2f{
float4 pos:SV_POSITION;
float atten:TEXCOORD0;
};
v2f vert(float4 vertex:POSITION)
{
float3 litDir;
litDir = WorldSpaceLightDir(vertex);//世界空間主光照相對於當前物體的方向
litDir = mul(_World2Ground,float4(litDir,0)).xyz;//光源方向轉換到接受陰影的平面空間
litDir = normalize(litDir);// 歸一
float4 vt;
vt = mul(_Object2World,vertex); //將當前物體轉換到世界空間
vt = mul(_World2Ground,vt); // 將物體在世界空間的矩陣轉換到地面空間
vt.xz = vt.xz - (vt.y/litDir.y)*litDir.xz;// 用三角形相似計算沿光源方向投射後的XZ
vt.y=0;// 使陰影保持在接受平面上
vt = mul(_Ground2World, vt); // 陰影頂點矩陣返回到世界空間
vt = mul(_World2Object, vt); // 返回到物體的座標
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, vt);//輸出到裁剪空間
o.atten = distance(vertex, vt) / _Instensity;// 根據物體頂點到陰影的距離計算衰減
return o;
}
float4 frag(v2f i):COLOR
{
//return float4(0.3, 0.3, 0.3, 1);//一個灰色的陰影出來了
return smoothstep(0,1,i.atten/2);
}
ENDCG
}
}
}
程式碼詳解:
在此Shader中,我們首先使用固定管線對物體做了一個簡單的照明。在計算陰影的ForwardBase中,首先使用一個可以疊加陰影的混合模式,然後使用z偏移保證出來的陰影在接受平面之上。_World2Ground和_Ground2World分別是我們自定義的兩個進出陰影接收平面矩陣。在具體計算中,首先將光源方向和投影物體的頂點都轉換到接收平面的空間,在它們都處於同一個空間後,通過簡單的三角形近似演算法,來計算投影物體頂點沿光線投射後在接收平面上的新位置。因為這是一個Build In的Unity Plane,所以只計算其xz分量即可,並將Y分量設為0,這樣投影出來的陰影就出現在接收平面上了。投影計算完成後,我們返回到世界空間,然後到投影物體本身的Object Space,之後的事情就如通常那樣,一個經典的MVP變換即可。
效果如下:
左邊為我們計算出的平面陰影,右邊為Unity的計算出的陰影;
(我們的物體顯示黑色,是因為在Shader的第一個pass中只是對物體本身做了一個簡單的光照計算,不夠完整,缺少漫反射等)
點光源的平面陰影效果:
Tip:
1:平面陰影作為一種最簡單的實時陰影實現,儘管其僅能侷限於在完全平坦的地面的情況下使用,但由於其效能良好,在許多移動端手遊中仍然可以發揮較強的使用價值。
2:以上是針對平行光來做的,點光源也是類似的,對於通過使用WorldSpaceLightDir()方法來計算光源方向來說,就完全一樣了,但效果不好;
3:一般情況下,比如用Shadow Mapping和Shadow Volumes計算陰影的衰減是比較困難的,但是在此例中,我們己經知道投射陰影物體的頂點在計算前和計算後的位置,根據這兩個位置的距離,我們還是可以考慮計算一下陰影的衰減問題的。
但這個方法還有一個顯而易見的問題,那就是物體本身是立體的,不是一個平面,因此這個計算前後的點的距離是包括物體本身厚度的,這個厚度就會表現在陰影上。要解決這個問題,我們可以先把物體變換到燈光空間,使用_World2Light矩陣沿著燈光方向把物體壓扁,然後投射物體,這樣計算出來的陰影衰減就不會包括物體的厚度了。
球體陰影實現原理:
1.根據陰影接收平面上 (點的入射光向量)和(點到球體的向量)計算【點積】求出【角度】。
2.通過【角度】的sin值,求出【對邊】並與【球體半徑】進行比較。
3.所求【對邊的長度】大於【半徑】,說明該點被光照,反之該點是陰影點。
新增到陰影接受平面(地面)物件上的C#指令碼:
using UnityEngine;
using System.Collections;
public class SphereShadow : MonoBehaviour
{
public GameObject sphere; //投影物件
void Update()
{
// Vector3 pos = sphere.transform.localPosition;
Vector3 pos = sphere.transform.position;
GetComponent<Renderer>().sharedMaterial.SetVector("_spPos", new Vector4(pos.x, pos.y, pos.z, 1f));
GetComponent<Renderer>().sharedMaterial.SetFloat("_spR", sphere.transform.localScale.x / 2);
}
}
新增到陰影接受平面(地面)物件上的Shader:
// shader,放在需要接收陰影的物件上
Shader "Custom/SphereShadow" {
Properties{
_spPos("Sphere pos", vector) = (0,0,0,1) // 球體位置
_spR("radius", float) = 1 // 球體半徑
_Intensity("Intensity", range(0,1)) = 0.5 // 陰影濃度
}
SubShader{
Pass{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _spPos; // 球體位置
float _spR; // 球體半徑
float _Intensity; // 陰影濃度
float4 _LightColor0; // 顏色
struct v2f {
float4 pos:SV_POSITION;
float3 litDir:TEXCOORD0;// 世界座標中燈光方向向量
float3 spDir:TEXCOORD1; // 在世界座標中投影球體方向向量
float4 vc:TEXCOORD2; // 逐頂點計算的光照
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);// 獲取頂點檢視位置
o.litDir = WorldSpaceLightDir(v.vertex);// 獲取世界座標中燈光對頂點的方向向量
o.spDir = (_spPos - mul(_Object2World, v.vertex)).xyz;//世界座標中該頂點到投影球體的向量
// 頂點的光照計算。該頂點在物件座標中的光照方向向量
float3 ldir = ObjSpaceLightDir(v.vertex);
ldir = normalize(ldir);
o.vc = _LightColor0 * max(0, dot(ldir, v.normal));//根據頂點的入射光線和法線角度求該頂點光照
return o;
}
float4 frag(v2f i) :COLOR
{
float3 litDir = normalize(i.litDir);//獲取點的入射光線的單位向量
float3 spDir = i.spDir; // 獲取該點到投影球體的向量
float spDistance = length(spDir); //該點到球體的距離
spDir = normalize(spDir); //該點到投影球體的單位向量
float cosV = dot(spDir, litDir);// 該點到球體 與 該點到入射光線的夾角
float sinV = sin(acos(max(0, cosV)));// 拿到餘弦值大於0的角度,求正弦
float D = sinV * spDistance; // 解三角形,求對邊
float shadow = step(_spR, D); // 如果對邊小於半徑返回0,該點為陰影點
float c = lerp(1 - _Intensity, 1, shadow);// shadow由0到1
return i.vc * c; // 為0的時候是陰影點
}
ENDCG
}
}
}
效果如下:
Tips:
不管對於哪種形狀的投影物件,球體陰影的投影均為圓形,如下:對於Cube(立方體)的投影,效果如下: