Quantcast
Channel: 英特尔开发人员专区文章
Viewing all articles
Browse latest Browse all 583

使用 Unity* 进行并行处理的一种方法

$
0
0

项目理念

该项目的理念是展示如何使用 Unity* 对游戏进行并行处理,以及如何使用游戏引擎执行与游戏相关的物理。在这个领域内,现实感是成功的一个重要标志。为了模拟真实世界,许多动作需要同时发生,这需要并行处理。创建两个不同的应用,然后将它们与单个内核上运行的单线程应用进行比较。

第一个应用在多线程 CPU 上运行,第二个应用在 GPU 上执行物理计算。为了显示这些技术的结果,新开发的应用展示了使用群集算法创建的鱼群。

群集算法

多数群集算法依赖 3 大规则:


图 1.3 大群集规则描述(资料来源:http://www.red3d.com/cwr/boids/)。

什么是群集?

在本示例中,一个群集被定义为一群鱼。如果每条鱼与鱼群里的其他鱼都保持一定的距离,经过计算得出,该鱼在一个鱼群里“游动”。鱼群的成员只能以群集成员的身份行动,不得单独行动,它们共享相同的参数,如速度和方向。


图 2.包含 4 条鱼的鱼群。

复杂度

该算法的复杂度为 O(n2),其中,n 为鱼的数量。为了更新单条鱼的移动,算法需要查看环境中的所有 n 条鱼,以确定鱼是否可以:1) 留在鱼群;2) 离开鱼群;或者 3) 加入新鱼群。单条鱼可能单独“游”一段时间,并有机会加入新鱼群。这需要针对每条鱼执行 n 次。

算法如下所示:

For each fish (n).

Look at every other fish (n).

If this fish is close enough.

Apply rules: Cohesion, Alignment, Separation.

使用 C# 实施群集算法

为了将规则应用于每条鱼,创建了一个 Calc函数,它需要一个参数:环境中现有鱼的索引。

数据被存储于两个缓冲区,以表示每条鱼的状态。交替使用这两个缓冲区进行读写。需要这两个缓冲区在内存中保存每条鱼之前的状态。然后,使用该信息计算每条鱼的下一个状态。每一帧前,读取当前读取缓冲,以更新场景。


图 3.功能流程框图。

鱼的状态

每条鱼的状态包括:

fishState {
	float speed;
	Vector3 position, forward;
	Quaternion rotation;
}


图 4.包含 fishState 的鱼。

forward变量包含了鱼面对的方向。

rotation变量是一个表示三维旋转的四元数,支持鱼旋转以面对目标方向。

群集算法

本项目使用的完整的群集算法为:

邻近状态

执行群集算法后,每条鱼都会被确认为鱼群成员或非鱼群成员。

将两条鱼之间的距离和每条鱼的前进方向用作参数,创建 neighbor 函数。这样做旨在使行为更逼真。如果两条鱼的距离足够小,并且沿着相同方向行进,它们有可能合并。但是,如果它们没有沿相同的方向移动,便不太可能合并。使用分段二次函数和前向矢量的点积创建该合并行为。


图 5.数学函数的表示。

两条鱼之间的距离必须小于最大距离,后者根据前向矢量的点积不断变化。

调用繁重的 Neighbor函数前,需要调用另一个函数:Call。Call函数会通知算法是否需要通过 Neighbor函数来确定两条鱼是否足够接近,能进入同一个鱼群。Call函数只检查这些元素(鱼)相对其 x 位置的位置。x 位置是首选位置,因为它拥有最宽的尺寸,支持鱼广泛分布。

状态更新

如果只有 1 条鱼,它会在特定速度范围内沿某个方向前进。如果鱼周围有同伴,它将调整自己的方向和速度,以适应鱼群的方向和速度。

速度总是以线性的方式变化,以提高平滑度。速度不会在没有过渡的情况下跳至另一级。

我们界定了一个环境。不允许鱼游到该环境的空间范围之外。如果鱼触碰了边界,将转向,并回到界定的环境中。


图 6.群集行为。

如果鱼将要游出边界,为鱼随机指定一个新的方向和速度,使其仍在界定环境以内。


图 7.边界行为。

检查鱼是否会撞上岩石也十分必要。该算法需要计算鱼的下一个位置是否在岩石内。如果是,鱼将以类似于避开边界的方式避开岩石。

一旦计算出状态,将指示所有鱼开始“游动”,并进行必要的旋转。然后,更新每条鱼的下一个状态,如方向、速度、位置和旋转变量(面向 n 条鱼)。该操作在每次更新帧时发生。

例如,为所有鱼的位置添加了方向,以确保鱼在环境内“游动”。

Unity* 内的集成

Unity 内的主要编程组件为 GameObject。您可以在 GameObject 内添加不同的元素,如待执行的脚本、碰撞器、纹理或材料,以自定义对象,使其按预想的方式运行。然后,可以在 C# 脚本内便捷地访问这些对象。脚本内的每个公共对象将在编辑器内创建一个字段,支持您放置满足要求的对象。

