NGUI的Draw Call优化方法

发表于2017-08-01
评论1 4.1k浏览

        在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也能获得更大的性能收益。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论