Andras Ketzer, the creator of FluidNinja, shared a step-by-step guide on how to build basic texture sampling emitters in Unreal's visual effects system Niagara.
In this tutorial, we are going to build two Niagara Systems from scratch. One that is setting initial particle properties by sampling a simple flow map texture - and by modifying this, a second system that is dynamically altering conditions by sampling a flipbook. All assets provided: packed to a 2.4 Mbytes ZIP.
The sample data for our second system is the flipbook rendered velocity map of a Kármán vortex street, a classic fluid-dynamics phenomenon of repeating swirling vortices, simulated and baked with FluidNinja in Unreal Editor.
When finished with the tutorial, we end up with this scene:
Utilizing Fluid dynamics is an ongoing effort in the VFX community and provides the basis for creating realistic gases, liquids, and all kinds of ethereal effects - be it a demon summoning or a space warp.
We are close to using real-time simulation in game: have a look at the new Simulation Stages feature in Niagara for particle-based simulation and the Volumetrics Plugin, capable of voxelizing meshes and simulating fluid behaviour in fixed grid systems.
While experimenting with real-time fluids and waiting for the hardware to grow up for this task, we are using pre-calculated (baked) fluid sim data, practically stored as a sequence of frames, collapsed to a single texture: a flipbook.
By default, we have been using flipbooks the same way as we used classic textures: sampling them with a material and mapping them on scene geometry. Have a look at these typical examples, captured from Mandragora game (fire and water):
Niagara provides an alternative use case: utilizing the Sample Texture Data Interface, we could directly sample fluid data with Niagara modules (no materials are needed) and interpret the texture-stored values as float / vector / color type variables to drive arbitrary particle parameters. We could drive masses of GPU particles with fluid data.
Unlike geometry texturing, "particle texturing" should not be a one-to-one topological mapping between the texture space and the particle-space: fluid data could (1) provide initial conditions for a dynamic simulation, (2) drive abstract parameters or (3) used additively. The point is that we are altering particle behaviour and not determining.
Maybe the most spectacular example is using velocity data from a fluid simulation to accelerate particles. Instead of overwriting intrinsic velocity, we are pushing particles to sim defined directions while allowing collisions with scene geometry. Unlike classic baked VFX, our systems could respond to physics.
Creating a Simple Texture Sampling Emitter
Imagine that we are moving on a two-dimensional plane and our moving direction and speed (together called velocity) are described by colors. Horizontal movement is described by shades of red: a value of 50% means we are standing, 100% is full speed right, 0% is full speed left. Vertical movement is green: 0% up, 50% standing, 100% down. Imagine stirring a bowl of liquid, and making a picture / snapshot of the surface from the top, and assigning red and green values to each sampling point on our picture depending on the velocity of fluid movement at a given point. We are describing motion data with a 2D vector-field stored as texture - this is the concept of velocity maps aka flowmaps. A velocity flipbook is a sequence of velo maps, describing the temporal changes in the velocity vectors of a fluid system.
We don't need a fluid simulator to make a single flowmap - thanks to Teck Lee Tan we have a simple, stable, free, 7 Mbytes piece of software to hand-paint such maps: FlowMapPainter v0.9.2, up since 2012.
I encourage you to download the tool and play a lot! By toggling the FlowLines and VertColor flags you could visualize the above-mentioned vectors and colors. You could also preview the animated flow. Create a flowmap and drag it to Unreal Content Browser to start developing! I made a simple, round, clockwise brush stroke - this flowmap, describing circular flow will be the sample data for our first system.
Start Unreal Engine, create a working folder in Content Browser, import the flowmap to this folder. Right-click on the empty area and pick FX / NiagaraEmitter / Create a new emitter from a template - then choose "Empty" type.
Create a new emitter:
Once in Niagara editor, save the empty system. As a first step, we are going to add a spawning location module. Click on the green plus (+) sign besides Particle Spawn and pick Location / Grid Location. The module appears - with a warning. Don't worry, GridLoc needs another module as a prerequisite. Choose "Fix Issue". A second, "Spawn Particles in Grid" module is created under the "Emitter Update" group - with a second warning. Again, choose to fix the issue. When all done, we are ending up with two new modules.
Grid Location and Spawn Particles modules added:
Right-click GridLocation module and choose Insert Above, then start typing "texture" in the module browser - do you see the "Sample Texture" module? Pick.
Adding Sample Texture module:
Sample Texture module added. Next, select Emitter Properties module at the top of the stack, and set Sim Target from CPU to GPU (only GPU emitters could sample textures). Plus, set Local Space flag ON.
In the meanwhile, stop the timeline from repeatedly playing (yellow bar at the bottom, drag the vertical positioner to 3-4 sec) + go to the viewport top, click Lit, go Exposure, turn OFF "auto" and set EV100 to 2.
We are going back to SampleTexture module. Pick the flowmap as Texture input.
Next, we are going to deal with the texture-to-particle mapping via UV coordinates. Axiom: we could address (sample) texture pixels using a 2D coordinate system - the two axes (U and V) are describing the horizontal and vertical position of our sampling head. The bottom left pixel of the image is 0,0 top right is 1,1. To assign (map) texture information to particles, we could use the first and second values (X,Y) of the 3D XYZ particle position as UV coordinate. Our particles are already arranged on a 2D plane / rectangular grid. All we need to do is: read-in particle positions and transform the values in a way that min-max coordinate values fit the 0-1 range. To perform this transformation, we'll need scaling (multiplication) and offsetting (addition).
Click on the tiny arrow on the right of the UV data row. Type in "break" - and choose Break Vector 2D - so we could deal with U, V separately.
Break Vector 2D:
Now, we are going to (1) pick the X value from the XYZ particle coordinates, (2) transform it and (3) use it as U. Click the arrow beside X and pick Make Float from Vector. Pick "X" as channel. Do the same with Y (V), but pick "Y" as channel.
Make float from vector:
Look at the screenshot above: we are going to use the "yellow" coloured values as U,V.
Now we are equipping our input fields with transformation tools (add, mult). Click tiny arrow, type "add", pick "Add Vector". Do this for both U,V (X,Y).
Notice that we have A and B rows now - we are adding B to A. Now, Click the tiny arrow besides _the A row_, and type in "mult", pick "Multiply Vector by Float". Do this for both U,V (X,Y). We should end up like this (see pic below).
Multiply Vector by Float:
We have all that we need for transforming Particle coords to UV 0-1 values - as a next step we request particle coords! Have a look at the picture above - we are going to fill the yellow coloured input fields with XYZ particle pos (using only X, Y respectively).
Click on the tiny arrow beside the input fields, go Link Inputs / Particles and pick "Particles Position".
Let's have a closer look at this Sample Texture module. Double click on the module in the stack. A graph editor (Niagara module editor) pops up.
Sample Texture module:
Notice the first one of the two rows in the Map Set node: OUTPUT MODULE SampledColor - this is the module output, the sampled texture data, that we are going to use by other modules downstream (lower) in the module stack. Close the module editor by now. Back in the generic module stack viewer. Select the "Initialize Particle" module. Click the tiny arrow beside the "Color" input field. Go Link Inputs / Output and pick the previously seen "OUTPUT SAMPLE TEXTURE".
Init Particle - pick OUTPUT SampledColor:
We have just channeled our sampled texture data to particles - you might have noticed a change in the (by default while) paricle color in the viewport. The particles are already "textured", but the texture scaling is wrong. One more thing: set "Sprite Size" to 15 (5 by default).
We are going back to the Sample Texture module and fiddle with the particle-coords-to-UV transformation a bit. Set the float multiplier to 0.001 (scale) and the X,Y values of the offset vector to 0.5.
Particle coords to UV transformation:
Hopefully, we are ending up with our particle-grid properly colour mapped with the flowmap. Colour mapping was just a reference - this way, we are visualizing the motion vector field. Remember, colors represent speed and direction. Let's channel this data to a velocity module, then!
Click on the green plus (+) besides the Particle Spawn group header, type "velo" and pick the "Add Velocity" module. Don't worry about the warnings: click Fix Issue: in theory, this is adding a solver on the bottom of the stack - a "Solve Fores and Velocity" module should appear. If not, we are going to create one by clicking on the green plus (+) besides particle update and manually selecting it. One more thing: in our first example, we'd like to set initial particle velo - so lets drag the Add Velocity module one step UP in the stack - it should be ABOVE the "Apply initial forces" module. We should end up like this (see pic below).
Velo module added:
Check "Apply initial forces", the "Apply force to velocity" flag should be enabled. Select the Add Velocity module. Next, we are going to make a 3 channel velocity vector (XYZ) from our sampled 4 channel colour data (RGBA) and use that as module input. Remember: sampled colour data is using 50% red and green values as null points and 0% - 100% as opposite max values. This is because textures normally can NOT hold NEGATIVE values. Now... engineers like to describe velocity in the (-100%) (0%) (+100%) range... so we need to do a transformation here. Click on the tiny arrow, type "add" and pick Add Vector. Focus on row-A: tiny arrow, type "make" and select Make Vector3 from Color. Our velo input panel should look like this (see pic below).
Velo input and transform:
Click the tiny arrow beside the color input fields, go Link Inputs / Output and Select OUTPUT SampledColor. Set the added offset (vector B) to (-0.25). Click the downward-pointing little arrow below vecB, and set Scale Added Velocity to (250,-250,0). The second value is negative because silly FlowMap Painter is flipping the green channel. You could tweak particle lifetime in the "Initialize Particle" module, and emitter lifetime / recycling in the Emitter Update module. To make the particles nicely fade in-and-out, add a Scale Color module at Particle Update and draw a bell curve for alpha (using the float from curve function).
Tutorial part 1 done, press play at the timeline - looking at the viewport, our particles should start their lives with a velocity defined by our texture - in my case, they are moving in a circular flow (see pic below).
Particle velo set:
Creating a Flipbook Playing Emitter
Our first emitter was setting initial params based on a texture. We are going to modify this emitter to continuously alter particle motion - using a baked simulation, stored in a flipbook.
Disable "Add Velocity" in the Particle Spawn group. Drag the "Sample Texture" module downwards, on top of the "Particle Update Group". Add a "Sub UV Texture Sample" module by using the green plus (+) at Particle Update. The stack should look like the pic below.
Adding SubUV module:
We are going to make a NEW Niagara module by merging SampleTexture and SubUV. Double click SampleTexture. We are in the graph editor. Click "Browse" in the top left corner. We have localised the module in Content Browser - and we are going to clone it. Right-click on the module asset, choose Duplicate, name it as you like. I have named it "SampleTextureFlipbook". Save it. Back to Niagara. Disable both "Sample Texture" and "Sub UV modules". Add the freshly created module to the stack with the green plus (+) besides Particle Update. Drag it upwards, below the two disabled modules in the stack. Double click on it to open the graph editor - leave the graph open and switch back to Niagara stack view: double click on SubUV to open a graph for that, too. We have both our custom module and SubUV open on two tabs. We are going to copy-paste nodes from SubUV to our module.
Copypaste module nodes:
Change the active tab to SubUV. Select the MapGet and SubUVTxtureCoordintes nodes. Ctrl+C (copy to clipboard). Change the tab to our custom module. Ctrl+V (paste). Connect nodes similar to the configuration as they are in the pic below.
Going back to stack view: select our custom flipbook-reader module. Pick the included velocity flipbook as Texture input (T_Fluidninja_Velocity_Flipbook.uasset).
Set X-count to 7, Y-count to 6. We are going to copy-paste the UV setup that we have done for the Texture Sample module last time. Pls select the Sample Texture module, hover your mouse over an empty area in the UV-row (below the texture options). Right-click, choose "Copy". This operation copies the whole hierarchy under the UV-row to the clipboard. Pls go back to our custom Flipbook reader module, hover mouse to an empty area in the UV row, right-click, Paste. The hierarchy should appear. At the top of the module details, click on the tiny arrow beside the "Phase" row - and type "time", select Engine Time. This continuously growing value is going to induce the frame-to-next-frame jumping (playback) on the flipbook. Like in the previous case, we'd like to have a color-preview. Pls add a Color module with the green plus (+) besides Particle Update, select the module, and (using the tiny arrow rolldown menu) besides the color input fields add the output of our flipbook player module as Color input.
Again, under the Link Inputs / Output, SampledColor (carefully, the output of the disabled SampleTexture module is also on the list - do not pick that). Press Play. Hopefully, you'll see the flipbook playing in the viewport (see pic below).
Velo flipbook playing in viewport:
Next, we are going to use this color information to accelerate particles. Right-click on the Solve Forces module and chose "Insert Above", then choose "Acceleration Force" module. Select the new module. Before calling the sampled color data, we prepare transforms. Click on the tiny arrow beside the acceleration row, and find "Multiply Vector by Float".
Accel module, mult:
Next: click the tiny arrow beside the vector row, and find "Add Vector". Click the tiny arrow beside Vector-A, and find the flipbook module output the same way as we did with Color (Link Inputs / Output). Set Vector-B to (-0.5, -0.5, 0) and the Float below to 3000. Set coordinate space to Local.
Add a "Drag" module to the Particle Update group, drag it below the Acceleration Force module (both should be above the Solver), and set Drag value to 3. By now, we should see our particles swirling.
Accel module, params:
Let's do a few adjustments! Select Emitter State module, and set loop duration to 0.5. Select the Initialize Particle module. Set Lifetime to 4, sprite size to 5. Go to Grid Location module, and enable "Randomize Placement Within Cell". You might have created a Scale Color module in the previous example - if not, pls go to Color module, press the arrow below Output Link, and on the freshly appeared panel area, set a "Float from Curve" for Scale Alpha, like this (pic below).
Color, Scale Alpha, Float from Curve:
As a final step, add a "Collision" module to the Particle Update group, move it between the Drag and Accel. Force modules, and set the following values (pic below).
Collision module params:
One more thing: instead of flooding the whole area with particles, let's try to confine the particle-emission area to a small rectangle, close to the assumed starting point of Kármán vortices.
Select the "Spawn Particles in Grid" module, and set both X,Y count to 20 (reducing the number of particles). Next, select Grid Location module, and set XYZ Dimensions to (2,3,2), Randomize to (10,10,10) and Offset to (0,400,0). Note: the Y=400 offset value is pushing the particle emitting rectangle from mid-field to bottom-field.
Note 2: to place the freshly created emitter on level, we should embed it to a NiagaraSystem. Create one in the Content Browser, right-click, FX, Niagara System, New System from Selected Emitter - and pick the previously created Emitter - then drag the system on level. The system should be working WITHOUT pressing the play button (in Level Editor standard mode). Try to move objects into the particle stream and experiment with collisions.
We have finished our second emitter - congrats, in case you have worked your way through this step-by-step process. Hope you like it! In case something went wrong, don't worry: all assets are available here (versioned under UE 4.25), just copy them to your content folder and compare your work with the existing assets.
Kármán vortex street in Editor:
There are so many things that could be done! Using density and velocity flipbooks at the same time, calculating real-time raymarching based shading on our particle clouds, experimenting with abstract parameters. You could continue reading and watch more example videos below: