源码分析NGUI的DrawCall合并原理
NGUI为了减少GPU状态切换的消耗(例如切换材质),会将使用相同材质(material)的widget进行合并,从而减少DrawCall的数量。本文将详细描述NGUI如何对widget进行归类,以及在减少DrawCall时需要注意的要点。
归类widget的代码分析
归类widget的代码位于UIPanel中的FillAllDrawCalls()方法,以下是该方法的详细代码及解释:
void FillAllDrawCalls ()
{
// 销毁所有已有的DrawCall
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;
// 如果需要对widget进行排序,则调用SortWidgets方法
if (mSortWidgets) SortWidgets();
// 遍历所有widget
for (int i = 0; i < widgets.size; ++i)
{
UIWidget w = widgets.buffer[i];
// 检查widget是否可见且有顶点数据
if (w.isVisible && w.hasVertices)
{
Material mt = w.material;
Texture tx = w.mainTexture;
Shader sd = w.shader;
// 如果材质、纹理或着色器发生变化
if (mat != mt || tex != tx || sdr != sd)
{
// 如果有顶点数据,则提交当前的DrawCall
if (mVerts.size != 0)
{
SubmitDrawCall(dc);
dc = null;
}
// 更新材质、纹理和着色器
mat = mt;
tex = tx;
sdr = sd;
}
// 如果材质、纹理或着色器不为空
if (mat != null || sdr != null || tex != null)
{
// 如果当前DrawCall为空,则创建一个新的DrawCall
if (dc == null)
{
dc = UIDrawCall.Create(this, mat, tex, sdr);
dc.depthStart = w.depth;
dc.depthEnd = dc.depthStart;
dc.panel = this;
}
else
{
// 更新DrawCall的深度范围
int rd = w.depth;
if (rd < dc.depthStart) dc.depthStart = rd;
if (rd > dc.depthEnd) dc.depthEnd = rd;
}
// 将widget与当前DrawCall关联
w.drawCall = dc;
// 将widget的顶点数据写入缓冲区
if (generateNormals)
{
w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);
}
else
{
w.WriteToBuffers(mVerts, mUvs, mCols, null, null);
}
}
}
else
{
// 如果widget不可见或没有顶点数据,则将其DrawCall置为null
w.drawCall = null;
}
}
// 如果还有剩余的顶点数据,则提交最后一个DrawCall
if (mVerts.size != 0) SubmitDrawCall(dc);
}
算法描述
排序操作
首先,UIPanel中的Widget会按照depth从小到大进行排序。如果depth相同,则按照material的ID进行排序。
合并操作
接着,遍历每个Widget元素,将使用相同material的Widget归类到同一个DrawCall中。合并后的结果会生成若干个DrawCall,并按顺序提交给GPU进行绘制。
示例结果
经过合并后,最终生成了3个DrawCall,并按顺序提交GPU绘制。
采用该算法的原因
NGUI的Material通常为透明材质,这类材质不会写入深度缓存(但会进行深度测试,以确保与非透明物体的层次关系正确)。以NGUI材质所使用的Unlit/Transparent Colored这个Shader为例,其中有一句ZWrite Off。这意味着widget的前后关系与z坐标无关,而是与DrawCall的绘制顺序有关。因此,如果要按照depth来显示widget,就必然需要将其分成多个DrawCall,并按顺序进行绘制。
综上所述,通过对相同材质的widget进行合并,NGUI能够有效减少DrawCall的数量,从而降低GPU状态切换的消耗,提高渲染性能。在实际开发中,我们可以根据这些原理,合理安排widget的材质和深度,以进一步优化渲染效果。