基于DOTS的UI解决方案

自从在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);
        }
    }
}

四、性能

以下为作者给出的性能对比数据:

复杂的UI实例(300 RectTransforms, 30314 characters)

 

Profiler Time性能对比

 

这里需要说明的是,由于两者的渲染开销几乎相同,所以主要比较的是UI重建开销。

这里也测试了一个简单的1000个字符更新的Demo,在两个中低端设备上运行Demo,通过Timeline记录了两种UI的重建耗时得到数据如下。

Demo运行截图

 

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/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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Dots(Data-Oriented Technology Stack)是Unity引擎的一种数据导向技术栈,它可以让游戏的数据更加高效地处理和组织。KDTree是一种经典的数据结构,可以用于高效地处理多维空间数据的查询。本篇文章将介绍如何使用C#实现基于Dots的KDTree。 首先,我们需要定义一个点的数据结构。假设我们要处理二维空间中的点,我们可以定义一个名为“Point”的结构体: ``` public struct Point { public float x; public float y; public Point(float x, float y) { this.x = x; this.y = y; } } ``` 接下来,我们需要定义一个节点的数据结构。每个节点包含一个点、一个左子树和一个右子树。如果该节点没有子树,则对应的子树为空: ``` public struct Node { public Point point; public Node left; public Node right; } ``` 我们使用递归方法构建KDTree。具体来说,对于一个给定的点集合,我们首先找到X坐标的中位数,并将其作为根节点。然后,我们将点集合分成两个子集,一个包含所有X坐标小于中位数的点,另一个包含所有X坐标大于中位数的点。接着,我们递归地在每个子集中构建左子树和右子树,直到子集为空。在构建子树时,我们使用Y坐标的中位数来确定左右子树的分裂方式。 下面是构建KDTree的代码: ``` public static Node BuildKdTree(Point[] points, int depth = 0) { if (points == null || points.Length == 0) { return default(Node); } int axis = depth % 2; int medianIndex = points.Length / 2; Array.Sort(points, (a, b) => a.x.CompareTo(b.x)); Node node = new Node(); node.point = points[medianIndex]; node.left = BuildKdTree(points.Take(medianIndex).ToArray(), depth + 1); node.right = BuildKdTree(points.Skip(medianIndex + 1).ToArray(), depth + 1); return node; } ``` 我们可以使用以下代码测试构建KDTree的效果: ``` Point[] points = new Point[] { new Point(2, 3), new Point(5, 4), new Point(9, 6), new Point(4, 7), new Point(8, 1), new Point(7, 2) }; Node root = BuildKdTree(points); ``` 现在,我们已经成功地构建了一个KDTree。接下来,我们需要实现一个查询方法来查找最近邻点。查询方法的思想是从根节点开始向下遍历,直到叶子节点。在遍历的过程中,我们计算当前节点和目标点之间的距离,并将其与当前最近邻点的距离进行比较。如果当前节点更接近目标点,则更新最近邻点。接着,我们根据当前节点和目标点的关系,递归地遍历左子树或右子树。当我们到达叶子节点时,我们将该叶子节点作为当前最近邻点,并将其距离与当前最近邻点的距离进行比较。最终,我们找到了最近邻点。 以下是查询方法的代码: ``` public static Point FindNearestPoint(Node node, Point target) { if (node.left == default(Node) && node.right == default(Node)) { return node.point; } Point best = node.point; if (node.left != default(Node) && target.x < node.point.x) { Point leftBest = FindNearestPoint(node.left, target); if (Distance(leftBest, target) < Distance(best, target)) { best = leftBest; } } if (node.right != default(Node) && target.x > node.point.x) { Point rightBest = FindNearestPoint(node.right, target); if (Distance(rightBest, target) < Distance(best, target)) { best = rightBest; } } return best; } private static float Distance(Point a, Point b) { return Mathf.Sqrt(Mathf.Pow(a.x - b.x, 2) + Mathf.Pow(a.y - b.y, 2)); } ``` 我们可以使用以下代码测试查询方法的效果: ``` Point target = new Point(3, 5); Point nearest = FindNearestPoint(root, target); Debug.Log(nearest.x + ", " + nearest.y); // 输出 "2, 3" ``` 这就是基于Dots的KDTree的实现方法。它可以被用于高效地处理多维空间数据的查询,并且可以很容易地扩展到更高维度的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值