1.引言
在数字孪生项目中,一个普遍存在的挑战是对巨量标牌(例如:信息标识、监控设备、 传感器节点等)的有效管理与优化。特别是在水利行业中,分布广泛的监测站点、气象站、 水文站等设施所产生的海量数据,以及它们之间的高效协同工作,成为了确保系统整体性能的关键因素。如何确保这些标牌能够高效、准确地服务于各自的功能目标,并且与整体数字孪生系统无缝集成,不仅关系到单个项目的成功与否,更直接影响到水资源管理和防灾减灾的效果。
本文将探讨在水利行业的数字孪生项目中遇到的巨量标牌问题,并提出一系列针对性的优化方案,旨在为相关实践提供参考和指导。(以下内容均来源于《数字孪生与智能算法白皮书2025》。)
2.技术挑战
通常开发者为了实现标牌的展示功能,通过 UseWidget 来制作标牌样式,然后通过创建 Actor 并在其添加 WidgetComponent,所有场景中展示的众多标牌就是 UE 中多个 Actor, 当场景中标牌过多时不可避免就会带来严重的性能消耗。
通过一个简单的测试,可以窥见标牌对于项目性能的影响。我们在 UE5.1 版本中创建 一个空场景,可以看到场景的帧率稳定在 120fps 左右。
此时我们模拟项目制作中的标牌制作流程,通过简单的for 循环在场景中生成预期数 量的 Actor,观察发现当场景中的标牌数量在300个时,场景的帧率只能稳定在70fps 左右。依次递增,当标牌数量达到800个时,场景的帧率只能稳定在30fps 左右了,当数量在1000 个时,帧率就跌破30fps 到了 25fps 左右。
300 个标牌帧率
800 个标牌帧率
1000 个标牌帧率
设想一下当场景中再包含一些模型、天空球、粒子特效等效果时,项目的帧率会发生多大的变化。特别是在一些大屏客户超大分辨率的情况下,项目很难做到流畅的运行。诚然该测试主要是展示极端情况下的标牌帧率消耗,测试中镜头一直聚焦在所有标牌上,并且将WidgetComponent 的 Space 设置成了 Wrold,如果设置 Space 为 Screen 性能消耗略有降低,但是不可置否的是,场景中过多的标牌对项目的帧率影响是巨大的。
3.技术方案
在分享具体的技术方案时,我想提几点核心观点:标牌的展示实际上主要是一些数据直观的展现在孪生场景中,核心主要是将数据存储并取出来;标牌与标牌之间区别主要是数据上的差异;用户在大部分情况下只能观察到一部分标牌。
明确上述三点后我们是否可以提出一个思路,场景中或许根本不需要生成那么多标牌,只需要生成衡定数量的标牌,我们只需要展示用户可以看到标牌,同时在用户观察其他位置标牌时,将之前生成好用户在新的位置无需观察到的标牌重设到新的位置,并且调整标牌数据,用来模拟新的标牌那么我们就能用较少数量的标牌来模拟大量标牌的展示了。
点击查看最终大屏效果
3.1.标牌数据的存储
明确上述观点后,我们进行我们的第一步操作既是数据存储,我们需要将数据以合理的方式来存储,并且能快速的取出数据。那么什么是合理的方式存储呢?回顾第三条观点: 用户在大部分情况下都只能看到一部分标牌,所以我们需要将标牌的数据按照瓦片分割的方式存储下来,这样子当我们实现用户是否观察到这一批标牌时,可以快速的取出数据。
我们可以通过经验来预设一个最大的瓦片范围,该范围应当能将所有展示的标牌包裹 进去,然后设定小瓦片瓦片的 X 和 Y 的数量,通过简单的算法生成 x*y 个相邻的小瓦片, 每个小瓦片存储了该范围内所有标牌的数据和瓦片所包含的范围。
在上述描述的瓦片就是存储标牌数据的数据集,在 UE 中我们可以创建一个处理主逻辑的 Actor,该 Actor 实现我们上述分割瓦片思路,同时存储了这些小瓦片,这些小瓦片 对应 UE 中的数据类型应当是 UObject 的类型。
3.2.标牌数据的取出
在记录瓦片信息的 UObject 中,我们存储了所有标牌的信息。
考虑取出数据的频繁性,我们应当将其存储到一个 TArray 中,而不是 TMap 中,同时 记录标牌所有关键信息的数据应当不少于以下三种类型:存储所有标牌唯一ID 的变量, 类型为 TArray;存储所有标牌所有相关信息的变量,如果要描述更多信息可以符合 Json 格式,类型为 TArray;存储所有标牌位置信息的变量,类型为 TArray。
这三类数据的数组长度应当一致,元素一一对应,我们可以通过 ID 的 TArray.Find(), 查询到数据的下标,同时通过查询出来的下标从其他数组中取出对应元素。只要将数据下标取出,我们就可以对查询到的元素进行任意操作。
同时为了避免当瓦片的数量过多时,通过遍历再查询数据的复杂性,我们在处理主逻 辑的 Actor 中通过两个数组来映射数据所在的位置,通过一个 TArray来记录所有 瓦片的 ID 的信息,再用一个 TArray来记录存储信息下标对应的信息,X对应ID所存在的瓦片的数组下标,Y对应瓦片内存取的数据的真正下标。这两个数组同之 前的逻辑一样,长度一致且下标对应的元素一一对应。
3.3.判断是否应当取出数据
在之前我们已经将数据以瓦片的形式存储在数组中,接下来只需要判断用户是否观察 到该瓦片即可,我们将用户可以看到的画面也就是屏幕画面当做一个四边形,同时瓦片也 是个一个四边形,那么有存在三种情况判断是否应当取出瓦片中标牌的数据:
A. 瓦片在屏幕中
B. 屏幕在瓦片中
C. 屏幕与瓦片重叠显示
我们可以通过 GetViewprotSize()函数来获得屏幕的位置 ,同时利用 DeprojectScreentoWorld()函数来讲屏幕所看到的范围由屏幕坐标转换为世界场景坐标,最后我们通过简单的逻辑计算判断是否观察到瓦片,如果观察到那么就将瓦片信息取出即可。
3.4.如何动态的设置标牌信息
当我们取出数据后,便是动态的更新标牌数据,在这里就涉及到大量标牌的数据生成和销毁,我们可以到动态池的思路来处理标牌数据更新。
动态池主要用在当场景中有大量的 Actor 进行动态的增删时的情况,为了避免大量 Actor 生成和销毁产生的性能消耗。核心思路在于一次性生成 Actor,当 Actor 被删除时并 不真正将其删除,而是将它放在废弃池中同时将其隐藏掉,当有 Actor 重新生成时从废弃池中获取取Actor,从而避免大量增删的情况,只有到数据大于当前可以使用的所有 Actor 时我们才生成新的Actor,或者到数据远远大于当前所应当存在的 Actor 数量时销毁部分Actor。
运用该思路我们主要是处理三种情况:
A. 更新可见的标牌信息
B. 将不可见标牌的位置和数据更新,使其成为新的标牌
C. 生成缺失的标牌或剔除多余的标牌信息
为了避免用户快速移动镜头导致数据变化较大,导致更新数据量过多,从而导致程序 运行偶发性卡顿的现象,我们可以将更新和剔除 Actor 的逻辑处理为批次逻辑。
3.5.最终效果
运用上述思路是实现逻辑,我们将一个在 10001000 的范围内的标牌,划分成 36 个 300300 的瓦片中。
最终我们利用少量的标牌模拟场景中大量标牌的效果,在用户观察的区域内动态更新 场景中标牌的信息,在用户观察到不多与五个瓦片时帧率一直稳定在 120 帧未有明显的浮动。
观察到五个瓦片的性能消耗
在实际项目中,我们可以针对标牌做简单的分层处理,或者根据需求调整瓦片的大小, 并且限制用户同时观察到的瓦片的数量,可以用更少的性能代价来模拟巨量标牌的效果。
4.自动化模型构建能力
创建高保真的数字孪生体虚拟模型是构建数字孪生应用的重要步骤之一,需要真实的再现物理实体的几何图形、属性、行为和规则等。数字孪生体模型不仅要在几何结构上与物理实体保持一致,更重要的是要能模拟物理实体的时空状态、行为、功能等。