How can this be made deterministic?
It looks like (at least part of) the issue is that the broad phase isn't being reset fully. It uses a temporally coherent algorithm, so the previous frame influences how the current frame's broad phase is built. Unfortunately, this can mean a collision pair is generated in a different order- that is, collidables A and B are swapped.
In collision detection, that difference in order can cause extremely small numerical differences. This is visible in the capsule's behavior, but it actually affects the sphere, too. The capsule just magnifies the effect due to its shape and more chaotic rotational behavior.
Here's two runs with a sphere:
Code: Select all
Frame 60: {0.8438894, 9.768148, -2.930157E-07}
Frame 120: {0.9153587, 7.004002, -9.766839E-07}
Frame 180: {1.166141, -0.6381797, -1.793709E-06}
Frame 240: {-3.461467, -2.013655, -3.395803E-06}
Frame 300: {-4.945804, -3.165692, -4.950115E-06}
Frame 360: {-3.452583, -20.23316, -6.366693E-06}
______
Frame 60: {0.8438894, 9.768148, -2.930157E-07}
Frame 120: {0.9153587, 7.004002, -9.766839E-07}
Frame 180: {1.166141, -0.6381797, -1.705321E-06}
Frame 240: {-3.461468, -2.013655, -3.310864E-06}
Frame 300: {-4.945806, -3.165688, -5.269535E-06}
Frame 360: {-3.452558, -20.23312, -7.225222E-06}
Here's four runs with the capsule:
Code: Select all
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307164}
Frame 240: {1.457408, -5.673208, 0.002140395}
Frame 300: {1.184698, -10.45159, 0.002349531}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902413, 6.274392, -3.698204E-05}
Frame 180: {2.012538, 0.7155204, -5.153625E-06}
Frame 240: {1.324261, -5.094629, -0.0008429385}
Frame 300: {0.8184211, -14.33493, -0.002961804}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902413, 6.274392, -3.698204E-05}
Frame 180: {2.012538, 0.7155204, -5.153625E-06}
Frame 240: {1.324261, -5.094629, -0.0008429385}
Frame 300: {0.8184211, -14.33493, -0.002961804}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902413, 6.274392, -3.513639E-05}
Frame 180: {1.956183, 0.8201287, 0.0007421858}
Frame 240: {1.569756, -5.83865, 0.001030846}
Frame 300: {3.530511, -14.99275, 0.001144263}
The two middle runs actually happen to be identical, courtesy of the pair orders lining up by chance.
To fully eliminate this sort of problem, the space should basically be reconstructed from scratch on each iteration. All elements must be inserted in exactly the same order as they were originally. Assuming multithreading is off, that should produce a deterministic result.
Indeed, if the space is created and recreated using this function in the given demo:
Code: Select all
private void CreateSpace()
{
Space = new BEPUphysics.Space();
Space.Solver.PermutationMapper.PermutationIndex = 0;
BEPUphysics.Settings.CollisionDetectionSettings.DefaultMargin = 0.04f;
BEPUphysics.Settings.CollisionResponseSettings.BouncinessVelocityThreshold = 1f;
BEPUphysics.Settings.CollisionResponseSettings.MaximumPenetrationRecoverySpeed = 10f;
BEPUphysics.Settings.CollisionResponseSettings.PenetrationRecoveryStiffness = 0.2f;
BEPUphysics.Settings.MotionSettings.CoreShapeScaling = 0.99f;
Space.ForceUpdater.Gravity = new Vector3(0f, -20f, 0f);
Space.TimeStepSettings.TimeStepDuration = 1f / 60f;
Space.BufferedStates.Enabled = false;
Space.Solver.IterationLimit = 20;
for (int x = -9; x <= 9; x += 3)
{
for (int y = -9; y <= 9; y += 3)
{
var peg = new Cylinder(new Vector3(x, y, 0), 1f, 0.5f);
peg.Orientation = Quaternion.CreateFromYawPitchRoll(MathHelper.PiOver2, 0, 0);
peg.Material.Bounciness = 0.828f;
Space.Add(peg);
}
}
entity = new Capsule(ballStartingPosition, 1, 0.5f, 1f);
//entity = new Sphere(ballStartingPosition, 0.5f, 1f);
entity.Material.Bounciness = 0.828f;
Space.Add(entity);
}
The simulation becomes fully deterministic:
Code: Select all
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
______
Frame 60: {0.649253, 10.35287, -2.569535E-06}
Frame 120: {1.902411, 6.274374, -3.512889E-05}
Frame 180: {1.959954, 0.8124294, 0.0007307049}
Frame 240: {1.457395, -5.673295, 0.00213943}
Frame 300: {1.191184, -10.46338, 0.002350604}
Technically, recreating the entire Space and readding everything is the brute force approach, but trying to deal with every single piece of bookkeeping in the engine would be more work than it's worth.
In situations where perfect determinism is required (especially when it's required across different computers, due to floating point implementation differences), it's often better to record the motion and play it back as an animation rather than to rerun the simulation.