1 渲染流程
NGUI的渲染流程其實就是把Widget組件生成Mesh所需要的緩存數據,然后生成對應的DrallCall組合對應數據,生成渲染需要的Mesh數據,提交渲染。
Widget(數據)
UIGeometry被UIWidget實例化之后,通過UIWidget的子類,也就是UISprit,UILabel等,在OnFill()函數里算出所需的Geometry緩存(頂點數,UV,Color,法線,切線), 參考Sprite的Fill函數為例,根據Widget的繪制區域、顏色、UV等信息生產頂點、uv、顏色信息,必要的時候其實還會生成法線信息。其他組件類似,但是計算方式差異比較大。
void SimpleFill (BetterList<Vector3> verts, BetterList<Vector2> uvs, BetterList<Color> cols)
{
Vector4 v = drawingDimensions;
Vector4 u = drawingUVs;
Color gc = drawingColor;
Color lc = gc.GammaToLinearSpace();
verts.Add(new Vector3(v.x, v.y));
verts.Add(new Vector3(v.x, v.w));
verts.Add(new Vector3(v.z, v.w));
verts.Add(new Vector3(v.z, v.y));
uvs.Add(new Vector2(u.x, u.y));
uvs.Add(new Vector2(u.x, u.w));
uvs.Add(new Vector2(u.z, u.w));
uvs.Add(new Vector2(u.z, u.y));
if (!mApplyGradient)
{
cols.Add(lc);
cols.Add(lc);
cols.Add(lc);
cols.Add(lc);
}
else
{
AddVertexColours(cols, ref gc, 1, 1);
AddVertexColours(cols, ref gc, 1, 2);
AddVertexColours(cols, ref gc, 2, 2);
AddVertexColours(cols, ref gc, 2, 1);
}
}
UIPanel(管理)
Panel相當於一個容器管理單元,負責下面Widget組件列表和DrallCall組件列表的維護和協調工作,其主要工作,監視Widget的修改、如果修改則修改DrawCall的數據,必要的時候會重建全部DrallCall。對於每個DrallCall而言,則根據widget填充頂點、uv、顏色等數據到自己緩沖中
對於所有Widget組件,UIPanel通過遍歷自己子類下所有的UIWidget組件(已經按深度排序),先創建一個UIDrawCall,然后把該Widget的material,texture,shader對象以及Geometry的緩存傳給UIDrawCall,如此反復循環搜索該UIPanel下的每一個Widget,只要是material,texture,shader都和上一個Widget一樣的Widget,他們的緩存都傳給同一個UIDrawCall,直到循環結束或者碰到一個材質球,貼圖,shader對象任一不相同的Widget。當遇到這種Widget,循環會再創建一個新的UIDrawCall,然后傳遞material,texture,shader,緩存,如此這般,直到循環完全結束。具體可以參考FillAllDrawCalls()函數
void FillAllDrawCalls ()
{
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();
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;
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;
}
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;
}
}
UIDrallCall
創建
對於每一個
更新
DrawCall其實就是在必要的時候根據組件數據生成對應的Mesh以及渲染相關的組件,其核心在UpdataGeometry()函數, 在該函數中做了一些幾個事情:
- 創建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();
DrawCall在什么時候更新呢?
UIDrawCall.UpdateGeometry()函數僅有在Panel.FillDrawCall()和Panel.FillAllDrawCalls ()被調用因為每個DrawCall之對應一個Mesh,如果該DrawCall所屬的Widget有改動,那么這個DrawCall就要通過UpdateGeometry修改新傳入的緩存重繪才能更新效果。
UIPanel.FillAllDrawCalls()調用的話基本是整個Panel重繪了,還好調用條件比較苛刻,除了第一次LateUpdate,之后若有新的Widget加入進來,並且深度不在之前DrallCall的范圍內,或者用了新的matiral shader texture那么就會影響之前已經布好的UI秩序,就會被重繪,調用的時候性能會損失很大。說簡單點,就是當有可能需要生成新的UIDrawCall或者剔除UIDrawCall的時候,就會觸發這個函數,這個機制,和之前遍歷Widget來生成DrawCall的原理以及目的都是一樣的。
UIPanel.FillDrawCall(UIDrawCall dc) 填充單獨的DrawCall.一般只有少量的widget更新的時候 沒必要更新所有的DrawCall(比如Label上的text有變化),只更新對應widget的DrawCall就好了.FillDrawCall()唯一的執行條件就是該DrawCall的isDirty為true,isDirty被切換為true的條件有三大類:1.widget上的視覺組件被更新,調用widget.MarkAsChanged();2.widget的忽然被添加刪除和移動;3.Panel的ALPHA被改動;
引用
NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)
以下討論基於NGUI3.8版本,后續版本代碼可能進行了修改