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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值