面向 Unity* 软件和虚拟现实的优化:运行时生成内容

NASA exploration vehicle model

自游戏行业诞生以来,优化游戏以实现高性能一直是游戏开发过程中的一个重要因素。虽然开发人员一直尝试将硬件推向极致,但当移动游戏成为主流时,优化技术变得尤为突出。Unity* 软件、Unreal* 等常见引擎最初都是面向 PC 游戏设计的,设计人员用它们在旧硬件上提供高性能体验时,发现它们存在许多缺点。开发人员急需新的技术和技巧,这种情况很快成了一种常态。今天,我们经历着同样的觉醒,随着虚拟现实 (VR) 成为一种资源密集型的媒介,我们必须不断创新才能确保提供最佳 VR 体验。

本文介绍了几个支持 VR 开发人员设计 VR 体验和视频游戏的技巧,以及这些技巧所带来的优势。

项目概况

文中所示的工作使用 Unity 软件引擎,但这些技巧也可以应用于其他引擎。为了帮助大家了解性能瓶颈并找到相应的解决方案,我们使用了不同的性能应用,例如 Unity 分析器、帧调试器和英特尔® 图形性能分析器(英特尔® GPA)。

项目使用基于英特尔® 酷睿™ i7-6700 处理器和 NVIDIA GeForce* GTX 970 图形处理单元 (GPU) 的戴尔* XPS 8910。此设置接近 PC VR 的标准最低规格。

软件堆栈使用:

  • Unity 2018.1.2f1
  • Simplygon* UI
  • 面向 Unity 软件的 Steam*VR 插件
  • Microsoft Visual Studio* Community
  • 英特尔® GPA

在 VR 中创建数千个高度详细的 3D 模型

使用这些技巧可以达到什么样的效果?首先,能够面向 VR 中运行时生成内容进行优化。您可以在无缝 VR 体验中设计包含成千上万个高度详细模型的 Unity 软件渲染场景,无需明显的细节级别 (LOD) 切换。

随着视频游戏、开放世界和大型环境的范围不断扩大以及 VR 中可感知到的细节不断增加,创建这些体验所需的计算能力也呈指数级增加。近年来,技术公司 Procedural Worlds - 借助雕塑、纹理化、填充然后渲染地形的标志性素材 Gaia、GeNa 和 CTS,支持专业人士和独立开发人员创造出令人惊叹的环境。运行时生成内容类似于 Minecraft*,已经成为创建广阔而有趣的世界的强大工具。您希望能够在游戏世界中靠近这些详细的模型,并清楚地观察它们。您想要很多这种模型。

Unity scene

图 1.本次练习的目标是在无缝 VR 体验中创建 Unity 软件渲染场景,其中包含成千上万个高度详细的模型。

此处介绍的项目充分利用了部分固有的 VR 设计选择,比如非连续移动(远距传动或类似的移动),尽管其中大部分也可以适应规则的平滑运动,但有一些变化。

性能测试设置

遇到丢帧情况时,大多数 VR 软件开发套件 (SDK) 会提供额外的优化保护层。这样做的好处是可以避免因性能引起的晕动症,创造更舒适的玩家体验。优化体验时,请务必停用这些措施,以便了解技巧和应用性能所带来的实际效果。

项目使用 SteamVR,停用特定于平台的保护层。为此,请选择 SteamVR 设置屏幕上的 Developer 选项卡,然后清除 Direct Mode 按钮下方的重投影复选框,如图 2 所示。

Steam VR settings

图 2.禁用 SteamVR 中的重投影将停用保护层。

其他 SDK 提供类似的保护,例如 Oculus* 平台中的异步空间扭曲 (ASW)。大多数这种技巧使用前一帧的数据来重新创建硬件错过的帧应呈现的近似值,并在头盔中显示这种近似值。

起点:选择模型

该项目使用一些高多边形,高清模型在 VR 中显示。逐个应用这一系列技巧将进一步优化 Unity 软件中生成的输出。这些模型非常重而且复杂,因此人们希望在屏幕上同时显示几个模型即可。该项目的重点是进行原始、大型的优化。

