本文基于NGUI3.9.6,目前我们游戏用的NGUI版本。主要介绍一下NGUI渲染框架,基于本人阅读NGUI源码及其相关资料得出,如有误欢迎指正。
1、渲染基础组件
- UIGeometry是用于保存UIWidget需要用的几何数据,里面主要有verts、uvs、cols
- UIWidget是NGUI基础组件(UILabel、UITexture和UISprite)的基类。主要包含了组件的一些基本信息(宽高、锚点和深度等)。
- UIDrawCall 用于负责绘制的类,负责把UIWidget和UIGeometry里面的数据绘制出来
- UIPanel 用来管理多个UIWidget、UIDrawcall的渲染、裁剪和更新等。
2、各个组件之间的关系
3、渲染流程
这里先借用网上一张渲染流程图。借用地址链接:https://www.cnblogs.com/Persona/p/4149086.html
UIGeometry
UIGeometry内提供了verts(顶点)、uvs(UV坐标)、cols(颜色)和相对于panel的mRtpVerts(顶点)、mRtpNormal(法线)、mRtpTan(切线)的数据变量。对外提供了三个方法Clear(清理数据)、ApplyTransform、WriteToBuffers(写缓存)。
主要方法:
//用来计算把widget数据通过矩阵转化到panel数据
public void ApplyTransform (Matrix4x4 widgetToPanel, bool generateNormals = true)
{
if (verts.size > 0)
{
mRtpVerts.Clear();
for (int i = 0, imax = verts.size; i < imax; ++i) mRtpVerts.Add(widgetToPanel.MultiplyPoint3x4(verts[i]));
// Calculate the widget's normal and tangent
if (generateNormals)
{
mRtpNormal = widgetToPanel.MultiplyVector(Vector3.back).normalized;
Vector3 tangent = widgetToPanel.MultiplyVector(Vector3.right).normalized;
mRtpTan = new Vector4(tangent.x, tangent.y, tangent.z, -1f);
}
}
else mRtpVerts.Clear();
}
UIWidget
UIWidget作为NGUI基础组件提供一些基本属性操作。这里主要还是讨论他和UIGeometry和UIPanel之间的关系。
UIWidget中定了两个变量。Panel用来管理widget,geometry是widget的数据。
/// <summary>
/// Panel that's managing this widget.
/// </summary>
[System.NonSerialized] public UIPanel panel;
/// <summary>
/// Widget's generated geometry.
/// </summary>
[System.NonSerialized] public UIGeometry geometry = new UIGeometry();
geometry相当于widget在整个渲染过程中的数据容器。实例化geometry的时候,通过UpdateGeometry方法来填充计算Geometry里面的数据。通过调用OnFill虚方法,最后在UILabel、UITexture、UISprite里面来填充具体对应的数据。
public bool UpdateGeometry (int frame)
{
//省略部分代码
if (mChanged)
{
mChanged = false;
if (mIsVisibleByAlpha && finalAlpha > 0.001f && shader != null)
{
bool hadVertices = geometry.hasVertices;
if (fillGeometry)
{
geometry.Clear();
OnFill(geometry.verts, geometry.uvs, geometry.cols);
}
//省略部分代码
}
//省略部分代码
}
//省略部分代码
panel和widget的关系是一个引用的关系。widget保留了对其管理的panel的引用。是start的时候会去创建panel的引用。
/// <summary>
/// Ensure we have a panel referencing this widget.
/// </summary>
public UIPanel CreatePanel ()
{
if (mStarted && panel == null && enabled && NGUITools.GetActive(gameObject))
{
panel = UIPanel.Find(cachedTransform, true, cachedGameObject.layer);
if (panel != null)
{
mParentFound = false;
panel.AddWidget(this);
CheckLayer();
Invalidate(true);
}
}
return panel;
}
UIDrawCall
UIDrawCall和UIWidget的关系相当于有一堆UIWidget的数据需要被绘制,通过UIDrawCall组装起来生成Material用来渲染。UIDrawCall的Material的创建和更新在UpdateGeometry中。这个方法只有在UIPanel的FillAllDrawCalls和FillDrawCall中调用。每个Drawcall对应一个mesh,如果该DrawCall里面的包含的UIWidget有变动。那么就会调用UpdateGeometry更新数据才能重新更新效果。所以在开发过程中,应该避免UIWidget的变动,比如SetActive等。
UIDrawCall重要函数UpdateGeometry
/// Set the draw call's geometry.
public void UpdateGeometry (int widgetCount)
{
//省略部分代码
if (verts.size < 65000)
{
//省略部分代码
// Create the mesh 创建mesh
if (mMesh == null)
{
mMesh = new Mesh();
mMesh.hideFlags = HideFlags.DontSave;
mMesh.name = (mMaterial != null) ? "[NGUI] " + mMaterial.name : "[NGUI] Mesh";
mMesh.MarkDynamic();
setIndices = true;
}
//省略部分代码
//填充绘制需要的数据
mMesh.vertices = verts.buffer;
mMesh.uv = uvs.buffer;
mMesh.colors32 = cols.buffer;
if (norms != null) mMesh.normals = norms.buffer;
if (tans != null) mMesh.tangents = tans.buffer;
if (setIndices)
{
mIndices = GenerateCachedIndexBuffer(count, indexCount);
mMesh.triangles = mIndices;
}
//省略部分代码
//更新材质
UpdateMaterials();
}
}
UIPanel
UIPanel在NGUI中充当了一个管理者的角色。管理这UIDrawCall和UIWidget的渲染、裁剪和更新时机。UIPanel中主要的函数LateUpdate、FillAllDrawCalls和FillDrawCall。在Unity的Profile中经常能看到UIPanel.LateUpdate这个函数的CPU和GC都是占用很高的。
LateUpdate
LateUpdate中更新了所有panel和drawcall。在LateUpdate中会遍历所有Panel,调用Panel中的UpdateDrawcall方法。更新所有DrawCall。LateUpdate中确保了每帧执行一次。
void LateUpdate ()
{
//这里确保每帧执行一次,这个对CPU和GPU消耗很大,需要控制执行次数
if (mUpdateFrame != Time.frameCount)
{
mUpdateFrame = Time.frameCount;
// Update each panel in order
for (int i = 0, imax = list.Count; i < imax; ++i)
list[i].UpdateSelf();
int rq = 3000;
// Update all draw calls, making them draw in the right order
for (int i = 0, imax = list.Count; i < imax; ++i)
{
UIPanel p = list[i];
if (p.renderQueue == RenderQueue.Automatic)
{
p.startingRenderQueue = rq;
p.UpdateDrawCalls();
//这里默认在Panel的RenderQueue之间拉开3个差值
rq += p.drawCalls.Count * 3;
}
else if (p.renderQueue == RenderQueue.StartAt)
{
p.UpdateDrawCalls();
if (p.drawCalls.Count != 0)
rq = Mathf.Max(rq, p.startingRenderQueue + p.drawCalls.Count);
}
else // Explicit
{
p.UpdateDrawCalls();
if (p.drawCalls.Count != 0)
rq = Mathf.Max(rq, p.startingRenderQueue + 1);
}
}
}
}
在阅读LateUpdate主要关注调用的UpdateSelf和UpdateDrawCalls方法。在UpdateSelf中主要更新了Transform、Layer、Widgets和根据需求重建Drawcall。UpdateDrawCalls更新裁剪区域和drawcall层级等信息。
FillAllDrawCalls和FillDrawCall
FillAllDrawCalls是处理所有Widget和重建所有drawcall。FillDrawCalls针对某一个特殊的Drawcall更新Geometry。这里主要介绍FillAllDrawCalls。
/// <summary>
/// Fill the geometry fully, processing all widgets and re-creating all draw calls.
/// </summary>
void FillAllDrawCalls ()
{
//清理所有Drawcall
for (int i = 0; i < drawCalls.Count; ++i)
UIDrawCall.Destroy(drawCalls[i]);
drawCalls.Clear();
//省略部分代码
//重建Widget
if (mSortWidgets) SortWidgets();
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;
//如果当前用的材质、纹理和shader和上一个widget的不同的话增加一个Drawcall
if (mat != mt || tex != tx || sdr != sd)
{
if (dc != null && dc.verts.size != 0)
{
drawCalls.Add(dc);
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
count = 0;
dc = null;
}
mat = mt;
tex = tx;
sdr = sd;
}
//drawcall为空的话,新建一个drawcall
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;
// 写入缓存
++count;
if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans);
else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null);
if (w.mOnRender != null)
{
if (mOnRender == null) mOnRender = w.mOnRender;
else mOnRender += w.mOnRender;
}
}
}
else w.drawCall = null;
}
if (dc != null && dc.verts.size != 0)
{
drawCalls.Add(dc);
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
}
}
FillAllDrawCalls中住的流程如下:
{% mermaid graph TD %}
A[清理所有DrawCall...] --> B[对Widget重新排序...]
B --> C[重新更新Geomtry...]
C --> D[写缓存...]
{% endmermaid %}
上图可以看到,在重建DrawCall的时候会重新对Widget排序,还会重新更新计算Geometry。所以这个调用是很耗时的。而FillAllDrawCalls在UIPanel中的UpdateSelf里面当mRebuild为true的时候才会重建所有Drawcall。而mRebuild的在UIPanel中在OnInit、OnEnable、RemoveWidget、Refresh中会调用。所以在我们制作UI界面的时候应该尽量避免触发这几个函数以免对Drawcall重建。造成大的消耗。
所以通过对上面的分析可以看得出,影响Drawcall的因素主要有下面几个。在实际开发中可以规划好widget,针对下面的因素来优化drawcall。
- 不同panel中的Widget无法合并drawcall
- 同一个panel中会根据panel下的widget中的depth排序,连续的widget使用的相同材质相同纹理以及shader的widget可以合并drawcall
- widget的add、remove和refresh都会影响drawcall重建。