Back to Blog

Let's play billiard

A toy-like example on the real-time collisions resolution using Verlet Integration method

JerryIAugust 21, 2025
physicscollisionsmodelling

We shall start with the solver function. The algorithm is straightforward and does not require any special symbolic computations, therefore we write it in the procedural CPU-friendly style and compile it:

ClearAll[balls, compiled, visible, frameTrigger, angle, reset];
compiled = Compile[{
  {p, _Real, 3}
}, Module[{b = p},
    Do[

     (* Verlet Integration *)
     b[[3]] = b[[2]];
     b[[2]] = b[[1]];
     b[[1]] = 2 b[[2]] - b[[3]];

     (* Particle-particle collisions *)
     Do[
      Do[
        If[i < j,
          Module[{pi = b[[1, i]], pj = b[[1, j]], d, dist, n, overlap},
            d = pj - pi;
            dist = Norm[d];
            If[dist < 0.1, (* Effective Radius *)
              n = Normalize[d];
              overlap = 0.1 - dist;
              b[[1, i]] -= 0.5 overlap n;
              b[[1, j]] += 0.5 overlap n;
            ];
          ]
        ],
        {j, Length[b[[1]]]}
      ],
      {i, Length[b[[1]]]}
     ];   
     , {3}];

     b
  ], "RuntimeOptions" -> "Speed"];

Now we can model our scene. We define a group of stationary balls:

p = With[{s = 
  Join @@ Table[Table[{1-y,x}, {x, -1.0 + y,1.0 - y, 0.1}], {y, 0,1.0, 0.1}]
}, {s,s,s}];

An additional ball will act as the cue, to which we will assign velocity and acceleration in advance

p[[1, -1]] = {-2,0};
p[[2, -1]] = {-2.05,0};
p[[3, -1]] = {-2,0};

Putting it all together

Module[{p, pDraw},
  p = Module[{s = 
      Join @@ Table[Table[{1-y,x}, {x,-1.0 + y, 1.0 - y,0.1}], {y, 0,1.0,0.1}]
  },
      s = Append[s, {0,0}];
      {s,s,s}
  ];

  p[[1, -1]] = {-2,0};
  p[[2, -1]] = {-2.05,0};
  p[[3, -1]] = {-2,0};

  pDraw = p[[1]];

  Graphics[{
      PointSize[0.03],
      Pink, Point[pDraw // Offload],
      EventHandler[AnimationFrameListener[pDraw//Offload],
        Function[Null,
          p = compiled[p];
          pDraw = p[[1]];
        ]
      ]
    }
  , PlotRange->3{{-1,1}, {-1,1}}, 
    "Controls"->False,
    AspectRatio->1, ImageSize->Medium, 
    TransitionType->None
  ]
]
(*VB[*)(Graphics[{PointSize[0.03], RGBColor[1, 0.5, 0.5], Point[Offload[pDraw$575937]], AnimationFrameListener[Offload[pDraw$575937], "Event" -> "db81a23e-125b-47e9-bf8e-123aad937658"]}, PlotRange -> {{-3, 3}, {-3, 3}}, "Controls" -> False, AspectRatio -> 1, ImageSize -> Medium, TransitionType -> None])(*,*)(*"1:eJyNkc1OwzAMxwuML4E4ckRC2rWHbZR2pwkGG0h8dnuBdHVGpDSpkg4Ed16D59jjcOcZULFTFbQJCXL4y44dx/75MNExX/E8zzZQLrRM+QZ5WyhDw/IHMbG8UcevhC2q7G2UOy1UMRIvYOYHrx/387ceX6vfxsPTvpbaCMo2njvvvW+jKrJeF6ncTZRbzqVmqd1FOz8z7KkZhEG3E/JVythHOVEiY4XQamBYBtQRKDD/rUBTxDMJI/r7/BFwgCZaaRK1WLsDfqsdJP5RCF0/4RG5HcZSfH0cRIsFKgJSFzFTU/iJOUQLnvgsy1IQmb/vXWvEr69VYbS0jtGASQtL3+8QCpvDBBtAGo7zLx1eZmwKtCNLa72GVMyypbQ9NMaGKSuI6vg5Bxe70Qq+AB+kgQg="*)(*]VB*)

Apparently I don't understand something about billiards 🫤

Ah yes, the balls... Let's fix a couple of lines at the beginning

Module[{p, pDraw},
  p = Module[{s = 
    With[{r=0.04}, {d=2r}, Flatten[
     Table[
       {d i, (j - i/2) d Sqrt[3]},   
       {i, 0, 8},                    
       {j, 0, i}                    
     ],
     1
    ]]
  }, 
      s = Append[s, {0,0}];
      {s,s,s}
  ];

  p[[1, -1]] = {-2,0};
  p[[2, -1]] = {-2.05,0};
  p[[3, -1]] = {-2,0};

  pDraw = p[[1]];

  Graphics[{
      PointSize[0.03],
      Pink, Point[pDraw // Offload],
      EventHandler[AnimationFrameListener[pDraw//Offload],
        Function[Null,
          p = compiled[p];
          pDraw = p[[1]];
        ]
      ]
    }
  , PlotRange->3{{-1,1}, {-1,1}}, 
    "Controls"->False,
    AspectRatio->1, ImageSize->Medium, 
    TransitionType->None
  ]
]
(*VB[*)(Graphics[{PointSize[0.03], RGBColor[1, 0.5, 0.5], Point[Offload[pDraw$577457]], AnimationFrameListener[Offload[pDraw$577457], "Event" -> "b9296b6b-e9ec-46e0-bed8-5b5e9a2dd876"]}, PlotRange -> {{-3, 3}, {-3, 3}}, "Controls" -> False, AspectRatio -> 1, ImageSize -> Medium, TransitionType -> None])(*,*)(*"1:eJyNkUtOwzAQhgOUl0AsWSIhdRsJVU3SrCootCDxTHsBOx4XS44d2WkR7LkG5+hx2HMGFDyJAmqFBF78mvGM5/H5mOqEr3meZ1tOLrVkfAu9HScjQ/JHkVreauLXwhZ19q6Tey1UMRYvYBZHrx8Pi7c+32jeJqOzgZbaCMw2XnXe+99GXWSzKVK7207uOJeaMLvv7PzckKd2EEXdIOLrmHHo5FSJjBRCq6EhGeBEoMD8twJukcwkjLH3xRzcAm1n0bgThzSkPsSQ+t0QTnwKrOcHNICYdBjrReFygZqA1EVC1BR+YhWiJU98lmUpkMzf99VoyG+gVWG0tBWjIZEWVtrvIQqbQ+oGcDQqzr9MeJWRKeAfWfzWG2Bilq2kHThjYoiyAqlOnnOoYrdawRfAXoGq"*)(*]VB*)

That's better 🥳

Adding Interactivity

We can give the user an opportunity to knock the ball themselves. For this, we need to track the mouse position and adjust the angle accordingly. And let's add the boundaries as well

reset%20%3A%3D%20balls%20%3D%20Module%5B%7Bs%20%3D%20%0A%20%20With%5B%7Br%3D0.04%7D%2C%20%7Bd%3D2r%7D%2C%20Flatten%5B%0A%20%20%20Table%5B%0A%20%20%20%20%20%7Bd%20i%2C%20%28j%20-%20i%2F2%29%20d%20%28%2ASqB%5B%2A%29Sqrt%5B3%5D%28%2A%5DSqB%2A%29%7D%2C%20%20%0A%20%20%20%20%20%7Bi%2C%200%2C%208%7D%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%7Bj%2C%200%2C%20i%7D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%5D%2C%0A%20%20%201%0A%20%20%5D%5D%0A%7D%2C%20%0A%20%20%20%20s%20%3D%20Append%5Bs%2C%20%7B-2%2C0%7D%5D%3B%0A%20%20%20%20%7Bs%2Cs%2Cs%7D%0A%5D%3B%0A%0Areset%3B%0A%0Aangle%20%3D%200.%3B%0Avisible%20%3D%20balls%5B%5B1%5D%5D%3B%0A%0AframeTrigger%20%3D%20CreateUUID%5B%5D%3B%0A%0AEventHandler%5BframeTrigger%2C%20Function%5BNull%2C%0A%20%20%20%20balls%20%3D%20compiled%5Bballs%5D%3B%0A%20%20%20%20balls%5B%5B1%5D%5D%20%3D%20Map%5B%7BClip%5B%23%5B%5B1%5D%5D%2C%20%7B-3%2C3%7D%5D%2C%20Clip%5B%23%5B%5B2%5D%5D%2C%20%7B-2%2C2%7D%5D%7D%26%2C%20balls%5B%5B1%5D%5D%5D%3B%0A%20%20%20%20visible%20%3D%20balls%5B%5B1%5D%5D%3B%0A%5D%5D%3B%0A%0ALabeled%5BEventHandler%5BGraphics%5B%7B%0A%20%20%20%20LightBlue%2C%20Rectangle%5B%7B-3%2C-2%7D%2C%7B3%2C2%7D%5D%2C%20PointSize%5B0.03%5D%2C%0A%20%20%20%20Pink%2C%20Point%5Bvisible%20%2F%2F%20Offload%5D%2C%0A%20%20%20%20Black%2C%20AbsoluteThickness%5B2%5D%2C%20%0A%20%20%20%20Line%5BWith%5B%7Bx%20%3D%20%7BSin%5Bangle%5D%2C%20Cos%5Bangle%5D%7D%7D%2C%20%7B-3x%20%2B%20%7B-2%2C0%7D%2C%20-0.15%20x%20%2B%20%7B-2%2C0%7D%7D%5D%2F%2FOffload%5D%2C%0A%20%20%20%20AnimationFrameListener%5Bvisible%20%2F%2F%20Offload%2C%20%22Event%22-%3EframeTrigger%5D%0A%20%20%7D%0A%2C%20PlotRange-%3E3%7B%7B-1%2C1%7D%2C%20%7B-1%2C1%7D%7D%2C%20%0A%20%20AspectRatio-%3E1%2C%20ImageSize-%3EMedium%2C%0A%20%20ImagePadding-%3E10%2C%0A%20%20TransitionType-%3ENone%2C%20Controls-%3EFalse%0A%5D%2C%20%7B%0A%20%20%22mousemove%22%20-%3E%20Function%5Bxy%2C%20angle%20%3D%20VectorAngle%5B%28xy-balls%5B%5B-1%2C-1%5D%5D%29%2C%20%7B0%2C1%7D%5D%20%2F%2F%20N%5D%2C%20%0A%20%20%22click%22%20-%3E%20Function%5BNull%2C%20%0A%20%20%20%20balls%5B%5B1%2C-1%5D%5D%20%2B%3D%200.03%20%7B2.85%60%20Sin%5Bangle%5D%2C2.85%60%20Cos%5Bangle%5D%7D%3B%0A%20%20%5D%0A%7D%5D%2C%20Button%5B%22Reset%22%2C%20reset%5D%5D