闲谈:
最近项目中我发现程序找了个影子插件PlaneShaodw 还挺好用的,效率也高,之后我就将所有项目中新优化制作的shader 全部采用插件的影子方案,结果不出所料,上周测试项目的时候 ,发现影子不能合批(SRP),更邻人费解的是插件自己写了个库文件,里面算法非常深(做了宏处理,分了好了些变体)。所以周末准备自己搞一波搞效果影子。
大多数网上找的影子大家是不是遇到这样的问题:
几乎无解
在研究的过程中(重点研究了王者荣耀,欸,又下载下来玩了一会儿,给自己打酱油找借口,HAHA~~!)之前我一直认为王者荣耀的实现是Projector 方式,今天才发现,大错特错,他的原理还是使用的PlaneShaodw ,重点对影子做了虚化处理(最终实现效果会 完美还原王者荣耀影子效果)。
一: 先来对比两种方式 优缺点和原理:
Projector:
投影纹理使用的技术叫做Projective Texture Mapping
对于被投影的物体,将他的顶点坐标变换到投影体的Texture Space中,计算得到UV坐标,使用UV坐标采样投影纹理,将颜色输出。
具体实现
实现投射阴影,需要如下几个关键步骤:
- 创建新的相机,将投射阴影的物体渲染到一张Render Texture中
- 利用
Projector
组件,阴影纹理投射到接受阴影的物体上 - 相机渲染阴影纹理的Shader
Projector
组件需要的材质纹理
优缺点:
1:优点
性能较为高效
简单场景下,相比Shadow Map,节省了深度纹理进行深度比较的计算。
2:缺点
复杂场景的性能消耗较大
URP、HDRP不支持
RT会额外占用现存;
大范围视角时,需要较高阴影精度;
复杂的地形,性能消耗较大,需要遍历所有接收阴影的物体;
需要定义Layer。(管理不当会加大开发难度)
未接收到阴影的物体,也会渲染一遍阴影。
总结 : 淘汰淘汰,淘汰就对了~~~!
Planar Shadow:
沿光线方向,将模型的顶点投射到某个平面上。
核心实现思路:将顶点有模型坐标
转为平面坐标
。
L
:平行光方向,单位向量(顶点指向光源)。
N
:平面法线方向。
d1
:单位长度的光方向向量在法线方向的投影长度。
A
:模型顶点,在世界空间的坐标。
B
:模型顶点,垂直于平面的投影坐标。
C
:模型顶点,沿平行光投射方向,在平面的投影坐标。
高中数学: 已知A,L,N,求C坐标 ?
分析:AC与-L平行长度应该为:d2/d1,最终就可以得到C与已知变量的关系:
d1= dot(L,N)
d2=A.y -B.y (注意这是空间,别看我画的图是平面, 所以空间坐标:xyzw 别忘记了)
c =A-L*(d2/d1)
知道大家看不懂,所以觉得厉害就行了~HAHA~~!
优缺点:
1:优点
性能高效
计算量较小;
无需额外的显存带宽存储阴影纹理;
支持SRP Batcher,渲染流程跟普通的模型无异,且更轻。
动态性较好
阴影跟随动画实时变动。
视觉效果好
不存在类似Shadow Mapping中出现的锯齿、失真等情况,轮廓清晰。
2:缺点
场景限制较大
仅适用于平面,阴影不会投射到平面以外的物体上。
Artifacts
瑕疵较多,淡出效果、地表穿模等情况下,效果存在问题。
不好意思,熊猫老师 完美解决了淡出效果。~HAHA~!
总结 : 用就对了,移动端高速起飞~!
核心实现:
添加模板缓冲区是解决影子重叠问题(降低透明度就能发现)
Stencil
{
Ref 1 //和模板缓冲区的值进行比较。
Comp NotEqual //比较函数, 决定参考值和模板缓冲区的值如何比较 (是否相等) NotEqual
Pass Replace //模板测试通过时候的操作 。 Replace 模板缓冲区中的值设为参考值。Replace
Fail Keep //模板测试失败时候的操作 。 keep是保持缓冲区当前的值
//ZFail Keep
}
模板测试语义:
1.Ref:参考值,和模板缓冲区的值进行比较。
2.Comp :比较函数,参考值和缓冲区的值如何比较
3.Pass : 模板缓冲区测试通过时的操作。
4.Fail : 模板缓冲区测试失败时的操作。
5.ZFail : 深度测试失败时的操作。
操作有以下几类类型:
1.Keep : 保持缓冲区中的当前值。
2.Zero : 修改缓冲区的值为0 .
3.Replace : 将模板缓冲区中的值设置为参考值。
4.IncrSat : 递增模板缓冲区中的值,且不超过最大值255 。
5.DecrSat : 递增模板缓冲区中的值,且不低于0 。
6.Invert : 按位取反模板缓冲区中的值。
7.IncrWarp : 递增模板缓冲区中的值,并在溢出时从0 重新开始 。
8.DecrWrap : 递增模板缓冲区中的值,并在溢出时从255重新开始 。
计算影子方向位置 算法 (其实就是上面的数学公式演变过来)
float3 PlanarShadowPos(float3 posWS)
{
float3 L = normalize(_LightDir);
float3 N = float3(0, 1, 0);
float d1 = dot(L, N);
float d2 = posWS.y - _PlaneY;
// 阴影坐标沿法线方向偏移一点,防止与平面重叠时出现z-fighting的问题
float3 offsetByNormal = N * 0.001;
return posWS - L * (d2 / d1) + offsetByNormal;
}
顶点坐标 处理影子:(建议放弃片元中处理影子)
重点之重,解决影子虚化效果:
//得到中心点世界坐标
float3 center = float3(unity_ObjectToWorld[0].w, _PlaneY, unity_ObjectToWorld[2].w);
//计算阴影衰减
float falloff = 1 - saturate(distance(posWS, center) * _ShadowFalloff);
最后说明下:URP其实不是不支持多Pass 渲染, URP最多支持3个Pass
但是每个Pass一定要对 Tags 做 处理:
m_ShaderTagIdList.Add(new ShaderTagId("UniversalForward"));
m_ShaderTagIdList.Add(new ShaderTagId("LightweightForward"));
m_ShaderTagIdList.Add(new ShaderTagId("SRPDefaultUnlit"));
上才艺:
Shader "XiongMaoWuDao/Shadow"
{
Properties
{
[MainColor] _BaseColor("BaseColor", Color) = (1,1,1,1)
[MainTexture] _BaseMap("BaseMap", 2D) = "white" {}
//影子处理
//影子淡出处理
_ShadowColor("Color", Color) = (0, 0, 0, 1)
_LightDir ("Light Direction", Vector) = (0, 1, 0, 0)
_PlaneY ("Plane Height", Float) = 0
_ShadowFalloff ("ShadowFalloff ",Range (0,1)) = 0.5
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Opaque"}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
float4 _ShadowColor;
half3 _LightDir;
half _PlaneY;
half _ShadowFalloff;
CBUFFER_END
ENDHLSL
pass
{
Name "Forward"
Tags {
"LightMode"="UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = TransformObjectToHClip(v.vertex.xyz);;
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
return o;
}
half4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv) * _BaseColor;
}
ENDHLSL
}
Pass
{
Name "XiongMaoWuDao"
Tags{
"LightMode" = "SRPDefaultUnlit"
}
//添加模板缓冲区是解决影子重叠问题(降低透明度就能发现)
Stencil
{
Ref 1 //和模板缓冲区的值进行比较。
Comp NotEqual //比较函数, 决定参考值和模板缓冲区的值如何比较 (是否相等) NotEqual
Pass Replace //模板测试通过时候的操作 。 Replace 模板缓冲区中的值设为参考值。Replace
Fail Keep //模板测试失败时候的操作 。 keep是保持缓冲区当前的值
//ZFail Keep
}
Blend SrcAlpha OneMinusSrcAlpha
// ZWrite off
// Offset -1 , 0
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 color : COLOR;
};
//阴影方向计算。
float3 PlanarShadowPos(float3 posWS)
{
float3 L = normalize(_LightDir);
float3 N = float3(0, 1, 0);
float d1 = dot(L, N);
float d2 = posWS.y - _PlaneY;
// 阴影坐标沿法线方向偏移一点,防止与平面重叠时出现z-fighting的问题
float3 offsetByNormal = N * 0.001;
return posWS - L * (d2 / d1) + offsetByNormal;
}
v2f vert (appdata v)
{
v2f o;
// 顶点坐标,转世界坐标
float3 posWS = TransformObjectToWorld(v.vertex.xyz);
// 世界顶点坐标转为平面坐标
posWS = PlanarShadowPos(posWS);
o.vertex = TransformWorldToHClip(posWS);
o.uv =v.uv;
//得到中心点世界坐标
float3 center = float3(unity_ObjectToWorld[0].w, _PlaneY, unity_ObjectToWorld[2].w);
//计算阴影衰减
float falloff = 1 - saturate(distance(posWS, center) * _ShadowFalloff);
o.color = _ShadowColor;
o.color.a *=falloff;
return o;
}
half4 frag (v2f i) : SV_Target
{
return i.color;
}
ENDHLSL
}
}}
效果如下:
下一篇:Unity URP无光照下Shadow,SRP高效合批 制作 <二> ,敬请期待!