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