Box2D Fluid (Part 3)

Integrating the fluid simulation with the physics engine.

Source: FluidPort_3.zip
Github: klutch/Box2DFluid

This is the third in a series of posts that will explain how to add a fluid simulation to your Box2D game. I will be using C#, XNA and the Farseer Physics Engine.

Introduction

This post will deal with integrating Box2D with our fluid demo. Box2D.XNA works just fine, but doesn’t seem to be maintained anymore, so I’m going to be using the Farseer Physics Engine.

Integrating Box2D

Download and build the latest Farseer source. Add a reference to the FarseerPhysicsXNA.dll you just built (to add a reference in Visual Studio 2010, you just right click on “References” in your Solution Explorer and click “Add Reference…”, then click the “Browse” tab and navigate to the .dll).

Creating the World

We need to create an instance of the World class, and create a couple of properties that are required (scale and delta time). At the top of Game1.cs, add the following using statement:

using FarseerPhysics.Dynamics;

Add the following member variables to Game1:

private World _world;
public const float SCALE = 35f;
public const float DT = 1f / 60f;

We want the Box2D World scale to match the fluid simulation’s scale, so remove the _scale variable from FluidSimulation, and replace all references to _scale with Game1.SCALE.

At the top of Initialize(), initialize the world:

_world = new World(new Vector2(0, 9.8f));

We also need to add a call to World ’s Step method in Update:

_world.Step(DT);

Setting up the Debug View

We need to implement the DebugView class so we can see what we’re doing, but doing that from scratch is lot of work. So instead, lets just copy the one that comes with Farseer’s samples. Open up Farseer’s samples solution, open DebugViewXNA.cs in the “DebugView XNA” solution. Copy all the code to the clipboard. Add a new class called DebugViewXNA to the FluidPort solution, and paste all the code in there. We need to change the namespace from FarseerPhysics.DebugViews to FluidPort, and add a using statement at the top:

using FarseerPhysics;

Repeat those steps for PrimitiveBatch.

Now we need to create an instance of DebugViewXNA. Add the following member variable to Game1:

private DebugViewXNA _debugView;

In Initialize(), after _world has been instantiated, add the following:

_debugView = new DebugViewXNA(_world);
_debugView.AppendFlags(FarseerPhysics.DebugViewFlags.Shape);
_debugView.DefaultShapeColor = Color.White;
_debugView.SleepingShapeColor = Color.LightGray;
_debugView.LoadContent(GraphicsDevice, Content);

DebugViewXNA ’s RenderDebugData method needs a projection and view matrix, so lets create those. Add the following member variables to Game1:

private Matrix _projection;
private Matrix _view;

And at the bottom of Initialize(), add:

_projection = Matrix.CreateOrthographic(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1);
_view = Matrix.CreateScale(new Vector3(SCALE, -SCALE, 1));

Note: You don’t have to invert the y axis, I just do it as a personal preference to make the world coordinates resemble the screen coordinates. If you don’t want to invert the y axis, just use Matrix.CreateScale(_scale) instead, and change the gravity vector used to initialize the world to new Vector2(0, -9.8f).

In Draw(), add:

_debugView.RenderDebugData(ref _projection, ref _view);

DebugViewXNA wants to load a SpriteFont called “font”, so go ahead and create one in your content projection.

Fixing Coordinates

I found a couple of bugs in the previous post that will need to be addressed in this post. In order for particle rendering to match up with the Box2D geometry that we’ll create later, the particles need to be offset by half of the screen’s size. So create a new member variable in FluidSimulation called _halfScreen:

private Vector2 _halfScreen;

In the draw method, change the line in the for loop that draws the particles to this:

_spriteBatch.Draw(_pixel, (particle.position * Game1.SCALE) + _halfScreen, new Rectangle(0, 0, 2, 2), Color.LightBlue, 0f, new Vector2(1, 1), 1f, SpriteEffects.None, 0f);

The mouse position also needs to be changed. In update, remove the following line:

_mouse = new Vector2(mouseState.X, mouseState.Y) / Game1.SCALE;

and replace it with:

_halfScreen = new Vector2(
    _spriteBatch.GraphicsDevice.Viewport.Width,
    _spriteBatch.GraphicsDevice.Viewport.Height) / 2f;
_mouse = (new Vector2(mouseState.X, mouseState.Y) - _halfScreen) / Game1.SCALE;

Creating Test Geometry

Now we need to create some bodies for our fluid to interact with. Add the following using statements to the top of Game1:

