Skip to main content

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());