版权声明:Davidwang原创文章,严禁用于任何商业途径,授权后方可转载。
Unity自带的Shadowmap阴影与Projector投影阴影生成方式都具有普适的特点,适用范围广,并且均是通过物理的方式生成阴影,因此阴影能够投射到自身以及不规则的物体表面,与场景复杂度没有关系。但通过前面的学习我们也可以看到,这两种生成阴影的方法在带来较好效果的同时,性能开销也是比较大的,特别是在使用高分辨率高质量实时软阴影时,非常有可能成为性能瓶颈。
在某些情况下,我们可能并不需要那么高质量、高通用性的阴影,或者由于性能制约不能使用那么高质量的阴影,因此可能会寻求一个“适用某些特定场合”的“看起来正确”的实时阴影以降低性能消耗。本节我们将要学习一种平面投影阴影(Planar Projected Shadows),以适应一些对性能要求非常高,以降低阴影质量换取性能的应用场景。
阴影区域其实就是物体在投影平面上的投影区域,因此我们可以直接将物体的顶点投影到投影平面上,并在这些投影区域里用特定的颜色着色即可,如下图所示。
因此,阴影计算转化为求物体在平面上的投影,求投影可以使用解释几何求投影的方法,也可以使用平面几何的方法。在上图中,阴影投影计算可以转化为数学模型,即已知空间内一个方向向量L(Lx,Ly,Lz)和一点P(Px,Py,Pz),求点P沿着L方向在平面y = h上的投影位置Q(Qx,Qy,Qz)的问题,下面我们使用平面几何的计算方式进行推导,为简体计算,只在二维空间内进行推导,对上图进行二维抽象成下图。
根据相似三角形定理,可以得到下面公式:
− L y P y − h = L x Q x − P x -\frac{Ly}{Py-h}=\frac{Lx}{Qx-Px} −Py−hLy=Qx−PxLx
因此
Q x = P x − L x ( P y − h ) ) L y Qx = Px - \frac{Lx(Py-h))}{Ly} Qx=Px−LyLx(Py−h))
Q y = h Qy = h Qy=h
坐标Q(Qx,Qy)的即是要求的投影点坐标,推广到三维,坐标Q计算公式如下:
Q x = P x − L x ( P y − h ) ) L y Qx = Px - \frac{Lx(Py-h))}{Ly} Qx=Px−LyLx(Py−h))
Q y = h Qy = h Qy=h
Q z = P z − L z ( P y − h ) ) L z Qz = Pz - \frac{Lz(Py-h))}{Lz} Qz=Pz−LzLz(Py−h))
该公式即为阴影投影公式。
有了公式以后,我们就可以使用Shader对阴影进行渲染,为了与原有渲染流程统一,我们只需要在原来渲染的shader中多写一个pass进行阴影渲染即可,具体Shader如下所示。
Shader "Davidwang/PlanarShadow"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_ShadowColor("_ShadowColor",color) = (0.5, 0.5, 0.5, 1.0)
_ShadowFalloff("ShadowInvLen", float) = 1.2
}
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;
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
{
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
Pass
{
//用使用模板测试以保证alpha显示正确
Stencil
{
Ref 0
Comp equal
Pass incrWrap
Fail keep
ZFail keep
}
//透明混合模式
Blend SrcAlpha OneMinusSrcAlpha
//关闭深度写入
ZWrite off
//深度稍微偏移防止阴影与地面穿插
Offset -1 , 0
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 color : COLOR;
};
float4 _LightDir;
float4 _ShadowColor;
float _ShadowFalloff;
float3 ShadowProjectPos(float4 vertPos)
{
float3 shadowPos;
//得到顶点的世界空间坐标
float3 worldPos = mul(unity_ObjectToWorld , vertPos).xyz;
//灯光方向
float3 lightDir = normalize(_LightDir.xyz);
//阴影的世界空间坐标(低于地面的部分不做改变)
shadowPos.y = min(worldPos.y , _LightDir.w);
shadowPos.xz = worldPos.xz - lightDir.xz * max(0 , worldPos.y - _LightDir.w) / lightDir.y;
return shadowPos;
}
v2f vert(appdata v)
{
v2f o;
//得到阴影的世界空间坐标
float3 shadowPos = ShadowProjectPos(v.vertex);
//转换到裁切空间
o.vertex = UnityWorldToClipPos(shadowPos);
//得到中心点世界坐标
float3 center = float3(unity_ObjectToWorld[0].w , _LightDir.w , unity_ObjectToWorld[2].w);
//计算阴影衰减
float falloff = 1 - saturate(distance(shadowPos , center) * _ShadowFalloff);
//阴影颜色
o.color = _ShadowColor;
o.color.a *= falloff;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
第一个pass是正常的物体着色渲染,第二个pass负责阴影投影着色,其中_LightDir.xyz是灯光方向,_LightDir.w是接受阴影的平面高度,_ShadowColor为阴影颜色,_LightDir我们使用脚本代码将值传过来,因为不能提前知道灯光方向和检测到的平面(接受阴影)的高度信息。使用模板与混合是为了营造阴影衰减的效果,如下图所示。
计算中心点世界坐标代码:float3 center =float3( unity_ObjectToWorld[0].w , _LightPos.w , unity_ObjectToWorld[2].w);该公式中unity_ObjectToWorld这个矩阵每一行的第四个分量分别对应物体Transform的xyz。float falloff = 1-saturate(distance(shadowPos , center) * _ShadowFalloff)为计算衰减因子,以便于后面的混合。
相应的,我们需要修改AppContoller控制脚本如下。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
[RequireComponent(typeof(ARRaycastManager))]
public class AppControler : MonoBehaviour
{
public GameObject spawnPrefab;
public GameObject ARPlane;
public Light mLight;
static List<ARRaycastHit> Hits;
private ARRaycastManager mRaycastManager;
private GameObject spawnedObject = null;
private float mARCoreAngle = 180f;
private List<Material> mMatList = new List<Material>();
private float mPlaneHeight = 0.0f;
private void Start()
{
Hits = new List<ARRaycastHit>();
mRaycastManager = GetComponent<ARRaycastManager>();
}
void Update()
{
if (Input.touchCount == 0)
return;
var touch = Input.GetTouch(0);
if (mRaycastManager.Raycast(touch.position, Hits, TrackableType.PlaneWithinPolygon | TrackableType.PlaneWithinBounds))
{
var hitPose = Hits[0].pose;
if (spawnedObject == null)
{
spawnedObject = Instantiate(spawnPrefab, hitPose.position, hitPose.rotation);
spawnedObject.transform.Rotate(Vector3.up, mARCoreAngle);
spawnedObject.transform.Translate(0, 0.02f, 0);
var p = Instantiate(ARPlane, hitPose.position, hitPose.rotation);
mPlaneHeight = hitPose.position.y;
p.transform.parent = spawnedObject.transform;
GameObject spider = GameObject.FindGameObjectWithTag("spider");
SkinnedMeshRenderer[] renderlist = spider.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var render in renderlist)
{
if (render == null)
continue;
mMatList.Add(render.material);
}
}
else
{
spawnedObject.transform.position = hitPose.position;
spawnedObject.transform.Translate(0, 0.02f, 0);
mPlaneHeight = hitPose.position.y + 0.02f;
}
}
if(spawnedObject != null)
UpdateShader();
}
private void UpdateShader()
{
Vector4 projdir = new Vector4(mLight.transform.forward.x, mLight.transform.forward.y, mLight.transform.forward.z, mPlaneHeight);
foreach (var mat in mMatList)
{
if (mat == null)
continue;
mat.SetVector("_LightDir", projdir);
}
}
}
为了方便找到小蜘蛛的Mesh Renderer,我们给该Mesh加了个Tag “spider”,同时为了实时的反映虚拟物体的位置变化与光照变化,我们还对光照方向与阴影投影平面进行了实时更新,运行效果如下。
从上图可以看到,阴影的渲染效果整体还是不错的,更关键的是性能与使用实时软阴影相比提升非常大,这也是当前很多流行游戏使用平面投影阴影代替内置阴影的主要原因。
从平面投影阴影数学原理可以看到,该阴影只会投影到指定的平整平面上,不能投影到凸凹不平的平面,同时该阴影不会截断,即会穿插到其他物体或墙体时面去,但在AR中,这些问题都不是很严重的问题,为了提高AR应用性能使用该阴影是一个不错的选择。
1、使用顶点投射的方法制作实时阴影 使用顶点投射的方法制作实时阴影