您可以使用所示的工具和编程技巧执行此处所示的任何操作。访问 NASA 3D 资源网站,可直接获取美国国家航空航天局免费提供的 NASA 探测飞行器。原始模型是 404,996 个多边形。

当您将原始形式的对象添加到有定向光的空场景中时,可以看到 PC 的性能明显降低。此时,屏幕中同时显示三辆以上探测飞行器将开始丢帧。

NASA exploration vehicle model

图 3.在 Unity 软件中以 Play 模式查看的 NASA 探测飞行器模型。可以从右上角的监视器中看到性能统计信息。

大家可以看到,现在多边形的数量比最初多。这是由于着色器通道的原因。宇宙飞船使用 Unity 软件的标准着色器和单向灯。另一件需要考虑的事情是绘制模型所需的绘制调用或批处理数量。大家可以通过图 4 看到英特尔 GPA 软件的运行情况。从图形监视器捕捉帧后,您可以在图形帧分析器中对其进行分析。

Intel GPA Graphics Frame Analyzer screen

图 4.英特尔图形性能分析器(英特尔 GPA)显示了 NASA 探测飞行器的绘图调用。

英特尔图形性能分析器显示该飞行器的绘制调用超过了 200。宇宙飞船上每个小部件的所有小型绘制调用都加起来。如果试图在屏幕上产生多个对象时,尤其是制作移动 VR 游戏的情况下,这是会出现问题的。大蓝色矩形是特定于 VR 的渲染步骤。

Unity 软件会在将小网格发送到 GPU 之前自动批处理小网格,从而以 Saved by Batching 的形式生成大量网格。

大家可以直接在 Unity 软件中将所有小网格组合成一个单独的网格,以减少绘制调用的数量,还可以在生成 LOD 的同时用 Simplygon 执行此操作。无论采用哪种方式,组合网格表明我们离最终目标更近了一步。组合网格可将绘制调用的数量减少到 1,而且现在 Unity 场景不会出现丢帧,直到您将超过七艘宇宙飞船放入场景中 —性能提升 2 倍。然而,由于 GPU 不能再接受多边形和顶点,因此设计已经达到了硬限值。

光照贴图和适当的照明

在 3D 游戏开发过程中,适当的照明是最重要的方面之一。在 VR 中,照明适当的对象可提供更真实可信的体验。实时照明虽然对移动的灯光和阴影有着很大的影响,但由于没有全局照明 (GI) 而缺乏深度。GI 模拟在物体之间反弹的间接光。通过在运行时创建应用在模型上的光照贴图问题的预计算阶段,现代引擎中可以达到这种效果。

现代引擎使用多种技巧将实时照明与预计算的 GI 数据相结合,从而提供更准确的描述。然而,所有这些都是以牺牲额外性能为代价的,因为实时照明本身就要求出色的性能。着色器中的每个通道都会使绘制的三角形和顶点数量增加一倍,而且阴影也需要强大的计算能力。性能最高的路径是光照贴图,它消除了方程式的实时照明。

NASA exploration vehicle model light shading

图 5.实时定向光(左图)与光照贴图模型(右图)。

图 5 显示了实时定向光模型与光照贴图模型之间的差异。场景的左侧使用一个实时定向光,由于有多个光照通道,因此绘制调用数量增加,渲染的几何体也是如此。右侧的场景是一个光照贴图模型,由于渲染的 VR 特性,因此几何体仅加倍(每只眼睛一个)。

通过光照贴图模型和组合网格,您可以保存所有额外的照明通道,从而轻松地将场景数量增加到 22 艘宇宙飞船,而且不会出现丢帧。这样可以使性能提高 3 倍,而且不会降低视觉质量。在大多数情况下,间接照明可以创建更加可信的沉浸式表现形式。

NASA exploration vehicle Lightmapped model and batching

图 6.光照贴图模型和批处理支持场景中包含 22 艘宇宙飞船,不会出现丢帧,表现为性能提高 3 倍。

运行时光照贴图

到目前为止,一切都很好,但有一个问题。Unity 软件中的光照贴图是一个在编辑器中进行的过程,这意味着您必须让对象静止,烘焙场景并创建光照贴图。只有这样才能运行场景,显示烘焙过的灯光。但如果要创建运行时生成内容会怎么样?能将光照贴图保存到预制件中吗?遗憾的是,Unity 软件无法自动完成这一步骤。在 Unity 5 之前,您可以烘焙对象并将其拖到素材文件夹中,从而创建包含存储在各渲染器中的光照贴图信息的预制件。从 Unity 5 开始,光照贴图信息不再存储在渲染器中,但您可以通过一些脚本方面的操作达到相同的目的。

为了能够检索光照贴图信息,您需要为每个渲染器保存对特定光照贴图纹理和 lightmapScaleOffset 的引用。这些参数指向所要使用的光照贴图纹理,以及 UV 贴图中的位置。(UV 映射是将 2D 图像投影到 3D 模型表面以进行纹理映射的 3D 建模过程。)因此,您需要在烘焙时保存这组数据,以便稍后可以在实例化预制件中使用。您可以使用以下代码保存此数据:

[System.Serializable]
    struct RendererInfo
    {
   	 public Renderer     renderer;
   	 public int    		 lightmapIndex;
   	 public Vector4    	 lightmapOffsetScale;
    }

    [SerializeField]
    RendererInfo[]    m_RendererInfo;
    [SerializeField]
    Texture2D[]     m_Lightmaps;

上面的代码可用于创建光照贴图纹理阵列,以 m_Lightmaps 的形式供对象使用。对于对象中的每个渲染器,此代码将渲染器的引用保存在结构中。它还会创建一个索引,指向要以 m_Lightmaps 及其 lightmapScaleOffset 形式使用的光照贴图,从而保存在 UV 空间中使用的光照贴图的坐标。

调用 UnityEditor.Lightmapping.Bake();烘焙灯光。请注意,每个字段都已序列化以保存数据。UnityEditor.PrefabUtility.ReplacePrefab(gameObject, targetPrefab);更新预制件。

static void ApplyRendererInfo(RendererInfo[] infos, int[] lightmapOffsetIndex)
	{
    	for (int i = 0; i < infos.Length; i++)
    	{
        	var info = infos[i];
        	info.renderer.lightmapIndex = lightmapOffsetIndex[info.lightmapIndex];
        	info.renderer.lightmapScaleOffset = info.lightmapOffsetScale;
    	}
    	LightmapSettings.lightmapsMode = LightmapsMode.NonDirectional;
    	LightmapSettings.lightmaps = combinedLightmaps;

	}

有了这些数据,您只需要实例化您的预制件,并在每个对象的 Awake 功能中应用上述代码。在上述代码片段中,combinedLightmaps 是一个包含所有待使用光照贴图的阵列,包括 m_Lightmaps(用于您的预制件)以及在运行时之前可能已经烘焙到场景中的所有光照贴图。确保准确计算待使用光照贴图的 lightmapindex。计算错误可能会导致光照贴图中出现重复以及过度使用内存。

请注意,只有一个阵列 (m_Lightmaps) 用于光照贴图,而且光照贴图模式设置为 “NonDirectional”。如果要保存定向光照贴图,必须对其进行扩展并保留两个阵列 — 一个用于颜色,一个用于方向。您可以从LightmapData 结构中提取这两个阵列。

您可以通过附带的代码示例下载上述完整的光照贴图脚本。这个脚本最初来源于 Unity 论坛中的这篇文章,其中 Joachim Ante(Unity Technologies 首席技术官)对部分流程进行解释。该论坛帖子中包含非常复杂的解决方案和编辑器脚本,引发了激烈的讨论。信息已经压缩成最简单的脚本。

细节级别

LOD 技术常用于优化视频游戏性能。LOD 要求每个项目都有几个 3D 模型,这样可以根据某些预定义的行为进行切换。例如,可以设置 LOD,以便在较远的距离,游戏显示相比接近对象时细节较少的模型。

稍后,本文将介绍一种非常实用的工作流程,用于此概念时可获得良好且一致的结果。示例 VR 设计改进了这一想法,而且项目在 Unity 软件中创建了一个系统,以进一步提高性能。

面向 VR 的 LOD 系统

Unity 软件提供了一个便利的 LOD 系统,用于交换内置于引擎的模型。Unity LOD 系统在大多数情况下都可以开箱即用。不过它也确实存在一个缺点:用于多个对象时它的成本很高昂,因为它经常进行距离检查计算。考虑到对象填充的屏幕百分比,此 LOD 系统还可在素材之间切换。如果您只想检查房间大小的距离而不是游戏中的主摄像头,则最好使用更简单的基于距离的 LOD 系统。(房间大小是 VR 体验的设计范例,支持用户通过在 VR 环境中反映的真实动作,在游戏区域内自由走动。)当您在房间范围内移动时,LOD 切换会破坏 VR 沉浸感。Unity 软件系统还提供混合功能,以便在交换模型时使效果看起来不那么刺眼。还有,这在 VR 中感觉非常奇怪。出于这些原因,最好在 Unity 软件内部用 C# 编写一个具有所需功能的简单 LOD 系统。

LOD 系统根据与头盔的距离让对象进行切换,但利用运动系统(在本案例中为远距传送(虚拟导航))可消除持续的距离检查。它只会在远距传送时切换模型,在室内移动时不会出现 LOD 切换。这样降低了性能成本,也无需混合。

用 C# 编写的简单事件系统将 LOD 系统绑定至远距传送系统,如以下示例代码所示。切换 LOD 网格的方法以距离为基础,并且订阅了包含在 LOD 管理器脚本中的事件。每个对象都负责订阅 LOD 管理器事件,因此您无需跟踪它们。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LODManager :MonoBehaviour {
	public static LODManager Instance { get; private set; }
	public delegate void LODChange();
	public static event LODChange LODrefresh;

	void Awake()
	{
    	if (Instance != null && Instance != this)
    	{

        	Destroy(gameObject);
    	}

    	Instance = this;

	}

	public void UpdateLOD()
	{
    	LODrefresh();

	}
}

LOD 管理器被写成一个单例(一种软件设计模式,将类的实例化限制为一个对象)。您可以通过游戏代码随时轻松访问 LOD 管理器。在以下示例中,它正处于远距传送期间。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LODManager :MonoBehaviour {
	public static LODManager Instance { get; private set; }
	public delegate void LODChange();
	public static event LODChange LODrefresh;

	void Awake()
	{
    	if (Instance != null && Instance != this)
    	{

        	Destroy(gameObject);
    	}

    	Instance = this;

	}

	public void UpdateLOD()
	{
    	LODrefresh();

	}
}

在以下代码中,绑定至每个对象 LOD 组的 LOD 系统包含一个简单的距离检查功能并订阅 LOD 管理器。

public class LODSystem :MonoBehaviour
	{

    	public MeshRenderer[] lods;
    	public float[] distances;
    	GameObject SceneCamera;
    	private float distance;

    	private void OnEnable()
    	{
        	LODManager.LODrefresh += DistanceCheck;

    	}
    	private void OnDisable()
    	{
        	LODManager.LODrefresh -= DistanceCheck;
    	}

    	void  Start ()
    	{
        	SceneCamera = GameObject.FindGameObjectWithTag("MainCamera");
        	DistanceCheck ();


    	}

    	public void DistanceCheck()
    	{

       	/distance = Vector3.Distance (SceneCamera.transform.position, transform.position);

        	int n = 0;
        	for (int i = 0; i < lods.Length; i++)
        	{
            	lods[i].enabled = false;
            	if (distance > distances[i])
                	n = i;

        	}

        	lods[n].enabled = true;

    	}
	}

simple distance-check function

图 7.我们绑定至每个对象 LOD 组的 LOD 系统包含一个简单的距离检查功能并订阅 LOD 管理器。

自动 LOD

Simplygon 是迄今为止最好的游戏开发工具之一。最近被微软收购,这种自动 3D 优化工具为团队节省了数百小时的游戏开发时间。在被微软收购后,这个神奇的工具现在免费提供,它可提供多种用于优化 3D 模型的工具。尽管 Simplygon 提供 C ++ API,但该项目使用的是 Simplygon UI — Simplygon 的图形界面。您只需注册 Simplygon 即可获得 SDK 下载许可。

Simplygon SDK download page

图 8.Simplygon* 下载页面。

从 Simplygon UI 中,选择 Local。(网格选项是支持 Simplygon 客户端使用中央服务器。)界面使用起来非常简单;将模型拖放至场景视图中就可以进行加载。

使用右侧面板中的工具处理不同的 LOD。

Simplygon* user interface

图 9.Simplygon* 用户界面有不同的 LOD 配置文件和优化工具选项。

您可以一次激活多个 LOD 配置文件,并为每个配置文件选择不同的优化参数。为简单起见,项目使用缩减组件,减少多边形的数量,同时保持相同的 UV 布局。

务必谨记,原始 NASA 探测器有 404,996 个多边形,基本上是小物体和部件的一个集合。如果这些部件不能移动或相互作用,唯一的结果就是导致绘制调用过量,如前所示。如果要最大限度地减少绘制调用的数量,需要使用 Simplygon 聚合组件来组合所有对象,并将绘制调用减少至一次。同时,启用材料烘焙组件,并选定保持相同 UV 布局的选项。组合对象并将绘制调用减为 1 之后,可以继续使用缩减组件来生成 LOD。

Simplygon user interface

图 10.Simplygon 聚合组件将绘制调用减少为 1。

如果您希望 LOD0 能够互动,可以组合更高的 LOD 级别,不过这取决于您的游戏设计。

Simplygon triangle reduction dialog

图 11.Simplygon 中的三角形缩减选项。

图 11 中的 Simplygon 屏幕显示了使用缩减组件的三角形目标。您可以同时计算所有这些。在 NASA 探测飞行器的例子中,分别在 200k、50k、10k、1k 和 400 的位置生成五种不同的 LOD。

在图 12 中,大家可以看到将缩减应用于组合网格后的最终结果,和 Unity 软件渲染的一样。

model rendered in Unity* software

图 12:在 Unity 软件中渲染的最终结果。

创建较低的多边形 LOD 时,您可能需要稍微调整参数。对于最远的 LOD,您需要增加 Simplygon 中轮廓的特征重要性。

object after increasing the silhouette importance to high

图 13.左侧,增加轮廓重要性之后的对象。

在本示例中,将轮廓增加到会生成视觉效果更好的模型,因为我们最关心最低的多边形 LOD 的大特征。

Settings editor

图 14.轮廓设为高。

如果要了解使用这些 LOD 与 LOD 系统脚本对性能的提升,可使用实时灯辨别实际的性能提升。您需要在内部创建 LOD 级别不同的预制件,然后根据您的级别和设计为 LOD 系统选择一些参数。

:LOD system configuration screen

图 15.LOD 系统配置。

预制件中的所有网格渲染器都将被停用,因为它们将由 LOD 系统控制。您可以轻松地放入 25-32 艘宇宙飞船且不会出现掉帧,然后近距离检查这些船,不会出现任何掉帧现象。

multiple rendered vehicles

图 16.近距离观察 25-32 艘飞船且没有出现掉帧。

这比组合网格的效果提高了 3-4 倍。将光照贴图与 LOD 系统相结合,可实现真正的性能提升。

批处理和实例化

最新显卡在遇到性能瓶颈之前可处理大量的绘制调用,因此您通常会由于几何体过多而碰壁。但在尝试将许多对象放在屏幕上时,绘制调用通常是一个问题。如果您曾操作过无法处理多次绘制调用的平台,例如移动 VR,那么请牢记这一点。即使使用组合网格,也缩减为一次绘制调用对象,项目也已达到多边形计数的硬限制。实际上,你应该平衡多边形计数和绘制调用。可以有少量多次实例化的多边形计数网格,还可以增加绘制调用直到它们成为瓶颈,或者倒过来,如前所示。

屏幕上显示的多边形计数将取决于创建的 LOD 和选择用来分布模型的距离。需进行一些实验,而且每个游戏都不同;但如果您想要较多的多边形计数,那么需要减少绘制调用的次数。批处理技巧用于在将绘制调用发送至 GPU 之前对绘制调用进行组合。具体做法是批量组合 CPU 中的网格信息。Unity 软件有三种批处理(组合)方法,以减少场景中的绘制调用次数:

  1. 静态批处理在构建时将大型网格中的静态对象网格组合起来,并在运行时以每个网格一批的形式进行渲染。
  2. 动态批处理找到小网格,并将它们批处理成更少的绘制调用。
  3. GPU 实例化并不完全是批处理,因为它全部在 GPU 中完成。这种技巧绘制相同的网格,但属性不同,例如变换。您可以进行调整,在 GPU 中添加每实例数据(如颜色和变换),以便 CPU 一次性发送网格数据,并按照实例发送每个实例的数据。

