UI优化策略-Shader下篇

原文:https://zhuanlan.zhihu.com/p/33458843

6. 进度条

在进行战斗中的Profile的时候也是发现了每帧都有一个Canvas重建的过程,排查后发现是用于显示倒计时效果的进度条在持续地被更新导致的。

UGUI的进度条控件功能非常通用,但是层次很复杂,包括Background、Fill Area和Handle Slide Area三个部分。它是实现原理是基于Mesh的修改:

Slider控件的更新过程

从上面的gif可以看出,当Slider的value更改的时候,mesh会跟着调整。这可以做到一些UI想要的效果,比如让Fill中的图是一张九宫格的形式,就可以做出比较好看的进度条效果,保证拉伸之后的效果是正确的。

当你需要一条可能持续变化的进度条一直在显示的时候,比如倒计时进度,持续的Canvas重建就不可避免。

针对具体的需求,通过Shader来进行一个简单的ProgressBar也非常容易,通过对于alpha的控制就可以做到截取的效果:

fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; #if _ISVERTICAL_ON float uvValue = IN.texcoord.y - _UVRect.y; float totalValue = _UVRect.w; #else float uvValue = IN.texcoord.x - _UVRect.x; float totalValue = _UVRect.z; #endif #if _ISREVERSE_ON uvValue = totalValue - uvValue; #endif color.a *= uvValue / totalValue < _Progress; color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; } 

这次是在ps阶段进行处理,当然也可以在vs中模拟顶点的缩放效果或者处理uv的偏移。为了支持垂直和反向,这里通过两个宏来进行控制。在实现了条状的进度条,然后准备根据UI的具体需求进行效果上优化的时候,UI同学表示设计方案修改了变成了圆形的进度条(=_=),而且是两个方向同时展示进度,最终效果如下图所示的效果:

红色部分的倒计时进度条效果

不要被酷炫的特效晃瞎眼。。。重点是红色的倒计时进度条部分。在ps中进行位置的计算,然后通过alpha的值同样可以达到控制进度的效果:

fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; float theta = atan2((IN.texcoord.y - _UVRect.y)/_UVRect.w - 0.5, (IN.texcoord.x + 1E-18 - _UVRect.x)/_UVRect.z-0.5); #ifdef IS_SYMMETRY color.a *= ((1 - _Progress) * UNITY_PI < abs(theta)); #else color.a *= ((1 - _Progress) * UNITY_PI * 2 < theta + UNITY_PI); #endif color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #ifdef UNITY_UI_ALPHACLIP clip(color.a - 0.001); #endif return color; } 

这里用了一个消耗比较大的atan2指令来进行弧度值的计算,支持对称和非对称的两种方式,对称的方式用于上面的特殊进度条,非对称的方式用于下面这种环形的进度条。

环形进度条的效果

UGUI针对Image提供了Filled的Image Type来做环形进度条的效果,其原理是根据角度来更改Mesh实现的。

为了兼容Image中使用的Atlas,这里需要将uv信息设置给材质:

/// <summary>
/// 更新贴图的uv值到材质中
/// 注意:需要在Image更新的时候调用本逻辑
/// </summary>
public void UpdateImageUV() { if (relativeImage != null) { Vector4 uv = (relativeImage.overrideSprite != null) ? DataUtility.GetOuterUV(relativeImage.overrideSprite) : defaultUV; uv.z = uv.z - uv.x; uv.w = uv.w - uv.y; relativeImage.material.SetVector(UVRectId, uv); } } 

对于进度条的修改,尤其是环形进度条,是在Shader的ps阶段做的,因此消耗可能还比较大,和Mesh重建的过程的消耗我没有做具体的对比,相当于拿GPU换取CPU的消耗,有可能在某些设备上还不够划算。这个具体使用哪种方法更好,或者是否需要继续优化就看读者自己具体的项目需求了。

7. 总结

针对UGUI的优化零零散散也做了不少,上面讨论到的是其中影响相对大的部分,另外一大块内容是在UGUI中使用特效,这块和本次博客的主题关系不大就不放一起聊了。

可以看到,虽然从结果上看,这些优化后使用的Shader技术都非常非常简单,大都是一些uv计算或者顶点位置的计算,相对于需要进行光照阴影等计算的3D Shader,UI中使用的Shader简直连入门都算不上,但是通过合理地使用它,配合部分C#代码逻辑,可以实现兼顾效果和效率的UI控件功能。

另外,虽然从结果看很简单,仿佛每一个Shader都只需要1-2个小时就可以完成,但是在排查问题和思考优化方法的过程中其实也花费了很多精力,有很多的纠结和思考。这些方案的对比和思考的过程由于时间关系没有全部反映在这篇博客里,但你从Tips和一些只言片语中也可以窥见到一些当时的心路历程……除此之外,由于项目已经到了中后期,还有不少时间花费在新控件的易用性和向前兼容的方面,以让UI和程序同学可以用尽量少的时间来完成对于之前资源的优化工作。

最后,我想坦诚地说,对于UGUI和基于Shader的方案,我没有进行定量的性能对比测试,所以也不能保证基于Shader的方法都一定效率更高,比如最后圆形的Mask就可能会是一个反例。我能做到的是尽量公正地从原理角度分析两者之间在Overdraw、Drawcall和Canvas重建方面的性能差异,也可能有考虑不全的地方,欢迎大家一起讨论~

最后的Tips:除了一些“傻X”bug引发的“神级”优化之外,大部分的优化都是琐碎而且成效不会那么直接、显著的工作。比如上面这些内容可能花费了我大约2个周左右的时间,还需要推进UI和程序对于已有的资源进行修改,而且面临着需求变更的问题……然而,面对优化工作还是那句话——“勿以恶小而为之,勿以善小而不为。”
另外,让团队中的每一个人都了解更多更深入的技术原理,拥有对于性能消耗的警觉,才不至于让问题在最后Profile的时刻集中爆发出来,而是被消化在日常开发的点点滴滴之中,这也是我对于理想团队的期(huan)待(xiang)。

 XXXXX

猜你喜欢

转载自www.cnblogs.com/TravelingLight/p/9002199.html
今日推荐