>>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.