目录
一、效果图
二、实战
三、算法核心
一、效果图
二、实战
适用场景:平面且周围没有墙体时,例如:足球游戏
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才是正常的效果。