在移动端实现阴影技术可以提升画面品质,我们说的实时阴影也成为软阴影,鉴于移动端硬件的限制,一些复杂的算法比如PSSM,CSM等不可能在移动端去实时,我们只能找一种捷径,既能把实时阴影实现出来,效率还要正常运行。本篇博客把市面上比较流行的在移动端运行的算法或者阴影插件给读者汇总一下,以便读者在项目开发中能够灵活的运用这些代码。另外,也给读者推荐一个抓取帧数据的非常实用的一个工具:Adreno Profiler工具,它需要安装在移动端,运行游戏时可以实时抓取到该帧使用的Shader代码以及其他数据,非常实用,建议大家好好研究一下。
这种方法是在角色的头上再增加一个摄像机,再角色脚底下加一个面片,通过角色身上的摄像机把角色实时的渲染到角色脚底的平面上,这样就实现了实时阴影的绘制。具体实现方案在我以前写的博客中有介绍:Unity3D实战之移动端实时阴影技术相对来说实现起来比较简单,而且对于角色透明材质实时阴影渲染以及角色重叠的阴影重叠都有解决方案,感兴趣的读者可以去学习一下。
在游戏中使用,一般是对主角使用,怪物一般就不要使用了。
该插件是Unity提供的,下载地址:AssetStore支持动态和静态两种方式,它的Shader代码如下所示:
而且还支持Mipmap阴影渲染,如下图所示:
MipMap具体实现方式可以参考:Percentage Closer Filter
该插件游戏开发中使用的比较少,可供学习。
插件 Fast Shadow Receiver 提供了一种称为“网格树”的网格搜索树,用于搜索接收阴影的多边形 ,网格树不仅适用于阴影,还适用于各种用途,如光投影,AI等。
通常在大场景中,阴影渲染会导致性能问题, 大场景中的对象将占据屏幕上的大部分像素,渲染阴影将使用大量GPU资源。而我们介绍的Fase Shadow Receiver它就适合用于大场景的实时阴影渲染,它会帮我们做一些优化操作。
此外,Fast Shadow Receiver还可以使用阴影贴图, 但是,不会应用光照,因为它会使阴影不真实, 阴影贴图将通过与场景相乘或混合来渲染。
它不仅适用于Blob Shadow Projector,也适用于Light Projector和Bullet Marks。
Fast Shadow Receiver具体实现,这里有篇文档教程,详情查看:
https://nyahoon.com/products/fast-shadow-receiver
Fast Shadow Receiver下载地址:
https://assetstore.unity.com/packages/tools/particles-effects/fast-shadow-receiver-20094
实现的效果图如下所示:
可以在游戏中使用,但是也要做一些优化操作。
王者荣耀游戏使用的就是该方法,已经有上线产品验证过的方法,这说明我们的游戏产品也可以使用,该技术叫平面投影阴影(Planar Projected Shadows)技术,由Jim Blinn 1988年提出。http://www.twinklingstar.cn/2015/1717/tech-of-shadows/#21_Blinns
它实现的原理是:通过相似三角形求一个物体每一个顶点在某个平面上的投影位置,说白了就是求直线在平面上的投影,建议大家在平时可以阅读一下几个比较好的会议论文:GDC系列文章和Siggraph系列文章,这个两个会议的论文都是代表当前比较超前的算法实现,这些算法我们可以将其用Unity或者UE4其他引擎实现出来增加渲染效果。
在这里也给读者简单的把算法实现一下,已知平面坐标系内一个单位向量L(Lx,Ly),坐标系内一点M(Mx,My),求点M沿着L方向 在y = h上的投影位置P,如下图所示:
计算公式具体实现可以参考:Line–plane intersection
根据相似三角形定理计算公式如下所示:
进而得到下面的公式:
公式有了以后,在Shader中的实现如下所示:
Shader "PlanarShadow/Shadow"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_ShadowInvLen ("ShadowInvLen", float) = 1.0 //0.4449261
}
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
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Cull Back
ColorMask RGB
Stencil
{
Ref 0
Comp Equal
WriteMask 255
ReadMask 255
//Pass IncrSat
Pass Invert
Fail Keep
ZFail Keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 _ShadowPlane;
float4 _ShadowProjDir;
float4 _WorldPos;
float _ShadowInvLen;
float4 _ShadowFadeParams;
float _ShadowFalloff;
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 xlv_TEXCOORD0 : TEXCOORD0;
float3 xlv_TEXCOORD1 : TEXCOORD1;
};
v2f vert(appdata v)
{
v2f o;
float3 lightdir = normalize(_ShadowProjDir);
float3 worldpos = mul(unity_ObjectToWorld, v.vertex).xyz;
// _ShadowPlane.w = p0 * n // 平面的w分量就是p0 * n
float distance = (_ShadowPlane.w - dot(_ShadowPlane.xyz, worldpos)) / dot(_ShadowPlane.xyz, lightdir.xyz);
worldpos = worldpos + distance * lightdir.xyz;
o.vertex = mul(unity_MatrixVP, float4(worldpos, 1.0));
o.xlv_TEXCOORD0 = _WorldPos.xyz;
o.xlv_TEXCOORD1 = worldpos;
return o;
}
float4 frag(v2f i) : SV_Target
{
float3 posToPlane_2 = (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(posToPlane_2, posToPlane_2)) * _ShadowInvLen) - _ShadowFadeParams.x), 0.0, 1.0)), _ShadowFadeParams.y) * _ShadowFadeParams.z);
// 另外的阴影衰减公式
//color.w = 1.0 - saturate(distance(i.xlv_TEXCOORD0, i.xlv_TEXCOORD1) * _ShadowFalloff);
return color;
}
ENDCG
}
}
}
该方法在游戏中已经使用过了,可以应用到我们项目中去。
SDF全称 Signed Distance Field Shadow ,该渲染是基于RayMarching实现的,也是一种新的实现方式,可以看到下图中物体的阴影随着距离由近到远也逐渐由清晰渐渐过渡到模糊的效果,表现更加自然而真实。
float calcSoftshadow( float3 ro, float3 rd, in float mint, in float tmax)
{
float res = 1.0;
float t = mint;
float ph = 1e10;
for( int i=0; i<32; i++ )
{
float h = map( ro + rd*t );
float y = h*h/(2.0*ph);
float d = sqrt(h*h-y*y);
res = min( res, 10.0*d/max(0.0,t-y) );
ph = h;
t += h;
if( res<0.0001 || t>tmax )
break;
}
return clamp( res, 0.0, 1.0 );
}
float3 calcNorm(float3 p);
fixed4 raymarching(float3 rayOrigin, float3 rayDirection)
{
fixed4 ret = fixed4(0, 0, 0, 0);
int maxStep = 64;
float rayDistance = 0;
for(int i = 0; i < maxStep; i++)
{
float3 p = rayOrigin + rayDirection * rayDistance;
float surfaceDistance = map(p);
if(surfaceDistance < 0.001)
{
ret = fixed4(1, 0, 0, 1);
//增加光照效果
float3 norm = calcNorm(p);
ret = clamp(dot(-_LightDir, norm), 0, 1) * calcSoftshadow(p, -_LightDir, 0.01, 300.0);
ret.a = 1;
//
break;
}
rayDistance += surfaceDistance;
}
return ret;
}
//计算光照
//计算法线
float3 calcNorm(float3 p)
{
float eps = 0.001;
float3 norm = float3(
map(p + float3(eps, 0, 0)) - map(p - float3(eps, 0, 0)),
map(p + float3(0, eps, 0)) - map(p - float3(0, eps, 0)),
map(p + float3(0, 0, eps)) - map(p - float3(0, 0, eps))
);
return normalize(norm);
}
可以考虑将该算法应用到手机端,效果相对来说更佳。
以上是在移动端经常使用的实时阴影也是软阴影的实现,感兴趣的读者可以深入研究一下。
参考网址:
https://zhuanlan.zhihu.com/p/37918356
https://zhuanlan.zhihu.com/p/42781261
代码下载地址:
https://pan.baidu.com/s/1B4MlzK7VCnCLTNMzFtYNJg 密码:m1vl