Unity开发中的性能优化——阴影优化
Unity开发中的性能优化——阴影优化
在使用Unity开发小程序游戏时,出现了ios端严重发烫的情况。检查发现cpu占用不算太高,推测性能压力主要集中在GPU侧。尝试对场景中的物体和UI合批优化,没有取得显著成效。当我尝试关闭全局阴影后,发热情况显著改善。但是策划要求必须要有阴影来展示立体感,于是我关闭全局阴影使用自定义shader,通过写死灯光方向计算单个物体的阴影投射实现了小范围的动态阴影。本篇记录这种阴影优化的实现过程和思路。
本文使用的渲染管线为Unity Build-in管线+前向渲染。
全局阴影原理简述
打开一个新项目,创建一个Cube和Plane,开启全局阴影并打开Frame Debugger。观察Frame Debugger的渲染流程。

观察发现Unity的全局阴影渲染流程大致分四个步骤:UpdateDepthTexture(深度纹理绘制)、RenderShadowMap(ShadowMap渲染)、CollectShadows(阴影收集)、接收方着色。
UpdateDepthTexture(深度纹理绘制)
绘制一张纹理,用来表示相机视角下每个场景中每个像素距离相机有多远。
当开启了屏幕空间阴影反投射、SSAO、软粒子、景深等效果时,Unity会自动进行深度纹理绘制,且这些效果共用一个深度纹理。
值得注意的是,当物体的shader中带有ShadowCaster pass,不管这个物体是否接收/投射阴影,Unity都会对这个物体进行深度纹理绘制。因此,当场景中大部分物体都没有接收或投影阴影的需求时,可以使用不带有ShadowCaster pass的自定义shader,避免深度纹理绘制多余的物体。
[!NOTE]
注意,如果当前 Shader 文件中没有 LightMode 为 ShadowCaster 的 Pass,Unity就会去 Fallback 指定的 Shader 中继续寻找,如果仍没有找到,就继续去 Fallback 里寻找,直到找到了 ShadowCaster Pass 或没有 Fallback,如果最后找到了,Unity 会使用该 Pass 更新光源的阴影映射纹理。因此想要彻底关闭ShadowCaster pass,请注意FallBack的选择。
RenderShadowMap(ShadowMap渲染)
ShadowMap的绘制与DepthTexture类似,区别在于ShadowMap是绘制灯光视角下的深度图,只记录遮挡关系,而且只会绘制开启了阴影投射/接收的物体。ShadowMap是否绘制与Shader文件中是否含有ShadowCaster pass无关。
CollectShadows(阴影收集)
这是一个全屏的Pass,对每个屏幕像素执行。执行时会对屏幕上所有像素查询一遍shadowmap,算出这点在不在阴影里,然后将结果绘制成一张全屏幕尺寸的明暗遮罩图,供后续着色时使用。
着色
最后就是根据明暗遮罩图和物体颜色计算着色,将阴影绘制出来
优化实现
通过对阴影绘制原理的大致了解,可以发现,在开启了阴影的情况下,不管物体是否接收/投射阴影,在CollectShadows阶段都要参与一次明暗遮罩图的绘制。如果该物体还使用了带有ShadowCaster pass的shader,那么还会参与一次UpdateDepthTexture的绘制。
那么在场景中大部分物体都不需要阴影的情况下,关闭全局阴影可以节省大量的渲染开销。
可以看到,当关闭了阴影后,一个立方体+一个平面的简单场景绘制次数从14次锐减到了3次。

平面投射阴影
在关闭了全局阴影后,如果还想要保留阴影效果,可以优先使用阴影贴图——美术出一张阴影的图片,放在物体的合适位置,但是这种阴影在物体高度或角度变化时难以实现动态的阴影效果。
如果想要保留部分动态的阴影,我这里提供一种方法:抛弃Unity的阴影渲染管线,使用自定义Shader,通过写死光照角度,使用纯数学计算的方法在对应位置绘制阴影。
关闭场景中Lisght的阴影选项,或者直接关闭Unity全局阴影,实现shader代码:
Pass
{
Name "PlanarShadow"
Cull Off
ZWrite Off
ZTest LEqual
Blend DstColor Zero
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
fixed4 _ShadowPlanarColor;
// 光线传播方向 = 场景灯 euler(120,0,0) 的 forward,单位向量,写死
#define LIGHT_TRAVEL half3(0.0, -0.8660254, -0.5)
// 光线与地面夹角 60°:|lightDir.y| = 0.866
#define LIGHT_Y 0.8660254
struct a2v
{
UNITY_VERTEX_INPUT_INSTANCE_ID
float4 vertex : POSITION;
};
struct v2f
{
UNITY_VERTEX_INPUT_INSTANCE_ID
float4 pos : SV_POSITION;
};
v2f vert (a2v v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
float3 wp = mul(unity_ObjectToWorld, v.vertex).xyz;
// 沿光线方向压到 y=0:t = y / |lightDir.y|
float t = max(wp.y / LIGHT_Y, 0.0);
wp += LIGHT_TRAVEL * t;
// 抬高 0.05 防止与地面 z-fight(与 BlockShade 贴片同高度)
wp.y += 0.05;
o.pos = mul(UNITY_MATRIX_VP, float4(wp, 1.0));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return _ShadowPlanarColor;
}
ENDCG
}

在使用了该Pass后,阴影成功绘制,且能够随物体移动和缩放。观察FrameDebugger也可以看到,渲染时不再进行全局的深度纹理绘制、ShadowMap绘制和CollectShadows。
这种阴影由于过于简易而具有明显的缺点:
- 只能投影在设定好的平面上(我这里设置为y=0的平面),无法显示类似在楼梯投影的效果。
- 没有遮挡关系
- 如果有两阴影叠加会导致阴影颜色加深
- 光照角度是写死的,阴影不随场景灯光移动而改变,如果调整了场景光源,则需要在shader中修改参数


结束
这种方式适用于类似边框、墙壁、门的阴影。需要动态且不会叠加遮挡。在实际游戏制作过程中,这种阴影可以与美术实现的假阴影配合使用,需要动态缩放、旋转、上下移动的物体使用这个平面投射阴影,而静态的、只会平移的物体可以使用假阴影。这样抛弃使用Unity的阴影渲染管线可以很大程度地缓解渲染压力。
本篇提供的方法灵感来自:优化光照以获得正常帧率 | 移动应用和游戏开发 | Unity
同时这篇文章中还介绍了有关光照烘焙和批处理的相关优化内容。

浙公网安备 33010602011771号