using FarseerPhysics.Factories;
using FarseerPhysics.Collision.Shapes;

Now, in the bottom of Initialize(), add the following:

// Create test geometry
Body bottomBody = BodyFactory.CreateBody(_world, new Vector2(0, 7f));
PolygonShape bottomShape = new PolygonShape(1f);
bottomShape.SetAsBox(12f, 1f);
bottomBody.CreateFixture(bottomShape);

Body leftBody = BodyFactory.CreateBody(_world, new Vector2(-11f, 0f));
PolygonShape leftShape = new PolygonShape(1f);
leftShape.SetAsBox(1f, 10f);
leftBody.CreateFixture(leftShape);

Body rightBody = BodyFactory.CreateBody(_world, new Vector2(11f, 0f));
PolygonShape rightShape = new PolygonShape(1f);
rightShape.SetAsBox(1f, 10f);
rightBody.CreateFixture(rightShape);

Body rampBody = BodyFactory.CreateBody(_world, new Vector2(6f, -2f));
PolygonShape rampShape = new PolygonShape(1f);
rampShape.SetAsBox(5.5f, 0.5f);
rampBody.CreateFixture(rampShape);
rampBody.Rotation = -0.25f;

Body circleBody = BodyFactory.CreateBody(_world, new Vector2(0, -0.2f));
CircleShape circleShape = new CircleShape(2f, 1f);
circleBody.CreateFixture(circleShape);

If you compile and run the program now, you should see something like this:

Applying Gravity

We’re going to want the particles to actually be affected by gravity now that we’re adding collisions that will prevent them from immediately falling off the screen.

Create a new vector in FluidSimulation called _gravity:

private Vector2 _gravity = new Vector2(0, 9.8f) / 3000;

In calculateForce, add the following to the line just before return accumulatedDelta:

// Apply gravitational force
particle.velocity += _gravity;

Now we need to modify the moveParticle method. Remove the following code:

// Move particle
particle.position += _delta[index] / MULTIPLIER;
particle.velocity += _delta[index] / (MULTIPLIER * DT);

And replace it with:

// Update velocity
particle.velocity += _delta[index];

// Update position
particle.position += _delta[index];
particle.position += particle.velocity;

We also need to change the call to calculateForce, and divide the accumulatedDelta values by MULTIPLIER. In update, in the Parallel.For call to calculateForce, change this line:

_delta[index] += accumulatedDelta[index];

to this:

_delta[index] += accumulatedDelta[index] / MULTIPLIER;

Resolving Collisions

The approach I’m going to take for resolving collisions may seem a little over-engineered at first, but it’s the approach I’ve found to work the best when dealing with scrolling levels and a large amount of fixtures.

Basically, we’re going to keep an AABB (axis-aligned bounding box) slightly larger than the screen. The AABB will be used to determine which fixtures need to be tested for particle collisions (later on the AABB will be used to flag particles as on/off screen, which will determine whether or not they should be included in the simulation, but that’s out of the scope of this tutorial). We’ll store the fixtures that need to be tested in the particle, and then resolve the collisions as needed.

We could just test every single particle for collisions using World ‘s QueryAABB method, but there’s a couple of downsides to that approach:

To clarify, the process for resolving collisions will be:

  1. Query the world using the simulation AABB.
    1. For every fixture found, get its bounding box.
    2. Loop through every grid cell inside the fixture’s bounding box, storing that fixture in any particles that exist in those cells.
  2. Calculate pressure and forces.
  3. Loop through each particle
    1. Loop through each fixture stored in that particle
      1. Test the particle’s new position using Fixture ’s TestPoint method.
      2. Modify the particle’s position and velocity if inside the fixture
  4. Move the particles

Simulation AABB

First, lets create a boundary for the simulation. Add the following using statement to FluidSimulation.cs:

using FarseerPhysics.Collision;

Add the following member variable to FluidSimulation:

private AABB _simulationAABB;

Now we need to define the boundary. Add the following to the bottom of the FluidSimulation constructor:

_halfScreen = new Vector2(
    _spriteBatch.GraphicsDevice.Viewport.Width,
    _spriteBatch.GraphicsDevice.Viewport.Height) / 2f;
_simulationAABB.LowerBound.X = -(_halfScreen.X + 100f);
_simulationAABB.LowerBound.Y = -(_halfScreen.Y + 100f);
_simulationAABB.UpperBound.X = _halfScreen.X + 100f;
_simulationAABB.UpperBound.Y = _halfScreen.Y + 100f;

