NGUI所見即所得之UIWidget , UIGeometry & UIDrawCall
UIWidget是所有UI組件的抽象基類,作爲基類當然定義了必須的成員變量和函數,接觸過MFC或其他UI組件開發,想必都知道有一堆參數設置,尤其是Visual Studio的可視化界面,簡直太豐富了,UIWidget要當UI組件的爹就必須得具備這些,下面就一一介紹:
Pivot
Pivot,這個枚舉,其實定義了GameObject中心座標在整個組件的位置,這個跟UIStretch很類似,只不過UIStretch說的是組件相對於屏幕的位置。
- public enum Pivot
- {
- TopLeft,
- Top,
- TopRight,
- Left,
- Center,
- Right,
- BottomLeft,
- Bottom,
- BottomRight,
- }
Pivot可以提供開發者更多的定位模式,可以方便實現組件對齊,如對個UILabel組件的文本居中對齊。當然這樣,在計算組件的四個角的頂點座標(localCorners)就得考慮Pivot。
localCorners,worldCorners & innerWorldCorners
這三個變量都是四個角的頂點座標,只是localCorners是計算局部的(相對gameObject的中心而言)座標,worldCorners只是將localCorners作爲世界座標空間的座標,innerWorlCorners則考慮了邊框Border。
- public virtual Vector3[] localCorners
- {
- get
- {
- Vector2 offset = pivotOffset;
- float x0 = -offset.x * mWidth;
- float y0 = -offset.y * mHeight;
- float x1 = x0 + mWidth;
- float y1 = y0 + mHeight;
- mCorners[0] = new Vector3(x0, y0, 0f);
- mCorners[1] = new Vector3(x0, y1, 0f);
- mCorners[2] = new Vector3(x1, y1, 0f);
- mCorners[3] = new Vector3(x1, y0, 0f);
- return mCorners;
- }
- }
上面代碼中的pivotOffset的計算就考慮了Pivot:
- static public Vector2 GetPivotOffset (UIWidget.Pivot pv)
- {
- Vector2 v = Vector2.zero;
- if (pv == UIWidget.Pivot.Top || pv == UIWidget.Pivot.Center || pv == UIWidget.Pivot.Bottom) v.x = 0.5f;
- else if (pv == UIWidget.Pivot.TopRight || pv == UIWidget.Pivot.Right || pv == UIWidget.Pivot.BottomRight) v.x = 1f;
- else v.x = 0f;
- if (pv == UIWidget.Pivot.Left || pv == UIWidget.Pivot.Center || pv == UIWidget.Pivot.Right) v.y = 0.5f;
- else if (pv == UIWidget.Pivot.TopLeft || pv == UIWidget.Pivot.Top || pv == UIWidget.Pivot.TopRight) v.y = 1f;
- else v.y = 0f;
- return v;
- }
發現pivotOffset是一個由0,0.5,1組成的二維向量,所以前面計算localCorners的原理就可想而知了。
當然還有諸如width(minWidth),height(minHeight),depth(raycastDepth),alpha(finalAlpha),看了代碼就自然一目瞭然了。rayCastDepth和finalAlpha都考慮了UIPanel的因素,所以有時表層看着沒問題,可能就要去看下底層的實現。
兩大基石:UIDrawCall和UIGeometry
根據我有限的3D知識(其實沒有學過),隱約覺得3D呈現出來的東西都是由頂點(vertice)構成的Mesh通過紋理貼圖構成的材質渲染出來的。
但是NGUI所有組件都沒有看到Mesh Render,只有transform信息和腳本,查看UIWidget:
- /// <summary>
- /// Internal usage -- draw call that's drawing the widget.
- /// </summary>
- public UIDrawCall drawCall { get; set; }
- // Widget's generated geometry
- UIGeometry mGeom = new UIGeometry();
這就是UIDrawCall和UIGeometry神一般的存在。
UIGeometry
UIGeometry其實是UI組件的數據倉庫,存儲UI組件的Vertices,UVs和Colors,以及相對UIPanel的頂點Vertices,法向量和切線:
- /// <summary>
- /// Widget's vertices (before they get transformed).
- /// </summary>
- public BetterList<Vector3> verts = new BetterList<Vector3>();
- /// <summary>
- /// Widget's texture coordinates for the geometry's vertices.
- /// </summary>
- public BetterList<Vector2> uvs = new BetterList<Vector2>();
- /// <summary>
- /// Array of colors for the geometry's vertices.
- /// </summary>
- public BetterList<Color32> cols = new BetterList<Color32>();
- // Relative-to-panel vertices, normal, and tangent
- BetterList<Vector3> mRtpVerts = new BetterList<Vector3>();
- Vector3 mRtpNormal;
- Vector4 mRtpTan;
UIGeometry爲此提供了三個函數,也可以說是爲渲染繪製做的三個步驟:
Step 1: Clear() 情況存儲的信息
Step 2: ApplyTransform() 將verts轉化爲相對UIPanel的頂點座標mRtpVerts
Step 3: WriteToBuffers() 將數據倉庫緩存的數據添加到UIPanel的緩存數據隊列中去。
UIWidget分別用UpdateGeometry和WriteToBuffers對UIGeometry的ApplyTransform和WriteToBuffers進行封裝調用,ApplyTransform是根據UIPanel的座標調整Vertices的座標,WriteToBuffers將UIGeometry的Vertices,UVs和Colors添加進UIPanel的Vertices,UVs和Colors的BetterList中。
這裏還有一個細節就是:UIGeometry中的Vertices,UVs和Colors的BetterList的buffer什麼時候得到,因爲這些都是UIWidget或其子類的信息,所以在UIWidget的子類UILabel,UISprite和UITexture中OnFill函數生成UIGeometry的BetterList的buffer。
UIDrawCall
如果是UIGeometry爲了渲染繪製準備數據,那麼UIDrawCall其實是定義了渲染繪製需要的基本組件。這裏拿煮菜做個比喻幫助理解:UIGeometry好比爲煮菜準備食材,UIDrawCall好比是煮菜的工具(鍋,爐子等),UIPanel就是大廚了決定着什麼時候該煮菜,UIWidget(UILabel,UISprite和UITexture)是這道菜怎麼樣的最終呈現。會不會很好理解呢?
下面就直接看下Set函數:
- if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();
- if (mRen == null) mRen = gameObject.GetComponent<MeshRenderer>();
在Set函數中就給gameObjdect添加了MeshFilter和MeshRenderer組件,當然還做了其他輔助的工作。
此外,在UpdateMaterials驚奇的發現了UIPanel clipView的AlphaClip和SoftClip的實現,其實就是更換了Material的Shader,這也體現了Shader強大,不得不學呀,DSQiu在後面也會對Unity的Shader編程做一個整理。
回放細節,回籠鍛造
看了上面就會覺得寫得的確是所見即所得——D.S.Qiu好像都沒有看到什麼要害,都說學武功就要學習上次心法,一點皮毛總是會惹來恥笑。
還是回到UIWidget這個腳本中的兩個函數:WriteToBuffers,OnFill,UpdateGeometry。
WriteToBuffers和OnFill這兩個函數都是將Vertices,UVs和Colors等add進參數的List中去,查看WriteToBuffers的調用出發現其參數是UIPanel的Vertices,UVs和Colors,而OnFill的參數是UIGeometry的Vertices,UVs和Colors。
WriteToBuffers只是對UIGeometry的封裝調用,也就是說將UIGeometry的Vertices,UVs和Colors等信息add進UIPanel的對應List中。
在看UIGeometry的腳本,一直有一個疑問:UIGeometry的Vertices,UVs和Colors的List沒有看到add方法的執行,只有在WriteToBuffers被add。直到看到了OnFill才恍然大悟,雖然UIWidget的OnFill是虛函數,沒有具體實現,看了下UISprite重寫的OnFill函數,就是把Vertices,UVs和Colors 添加到UIGeometry的Vertices,UVs和Colors中。
轉了一大圈,發現最後所有UI組件的Vertices,UVs和Colors都彙集到UIPanel的Vertices,UVs和Colors去了,然後UIPanel指定給UIDrawCall渲染就行了,具體細節還要等攻克UIPanel就明白了。
其他細節
到這裏差不多把本文的內容都掏乾淨了,剩下的就是看下UIWidget的一些實現細節,MakePixelPerfect():對gameObject的localPosition和locaScale進行微調和糾正。
SetDirty():調用UIPanel的SetDirty()對組件的變更進行重建(rebuilt)。
CreatPanel():這個函數可以得知,所有UI組件一定是放在UIPanel的子節點上的,從前面的分析也可以知道:因爲是UIPanel對統一對組件進行渲染繪製的,所以如果沒有UIPanel有點“皮之不存,毛將附焉”的意思。
這也越發激發D.S.Qiu 去啃UIPanel這個主心骨。
小結:
總算對UIWidget,UIGeomerty 和UIDrawCall有了一個清晰的認識,這三個組件可以說是NGUI的背後無名英雄(平時使用都不會接觸者三個腳本),功能很強大但很簡單,真的很佩服NGUI的設計思路。
但我也有點小微詞:UIWidget都被定義成長方形的,要是能提供自定義UI的接口(如我想畫一個圓形UI),那將會更強大;底層緩存了很多數據,這個是內存和開發者的代碼邏輯也是要考慮到的。
最後附上①畫的NGUI框架圖:
如果您對D.S.Qiu有任何建議或意見可以在文章後面評論,或者發郵件([email protected])交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在文首註明出處:http://dsqiu.iteye.com/blog/1965340
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
參考:
①dujimache:http://www.unitymanual.com/forum.php?mod=viewthread&tid=5579&highlight=NGUI%E6%A1%86%E6%9E%B6