Main

Simulating Large Crowds In Niagara | Unreal Engine

Displaying thousands of characters on-screen in real-time has always been a challenge - until now. Unreal Engine's Niagara visual effects system opens up doors to go far beyond what was previously thought possible and this presentation by Epic's Joseph Azzam showcases how you can utilize the power and versatility of Niagara to create massive crowds in real-time, and simulate a million units! Plus, learn how to make the most out of Niagara's new features to create "smart mesh particles" that can replace basic AIs, allowing you to display an enormous amount of characters on-screen all at once. Learn more about Niagara VFX and Unreal Engine at http://www.unrealengine.com **TIMESTAMPS** 00:00 Intro 00:59 Credits 1:16 Traditional techniques 4:58 Understanding Bottlenecks 07:13 AI Classification 08:28 Particle Effects 08:50 Vertex Animation 12:29 Animation Player Module 13:25 Finish Current Animation First Module 18:17 Examples 23:34 Large Scenes 31:04 Multiplayer 31:54 Note About Performance 32:25 Future Work

Unreal Engine

3 years ago

JOSEPH AZZAM:嗨 这是一个关于用Niagara模拟大型群体的演示 我叫Joseph Azzam 是负责中东北非地区和土耳其的推广专员 在屏幕上显示许多角色向来是件难事 现在 一旦有了几百个小兵 帧率就会开始骤降 不过随着Niagara推出 开启了超越这种限制的大门 这个讲座的目的就是 帮助观众理解瓶颈和可用的变通办法 我将要充分利用Niagara的新功能 创建出智能粒子 它们可以在某些情况下取代基本的AI 这不仅能帮助你超越几百个角色的限制 还能碾压这种限制 让你能随心所欲地用无数小兵充满场景 在这个项目中 我使用了来自Quixel Megascan库的资源 在我们的虚幻商城中 我选用了VineBranch的SciFi Town 2 和Protofactor Inc.的Crow 而且我要诚挚地感谢Niagara团队的同事 和Hassan Mourad 在谈到Niagara之前 我们先来讲讲模拟群体的传统技术 并且做一个快速的试验 我们要从生成一群AI开始 不过在此之前 我要给你看看它们的逻辑 就是很简单的MoveTo: TargetActor 在这个例子中 目标Actor就
是这个模拟物理的方块 我要使用一个自定义蓝图 生成10×10的方阵 总共100个AI 现在我们以很流畅的帧率运行 没有问题 那么我们暂停一下 来增加数量 选择20×20 也就是总共400个AI 我们确实看到了帧率下降 游戏现在以30 FPS运行 这很低 不过在某些游戏里是可以接受的 我们来试试把数量再增加一点 提高到30×30 也就是900个AI 在这种情况下 我们可以看到帧率下降非常明显 FPS本身并不能指出问题的原因 所以我们要使用统计单元 它将会让我们知道 是受限于CPU还是受限于GPU 现在我们可以看到 瓶颈基本上在GPU 但是游戏线程也消耗了许多性能 为了得到更多信息 我们要使用Unreal Insights 我实际上已经在后台运行它了 它会给我们提供更多信息 让我们知道是什么导致了帧率下降 现在 如果你看看我们的游戏线程 会看到有几个问题很突出 我们可以看到 主要是角色移动组件Tick耗用了许多性能 还有各种与骨架网格体本身相关的东西 也耗用了一些性能 为了方便起见 我已经在蓝图中公开了一些变量 这样我们就能禁用某些功能 比方说 如果完全禁用动画会怎样? 这样一来 骨架就没
有任何更新了 现在如果我们按“运行” 就能看到性能恢复了一些 现在我们的运行速度在20 FPS左右了 这比先前好多了 这些小兵没有任何动画 如果我们把移动组件也禁用 又会怎样? 现在可以看到 我们的运行速度有50 FPS 不过我们的小兵全都不动了 好 如果我们用分析器来看看 可以看到 实际上移动组件耗用性能的就是 与方块的物理交互 所以我们可以禁用它 然后再次运行 现在我们的小兵又移动起来了 我们的帧率也是可以接受的 是的 只要禁用一个功能就好 所以一定要使用分析器 确定哪个功能耗用的性能最多 然后相应地优化 现在有很多东西可以跟踪 那么究竟要怎样优化呢? 很显然 我们希望小兵和这个方块交互 我们希望它们能移动 也希望它们有动画 首先 要理解瓶颈是在GPU还是CPU 又或者是绘图调用 为此要使用分析器 不要随机尝试优化所有东西 一定要经常做快速的分析 如果你的问题是多边形太多 那就使用LOD 这在虚幻引擎中只要点击几下就能生成 如果你有太多的骨架网格体要更新 那就启用动画预算分配器 它会动态地管理网格体应该怎样思考 如果是阴影成本太高 那就调整级联阴影设置 甚至可以使用胶囊体阴影 如果
生成要花几秒钟时间 那就考虑池化 就是当一个小兵死掉的时候 不是把它销毁再另外生成一个 而是复用它 办法就是在它的位置放一个网格体 重置它的变量 然后把它移动到新的位置 这可以保证为固定数量的小兵保留足够的内存 也可以大大简化内存分配过程 另一种办法是跨多个帧生成 你需要使用C++实现异步 或者在蓝图中使用一个小技巧 就是设定延迟为0 也就是延迟一帧 如果一帧中的计算太多 也可以用这样的办法 尽可能把这些计算拆分 如果Tick太多 就考虑减小Tick间隔 甚至可以考虑用计时器 如果你的小兵太多 那就群集化 不要让每个小兵分别计算自身的行动 你可以把它们分组 让它们作为一个实体移动 如果移动组件开销太高 那就禁用你不需要的功能 比如 如果我们禁用物理交互 但这是和方块交互所必需的 那就设置一个计时器 检查AI是否在方块周围的一定半径内 然后只对在这个半径中的AI启用 至于绘图调用 一切绘图调用并不是生来就平等的 有500个多边形和10块骨骼的网格体的绘图调用 和50万个多边形及300块骨骼的网格体的绘图调用是不同的 LOD在这方面可以帮到你 不过动画共享管理器也可以帮你降低它们的成本 这
里有一个在你喜欢的大多数游戏里用到的技巧 根据AI与玩家的距离 把它们分组 高级AI就是你通常构建的典型AI 中级AI就是开销比较低的 多边形比较少 动画比较简单 通常组成群集 在远距离上 就是远平面上 你只需要制造有什么事情在发生的印象就行了 在那个距离 动画的保真度可以比较低 而玩家甚至不会注意到 最后 你可以调用玩家视野之外的AI 只计算影响游戏性的功能 通过应用这套结构 你就不必为角色优化抓狂了 不需要每时每刻都计算1000个AI 可以让50个和玩家交互 其他的在外围移动 等待玩家进入足够近的距离 不过 1000是个不值一提的小数目 在这个视频中我将让你看到 怎样让远平面最大化 使它能够处理几十万个角色 同时又让他们显得足够真实和智能 可以替换掉中级AI 粒子效果可以处理许多移动的实体 而且能完美利用实体化和顶点动画 这种技术已经在游戏中运用了 通常是用于填充环境的小动物 而配合Niagara 它的表现将会好得多 在创建这种效果之前 我们先讲讲在虚幻中设置顶点动画 这是一只使用顶点动画的鸟的静态网格体 我可以更改帧材质参数 然后就能看到鸟开始动起来 我们来看看这个材质 已经设置
了典型的漫反射、法线、粗糙度 这里我用漫反射乘以粒子颜色 这可以用来增加以后的变化 下面是一个内置顶点动画模块 它有纹理包含 顶点位置 还有一个用于顶点法线 有一个MaxFrame参数设置总的动画帧数 我在这里送入要运行的动画帧 来自参数或粒子系统 看看我的纹理和烘焙过程是怎样的 我先从一个骨架网格体开始 把要使用的所有动画合并为一个动画复合 你可以看到 这个动画复合有闲置动画、行走、起飞等动画 共计281帧 然后我向3D贴图公开动画 使用虚幻自带的顶点动画工具脚本 烘焙了顶点动画纹理 你可以在文档中查看这个过程的详细说明 包括怎样设置材质 怎样进行纹理设置 以及怎样配置网格体来获得最佳结果 如果你没法使用Max 有很多工具都支持开盒即用的烘焙 比如Houdini和Blender 而且引擎团队正在考虑 把它做成引擎内部大大简化的过程 我们来创建我们的第一个Niagara发射器 我们可以选择FX 然后创建一个空系统 我要叫它BasicBird 好了 然后我们来添加一个新的空发射器 如果你看不到空发射器 请确保启用你的引擎内容 和你的插件内容 现在我们有发射器了 我们要删除Sprite渲染
器和初始化粒子 因为我不需要它们 然后添加一个网格渲染器 再选择我们的鸟 要生成一只新鸟 我需要选择“Spawn Burst Instantaneous” 并且把计数设置为1只鸟 我们还要确保这只鸟不会死 我还要做一件事 就是转到粒子设置 确保它不循环 所以我要把循环行为设置为一次 持续时间设置为无限 好了 现在我们有一只鸟了 让我们来添加一个新的自定义模块 我们想让这只鸟动起来 我们要计算它应该运行哪些帧 为此我们要使用一个很酷的新功能 它叫Scratch Pad 这是Niagara中新的可视化脚本语言 让你能够发挥HLSL的威力 所以我们来创建新的Scratch Pad模块 它将会打开这个 为此我要使用一个函数 叫Play Animation 这是一个自定义函数 它不是引擎中内置的 但是它很简单 我要快速讲解一下它 每个动画都有三个属性: 一个是状态 就是一种唯一的ID 然后是起始帧和结束帧的值 它们定义动画间隔 例如 闲置动画是状态1 它从第0帧开始 到第104帧结束 这个函数计算要运行哪个帧的办法就是 在每次Tick 它都会根据变量时间、动画帧率和运行速度增加帧 如果达到了当前
的动画结束帧 它就循环回到起始帧 如果动画发生了改变 它将重置到新的动画起始帧 我还添加了一个配置状态 在这种状态下会忽略重置已经过的帧 这可以用来在运行动画时添加时间偏移 我制作的另一个函数是Finish Current Animation 顾名思义 它的作用就是结束当前动画 我将在演示的后半部分讲到它 要使用这个函数 只要把它的输出拖进我们的Map Set就好 这样一来 它就会存储AnimationFrame、 AnimationElapsedFrames和AnimationState 但是我们要为每个粒子存储这些 所以要把名称空间从本地改为粒子 现在我们有了这个输入 就是我们已经保存的AnimationElapsedFrames 所以我们就在这里拖放 连接这个 AnimationSpeed和AnimationFrameRate 这两个是参数 只要链接它们 它们就会转换为输入 还要确保设置它们的默认值 对于帧率 我们希望是30帧 对于默认速度 就设为普通的1 先前的状态也是我们存储的状态 所以我要把状态拖到这里连接 然后在这里 我们要添加动画属性 对于闲置动画 状态将是1 它将从第
0帧开始 在第104帧结束 然后我按“应用” 回到我们的系统 现在什么都没有发生 这是因为我们需要把那些变量发送到材质 所以如果你看一下材质就知道 我们还需要发送动画帧 这个问题解决起来非常容易 只需要添加一个动态参数 对于帧 只需要搜索我们刚才创建的帧参数 就是AnimationFrame 把它拖过来就好 现在我们有了一只会动的鸟 如果我们想要好几只鸟 该怎么办? 只要转到“Spawn Burst Instantaneous” 把值增大到4就好 现在我们没有看到任何变化 因为所有鸟都是叠在一起的 调整它的简单方法就是添加方框位置 然后把这里调成0 好了 现在我们有了4只鸟 但是它们都同时运行着同样的动画 为了增加随机性 我们可以使用我创建的函数中的忽略状态 所以我们要在粒子生成时设置AnimationState 然后我们可以把它设为0 这是一个配置状态 接着我们需要设置AnimationElapsedFrames 这将是一个随机值 所以我们来添加随机范围浮点数 我们想让它从0到 因为动画有104帧 就把它除以30帧 这样就是3.46 但我们的鸟会运行不同的动画了 我这里要快速讲解一下
怎样可以得到多个动画 如果我们回到刚才创建的Scratch模块 我们可以添加一个简单的If条件 所以如果连接动画帧、起始帧和结束帧 你甚至可以方便地重命名它们 那么就叫它State 然后是StartFrame和EndFrame 我们现在可以在两种动画状态之间改换 我们可以把它设为104 这里可以设置第二个 从155到170 这是用于我们的行走动画 为了简单地确定要运行哪个动画 我们必须根据数据来检查 所以我要在这里按加号按钮 查找velocity 检查它是否等于0 如果等于0就播放闲置动画 如果不等于 那么它可能就在行走 基本上就这样 如果我们回到这里的函数 我们甚至可以设置我们的速度 这样我们就可以调节它 我只要设置不等于0的值 我们的鸟就会行走了 就是这样 我们刚刚创建了第一个有会动的动物的系统 很酷吧? 这里有一些示例 用的就是我们刚才创建的参数 这个效果显示了多个动画 从闲置到起飞再到降落 我们进入系统 专门看看动画模块 它看起来和我们刚才创建的模块很像 只不过这个模块有更多条件 根据速度、地平面 是向上移动还是向下移动 我们可以在各种动画之间切换 给你一个忠告-- 不要尝试
创建一个什么事都做的系统 它很快就会变得非常复杂 你应该共享功能 为每种情况各创建一个不同的专门系统 我们来看下一个示例 这里我们使用碰撞让鸟落到各个表面 这实现起来很简单 我们需要添加一个重力模块 然后加一个碰撞模块 这样就行了 第三个示例是创建鸟群的炫酷方法 我们可以把半径设为400单位 把鸟的数量增大到100、500 我们甚至可以增加到5000 不过请记住 你总会在某个时候碰到极限 比如50000 但是和先前使用AI与骨架网格体相比 它的性能还是要好得多 而且有办法进一步提高这个极限 如果转到动画模块 我们可以看到 它使用了Finish Current Animation First模块 使动画之间的过渡比以前方便得多 流畅得多 如果你觉得飞行动画有点太统一了 可以使用矢量场增加随机性 这个示例非常酷 Niagara可以对网格体采样 鸟能够理解树 并落在树枝上 我把树枝的顶部放在不同的材质ID中 用它作为歇脚位置的过滤器 这里还有更有趣的功能 这个孩子穿过鸟群 鸟纷纷躲开他 我们仔细看一下就会发现 我是在对孩子的骨骼采样 如果我提高采样计数 你们就能看清楚 现在来仔细看一下这个系
统 我们有两个发射器 一个对孩子采样 另一个对鸟采样 鸟发射器可以使用直接读取从孩子发射器获取信息 发射器可以相互理解和通信 这就开启了无限的可能性 我使用自定义模块让鸟避开玩家 并且在他靠近时增加它们的移动速度 如果效果做得大了 你就需要有办法调试 这是我的工作流程 我们回头来看第一个示例 这一个很简单 如果我们转到发射器 我要启用ShowDebugColors 这是我重命名过的颜色模块 我把颜色映射到一条颜色曲线 我使用动画状态作为这条曲线的指数 这样就对动画进行了颜色编码 让我能够以可视化的方式对它进行调试 Niagara中有个内置的方法 就是属性电子表格 还是来看孩子奔跑的示例 属性电子表格就是一个表格 它列出一个发射器在给定时间的所有变量 要启用它 转到“窗口” 选择“属性电子表格” 选择你要调试的发射器 然后按“捕获” 然后它就会列出所有属性 我们把所有选项都关闭 然后打开位置 按“捕获” 它就会显示当前帧的值 然后又跳转到下一帧 不过有时候你需要调试场景中已经有的发射器 这是可以做到的 只要来到系统设置 启用Force Solo 然后按“模拟” 现在当我打开电子表格 就能
看到我的效果列出来了 如果我按“捕获” 可以看到位置的变化 这很了不起 不过有时我还想看到这些数值显示在每个粒子上方 多亏了我的同事Aaron展示的一个方法 这也是可以做到的 只要添加一个用于调试的Sprite 它的材质很简单 它包含一个动态参数 我把它的指数设定为3 这样你就能访问四个用于调试的浮点数 我还把这个Sprite的位置和大小 绑定到我在这里设置的两个变量 这意味着我可以在需要时更改文本大小 在那个动态材质参数中 我连接了四个浮点数 我可以在发射器中的任何位置访问它们 在这个示例中 我要监视速度 现在我可以看到那些值了 不过现在它们都等于0 因为鸟已经落地了 由于这个模拟运行速度很快 很难看清这些数值 因此我们可以使用一个实用工具 把时间膨胀设置为1/10秒 然后激活效果 我们现在看到它以慢动作运行了 很酷吧? 这样一来就容易看清了 在你完成工作以后可以把时间调回去 在这个场景中 我们有大约50万个小兵 它们将要与另外50万个小兵战斗 所以屏幕上总共有100万个小兵 现在的瓶颈是多边形计数 我们快速看一下这里 初始网格体是4000个多边形 为了解决这个问题 我只要使用LOD
在一定的距离上 我把我的网格体替换成400个多边形的 我们很难看出差别 然后我更进一步 在背景中 我用一些公告板来替换它 请注意这里 它们在屏幕上只占据一个像素 所以玩家根本注意不到 这会带来巨大的性能增益 实现这个的方法 我进入发射器给你看 基本上我做的就是 增加了三个不同的渲染器 一个用于高级网格体 一个用于低级网格体 还有一个用于公告板 然后为它们分配不同的渲染可视性标记 据我给它们的可视性标记 我可以使用我制作的一个自定义模块 在它们之间切换 这很简单 基本上我做的就是 检查粒子位置和摄像机位置之间的距离 然后我根据这个距离设置可视性标记 至于公告板本身 如果我们双击就可以看到 它就是个基本的Flipbook 我实现它的办法就是 进入Sequencer 将网格体本身渲染到图像序列中 然后创建我自己的Flipbook 你可以做的另一个优化调整是 找到你的网格体渲染器 启用摄像机距离剔除和视锥剔除 这将会隐藏摄像机视野以外的所有网格体 同时还会隐藏一定距离外的网格体 这会给你带来巨大的性能提升 我还要讲一下我怎样实现移动 为了处理这么多小兵 我创建了一个链式系统 基本上 每个小兵
都持续跟踪其前方小兵的ID 它将使用Directory 设法与前方小兵保持一定距离 要猜测ID是非常容易的 因为我们是在网格中生成小兵 所以如果有一个小兵决定离开这个链条 它不会立即离开 它会生声明自己是要离开的小兵 然后等待几帧 这点时间足够让它身后的小兵 与自己领队的领队关联起来 我们可以在这个模块中看到 它叫Kill Unit 因为它对我来说已经死了 总之 这个小兵请求被杀死 然后 在几帧之后 它将会死去 我们在这段时间里基本上就是作检查 嘿 你是不是请求被杀死? 好的 你的领队ID是什么? 再给我更多关于你自己的信息 总之这就是会发生的情况 我来让你看看这个过程 一旦我操纵我的角色射击几个小兵 就会发生这样的情况 我们来看吧 哦 这太远了 现在好了 所以一旦我射中一个小兵 后面的小兵会占据它的位置 而这个出队的小兵会成为我可以交互的AI 我们再来做一次 好 又一个 现在有两个AI跟着我 这个过程实际上分成两步 一步是由Niagara发起的 而另一步必须由蓝图本身发起 我有一个蓝图叫Crowd Manager 它会跟踪在这个效果中发生的许多信息 比方说 一旦我开枪射击 它就会跟踪
子弹位置 并且把它发送到粒子效果 另一种情况是 如果我射出爆炸弹 比方说我进入游戏 射出一发火箭 转眼间 这个信息也会传输给Niagara 可以发生的另一种情况是 我靠近小兵 小兵会避开我 如果我让它们跑起来 请注意 它们会绕开我跑 我可以跑到它们身边 当我靠近一个小兵时 它就会开始往边上移动 我们再做一次 这样我就能和这些小兵一起动 而不会和它们碰撞 这和我前面展示的孩子穿过鸟群的效果很像 所有这些信息都是由蓝图发送的 它会发送玩家位置、爆弹位置 子弹位置 甚至指挥领队 我们的链式系统的一个组成部分就是主单位 就是这里的领队 负责指引身后的所有小兵 蓝图实际上会命令不同队伍的各个领队聚到某个位置 然后开始互相战斗 Niagara还有一个非常有趣的功能: 不仅是蓝图能够与Niagara通信 Niagara也可以回话 我们前面已经看到了 当我射击一个小兵 在它的位置会生成一个AI 这个过程很简单 你要做的就是使用回调句柄 我们回到Niagara 看看Activate AI 这个函数本身没有多少内容 它就是检查 嘿 子弹是否靠近了我的粒子 很好 那么请发送关于位置的信息 虽然这里的名称是S
ize 但它可以是你需要的任何浮点 在这个例子中 我发送的是动画帧 我还发送比例 有了这三个信息 我就回到我的蓝图 它将触发一个事件 这里它告诉我 嘿 我被击中了 请在我的位置生成AI 于是我就生成一个AI 我会设置它的位置、旋转和比例 我也可以匹配动画 因为现在我可以读取动画帧了 我可以判断它现在运行的是哪个动画位置 并在完全相同的位置生成AI 这很酷 但是如果我想在多人游戏中做这种效果 该怎么办呢? 这里有几个要点需要记住 视觉效果在传统上属于美学方面 它们不会在服务器上运行 但是 因为在我们的示例中 我们使用了蓝图来驱动模拟 所以可以通过复制馈送数据的蓝图来实现多人游戏 不过有几点你必须记住 重要的游戏逻辑还是必须在蓝图这边运行 Niagara只能用于视觉表现 Niagara函数在必要的时候 必须有某种形式的决定性结果 从Niagara调用蓝图是不可能的 例如 以我们的子弹命中检测为例 它就必须在蓝图这边运行 另一个要记住的事项是性能 如果你有100万个小兵 就需要检查你的内存使用情况 假设你给你的粒子添加了一个整数变量 它是N32 也就是32位的 现在把它乘以100万 你要使用
31.25兆字节的VRAM 所以每添加一个变量 都会有相当大的影响 要尽量控制好 做这个项目很开心 我有许多功能想添加进去 但是我的演示总得结束 说到顶点动画 我在每一帧烘焙顶点位置 这个技术非常适合用于简单的效果 但如果你想要一些特定的功能 比如动画混合 甚至在不同的网格体之间共享动画 那么你可能要研究把骨骼数据烘焙到纹理中 并且单独做一个纹理 把顶点绑定到相应的骨骼 实际上John Lindquist做了一个脚本 叫StaticMeshSkeletalAnimation.ms 你已经可以在引擎的Extra文件夹中找到它 还有一件事情我没说 就是从枪射出的子弹 这很容易实现 我只需要把子弹添加到小兵网格体中 把它的运动烘焙到小兵射击动画中就行了 Neighbor Grid是我没有机会尝试的功能 它很酷 可以让你知道周围粒子的位置 值得一提的是 如果把它用于全部100万个单位 消耗会很大 但如果你在战斗区域周围设置体积 它将会帮助你改善场景 让你的小兵散开 仅在那个特定位置与各个方向的小兵交互 这就是我的演示 真心希望你学到了一点东西 如果你有什么问题 总是可以在推特上联系我 感谢观看

Comments