静态批处理

静态批处理仅适用于在运行时标记为静态批处理的静态网格物体。引擎计算它们的贡献、组合网格,并减少类似对象的绘制调用。尝试操作这类批处理时,您需要考虑几点。例如,您需要使用相同的材料,单在某些情况下可能不利。更多信息,请参阅 Unity 文档中的绘制调用批处理

静态批处理没有用,因为您希望在运行时实例化这些对象。

动态批处理

动态批处理适用于运行时,但它有几个限制。不过,它可能适用于本项目中的模型。

multiple rendered vehicles closing to horizon

图 17.Unity 软件内的批处理效率分析。

在这种情况下,动态批处理似乎不起作用。Unity 帧调试器是一个很棒的工具 — 类似于英特尔 GPA,但特定于引擎 — 可以帮助您查明批处理不起作用的原因。在调试器中,您可以分析每次针对渲染特定帧进行的绘制调用。

Frame debugger screen

图 18.帧调试器指出无法对绘制调用进行批处理的原因。

您可以看到,调试器抱怨是因为光照贴图。批处理不起作用还有许多其他原因,帧调试器会显示大多数(若非全部)原因。

GPU 实例化

如前所述,GPU 实例化允许 GPU 执行大部分工作。CPU 仅发送一次网格信息,但支持性能消耗较小的每实例参数。

在 Unity 软件中,这通过将其材料中的对象标记为 GPU 实例来实现。您只能将这种方法应用于共享相同网格和材料的对象。默认情况下,Unity 标准着色器中唯一的每实例数据是变换。您可以创建自己的着色器来添加其他变体。此项目使用支持 GPU 实例化的标准着色器。

shader dialog highlighting enable gpu instancing

图 19.Unity *软件中启用 GPU 实例化。

有关 GPU 实例化的说明,请参阅有关 GPU 实例化的 Unity 软件文档。

debugger report of time saved

图 20.调试器报告批处理节省的时间。

图 19 显示 GPU 实例工作奇迹!您可以看到 Unity 软件支持批处理。

组合技巧

现在这个项目结合使用了上述所有技巧。将光照贴图脚本添加到预制件,您可以设置场景并在同一个方向烘焙所有 LOD 级别。

:Bake Prefab Lightmaps menu item

图 21.烘焙预制光照贴图在同一个方向烘焙所有 LOD 级别。

计算完成后,所有光照贴图的预制件都准备就绪。您可以制作一个包含上述所有模型的预制件,并为其提供和之前相同的参数。

请注意,使用之前制作的光照贴预制件制作预制件效果很好,因为光照贴图的预制件包含序列化的正确信息。但如果您要重新设置素材,必须手动重新制作 LOD 预制件。希望通过在 Unity 软件中包含嵌套预制件,可以很快完成这一额外步骤。

在使用光照贴图 LOD 和 GPU 实例化材质设置预制件后,您可以继续构建场景。根据运行时生成内容的目标,制作一个简单的 spawner 脚本,以在运行时实例化预制件。

public class SpawnerTest :MonoBehaviour {

    public float spacing;
    public int linearamount;
    public GameObject prefab;

	// Use this for initialization
	void Start () {

        StartCoroutine(Spawning());

	}

    public IEnumerator Spawning() {

        for (int i = 0; i < linearamount; i++)
        {

            for (int j = 0; j < linearamount; j++)
            {

                GameObject p = Instantiate(prefab, new Vector3((float)i * spacing, 0f, (float)j * spacing), prefab.transform.rotation) as GameObject;


            }

            yield return null;

        }


    }


}

操作实例化对象的距离,以将屏幕上的多边形数量维持在一个 Unity 软件可以显示的值。结果真是令人印象深刻。您可以轻松实例化超过 10,000 艘宇宙飞船,无任何丢帧情况,并且玩家不会看到 LOD 切换。

