Game Engine
๐ฆ Bounding Volumesโ
Fast broad-phase overlap tests for spatial queries.
AABB (Axis-Aligned Bounding Box)
var box = new AABB(new Vector(0, 0, 0), new Vector(2, 2, 2));
var box2 = AABB.FromCenterExtents(new Vector(5, 5, 5), new Vector(1, 1, 1));
Vector center = box.Center; // (1, 1, 1)
Vector half = box.HalfExtents; // (1, 1, 1)
bool inside = box.Contains(new Vector(1, 1, 1)); // true
Vector closest = box.ClosestPoint(new Vector(5, 1, 1)); // (2, 1, 1)
AABB merged = box.Merge(box2); // smallest box containing both
AABB padded = box.Expand(0.1); // uniform margin
BoundingSphere
var sphere = new BoundingSphere(new Vector(0, 0, 0), 5);
bool inside = sphere.Contains(new Vector(3, 0, 0)); // true
Vector closest = sphere.ClosestPoint(new Vector(10, 0, 0)); // (5, 0, 0)
BoundingSphere merged = sphere.Merge(otherSphere);
๐ Overlap Tests (Broad Phase)โ
bool hit = boxA.Intersects(boxB); // AABB vs AABB
bool hit2 = sphereA.Intersects(sphereB); // Sphere vs Sphere
bool hit3 = box.Intersects(sphere); // AABB vs Sphere
bool hit4 = sphere.Intersects(box); // Sphere vs AABB
๐ฏ Contact Generation (Narrow Phase)โ
Returns ContactPoint? โ null if no overlap. Normal points from A toward B.
// Sphere vs Sphere
ContactPoint? contact = sphereA.SphereSphereContact(sphereB);
// Sphere vs AABB
ContactPoint? contact2 = sphere.SphereAABBContact(box);
// AABB vs Sphere (reversed normal)
ContactPoint? contact3 = box.AABBSphereContact(sphere);
if (contact is ContactPoint c)
{
Vector pos = c.Position; // world-space contact point
Vector normal = c.Normal; // unit normal from A โ B
double depth = c.PenetrationDepth; // overlap distance
}
๐ฅ Collision Responseโ
Impulse-based resolution with angular effects, Coulomb friction, and Baumgarte positional correction.
var a = RigidBody.CreateSolidSphere(mass: 2, radius: 1);
a.Position = new Vector(0, 0, 0);
a.Velocity = new Vector(5, 0, 0);
var b = RigidBody.CreateSolidSphere(mass: 3, radius: 1);
b.Position = new Vector(3, 0, 0);
b.Velocity = new Vector(-2, 0, 0);
// Detect
var sA = new BoundingSphere(a.Position, 1);
var sB = new BoundingSphere(b.Position, 1);
var contact = sA.SphereSphereContact(sB);
if (contact is ContactPoint c)
{
// Resolve velocity (normal + friction impulse)
CollisionResponse.ResolveCollision(ref a, ref b, c,
restitution: 0.8, // 0 = sticky, 1 = fully elastic
friction: 0.3); // Coulomb friction coefficient
// Fix overlap (Baumgarte stabilization)
CollisionResponse.CorrectPositions(ref a, ref b, c,
correctionFraction: 0.4, // how aggressive (0.2โ0.8)
slop: 0.01); // ignore tiny penetrations
}
Properties of the impulse solver:
- Conserves linear momentum exactly
- Conserves kinetic energy when
restitution = 1.0 - Handles static (immovable) bodies โ only the dynamic body moves
- Includes angular velocity effects via
InverseInertiaTensorWorld - Friction impulse is clamped to the Coulomb cone (
|jt| โค ฮผยทjn)
๐ Constraints & Jointsโ
Sequential impulse solver for connected bodies. Each constraint corrects velocities iteratively until convergence.
Simulation loop with constraints:
// Setup
var bodies = new RigidBody[]
{
RigidBody.CreateStatic(new Vector(0, 0, 10)), // ceiling
RigidBody.CreateSolidSphere(mass: 1, radius: 0.2),
RigidBody.CreateSolidSphere(mass: 1, radius: 0.2),
};
bodies[1].Position = new Vector(0, 0, 7);
bodies[2].Position = new Vector(0, 0, 4);
var constraints = new IConstraint[]
{
new DistanceConstraint(0, 1, new Vector(0,0,0), new Vector(0,0,0), distance: 3),
new DistanceConstraint(1, 2, new Vector(0,0,0), new Vector(0,0,0), distance: 3),
};
// Each frame:
double dt = 0.001;
for (int i = 0; i < bodies.Length; i++)
if (!bodies[i].IsStatic)
bodies[i].Velocity += dt * new Vector(0, 0, -9.8);
ConstraintSolver.Solve(bodies, constraints, dt, iterations: 10);
for (int i = 0; i < bodies.Length; i++)
if (!bodies[i].IsStatic)
bodies[i].Position += dt * bodies[i].Velocity;
Constraint types:
// Distance โ rigid rod between two anchors
var rod = new DistanceConstraint(bodyA: 0, bodyB: 1,
localAnchorA: new Vector(0, 0, 0),
localAnchorB: new Vector(0, 0, 0),
distance: 3.0);
// Ball-socket โ shared pivot, free rotation (shoulder, ragdoll)
var socket = BallSocketJoint.FromWorldPivot(0, 1,
worldPivot: new Vector(0, 0, 4), bodies);
// Hinge โ single-axis rotation (door, elbow, wheel)
var hinge = HingeJoint.FromWorldPivot(0, 1,
worldPivot: new Vector(0, 0, 5),
hingeAxis: new Vector(0, 0, 1), bodies);
// Spring-damper โ soft elastic connection
var spring = new SpringJoint(0, 1,
new Vector(0, 0, 0), new Vector(0, 0, 0),
stiffness: 20, damping: 2, restLength: 1);
Solver properties:
- Sequential impulse method โ iterates for convergence (4โ20 iterations typical)
- Baumgarte positional stabilization prevents drift
- Handles mixed static + dynamic bodies
- Hard constraints (Distance, BallSocket, Hinge) iterated; soft constraints (SpringJoint) applied once
๐ PhysicsWorldโ
The main orchestrator โ manages bodies, constraints, collisions, and the full simulation pipeline in one call.
var world = new PhysicsWorld
{
Gravity = new Vector(0, 0, -9.8),
DefaultRestitution = 0.7,
DefaultFriction = 0.3,
SolverIterations = 10,
FixedTimeStep = 1.0 / 60.0,
};
// Add bodies (returns index for constraints/queries)
var floor = RigidBody.CreateStatic(new Vector(0, 0, 0));
int iFloor = world.AddBody(floor, boundingRadius: 100);
var ball = RigidBody.CreateSolidSphere(mass: 1, radius: 0.5);
ball.Position = new Vector(0, 0, 10);
int iBall = world.AddBody(ball, boundingRadius: 0.5);
// Add constraints
world.AddConstraint(new DistanceConstraint(0, 1,
new Vector(0, 0, 0), new Vector(0, 0, 0), distance: 5));
// Collision callback
world.OnCollision = (a, b, contact) =>
Console.WriteLine($"Collision: body {a} โ body {b}, depth={contact.PenetrationDepth:F3}");
// Modify bodies directly via ref
ref var b = ref world.Body(iBall);
b.Velocity = new Vector(3, 0, 0);
Simulation step โ full pipeline in one call:
// Option 1: Single fixed step
world.Step(dt: 0.01);
// Pipeline: gravity โ constraints โ positions โ broadphase โ collisions
// Option 2: Fixed-timestep accumulator (deterministic, framerate-independent)
int steps = world.Update(elapsed: deltaTime); // consumes time in fixed chunks
// Smooth rendering between physics steps
double alpha = world.Alpha; // interpolation factor [0, 1)
Broad phase algorithms:
// Default: sweep-and-prune on X axis โ O(n log n)
var world = new PhysicsWorld(); // uses SweepAndPruneBroadPhase
// Brute force โ O(nยฒ), simpler, for small scenes
var world2 = new PhysicsWorld(new BruteForceBroadPhase());