阴影效果:
上面两幅图分别为:平面阴影和球体阴影的效果
平面阴影简述:
平面阴影是一种比较特殊的情形。在这种情形里,我们只考虑物体的阴影投射到平面上的情形,所以有一套相对比较简单的专用算法。
首先考虑最简单的情况,如何计算一个平行光的投影。平行光在我们的计算中其实就是一个方向矢量,是阴影的投射方向,而平面是阴影要影响的目标物体。我们需要知道到目标物体的Object Space矩阵,在目标物体的空间内将投影物体的顶点进行重新计算,计算其沿光线方向,在阴影接受平面上的位置,这个位置关系可以通过三角形相似来计算。如果我们使用Unity自带的Plane作为阴影接受平面,那么我们只需要重新计算顶点的xz位置,如果阴影投射到Build In的Plane上,那么在其Object Space中,y应该为0,但是实际使用时,为了保证阴影永远在物体上面,我们会对z进行偏移。
平面阴影实现原理:
首先,每一个投射平面阴影的物体都需要下面这么一个脚本来告诉它阴影接受物体的信息,
具体来说就是到其Object Space空间的矩阵,以及从平面的Object Space返回的矩阵。
//C#代码:放在需要显示阴影的对象上
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class PlaneShadowCaster : MonoBehaviour
{
public Transform reciever; //阴影接收平面(通常是地面)
void Update()
{
GetComponent<Renderer>().sharedMaterial.SetMatrix("_World2Ground", reciever.GetComponent<Renderer>().worldToLocalMatrix);
GetComponent<Renderer>().sharedMaterial.SetMatrix("_Ground2World", reciever.GetComponent<Renderer>().localToWorldMatrix);
}
}
// shader,放在需要显示阴影的对象上
// shader,放在需要显示阴影的对象上
Shader "Custom/PlanarShadow" {
Properties {
_Instensity ("Shininess", Range (2, 4)) = 2.0 //光照强度
}
SubShader {
//对物体本身做一个简单的光照计算
pass
{
Tags{"LightMode"="ForwardBase"}
Material{Diffuse(1,1,1,1)}
Lighting On
}
//计算阴影
Pass
{
Tags{"LightMode"="ForwardBase"}
Blend DstColor SrcColor
Offset -1, -1 //使阴影在平面之上
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4x4 _World2Ground; //阴影接收平面(世界空间到模型空间的转换矩阵)
float4x4 _Ground2World; //阴影接收平面(模型空间到世界空间的转换矩阵)
float _Instensity;
struct v2f{
float4 pos:SV_POSITION;
float atten:TEXCOORD0;
};
v2f vert(float4 vertex:POSITION)
{
float3 litDir;
litDir = WorldSpaceLightDir(vertex);//世界空间主光照相对于当前物体的方向
litDir = mul(_World2Ground,float4(litDir,0)).xyz;//光源方向转换到接受阴影的平面空间
litDir = normalize(litDir);// 归一
float4 vt;
vt = mul(_Object2World,vertex); //将当前物体转换到世界空间
vt = mul(_World2Ground,vt); // 将物体在世界空间的矩阵转换到地面空间
vt.xz = vt.xz - (vt.y/litDir.y)*litDir.xz;// 用三角形相似计算沿光源方向投射后的XZ
vt.y=0;// 使阴影保持在接受平面上
vt = mul(_Ground2World, vt); // 阴影顶点矩阵返回到世界空间
vt = mul(_World2Object, vt); // 返回到物体的坐标
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, vt);//输出到裁剪空间
o.atten = distance(vertex, vt) / _Instensity;// 根据物体顶点到阴影的距离计算衰减
return o;
}
float4 frag(v2f i):COLOR
{
//return float4(0.3, 0.3, 0.3, 1);//一个灰色的阴影出来了
return smoothstep(0,1,i.atten/2);
}
ENDCG
}
}
}
代码详解:
在此Shader中,我们首先使用固定管线对物体做了一个简单的照明。在计算阴影的ForwardBase中,首先使用一个可以叠加阴影的混合模式,然后使用z偏移保证出来的阴影在接受平面之上。_World2Ground和_Ground2World分别是我们自定义的两个进出阴影接收平面矩阵。在具体计算中,首先将光源方向和投影物体的顶点都转换到接收平面的空间,在它们都处于同一个空间后,通过简单的三角形近似算法,来计算投影物体顶点沿光线投射后在接收平面上的新位置。因为这是一个Build In的Unity Plane,所以只计算其xz分量即可,并将Y分量设为0,这样投影出来的阴影就出现在接收平面上了。投影计算完成后,我们返回到世界空间,然后到投影物体本身的Object Space,之后的事情就如通常那样,一个经典的MVP变换即可。
效果如下:
左边为我们计算出的平面阴影,右边为Unity的计算出的阴影;
(我们的物体显示黑色,是因为在Shader的第一个pass中只是对物体本身做了一个简单的光照计算,不够完整,缺少漫反射等)
点光源的平面阴影效果:
Tip:
1:平面阴影作为一种最简单的实时阴影实现,尽管其仅能局限于在完全平坦的地面的情况下使用,但由于其性能良好,在许多移动端手游中仍然可以发挥较强的使用价值。
2:以上是针对平行光来做的,点光源也是类似的,对于通过使用WorldSpaceLightDir()方法来计算光源方向来说,就完全一样了,但效果不好;
3:一般情况下,比如用Shadow Mapping和Shadow Volumes计算阴影的衰减是比较困难的,但是在此例中,我们己经知道投射阴影物体的顶点在计算前和计算后的位置,根据这两个位置的距离,我们还是可以考虑计算一下阴影的衰减问题的。
但这个方法还有一个显而易见的问题,那就是物体本身是立体的,不是一个平面,因此这个计算前后的点的距离是包括物体本身厚度的,这个厚度就会表现在阴影上。要解决这个问题,我们可以先把物体变换到灯光空间,使用_World2Light矩阵沿着灯光方向把物体压扁,然后投射物体,这样计算出来的阴影衰减就不会包括物体的厚度了。
球体阴影实现原理:
1.根据阴影接收平面上 (点的入射光矢量)和(点到球体的矢量)计算【点积】求出【角度】。
2.通过【角度】的sin值,求出【对边】并与【球体半径】进行比较。
3.所求【对边的长度】大于【半径】,说明该点被光照,反之该点是阴影点。
添加到阴影接受平面(地面)对象上的C#脚本:
using UnityEngine;
using System.Collections;
public class SphereShadow : MonoBehaviour
{
public GameObject sphere; //投影对象
void Update()
{
// Vector3 pos = sphere.transform.localPosition;
Vector3 pos = sphere.transform.position;
GetComponent<Renderer>().sharedMaterial.SetVector("_spPos", new Vector4(pos.x, pos.y, pos.z, 1f));
GetComponent<Renderer>().sharedMaterial.SetFloat("_spR", sphere.transform.localScale.x / 2);
}
}
添加到阴影接受平面(地面)对象上的Shader:
// shader,放在需要接收阴影的对象上
Shader "Custom/SphereShadow" {
Properties{
_spPos("Sphere pos", vector) = (0,0,0,1) // 球体位置
_spR("radius", float) = 1 // 球体半径
_Intensity("Intensity", range(0,1)) = 0.5 // 阴影浓度
}
SubShader{
Pass{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _spPos; // 球体位置
float _spR; // 球体半径
float _Intensity; // 阴影浓度
float4 _LightColor0; // 颜色
struct v2f {
float4 pos:SV_POSITION;
float3 litDir:TEXCOORD0;// 世界坐标中灯光方向矢量
float3 spDir:TEXCOORD1; // 在世界坐标中投影球体方向矢量
float4 vc:TEXCOORD2; // 逐顶点计算的光照
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);// 获取顶点视图位置
o.litDir = WorldSpaceLightDir(v.vertex);// 获取世界坐标中灯光对顶点的方向矢量
o.spDir = (_spPos - mul(_Object2World, v.vertex)).xyz;//世界坐标中该顶点到投影球体的矢量
// 顶点的光照计算。该顶点在对象坐标中的光照方向矢量
float3 ldir = ObjSpaceLightDir(v.vertex);
ldir = normalize(ldir);
o.vc = _LightColor0 * max(0, dot(ldir, v.normal));//根据顶点的入射光线和法线角度求该顶点光照
return o;
}
float4 frag(v2f i) :COLOR
{
float3 litDir = normalize(i.litDir);//获取点的入射光线的单位向量
float3 spDir = i.spDir; // 获取该点到投影球体的矢量
float spDistance = length(spDir); //该点到球体的距离
spDir = normalize(spDir); //该点到投影球体的单位向量
float cosV = dot(spDir, litDir);// 该点到球体 与 该点到入射光线的夹角
float sinV = sin(acos(max(0, cosV)));// 拿到余弦值大于0的角度,求正弦
float D = sinV * spDistance; // 解三角形,求对边
float shadow = step(_spR, D); // 如果对边小于半径返回0,该点为阴影点
float c = lerp(1 - _Intensity, 1, shadow);// shadow由0到1
return i.vc * c; // 为0的时候是阴影点
}
ENDCG
}
}
}
效果如下:
Tips:
不管对于哪种形状的投影对象,球体阴影的投影均为圆形,如下:对于Cube(立方体)的投影,效果如下: