文章摘要
UIDrawCall是NGUI中封装Unity底层DrawCall的核心组件,每个实例对应一次Mesh渲染调用。其流程包括:1)由UIWidget属性变更或UIPanel脏标记触发;2)UIPanel按材质/贴图/Panel/Depth分组分配UIDrawCall对象;3)收集所属Widget的顶点数据并填充至Mesh;4)提交Unity渲染。通过对象池复用DrawCall减少GC,优化性能。关键问题包括DrawCall过多(合批失败)、Mesh数据异常或频繁创建销毁,需检查分组条件或UI动态变更。核心源码涉及UIDrawCall.cs、UIPanel.cs和UIWidget.cs。
1. UIDrawCall的作用
UIDrawCall是NGUI中对Unity底层DrawCall的封装。每个UIDrawCall对象对应一次Unity的Mesh渲染调用,负责收集一组可合批的UIWidget的顶点数据,生成Mesh并提交给Unity渲染。
2. UIDrawCall全流程详解
2.1 触发时机
- UIWidget属性变更(如文本、图片、颜色、位置等)→
MarkAsChanged - UIPanel被标记为脏 → 下一帧
LateUpdate时刷新
2.2 DrawCall分组与分配
入口:UIPanel.UpdateDrawCalls()
- 遍历所有Widget,按材质/贴图/Panel/Depth分组
- 为每组分配一个UIDrawCall对象
- 复用池中已有的UIDrawCall
- 不够用则新建
- Widget的drawCall字段指向所属UIDrawCall
关键源码片段(UIPanel.cs):
for (int i = 0; i < mWidgets.size; ++i)
{
UIWidget w = mWidgets.buffer[i];
if (!w.isVisible) continue;
Material mat = w.material;
Texture tex = w.mainTexture;
if (drawCall == null || mat != lastMat || tex != lastTex)
{
drawCall = UIDrawCall.Create(this, mat, tex, ...);
mDrawCalls.Add(drawCall);
lastMat = mat;
lastTex = tex;
}
w.drawCall = drawCall;
}
2.3 顶点数据收集与填充
入口:UIPanel.FillDrawCall(UIDrawCall dc)
- 清空临时顶点缓存(verts、uvs、cols等)
- 遍历所有属于该DrawCall的Widget
- 调用
w.WriteToBuffers()将自己的顶点、UV、颜色等数据写入缓存
- 调用
- 调用
dc.Set()将数据传递给UIDrawCall
关键源码片段:
public void FillDrawCall(UIDrawCall dc)
{
mVerts.Clear();
mUVs.Clear();
mCols.Clear();
for (int i = 0; i < mWidgets.size; ++i)
{
UIWidget w = mWidgets.buffer[i];
if (w.isVisible && w.drawCall == dc)
{
w.WriteToBuffers(mVerts, mUVs, mCols);
}
}
dc.Set(mVerts, mUVs, mCols);
}
2.4 Mesh生成与提交
入口:UIDrawCall.Set()
- 将收集到的顶点、UV、颜色等数据写入Mesh
- 设置Mesh的三角面(indices)
- 将Mesh赋值给MeshFilter/MeshRenderer,准备渲染
关键源码片段(UIDrawCall.cs):
public void Set(List<Vector3> verts, List<Vector2> uvs, List<Color32> cols)
{
mesh.Clear();
mesh.SetVertices(verts);
mesh.SetUVs(0, uvs);
mesh.SetColors(cols);
// 计算并设置三角面索引
mesh.SetTriangles(indices, 0);
mesh.RecalculateBounds();
}
2.5 渲染
- Unity的渲染管线会自动根据MeshRenderer和材质渲染UIDrawCall生成的Mesh。
- 每个UIDrawCall对应一次Unity DrawCall。
2.6 回收与复用
- UIPanel会维护一个DrawCall池,每帧只保留需要的DrawCall,多余的会被隐藏并缓存,下次复用。
- 避免频繁创建/销毁DrawCall对象,减少GC。
关键源码片段:
// 多余的DrawCall
if (mDrawCalls.Count > requiredCount)
{
for (int i = requiredCount; i < mDrawCalls.Count; ++i)
{
mDrawCalls[i].gameObject.SetActive(false);
mInactiveDrawCalls.Add(mDrawCalls[i]);
}
mDrawCalls.RemoveRange(requiredCount, mDrawCalls.Count - requiredCount);
}
3. UIDrawCall的生命周期流程图
UIWidget属性变更
↓
UIPanel被标记为脏
↓
UIPanel.LateUpdate()
↓
UpdateDrawCalls()
↓
分组分配UIDrawCall
↓
FillDrawCall(收集顶点数据)
↓
UIDrawCall.Set(生成Mesh)
↓
Unity渲染
↓
DrawCall回收/复用
4. 关键点总结
- UIDrawCall的创建/分配:由UIPanel统一管理,按材质/贴图/Panel/Depth分组。
- 数据填充:所有Widget的顶点数据合并到DrawCall的Mesh中。
- Mesh生成:UIDrawCall负责Mesh的创建和更新。
- 渲染:每个UIDrawCall就是一次Unity DrawCall。
- 回收复用:DrawCall对象池优化性能。
5. 典型问题与优化
- DrawCall数量过多:通常是分组条件不满足(见前一问“合批失败的典型原因分析”)。
- Mesh数据异常:Widget未正确写入顶点数据或Widget未被分配到DrawCall。
- DrawCall频繁创建/销毁:应检查是否有UI频繁增删,优化为复用。
6. 相关源码文件
UIDrawCall.cs:DrawCall对象本体,Mesh生成与渲染UIPanel.cs:DrawCall的分组、分配、回收、数据填充UIWidget.cs:Widget顶点数据的生成与写入
1675

被折叠的 条评论
为什么被折叠?



