常见方案问题
半透明的阴影主要涉及2个功能 投递阴影和接收阴影,投递阴影 unity里比较好实现,可自定义shadowcast的pass 或直接fallback 带这个pass的 内置shader即可。甚至可以不改任何shader 直接同位置摆放一个阴影代替投射物也行,unity支持 不显示自己但只投递阴影的shadowonly 渲染模式。所以不会代码的美术自己也能搞定这问题,网上大量可用教程。
这里讨论的是接受阴影,虽然也有大量教程但都是说一半就停了,基本都是利用 "AutoLight.cginc" 内对shadowmap的计算 然后 渲染队列改成 2500以下。这种篡改了正确排序的做法, 至少有2种错误。1与天空盒遮挡关系,2与远处半透明遮挡关系。如图错误方法 与本方案对比
因为强改渲染顺序 导致 天空混合 和半透明绿球混合错误
正确做法 依然用3000队列渲染 天空和半透明绿球混合正确
原因分析
半透明是个很模糊的概念,至少有2种不同技术角度描述,1根据混合模式,2根据渲染队列。这里讨论隐藏较多的渲染队列问题。
引擎有自己的规则 2500以下按OpaqueGeometry阶段 (不论forward还是deferred)渲染。2501及以上 按TransparentGeometry阶段渲染。这2个阶段有哪些额外区别呢?
1 这2个阶段中间会插入 skybox的渲染。skybox 虽然定义成1000的渲染队列 符合理论的描述 1clear 2天空 3实体从近到远排序渲染 4半透明从远到近排序渲染 。但实际上unity没这样做 而是在2500之后 2501之前渲染,就是所有的实体渲染完渲染。这是为了性能 把天空作为 最远的实体 避免overdraw。
2 这2个阶段引擎渲染的keywords 不同,这里主要是2501之后 不传shadowmap了
代码实现
知道了 这2个原因后 就可以实现 3000队列的阴影接收了。
首先获得shadowmap并传给shader的一个全局贴图变量 这种做法避免了rt拷贝 但不支持多份
然后shader内 有了这张图 就可以 走一般的 阴影流程
Tags{ "RenderType" = "Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : texcoord0;
SHADOW_COORDS(2)
};
fixed4 _ShadowColor;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(UNITY_MATRIX_M, v.vertex);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(_ShadowColor.rgb * atten ,0.5);
}
ENDCG
}
虽然这样已经好了很多 解决了2个顺序问题,但依然不足以一个真实项目的使用(这就是我一直不肯离开一线开发的原因吧),实际上 还需要考虑 非平行光的阴影,如果仅仅考虑平行光,上面改用screenshadowmask 性能更好些。
应用扩展
如果要支持点光或聚光灯的半透明阴影接收,cs代码和上面一样,但需要增加一个forwardadd的pass 特别是 #pragma multi_compile_fwdadd_fullshadows
.但是这也仅仅是实现单个光源阴影接收。如果要实现 同个场景同时多个光源给半透明物体 投递阴影,可以实现但比较复杂,要么挨个复制shadowmap 挨个采样。要么做个挨个计算出screenshadowmask合并到引擎默认的screenshadowmask 图上,对象渲染不需要额外采样。都是性能不高的做法,最后说下 unity默认不让半透明采样shadowmap的原因我做了个小小猜测,粒子等 大量半透明重叠的情况下 如果半透明默认采样shadowmap会非常非常消耗性能。
完整代码如下
Shader "Unlit/alphaShadow"
{Properties
{
_ShadowColor("Shadow Color", Color) = (0.1, 0.1, 0.1, 0.53)
}
SubShader
{
Tags{ "RenderType" = "Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : texcoord0;
SHADOW_COORDS(2)
};
fixed4 _ShadowColor;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(UNITY_MATRIX_M, v.vertex);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(_ShadowColor.rgb * atten ,0.5);
}
ENDCG
}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
Tags{ "LightMode" = "ForwardAdd" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : texcoord0;
#ifdef DIRECTIONAL
SHADOW_COORDS(1)
#endif
};
fixed4 _ShadowColor;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(UNITY_MATRIX_M, v.vertex);
#ifdef DIRECTIONAL
TRANSFER_SHADOW(o);
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target
{
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(_ShadowColor.rgb * atten ,0.5);
}
ENDCG
}
}
FallBack "Diffuse"
}