This creates a boundary slightly larger than the screen. Eventually we’ll want to update the boundary every frame based on the view’s position in the world, but for now the view isn’t moving and we can just define it once.

Prepare Collisions

Now we need to query the Box2D world, and tell the particles which fixtures they’ll need to be tested against.

Add the following member variables to the Particle class to hold the fixtures:

public const int MAX_FIXTURES_TO_TEST = 20;
public Fixture[] fixturesToTest;
public int numFixturesToTest;

We’ll need to add the following using statement to Particle.cs:

using FarseerPhysics.Dynamics;

In the Particle constructor, initialize the fixturesToTest array:

fixturesToTest = new Fixture[MAX_FIXTURES_TO_TEST];

We’ll need to have access to the World instance we created in Game1, so modify the FluidSimulation constructor to take a World parameter:

private World _world;
// ...
public FluidSimulation(World world, SpriteBatch spriteBatch, SpriteFont font)
{
    _world = world;
    // ...
}

Update Game1 to reflect those changes:

_fluidSimulation = new FluidSimulation(_world, _spriteBatch, _debugFont);

Now we need to write the prepareCollisions method in FluidSimulation, but first add these using statements:

using FarseerPhysics.Dynamics;
using FarseerPhysics.Common;

Now, create the method:

// prepareCollisions
private void prepareCollisions()
{
    Dictionary<int, List<int>> collisionGridX;
    List<int> collisionGridY;

    // Query the world using the screen's AABB
    _world.QueryAABB((Fixture fixture) =>
        {
            AABB aabb;
            Transform transform;
            fixture.Body.GetTransform(out transform);
            fixture.Shape.ComputeAABB(out aabb, ref transform, 0);

            // Get the top left corner of the AABB in grid coordinates
            int Ax = getGridX(aabb.LowerBound.X);
            int Ay = getGridY(aabb.LowerBound.Y);

            // Get the bottom right corner of the AABB in grid coordinates
            int Bx = getGridX(aabb.UpperBound.X) + 1;
            int By = getGridY(aabb.UpperBound.Y) + 1;

            // Loop through all the grid cells in the fixture's AABB
            for (int i = Ax; i < Bx; i++)
            {
                for (int j = Ay; j < By; j++)
                {
                    if (_grid.TryGetValue(i, out collisionGridX) && collisionGridX.TryGetValue(j, out collisionGridY))
                    {
                        // Tell any particles we find that this fixture should be tested
                        for (int k = 0; k < collisionGridY.Count; k++)
                        {
                            Particle particle = _liquid[collisionGridY[k]];
                            if (particle.numFixturesToTest < Particle.MAX_FIXTURES_TO_TEST)
                            {
                                particle.fixturesToTest[particle.numFixturesToTest] = fixture;
                                particle.numFixturesToTest++;
                            }
                        }
                    }
                }
            }

            return true;
        },
        ref _simulationAABB);
}

This illustration might help clarify what this method is doing.

By converting the fixture’s AABB into a range of grid cells, we can get the particles inside a fixture’s AABB. Then we tell the particle which fixture needs to be tested.

We’re going to need to reset which fixtures the particle tests every frame, so in prepareSimulation, add this:

// Reset collision information
particle.numFixturesToTest = 0;

Now, add a call to prepareCollisions in FluidSimulation ’s Update method, directly after the call to prepareSimulation:

// Prepare collisions
prepareCollisions();

We’re almost ready to test particle positions, and correct their position/velocities as necessary.

Resolving the Collisions

This section involves a decent amount of math (mainly the dot product). If you’re not familiar with the dot product, there are a few videos on the Khan Academy website that might make things a little clearer (dot product, projection).
I’m not great at math myself, and should note that the majority of this code is based on this Box2D forum post.

Before we get into writing the resolveCollisions method, we need to create a couple more variables on the Particle class. In order to resolve collisions against polygons, we’re going to have to perform some calculations with the polygons’ edges and the normals of those edges. So add the following member variables to the Particle class:

public Vector2[] collisionVertices;
public Vector2[] collisionNormals;

Initialize the collisionVertices and collisionNormals arrays in Particle ’s constructor:

collisionVertices = new Vector2[Settings.MaxPolygonVertices];
collisionNormals = new Vector2[Settings.MaxPolygonVertices];

In order to reference Settings, you’ll need to add another using statement to Particle.cs: using FarseerPhysics;

Add the following using statement to FluidSimulation.cs, so we can access Shape types in our resolveCollisions method:

using FarseerPhysics.Collision.Shapes;

Now create the resolveCollisions method:

// resolveCollisions
private void resolveCollision(int index)
{
    Particle particle = _liquid[index];

    // Test all fixtures stored in this particle
    for (int i = 0; i < particle.numFixturesToTest; i++)
    {
        Fixture fixture = particle.fixturesToTest[i];

        // Determine where the particle will be after being moved
        Vector2 newPosition = particle.position + particle.velocity + _delta[index];

        // Test to see if the new particle position is inside the fixture
        if (fixture.TestPoint(ref newPosition))
        {
            Body body = fixture.Body;
            Vector2 closestPoint = Vector2.Zero;
            Vector2 normal = Vector2.Zero;

            // Resolve collisions differently based on what type of shape they are
            if (fixture.ShapeType == ShapeType.Polygon)
            {
                PolygonShape shape = fixture.Shape as PolygonShape;
                Transform collisionXF;
                body.GetTransform(out collisionXF);

                for (int v = 0; v < shape.Vertices.Count; v++)
                {
                    // Transform the shape's vertices from local space to world space
                    particle.collisionVertices[v] = MathUtils.Multiply(ref collisionXF, shape.Vertices[v]);

                    // Transform the shape's normals using the rotation matrix
                    particle.collisionNormals[v] = MathUtils.Multiply(ref collisionXF.R, shape.Normals[v]);
                }

                // Find closest edge
                float shortestDistance = 9999999f;
                for (int v = 0; v < shape.Vertices.Count; v++)
                {
                    // Project the vertex position relative to the particle position onto the edge's normal to find the distance
                    float distance = Vector2.Dot(particle.collisionNormals[v], particle.collisionVertices[v] - particle.position);
                    if (distance < shortestDistance)
                    {
                        // Store the shortest distance
                        shortestDistance = distance;

                        // Push the particle out of the shape in the direction of the closest edge's normal
                        closestPoint = particle.collisionNormals[v] * (distance) + particle.position;
                        normal = particle.collisionNormals[v];
                    }
                }
                particle.position = closestPoint + 0.05f * normal;
            }
            else if (fixture.ShapeType == ShapeType.Circle)
            {
                // Push the particle out of the circle by normalizing the circle's center relative to the particle position,
                // and pushing the particle out in the direction of the normal
                CircleShape shape = fixture.Shape as CircleShape;
                Vector2 center = shape.Position + body.Position;
                Vector2 difference = particle.position - center;
                normal = difference;
                normal.Normalize();
                closestPoint = center + difference * (shape.Radius / difference.Length());
                particle.position = closestPoint + 0.05f * normal;
            }

            // Update velocity
            particle.velocity = (particle.velocity - 1.2f * Vector2.Dot(particle.velocity, normal) * normal) * 0.85f;

            // Reset delta
            _delta[index] = Vector2.Zero;
        }
    }
}

What we’re doing is looping through all of the fixtures that were stored in the particle in prepareCollision. Then we’re testing the particle’s new position against the fixture to see if it is inside the fixture. If it is, we push the particle out of the shape. We use different approaches based on what type of shape it is. I’ll explain how we push it out of a Polygon first.

We need to find which edge on the polygon is closest to the particle’s position. We do this by looping through the shape’s vertices, and calculating the vertices positions relative to the particle’s position. We then project that vector onto that edge’s normal, and compare the value. The edge with the smallest dot product value is the closest edge. Here is an illustration that will hopefully make that idea a little clearer:

Pushing particles out of a Circle is a little easier. All we have to do is find the particle’s position relative to the center of the circle, and push the particle out in that direction.

After pushing the particle out of the shape, we update the velocity by using the reflection formula:

V' = (V - 2 * Dot(V, N) * N)

The 2 in that equation affects the “bounciness” or “restitution” of the reflection. I lowered it to 1.2, so that the particles will hardly “bounce” at all. I also multiply the end result by 0.85 so that the particles slow down a little bit after hitting an edge.

Since we’re manually correcting the position and velocity of particles, we set the particle’s delta to zero.

Finally, we need to add a call in update to resolveCollisions. Between the call to calculateForce and moveParticle, add the following:

// Resolve collisions
Parallel.For(0, _numActiveParticles, i => resolveCollision(_activeParticles[i]));

Result

This is the behavior you should see now when you compile and run the program:

yt. 520BH_JfylY

There are still some improvements to be done such as having particles influence dynamic bodies, limit particle pressures, add a small margin to TestPoint(), etc… But this post is already really long, so I’ll cover those in the next post.

Thanks for reading, and please let me know if you spot any errors or have any questions!