Smoothed-particle Hydrodynamics in Niagara

Edit, 6th June 2021: I have this working pretty well now. It was mostly an enormous amount of trial and error with the constants, with a little help from an Excel spreadsheet I made to visualise the smoothing kernels. The two main things I need to get working but am now having trouble with are:

  • implementing the rendering method Asher shows in the video. I can’t get my head around it as there are huge gaps in the explanation he gives;
  • getting it to work on mobile. GPU particles now work on mobile, at least with Vulkan enabled, and I can get simple GPU particle systems to work on a mobile 'phone (specifically, my Galaxy S21 Ultra). However, this SPH system won’t work at all. I wondered if it’s because Simulation Stages aren’t supported.

Edit, 6th June 2021 - update: Further testing would seem to confirm that Simulation Stages stop it working on mobile. With a very simple GPU particle system (one of the templates with the emitters changed to GPU) that works otherwise, adding an empty Simulation Stage causes the app to crash straight after the splash screen.

This is a real shame because it completely kills the core basis of the mobile game I wanted to develop.

Original post:

Hello. This is going to be quite a long post, sorry, but I think it’s worth it because it’s quite complicated and I would imagine quite a lot of other people would really like to be able to achieve this effect.

I’ve been attempting to replicate the fluid simulation in Niagara as shown by Asher Zhu here: [The Art of Illusion - Niagara Simulation Framework Overview][1]. Skip to 20:25 for the effect I’m after.

Seeing as he shows none of the Niagara system at all part from some of the bits for rendering it (as far as which I’ve yet to get), I’ve followed the article here: [link][2].

Now, I have it looking more or less like a fluid. However, it doesn’t really look anything like Asher’s. It’s rather unstable and will tend to sit for a few seconds with a region of higher pressure before exploding and then settling down. It also never develops any depth. All the particles, unless they’re flying about erratically, sit on the floor. The other problem is collision - I can’t see how Asher has managed to get such clean collisions with the environment. My signed distance fields are big, round and uneven and the particles never get anywhere near the walls.

The fourth image below shows it exploding just after it got to the third image and the fifth image is what it looks like after it finally settles down (as well as how far away from the walls the particles end up). The last image shows that it’s completely flat (this isn’t an issue with the volume of the box; I’ve tested that).

It’s difficult to show everything in the Niagara system on here but the crucial bit is the HLSL code:

OutVelocity = Velocity;
OutPosition = Position;
//CombinedForce = 0;
Density = 0;
float Pressure = 0;

float smoothingRadius = 1.0f;
float restDensity = 0.2f;
float viscosity = 0.018f;
float gas = 500.0f;

const float3 gravity = float3(0, 0, -98);
float pi = 3.141593;

//float radius;
//float mass;
//float drag;
//float damping = -0.5f;

int numParticles;
DirectReads.GetNumParticles(numParticles);

const float Poly6_constant = (315 / (64 * pi * pow(smoothingRadius, 9)));
const float Spiky_constant = (-45 / (pi * pow(smoothingRadius, 6)));

float3 forcePressure = float3(0,0,0);
float3 forceViscosity = float3(0,0,0);


#if GPU_SIMULATION

//Calculate the density of this particle based on the proximity of the other particles.
for (int i = 0; i < numParticles; ++i)
{
    bool myBool; //Temporary bool used to catch valid/invalid results for direct reads.

    // Get variables for SPH calculations
    float OtherMass;
    DirectReads.GetFloatByIndex<Attribute="Mass">(i, myBool, OtherMass);
    float3 OtherPosition;
    DirectReads.GetVectorByIndex<Attribute="Position">(i, myBool, OtherPosition);

    // Calculate the distance and direction between the target Particle and itself
    float distanceBetween = distance(OtherPosition, OutPosition);
    
    if (distanceBetween < smoothingRadius)
    {
    	Density += OtherMass * Poly6_constant * pow(smoothingRadius - distanceBetween, 3);
    }
}

//Avoid negative pressure by clamping density to reference value
Density = max(restDensity, Density);

//Calculate pressure
Pressure = gas * (Density - restDensity);

//Calculate the forces.
for (int i = 0; i < numParticles; ++i)
{
    if (i != InstanceId) //Only calculate the pressure-based force and Laplacian smoothing function if the other particle is not the current particle.)
    {
        bool myBool; //Temporary bool used to catch valid/invalid results for direct reads.

        // Get variables for SPH calculations.
        float OtherMass;
        DirectReads.GetFloatByIndex<Attribute="Mass">(i, myBool, OtherMass);
        float OtherDensity;
        DirectReads.GetFloatByIndex<Attribute="Density">(i, myBool, OtherDensity);
        float3 OtherPosition;
        DirectReads.GetVectorByIndex<Attribute="Position">(i, myBool, OtherPosition);
        float3 OtherVelocity;
        DirectReads.GetVectorByIndex<Attribute="Velocity">(i, myBool, OtherVelocity);

        float3 direction = OutPosition - OtherPosition;
        float3 normalisedVector = normalize(direction);
        float distanceBetween = distance(OtherPosition, OutPosition);

        if (distanceBetween > 0 && distanceBetween < smoothingRadius) //distanceBetween must be >0 to avoide a div0 error.
        {
            float OtherPressure = gas * (OtherDensity - restDensity);

            //Calculate particle pressure.
            forcePressure += -1 * Mass * normalisedVector * (Pressure + OtherPressure) / (2 * Density * OtherDensity) * Spiky_constant * pow(smoothingRadius - distanceBetween, 2);

            //Viscosity-based force computation with Laplacian smoothing function (W).
			   const float W = -(pow(distanceBetween, 3) / (2 * pow(smoothingRadius, 3))) + (pow(distanceBetween, 2) / pow(smoothingRadius, 2)) + (smoothingRadius / (2 * distanceBetween)) - 1;
            forceViscosity += viscosity * (OtherMass / Mass) * (1 / OtherDensity) * (OtherVelocity - Velocity) * W * normalisedVector;
            //forceViscosity += viscosity * (OtherMass / Mass) * (1 / OtherDensity) * (OtherVelocity - Velocity) * (45 / (pi * pow(smoothingRadius, 6))) * (smoothingRadius - distanceBetween);
        }
    }
}

