NGUI的Draw Call优化方法
在U3D中,Draw Call数量是检验游戏性能和效率的重要指标之一。一个Draw Call其实就是一次对底层图形绘制接口的调用,以通知GPU在屏幕上绘制出相应的对象。对于渲染场景中的这些对象,在每一次Draw Call中除了GPU渲染上比较耗时之外,切换材质与Shader也是非常耗时的操作。由于Draw Call是一个很费资源的操作,因此我们需要保持较低的Draw Call来保证较高的帧率。
对于Draw Call优化我们主要的思路就是合并Draw Call,从而减少GPU的渲染次数。由于目前本人所在的项目组使用NGUI,下面主要讨论NGUI中Draw Call的优化方法:
1.打包图集
每个单独的材质/纹理的渲染一定是会产生DrawCall的,将多张小图打包成一个图集,在渲染UI时就可使用同一个材质/纹理,即可有效降低Draw Call的产生,在NGUI中使用Atlas是一个普遍的好做法。制作图集一般遵循以下几个规则:
- 从功能角度划分图集,例如将公共UI打包成一个图集,将每个系统的UI分别打成单独的图集,主要原则是将显示上密切相关的图片打包到一起 ;
- 避免将无关联的东西打包在一个图集里,特别是那些不可能同时出现的元素,这样不但无法减少DrawCall,还会增加内存消耗;
- 控制图集大小,不要让图集太大;
2.合理规划UI层级
控制好Unity的渲染顺序,才能控制好Draw Call,通过分析NGUI源码发现,NGUI会根据控件的Depth大小来决定渲染顺序,从小到大渲染当前控件与上一个控件使用相同材质时会合并为一个Draw Call,如果当前控件所使用的材质和前一个不同则会产生一个新的Draw Call。
void FillAllDrawCalls(){ //清空所有的DrawCall for (int i = 0; i < drawCalls.Count; i) UIDrawCall.Destroy(drawCalls[i]); drawCalls.Clear(); Material mat = null; Texture tex = null; Shader sdr = null; UIDrawCall dc = null; int count = 0; if (mSortWidgets) SortWidgets(); //对Widget按depth从小到大排序 for (int i = 0; i < widgets.Count; i){ UIWidget w = widgets[i]; if (w.isVisible && w.hasVertices){ Material mt = w.material; Texture tx = w.mainTexture; Shader sd = w.shader; //判断Material,Texture,Shader与当前的widget所使用的是否相同 if (mat != mt || tex != tx || sdr != sd){ //不同时,先缓存上一个DrawCall再创建新DrawCall if (dc != null && dc.verts.size != 0){ drawCalls.Add(dc); //缓存widgets[i-1]的DrawCall dc.UpdateGeometry(count); dc.onRender = mOnRender; mOnRender = null; count = 0; dc = null; //重置DrawCall } //缓存当前widgets的material,texture,shader mat = mt; tex = tx; sdr = sd; } if (mat != null || sdr != null || tex != null){ // 创建新的DrawCall if (dc == null){ dc = UIDrawCall.Create(this, mat, tex, sdr); dc.depthStart = w.depth; dc.depthEnd = dc.depthStart; dc.panel = this; } //材质 贴图 Shader与当前的所使用的相同时,共用一个DrawCall else{ //更新DrawCall的深度范围 int rd = w.depth; if (rd < dc.depthStart) dc.depthStart = rd; if (rd > dc.depthEnd) dc.depthEnd = rd; } w.drawCall = dc; count; ... } } else w.drawCall = null; } ... }
因此规则层级的主要思想就是,在不影响功能实现的前提下,减少不同图集的层级穿插情况,从而利用NGUI的Draw Call合并特性减少Draw Call数量。总的来说,就是为同一图集的元素设置一段独立的深度值区间,不同的图集元素使用不同的深度值区间,尽量不要穿插。
3.不要频繁添加、删除Widget和修改Widget的Depth
NGUI中添加和删除Widget,修改Widget的Depth,会导致该UIPanel下所有DrawCall的重建,这是一个非常耗资源的操作,因此建议不要过于频繁添加、删除Widget和修改Widget的层次。
public int depth { set{ if (mDepth != value){ if (panel != null) panel.RemoveWidget(this); mDepth = value; if (panel != null){ panel.AddWidget(this); if (!Application.isPlaying){ //根据Widget的Depth进行排序 panel.SortWidgets(); //重建所有DrawCall panel.RebuildAllDrawCalls(); } } } } }
4.避免频繁调用SetActive
通过SetActive显示或隐藏一些对象,会影响节点下面所有的组件,这类操作的CPU开销较大,尤其是NGUI的UIWidget在激活的时候会做很多初始化工作,而且会触发GC.Alloc。对于频繁切换显示状态的UI可以采用以下两种方法来解决
- 通过设置localPosition将控件移出或移入视野;
- 通过设置Widget的alpha值,可以将alpha设置为一个很小但不为0的值,例如0.01;
4.动静UI元素分离
NGUI是以Panel为单位来整理Draw Call的,一个Widget发生改变会引起其他使用相同Draw Call的Widget重新填充顶点,UV等信息到DrawCall,并重新绘制一次DrawCall。如果Panel组件数量比较多的话,会造成CPU的开销突然上升,这时可以通过划分动静态Panel的方式来避免这种情况。比如讲游戏主城中把经常变化的组件专门提取出来单独设立一个Panel管理它,比如时间、血条等,同时把静态的不经常变动的UI组件放在同一个UIPanel下,从而降低动态UI刷新时造成的CPU消耗。
总结
本文主要描述NGUI的Draw Call优化,有时候项目中的不是一味的最求减少DrawCall就行。因为Draw Call减少必定会造成大量组件集中在一个Panel之中,这时其中一个控件发生变化的,很可能会引起其他使用相同DrawCall的控件也相应刷新。虽然这个开销一般不会每帧都发生,但是不可避免地会在性能曲线上产生峰值毛刺,游戏帧率的不稳定/卡顿往往就是由这种小毛刺组合而成的,因此在实际开发中为了在CPU和GPU性能间找平衡,多增加几个Draw Call也能获得更大的性能收益。