Diving Into Niagara: Intelligent Particle Effects | Unreal Fest Online 2020

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
>>Arran: Hey, everyone. My name's Arran. I am a Tech Artist and the UK Evangelist at Epic Games. Today, I'm going to be taking you through some of the cool new features inside Niagara in Unreal 4.25. It's production ready, which means that you can use it in your games. I'm sure most of you have been using it already. It's absolutely incredible. You can do some really cool stuff with it. And in 4.25, there's been a load of new features that have been added that you might not be aware of. I've made six, I think, six different particle effects that we will be going through today. I'm going to start us off with just a quick overview of the new UI, just to get used to it. Then I'll be moving on to how we can create systems, modules, and emitters using the new system. And then moving on to a sample skeletal mesh example, just so you can kind of get an idea of how we build these things up. Then I'll be moving on to some of the new query and attribute reading systems. So the new querying system allows us to do things like sample the distance field on particles, or figure out whether those particles are being occluded or not. And the attribute reader allows us to read or share particle data between other particles. This stuff is incredibly useful, and you can have a lot of fun with it. So let's just dive straight into the Engine. One of the key differences between the old early access version of Niagara and the new version of Niagara is this new version is a lot more free form. To give an example, we can start by creating a new empty system rather than going through and creating a new emitter. I'm going to open this one up here, and just expand the window. At the moment we have a completely blank system, so there's no emitters inside this at the moment. Right clicking, we can go and add any emitter type that we want. So in this instance, I'm going to start with just an empty emitter. And then we can start populating this with whatever we need. I'm going to add just a quick spawn rate, and let's add an initial velocity. Now we can start creating particles straight away, and worry about creating the emitter templates and parenting and inheritance a bit later on. Once I'm happy with this particular emitter, I can choose a few different options. So I can actually update my parent emitter, if I want to parent it to a pre-existing particle effect. Or if I want, I can actually create a new asset from this, which will generate my new emitter. Creating modules has been made much more easy, as well. Now, instead of right clicking in the Content Browser going to effects, we can now do this directly inside the emitter or the system. This is called Scratch Pad. And we can do this just by clicking the plus button next to any of the elements where we want to add it. So in this instance, I want add it at the particle update level, and I want to create a new vector, which will be an input. And I'm going to add this to physics force, set up to transient, and get physics force, have these two together, and plug it in. Now I've created a very simple Scratch Module. If we're happy with this particular module, we can actually promote it to a fully fledged module, just right click, create asset, and then choose the location. Now we have a stand alone object in our scene that we can work with. Variables have now got a color coded tag to each one, so you can see system, emitter, and particles. If we create a new Scratch Module, we can press add on map get, and open up all of these variable types. Particles, under common particle attributes, lists all of the common particles that you're likely to access. Inputs are variables that can be set by the user. And then we have access to the Engine constants, as well, that we can get. We don't have to just rely on these though. We can convert these variables, as well. Right click, change namespace, and set to the variable you want to use. You can probably tell what system emitter particles do, but local, output, and transient might be new to you. Local variables are only accessible by the current module that you're working in. These are really useful for breaking up your module into smaller chunks. If we open up a pre-existing variable, you can see that they're broken up into these sections here. You'll notice the variables are being set to local, and then accessed again later on. This allows you to do small chunks of maths, which you can then access, again, when you need. Then we have outputs and transient. These variables can be written to and accessed from any module. So if we have the particles update here, we can access these at any point along here, but they aren't persistent, and they don't get exposed to the parameters variable. As an example, here's curl noise force, you can see it's writing to the transient variable physics force. This variable's then getting accessed by the sole forces and velocity along with any other variable that's accessing physics force, and calculating what the velocity needs to be for this particle. As well as namespaces, we also have modifiers. I'll change this from local to particles, right click, go to change namespace modifier, and now we have several modules to choose from. If we choose initial, we'll get a starting variable for this particular type, which we can then access again. This is really useful for placing variables within particle spawn, as a starting variable, and then changing that variable over time, but still referencing back to the initially set variable. We can also change it to module. The module namespace will make this particular variable unique, which means that if we want to add multiple versions of this module, it will create unique attributes per module. For example, if I just do an arbitrary set of getting this variable and setting it, you'll see that this variable's created here, but it's listed as Scratch Module 5. If I copy and paste this module, we now have two versions of this Boolean, rather than just the one. Lastly, we have custom, which allows us to name our own namespace modifier. Now let's try a few practical examples of this, using skeletal mesh sampling. So here I have a skeletal mesh, and if I press play and move it around, you can see that it's got a bit of a spring component to it. So I can move this base object around, and this component on the top is going to try and stay within a certain range. This is set up really simply on the skeletal mesh with an Anim Graph Blueprint. So we can see here, we've got the spring controller affecting bone 001, text naming conventions there, and that gives me this kind of motion. And this looks kind of nice, but I've got a bit of a kind of raman effect here, where the two parts are disconnected. And Niagara comes in really handy here by creating an illusion between these two points. So I've made a system, and all it is is a very simple ribbon that I'm adjusting the position of, so it wraps around two points rather than going from one point straight to another. What's really great about this is that I can just throw it into this scene, attach it to my box, specify the bone that I want to attach it to, zero out these components, and then when I press play, the bone data is automatically grabbed. Now I can expose and I can edit this if I want to, but this is a really great little example of how you can add substance to your effects really quickly inside Niagara. Let's open up a slightly more complex example. Here I have a deck of cards that gets thrown into the air, and then reforms as a pyramid. Now in this instance, I'm using the skeletal mesh to give me the pyramid data. So you can see, I've got my pyramid shape here, and then I have my hierarchical planes all listed below. I can then store that data, and then access it later on when I need to. Let's open up a system. Here we can see from the system overview, we have a single emitter, and it's quite simple. We're not doing too much stuff here. We start with the emitter update, so we're just going to burst spawn, the right number of cards for our deck, and then I'm doing a set pyramid. So this is grabbing a user set skeletal mesh. So you can see, we're grabbing that skeletal mesh there. Then on our execution index, we're going through each individual bone, so each particle, which is the execution index, equals a bone. And all we're doing here is grabbing the position, grabbing the rotation, and setting that to a variable that we're going to access later. Then on position, if I just rewind here, just to the start, you can see that our deck of cards is in a particular set right now. And the way that we did that was really simple. We just grabbed our particles position, set X and Y to 0, and then Z, I'm just setting an arbitrary value that's multiplied by the execution index. So if you imagine, 0 to 1, 0 being our first card, 1 being our last card, we're just multiplying that value. So we can actually increase and decrease this deck of cards as we need. So once they've spawned in, and they've been set, and they've gathered their end rest location, we need to do a little bit more stuff to them. So the first thing I want to do is offset the particle's action. So the first thing I want to do is create an offset to the age of each individual particle. If I have them all running on the same age, then they'd all perform the action exactly at the same time. But if I offset it, then I'll get a nice flurry effect where the particles are kind of delayed as they're going in, like so. Again, this is really simple. We're just setting a custom value, which is called NAgeCardOffset. All I'm doing is adding the normalized loop age of this particular emitter by the particle's execution index. So that gives me my particle offset. And then I just start performing the actions that I want to do to it. So I start adding a velocity. I add a curl noise. And these are both mapped to curves. So I control the strength of them over time, so you have kind of a rest state right at the start here and here. And then as they kind of go in, we increase the force and the velocity. And then as we get them to go back into their rest state, we drop that back down. Then over time, we want to start realigning back to their target state. And again, this is just a custom module I very quickly wrote. So all I'm doing here is figuring out my look at direction, normalizing it, multiplying it by my curve, which I'm defining here, and then slowing that down based on the distance to the actual target. And then I'm multiplying that by just a simple value. And then all I'm doing here is lerping between the kind of the current rotation. So I'm kind of getting a bit of a hacky look at rotation, and then I'm taking in that initial pyramid rotation that I'd already calculated, and then just blending between the two. Next up, I want to go over a few examples of some of the new query and reading systems that we've got in Niagara. Particles can now read data between other particles using the new attribute reader. You can also query camera, collision, and occlusion. Let's go through a few examples, and I'll show you how to set these up. Let's start off with the occlusion query. Here's an example of a full game world effect, where we spawn thousands of particles in the world around us, and then only include the ones that are partially occluded. This creates a kind of background effect where the particles are only rendering behind objects. As we go around, we can make out the shape of the world around us, because the particles are visible behind the shapes that they're rendering in. The occlusion query is easy to set up and use. Here we've created a Scratch Module that's accessing, not just the camera, but also it's in the occlusion query. This does a number of samples, checking the object's position, at a radius, and comparing it against the depth buffer. We can increase the number of samples using number of sample rings and the samples per ring. And we can increase the diameter of the object that we're checking for using sample window diameter. And then we can take the visibility fraction, and then we lerp to this from current visibility f. This lerp will create a gradual change over time, rather than just snapping in and out. I'm also updating the past calls opacity using this value. Then I'm doing a second get, which is using camera query, which is another new query that we have inside Niagara. I'm getting the position, the world position, and the particle position, subtracting one from the other, normalizing, and then multiplying by 250 to set the sprite size. This means that as I get closer to the particles, they disappear, which can help reduce any size issues that we may have on the object. Then we have some standard spawning, and some motion that we're putting into place. And that's all it needs. Next, let's take a look at the distance filled query. This is incredibly useful for figuring out how close particles are to surfaces in the environment. So here you can see, I've got, kind of in blue, these particles are just acting on their own. They're just following a standard force that's being applied to them. But the ones that are kind of coming towards this pinky red, they're getting closer and closer to a distance field. So you can see over here-- I'm just going to turn off real-time for a second. Here you can see that as they get closer to the this surface, they start to change color. And then I'm applying a different force to them. What's really cool is that we can see this on moving objects, as well. So I've got this cube that's kind of rotating around. And you can see that as the cube moves, it interacts with these particles, and starts throwing them in different directions. This is incredibly useful, not just for creating water simulation effects, but also for creating simplistic animals, who need to be able to avoid certain surfaces. Let's take a quick look at how we can actually implement this interface. So here's my system that I'm using. Again, it's super simple. I've tried to make it as easy to use as possible. So all I'm doing is kind of spawning out below the particles. I'm working on the GPU here. It's got quite a high spawn rate. And then I'm setting up the box location, which is just giving me kind of an initial space for the particles to spawn into. The real meat of the objects is coming in here, inside this custom Scratch Module. So I have a new collision query. And then I'm using a thing called sphere cast global distance fields. So all I need to do is grab my position, and that's really all I need to get started. Once I have that, I can find the final location field gradient. I'm doing a bit of access looking here, which is stopping the object from moving in Z. And here's my particle color, what I'm saying, which is more of a visualization than anything. Again, it would be really easy to swap this out with the current velocity of the particle, and you could even start outputting flow maps for these particular effects. Lastly, let's take a look at the attribute reader, which allows you to share particle information between multiple particles and emitters within the same system. Here's an example that I've got here that just has some floating sphere components with some connectors running between them. Let's open that up. This particular system consists of two emitters, the spheres, and the connectors between the two. We're actually using two different types of attribute reader. We're using one type, which reads attributes from another emitter. So in this case, our connectors are reading information from the spheres. And then we're using a secondary attribute reader, which is reading information of different particles in the same emitter. This system has a few different components that it's trying to do. So the central sphere here is the first sphere that gets spawned in in the burst. And its job is to try and stay as close to 0 as it can. These almost orbiting spheres, their job is to try and maintain a distance from the central sphere, but also to try and maintain a distance from one another, as well. And the connectors are finding the central sphere location, and then a single orbiting position, and creating a connection between the two. We've created a new module here for position beams. Let's start off by looking at this in chunks. So we have our input map, which is our initialization starter. And then we have a map get, which is grabbing our attributes. And then inside the emitter level, we've named the emitter as dots. Once we know the emitter that we're targeting, we can choose what data we want to extract. We can choose a number of different types using the attribute reader, get variable by ID or index. And you can see the different types that we can get here. Here I am getting two vectors. We have to name the attribute we're getting, as well. In this case, it's called position. So we're grabbing the position of this particular object, and we're getting that at particle index 0 for this one. And at this one, we're getting the execution index of our connector. So in this case, you can imagine it as-- let's pause this-- as connectors 0, 1, and 2. What this will do is we'll get that number, say 0, 1, and 2, adds 1 to it, so this becomes 1, 2, and 3, and then gets the corresponding particle. In this case, 1, 2, and 3, ignoring 0. This means that we will never get a connector trying to connect 0 to 0. And as long as the number of spawned spheres is one more than the number of connectors being made, we'll always have the correct number of spheres to connectors. Then I'm getting a lerp, and finding the middle value between these two points, by having it set to 0.5. I'm plugging that directly into particles position, so we are hard setting the position value of these three particles. Then we're doing a little bit of math. Here I'm getting my particle position, which will be here. And my secondary sphere location, which in this case will be here. Subtracting one from the other, and then normalizing it to give me a look at vector. This is going to be used later on. Then a little bit of extra stuff in here, just to-- just to make it look a little bit nicer. You should be able to see that as these connectors get closer together, as the spheres get closer together, the connectors get thicker. And this is just a very simple calculation here. We are getting the length of this vector, normalizing it by clamping and dividing, and then lerping between 0.3 and 0.1, which is going to be our scale value based on Y and Z. And then, finally, to get the correct X value, which is the length between the two points, I'm getting the length of this particle read, and dividing that by 50. That all gets plugged into this make vector, and then we set that directly to particle scale. Finally, to finish off the connectors, we do an orient mesh to vector. We already have our target position, so all we need to do is set it inside here, like that. Now let's take a look at the sphere component. The first sphere being spawned, this one in the center, is always trying to return a 000. And the component spheres around it are trying to stay away from the central sphere, and also stay away from each other. I've made two modules that do this. The first I'm going to go through is repel all. You can see we've already got this particle attribute reader, and it's referencing the dot emitter. We're doing two key things in this emitter. The first is looping a custom generated count integer. So we're going through, we're checking to see if we've exceeded the execution count, which is the number of total number of particles that have been spawned. If that's true, it sets it back to 1. But if it's false, we add 1 to particle counts, and we set it over here. Then for each of those integers, as we go through, we're getting the position of the particle at this integer, and subtracting it from our current position, normalizing it, and getting a standard length, and, finally, multiplying it. We then put this into an if statement, which will return A. If the execution index, the current particle, is equal to count, or is equal to 0, this means we won't add any velocity, if the check we're doing is a particle against itself, or against the first particle. If neither of those things are true, we'll return this value, add it to the transient force variable, and set it. The second module, well-named Scratch Module 01, is checking particle position 0. So this is every particle is checking its distance from the central particle. And then depending on how close we are, we increase or decrease the velocity going away from this central particle. We have an exclusion parameter, where if this particle is particle 0, and if it is particle 0, we have got some logic to account for that, as well. This checks the Engine owner position, which is going to be 000 for the particle, and the particle's current position, and then multiply it, normalizes and multiplies that value. And then, finally, we add that to the transient physics force value. The physics force transient value is handled by the sole forces of velocity module. So as long as these values sit above this one, these will be included in. We have other variables that are added to the physics movement of each of these particles. And they accumulate to form a final value, which the sole forces of velocity figures out. And that's all we need to get these particles reading data from one another. Lastly, I just want to very quickly touch on debugging. We've already got some pretty powerful debugging tools using the Attribute Spreadsheet. You can use this to capture data on the particles that are currently within your emitter. So here we have all of the data that's being generated for each of the particles. This can be really useful for checking different attributes, and you can even filter out data that you don't need. So here we can just take a look at position X, Y, and Z. Sometimes it's really useful to be able debug directly at viewport. Here I've created a secondary sprite render that sits within the sphere emitter. We enable it. And here you can see, we have these red, green, and blue numbered variables sitting next to each of the sphere properties. This is super useful as you can directly see what data is being set to what particle, rather than the abstraction of a spreadsheet. Here, I'm gathering the index data of each of these points, so that I can see where they are. I can see this one's 0. This one's 1. This one's 3. And this one's 2. Let's change these values so that you can see how we can set this up. Inside my particle update, I have a dynamic material parameter. And at the moment, I'm setting X to be an integer, and to return the execution index of each of these particles. Instead, I'm going to change these to take in the particle position at channel X, channel Y, and channel Z. These particles will now output the current position of each of these particles. This is being handled mostly by the material that's being rendered on this sprite. Inside m_debug, you can see I'm grabbing my dynamic parameter, appending it together, and then setting it to a custom material function called debug float 3 values. Then I'm plugging this into the emissive and opacity. And that's what's giving me my current render. You may need to overwrite some of the binding on these particle sprites, especially if you're rendering a sprite by default on your object rather than a sphere. Here, I've overwritten the sprite size binding, so instead of using the standard variable that's getting set, I'm using a custom variable that I'm setting manually.
Info
Channel: Unreal Engine
Views: 28,901
Rating: 4.8723402 out of 5
Keywords: Unreal Engine, Epic Games, UE4, Unreal, Game Engine, Game Dev, Game Development
Id: oX6uiPWXJDY
Channel Id: undefined
Length: 29min 44sec (1784 seconds)
Published: Fri Aug 07 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.