Visualizing physics using FireMonkey

By: Anders Ohlsson

Abstract: This article discusses the seemingly simple case of billiard physics. We will visualize collisions and roll using Delphi XE2 and FireMonkey.

    Prerequisites!

Having played a few games of pool (or billiards) doesn't hurt. ;)

    The math of billiards

We will be focusing on collisions between billiard balls. We'll also simulate friction between rolling balls and table, as well as simulating roll.

We will completely ignore things like English (side spin), slipping (zero friction), spin transfer, as well as just about everything else. Oh, no jump shots either (gravity).

You can find an excellent discussion on Billiard Physics here.

OK, are you ready to dust off some vector algebra? Collisions between two objects in 3 dimensions... Conservation of kinetic energy and conservation of momentum gives us the following velocities post impact:

         v1 = v1i - ((m2*c)/(m1 + m2))*(1 + Cr)*n

         v2 = v2i + ((m1*c)/(m1 + m2))*(1 + Cr)*n

where:

  • v1, v2 = Velocity vector of Ball 1 and Ball 2 just after collision
  • v1i, v2i = Initial velocity vectors of Ball 1 and Ball 2 just before collision
  • m1, m2 = mass of Ball 1 and Ball 2
  • Cr = Coefficient of restitution (Cr = 1 for elastic collision)
  • n = normal unit vector from Ball 1 to Ball 2
  • c = dot-product (n, v1i - v2i)

In Delphi this becomes:

normal := Vector3DSubtract(Ball1.Position.Vector, Ball2.Position.Vector);
normal := Vector3DNormalize(normal);

c := Vector3DDotProduct(normal,Vector3DSubtract(Ball1.Velocity,Ball2.Velocity));

Ball1.Velocity := Vector3DSubtract(Ball1.Velocity, Vector3DScale(normal,Ball2.Mass*c/(Ball1.Mass+Ball2.Mass)*(1+Cr)));

Ball2.Velocity := Vector3DAdd(Ball2.Velocity, Vector3DScale(normal,Ball1.Mass*c/(Ball1.Mass+Ball2.Mass)*(1+Cr)));

OK, so that takes care of the collisions between balls. Collision with the rail can be (over)simplified as simply reversing the component of the velocity vector that is perpendicular to the rail in question.

    Rolling, rolling, rolling...

If we ignored roll (on the table) we would have a bunch of billiard balls that didn't roll and the simulation would look really weird. I couldn't quite figure out how to do roll correctly, but the best I came up with was the following relationship:

         TDummyZ

          TDummyX

          TDummyY

          TBall

This allows me to rotate around each axis independently and avoids complicated vector rotations (please tell me how to do it if you know how).

What we need to do is to rotate the ball in the direction of travel. The direction of travel (in degrees) is given by:

         180/Pi*ArcTan2(Velocity.Y,Velocity.X);

In Delphi:

NewZ := 180/Pi*ArcTan2(Ball[n].Velocity.Y,Ball[n].Velocity.X);
Ball[n].DummyZ.RotationAngle.Z := NewZ;
Ball[n].DummyX.RotationAngle.Z := -NewZ;
rX := Ball[n].RotationAngle.X;
rY := Ball[n].DummyY.RotationAngle.Y;
Ball[n].DummyY.RotationAngle.Y := rY - Ball[n].Velocity.X*2/Pi;
Ball[n].RotationAngle.X := rX + Ball[n].Velocity.Y*2/Pi;

    Generating the ball textures

The coolest part besides collision handling is making the billiard balls appear real. The following piece of Delphi code generates a texture that we use for our billiard balls:

function MakeBallTexture(n : Integer) : TBitmap;
var
  BMP   : TBitmap;
  C     : TCanvas;
  Color : TAlphaColor;
begin
  BMP := TBitmap.Create(200,100);
  C := BMP.Canvas;

  C.BeginScene(nil);

  case n mod 8 of
    0 : Color := TAlphaColorRec.Yellow;
    1 : Color := TAlphaColorRec.Blue;
    2 : Color := TAlphaColorRec.Red;
    3 : Color := TAlphaColorRec.Violet;
    4 : Color := TAlphaColorRec.Orange;
    5 : Color := TAlphaColorRec.Green;
    6 : Color := TAlphaColorRec.Brown;
    7 : Color := TAlphaColorRec.Black;
  end;

  // Center fill
  C.Fill.Color := Color;
  C.Stroke.Color := Color;
  C.FillRect(RectF(0,30,200,70),0,0,[],1);

  // Top and bottom fill
  if n div 8 > 0 then begin
    C.Fill.Color := TAlphaColorRec.White;
    C.Stroke.Color := TAlphaColorRec.White;
  end;
  C.FillRect(RectF(0,0,200,30),0,0,[],1);
  C.FillRect(RectF(0,70,200,100),0,0,[],1);

  // Center circles and numbers
  C.Fill.Color := TAlphaColorRec.White;
  C.Stroke.Color := TAlphaColorRec.Black;
  C.FillEllipse(RectF(35,35,65,65),1);
  C.FillEllipse(RectF(135,35,165,65),1);
  C.Fill.Color := TAlphaColorRec.Black;
  C.Stroke.Color := TAlphaColorRec.Black;
  C.Font.Size := 24;
  C.FillText(RectF(135,35,165,65),IntToStr(n+1),False,1,[],TTextAlign.taCenter);
  C.FillText(RectF(35,35,65,65),IntToStr(n+1),False,1,[],TTextAlign.taCenter);

  C.EndScene;

  Result := BMP;
end;

In case you're wondering, this produces the following 8-ball texture...

Hide image
Ball8

And the following 15-ball texture...

Hide image
Ball15

    Screen shots

The screen shots below show the demo application. Before hitting the cue ball, final stage of simulation of a perfect scenario, as well as the final stage rotated around.

Hide image
Click to see full-sized imageHide image
Click to see full-sized imageHide image
Click to see full-sized image

    Demo application

You can find my demo application in CodeCentral. You can play around with a lot of things in this demo. Feel free to elevate the cue ball above the table and watch the collision happen in 3 dimensions (velocity in Z-axis as well).

    Contact

Please feel free to email me with feedback to aohlsson at embarcadero dot com.

Server Response from: ETNASC02