使用 C# 脚本创建群集行为。

将资产导入 Unity

  1. 单击 Assets,,单击 Import package,然后单击 Custom package。 
  2. 单击 All。
  3. 单击 Import。

接下来,将 Main场景从 Project 选项卡拖放到 Hierarchy 选项卡。右键单击默认场景并选择“Remove Scene”。

运行应用所需的所有游戏对象和附带的脚本均可以随时运行。唯一缺少的部分是岩石模型,这个必须手动添加。

从 Unity 资产商店下载“Yughues Free Rocks”。可以在 Unity 内访问资产商店(或使用该链接:http://u3d.as/64P访问)。下载完成后,将出现左侧的窗口。选择“Rock 01”并导入。

由于默认模型的规模太大,需要在使用岩石模型前进行调整。网格导入设置的缩放系数应重新调整为 0.0058。将岩石添加到场景后,如果其规模为 1,将匹配比例为 1 的三维球体,后者被用作面向对象的碰撞器。


图 13.分割矩阵阵列-计算变量。

计算变量:nbmat表示应用使用的矩阵队列数量;rest表示最后一个矩阵中鱼的数量。

更新每条鱼:表示矩阵索引;j表示当前数据块内鱼的索引。只有这样做,才能更新上述阵列内的正确矩阵。

其他特性

水下效果

为了创建多种水下效果,在 Unity 项目中添加了不同的资产。Unity 提供了多款内置软件包,包括本项目使用的水模型。还提供了适用于任意对象的多种纹理和相关材料(“皮肤”)。上述全部(甚至更多)资产可在 Unity 资产商店中获取。

焦散-光反射与阴影

在该项目的水下场景中添加了焦散光照效果。焦散需要一个“投影器”(projector,一种 Unity 对象)。投影器显示场景中的焦散效果。通过假设特定的频率(Hz)改变投射的焦散,提供一种焦散正在移动的效果。

模糊

在水下场景中添加了模糊效果。如果摄像头位于水平面以下,将启用并显示渐进模糊。场景的背景将变为蓝色。默认背景为天空背景(天空盒)。此外,在 Unity 内启动了雾设置。(Microsoft Windows*, 光照,其他设置,已检查雾盒)。

移动摄像头

在摄像头对象中添加脚本,以支持使用键盘和鼠标在场景内移动。这种操作提供了类似于第一人称射击游戏的控制。可以使用方向键进行前进/后退、向左/向右扫射。鼠标支持上/下移动,以及转动摄像头,以对准左侧/右侧。

transform.Rotate (0, rotX, 0);

move变量表示方向键输入,rot* 表示鼠标方向。修改保留脚本的对象(在本示例中为摄像头)的转换,使其在场景中旋转与平移。

创建 .exe 文件

如前所述,在不修改源代码的情况下,可以构建一个 .exe 文件,以改变应用的参数。请按照以下步骤操作:

  1. 单击:Edit ,并单击  Project Settings ,然后单击  Quality。
  2. Quality 选项卡中,向下滚动至 Other,,并找到 V Sync Count
  3. V Sync Count设置改为 “Don’t Sync”。这可能使应用显示 60fps 以上的帧速率。
  4. 单击:File ,然后单击  Build and Run,以创建 .exe 文件。

注:除了使用 Build and Run, 之外,您也可以访问 Build Settings ,以选择特定平台(如 Microsoft  Windows、Linux*、Mac* 等)。

编码差异:CPU 对比 GPU

CPU

面向单线程和多线程应用的编码之间只包含一个差异:Calc 函数的调用方式。在本示例中,Calc 函数对执行时间至关重要,因为每一帧它都被调用 n 次。

单线程

如下所示,通过一个“for 循环”以传统的方式完成面向单线程应用的编码:

多线程

使用“Parallel.For”类完成面向多线程应用的编码。Parallel.For 类的作用是分割多个函数调用,并在不同的线程中并行执行这些调用。每个线程包含需要执行的多个调用。当然,应用性能取决于 CPU 可用内核数量。

GPU

计算着色器

GPU 处理以类似于 CPU 多线程处理的方式完成。通过将流程繁杂的 Calc 函数迁移至 GPU(GPU 的内核数量多于 CPU),将更快地预测结果。为此,在 GPU上使用并执行“着色器”。着色器为场景添加图形效果。本项目使用了“计算着色器”。使用 HLSL(高级着色器语言)编写计算着色器。计算着色器复制Calc函数的行为(如速度、位置、方向等),无需计算旋转。

使用 Parallel.For函数的 CPU 调用面向每条鱼的 UpdateStates函数,以便在绘制每条鱼前,计算其旋转并创建 TRS 矩阵。使用“四元数”类的 Unity 函数 Slerp计算鱼的旋转。

根据计算着色器调整代码

尽管主要思路是将 Calc 函数循环迁移至 GPU,仍需要考虑以下几点:随机数生成以及需要和 GPU 交换数据。

面向 CPU 的 Calc函数与面向 GPU 的计算着色器之间最大的差异在于随机数生成。在 CPU 中,使用来自 Unity 随机类的对象生成随机数。计算着色器中使用的是 NVidia* DX10 SDK 函数。

需要在 CPU 和 GPU 之间交换数据。

某些应用参数(如鱼或岩石的数量)包含在浮点矢量或单个浮点中。例如,CPU 中来自 C# 的 Vector3 将匹配 GPU 上以 HLSL 编写的 float3 的内存映射。

读/写缓冲中的鱼状态数据(fishState)和 CPU 中第三个缓冲区上的岩石状态数据(s_Rock)必须在 GPU 上被定义为计算着色器的 3 个不同的 ComputeBuffer。例如,CPU 上的四元数匹配 GPU 上 float4 的内存映射。(四元数是一种包含 4 个浮点的结构。)读/写缓冲在 GPU 上的计算着色器中被声明为 RWStructureBuffer <State>。在 CPU 上描述岩石的结构与之类似,使用浮点表示每块岩石的大小,使用包含 3 个浮点的矢量表示每块岩石的位置。

CPU 上的 RunShader函数创建了 ComputeBuffer 状态,并调用 GPU,使其在每帧开始时执行计算着色器。

<img data-fid="613900" data-cke-saved-src="/sites/default/files/managed/b7/7a/An-Approach-to-Parallel-Processing-with-Unity-code09_0.png” typeof=" src="/sites/default/files/managed/b7/7a/An-Approach-to-Parallel-Processing-with-Unity-code09_0.png” typeof=" foaf:image"="">

在 CPU 上创建 ComputeBuffer 状态后,对它们进行设置,以匹配 GPU 上相关的缓冲状态(例如,CPU 上的读取缓冲与 GPU 上的“readState”相关)。然后,使用鱼状态数据对两个空缓冲区进行初始化,执行计算着色器,使用与写入缓冲相关的 ComputeBuffer 中的数据更新写入缓冲。

在 CPU 上,Dispatch 函数设置并启动 GPU 上的线程。nbGroups表示在 GPU 上执行的线程组数。在本示例中,每组包含 256 个线程(一个线程组所包含的线程数不能超过 1,024 个)。

在 GPU 上,“numthreads” 属性必须与 CPU 上建立的线程数一致。如下所示,“int index = 16*8*8/4”提供了 256 个线程。需要将每个线程的索引设置为每条鱼相应的索引,每个线程需要更新每条鱼的状态。

结果


图 14.鱼数量较少时的结果。

结果显示,当鱼的数量少于 500 条时,相比 GPU,单线程与多线程 CPU 的性能更高。这可能得益于在 CPU 与 GPU 之间逐帧完成的数据交换。

当鱼的数量达到 500 条时,相比多线程 CPU 和 GPU,单线程 CPU 的性能降低(单线程 CPU = 164fps 对比多线程 CPU = 295fps,GPU = 200fps)。当鱼的数量达到 1,500 条时,多线程 CPU 的性能降低(单线程 CPU = 23fps,多线程 CPU = 88fps 对比 GPU = 116fps)这可能是因为 GPU 拥有更多内核。

当鱼的数量大于等于 1500 条时,GPU 的性能在所有情况下均优于单线程和多线程 CPU。


图 15.鱼数量较多时的结果。

尽管在所有情况下,GPU 的性能优于两个 CPU 实例的性能,结果显示当鱼的数量为 1,500 条时,整体 GPU 性能最佳(116fps)。随着鱼数量的继续增加,GPU 性能随之下降。即便如此,当鱼的数量为 2000 条时,只有 GPU 的性能超过 60fps,鱼的数量为 2500 条时,GPU 的性能超过 30fps。当鱼的数量约为 6500 条时,GPU 最终降至 30fps 以下。

随着鱼数量的增加,GPU 性能下降的最可能的原因是算法复杂性。例如,当鱼的数量为 10,000 条时,每条鱼需要进行 10,0002 ,即 1 亿次迭代,以在每一帧中与其它鱼进行交互。

对应用进行分析后,需要强调应用内的几个关键点。用于计算每条鱼距离的函数非常繁重,点积导致 Neighbor函数速度变慢。将 Neighbor调用替换为两条鱼之间的距离(必须小于最大距离)将小幅提升性能。这意味着邻近的任意两条鱼现在将向同一个方向游动。

关注算法的 O(n2)复杂度是提升性能的另一种方法。面向鱼的替代整理算法可能会改进性能。

(假设两条鱼:f1 和 f2。面向 f1 调用 Calc函数后,将针对 f2 计算 f1 的 Neighbor状态。保存 Neighbor状态值,以在稍后面向 f2 调用 Calc 函数时使用。)

该性能指标评测使用的硬件


图 16.运行测试所用的硬件。


Viewing all articles
Browse latest Browse all 583

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>