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.