Using Unreal's "Niagara Data Channel"
Hi, all. Today I'm going to be talking about one of Unreal's newer and experimental features that they have added to their FX package, Niagara. The feature in question is the "Niagara Data Channel" introduced in Unreal 5.3. In this post, I'll be talking about what it does, how it's currently being used, and why I think it's going to be an important feature for more advanced effects in the future.
What is a "Niagara Data Channel"?
A Niagara Data Channel (NDC) seems to be a new method of communication between a niagara system, blueprints, and other niagara systems. One can think of it almost like a Particle Attribute Reader that connects Niagara systems and game code, meaning that arbitrary data like position, size, or colors can be sent across in a stream of data and used by a niagara system in a customized way.
So for instance, one could maybe make an advanced global "raindrop splash effect" which can create splash particles based off of events fired off in a blueprint, or a collision event shot off by another particle system spawning raindrops. All of this sharing the same data, and making it flexible and easy to use with other future systems down the line.
How is it currently being used by Epic?
One example of an NDC being used by the Epic team is within the Niagara Fluids plug-in, where they have already set up some of their systems to take data from other emitters to serve as a source for the smoke/fire.
You can get a grasp of how they use it by doing the following :
Create a niagara system and place the GRID3D_Gas_Master_Emitter within it. You can find this in the Niagara Fluid plug-in, under the path Plugins > NiagaraFluids > Templates > Gas > 3D > Emitters.
By clicking on the tab, "Emitter Summary" on the GRID3D_Gas_Master_Emitter, you will find a tab at the top called "Source", which will lead you to a drop down option for "Particle Source Type". You will select "Data Channel (Experiemental)".
Great, so now we set up a smoke system which will read data written to an NDC made by Epic for their smoke simulations.
Next, we are going to create our own emitter which will "write" to this NDC with its own particle data. Create another new system with a CPU Emitter. This emitter MUST BE A CPU EMITTER as Niagara Data Channels do not support the ability to have GPU emitters write to them.
In this emitter, you will set it up to behave how you want. So have it burst, or fountain, or flow however you want.
Next, under "Particle Update", you will want to add the module "Set Fluid Source Attributes".
Under the details panel of the newly placed module, you will find a setting at the bottom called "Write to Data Channel (Experimental)". You will want to check this.
Congratulations, you now have a system which will write to an NDC. Now it's time for the magic. Drop in both of your systems into your level, and place them right next to each other. You should see fire and smoke come from the particles of your CPU emitter.
WOW. Niagara systems now have an easy way to talk to each other.
How I Am Using NDCs For My Own Advanced Effects
A while back, I started on a new painting system for my in-development game "Lying Saturday", where, long story short, you are painting the world with the blood of your enemies.
My previous prototype of this painting system proved to be finnicky, as it was handled using prebaked position maps, and painting to a render target through the game process using blueprints. But if anybody knows the issues with drawing to render targets in blueprints, it's that it blocks the game thread and flushes the command queue in the RHI. This means that by the time things got hectic and you were pouring out gallons of blood, you would get a considerable blow to performance because Unreal would have to keep dropping what it's doing to access the render target and draw to it. This limited the amount of fun you could have with a system as crazy and hyper-violent as 'painting the world with blood'.
Thus, I have since moved this mechanic to a Niagara system, utilizing GPU simulation stages to bake a barycentric map on initialization, and then dilating it to try and reduce mesh seams, before handling any sort of drawing afterwards. And since Niagara is an easily accessible compute shader, that meant that I could paint to the render target with considerably less headache as opposed to the previous system which used the problematic blueprint nodes and functions.
But this came with another problem to solve. The main question to answer was : "If the blood I was using to paint the world came from another niagara system, then how would I send this data to say 'I want to paint this big, and at this location'?"
You can probably see where I was heading with this : an NDC.
Setting Up A Custom NDC
Setting up a custom NDC is relatively easy. You can find it in the FX menu when creating a new asset under FX > Advanced.
Once in the menu of the NDC details, you will be given a choice between two ways that NDC data is handled. The 'Islands' Channel type, which essentially creates various divisions across the map, each with their own set of data only related to that area. Or a 'Global' type, which is a channel readable throughout the entire world.
In this, you can adjust things to your liking when it comes to the size of Islands if you chose the Islands type, or you can add the types of variables you would like to send in the NDC. Mine is a basic set of position and float values, the "where to paint and how big to paint".
Custom Modules in ScratchPad
When setting up your NDC to be able to use it, you need to create custom niagara functions or scratchpad modules in order to read or write to it. Within Niagara, you are able to set NDC reader or NDC Writer variables depending on how your emitter behaves. Will it send data, or will it read data?
In order to write, one must append to the data set with the data they want to write. In my case, I wanted to write the position at where a blood drop hits, and the size it will paint. This information is then sent to the NDC for other Niagara Systems to read.
Now in the other emitter.
In order to spawn a particle based off of an entry, at the moment, you can only use the "spawn conditional" function. It acts similar to 'Spawn Instantaneous', but is related specifically to an NDC. One key note is that you are allowed to set certain conditions that determine whether a particle spawns from the data that it's reading from the NDC. So for instance, you can filter out a particle based off of a size value and only spawn particles based off of a float being greater than 1.0.
Next, you will want to create a scratchpad module in order to read the values and then set them accordingly on a per emitter basis. You do this using the 'Read' function, which will use the emitter attribute 'SpawnGroup' as the index for input (don't worry too much about this variable, as it's automatically set by 'Spawn Conditional'). You then want to set the read value as a module output, or as a particle attribute directly.
With a couple of simple modules, you are now able to communicate cross 'Niagara System' or via blueprint nodes, meaning that you can now set up custom behaviors for any system. This is huge, because now it means that Niagara can get data from more than just an attribute reader, or by the various number of blueprint nodes which need to exactly target a system. This opens up advanced and flexible effects, while also opening up the door for potential optimizations for effects that are spawned frequently via "Spawn Emitter At Location" nodes.
So for instance, one could have an emitter write and spawn different sized particles, and have another system spawns Explosions off of big particles written to the NDC, and another system that spawns sparks only on small ones in the NDC.
The possibilities seem endless.
Hopefully you learned something new, and are able to now create some cool effects interact with each other. I can't wait to see more people use this new experimental feature.