//CombinedForce = forcePressure + forceViscosity + gravity;

//OutVelocity += DeltaTime * (((forcePressure + forceViscosity) / Density) +  gravity);
OutVelocity += DeltaTime * ((forcePressure + forceViscosity) / Density);
OutPosition += DeltaTime * OutVelocity;
#endif

This code does two loops through all the other particles in the system, one to calculate the pressure and one to calculate the forces. Then it outputs the velocity and position. Just like the article I linked to above and like some other things I’ve seen. Yet it simply doesn’t behave as shown in those resources.

I’ve looked at a few resources (articles, videos and academic research papers) and I’ve spent a fortnight experimenting, including trial and error on the values at the top of the code. I’m obviously missing something crucial. What can it be? I’m so frustrated now that any help would be much appreciated.

Have you tried reaching out to Asher about this? I commented under his videos and he answers quite fast. I really wish he would just share his work so everyone could benefit from it. UE4s lack of proper PBD is the reason I am thinking of switching to Unity and its OBI plugin.

Hi,

I’m trying the same thing, looks like your HLSL does not work right, because you iterate through all the particles. in HLSL you only check one particle against the neighbors. Can you upload your whole project and I will try to fix it. You can use google drive for uploading it.

Looking through the code I start to wonder why you use to seperate loops to iterate over all particles. Try putting your computations into a single loop as the article suggests and see if you get more stable results

I did ask a few questions under the video but Asher never replied. I do have this working pretty well now. The two main things I need to get working but am now having trouble with are the rendering and getting it to work on mobile.

Thanks. I have it working now, including the grid optimisation. I’m not sure what was wrong with my initial implementation of the optimisation but it works now.

Every implementation of SPH I’ve seen, in various articles and academic papers, uses two separate loops. One loop that includes the current particle to calculate the density and pressure, followed by a second loop that excludes the current particle to calculate the forces. You need to calculate the density and pressure first.

Hi, can you please contact me via email. I like to work together with you on this, so we can get everything working, I already made some fluid systems in other engines so I have a basic knowledge about this.

What did you end up tweaking to get them to behave properly? Mine still stick to the ground and don’t stack properly just as yours did before you got them working.

It’s mainly due to the scale of things in the engine. The constants need to be much bigger.

So the smoothing radius I use is 12, not 1. And the values in the Poly6_constant and Spiky_constant are in the thousands and tens of thousands, not the tens and hundreds.

I don’t want to be too prescriptive and give you my constants because they’re not perfect, and different values might work better for you. And besides, I learnt a lot about the behaviour of the system by experimenting with the values, so there’s quite a bit of value in doing that yourself. But I can save you some time by telling you to use much bigger numbers!

Oh thanks! I’m excited to get this working, thanks for saving me some time.

I’m super excited to see someone active on this problem and able to make it work. I also want to help (or at least follow along) with achieving Asher’s final effect, but I can’t quite get your code to run in my case. Do you plan on posting any final results of your work on this here or elsewhere?

Sounds plausible :slight_smile:

I found this GitHub page helpful in getting mine working:

It’s all built in Niagara and is close to Asher’s effect, the water shaders just need to be tweaked. It’s helpful to see something like this already put together before giving it a shot yourself.

Wow, thank you for this. However I’m not familiar with how to build UE4 myself through C++ and using git requests, I just use the launcher, so I don’t know if I’ll be able to get much out of this repository. I can at least see their emitter settings images, so I’ll start there. It’s certainly better than stabbing in the dark.

Perhaps in the future. For now, I’ve put this project to bed as the lack of support on mobile has killed it, and so my time will be best spent on other projects. Thanks though!

I just downloaded the repository as a zip, you don’t need to build anything as it’s just an unreal project with a .uproject file and all that. You can even just grab the SPH folder from the content folder and drop that into your own project.

I’ve just had a look at that. Unfortunately, opening any of the 3D maps or Niagara files immediately crashes the editor.

There are 3d fluid systems for Unity that work on mobile, maybe that would be an option for you. I think Unity is better for making mobile games.

I also tested the sandbox, thanks for the link! However the performance of the GPU SPH is not that good compared to other systems, I hope I can improve it. But it’s a great starting point. I still don’t understand why Asher is not posting this work as a github repo. He works for Epic anyway and if he releases his setup as early access version it’s totally fine if it’s not working perfectly.