本文基于混合现实的性能建议,但重点介绍特定于Unity改进。
我们最近发布了一个名为“质量基础知识”的应用程序,它涵盖了HoloLens 2应用的常见性能、设计和环境问题以及解决方案。 此应用是一个出色的视觉演示,适用于以下内容。
使用建议Unity项目设置
优化Unity中混合现实应用的性能时,最重要的第一步是确保使用建议的环境设置进行Unity。 本文包含一些最重要的场景配置的内容,这些配置用于构建高性能混合现实应用。 下面还突出显示了其中一些建议的设置。
如何使用Unity进行分析
Unity提供内置 Unity Profiler,这是收集特定应用的宝贵性能见解的绝佳资源。 虽然可以在编辑器中运行探查器,但这些指标并不表示真正的运行时环境,因此应谨慎使用结果。 建议在设备上运行时远程分析应用程序,以获取最准确和可作的见解。
Unity为以下项提供了很好的文档:
GPU 分析
Unity探查器
连接Unity探查器后,添加 GPU 探查器后 (请参阅在右上角) 添加探查器,可以看到在探查器中间分别在 CPU & GPU 上花费了多少时间。 这样,如果开发人员的应用程序是 CPU 或 GPU 有限,则开发人员可以快速获得近似值。
注意
若要使用 GPU 分析,需要在“Unity播放器设置”中禁用图形作业。 有关更多详细信息,请参阅Unity的 GPU 使用情况探查器模块。
Unity帧调试器
Unity的帧调试器也是一个功能强大且具有洞察力的工具。 它很好地概述了 GPU 在每个帧中执行的作。 需要注意的其他呈现目标和 blit 命令在它们之间复制,因为这些命令在 HoloLens 上成本很高。 理想情况下,不应在 HoloLens 上使用屏幕外呈现目标。 在启用昂贵的渲染功能时,会添加这些功能 (例如 MSAA、HDR 或全屏效果,例如应避免的开花) 。
HoloLens 帧速率覆盖
“设备门户 系统性能 ”页很好地总结了设备的 CPU 和 GPU 性能。 可以启用 在头戴显示设备中显示帧速率计数器 和 在头戴显示设备中显示帧速率图。 这些选项分别启用 FPS 计数器和图形,从而在设备上任何正在运行的应用程序中立即提供反馈。
PIX
PIX 也可用于分析Unity应用程序。 此外,还提供了有关如何使用和安装 PIX for HoloLens 2的详细说明。 在开发版本中,在 Unity 的帧调试器中看到的相同范围也会显示在 PIX 中,并且可以更详细地检查和分析。
注意
Unity可以通过 XRSettings.renderViewportScale 属性在运行时轻松修改应用程序的呈现目标分辨率。 设备上显示的最终图像具有固定分辨率。 该平台对较低分辨率输出进行采样,以生成用于在显示器上呈现的更高分辨率图像。
UnityEngine.XR.XRSettings.renderViewportScale = 0.7f;
CPU 性能建议
以下内容介绍了更深入的性能实践,特别是针对Unity & C# 开发。
缓存引用
我们建议在初始化时缓存对所有相关组件和 GameObject 的引用,因为与存储指针的内存成本相比,重复函数调用(如 GetComponent<T> () 和 Camera.main )的成本更高。 . Camera.main 仅在下方使用 FindGameObjectsWithTag () ,这会在场景图中搜索具有 “MainCamera” 标记的相机对象。
using UnityEngine;
using System.Collections;
public class ExampleClass : MonoBehaviour
{
private Camera cam;
private CustomComponent comp;
void Start()
{
cam = Camera.main;
comp = GetComponent<CustomComponent>();
}
void Update()
{
// Good
this.transform.position = cam.transform.position + cam.transform.forward * 10.0f;
// Bad
this.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 10.0f;
// Good
comp.DoSomethingAwesome();
// Bad
GetComponent<CustomComponent>().DoSomethingAwesome();
}
}
注意
避免 GetComponent (字符串)
使用 GetComponent () 时,有一些不同的重载。 请务必始终使用基于类型的实现,而不要使用基于字符串的搜索重载。 与按类型搜索相比,场景中按字符串搜索的成本更高。
(良好) 组件 GetComponent (类型)
(良好) T GetComponent<T> ()
(错误的) 组件 GetComponent (字符串) >
避免昂贵的作
避免使用 LINQ
尽管 LINQ 可以简洁且易于读取和写入,但与手动编写算法相比,它通常需要更多的计算和内存。
// Example Code using System.Linq; List<int> data = new List<int>(); data.Any(x => x > 10); var result = from x in data where x > 10 select x;常见Unity API
某些Unity API 虽然有用,但执行开销可能很高。 其中大多数都涉及在整个场景图中搜索一些匹配的 GameObject 列表。 通常可以通过缓存引用或实现 GameObjects 的管理器组件在运行时跟踪引用来避免这些作。
GameObject.SendMessage() GameObject.BroadcastMessage() UnityEngine.Object.Find() UnityEngine.Object.FindWithTag() UnityEngine.Object.FindObjectOfType() UnityEngine.Object.FindObjectsOfType() UnityEngine.Object.FindGameObjectsWithTag() UnityEngine.Object.FindGameObjectsWithTag()
注意
应不一切代价消除 SendMessage () 和 BroadcastMessage () 。 这些函数的速度可能比直接函数调用慢 1000 倍。
提防装箱
装箱 是 C# 语言和运行时的核心概念。 这是将值类型变量(如
char、int、bool等)包装到引用类型变量的过程。 当值类型变量“装箱”时,它将包装在System.Object存储在托管堆上的 。 内存已分配,最终在释放时必须由垃圾回收器处理。 这些分配和解除分配会产生性能成本,在许多情况下是不必要的,或者很容易被更便宜的替代方法所取代。若要避免装箱,请确保存储数值类型和结构 (变量、字段和属性(包括
Nullable<T>) )类型为特定类型(如int、float?或MyStruct),而不是使用 对象。 如果将这些对象放入列表中,请确保使用强类型列表,例如List<int>而不是List<object>或ArrayList。C 中的装箱示例#
// boolean value type is boxed into object boxedMyVar on the heap bool myVar = true; object boxedMyVar = myVar;
重复代码路径
任何重复Unity回调函数 (即每秒和/或帧执行多次的更新) ,都应仔细编写。 此处任何昂贵的作都对性能产生巨大且一致的影响。
空回调函数
尽管以下代码在应用程序中可能看起来是无辜的,尤其是因为每个Unity脚本都使用 Update 方法自动初始化,但这些空回调可能会变得昂贵。 Unity在非托管代码边界和托管代码之间、UnityEngine 代码与应用程序代码之间来回运行。 在此桥上切换上下文的成本相当昂贵,即使没有要执行的内容。 如果应用具有 100 个 GameObject,并且组件具有空重复Unity回调,则这尤其成问题。
void Update() { }
注意
更新 () 是此性能问题的最常见表现形式,但其他重复Unity回调(如以下)可能同样糟糕(如果不是更糟):FixedUpdate () 、LateUpdate () 、OnPostRender“、OnPreRender () 、OnRenderImage () 等。
支持每帧运行一次的作
以下Unity API 是许多全息应用的常见作。 尽管并非总是可行,但这些函数的结果通常可以计算一次,并且针对给定帧跨应用程序重新使用结果。
) 最好有一个专用的 Singleton 类或服务来处理你的视线光线投射到场景中,然后在所有其他场景组件中重复使用此结果,而不是由每个组件执行重复和相同的 Raycast作。 某些应用程序可能需要来自不同源或针对不同 LayerMask 的光线投射。
UnityEngine.Physics.Raycast() UnityEngine.Physics.RaycastAll()b) 通过在 Start () 或 Awake () 缓存引用来避免在重复Unity回调(如更新 () )中执行 GetComponent () 作
UnityEngine.Object.GetComponent()c) 最好在初始化时实例化所有对象(如果可能),并使用 对象池 在整个应用程序的运行时回收和重用 GameObject
UnityEngine.Object.Instantiate()避免使用接口和虚拟构造
通过接口调用函数与直接对象或调用虚拟函数通常比使用直接构造或直接函数调用要贵得多。 如果不需要虚拟函数或接口,则应将其删除。 但是,如果使用这些方法可以简化开发协作、代码可读性和代码可维护性,则这些方法的性能下降是值得权衡的。
通常,建议不要将字段和函数标记为虚拟,除非有明确预期需要覆盖此成员。 对于每帧调用多次甚至每帧调用一次(例如方法)
UpdateUI()的高频代码路径,应格外小心。避免按值传递结构
与类不同,结构是值类型,当直接传递给函数时,其内容将复制到新创建的实例中。 此副本会增加 CPU 成本,以及堆栈上的更多内存。 对于小型结构,效果最小,因此可以接受。 但是,对于重复调用每个帧的函数和采用大型结构的函数,如果可能,请修改函数定义以通过引用传递。 在此处了解详细信息
其他
物理
) 通常,改进物理的最简单方法是限制在物理上花费的时间或每秒的迭代次数。 这会降低模拟准确性。 请参阅 Unity 中的 TimeManager
b) Unity中的碰撞体类型具有截然不同的性能特征。 下面的顺序从左到右列出了性能最高的碰撞体和性能最低的碰撞体。 请务必避免网格碰撞体,后者比基元碰撞体贵得多。
球形 < 胶囊 < 盒 <<< 网格 (凸) < 网格 (非凸)
动画
通过禁用 Animator 组件来禁用空闲动画, (禁用游戏对象不会) 具有相同的效果。 避免设计模式,其中动画器位于循环中,将值设置为同一对象。 此技术具有相当大的开销,对应用程序没有影响。 在此了解更多信息。
复杂算法
如果应用程序使用复杂的算法(如反向运动学、路径查找等),请查找更简单的方法或调整其性能的相关设置
CPU 到 GPU 性能建议
通常,CPU 到 GPU 的性能归结于提交到图形卡的绘图调用。 若要提高性能,绘制调用需要从战略上 减少) 或 b) 重构 以获得最佳结果。 由于绘图调用本身是资源密集型的,因此减少调用会减少所需的整体工作。 此外,绘制调用之间的状态更改需要在图形驱动程序中执行昂贵的验证和转换步骤,因此,重新调整应用程序的绘图调用以限制 (状态更改(例如不同的材料等),) 可以提高性能。
Unity有一篇很好的文章,概述了并深入探讨其平台的批处理绘图调用。
单通道实例化呈现
Unity中的单通道实例化渲染允许将每只眼睛的绘制调用减少到一个实例化绘制调用。 由于两个绘制调用之间的缓存一致性,GPU 的性能也有一些改进。
在Unity项目中启用此功能
- 打开 OpenXR 设置 (转到 编辑>项目设置>XR 插件管理>OpenXR) 。
- 从“呈现模式”下拉菜单中选择“单通道实例”。
有关此呈现方法的详细信息,请阅读 Unity 中的以下文章。
注意
如果开发人员已有未编写用于实例化的现有自定义着色器,则单通道实例化呈现会出现一个常见问题。 启用此功能后,开发人员可能会注意到某些 GameObject 仅在一只眼中呈现。 这是因为关联的自定义着色器没有用于实例化的适当属性。
静态批处理
Unity能够批处理许多静态对象,以减少对 GPU 的绘制调用。 静态批处理适用于大多数 Renderer 对象,Unity 1 个) 共享相同的材料,2 个) 都标记为静态 (选择Unity中的对象,然后选中检查器) 右上角的复选框。 标记为 静态 的 GameObject 不能在整个应用程序的运行时中移动。 因此,静态批处理可能很难应用于 HoloLens,其中几乎每个对象都需要放置、移动、缩放等。对于沉浸式头戴显示设备,静态批处理可以大大减少绘图调用,从而提高性能。
有关详细信息,请阅读 Unity 中的“绘制调用批处理”下的静态批处理。
动态批处理
由于在 HoloLens 开发中将对象标记为 静态 是有问题的,因此动态批处理是一个很好的工具,可以弥补这种缺乏的功能。 它也可用于沉浸式头戴显示设备。 但是,Unity中的动态批处理可能很难启用,因为 GameObjects 必须 ) 共享相同的材料,b ) 满足其他条件的长列表。
阅读Unity中的“绘制调用批处理”下的“动态批处理”以获取完整列表。 最常见的情况是,由于关联的网格数据不能超过 300 个顶点,因此 GameObject 将变为动态批处理无效。
其他技术
仅当多个 GameObject 能够共享相同的材料时,才会发生批处理。 通常,由于需要 GameObject 为其各自的 Material 提供唯一的纹理,因此会阻止此作。 通常将纹理合并为一个大纹理,这是一种称为 纹理图谱的方法。
此外,最好尽可能合理地将网格合并为一个 GameObject。 Unity中的每个呈现器都有其关联的绘制调用,而不是在一个呈现器下提交组合网格。
注意
在运行时修改 Renderer.material 的属性会创建 Material 的副本,从而可能会中断批处理。 使用 Renderer.sharedMaterial 修改 GameObjects 中的共享材料属性。
GPU 性能建议
带宽和填充速率
在 GPU 上呈现帧时,应用程序受内存带宽或填充速率的约束。
-
内存带宽 是 GPU 可从内存中读取和写入的速率
- 在Unity中,在“编辑>项目设置质量设置”>中更改纹理质量。
-
填充率 是指 GPU 每秒可以绘制的像素。
- 在 Unity 中,使用 XRSettings.renderViewportScale 属性。
优化深度缓冲区共享
建议 启用 深度缓冲区共享 ,以优化 全息影像稳定性。 使用此设置启用基于深度的后期重新投影时,建议选择 16 位 深度格式而不是 24 位 深度格式。 16 位深度缓冲区将大幅降低带宽 (,从而为与深度缓冲区流量相关的) 供电。 这在功率降低和性能方面都有很大的改进。 但是,使用 16 位深度格式可能会产生两种负面结果。
Z-Fighting
深度范围保真度降低使得 16 位比 24 位更有可能发生 z 冲突 。 若要避免这些工件,请修改Unity相机的近/远剪辑平面,以考虑较低的精度。 对于基于 HoloLens 的应用程序,50 米的远剪平面而不是默认的 1000 米Unity通常可以消除任何 z 冲突。
已禁用模具缓冲区
当Unity创建深度为 16 位的呈现纹理时,不会创建模具缓冲区。 选择 24 位深度格式(如Unity文档中所述)将创建一个 24 位 z 缓冲区和一个 8 位模具缓冲区, (如果 32 位适用于设备, (例如 HoloLens) ,这种情况通常) 。
避免全屏效果
全屏作的技术可能很昂贵,因为它们的数量级是每帧数百万次作。 建议避免 后期处理效果 ,例如抗锯齿、开花等。
最佳照明设置
Unity中的实时全局照明可以提供出色的视觉结果,但涉及昂贵的照明计算。 建议通过“窗口>渲染>照明设置”取消选中“实时全局照明”>,为每个Unity场景文件禁用实时全局照明。
此外,建议禁用所有阴影强制转换,因为这还会将昂贵的 GPU 传递添加到Unity场景中。 阴影可以按光线禁用,但也可以通过质量设置进行整体控制。
编辑>“项目设置”,然后选择“ 质量 ”类别 > ,为 UWP 平台选择 “低质量 ”。 还可以将 Shadows 属性设置为 “禁用阴影”。
建议在 Unity 中将烘焙照明与模型配合使用。
减少多边形计数
多边形计数减少两者之一
- 从场景中删除对象
- 资产抽取,可减少给定网格的多边形数
- 在应用程序中 实现详细级别 (LOD) System ,从而使用同一几何图形的低多边形版本呈现较远的对象
了解Unity中的着色器
比较着色器性能的一个简单近似值是确定每个在运行时执行的平均作数。 这可以在Unity中轻松完成。
选择着色器资产或选择材料,然后在检查器窗口的右上角。 选择齿轮图标,然后选择 “选择着色器”
选中着色器资产后,选择检查器窗口下的 “编译并显示代码” 按钮
编译后,在结果中查找统计信息部分,其中包含顶点着色器和像素着色器的不同作数 (注意:像素着色器通常也称为片段着色器)
优化像素着色器
使用上述方法查看编译的统计信息结果, 片段着色器 通常执行比 顶点着色器更多的作。 片段着色器(也称为像素着色器)在屏幕输出上按像素执行,而顶点着色器仅按在屏幕上绘制的所有网格的顶点执行。
因此,片段着色器不仅比顶点着色器具有更多的指令,因为所有照明计算,片段着色器几乎总是在更大的数据集上执行。 例如,如果屏幕输出是 2k x 2k 图像,则片段着色器可以执行 2,000*2,000 = 4,000,000 次。 如果呈现两只眼睛,则此数字会翻倍,因为有两个屏幕。 如果混合现实应用程序具有多个传递、全屏后处理效果或将多个网格呈现为同一像素,则此数字会显著增加。
因此,与顶点着色器中的优化相较,减少片段着色器中的作数通常可带来更高的性能提升。
Unity Standard着色器替代项
不要使用基于物理的渲染 (PBR) 或其他高质量着色器,而是使用性能更高、成本更高的着色器。 混合现实工具包提供 MRTK 标准着色器,该着色器已针对混合现实项目进行了优化。
Unity还提供不亮、顶点亮、漫射和其他简化着色器选项,与Unity Standard着色器相比速度更快。 有关更多详细信息 ,请参阅内置着色器的使用情况和性能 。
着色器预加载
使用 着色器预加载 和其他技巧来优化 着色器加载时间。 具体而言,着色器预加载意味着不会因运行时着色器编译而出现任何问题。
限制过度绘制
在Unity中,可以通过切换“场景”视图左上角的绘制模式菜单并选择“过度绘制”来显示场景的过度绘制。
通常,可以通过在对象发送到 GPU 之前提前剔除对象来缓解过度绘制。 Unity提供有关为其引擎实现遮挡剔除的详细信息。
内存建议
过多的内存分配 & 解除分配作可能会对全息应用程序产生不利影响,从而导致性能不一致、帧冻结和其他有害行为。 在 Unity 中进行开发时,了解内存注意事项尤其重要,因为内存管理由垃圾回收器控制。
垃圾收集
激活 GC 以分析在执行期间不再位于范围内的对象,并且需要释放其内存,以便重复使用时,全息应用会丢失垃圾回收器 (GC) 的处理计算时间。 持续分配和取消分配通常需要垃圾回收器更频繁地运行,从而损害性能和用户体验。
导致过度垃圾回收的最常见做法之一不是在开发Unity缓存对组件和类的引用。 应在 Start () 或 Awake () 期间捕获任何引用,并在更新 () 或 LateUpdate () 等后续函数中重复使用。
其他快速提示:
- 使用 StringBuilder C# 类在运行时动态生成复杂字符串
- 在不再需要时删除对 Debug.Log () 的调用,因为它们仍在应用的所有生成版本中执行
- 如果全息应用通常需要大量内存,请考虑在加载阶段(例如显示加载或切换屏幕时)调用 System.GC.Collect ()
对象池
对象池是降低连续对象分配和解除分配成本的常用技术。 这是通过分配包含相同对象的大型池并重用此池中的非活动可用实例来完成的,而不是随着时间的推移不断生成和销毁对象。 对象池非常适合在应用期间具有可变生存期的可重用组件。
启动性能
请考虑使用较小的场景启动应用,然后使用 SceneManager.LoadSceneAsync 加载场景的其余部分。 这使你的应用能够尽快进入交互式状态。 激活新场景时,可能会有较大的 CPU 峰值,并且任何呈现的内容都可能会断断续续或停滞。 解决此问题的一种方法是在加载的场景中将 AsyncOperation.allowSceneActivation 属性设置为“false”,等待场景加载,将屏幕清除为黑屏,然后将其重新设置为“true”以完成场景激活。
请记住,在加载启动场景时,将向用户显示全息初始屏幕。