unity之NGUI 渲染原理

NGUI 渲染原理

NGUI内部架构

NGUI的核心架构,其实就在于3块:UIPanel、UIWidget、UIDrawcall。
其中: Panel和Widget是会显示在Hierarchy层级中的,而UIDrawcall则不会。

Panel和Widget

Widget是NGUI中负责界面显示的基础单位。

  • 所有需要在屏幕上显示出来的2D UI本质上都是一个Widget——包括Label、Sprite、Texture。
  • Widget作为一个整体,内部包含了渲染所需要的所有原材料:mesh面片,贴图,UV,材质,位置信息等等。

Panel,就是一个用来管理Widget的控件。

  • 每一个Widget都必定从属于一个Panel。不可例外——NGUI的根节点Root本身也包含一个Panel
  • 不可能在 Root之外创建Widget
  • 需要注意的是,Hierarchy中的UI层级关系,与NGUI自己内部的层级关系,并不一致。
    在这里插入图片描述
    比如上图这个结构。Panel_D是Widget_2的儿子,是Panel_A的孙子。

但是对于NGUI内部来说:

  • Panel_A和Panel_D是平级的
  • Widget_2和Panel_D并没有父子关系。

在NGUI的架构中,首先存在一个静态的static List,这个List包含所有的Panel,
而每个Panel有两个属于自己的List:一个List,一个List< UIDrawcall >,
每个Panel都会在自己的子节点中向下寻找,把找到的Widget丢进自己的List中

这个行为在每一次走到叶节点,或者遇到Panel的时候就会中断当前分支,跳到下一个分支。

比如说上图中Panel_A在找孩子的时候,走到Panel_D的时候就会中断当前分支,然后继续到Widget_3中去寻找。如此循环直到所有可获取的Widget都被装进List。
而对于Panel_D,也是如此。

其结构如下所示:
在这里插入图片描述

static List内部包含一个Sort排序方法,会基于Panel的Depth进行一次排序
而Panel内部也包含一个Sort排序方法,会基于Widget的Depth进行一次排序
所以,虽然Panel和Widget都有Depth这个参数,但是这两个参数的地位是不一样的
只要一个Panel的depth比另一个Panel小,这个Panel内部的所有Widget都会被先处理。
深度Depth越小,越先处理。

Drawcall

一次Drawcall,就是CPU调用GPU干一次活。

Drawcall是衡量渲染负担的一个重要指标。而且通常情况下,主要就是衡量CPU的负担。

GPU高度定制优化。若一次DrawCall能够做尽量多的指令。那么效率会越高。
优化Drawcall主要就是尽可能的合并指令,让一个指令包含尽可能多的内容,以此降低CPU的负担。
当然,有时候我们也会先让目标合体,比如说砍一个合体葫芦娃,比起砍7个小葫芦娃,CPU和GPU双方的工作量都可以减少。

总结:
所谓Drawcall,就是CPU加载并整理好美术资源(包括模型、贴图、材质等)及相关指令后,调用GPU进行工作的过程。

NGUI的DrawCall处理

当一个Panel的List排序完成后,Panel就会根据List来生成List,
List中的第一个Widget必定会创建一个新的Drawcall,之后的每一个Widget都会拿出来和前一个进行对比,如果两者的material、texture、shader(下简称M/T/S)一致,则把后面这个Widget也丢给同一个Drawcall处理,如果两者的M/T/S有一丢丢不一样的地方,就创建一个新的Drawcall。
也就是说:

  • 相同M/T/S且下标连续的Widget会共用一个Drawcall,
  • 而如果相同M/T/S的Widget中间隔着一个或多个不同M/T/S的Widget,就会拆分出许多额外的Drawcall。

如图,在Widget相同但深度排序不同的情况下,各自所产生的List。
在这里插入图片描述
注:各个Panel的List最终会合并汇总一个静态的ActiveDrawcallList,最终供渲染时调用。

通常情况下,做好depth深度管理,尽可能的减少Drawcall就可以有效提高渲染效率。

对于CPU来说,调用Drawcall需要耗费时间,而构建Drawcall同样需要耗费时间。

NGUI在运行的过程中,如果某一个Panel下面有任意一个Widget进行了一点非常微小的变动:比如移动了一点点距离。那么这个Panel就会清空自己的List,从头再遍历所有Widget,重新构建所有Drawcall。而这个过程显然是非常耗费性能的。
所以,有时候可以根据Widget的用途,将动态Widget和静态Widget拆分到不同Panel,如此一来虽然增加了Drawcall的数量,但最终结果却反而能提升渲染效率。

渲染序列

NGUI里的每一个Panel都有一个 Render Q 的设置选项。
Render Q主要有两种模式:

  • Automatic : 自动模式
  • ·StartAt` : 需要设置一个整数参数。

大多数时候我们不需要动这个东西。如果全部都默认的Automatic的话,NGUI会自己帮我们按照Panel的depth为第一优先级,Widget的depth为第二优先级来处理好渲染序列。

不过有时候我们难免会遇到想人为调整RenderQ的情况——比如说想在两个Panel之间插入特效之类的。这个时候可能就需要去设置RenderQ的type和数值。

  • 对于StartAt模式的Panel,会以用户配置的参数作为第一个Drawcall的渲染序列,之后每个Drawcall递增1。
  • 而对于Automatic的Panel,会以Max ( 3000 , ActiveList中的最大RenderQ+1)作为第一个Drawcall的渲染序列,之后每个Drawcall递增1。比如说,如果当前最大序列是2500,那么处理到当前Panel的时候,渲染序列还是会从3000开始,每个Drawcall递增1;但是如果当前最大序列是3500,那么处理到当前Panel的时候,就会从3501开始,每个Drawcall递增1。

而如果用户想要在两个Panel之间插入想要渲染的东西,只需要把要插入的东西的渲染序列设置为渲染序列较高的那个Panel的StartAt值-1就可以了。