自从在GDC 2019上,Unity分享了名为“连接DOTS:Unity面向数据技术栈”的技术演讲,关于DOTS的讨论和应用一直在业内备受关注。前段时间我们连载的“Unity手游实战”系列中,也有对于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组件销毁。
运行一下,即可看到运行时生效的效果。
由于测试的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示例,展示了目前支持的几种效果。
如果需要使用脚本控制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,MaterialInfo>,用来记录需要渲染的UI元素(Entity)和Material的对应关系。目前只包含Sprite和Text。
public NativeHashMap<Entity, MaterialInfo> EntityToMaterial;
这里面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) {...}
如果连续两个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);
}
}
}
四、性能
以下为作者给出的性能对比数据:
![](https://i-blog.csdnimg.cn/blog_migrate/e7958ae8ba9c53859ca37f84eaca082d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/740cc11c132450346003d3a74a0c500a.png)
这里需要说明的是,由于两者的渲染开销几乎相同,所以主要比较的是UI重建开销。
这里也测试了一个简单的1000个字符更新的Demo,在两个中低端设备上运行Demo,通过Timeline记录了两种UI的重建耗时得到数据如下。
![](https://i-blog.csdnimg.cn/blog_migrate/2df2fec8f5bc9648364399008084ecf4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6fc0dd8652fc3e23a1d7d341382439d7.png)
可见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/com.unity.entities@0.0/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/com.unity.entities@0.0/api/Unity.Entities.ConvertToEntity.html
今天的推荐就到这儿啦,或者它可直接使用,或者它需要您的润色,或者它启发了您的思路......
请不要吝啬您的点赞和转发,让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见,我们会越做越好。
【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。
更多精彩内容请关注:lab.uwa4d.com