基於DOTS的UI解決方案

自從在GDC 2019上,Unity分享了名為“連接DOTS:Unity面向數據技術棧”的技術演講,關於DOTS的討論和應用一直在業內備受關注。前段時間我們連載的“Unity手遊實戰”(https://blog.uwa4d.com/archives/USparkle_ECS3.html)系列中,也有對於DOTS的相關論述。

本文要給大家介紹的是supron在社區中與大家分享的高性能UI解決方案:Pure DOTS UI System[3]。

The current Unity UI solution is very powerful but struggles with performance (especially with many objects instantiation). DOTS seems like a great solution to this problem.

DOTS UI開源庫:https://lab.uwa4d.com/lab/5d3a18365b9dec79de05d348


一、功能

DOTS UI System可以將Unity的UI系統UGUI的組件轉化映射成Entity,利用ECS、JobSystem、Burst的性能優勢,明顯提升UI運行效率。除此之外,在網格重建部分還使用到了2019.3的新特性:Advanved Mesh API[5],可以直接寫入Mesh數據,運行效率更快。

目前DOTS UI的版本為0.3.0,功能還是非常不完善的。目前已支持的主要功能如下:

Canvas Render mode:

  • Screen space camera
  • Screen space overlay

Canvas Scaling mode:

  • Constant pixel size
  • Constant physical size

Controls:

  • Image
  • TextMeshProUGUI (SDF fonts, not all features are supported)
  • TMP_InputField (very simple implementation)
  • Selectable
  • Button
  • RectMask2D
  • CanvasScaler
  • ScrollRect

Input events:

  • Down
  • Up
  • Click
  • Enter
  • Exit
  • Selected
  • Deselected
  • BeginDrag
  • Drag
  • EndDrag
  • Drop
  • Button click
  • InputField OnEndEdit
  • InputField OnReturn

二、使用

在開源庫中下載資源後,將com.dotsui.core和com.dotsui.hybrid兩個資源包複製到項目工程(Unity 2019.3.0a8以上)的Packages路徑下進行導入,則Unity會自動導入其依賴的Entities、Jobs等Package。

打開一個UI預製體,在Canvas節點掛上ConvertToEntity[6]腳本,表示需要將這個UI轉換成Entity。默認選擇的Conversion Mode為Convert And Destroy,即替換為Entity之後會把原有的UGUI組件銷燬。

基於DOTS的UI解決方案

運行一下,即可看到運行時生效的效果。

基於DOTS的UI解決方案

由於測試的Prefab中的Text不是使用TextMeshPro做的,所以沒有成功轉換,但這並不影響其它組件的正常運行。

多做幾次測試之後,還會發現RectTransform的Rotation和Scale屬性沒有被正確顯示。這是因為作者在第一個版本中簡化了很多屬性,其類定義如下:

public struct RectTransform : IComponentData
{
public float2 AnchorMin;
public float2 AnchorMax;
public float2 Position;
public float2 SizeDelta;
public float2 Pivot;
}

作者還在工程中提供了Sample示例,展示了目前支持的幾種效果。

基於DOTS的UI解決方案

如果需要使用腳本控制UI變化,也要用ECS的方式來編寫。可以參考Sample中的簡單例子(如:FpsCounter、FPSSystem)進行改寫。


三、實現

1、Conversion

Conversion的部分相對比較簡單,核心在於將Canvas、Image等組件轉換為Enity。

以上文提到的RectTransfrom為例,其Convert函數的代碼如下:

private void Convert(RectTransform transform)
{
var entity = GetPrimaryEntity(transform);
DstEntityManager.AddComponentData(entity, new DotsUI.Core.RectTransform()
{
AnchorMin = transform.anchorMin,
AnchorMax = transform.anchorMax,
Pivot = transform.pivot,
Position = transform.anchoredPosition,
SizeDelta = transform.sizeDelta,
});
DstEntityManager.AddComponent(entity, typeof(WorldSpaceRect));
DstEntityManager.AddComponent(entity, typeof(WorldSpaceMask));
DstEntityManager.RemoveComponent(entity, typeof(Translation));
DstEntityManager.RemoveComponent(entity, typeof(Rotation));
DstEntityManager.RemoveComponent(entity, typeof(NonUniformScale));
}

這部分相關的主要相關代碼在Dots UI Core Package當中。

2、UI Mesh Batching

UI Mesh的合批處理是UI模塊非常重要的部分,在這部分DOTS UI將需要渲染的網格信息進行合批並存儲起來,為之後的渲染步驟做準備。在DOTS UI中這一步主要分成兩步完成。

(1)信息收集

首先定義一個NativeHashMap<entity>,用來記錄需要渲染的UI元素(Entity)和Material的對應關係。目前只包含Sprite和Text。/<entity>

public NativeHashMap<entity> EntityToMaterial;
/<entity>

這裡面MaterialInfo包含兩個信息,一個是Material類型(Sprite、Text),另一個是MaterialId,在這裡指Sprite或Text中記錄的NativeMaterialId,實質為Sprite或Text的SCD(SharedComponentData[4])在Chunk中的Index。

spriteData.NativeMaterialId = chunk.GetSharedComponentIndex(assetType);

到網格更新這一步時,遍歷ChunkArray中的Chunk,將SpriteImage和TextRenderer的上述信息記錄到HashMap中。

(2)網格合批

在MeshBatching的Job中,將上一步的HashMap作為輸入,並遞歸遍歷節點之間的父子關係構建三個DynamicBuffer:

private void GoDownRoot(Entity parent, 
ref DynamicBuffer<meshvertex> vertices,
ref DynamicBuffer<meshvertexindex> triangles,
ref DynamicBuffer<submeshinfo> subMeshes) {...}
/<submeshinfo>/<meshvertexindex>/<meshvertex>

如果連續兩個Entity的Material信息相同,則記錄到一個SubMesh中,完成合批。如果前後兩個Entity信息不同,就會創建一個新的SubMesh,也就是一個新的DrawCall。

bool materialAssigned = EntityToMaterial.TryGetValue(entity, out MaterialInfo material);
if (!materialAssigned)
{
material.Type = SubMeshType.SpriteImage;
material.Id = -1;
}
if (m_CurrentMaterialId != material.Id)
{
subMeshes.Add(new SubMeshInfo()
{
Offset = triangles.Length,
MaterialId = material.Id,
MaterialType = material.Type
});
m_CurrentMaterialId = material.Id;
}
int startIndex = vertices.Length;
if(VertexPointerFromEntity.Exists(entity))
VertexPointerFromEntity[entity] = new ElementVertexPointerInMesh(){VertexPointer = startIndex};

3、RenderSystem

完成了UI網格的合批之後,就可以根據已生成的頂點信息、SubMesh等數據生成Mesh,並將這些Buffer信息上傳至GPU,最後調用CommandBuffer的DrawMesh進行繪製了。也就是在這一步中使用到了Mesh.SetVertexBufferData等2019.3新支持的Mesh API,可以傳遞NativeArray參數直接修改Mesh,達到了效率的提升。

但由於這一步的Mesh和CommandBuffer都必須在主線程中完成,所以並不像網格合批可以得益於多線程帶來的巨大效率提升。

其主要實現邏輯在HybridRenderSystem.cs中,以下為Build CommandBuffer部分的實現邏輯:

private void BuildCommandBuffer(DynamicBuffer<meshvertex> vertexArray, DynamicBuffer<meshvertexindex> indexArray, DynamicBuffer<submeshinfo> subMeshArray, Mesh unityMesh, CommandBuffer canvasCommandBuffer)
{
using (new ProfilerSample("RenderSystem.SetVertexBuffer"))

{
unityMesh.Clear(true);
unityMesh.SetVertexBufferParams(vertexArray.Length, m_MeshDescriptors[0], m_MeshDescriptors[1], m_MeshDescriptors[2], m_MeshDescriptors[3], m_MeshDescriptors[4]);
}
using (new ProfilerSample("UploadMesh"))
{
unityMesh.SetVertexBufferData(vertexArray.AsNativeArray(), 0, 0, vertexArray.Length, 0);
unityMesh.SetIndexBufferParams(indexArray.Length, IndexFormat.UInt32);
unityMesh.SetIndexBufferData(indexArray.AsNativeArray(), 0, 0, indexArray.Length);
unityMesh.subMeshCount = subMeshArray.Length;
for (int i = 0; i < subMeshArray.Length; i++)
{
var subMesh = subMeshArray[i];
var descr = new SubMeshDescriptor()
{
baseVertex = 0,
bounds = default,
firstVertex = 0,
indexCount = i < subMeshArray.Length - 1
? subMeshArray[i + 1].Offset - subMesh.Offset
: indexArray.Length - subMesh.Offset,
indexStart = subMesh.Offset,
topology = MeshTopology.Triangles,
vertexCount = vertexArray.Length
};
unityMesh.SetSubMesh(i, descr);
}
unityMesh.UploadMeshData(false);
}
using (new ProfilerSample("BuildCommandBuffer"))
{
canvasCommandBuffer.Clear();
canvasCommandBuffer.SetProjectionMatrix(Matrix4x4.Ortho(0.0f, Screen.width, 0.0f, Screen.height, -100.0f, 100.0f));
canvasCommandBuffer.SetViewMatrix(Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one));
for (int i = 0; i < unityMesh.subMeshCount; i++)
{
var subMesh = subMeshArray[i];
var renderMaterial = SetMaterial(ref subMesh);
canvasCommandBuffer.DrawMesh(unityMesh, float4x4.identity, renderMaterial, i, -1, m_TemporaryBlock);
}
}
}
/<submeshinfo>/<meshvertexindex>/<meshvertex>