但为什么要被大约 10,000 艘船束缚?本项目中应用的技巧确实限制了屏幕上显示的多边形数量。原则上,您可以期望对象的数量更大;从理论上讲,因为使用我们的技巧的多边形总输出量有限,因此对象的数量和您期望的一样大。事实上,可以达到这一目标,但需要进行大量次要的脚本优化。无论相信与否,本案例中的瓶颈在 LOD 系统中。尽管它的编程方式清晰明了,但在切换 LOD 模型时会产生巨大的性能峰值。每次玩家远距传送时,都会更新数千个对象,而且距离的计算会降低 Unity 软件的性能、内存管理和分配。

第一个尝试的优化是将计算从 “Vector3.Distance” 改为 “Vector3.sqrMagnitude”。这种更改没有害处,还会提供一个很好的提升空间,因为执行平方幅度计算要便宜得多。但系统仍然存在缺陷,因为场景中的所有对象都需要针对每次远距传动进行距离计算。这可以包含几千个对象,但当你试图放入越来越多的对象,这个点就没有实际意义了。你只需要一些对象来切换他们的 LOD 属性 — 那些接近玩家的对象。其余只需保持最低的 LOD 设置。

有一项测试可帮助提升性能,就是将对象订阅到 LOD 管理器和取消订阅,具体取决于与播放器的距离。当远距传送到新位置时,找到离玩家更近的对象,将它们的方法订阅到 LOD 管理器代理,并更新它们的 LOD。您将发现,看起来似乎是性能峰值。查看 Unity 分析器找到罪魁祸首。

display of performance spikes

图 22.Unity* 分析器显示为何出现明显的性能峰值。

使用 C# 订阅和取消订阅事件或委托似乎是一项繁重的操作,而且峰值比以前更糟糕。最好的方法是完全忘记 LOD 管理器。相反,执行靠近玩家的模型扫描,并直接更新其 LOD,如下所示:

void Teleport(Vector3 whereto)
	{
    	//Do some cool teleport code here
    	//...

    	Collider[] objs = Physics.OverlapSphere(transform.position, 100f);

    	for (int i = 0; i < objs.Length; i++)
    	{

        	LODSystem lod = objs[i].GetComponent<LODSystem>();

        	if (lod != null)
            	lod.DistanceCheck();


    	}

	}

现在系统运行良好。你可以装载超过 250,000 艘宇宙飞船,一切似乎都很顺利。

现在,一个 gotcha 的时刻:拥有 250,000 艘船的场景在一段时间内可以完美地运行,但是在你玩一会并在围绕地图远距传送后,性能开始急剧下降。基本上,当向前远距传送时,LOD 系统在传送后更新玩家周围的对象,留下一堆非常详细的对象,而不是更新它们。每次当你远距传送时,都会留下更多不必要的高多边形模型。

更新代码,更改所有对象;和玩家与留下的对象比较接近的新对象。

void Teleport(Vector3 whereto)
	{

	//Objects close to the player before teleporting
    	Collider[] objs = Physics.OverlapSphere(transform.position, 100f);

    	//Do some cool teleport code here
    	//...
	//Objects close to the player after teleporting
    	Collider[] objs2 = Physics.OverlapSphere(transform.position, 100f);
    	for (int i = 0; i < objs.Length; i++)
    	{

        	LODSystem lod = objs2[i].GetComponent<LODSystem>();

        	if (lod != null)
            	lod.DistanceCheck();


    	}
    	for (int i = 0; i < objs.Length; i++)
    	{

        	LODSystem lod = objs[i].GetComponent<LODSystem>();

        	if (lod != null)
            	lod.DistanceCheck();


    	}

	}

250,000+ ships rendered on screen

图 23.现在场景中有 250,000 多艘宇宙飞船

图 21 中的每个小点都是一艘宇宙飞船,相比最开始只有三四艘飞船的场景,超过 25 万艘飞船的场景是很大的改进。

结论

显然这是一个夸张的例子,但这些技巧可以帮助您创建包含许多元素的大型游戏。想象一下,在虚拟现实体验中,茂密的森林和广阔的地图,这一切都是随机生成的,并添加了大量详尽的细节。您可以在游戏开发过程中使用这些技巧,创建一款出色的游戏。

有关编译器优化的更完整信息,请参阅优化通知