How to make a beam-ribbon hybrid in Niagara (tutorial)

Two years ago I asked a question:

It didn’t get a response, which isn’t too surprising as it’s a pretty weird thing to be attempting. But I never gave up on it. And now, after quite a bit of tinkering in my spare time, I have come up with a solution I wanted to share using some custom Niagara scratch-pad work.

There isn’t a lot of information out there I could find about using Niagara’s weird brand of blueprints, and I did a lot of this via reverse engineering and trial-and-error, but I’m very happy with the result

THE GOAL
The idea here is to create a “wavering jet”; what we want is to make a jet which begins with a very rigid, almost static orientation in space, but over time loosens up into a trail which lingers behind it off the tip. This is meant to be what a jet of fire might look like (starting as a fixed stream, but the tip wavering and flickering as it moves), though I also found it very useful for using particles to create a surface-conforming cable attached to a skeletal mesh.

While this seems pretty straightforward to accomplish on the surface, it’s not actually very easy to set up in Niagara. What we are essentially trying to do is create a string of particles which begin in a beam orientation, but then sort of “lerp off” into a fixed position.

THE BEAM/TRAIL DISCONNECT
The way most tutorials show the ribbon renderer being used for things like trails, what is essentially done is particles are spawned in-order, one after another. Maybe they have velocity as they are emitted from a static source, maybe they are static as the source moves, maybe both are moving… whichever way, they are a sequentially-spawned sequence of points, occurring one after another. The ribbon renderer then draws a series of triangles between these points, in order, to give the impression of a single connected ribbon.

But a beam works differently. A beam defines a spline, then it takes a fixed set of particles (usually spawned with Burst Instantaneous) , places them at intervals along the beam, and manually defines a ribbon link order for each one. This disconnect is crucial, because what it means is that a beam is not a path. When you define a beam, you are not defining a trajectory that particles will follow, you are defining a sequence of positions particles will inhabit, and updating it every frame, while manually specifying which particle is which for the purposes of drawing the ribbon.

Why does this matter? Well, in our case here, we’re trying to use the beam not as a defined sequence of positions for particles to be in, but rather for particles to move through. This is the first major hurdle we have to overcome; we want to define a spline, but we want one end of that spline to “open up” into free motion in space in some way.

Imagine our beam as a tube of water. We want particles to flow through the tube, following a rigid path, then “pour out” the open end of that tube. If we move the tube, the particles inside of it move with it, but the particles that have “poured out” ignore this motion. But the default functionality of the beam emitter setup in Niagara demands our tube be closed at both ends, with everything fixed inside as it moves.

OPENING THE TUBE
UpdateBeam

The problem here is this little guy: the module Update Beam. This is the module that is required to allow the Beam Emitter setup to work, as it redefines particle positions along the beam. It’s kind of a black box; it doesn’t expose any parameters to the user, nothing to control. It just does what it does. And it’s not doing what we want.

What we CAN do, however, is double-click this emitter to get into its guts.

We’re going to make a copy of this and paste it into a new dynamic scratch pad, or save a copy of it as a new script, or something similar; we are NOT going to modify this directly. Do not break the factory content beam emitter! That would be bad.

This is what you’ll find when you open the Update Beam script (I have moved some nodes around to fit it in one screenshot).

If you’ve never seen Niagara guts before this can be confusing, so I’ll do my best to explain what we’re doing. First, we’re using Map Get to essentially bring some variables in to this script from the emitter; in this case, it brings a bunch of beam parameters (incidentally, if you’ve ever wondered what was going on with the Unmet Dependency warnings you sometimes get, this is why; a Niagara Script can ask for parameters before they’ve been defined, and the Unmet Dependency warning is telling you that you need to add a module which will define them).

We’re also, notice, bringing in the RibbonLinkOrder. This, you may recall, is what the Spawn Beam module generates. It defines the particles to conform to a beam and then assigns them a Ribbon Link Order along the beam so they’re connected in order.

From here, it’s a few basic functions that compute the position along the beam spline, determine their world position, and then, at the output node, assign a new particle position based on RibbonLink Order.

So that output node is going to be our ticket here; we’re going to need to do something which changes what value is written to that if we want the position that our particles are assigned to to be different.

MAKING A FLOWING BEAM
As I mentioned above, with my whole pipe metaphor, the first thing we need to do is move away from a beam that defines particle positions manually from an initial spawn, and instead turn the beam into a “course” that particles flow down. The easiest way to do that is to stop explicitly defining their link order, and instead use a different input parameter to control their position along the beam.

So, go delete your Spawn Beam module.

Back in your Update Position scratch-pad, add a new input, for Particles.NormalizedAge, and connect it up to where RibbonLinkOrder was connected.

Note that there are two places where this occurs, from two separate MapGet nodes. You’ll need to connect that NormalizedAge up to both the initial Multiply node, and the later “position along spline” node!