四、性能

以下為作者給出的性能對比數據:

基於DOTS的UI解決方案

複雜的UI實例(300 RectTransforms, 30314 characters)

基於DOTS的UI解決方案

Profiler Time性能對比

這裡需要說明的是,由於兩者的渲染開銷幾乎相同,所以主要比較的是UI重建開銷。

這裡也測試了一個簡單的1000個字符更新的Demo,在兩個中低端設備上運行Demo,通過Timeline記錄了兩種UI的重建耗時得到數據如下。

基於DOTS的UI解決方案

基於DOTS的UI解決方案

Demo運行截圖

基於DOTS的UI解決方案

OPPO K1上的DOTS UI耗時

可見DOTS UI在移動端設備上確實是有明顯的性能優勢。雖然日後必然會隨著功能的擴充,逐漸減小這種優勢,但目前的實現方式上也還是有優化空間的。所以DOTS UI的性能表現很值得期待。


相關鏈接:

[1]DOTS UI開源庫:https://lab.uwa4d.com/lab/5d3a18365b9dec79de05d348

[2]DOTS UI Github:https://github.com/supron54321/DotsUI

[3]DOTS UI介紹:https://forum.unity.com/threads/showcase-pure-dots-ui-system-detailed-description-feedback.688531/

[4]SharedComponentData:https://docs.unity3d.com/Packages/[email protected]/manual/shared_component_data.html

[5]Mesh API:https://docs.unity3d.com/2019.3/Documentation/ScriptReference/Mesh.html

[6]ConvertToEntity:https://docs.unity3d.com/Packages/[email protected]/api/Unity.Entities.ConvertToEntity.html


今天的推薦就到這兒啦,或者它可直接使用,或者它需要您的潤色,或者它啟發了您的思路......

請不要吝嗇您的點贊和轉發,讓我們知道我們在做對的事。當然如果您可以留言給出寶貴的意見,我們會越做越好。

【博物納新】是UWA旨在為開發者推薦新穎、易用、有趣的開源項目,幫助大家在項目研發之餘發現世界上的熱門項目、前沿技術或者令人驚歎的視覺效果,並探索將其應用到自己項目的可行性。很多時候,我們並不知道自己想要什麼,直到某一天我們遇到了它。

更多精彩內容請關注:lab.uwa4d.com


分享到:


相關文章: