首页 > 代码库 > 【Unity3D游戏开发】NGUI之DrawCall数量 (四)

【Unity3D游戏开发】NGUI之DrawCall数量 (四)

看了非常多关于NGUI drawCall的文章。见得比較多的一个观点是:一个 Atlas 相应一个Drawcall。

但事实上NGUI内部有自己的一套对DrawCall的处理规则。

相关的规则有:

1.Atlas图集数量有关

2.Atlas图集的调用顺序(绘制顺序)有关

3.和UIPanel的数量有关


一、降低NGUI 3的DrawCall数量

升级到NGUI3。 DrawCall数由5个增长到了十七八个。想想应该不会是NGUI的问题吧。后来整理了一下。发现有两点: 
        1)对于同一Atlas。DrawCall数取决于Panel的数量(实际上是UIPanel这个脚本的数量)。比方说,我有两个Sprite,这两个Sprite属于同一Atlas。可是位于不同的Panel下。这时候DrawCall 数是2, NGUI 2中则是1。使用建议就是仅仅使用一个Panel。

 
        2)对于不同Atlas,同一Panel下的Sprite。可通过Depth调节显示层级,Z值无论用,这点跟NGUI 2中刚好相反。还有就是不同Atlas的Sprite 的Depth值尽量不要来回穿插。比方Atlas A中有两个Sprite a 和 aa,Depth分别为1,3;Atlas B中有两个Sprite b 和 bb。 Depth分别为2,4。 则DrawCall 总数为4而不是2。

(在NGUI 3中,你能够点击Panel ,在Inspector面板中看到每个DrawCall的调用细节 ) 

        简单的说就是DrawCall的数量不仅仅跟Atlas的数量有关。还跟Atlas调用顺序有关,使用的时候最好仅仅用一个Panel。 不同Atlas的Sprite Depth尽量不穿插

參考文章:http://game.ceeger.com/forum/read.php?tid=14653

 

二、NGUI 降低DrawCall

前置说明一:

Unity中的drawcall定义:

每次引擎准备数据并通知GPU的过程称为一次Draw Call。

Unity(或者说基本全部图形引擎)生成一帧画面的处理过程大致能够这样简化描写叙述:引擎首先经过简单的可见性測试,确定摄像机能够看到的物体。然后把这些物体的顶点(包含本地位置、法线、UV等)。(顶点怎样组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源。纹理。渲染方式(由材质/Shader决定)等数据准备好,然后通知图形API——或者就简单地看作是通知GPU——開始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,终于构成一幅图像。

 

前置说明二:

NGUI中的UIWidget的显示顺序:

每个UIWidget的显示顺序由depth值决定。跟z轴没关系,而这个depth值是由两部分组成的,一个是UIWidget所在的UIPanel的depth和UIwidget自身的depth值进行加权计算。

而且,UIPanel的权重很大。能够觉得,UIPanel的depth大的全部UIWidget比UIPanel的depth小的全部UIWidget比最后计算的depth一定大。

举个样例:

 

UIPanel1    depth  x                      UIPanel2    depth  y

UIWidget1  depth  m                      UIWidget2  depth  n

 

仅仅要 x > y。那么无论m和n的大小,UIWidget1最后的depth一定大于UIWidget2。

 

 

降低drawcall的规则:

1、同一个UIPanel下的texture和font尽量放在同一个altals下。

也表达了另外一个意思,使用同一个altals的元素尽量放在同一个UIPanel以下。

2、假设一个UIPanel以下使用了多个altals,那么尽量让使用同样altals的元素连续,尽量避免altals交叉。

 

规则1的前半部分好理解。后半部分,參照前面显示顺序问题能够知道。假设使用同一个altals的元素在两个不同的UIPanel以下。这就必定导致它们的drawcall分离。

所以即使调整它们的depth一致,也无法合并成一个drawcall.

 

规则2的意思,举个样例就明确了:

同一个UIPanel下有4个UIWidget。w1。w2,w3,w4。

当中 W1和W2引用altals1。

当中 W3和W4引用altals2。

 

假设它们的depth顺序为  w1 : 1,w2 :2,w3 : 3,w4 : 4。

那么整个渲染须要2个drawcall,由于渲染顺序为 w1,w2,w3。w4。

而w1和w2公用一个altals,所以能够合并成一个drawcall,同理w3和w4能够合并成一个drawcall。

 

而假设它们的depth顺序为: w1 : 1。w2 :3,w3 : 2,w4 : 4。

那么整个渲染须要4个drawcall。由于渲染顺序为 w1。w3,w2,w4。

由于w1和w3不是公用一个altals。所以仅仅能分开渲染。

同理w3和w2。w2和w4也仅仅能分开渲染。

參考文章:http://blog.csdn.net/monzart7an/article/details/25212561

 

三、源代码分析NGUI的DrawCall合并原理

NGUI为了降低GPU状态切换的消耗(比方切换material),把同样material的widget合并,降低DrawCall的数量。下文描写叙述了NGUI怎样对widget归类。以及降低DrawCall须要注意的地方。

归类widget的代码在UIPanel中的FillAllDrawCalls()里。代码例如以下:

void FillAllDrawCalls ()
        {
                for (int i = 0; i < drawCalls.size; ++i)
                        UIDrawCall.Destroy(drawCalls.buffer[i]);
                drawCalls.Clear();

                Material mat = null;
                Texture tex = null;
                Shader sdr = null;
                UIDrawCall dc = null;

                if (mSortWidgets) SortWidgets();

                for (int i = 0; i < widgets.size; ++i)
                {
                        UIWidget w = widgets.buffer[i];

                        if (w.isVisible && w.hasVertices)
                        {
                                Material mt = w.material;
                                Texture tx = w.mainTexture;
                                Shader sd = w.shader;

                                if (mat != mt || tex != tx || sdr != sd)
                                {
                                        if (mVerts.size != 0)
                                        {
                                                SubmitDrawCall(dc);
                                                dc = null;
                                        }

                                        mat = mt;
                                        tex = tx;
                                        sdr = sd;
                                }

                                if (mat != null || sdr != null || tex != null)
                                {
                                        if (dc == null)
                                        {
                                                dc = UIDrawCall.Create(this, mat, tex, sdr);
                                                dc.depthStart = w.depth;
                                                dc.depthEnd = dc.depthStart;
                                                dc.panel = this;
                                        }
                                        else
                                        {
                                                int rd = w.depth;
                                                if (rd < dc.depthStart) dc.depthStart = rd;
                                                if (rd > dc.depthEnd) dc.depthEnd = rd;
                                        }

                                        w.drawCall = dc;

                                        if (generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);
                                        else w.WriteToBuffers(mVerts, mUvs, mCols, null, null);
                                }
                        }
                        else w.drawCall = null;
                }
                if (mVerts.size != 0) SubmitDrawCall(dc);
        }

算法描写叙述例如以下

先把UIPanel中的Widget按depth从小到大排序。假设depth同样那依照material的ID来排序。然后遍历每一个元素。把material同样的Widget归类到同一个drawCall。合并之后的结果例如以下图

技术分享

最后生成了3个DrawCall,并按顺序提交GPU绘制。

为何要採用这个算法呢?由于NGUI的Material是透明材质,不会写入深度缓存(可是会进行深度測试,以保证与非透明物体的层次正确),我们能够看NGUI材质所使用的Unlit/Transparent Colored这个Shader,里面有一句ZWrite Off。所以widget的前后关系与z坐标是没有关系的。而是与DrawCall的绘制顺序有关。所以假设要依照上图的depth来显示widget,必定仅仅能分成3个DrawCall。而且按顺序绘制。


參考文章:http://bbs.9ria.com/thread-282804-1-1.html


【Unity3D游戏开发】NGUI之DrawCall数量 (四)