A VFX Artist from Allods Team studio Alexey Kropachev talked about creating particle effects in Niagara and shared three extensive breakdowns of different particle effects.
My name is Aleksey. I have been working as a senior VFX artist in Allods Team (My.Games) for eight years, even though I've got a degree in programming. However, sometimes, motivation alone can be enough to help you on your chosen path. I have worked on such projects as Skyforge, Armored Warfare, Warface Mobile, as well as helped our company’s other teams with various projects. Right now, I am working on a new ambitious Unreal Engine project for PC and consoles.
In our country Allods Team studio is a pretty legendary team and joining them was a dream come true. I started as a junior VFX artist and my seniors helped me to develop most of my basic work skills and understand my job.
Right now, I am responsible for the full cycle of effects creation: from coming up with visuals alongside the designers to creating textures, models, shaders, building the effects in Niagara, getting rid of bugs, and supporting them after incorporating them into the project. As a VFX artist, I also take care of post-process materials, various effects for environmental and character materials. Teamwork is key!
Working on Particle Effects
About nine years ago, when I was still a game designer/generalist at a small studio I started working on particle effects. I was really attracted to the artistic side of the job, knew how to draw and model. So, I got the offer to make effects for MOBA-like games. We worked with BigWorld engine and built all effects directly in XML in Notepad. It was all very interesting and I soon trained myself into a VFX artist. At that time, I had no idea the world of visual effects was so deep and complex. Later, I had to learn a lot of various important things I’d known nothing about then.
It is no secret that there are only two major players left on the market when it comes to open engines available to any studio. Unreal Engine and Unity. Unreal Engine was initially geared towards large projects and its particle engine and material editor were perfect for any artist’s needs and didn’t require a programmer’s help. Easy to learn and incredibly powerful. But Unity has been breathing down its neck all this time. It never stopped advancing and its developers regularly added new tools. I think the competition helps everyone. But since Niagara came out for UE 4, Unreal Engine managed to substantially widen the gap.
Niagara in UE4
I have picked up Niagara as soon as its test mode was released. Using something so deep was an unforgettable and inspirational experience. In one word, incredible.
Niagara works similarly to most particle systems of other game engines and 3D suites. You create an effect (Niagara System) that consists of emitters that produce particles. Inside the emitters, there is a set of options for adjusting particles, as well as an option to add modifiers (Modules) that let you control particle behavior. There are a lot of these modules, and they cater to almost any need of a VFX artist. But more importantly, you can create the modules yourself, assigning to them the logic you need. And that’s one of Niagara’s most incredible and exciting features.
In the beginning, the number of features might seem rather imposing and difficult to master. But in truth, the toolkit is pretty intuitive and easy to understand. It resembles other similar editors a lot. You can also find many handy tools that will help you master simulations. For example, the editor itself contains emitter templates that are already configured with the main simulation types in mind, and you can see how they are built. There are also Content Examples in Epic Games Store that include example effects and you can use them to come up with new ideas and solutions. Youtube has a myriad of tutorial videos. And there is a Discord server called “Unreal Slackers” where you can always ask for help.
One of Niagara’s interesting features is its flexibility. You can do (or try to do) almost anything, even if it is not something Niagara was initially designed for. You can see the workings of any module simply by clicking twice. You can change already existing modules and expand or modify their functions. And naturally, you can create your own modules.
Below I will share some examples of what every VFX artist might eventually run into. Perhaps, it will prove useful to some of the readers.
For example, we might need to position the sprites in a circle with rays coming from the center. And we also want it to work for any number of particles.
Something like in the picture below.
First, we put each particle’s rotation on the curve normalized according to their number.
However, in our case, we end up seeing one particle less than we’ve specified. For 2 particles we will see only 1, for 5 only 4. That is because the first and the last particles get combined since they have the same rotation angle.
To solve this issue we need to normalize our curve based on the number of particles that actually spawn but add one more additional particle. To do this we can write a simple script (Niagara Dynamic Input Script). I will call it NSC_ReturnNormalizedIndexPlus1.
Let’s put it into our Curve Index instead of Return Normalized Exec Index.
We could also do the same thing with Scratch Dynamic Input which is pretty useful as it lets us work inside the asset.
But since we might run into the same issue in the future, it is easier to just create an external script and use it as we need.
After that, we can spawn any number of particles and they will all be displayed correctly.
Our script will also work well if we need to position our particles in space, whether it be sprites or geometry.
Instead of rotation we will use Custom Alignment in sprites’ Alignment and Align Sprite to Mesh Orientation module. For the geometry, we will need Initial Mesh Orientation. We will also use a normalized curve to orient each particle. Instead of a Vector, we need to use a Vector from Curve and set particle orientation with curves.
That way we can set the position in the System Location module.
And here are our particles:
Let’s move onto something more difficult.
For example, we might want some particle, e.g. a magic spark, to fly around a hand or something that can move on a random trajectory. To do that, we can pull it to other emitter's particles that appear randomly around the hand.
Let’s create such an emitter and call it Attractor. It must be positioned in local space so that our attraction points always stay around the hand.
You will find all properties of this emitter’s particles in the Particle Spawn section.
We will spawn particles with SpawnRate 2 and Lifetime 0,5. For a more dynamic spark movement, we can increase SpawnRate and decrease Lifetime accordingly.
We will spawn the particles at the edges of a small sphere so that they are scattered and the spark would be able to fly in the spaces between.
We can also go ahead and attach the effect to the character’s SkeletalMesh to see our effect working. To do so, we need to add the SkeletalMeshLocation module in the emitter. Let’s also set the location right away so that the particles spawn between the bones of the pointer and ring fingers on the right hand.
To work the SphereLocation needs to be set lower than SkeletalMeshLocation.
You can also add CameraOffset to see the particles even if they sink into the hand.
These particles will attract our main magic spark particle.
Let’s create an emitter for our spark and call it Core. It had to be set in local space as well.
Next, let’s spawn a single particle with SpawnBurstInstantaneous and make it “immortal”.
To make sure it is positioned correctly at the start, we will spawn it in the right hand like the previous emitter.
To attract our spark to the Attractor emitter’s particles we need to send their position to the PointAttractionForce module. To do so, let’s create Niagara Module Script which will receive the attribute of the designated emitter’s particles. Let’s call it NSC_GetPositionAttribute.
This script scans particle position from the emitter and writes it down into a SoughtPosition variable. When we add this script to our emitter, the variable will appear in the list of variables and we will be able to use it in other modules. After adding our script, put Attractor in the Emitter Name field. Then add the Point Attraction Force module and set our new variable as SoughtPosition in Attractor Position. We should also limit the speed of our spark to prevent it from flying too far away from our attraction points.
You can adjust the color of the spark and add a light source to the same emitter. To hide our attraction points, you need to set their color as A = 0.
Our spark is flying around the hand and even if our character moves it will remain there. Now, since our spark is magical, we might want to add some small particles in its wake or add a Ribbon trail. If our hand was motionless, we could have just used a local emitter for the trail. We could have used our NSC_GetPositionAttribute script and marked SoughtPosition in System Location. But if we want our trail to stay in place even when moving, we need the world coordinates for our trail emitter. Since the spark coordinates are local (necessary to make the spark follow the hand), we cannot get its world coordinates directly.
Let’s write a couple of new scripts. The first will form world coordinates, the second will receive them.
First, let’s create a Niagara Module Script and call it NSC_SetWorldPositionAttribute to form world coordinates. Initially, our particles have local coordinates and orientation relative to their emitter. This means we need to give them the system’s world coordinates and keep its orientation in mind. Let’s write the coordinates we got into a new variable – WorldPosition.
The script that reads the world coordinates attribute is similar to the one that gets normal particle coordinates. Let’s call it NSC_GetWorldPositionAttribute.
Next, let’s add the NSC_SetWorldPositionAttribute module into the Core emitter to the Particle Update section.
We can now get the world coordinates we need for the trail. For that, we need to add the NSC_GetWorldPositionAttribute module to the Particle Spawn section in the trail emitter. If we want to use modules like Location, they need to be set below our script.
As a result, we get a working effect with a spark with local coordinates that produces a trail with world coordinates.
And finally, I’ve left the best for last.
We might want to be able to rotate the particles around the axis and orient them based on movement. That could have a lot of uses, for example, rotation of stones and debris in a whirlwind or some magical vortex. We could try to use a standard Vortex Force module, but because of its centrifugal force, the particles would simply scatter. So, we need something more manageable. Let’s start with the simplest case where the rotation axis is fixed at Z.
The explanation might be a little difficult to understand, but I tried to make it as clear as possible.
To start with, we will need to create two scripts. The first, NSC_InitialCylindricalMove, will initialize the base values, while the second, NSC_UpdateCylindricalMove, will animate our particles. To make debugging easier, we will also create modules in Scratch Pad and transform them into assets at the very end.
Our particle moves in a circle, so to describe its movement we will need a radius and the speed of rotation. Now, set them in the first module and make them into variables. To ensure these variables remain unique, add a Cylinder prefix to each name.
In the second script, we will describe the circular motion.
Do not forget to press Apply, so that the changes to the script go into effect. We can already see our particle move in a circle if the values in the initializing module are not set to zero.
The particle keeps starting from the same point, so let’s add an option to set the position on the circle and the height.
The first script:
Now we can move the particles inside a cylinder by setting a radius, radial position, and height.
Next, we need to add movement orientation. To do that let’s take the particle position we got after all the calculations and subtract from it the position before the calculations. The result is a vector distance of one tick or a velocity vector. However, in the first tick, the previous position hasn’t been calculated yet and is set to 0, which means our distance vector for the first tick will be incorrect. You can check this, and when the previous position is 0, we will set the orientation based on Cross product between the position after calculations and the vector of our cylinder’s axis. Which is (0,0,1). This isn’t the best vector, but at least this way we can avoid the vector distance glitch in the first tick.
Now, all we need to do is set particle orientation to Custom Alignment in the Sprite Renderer section, and our sprites should be oriented according to their movements’ vector.
To finish the script, let’s add orientation for the Meshes. In the first script, initialize the CylinderMeshOrientation variable, which will help us set the orientation when we already have the speed vector calculated.
Check that everything is working correctly.
And now, finally, we can turn our scripts in Scratch Pad into assets and put them to good use. The script can be expanded upon, of course. You can add offset position, change axis orientation, animate radius and height, etc. You can easily add these functions since Niagara scripts are so incredibly versatile!
I can’t wait to see new cool scripts and effects made with Niagara!