Role: Physics Programmer
Engine: Unity, C#
Project Type: Solo, Personal Project
Duration: ~3 months
I started this project during my 2nd year at Staffordshire university, not too long after completing my module on Applied Mathematics. I really enjoyed physics programming, so I decided to work on this personal project in my spare time. Its a custom soft body physics simulation built in Unity using Verlet integration, iterative constraint solving, pressure-based volume preservation, collision resolution, and CCD inverse kinematics. I designed the project as a ground-up physics experiment without relying on Unity’s built-in rigidbody joints, which taught me to debug difficult Heisenbugs, crucial maths for creating stable, optimised and accurate physics.
Implemented position-based integration storing current and previous positions to derive implicit velocity and enable stable constraint solving.
Distance constraints solved using weighted inverse-mass distribution across multiple solver iterations per physics step.
Custom signed-distance plane collision with tangent/normal velocity decomposition for stable bounce control.
Polygon area calculated using the Shoelace formula and corrected via pressure-based force distribution.
Cyclic Coordinate Descent solver integrated directly with point-mass system.
Centralized solver orchestrating integration, constraints, collision, area correction, and IK per FixedUpdate step.
Implemented position-based Verlet integration for stable simulation without explicit velocity storage.
Velocity is implicitly derived from position delta. Damping applied to control energy growth. Continuous forces accumulated before integration.
// Verlet integration
Vector2 implicitVelocity = position - prevPosition;
implicitVelocity *= 0.98f;
prevPosition = position;
position += implicitVelocity;
position += netForce * Time.deltaTime * Time.deltaTime;
This approach improves numerical stability when combined with iterative constraint correction.
Implemented weighted distance constraints with inverse-mass distribution.
Vector2 delta = c.pointB.position - c.pointA.position;
float distance = delta.magnitude;
float difference = (distance - c.restLength) / distance;
Vector2 offset = delta * difference * c.springCoefficient;
float totalWeight = c.pointA.inverseMass + c.pointB.inverseMass;
c.pointA.position += offset * (c.pointA.inverseMass / totalWeight);
c.pointB.position -= offset * (c.pointB.inverseMass / totalWeight);
Constraints are solved iteratively per frame to improve convergence. Inverse mass enables fixed or dynamic points.
Implemented signed distance collision against planar surfaces with velocity decomposition for stable response.
float signedDistance = Vector2.Dot(toPoint, normal);
if (signedDistance <= point.radius)
{
float penetration = point.radius - signedDistance;
point.position += normal * penetration;
Vector2 v = point.position - point.prevPosition;
Vector2 vNormal = normal * Vector2.Dot(v, normal);
Vector2 vTangent = v - vNormal;
point.prevPosition = point.position - vTangent;
}
Separating normal and tangential components allows controlled bounce and prevents instability.
Maintains soft-body volume using polygon area preservation.
for (int i = 0; i < points.Count; i++)
{
int next = (i + 1) % points.Count;
area += (points[i].position.x * points[next].position.y
- points[next].position.x * points[i].position.y);
}
area *= 0.5f;
Pressure force derived from area error and distributed using partial derivatives of the polygon area.
Implemented Cyclic Coordinate Descent solver integrated directly into the same point-mass simulation.
float cross = (r2.x * r1.y) - (r2.y * r1.x);
float dot = Vector2.Dot(r2, r1);
float angle = Mathf.Atan2(cross, dot);
Uses cross and dot products to compute signed angle correction. Joints above the pivot are rotated incrementally for convergence.
Centralized solver executes in FixedUpdate:
void FixedUpdate()
{
foreach (var point in _points)
point.VerletStep();
for (int i = 0; i < 7; i++)
{
ResolveCollisions();
ConstraintStep();
CalculateArea();
CCDSolver();
}
}
Multiple solver passes improve constraint stability and reduce jitter.