For a while I’d been planning to look into making looping particle systems in Houdini, but hadn’t found a good excuse to jump in. I don’t really do much VFX related work at the best of times, something I need to do more of in the future 🙂
Anyway, I was recently chatting with Martin Kepplinger, who is working on Clans Of Reign, and he was looking to do a similar thing!
So begins the looping particle journey…
Technique overview
I won’t go into the fluid sim setup, it doesn’t really matter too much what it is.
There are a few conditions that make my approach work:
- Particles must have a fixed lifetime
- The first chosen frame of the simulation must have a lead up number of frames >= the particle lifetime
- The last frame of the loop must be >= the first frame number + the particle lifetime
I have some ideas about how to get rid of these requirements, but not sure if I’ll get back to that any time soon.
For the example in this post, I am keeping particle lifetime pretty low (0.8 to 1.0 seconds, using a @life attribute on the source particles, so a 24 frame loop).
The fluid sim I’m using is some lumpy fluid going into a bowl:
The simulation is 400 frames long (not all shown here), but that ended up being overkill, I could have got away with a much shorter sim.
Going back to my rules, with particles that live 24 frames I must choose a first frame >= 24 (for this example, I’ll choose 44).
The last frame needs to be after frame 68, so I’m choosing 90.
This makes a loop that is 46 frames long, here it is with no blending:
The technique I’m going to use to improve the looping is somewhat like a crossfade.
For this loop from 44 –> 90, I’m modifying the particles in two ways:
- Deleting any particles that are spawning after frame 66 (i.e, making sure all particles have died before frame 90)
- From frames 66 to 90, copying in all the particles that spawn between frame 20 –> 44.
This guarantees that all the particles that are alive on frame 89 match exactly with frame 44.
To illustrate, this gif shows the unedited loop on the left, and next to it on the right is the loop with no new particles spawned after frame 66 (particles go red on 66):
Next up is the loop unedited on the left, and on the right are the new particles from frames 20 – 44 that I’m merging in from frame 66 onward:
And now, the unedited loop next to the previous red, green and blue particles combined:
And finally, just the result of the looping by itself without the point colours:
One thing that might be slightly confusing about the 2nd gif with the green pre-loop particles is that I’m always spawning particles in the bowl itself to keep the fluid level up, in case you were wondering what that was about 🙂
Setting up the loop
This is the SOPs network that takes the points of a particle sim, and makes it loop:
The first part of setting up the looping simulation is picking the start and end frames (first and last) of the loop.
I created a subnetwork (FrameFinder, near the top of the above screenshot) that has parameters for two frame numbers, and displays a preview of the two frames you are selecting, so you can find good candidates for looping:
The loop setup I chose for the Unity test at the top of the blog was actually a bit different to the range I chose for the breakdowns in the last section.
For Unity, I wanted the shortest looping segment I could, because I didn’t want it to be super expensive (memory wise), so I chose start and end frames 25 frames apart.
You can see that the frames don’t need to match exactly. The main thing I wanted to avoid was having a huge splash in the bowl at the bottom, or over the edge, because that would be hard to make look good in a short loop.
Node parameters
In the screenshot above, you can see that I have First Frame and Last Frame parameters on my FrameFinder network.
I don’t tend to make my blog posts very tutorial-y, but I thought I’d just take the time to mention that you can put parameters on any node in Houdini.
Example:
- Drop a subnetwork node
- Right click and select “Parameters and Channels –> Edit Parameter Interface…”:
- Select a parameter type from the left panel, click the arrow to add the parameter, and set defaults and the parameter name on the right:
- Voila! Happy new parameter:
You can then right click on the parameter and copy a reference to it, then use the reference in nodes in the subnetwork, etc.
In the edit parameter interface window, you can also create parameters “From Nodes”, which lets you pick an existing parameter of any node in the sub network to “bubble up” to the top, and it will hook up the references.
If this is new to you, I’d recommend looking up tutorials on Digital Assets (called “HDAs”, used to be called “OTLs”).
I do this all the time on subnetworks like on the FrameFinder, but also to add new fields to a node that contain parts of an expression that would get too big, or that I want to reference from other nodes, etc.
On Wrangles (for example), I find myself adding colour ramp parameters a lot for use within the wrangle code.
FrameFinder
This subnetwork has two outputs: the particles with some Detail Attributes set up, and another output that is a preview of the two frames which I showed before, but here’s what that looks like again:
It’s the first time I’ve created multiple outputs from a subnetwork, usually I just dump a “Preview” checkbox as a parameter on the network, but I like this more I think, particularly if I end up turning the whole thing into an HDA.
Here is what the FrameFinder network looks like:
In this project, I’m using Detail attributes to pass around a lot of data, and that starts in with the attribcreate_firstAndLastFrame node.
This node creates Detail parameters for each of the frames I chose in the subnet parameters (firstFrame and lastFrame):
Right under the attribCreate node, I’m using two timeshift nodes: one that shifts the simulation to the first chosen frame, and one to the last frame, and then I merge them together (for the preview output). I’ve grouped the lastFrame particles so that I can transform them off to the right to show them side by side, and also giving all the particles in both frames random colours, so it’s a little easier to see their shape.
Time ranges and ages
Back in the main network, straight after the frameFinder subnetwork I have another subnetwork called timeRangesAndAges, which is where I set up all the other Detail attributes I need, here is what is in that subnetwork:
The nodes in the network box on the right side are used to get the maximum age of any particle in the simulation.
In hindsight, this is rather redundant since I set the max age on the sim myself (you could replace all those nodes with an Attribute Create that makes a maxAge attribute with a value of 24), but I had planned to use it on simulations where particles are killed through interactions, etc 🙂
The first part of that is a solver that works out the max age of any particle in the simulation:
For the current frame of particles, it promotes Age from Point to Detail, using Maximum as the promotion method, giving me the maximum age for the current frame.
The solver merges in the previous frame, and then uses an attribute wrangle to get the maximum of the previous frame and current frame value:
@maxAge = max(@maxAge, @opinput1_maxAge);
Right after the solver, I’m timeshifting to the last frame, forcing the solver to run through the entire simulation so that the MaxAge attribute now contains the maximum age of any particle in the simulation (spolier: it’s 24 :P).
I then delete all the points, since all I care about is the detail attribute, and use a Stash node to cache that into the scene. With the points deleted, the node data is < 12 Kb, so the stash is just a convenient way to stop the maxAge recalculating all the time.
If I turn this whole thing into an HDA, I’ll have to rethink that (put “calculate max particle age” behind a button or something).
There are two more wrangle nodes in time ranges and ages.
One of them is a Point wrangle that converts the particle age from seconds into number of frames:
@frameAge = floor(@age/@TimeInc);
And the next is a Detail wrangle that sets up the rest of the detail attributes I use:
// Copy max age from input 1 int maxAge = i@opinput1_maxAge; // A bunch of helper detail variables referenced later int loopLength = i@lastFrame - i@firstFrame; int loopedFrame = (int)(@Frame-1) % (loopLength+1); i@remappedFrame = i@firstFrame + loopedFrame; int distanceFromSwap = loopedFrame - loopLength; i@blendToFrame = max(1, i@remappedFrame - loopLength); i@numberOfFramesIntoBlend = max(0, maxAge + distanceFromSwap);
When I hit play in the viewport, I want to see just the looping segment of the simulation over and over, so that complicates all this a little.
With that in mind, there are 3 important attributes set up here.
@remappedFrame
If my start frame is 20, for example, and the end frame is 60, I want to see the 20-60 frame segment over and over, hence the wrapping loopedFrame temporary variable.
So if my viewport time slider is on 15, I really want to see frame 34, the value of remappedFrame will be 34. It will always be a number between firstFrame and lastFrame.
@blendToFrame
This takes the remappedFrame, and shifts it back to before the start of the loop.
I only use this value when we hit the last 24 frames of the loop, but I’m clamping it at one just so the timeshift I use later doesn’t freak out.
This attribute will be used in the 2nd part of the technique: combining in pre-loop particles.
@numberOfFramesIntoBlend
When we are getting into the last 24 frames of the loop, this value increases from 0 to 24.
It’s used in the 1st part of the technique to stop spawning particles that have an age less than this value.
Timeshifts and recombining particles
Back to the main network:
After the timeRangesAndAges node, the network splits: on the left side, I’m timeshifting the simulation using the remappedFrame detail attribute as the frame using this expression:
detail("../timeRangesAndAges/", "remappedFrame", 0)
On the right side I’m time shifting the simulation using the blendToFrame attribute as the frame using this expression:
detail("../timeRangesAndAges/attribwrangle_calcTimeRanges", "blendToFrame", 0)
I’ve colour coded the nodes in the network with the same colours I’ve shown in the gifs in the technique section.
Since I’ve timeshifted the simulation, the detail attributes get time-shifted too.
But I don’t really want that, so I’m attribute transferring the two detail attributes I care about (remappedFrame and numberOfFramesIntoBlend) back onto the remapped sims using Attribute Transfer.
After the attribute transfers, on both sides I’m creating a new point group called inBlendingFrames.
detail(0, "numberOfFramesIntoBlend", 0) > 0
I probably didn’t need a point group for this, considering every particle is either in or out of this group on a given frame, it just made life easier with the Split node I use on the left.
On the left side, I do a split using inBlendingFrames.
When we’re not in the blending range, we don’t have to do anything to the particles, so that’s the blue colour node.
For both the red and green nodes section, I start with deleting anything not in the inBlendingFrames group.
For the green particles (the pre-loop particles that we’re merging in), we’ve already got the right frame, due to the timeshift up the top.
If we’re on blending frame 2 of the blend (for example), we will still have particles that were spawned 24 frames ago, but we really only want particles that should spawn after the blend starts.
I use an attribute wrangle to clean the older particles up, since I have the frameAge attribute I can use:
if ((@frameAge > i@numberOfFramesIntoBlend)) { removepoint(0, @ptnum); }
Here’s what that looks like for a frame about halfway through the blend.
For the red nodes section (where we take the original loop, and delete any particles that start spawning after the blend), I use an attribute wrangle to clean the new particles up:
if (@frameAge < i@numberOfFramesIntoBlend) { removepoint(0, @ptnum); }
So, I merge the red, blue and green particles all together, and we end up with the result I showed in the technique section!
Here again uncolourised:
Unity, Alembic and all that jazz
This post is already crazy long, so I’m just going to gloss over the Houdini –> Unity stuff.
If anyone is really interested in those details, I might do another post.
So now that I have a looping particle system, I can use a regular Particle Fluid Surface with default settings, and a polyreduce node to keep the complexity down:
I exported the range of frames as an Alembic file, and imported it into Unity with the Alembic plugin.
I threw together a really quick monoBehaviour to play the Alembic stream:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UTJ.Alembic; [RequireComponent(typeof(AlembicStreamPlayer))] public class PlayAlembic : MonoBehaviour { public float playSpeed = 0.02f; AlembicStreamPlayer sPlayer; // Use this for initialization void Start () { sPlayer = GetComponent(); } // Update is called once per frame void Update () { sPlayer.currentTime += playSpeed; sPlayer.currentTime = sPlayer.currentTime % 1.0f; } }
As a last little thing, I packaged the network up a lot neater, and dumped it in a loop that ran the process on 16 different 46 frame range segments of the original simulation.
The idea being, why try to find good first and last frames when you can just go for a coffee, and come back and have 16 to choose from!
The loops with big splashes definitely don’t work very well (they look like they aren’t looping because lots of particles dying on the same frame), but there are some fun examples in here: