Simulating Large Crowds In Niagara | Unreal Engine

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Unreal Engine
Views: 258,008
Rating: undefined out of 5
Keywords: Unreal Engine, Epic Games, UE4, Unreal, Game Engine, Game Dev, Game Development
Id: CqXKSyAPWZY
Channel Id: undefined
Length: 34min 6sec (2046 seconds)
Published: Fri Jan 15 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.