JOSEPH AZZAM: Hi. This is a presentation
on Niagara for simulating large crowds. My name is Joseph Azzam
and I'm the evangelist for MENA and Turkey. It's always been difficult to
have many characters on screen. Right now, once you have
a few hundred units, the frame rate
starts taking a hit. But with the
introduction of Niagara, it opens up doors
to go beyond that. This talk aims to help
viewers understand bottlenecks and the workaround to use. I'll be making the most out
of Niagara's new feature to create smart particles
that can replace basic AIs in some cases. This will not only help
you exceed the few hundred character limit, but it
will obliterate that goal and enable you to flood your
scene with as many units as you desire. For this project, I used assets
from the Quixel Megascan library. From our Unreal Marketplace,
I use the SciFi Town 2 by VineBranch and
Crow by Protofactor Inc. And I would like to warmly
thank our colleagues over at the Niagara team
and Hassan Mourad. Before jumping to
Niagara, let's go over the traditional techniques
for simulating crowds and do a quick experiment. Let's start by spawning a bunch
of AIs, but before I do that, I'm just going to
show you their logic. And right now, it's a
simple MoveTo: TargetActor, and in this case,
the target actor is the box that is
simulating physics. Using a custom
Blueprint, I'm going to spawn 10 by 10, which
is a total of 100 AIs. Right now, we're running
at a smooth frame rate, which there's no issue there. So let's stop,
increase the number, and let's go for 20 by 20,
which is a total of 400 AIs. We really see a frame drop. The game is currently
running at 30 FPS, which is low but sometimes
acceptable in some games. So let's try increasing the
number just a tiny bit more and go all the way to
30 by 30 or 900 AIs. And in this case, we can
see that the frame rate has dropped really drastically. Now, the FPS itself is
not an indicator of what's causing the issue,
so we have to use stat unit, which will
let us know if it's a CPU bound or GPU bound. And right now, we can see
that it's mostly GPU bound. However, the game thread is also
taking a lot of performance. To get more information, we have
to use Unreal Insights, which I actually have running
in the background, and it will give us more
information of what's causing the frame rate drop. Now, if you look
at our game thread, there's a few things
that stick out. Right now, we can
see that mostly it's the character movement
component ticking that is using a lot of
performance, and then also, we have all the stuff related
to the skeletal mesh itself that are using some
performance as well. So to make things
easier, I already exposed some variables
in the Blueprint so we can disable some features. For example, what if we
disable animation completely? So the skeletons are
not updating whatsoever, and now when we hit
Play, we can see that we gain some performance back. Now we're running
back around 20 FPS, which was a lot better than
how much we did before. The units are not
animating at all. Now, what if we disable the
movement component as well? We can see right now
we're running at 50 FPS, but great, none of our
units are moving, so yay. OK, now if we use the
profiler and we look into it, we can see that actually, what's
using some of the performance for the movement component
is physics interaction with the box. So we can just disable that, and
let's run the simulation again. So now our units
are moving again and our frame rate is
kind of acceptable. And yeah, just by
disabling one feature. So make sure to use the profiler
to identify which feature is using the most performance, and
based on that, try to optimize. Now, that's a lot of stuff
to keep track of, so how do you actually optimize? And obviously, we do want our
unit to interact with the box. We want them to be able to move
and we want them to animate. First, understand if you're
GPU bound or CPU bound or draw call bound. For that, use the profiler. Don't try to optimize
everything randomly and make sure to profile soon
and profile often. If your problem is
having too many polygons, use LODs, which can
be generated in Unreal in a couple of clicks. If you have too many
skeletal meshes updating, enable the animation budget
allocator that dynamically manages how they should think. If shadows costs too much, tweak
your cascaded shadow settings or even use capsule shadows. If spawning takes a few
seconds, consider pooling, which is when a
unit dies, instead of destroying it and
spawning another one, you can reuse it by placing
a mesh in its place, reset its variable and move
it to a new unit location. This will guarantee you
have enough memory reserved for a fixed amount of
units but also makes the memory allocation
process a ton simpler. Now, another method is to
spawn over multiple frames. You would need to
use C++ for async, or a trick in Blueprint
is to use a delay of 0, which is a delay by one frame. The same thing goes for
having too many calculations in one frame. Split those when possible. If you have too many ticks,
consider reducing the tick interval or even use a timer. If you have many units,
try clustering them. Instead of having each unit
calculating its movement, you can group them and move
them as a single entity. As for the movement
component cost adding up, just disable features
that you don't need. For example, when we disable
the physics interaction which was needed to
interact with the box, set a timer to check
the AI that are in a certain radius of the box
and enable physics interaction just for those. For draw calls, not all draw
calls are created equal. A draw call for a mesh with 500
polygons and 10 bones is not the same as a draw call
for a mesh with 500k polygons and 300 bones.
LODs can help with that, but also, the Animation Sharing Manager
can help you reduce their cost. Here is a technique that is used
in most of your favorite games. AIs are split into groups
based on their distance to the player. The high AIs are the typical
AIs you usually build. The medium AIs are less heavy
with fewer polygons, simpler animation, and are
usually clustered. And way in the distance
is the far plane where you only need to give
the illusion that something's happening. From that distance, animations
can be lower fidelity and the player
wouldn't even notice. Finally, you can call AIs
outside the player view, and you only calculate functions
that affect the gameplay. By applying the
structure, you don't have to do crazy optimization
on your characters. Instead of having
1,000 AIs at all times, you can have 50 that are
interacting with the player while the rest are moving
around waiting for the player to get close enough. However, 1,000 is a puny number. In this video, I
want to show you how to maximize
the far plane, make it bigger to handle hundreds
of thousands of characters, but also make them realistic
and smart enough to replace the medium AIs as well. Particle effects can handle
lots of moving entities, and it makes perfect use
of instancing and vertex animation. This technique is already
used in games, usually for small animals to
populate the environment, and with Niagara, it's
about to get a lot better. Before creating the effect,
let's go over setting up vertex animation in Unreal. Here's a static mesh of a
bird using vertex animation. I can change the frame
material parameter and notice how the
birds start animating. Let's go over this material. You got your typical diffuse,
normal, roughness set up, and here I am
multiplying the diffuse with the particle color, which
is useful for adding variation later. Below is a built-in
vertex animation module that takes texture holding, the
vertex position and another one for the vertex normal. There's a mass frame parameter
that sets the total animation frame, and here I am feeding in
which animation frame to play, whether from a parameter
or the particle system. Let's go over how I got
textures and my baking process. I started from a
skeletal mesh and I combined all the
animation I'll be using into a single
animation composite. As you can see, the
animation composite has the idle animation, the
walk, take-off, and others for a total of 281 frames. Then I exposed the
animation to 3D maps and baked the vertex
animation textures using vertex animation
tool script included with Unreal. You can check out the docs
for an extensive breakdown of the process, including
how to set up the material, setting up the texture
settings and configuring the mesh for optimal result. If you do not have
access to Max, many tools like
Houdini and Blender support baking out of the
box, and the engine team is looking into making
it a much easier process inside of the engine. Let's create our
first Niagara emitter. We can go to VFX,
and we're going to create an empty system. Going to call it Basic Bird. There we go. And let's add a
new empty emitter. If you can't see
the empty emitter, make sure to enable
your engine content and your plugin content. Now that we have
it, we're just going to delete our sprite renderer
and our initialize protocol because I don't need those,
and then add a mesh renderer, then select our bird. And to spawn a new bird,
I need to actually hit Spawn Burst Instantaneous,
and set the count to 1 bird. Also, we have to make
sure the bird doesn't die. One more thing I want to do
is go to our particle settings and make sure that
it doesn't loop. So I'm just going to
set the loop behavior to once to an infinite duration. And that's it. Now we have a bird. So let's add a
new custom module. We want this bird
to animate and we want to calculate which
frame it should play. To do so, we're going to use
the cool new feature called Scratch Pad. It's the new visual scripting
language inside of Niagara that allows you to
unlock the power of HLSL. So let's create the
new Scratch Pad module, and it will open this. And for that, I'm going to use a
function called Play Animation. Now, this is a custom function. It's not built into the engine. It is, however,
quite simple, and I'm going to quickly go over it. Each animation has three
properties, a state, which is a unique ID, and the
start frame and an end frame value defining the
animation interval. For example, the idle
animation is state 1, and it starts from frame
0 and ends on frame 104. The way the function calculates
which frame to play is each tick it will increment
the frame based on delta time, the animation
frame rate and play speed. If the current animation
end frame is reached, it will loop back
to the start frame. If the animation changes, it
will reset to the new animation start frame. I also added a
configuration state where it will ignore resetting
the elapsed frames, which is useful for
adding time offsets when playing the animations. Another function I made is
the Finish Current Animation, which does exactly
what it sounds like, and I'll be covering it
later in the presentation. OK, so to use this
function, we simply have to drag its output
into our map set. So it's going to store in the
animation frame, animation elapse frame and the
animation state, though however we want to
store those for each particles, we want to change the namespace
from local to particle. All right, now, we take this input,
the animation elapse frame which we actually saved. So we just drag and drop
here and connect this. Now, animation speed
and animation frame rate, those are parameters,
so by linking them, it will convert them to input. Also make sure to set
their default value. For the frame rate, we
want it to be 30 frames, and for the default
speed, it's going to be the regular 1 speed. The previous state is
also the state we stored, so I'm just going to drag
the state here and attach it, and then here, we just have to
add the animation properties. So for the idle animation,
it's going to be state 1. It's going to start from
frame 0 and end at frame 104. Then I hit Apply and
go back to our system. And nothing is
happening right now. That's because we need
to send those variables to the material. So if you look at the
material, we still have to send the
animation frame. Well, this is a very easy fix. Just have to add a
dynamic parameter. And for the frame, we
just have to search for the frame parameter we just
created, the animation frame, and drag it. And now we have a bird
that is animating. Now, what if we want
to have multiple birds? Well, just go to Spawn
Burst Instantaneous and increase the value to 4. We don't see anything right
now because all the birds are on top of each other. A simple way to adjust that
is to add a box location. And just kind of
zero out of this. And here we go. Now we have four birds. But however, they are still
playing the same animation at the same time, so
to add randomization, we can use that ignore state
in the function I created. So we have to do a set animation
state on our particle spawn, and then we can set it to 0,
which is a configuration state, and then we need to set
the animation elapse frame. It's going to be a random value,
so let's add a random range float. We want it to be from 0 to,
since the animation's 104 frames, divide it by 30 frames. That makes it 3.46. And now our birds are
playing different animations. One thing I want
to cover quickly is how can you have
multiple animations. So if we go back to our
Scratch module we just created, We can add a simple
if condition. So if you connect the animation
frame, start frame and end frame, you can even
rename them conveniently, so it's going to be state, and then start frame and end frame. We can now change between
two animation states. We can set it to
104, and here we can set the second
one from 155 to 170. And this is for
our walk animation. And to simply decide
which animation to play, we have to check
based on the velocity. So I'm just going to
grab the velocity here by hitting the plus button
and looking for velocity, and going to check
if it's equal to 0, then it's playing
the idle animation, and if it's different,
then it's probably walking. That's basically it. If we go back to
our function here, we can even set
our velocity just so we can actually adjust it. So if I put anything
different than 0, now our birds are
actually walking. And that's it. We just created our
first system that has animals that are animating. How cool is that? Now here are some examples
that are using this parameter we just created. This effect is displaying
multiple animations from idle to takeoff to landing. And let's go into the
system more specifically into the animation module. It looks similar to
the module we just created except this one
has more conditions. Based on the velocity,
the ground level and if it's moving
upward or downward, we can switch between
those animations. Word of advice--
don't try to create a system that does everything. That will get complex very fast. Instead, share
functionalities and create a different specialized
system for each case. Let's go into the next example. Here we are placing the birds
on the surface using collision. It is quite simple to implement. We need to add a gravity module
followed by a collision module, and you're done. The third example is a cool
way to create bird fleets. We can set the
radius to 400 units, increase the bird
count to 100, 500. Heck, we can even go through
5,000, though keep in mind, you will hit a limit at
some point, like for 50,000. It is, however, still a
lot more performant from when we had the AIs and
the skeletal mesh earlier, and there are ways to
push this limit further. If you go to the
animation module, we can see that this
effect is using the finish current animation
first module, which makes transitioning
between animations a lot easier and smoother. If you think the flying
was a little uniform, you can use vector fields
and add randomisation. This is a very cool one. Niagara can sample meshes. Birds can understand the tree
and land on its branches. I place the top of the branch
in a different material ID and used that as a
filter for where to land. And here are even more
interesting features. This kid is running
through the birds and they are running
away from him. If we take a closer
look, I am sampling the skeleton of the
kid as you can see if I increase the sample count. Now, let's take a closer
look at the system. We have two
emitters, one that is sampling the kid and the
second for the birds. The bird emitter
can get information from the kid emitter
using direct read. Now, emitters can
understand and communicate with each other, which
opens endless possibilities. Using the custom module,
I am moving the birds away from the player and
increasing their velocity as he gets closer. As your effect gets bigger, you
will need ways to do debug it. And here's my workflow. Let's go back to
our first example, and this is an easy one. If we go to the
emitter, I'm going to enable the show debug colors,
which is the color module that I renamed. I map the color to a color curve
that I use the animation state as an index. This color codes the
animation and enables me to visually debug it. A built-in method into Niagara
is the Attribute Spreadsheet. Let's go back to our
kid running example. The attribute
spreadsheet is a table that list all the variables
of an emitter at a given time. To enable it, go to Window and
select Attribute Spreadsheet. Select the emitter you want
to debug and hit Capture. It will then list
all the properties. Let's turn everything
off and turn on position. When I hit Capture, it shows
the value of the current frame and then it jumps to the
next, though sometimes you want to debug an emitter that
is already in your scene. That is possible. Simply go through the system
settings and enable Force Solo, then hit Simulate. Now when I open the spreadsheet,
I can see my effect listed, and if I hit Capture, I can
see the position changing. This is awesome,
though sometimes, I also want to see those values
displayed above each particle. Thanks to a method my
colleague Aaron showed, that is also possible. Just add a sprite for debugging. Its material is quite simple. It consists of a
dynamic parameter that I gave the index of 3. This gives you access to four
floats to use for debugging. I also bind the position
and size of the sprite to two variables which
I am setting here, which means I can change
the text size if needed. And in the dynamic material
parameter, I connected four floats that I can access
anywhere in the emitter. In this example, I am going
to monitor the velocity. Now, I can see those values. However, they are equal to
0 since the bird has already landed. And since this simulation
is running fast, it is hard to read those values. We can use a utility for that
and set the time dilation to 1/10 of a second and
activate the effect. We are now watching
it in slow motion. How cool is that? Which makes things easier,
and when you're done, you can set time back. In this scene, we have
around half a million units that is going to be fighting
another half a million for a total of 1
million units on screen. Right now, the bottleneck
was the poly count, so if we go quickly here, our
initial mesh is 4,000 polygons. To solve that, I'm
just using LODS, and based on a
certain distance, I'm replacing my mesh with
a 400 polygon one, and we can barely
see the difference. And I took it even a step
further, and in the back, I replaced it with
some billboards. And notice here how,
well, they're only occupying a pixel on screen
so it's not really noticeable and it's a huge
gain in performance. To implement that, I'm just
going to go into the emitter to show you, and
basically what I did was add in three different
renderers, one for high mesh, one for low and one
for the billboard, and then assign a different
render visibility tag for them. Based on the visibility
tag I give them, I can actually switch between
them using a custom module that I made. It is quite simple, and
basically, what I'm doing is I am checking the distance
between the particle position and the camera
position, and based on that, I am setting the visibility tag. As for the billboard itself,
if we double click it, it's a basic
flipbook, and the way I achieve that is by
going into Sequencer and rendering the mesh
itself into image sequence and then just creating
my own flipbook. Another optimization
tweak that you can do is to go to your mesh renderer
and enable camera distance culling and frustum culling. This will hide all meshes
outside of the camera view, but as well, it will hide
meshes up to a certain distance, and this will give
you a huge boost. Another thing I want to cover
is how I achieved movement. So to handle this
amount of units, I created a chain system. Basically, each unit keeps track
of the ID of the unit in front of it, and using
Directory, it will try to maintain a distance
towards that unit. It is very easy to guess
the IDs since we are spawning the units in a grid. So if a unit decides to
leave the chain, what's going to happen is instead
of leaving immediately, it is going to declare itself
as a unit that is leaving and then wait for a
couple of frames, which is enough time for
the unit behind it to attach itself to
its lead's leads. We can see that in this module
where it's called kill unit, because it's dead to me. Anyway, and then it just
requests to be killed, and then after a few
frames, it will die. And during that time,
we're basically just checking, hey, are you
requesting to be killed. OK, what's your
lead ID and give me more information about
yourself, and this is how it's going to happen. And let me show
you that process. It's happening here once
I go into my character and shoot a couple of units. So let's see. OK, this is too far. Here we go. So once I shot the unit, the
unit behind it took its place and the new unit became an
AI that I can interact with. Let's do it again. Here we go, another
unit, and now I have two AIs following me. Now, that process
is actually part of two steps, one that
is initiated by Niagara, but the other one
has to be initiated by the Blueprint itself. So I have a Blueprint
called Crowd Manager, and this keeps track
a lot of information happening in this effect. Like for example, once
I shoot with my gun, it will keep track of
the bullet location and it will send it to
the particle effect. Another thing that's going to
happen, if I shoot a blast, so once if I go into the
game and shoot a rocket, wait a split second,
that information is also transferred to Niagara. Another thing that can happen
is I can make a unit avoid me by approaching them. So if I ask them to run, notice
how the units are actually moving away from me. I can actually run next to them,
and as I get closer to a unit, they start moving to the side. Let's do this again. And this enables me to be
with units without actually colliding with them. This is very similar
to the bird effect where I showed the
kid runs through them. All right, and all
of that information is being sent
basically by Blueprint. It's sending the player
location, the blast location, the bullet location, even
commanding the leads. So part of our chain system is
the main unit, the leader here, is responsible for directing
everyone behind it. So the Blueprint actually orders
each lead from different teams to meet at a certain location
and start fighting each other. Now, another really
interesting feature of Niagara is not only Blueprint can
communicate with Niagara, but actually Niagara
can talk back, and this is something
we already saw when I shot one of the units
and an AI spawned in its place. So this process is
quite simple and all you have to do is use
a callback handle. So if you go back to Niagara
and check out the Activate AI, the function itself
has not much in it. It's just checking, hey, is
the bullet next to my particle. Well, great. Then please send the
information about the position, and then even though
it's named size, it can be any float you want. In this case, I'm sending
the animation frame and I'm sending the scale. Having those three information,
I go back to my Blueprint. It will trigger an event. And here it's telling
me, hey, I'm hit. Please spawn AI in my place. So I'm going to
spawn an AI and I'm going to set its location,
rotation, and scale. But also, I can match the
animation as well because now that I can read the
animation frame, I can figure out which
animation position it is right now playing
and spawn the AI at the exact same position. Now this is cool,
but what if I wanted to do this in multiplayer? So here are a few
things to keep in mind. Visual effects are
traditionally aesthetic. They do not run on the server. However, since in our case,
we are using Blueprint to drive a simulation, it is
possible to achieve multiplayer by replicating the Blueprint
that is feeding the data, though here are some things
you need to keep in mind. Important game logic still has
to run on the Blueprint side, and Niagara has to be used
only for visual representation. Niagara functions
need to have some form of deterministic outcome
where it matters, and calls from Niagara to a
Blueprint are not possible. For example, for our
bullet hit detection, that would have to run
on the Blueprint site. Another thing to keep
in mind is performance. Once you have 1
million units, you want to check your memory usage. Let's say you added an integer
variable to your particle which is an N32, meaning 32-bit. Now multiply that by a million. You are using 31.25
megabytes of VRAM, so every time you
add a variable, that is considerably a lot. So try to keep
that under control. Working on this
project was a blast, and there's a lot of features
that I would have loved to add, but I had to finish the
presentation at some point. For vertex animation, I was
baking the vertex position at each frame,
and that technique is great for simple
effects, but if you want to have specific features
like animation blending or even share animation
across different meshes, you might want to look into
baking bone data into a texture and having a
separate texture that binds the vertex to
the corresponding bone. There is actually a script
by John Lindquist called StaticMeshSkeletalAnimation.ms and you can find it already
in the Engine Extra Folder. Another thing
that I was missing from the battle is bullets
going out of the guns, and this is an easy fix. I just have to add
bullets into the unit mesh and bake its movement into
the unit shooting animation. Neighbor grid is a feature that
I didn't get a chance to try. It is really cool and
it lets you know the location of surrounding particles. It is worth noting that
it is heavy to use it for all 1 million
units, but if you set up volume around the
fighting area, it will help you
enhance your scene, along your units to
spread and interact with units in all directions
only in that specific location. This is my presentation. I really hope you
learned something. If you have any
questions, you can always reach out to me on Twitter,
and thank you for watching.