P04: Physics Simulation with Constraints
P04: Physics Simulation with Constraints
Build a 2D physics engine that simulates rigid body dynamics with constraints (springs, joints, collisions)โrequiring you to solve systems of linear equations each frame.
Overview
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 3-4 weeks |
| Language | C (recommended) or Python |
| Prerequisites | Basic physics intuition, matrix operations |
| Primary Book | โGame Physics Engine Developmentโ by Ian Millington |
Learning Objectives
By completing this project, you will:
- See vectors as physical state - Position, velocity, and acceleration as mathematical objects
- Master Ax = b solvers - Gaussian elimination and iterative methods
- Understand projections geometrically - Collision response via vector projection
- Grasp matrix conditioning - Why some systems are โhardโ to solve
- Apply null spaces and least squares - Handle over/under-constrained systems
- Experience numerical stability - When math works on paper but fails in code
Theoretical Foundation
Part 1: Vectors as Physical State
Position, Velocity, Acceleration
Every physics simulation begins with vectors representing physical quantities:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PHYSICS VECTORS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Position (p) Velocity (v) Acceleration (a) โ
โ "where" "how fast" "how much speeding up" โ
โ โ
โ p = [x] v = dp/dt a = dv/dt = dยฒp/dtยฒ โ
โ [y] โ
โ โ
โ For a 2D point: โ
โ p = (3, 5) means 3 units right, 5 units up โ
โ v = (1, 0) means moving right at 1 unit/second โ
โ a = (0, -9.8) means accelerating downward (gravity) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Newtonโs Second Law in Vector Form
F = ma
Force vector โ acceleration vector โ velocity change โ position change
For multiple forces:
a = (1/m) ร ฮฃF = (1/m) ร (F_gravity + F_spring + F_collision + ...)
Each force is a vector; we sum them.
The State Vector
For a system of n particles, collect all positions and velocities:
State vector S:
[pโ] position of particle 1
[pโ] position of particle 2
S = [...]
[vโ] velocity of particle 1
[vโ] velocity of particle 2
[...]
For n particles in 2D: S has 4n components (2 position + 2 velocity each)
This state vector evolves over time according to physics equations.
Part 2: Numerical Integration
The Euler Method
The simplest integration scheme:
v_new = v_old + a ร ฮt
p_new = p_old + v_old ร ฮt
Or more accurately (semi-implicit Euler):
v_new = v_old + a ร ฮt
p_new = p_old + v_new ร ฮt โ use new velocity
Euler is simple but unstable - errors accumulate, springs explode.
Why Integration is Tricky
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INTEGRATION ERROR โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ True path Euler approximation โ
โ โ
โ โญโโโโโโโฎ โญโโโโโโโโโโโโโฎ โ
โ โฑ โฒ โฑ โฒ โ
โ โฑ โฒ โฑ โฒ โ
โ โฑ โฒ โฑ โฒ โ
โ โ โ
โ Each step overshoots โ
โ Error accumulates โ
โ โ
โ For springs/pendulums: Energy grows โ explosion! โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Better Methods: Verlet Integration
Position-based method that conserves energy better:
p_new = 2รp_current - p_old + a ร ฮtยฒ
Velocity is implicit: v โ (p_new - p_old) / (2รฮt)
Advantages:
- Second-order accurate (error โ ฮtยฒ)
- Energy-conserving (symplectic)
- Simple to implement
RK4 (Runge-Kutta 4th Order)
More accurate but expensive:
kโ = f(t, y)
kโ = f(t + ฮt/2, y + ฮtรkโ/2)
kโ = f(t + ฮt/2, y + ฮtรkโ/2)
kโ = f(t + ฮt, y + ฮtรkโ)
y_new = y + (ฮt/6) ร (kโ + 2kโ + 2kโ + kโ)
For this project, semi-implicit Euler or Verlet is sufficient.
Part 3: Collision Detection and Response
Detecting Collisions
For circles, collision detection is simple:
Circle A: center pA, radius rA
Circle B: center pB, radius rB
Distance: d = ||pA - pB||
Collision: d < rA + rB
Penetration depth: ฮด = (rA + rB) - d
Collision normal: n = (pB - pA) / d
For boxes, polygons, etc., it gets more complex (SAT, GJK).
Collision Response via Projection
The key linear algebra insight: collision response is projection!
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ COLLISION RESPONSE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Before collision: โ
โ โ
โ v (incoming velocity) โ
โ โ โ
โ โ โ
โ โโโโโโโโโโโโโโโโโโโ surface (normal n) โ
โ โ
โ Decompose v into components: โ
โ โ
โ v_normal = (v ยท n) ร n (component along normal) โ
โ v_tangent = v - v_normal (component along surface) โ
โ โ
โ For elastic bounce: โ
โ v_new = v_tangent - e ร v_normal (e = coefficient of โ
โ restitution) โ
โ โ
โ For e = 1 (perfectly elastic): โ
โ v_new = v - 2(v ยท n)n (reflection formula) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

The math:
Reflection: v_reflected = v - 2(v ยท n)n
This is the formula for reflecting a vector across a plane!
The dot product measures how much v points into the surface.
Multiplying by n and subtracting reverses that component.
Impulse-Based Collision
For realistic physics, we compute impulses (instantaneous momentum changes):
For two colliding bodies A and B:
Relative velocity: v_rel = vA - vB
Normal component: v_n = v_rel ยท n
If v_n > 0: bodies separating, no collision response needed
Impulse magnitude: j = -(1 + e) ร v_n / (1/mA + 1/mB)
Apply impulse:
vA_new = vA + (j/mA) ร n
vB_new = vB - (j/mB) ร n
Part 4: Constraints and Solving Ax = b
What is a Constraint?
A constraint is a restriction on how bodies can move:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TYPES OF CONSTRAINTS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Distance constraint (rod/rope): โ
โ ||pA - pB|| = L (maintain fixed distance) โ
โ โ
โ Point constraint (pin joint): โ
โ pA = pB (two points coincide) โ
โ โ
โ Angle constraint (hinge limit): โ
โ ฮธ_min โค ฮธ โค ฮธ_max (rotation bounded) โ
โ โ
โ Contact constraint (no penetration): โ
โ (pA - pB) ยท n โฅ 0 (inequality constraint) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Constraint Equations
Most constraints can be written as: C(x) = 0
For a distance constraint between particles at pโ and pโ:
C = ||pโ - pโ||ยฒ - Lยฒ = 0
Taking the derivative (for velocity-level constraint):
ฤ = 2(pโ - pโ) ยท (vโ - vโ) = 0
This says: relative velocity must be perpendicular to the connecting line.
The Jacobian Matrix
For multiple constraints on multiple bodies, we form the Jacobian:
C = [Cโ] Jacobian J = [โCโ/โxโ โCโ/โxโ ...]
[Cโ] [โCโ/โxโ โCโ/โxโ ...]
[...] [ ... ... ...]
The constraint becomes: J ร v = 0 (or = -baumgarte ร C for stability)
Where v is the velocity vector of all bodies.
Solving for Constraint Forces
To satisfy constraints, we need forces. The constraint force ฮป:
Total acceleration: a = Mโปยน(F_external + Jแตฮป)
Constraint: J ร a = b (where b comes from the constraint)
Substituting:
J ร Mโปยน ร Jแต ร ฮป = b - J ร Mโปยน ร F_external
This is a system: A ร ฮป = b
Where A = J ร Mโปยน ร Jแต (the constraint mass matrix)
This is the heart of constraint solving: solve Ax = b to find ฮป, then apply forces.
Part 5: Solving Systems of Linear Equations
The General Problem
A ร x = b
Given: A (nรn matrix), b (n vector)
Find: x (n vector)
For physics:
- A encodes how constraints couple to each other
- b encodes how much the constraints are violated
- x (ฮป) gives the correction forces
Gaussian Elimination
The classic direct method:
Step 1: Forward elimination (create upper triangular form)
Step 2: Back substitution (solve from bottom up)
Example:
[2 1 1 | 8] [2 1 1 | 8] [2 1 1 | 8]
[4 3 3 | 20] โ [0 1 1 | 4] โ [0 1 1 | 4]
[8 7 9 | 46] [0 3 5 | 14] [0 0 2 | 2]
Back substitute: z = 1, y = 3, x = 2
Complexity: O(nยณ) - too slow for large systems every frame!
LU Decomposition
Factor A once, solve quickly for different b:
A = L ร U (Lower triangular ร Upper triangular)
Solve: L ร y = b (forward substitution)
Then: U ร x = y (back substitution)
Both steps are O(nยฒ).
Useful when A doesnโt change (same constraints, different forces).
Iterative Methods: Gauss-Seidel
For constraint solving, iterative methods work well:
def gauss_seidel(A, b, x, iterations):
for _ in range(iterations):
for i in range(n):
sigma = sum(A[i][j] * x[j] for j in range(n) if j != i)
x[i] = (b[i] - sigma) / A[i][i]
return x
Why iterative works for physics:
- Constraints are local (sparse matrix)
- Approximate solution is often good enough
- We solve many times per second (converges over frames)
Projected Gauss-Seidel (PGS)
For inequality constraints (contacts), clamp solutions:
def pgs(A, b, x, lo, hi, iterations):
for _ in range(iterations):
for i in range(n):
delta = (b[i] - sum(A[i][j] * x[j] for j in range(n))) / A[i][i]
x[i] = clamp(x[i] + delta, lo[i], hi[i])
return x
This handles: โbodies can push apart but not pull togetherโ
Part 6: Matrix Conditioning and Numerical Stability
What is Condition Number?
The condition number ฮบ(A) measures how sensitive Ax = b is to perturbations:
If ฮบ(A) is small (close to 1): well-conditioned, stable
If ฮบ(A) is large: ill-conditioned, small errors โ large errors
ฮบ(A) = ||A|| ร ||Aโปยน|| = ฯ_max / ฯ_min (ratio of singular values)
Why Conditioning Matters for Physics
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ILL-CONDITIONED SYSTEMS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Scenario: Very stiff springs mixed with soft springs โ
โ โ
โ Stiff spring: k = 10000 โ
โ Soft spring: k = 1 โ
โ โ
โ Constraint matrix eigenvalues: 10000 and 1 โ
โ Condition number: ~10000 โ
โ โ
โ Result: Gauss-Seidel converges slowly โ
โ Numerical precision issues โ
โ Simulation may become unstable โ
โ โ
โ Fixes: โ
โ - Scale units to reduce spread โ
โ - Use regularization (add small diagonal) โ
โ - Use higher precision (double instead of float) โ
โ - More iterations โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Numerical Stability Tips
- Use double precision for constraint solving
- Add bias/regularization: A + ฮตI instead of A
- Warm starting: Use previous frameโs solution as initial guess
- Scale appropriately: Keep values in reasonable ranges
Part 7: Special Matrix Structures
Sparse Matrices
Physics matrices are typically sparse (mostly zeros):
For a chain of particles connected by springs:
[ร ร 0 0 0 0] ร = non-zero
[ร ร ร 0 0 0] 0 = zero
[0 ร ร ร 0 0]
[0 0 ร ร ร 0] Each particle only interacts with neighbors.
[0 0 0 ร ร ร]
[0 0 0 0 ร ร]
Store only non-zeros โ memory efficient
Skip zero multiplications โ faster solving
Symmetric Positive Definite (SPD)
Many physics matrices are SPD:
- Symmetric: A = Aแต
- Positive definite: xแตAx > 0 for all x โ 0
SPD matrices have:
- All positive eigenvalues
- Cholesky factorization: A = LLแต (faster than LU)
- Guaranteed convergence for iterative methods
Block Structure
For rigid bodies, matrices have block structure:
[Mโ 0 0 ] [vโ] [Fโ]
[0 Mโ 0 ] ร [vโ] = [Fโ]
[0 0 Mโ] [vโ] [Fโ]
Each Mแตข is a 3ร3 (or 2ร2 for 2D) mass/inertia matrix.
Block-diagonal structure allows efficient solving.
Part 8: Least Squares and Overdetermined Systems
When Ax = b Has No Exact Solution
If we have more constraints than degrees of freedom:
3 constraints, 2 unknowns:
[1 0] [3]
[0 1] ร x = [4]
[1 1] [8] โ inconsistent!
No x satisfies all three equations exactly.
The Least Squares Solution
| Minimize | ย | Ax - b | ย | ยฒ: |
x* = (AแตA)โปยน Aแต b (the normal equations)
This finds x that minimizes the sum of squared residuals.
For physics: โsatisfy constraints as well as possibleโ
Underdetermined Systems (Null Space)
More unknowns than constraints:
1 constraint, 2 unknowns:
[1 1] ร x = [5]
Infinite solutions: x = [5-t, t] for any t
The null space of A contains all directions of "free motion"
that don't violate constraints.
For physics: A system with slack has infinite valid configurations.
Part 9: Putting It Together - The Physics Loop
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PHYSICS SIMULATION LOOP โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ For each time step: โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 1. APPLY EXTERNAL FORCES โ โ
โ โ F_total = F_gravity + F_user_input + F_air_resistance + ... โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 2. INTEGRATE (predict new positions/velocities) โ โ
โ โ v_predicted = v + (F/m) ร ฮt โ โ
โ โ p_predicted = p + v_predicted ร ฮt โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 3. DETECT COLLISIONS โ โ
โ โ Find all overlapping/intersecting pairs โ โ
โ โ Compute collision points, normals, depths โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 4. BUILD CONSTRAINT SYSTEM โ โ
โ โ Assemble Jacobian J from all constraints โ โ
โ โ Form A = J Mโปยน Jแต and b = -J v - bias ร C โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 5. SOLVE FOR CONSTRAINT FORCES โ โ
โ โ Solve A ฮป = b using Gauss-Seidel or PGS โ โ
โ โ (possibly with warm starting from previous frame) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 6. APPLY CONSTRAINT IMPULSES โ โ
โ โ v = v + Mโปยน Jแต ฮป โ โ
โ โ (constraints now satisfied) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 7. UPDATE STATE โ โ
โ โ p = p + v ร ฮt (with corrected velocity) โ โ
โ โ Store for next frame โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Project Specification
What Youโre Building
A 2D physics simulator that:
- Simulates particles/circles under gravity
- Handles collisions with walls and other objects
- Supports spring/distance constraints
- Supports joint/pin constraints
- Renders in real-time
Minimum Requirements
- Particle system with gravity
- Semi-implicit Euler or Verlet integration
- Circle-circle collision detection and response
- Circle-wall collision (axis-aligned boundaries)
- Distance constraints (springs/rods)
- Simple constraint solver (Gauss-Seidel)
- Real-time rendering (SDL2 or similar)
Stretch Goals
- Rigid body rotation (not just particles)
- Friction in collisions
- Multiple joint types (hinges, sliders)
- Stacking and piles of objects
- Continuous collision detection
- Broad-phase acceleration (spatial hashing)
Solution Architecture
Data Structures
typedef struct {
Vec2 position;
Vec2 velocity;
float mass;
float inv_mass; // 1/mass, or 0 for static objects
float radius;
} Particle;
typedef struct {
int particle_a;
int particle_b;
float rest_length;
float stiffness;
} DistanceConstraint;
typedef struct {
int particle_a;
int particle_b;
Vec2 normal;
float penetration;
} Contact;
typedef struct {
Particle* particles;
int num_particles;
DistanceConstraint* constraints;
int num_constraints;
Contact* contacts;
int num_contacts;
Vec2 gravity;
float dt;
} PhysicsWorld;
Module Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ main.c โ
โ - Initialize world โ
โ - Main loop: update physics, render โ
โ - Handle input โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ
โผ โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ vec.c โ โ physics.c โ โ render.c โ
โ โ โ โ โ โ
โ vec2_add โ โ apply_forces โ โ draw_circle โ
โ vec2_sub โ โ integrate โ โ draw_line โ
โ vec2_scale โ โ detect_collisions โ โ draw_world โ
โ vec2_dot โ โ solve_constraints โ โ โ
โ vec2_length โ โ step_world โ โ โ
โ vec2_normalizeโ โ โ โ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ
โผ โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ collision.c โ โ constraint.cโ โ solver.c โ
โ โ โ โ โ โ
โ circle_circleโ โ distance_initโ โ gauss_seidel โ
โ circle_wall โ โ distance_solveโ โ build_matrix โ
โ find_contactsโ โ contact_solveโ โ solve_system โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ

Simulation Loop Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PHYSICS STEP IMPLEMENTATION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ void physics_step(PhysicsWorld* world) { โ
โ โ
โ // 1. Apply external forces (gravity) โ
โ for each particle p: โ
โ p.velocity += world.gravity * world.dt โ
โ โ
โ // 2. Predict positions (integrate) โ
โ for each particle p: โ
โ p.position += p.velocity * world.dt โ
โ โ
โ // 3. Detect collisions โ
โ world.num_contacts = 0 โ
โ detect_particle_collisions(world) โ
โ detect_wall_collisions(world) โ
โ โ
โ // 4. Solve constraints iteratively โ
โ for (int iter = 0; iter < NUM_ITERATIONS; iter++) { โ
โ // Solve distance constraints โ
โ for each constraint c: โ
โ solve_distance_constraint(c, world.particles) โ
โ โ
โ // Solve contact constraints โ
โ for each contact c: โ
โ solve_contact(c, world.particles) โ
โ } โ
โ โ
โ // 5. Derive velocities from position changes (for Verlet) โ
โ for each particle p: โ
โ p.velocity = (p.position - p.old_position) / world.dt โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

Phased Implementation Guide
Phase 1: Basic Particle Simulation (Days 1-4)
Goal: Particles falling under gravity
Tasks:
- Implement Vec2 math library
- Create Particle structure
- Implement semi-implicit Euler integration
- Add gravity
- Render particles with SDL2
Implementation:
void integrate_particles(PhysicsWorld* world) {
float dt = world->dt;
for (int i = 0; i < world->num_particles; i++) {
Particle* p = &world->particles[i];
// Apply gravity
p->velocity = vec2_add(p->velocity, vec2_scale(world->gravity, dt));
// Update position
p->position = vec2_add(p->position, vec2_scale(p->velocity, dt));
}
}
Verification:
- Particles should fall downward
- Velocity should increase over time
- Position change should match v ร ฮt
Phase 2: Wall Collisions (Days 5-7)
Goal: Particles bounce off screen boundaries
Tasks:
- Detect when particle exits bounds
- Compute collision normal
- Implement reflection formula
- Add coefficient of restitution
Implementation:
void collide_with_walls(Particle* p, float width, float height, float restitution) {
// Left wall
if (p->position.x - p->radius < 0) {
p->position.x = p->radius;
p->velocity.x = -p->velocity.x * restitution;
}
// Right wall
if (p->position.x + p->radius > width) {
p->position.x = width - p->radius;
p->velocity.x = -p->velocity.x * restitution;
}
// Similarly for top/bottom
}
Verification:
- Particles bounce off walls
- Restitution = 1: energy conserved
- Restitution = 0.5: each bounce loses energy
Phase 3: Particle-Particle Collisions (Days 8-11)
Goal: Particles collide with each other
Tasks:
- Detect circle-circle overlap
- Compute collision normal and penetration
- Implement impulse-based response
- Handle position correction
Implementation:
void detect_circle_collision(Particle* a, Particle* b, Contact* contact) {
Vec2 delta = vec2_sub(b->position, a->position);
float dist = vec2_length(delta);
float min_dist = a->radius + b->radius;
if (dist < min_dist) {
contact->normal = vec2_scale(delta, 1.0f / dist);
contact->penetration = min_dist - dist;
contact->valid = true;
} else {
contact->valid = false;
}
}
void resolve_collision(Particle* a, Particle* b, Contact* c, float restitution) {
Vec2 rel_vel = vec2_sub(a->velocity, b->velocity);
float vel_along_normal = vec2_dot(rel_vel, c->normal);
// Don't resolve if separating
if (vel_along_normal > 0) return;
// Compute impulse
float j = -(1 + restitution) * vel_along_normal;
j /= a->inv_mass + b->inv_mass;
Vec2 impulse = vec2_scale(c->normal, j);
a->velocity = vec2_add(a->velocity, vec2_scale(impulse, a->inv_mass));
b->velocity = vec2_sub(b->velocity, vec2_scale(impulse, b->inv_mass));
// Position correction (prevent sinking)
float correction = c->penetration / (a->inv_mass + b->inv_mass) * 0.8f;
a->position = vec2_sub(a->position, vec2_scale(c->normal, a->inv_mass * correction));
b->position = vec2_add(b->position, vec2_scale(c->normal, b->inv_mass * correction));
}
Verification:
- Particles bounce off each other
- Momentum is conserved
- No permanent interpenetration
Phase 4: Distance Constraints (Days 12-15)
Goal: Connect particles with springs/rods
Tasks:
- Define distance constraint structure
- Compute constraint violation
- Implement iterative position correction
- Visualize constraints
Implementation:
void solve_distance_constraint(DistanceConstraint* c, Particle* particles) {
Particle* a = &particles[c->particle_a];
Particle* b = &particles[c->particle_b];
Vec2 delta = vec2_sub(b->position, a->position);
float current_length = vec2_length(delta);
float error = current_length - c->rest_length;
if (current_length < 0.0001f) return; // Avoid division by zero
// Correction direction
Vec2 correction = vec2_scale(delta, error / current_length);
// Apply correction based on inverse masses
float total_inv_mass = a->inv_mass + b->inv_mass;
if (total_inv_mass < 0.0001f) return; // Both static
a->position = vec2_add(a->position, vec2_scale(correction, a->inv_mass / total_inv_mass));
b->position = vec2_sub(b->position, vec2_scale(correction, b->inv_mass / total_inv_mass));
}
Verification:
- Connected particles maintain distance
- Multiple iterations improve constraint satisfaction
- Chain of particles behaves like rope
Phase 5: Building the Constraint Solver (Days 16-19)
Goal: Unified constraint solving system
Tasks:
- Abstract constraint interface
- Build constraint Jacobian
- Implement Gauss-Seidel solver
- Handle mixed constraint types
Implementation:
// Generic constraint solver (simplified)
void solve_constraints(PhysicsWorld* world, int iterations) {
for (int iter = 0; iter < iterations; iter++) {
// Distance constraints
for (int i = 0; i < world->num_constraints; i++) {
solve_distance_constraint(&world->constraints[i], world->particles);
}
// Contact constraints
for (int i = 0; i < world->num_contacts; i++) {
solve_contact(&world->contacts[i], world->particles);
}
}
}
Verification:
- All constraints satisfied (within tolerance)
- Stable with many constraints
- Convergence improves with iterations
Phase 6: Complex Scenes (Days 20-23)
Goal: Build interesting simulations
Tasks:
- Create chain/rope scene
- Create bridge scene
- Create cloth-like grid
- Add user interaction (drag, spawn)
Scenes to implement:
void create_rope_scene(PhysicsWorld* world) {
// Chain of particles with distance constraints
for (int i = 0; i < 10; i++) {
add_particle(world, i * 30, 100, 5);
if (i > 0) {
add_distance_constraint(world, i-1, i, 30);
}
}
// Pin first particle
world->particles[0].inv_mass = 0; // Static
}
void create_soft_body_scene(PhysicsWorld* world) {
// Grid of particles with structural and shear constraints
int grid_size = 5;
for (int y = 0; y < grid_size; y++) {
for (int x = 0; x < grid_size; x++) {
add_particle(world, 200 + x * 30, 100 + y * 30, 5);
}
}
// Add constraints (structural, shear, bend)
// ...
}
Verification:
- Rope swings realistically
- Soft body maintains shape but deforms
- No explosions or jitter
Phase 7: Polish and Optimization (Days 24-28)
Goal: Make it robust and fast
Tasks:
- Add friction to collisions
- Implement warm starting
- Add broad-phase collision detection
- Profile and optimize
Friction:
void apply_friction(Particle* a, Particle* b, Contact* c, float friction) {
Vec2 rel_vel = vec2_sub(a->velocity, b->velocity);
Vec2 tangent = vec2_sub(rel_vel, vec2_scale(c->normal, vec2_dot(rel_vel, c->normal)));
float tangent_speed = vec2_length(tangent);
if (tangent_speed < 0.001f) return;
tangent = vec2_scale(tangent, 1.0f / tangent_speed);
float jt = -vec2_dot(rel_vel, tangent) / (a->inv_mass + b->inv_mass);
// Clamp to Coulomb friction
jt = clamp(jt, -friction * c->normal_impulse, friction * c->normal_impulse);
Vec2 friction_impulse = vec2_scale(tangent, jt);
a->velocity = vec2_add(a->velocity, vec2_scale(friction_impulse, a->inv_mass));
b->velocity = vec2_sub(b->velocity, vec2_scale(friction_impulse, b->inv_mass));
}
Verification:
- Objects stack without sliding
- System remains stable under stress
- 60 FPS with 100+ objects
Testing Strategy
Unit Tests
- Vector operations:
- Dot product of perpendicular vectors = 0
- Normalized vector has length 1
- Reflection formula is correct
- Collision detection:
- Overlapping circles detected
- Non-overlapping circles not detected
- Normal points from A to B
- Integration:
- Constant velocity โ position increases linearly
- Constant acceleration โ velocity increases linearly
- Constraints:
- Distance constraint maintains length
- Constraint solver converges
Physical Validation
- Conservation of energy (elastic collision, no gravity)
- Conservation of momentum (any collision)
- Projectile motion (matches vโt + ยฝgtยฒ)
- Pendulum period (matches 2ฯโ(L/g))
Stress Tests
- Many particles: 100+ without slowdown
- Long chains: 50+ links stable
- Stacking: 10+ objects stable
- High speeds: No tunneling through walls
Common Pitfalls and Debugging
Explosion/Instability
Symptom: Particles fly off to infinity
Causes:
- Time step too large
- Forces computed incorrectly
- Penetration not resolved
Fixes:
- Reduce ฮt (try 1/120 instead of 1/60)
- Add position correction
- Clamp velocities to maximum
Sinking Objects
Symptom: Objects slowly fall through floors
Causes:
- Collision detected but not resolved
- Bias/slop too large
- Not enough solver iterations
Fixes:
- Add position correction after velocity correction
- Reduce bias (e.g., 0.01 instead of 0.1)
- Increase iterations (10-20)
Jitter/Vibration
Symptom: Objects vibrate in place
Causes:
- Over-correction
- Conflicting constraints
- No damping
Fixes:
- Add damping: v *= 0.99
- Reduce correction factor
- Warm start solver
Constraint Stretching
Symptom: Springs stretch too much
Causes:
- Not enough solver iterations
- Stiffness too low
- Integration error
Fixes:
- More iterations (20+)
- Use position-based dynamics
- Smaller time step
Tunneling
Symptom: Fast objects pass through walls
Causes:
- Discrete collision detection
- Large time step
- Small obstacles
Fixes:
- Continuous collision detection (CCD)
- Limit max velocity
- Sub-step physics
Extensions and Challenges
Beginner Extensions
- Add mouse interaction (drag particles)
- Create preset scenes (bridge, rope, dominos)
- Add visual effects (trails, colors)
Intermediate Extensions
- Implement friction
- Add rigid body rotation
- Create hinge joints
- Implement spatial hashing for broad phase
Advanced Extensions
- Continuous collision detection
- 3D extension
- Deformable bodies
- Fluid simulation (SPH)
Real-World Connections
Game Engines
Every physics engine (Box2D, Bullet, PhysX) uses these concepts:
- Constraint-based dynamics
- Iterative solvers
- Warm starting
- Continuous collision detection
Robotics
Robot dynamics and control:
- Forward/inverse kinematics
- Constraint satisfaction
- Force/torque computation
Computer Graphics
Physically-based animation:
- Cloth simulation
- Hair dynamics
- Destruction effects
Engineering Simulation
Structural analysis:
- Finite element methods
- Stress/strain computation
- Dynamic loading
Self-Assessment Checklist
Conceptual Understanding
- I can explain why we need to solve Ax = b for physics
- I understand the role of the Jacobian matrix
- I can describe why iterative solvers work for constraints
- I know what matrix conditioning means for stability
- I understand the tradeoffs of different integration methods
Practical Skills
- I implemented collision detection and response
- I built a working constraint solver
- My simulation runs stable at 60 FPS
- I can create various constraint types
- I can debug common physics bugs
Teaching Test
Can you explain to someone else:
- How does impulse-based collision response work?
- Why do constraints create systems of equations?
- What makes a simulation stable vs. unstable?
- How does Gauss-Seidel iteration work?
Resources
Primary
- โGame Physics Engine Developmentโ by Ian Millington
- โPhysics for Game Developersโ by David Bourg
- Box2D source code - Reference implementation
Reference
- โReal-Time Collision Detectionโ by Christer Ericson
- โNumerical Recipes in Cโ by Press et al. - Equation solvers
- Erin Cattoโs GDC presentations - Box2D authorโs talks
Online
- Box2D documentation - Algorithm explanations
- Gaffer On Games (gafferongames.com) - Physics articles
- Two-Bit Coding YouTube - Physics engine tutorials
When you complete this project, youโll understand that physics simulation is applied linear algebraโsolving equations, projecting vectors, and navigating numerical challenges. Every game, every simulation, every robot uses these techniques.