【Shader案例】模型虛擬陰影(實體陰影)

目錄

一、效果圖

二、實戰

三、算法核心


一、效果圖

二、實戰

適用場景:平面且周圍沒有牆體時,例如:足球遊戲

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纔是正常的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章