目錄
一、效果圖
二、實戰
適用場景:平面且周圍沒有牆體時,例如:足球遊戲
1、準備資源:Unity醬模型 (可直接在Unity商店搜索)
2、一個C#腳本和一個材質和Shader
3、去掉模型身上的材質陰影投射效果(即去除ShadowCaster的Pass),在Unity醬身上的材質Shader都是通過Fallback的Shader進行投射陰影的所以註釋掉Fallback即可。
Shader原理:頂點偏移(目前還未搞懂)
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Unlit/ModelVirtualShadowShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+10"}
LOD 100
//Pass
//{
// CGPROGRAM
// #pragma vertex vert
// #pragma fragment frag
// // make fog work
// #pragma multi_compile_fog
//
// #include "UnityCG.cginc"
// struct appdata
// {
// float4 vertex : POSITION;
// float2 uv : TEXCOORD0;
// };
// struct v2f
// {
// float2 uv : TEXCOORD0;
// UNITY_FOG_COORDS(1)
// float4 vertex : SV_POSITION;
// };
// sampler2D _MainTex;
// float4 _MainTex_ST;
// fixed4 _Color;
//
// v2f vert (appdata v)
// {
// v2f o;
// o.vertex = UnityObjectToClipPos(v.vertex);
// o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// UNITY_TRANSFER_FOG(o,o.vertex);
// return o;
// }
//
// fixed4 frag (v2f i) : SV_Target
// {
// // sample the texture
// fixed4 col = tex2D(_MainTex, i.uv);
// // apply fog
// UNITY_APPLY_FOG(i.fogCoord, col);
// return col * _Color;
// }
// ENDCG
//}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Cull Back
ColorMask RGB
Stencil{
Ref 0
Comp Equal
WriteMask 255
ReadMask 255
Pass Invert //消除所有位
Fail Keep
ZFail Keep
}
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 xlv_TEXCOORD0 : TEXCOORD0;
float3 xlv_TEXCOORD1 : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _ShadowPlane;
float4 _ShadowProjDir;
float4 _WorldPos;
float _ShadowInvLen;//陰影長度
float4 _ShadowFadeParams;
v2f vert(appdata v)
{
v2f o;
float3 lightDir = normalize(_ShadowProjDir);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float distance = (_ShadowFadeParams.w - dot(_ShadowPlane.xyz, worldPos)) / dot(_ShadowPlane.xyz, lightDir.xyz);
worldPos = worldPos + distance * lightDir.xyz;
o.vertex = mul(UNITY_MATRIX_VP, float4(worldPos, 1.0));
o.xlv_TEXCOORD0 = _WorldPos.xyz;
o.xlv_TEXCOORD1 = worldPos;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float3 planeToSelfDir = (i.xlv_TEXCOORD0 - i.xlv_TEXCOORD1);
float4 color;
color.xyz = float3(0.0, 0.0, 0.0);
color.w = (pow((1.0 - clamp(((sqrt(dot(planeToSelfDir, planeToSelfDir)) * _ShadowInvLen) - _ShadowFadeParams.x), 0.0, 1.0)), _ShadowFadeParams.y) * _ShadowFadeParams.z);
return color;
}
ENDCG
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ModelVirtualShadowTest : MonoBehaviour {
public Material mat;
public GameObject lightGo;
List<Material> mMatList = new List<Material>();
private Camera camera;
public float _ShadowInvLen;
public Vector4 _ShadowFadeParams;
// Use this for initialization
void Start () {
Renderer[] renderers = GetComponentsInChildren<Renderer>();
foreach(var v in renderers)
{
if (v == null) continue;
//foreach(var m in v.materials)
//{
// if(m.shader.name == "Unlit/ModelVirtualShadowShader")
// {
// mMatList.Add(m);
// }
//}
Material tempMat = new Material(mat);
var matList= new List<Material>(v.materials);
matList.Add(tempMat);
v.materials = matList.ToArray();
mMatList.Add(tempMat);
}
camera = Camera.main;
}
// Update is called once per frame
void Update () {
foreach(var v in mMatList)
{
if (v == null) continue;
v.SetVector("_ShadowPlane", new Vector4(0.0f, 0.4f, 0.0f, 0.0f));
v.SetVector("_ShadowProjDir", lightGo.transform.forward);
v.SetVector("_WorldPos", transform.position);
v.SetFloat("_ShadowInvLen", _ShadowInvLen);
v.SetVector("_ShadowFadeParams", _ShadowFadeParams);
}
}
}
腳本放置在人物身上,並將上方Shader對應的材質放入Mat參數,光源物體放入Light Go參數,其他參數參考上圖。
其中Shadow Fade Params是調整陰影效果的,Shadow Inv Len是調整陰影長度的。
注意事項:
1、陰影是頂點偏移後渲染出來的,所以當人物貼近牆體時,陰影不會投射到牆體上,還是在地板上。
2、陰影是在Geometry+10渲染隊列,所以假如有比它還要高的渲染隊列物體在陰影範圍內渲染,將會覆蓋掉陰影,即使陰影確實在那個物體之上,因爲渲染陰影的Pass關閉了深度寫入。比如我測試出的一種情況是當地板沒有時,陰影會消失,原因是渲染天空盒時,天空盒覆蓋掉了陰影,如下圖。
三、算法核心
核心算法在頂點偏移,求出distance陰影偏移大小,再以光源方向*distance得到偏移向量,進行偏移頂點世界座標,再將此座標轉到裁剪空間,交由Unity進行後續處理。
float3 lightDir = normalize(_ShadowProjDir);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float distance = (_ShadowFadeParams.w - dot(_ShadowPlane.xyz, worldPos)) / dot(_ShadowPlane.xyz, lightDir.xyz);
worldPos = worldPos + distance * lightDir.xyz;
o.vertex = mul(UNITY_MATRIX_VP, float4(worldPos, 1.0));
在實戰中,_ShadowFadeParams.w = 0.2,_ShadowPlane.xyz = (0.0f, 0.4f, 0.0f),lightDir.xyz 均爲常量,因此dot(_ShadowPlane.xyz, lightDir.xyz)是常量,那麼只剩下dot(_ShadowPlane.xyz, worldPos)是一個變化值,dot點積操作可理解cos求餘弦值,此時是求出世界向量(0.0f, 0.4f, 0.0f)和(世界中心點->模型頂點)向量的餘弦值,觀察上方的圖,可將Y軸看成(0, 0.4f,0.0f)向量,Y軸與(世界中心點->模型頂點)向量的角度是從模型腳部到模型頭部逐漸變小,所以餘弦值是逐漸增大,故陰影偏移逐漸增大。並且,這個計算效果會將人物頂點整體變扁。其中_ShadowFadeParams.w是對整體陰影的一個偏移作用。
還有一個奇怪的問題,下面這個公式爲什麼是減去上面我所說的餘弦值呢,理應是加上這個餘弦值。
float distance = (_ShadowFadeParams.w - dot(_ShadowPlane.xyz, worldPos)) / dot(_ShadowPlane.xyz, lightDir.xyz);
這是因爲分母幾乎肯定是一個負數導致的,_ShadowPlane.xyz向量和lightDir.xyz向量的角度幾乎是絕對大於90度角的,那麼餘弦值是負數的,所以這裏就是用減號了。因爲負負得正,而_ShadowFadeParams.w是0.2,它的效果就是往上偏移。
當然,你可以改爲如下:
float distance = (_ShadowFadeParams.w + dot(_ShadowPlane.xyz, worldPos)) / -dot(_ShadowPlane.xyz, lightDir.xyz);
此時_ShadowFadeParams.w爲負數時纔是往上偏移,實戰的值要改爲-0.2纔是正常的效果。