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.
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.
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).
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);
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.
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;
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:
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;
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:
Fixture
’s TestPoint
method.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.
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.
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]));
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!