Now all we have to do is delete our Spawn Burst Instantaneous and add something that spawns particles in a flowing fashion, like a Spawn Rate. So go ahead and configure that.

(If you don’t see the beam, make sure you have the initialized particle configured to set a ribbon width)

Now, you should have a beam being output. But crucially, it is not a fixed beam. Rather, particles are regularly spawning one at a time, and as they age, they move (over their normalized 0-1 lifetime) from the top of the beam to the bottom, and then at the bottom they die. Their ribbon link order is the default that is assigned in chronological order as they spawn. We now have particles flowing through our “pipe”; what we want to do next is to change their behavior as they near the end of the pipe

LERPING AWAY FROM A METAPHOR
When I started this, I described the behavior of the beam as being like particles flowing through a pipe; but that metaphor isn’t exactly right. When something flows through a pipe, it conforms to the pipe entirely, until it reaches the end. You could do that, but my use case demands something proximal but not identical: lerping. That is to say, we don’t want the particles to become free at the end of the pipe, but rather, to transition gradually from conforming to the pipe to not conforming to the pipe as they traverse it.

Doing it this way allows our beam to gradually “bend” from its initial configuration to a trail configuration, rather than abruptly stopping in a freefall out the back. And to do this, we need to use the simple but powerful Lerp (linear interpolate) math.

Lerping, you ought to already know, is like a blend or crossfade between two values. Obviously, the first of those values is going to be the position as defined by the beam. And the factor that drives the lerp is going to be NormalizedAge, because we want the particles to lerp across the distance of our pipe, and we’re already using NormalizedAge to drive them down that pipe. But what is the second value going to be? What are we going to crossfade to?

The answer, pretty simply, is “whatever position the particle already had before we went to update it”.

The scratch pad Particle Update script we’re working on, recall, is telling each particle to move from its current position to some new position the following frame. We’re going to have each particle, essentially, determine how far towards this goal it should move based on its life. The older it is, the further it’s updated down the pipe, but also the less strongly it’s updated down the pipe, so the further it is from the pipe target position, the more it will lag behind it.

How do we define this? Just add another input to our first MapGet for Particle Position.

Note that in UE5 (this wasn’t always the case) “Position” and “Vector” are different things in Niagara. You’ll have to use a Position to Vector node to fix this.

And that’s it! We have our cursory configuration of a beam that becomes a trail towards the end!

SHOELACE TROUBLESHOOTING

The most common issue you’ll get is “shoelaces”. Shoelaces is a symptom of the ribbon link order not following the spawn order. The ribbon is attempting to connect itself back to the particles at the top of the chain, causing it to knot in on itself periodically.

Weirdly, just including a RibbonLinkOrder pin on your MapGet, EVEN IF IT ISN’T CONNECTED TO ANYTHING, will cause this to occur, presumably because your scratch pad is attempting to manipulate the value in some way. You absolutely MUST delete any and all instances of the RibbonLinkOrder, from both of the MapGet nodes in your scratch pad, to prevent this. It can be a real headache to figure out the first time. I dealt with a lot of shoelaces.

OTHER CONSIDERATIONS

There were some specific considerations I had to deal with when setting this up, to get my specific use cases working.

KILL VOLUME
If you’re trying to bend a jet without allowing it to expand or contract, you need to kill particles which get too far from the beam start. A spherical kill volume, set to inverted, can accomplish this. This way, the beam jet can bend as you move, but it won’t be elongated by leaving particles behind, as they die when they get too far, capping the jet’s total possible radius fairly effectively.

CURVE STRENGTH
You can add an Input parameter for a float curve, and then connect your NormalizedAge input to it. This way, instead of a linear blend between 0 and 1 across the life of a particle, you can control how quickly the particle moves through its pipe journey. You can actually use multiple curves for this as well; one can control the particle’s “pipe duration” and one can control its “blend strength”, which can allow you to independently manipulate the density of particles along your beam spline AND the tension with which they ease into the trail. By having these curves reach an output value of 1 before the normalized age of 1, you can ensure that particles have some duration of lifetime where they are totally ungoverned by the beam spline.

LIFETIME AND WARMUP
Remember, we are using normalized age to drive the particle along the beam. That means the default age of each particle not only controls how long it lives, but how quickly it moves from the top of the beam to the bottom. Curve strength manipulates this as well, but it will always be (necessarily) a non-zero number. It was helpful in my case to set up a warmup on the emitter, so that the beam spawned full of particles of varying ages, rather than taking a half-second to totally fill out the beam.

Anyway, that’s it! I hope someone out there has been frustrated trying to make an acetylene torch emitter and finds this helpful. And remember, Beam Start and Beam End are specified in the usual way; when I was using this to create a spooling-out cable, I set the beam start and end to be attached to a projectile and a static mesh socket. This let me use tension curves to fix the particles to the beam at both ends (pointing between source and target) with a gravity-and-collision driven ribbon between them. There are a lot of possibilities with this sort of beam lerp.

2 Likes