controllerface / bvge

Personal game dev experiments
0 stars 0 forks source link

Explore Fluid Simulation Enchancements #85

Closed controllerface closed 1 month ago

controllerface commented 1 month ago

Right now, all the "liquid" being simulated is more or less just relying on the emergent behavior of the physics simulation plus some really minor "nudges" to make it move in a somewhat fluid like way, but it's really very basic.

Just for fun I worked through a portion of a youtube tutorial here which uses the concepts from a paper here to drive the simulation. It actually looks quite good, but of course being implemented in JS, and not parallelized in any way, I can't just drop it in as-is. However, I do think it may be possible to take this idea and integrate into the physics simulation I do have.

After going through the code and the paper, the real important logic is really the doubleDensityRelaxation(dt) function. The way it is applied in JS is serial, and has some aspects that make it tricky, and I am currently going through the logic with a fine tooth comb to try and see how I can map this into the system I already have in place, and will post notes as I go.

controllerface commented 1 month ago

For reference I will post the core function that handles the displacement that I wrote while doing the tutorial, will make it easier to refer to in later notes:

 doubleDensityRelaxation(dt)
    {
        for (let i =0; i < this.particles.length; i++)
        {
            let density = 0;
            let densityNear = 0;
            let neighbors =  this.fluidHashGrid.getNeighborOfParticleIdx(i);
            let particleA = this.particles[i];
            for (let j = 0; j < neighbors.length; j++)
            {
                let particleB = neighbors[j];
                if (particleA == particleB) continue;
                let dir = sub(particleB.position, particleA.position);
                let dist =  dir.Length() / this.INTERACTION_RADIUS;

                if (dist < 1)
                {
                    density += Math.pow(1-dist, 2);
                    densityNear += Math.pow(1-dist, 3);
                }
            }

            let pressure = this.K * (density - this.REST_DENSITY);
            let pressureNear = this.K_NEAR * densityNear;
            let particleADisplacement = Vector2.Zero();

            for (let j = 0; j < neighbors.length; j++)
            {
                let particleB = neighbors[j];
                if (particleA == particleB) continue;
                let dir = sub(particleB.position, particleA.position);
                let dist =  dir.Length() / this.INTERACTION_RADIUS;

                if (dist < 1.0)
                {
                    dir.Normalize();
                    let displacementTerm = Math.pow(dt, 2) * 
                        (pressure * (1 - dist) + pressureNear * Math.pow(1 - dist, 2)); 
                    let D = scale(dir, displacementTerm);
                    particleB.position = add(particleB.position, scale(D, 0.5));
                    particleADisplacement = sub(particleADisplacement, scale(D, 0.5));
                }
            }
            particleA.position = add(particleA.position, particleADisplacement);
        }
    }
controllerface commented 1 month ago

Interaction distance notes

An important part of this implementation is that it relies on being able to see neighbor particles that are withing an "interaction distance". This is used to determine the density and pressure values. In my physics sim, I have a grid based spatial system, similar to the approach the tutorial used. However, I currently filter out everything that isn't actually touching. So, in order to handle this case, I would need to make some changes that would apply only for liquid particles.

I think the most efficient way to handle this would be to increase the radius that is used for liquids when considering their bounding box. This would need to happen either separately from the current bounding check, or with some special handling of the case when liquids collide with non-liquids. I don't like this idea of making a separate hull type for liquids, but I have to consider it as a possibility only because there will be quite a bit of a difference in how they are handled. If I really do end up needing two bounding boxes for all liquids, that would kind of force my hand because I'd be spending a LOT of extra memory for all hulls to implement this only for liquids.

Alternatively, I may decide to do something where I have AABBs that apply to entire entities, which is something I have been considering for the future as is may help for implementing CDD. If I do that, I could use the entity AABB for liquid-liquid collisions and keep the hulls as-is for liquid/non-liquid ones. The entity AABB idea would already be a somewhat special case anyway, so adding logic there wouldn't be so difficult.

controllerface commented 1 month ago

Collision order notes

In my current simulation, I ensure that there's never any double collisions by checking the numerical values of the object indices. So for example when hull 0 is in the first position and hull 1 is in the second position, and these are a candidate collision pair, I allow it. But when hull 1 is first and 0 is second, I drop that candidate pair.

However, the double density relaxation technique kind of relies on the duplicates not being filtered out. When the density checks are done for particle 0, they are cumulative for that particle and then the final displacement is applied, and the same is true for other hulls. This would require a change to how this works in my simulation then, in order to work the same as I have implemented it in the JS code.

The makes me want to lean more into a separate AABB for liquids vs solids. I still want liquids to collide as they do now with solid objects and having duplicate reactions would very likely cause issues like extra movement being applied when liquids collide with solids, which I don't want.

Something else I'm going to have to consider is making sure to separate liquids from circular hulls in general. Right now, I do not make this distinction that clearly, and only really have special logic for it inside the collision function itself. However, the mouse cursor uses a circular hull and I would need to make sure it still operates as intended. I may also want circular hulls that aren't considered liquids to be supported, for example I have though about ropes being added at some, which would essentially be several circle entities connected by edge constraints.

This does sort of make me favor a separate set of kernels for liquids as it does look like the logic is going to deviate more and more from how solids work.

controllerface commented 1 month ago

Ok, gonna define a few terms so I can make sense of these notes later:

neighbor loop: in the double-density-relaxation algorithm, there are two loops in which a target particle searches for its neighbors and loops over them. There are two such loops. density loop: this is the first neighbor loop, where the density of the target particle is calculated. The final density is then used to calculate the pressure values. displacement loop: the second neighbor loop, where the pressure is used to calculate displacement values for the target particle, as well as all neighbors. A particle: this is the particle which has the cumulative displacement applied at the end, after the neighbor loops B particle: this is the particle which has the scaled displacement applied within the displacement loop

controllerface commented 1 month ago

Ok so, breaking things down a bit, I am pretty sure I have a way to deal with the A particle without any extra buffers, just a modified AABB kernel. In that kernel, I can implement both neighbor loops and since there is a guarantee that each hull only appears in the A position once per candidate pair, I can even apply the reaction at the end of the kernel.

Since there is no actual collision being done for the liquid-liquid checks, there is no need for an SAT kernel or anything like that either. In fact, applying the displacement to the A particle may even be pretty trivial. I did some tests in the JS code applying only this displacement (leaving out the B particle adjustment) and while it does have a few minor quirks, it honestly looks pretty good even without those B reactions. This is great because I should be able to wire that up first and just make sure it passes the basic visual test before trying to wire up the B reactions.

For the B particles, I think the best approach is probably going to be to define a new reaction buffer (or really, set of buffers) that work pretty much exactly as the SAT buffers work now, but just only store liquid based reactions in them. Then, a new liquid specific version of the apply_reactions kernel will be needed to accumulate and apply the displacement values.

The big unknown I have right now is how the dt value will work, since I use verlet integration, those calculations are done with dt * dt terms, but the algorithm is defined using only dt alone. I ran into weirdness with the friction and restitution implementations because of this, so I anticipate I may also run into similar issues here. I may be lucky though and since this process has no bearing on the integration step, it may just work out 🤞 but I don't know if I will be that lucky 😆

controllerface commented 1 month ago

Well... after a few hours of work and several different approaches, I think I am going to have to abandon this. While the JS code was pretty simple to write and I DO think it can be parallelized, unfortunately the way it functions is just too different from my current physics simulation approach. In order to really work, I would need to completely restructure how I do physics and entities in general. Perhaps in the future I will look into doing this in a separate project, perhaps build around it from the get go, but re-doing everything at this stage is too big of a project.