Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 70

Bouncing Spheres: Reflecting Velocity for Collision Response

Recommended resources

Introduction

Welcome back! In the last lesson, you built a robust collision system for AABBs using a proactive clipping method. This was a fantastic achievement, resulting in solid objects that stop correctly and avoid the "tunneling" problem. Your physics engine now has a solid foundation for static geometry.

However, the world isn't just made of things that stop dead. Objects bounce, ricochet, and transfer energy to one another. Today, we'll implement this more dynamic behavior. Our learning outcome is to implement a 'bounce' collision response for sphere intersections by reflecting velocity.

We will cover:

  1. Switching our collision shape from boxes to spheres.
  2. The mathematics of vector reflection for simple bounces (e.g., off a static wall).
  3. The physics of dynamic collisions between two moving spheres, involving conservation of momentum.
  4. Implementing a coefficient of restitution to control the "bounciness" of a collision.

By the end of this lesson, you'll be able to simulate objects like billiard balls colliding and realistically bouncing off each other.


Part 1: From Boxes to Spheres and Simple Bounces

While AABBs are great for many things, spheres are often simpler for physics calculations, especially for rotation-agnostic objects.

Sphere-Sphere Collision Detection

Detecting if two spheres overlap is much simpler than with AABBs. You just need to check if the distance between their centers is less than or equal to the sum of their radii.

distance(center1, center2) <= radius1 + radius2

A common optimization, to avoid a costly square root operation, is to compare the squared distance with the squared sum of radii:

distance_squared(center1, center2) <= (radius1 + radius2)^2

Particle Collision // Game Dev Physics #2

This video provides a clear, quick explanation of this detection method and the squared distance optimization.

Watch the segment from 01:57 to 03:53. The presenter explains sphere collision detection and how to implement it efficiently.

The Math of Reflection

Now for the bounce. Let's start with the simplest case: a sphere hitting a static, immovable surface like a wall. The bounce is governed by the law of reflection: the angle of incidence equals the angle of reflection. In vector math, we can calculate the new velocity vector without dealing with angles directly.

The formula for a perfectly elastic reflection is:

Where:

  • is the incoming velocity vector.
  • is the outgoing (reflected) velocity vector.
  • is the normal vector of the surface at the point of collision.
  • is the projection of the incoming velocity onto the normal.
Vector Form of the Law of Reflection
This diagram illustrates how the reflected vector (\(\hat{e}_r\)) is calculated from the incident vector (\(\hat{e}_i\)) and the surface normal (\(\hat{n}\)). Subtracting the projection of the incident vector onto the normal twice effectively flips the component of the velocity that is perpendicular to the surface.

Game Math: Vector Reflection

This article provides an excellent breakdown of the vector reflection formula and gives a clear C#-like implementation that you can adapt to C.

Read the 'Vector Reflection' section. Pay close attention to the two alternative formulas presented. The second one, vec - 2.0f * Vector3.Project(vec, normal), is often the most direct to implement.

Introducing Restitution (Bounciness)

A perfect bounce, where no energy is lost, is rare. Most collisions are inelastic to some degree. We can model this with a coefficient of restitution (often denoted as e), a value between 0 and 1.

  • e = 1: Perfectly elastic collision (full bounce).
  • e = 0: Perfectly inelastic collision (no bounce, objects stick or stop).
  • 0 < e < 1: A realistic, partially bouncy collision.

We can modify our reflection logic to incorporate restitution. Instead of fully reversing the velocity component along the normal, we only reverse a fraction of it.

Game Math: Bouncing A Ball Off A Slope

The same article also explains how to modify the reflection logic to account for restitution.

Read the 'Exercise: Bouncing A Ball Off A Slope' section. Focus on the C# Reflect function that takes restitution as a parameter and the explanation of how it modifies the perpendicular component of the velocity. This is exactly what we need for our bounce response.


Part 2: Dynamic Sphere-Sphere Collisions

The simple reflection formula works for hitting static surfaces, but what happens when two moving spheres collide? Both objects have mass and velocity, and they will exchange momentum and energy. A simple reflection is not enough.

The solution is to temporarily simplify the 3D problem into a 1D problem along the axis of collision.

How to calculate sphere-sphere elastic collision response
This image shows the vectors involved in a two-sphere collision. The key idea is to decompose each sphere's velocity vector into two components: one parallel to the collision axis (the line connecting the centers) and one perpendicular to it. The collision only affects the parallel components.

The process is as follows:

  1. Find the Collision Normal: For two spheres, this is the normalized vector pointing from the center of one sphere to the center of the other.
  2. Project Velocities: Project both spheres' velocity vectors onto the collision normal. This gives you their 1D velocities along the collision axis.
  3. Solve the 1D Collision: Use the principles of conservation of momentum and the coefficient of restitution to calculate the new 1D velocities after the collision.
  4. Update the 3D Velocities: Calculate the change in the 1D velocities and apply this change back to the original 3D velocity vectors along the collision normal. The velocity components perpendicular to the normal are unchanged.

Particle Collision // Game Dev Physics #2

This is the most critical part of the lesson. The following video explains this entire process, from the underlying physics principles to the final 3D algorithm.

Watch this video carefully from 03:53 to 12:38. (03:53 - 07:22): This part covers the physics of momentum and energy conservation that underlie the collision equations. Since you're a chemical engineering student, these concepts should be familiar, but this is a great refresher in the context of programming. (07:22 - 10:32): This section introduces the coefficient of restitution and provides the final 1D collision velocity equations that we will use. (10:32 - 12:38): This is the crucial algorithm. It shows exactly how to use the 1D equations to solve a 3D collision by projecting onto the normal and then converting back. Pay close attention to this part.


Part 3: Implementation

Let's put this theory into practice. For dynamic-dynamic collisions, we'll shift from the proactive clipping of the last lesson to a "detect and resolve" approach. We'll check for overlaps each frame, and when one is found, we'll adjust the velocities and slightly push the spheres apart to resolve the penetration.

1. Update Data Structures

First, modify your PhysicsBody struct to use a radius instead of half-extents. We'll also add a restitution value.

// In your physics component header
typedef struct {
    vec3 velocity;
    vec3 force_accumulator;
    float mass;
    float inverse_mass;
    float radius;       // New!
    float restitution;  // New!
} PhysicsBody;

When creating entities, you'll now set a radius and a restitution value (e.g., 0.8f for a bouncy ball).

2. Implement the Response

We'll create a single function, resolve_sphere_collision, that takes two entities and handles everything.

Particle Collision // Game Dev Physics #2

The OGLDEV video also provides a complete C++ implementation walkthrough. We will adapt this logic to C.

Watch from 12:38 to 16:13. Observe how the algorithm is structured in code. Notice the important early-exit checks: one to ensure the objects are actually moving towards each other, and another to avoid division by zero.

Here is the C logic, closely following the video's implementation. You can place this in your physics_system.c.

void resolve_sphere_collision(
    Transform* t1, PhysicsBody* p1, 
    Transform* t2, PhysicsBody* p2) 
{
    // 1. Initial Checks
    vec3 collision_vec;
    glm_vec3_sub(t2->position, t1->position, collision_vec);

    float distance = glm_vec3_norm(collision_vec);
    float radii_sum = p1->radius + p2->radius;

    if (distance > radii_sum) {
        return; // No collision
    }

    // 2. Calculate Collision Normal
    vec3 normal;
    glm_vec3_normalize_to(collision_vec, normal);

    // 3. Check if objects are moving towards each other
    vec3 relative_velocity;
    glm_vec3_sub(p2->velocity, p1->velocity, relative_velocity);
    float vel_along_normal = glm_vec3_dot(relative_velocity, normal);

    if (vel_along_normal > 0) {
        return; // Objects are already separating
    }

    // 4. Calculate Restitution
    // Use the minimum of the two restitution values
    float e = fminf(p1->restitution, p2->restitution);

    // 5. Calculate Impulse Magnitude (j)
    // This is the core 1D collision formula from the video
    float j = -(1 + e) * vel_along_normal;
    j /= p1->inverse_mass + p2->inverse_mass;

    // 6. Apply Impulse
    vec3 impulse;
    glm_vec3_scale(normal, j, impulse);

    vec3 v1_change;
    glm_vec3_scale(impulse, p1->inverse_mass, v1_change);
    glm_vec3_sub(p1->velocity, v1_change, p1->velocity);

    vec3 v2_change;
    glm_vec3_scale(impulse, p2->inverse_mass, v2_change);
    glm_vec3_add(p2->velocity, v2_change, p2->velocity);

    // 7. Positional Correction (to prevent sinking)
    const float percent = 0.2f; // How much to correct (0.2 to 0.8 is usually good)
    const float slop = 0.01f;   // A small buffer to prevent objects from re-colliding immediately
    
    float penetration = radii_sum - distance;
    float correction_amount = fmaxf(penetration - slop, 0.0f) / (p1->inverse_mass + p2->inverse_mass) * percent;

    vec3 correction_vec;
    glm_vec3_scale(normal, correction_amount, correction_vec);

    vec3 t1_correction;
    glm_vec3_scale(correction_vec, p1->inverse_mass, t1_correction);
    glm_vec3_sub(t1->position, t1_correction, t1->position);

    vec3 t2_correction;
    glm_vec3_scale(correction_vec, p2->inverse_mass, t2_correction);
    glm_vec3_add(t2->position, t2_correction, t2->position);
}

Note: The formula for impulse j is a more direct way to implement the 1D velocity change derived from the conservation laws. It combines the mass and velocity terms into a single scalar impulse value.

3. Update the Physics Loop

Finally, modify your physics_system_update function. Remove the axis-by-axis clipping and replace it with a simple loop that checks every pair of dynamic objects.

void physics_system_update(World* world, float dt) {
    // --- Phase 1: Apply forces and update velocities (same as before) ---
    for (int i = 0; i < world->entity_count; ++i) {
        // ... (check for components, apply gravity, update velocity from acceleration)
        // ... This part remains mostly the same as the last lesson's physics update.
    }

    // --- Phase 2: Integrate positions ---
    for (int i = 0; i < world->entity_count; ++i) {
        // ...
        // Move object based on velocity:
        // vec3 move_delta;
        // glm_vec3_scale(physics->velocity, dt, move_delta);
        // glm_vec3_add(transform->position, move_delta, transform->position);
        // ...
    }

    // --- Phase 3: Resolve Collisions ---
    for (int i = 0; i < world->entity_count; ++i) {
        // Check this entity against all subsequent entities
        for (int j = i + 1; j < world->entity_count; ++j) {
            // Check if both entities have the required components
            uint32_t required_mask = COMPONENT_TRANSFORM | COMPONENT_PHYSICS_BODY;
            if ((world->entity_masks[i] & required_mask) != required_mask ||
                (world->entity_masks[j] & required_mask) != required_mask) {
                continue;
            }

            Transform* t1 = &world->transforms[i];
            PhysicsBody* p1 = &world->physics_bodies[i];
            Transform* t2 = &world->transforms[j];
            PhysicsBody* p2 = &world->physics_bodies[j];
            
            // Static objects don't collide with each other
            if (p1->inverse_mass == 0.0f && p2->inverse_mass == 0.0f) {
                continue;
            }

            resolve_sphere_collision(t1, p1, t2, p2);
        }
    }
    
    // --- Phase 4: Clear forces ---
     for (int i = 0; i < world->entity_count; ++i) {
        // ...
        // glm_vec3_zero(physics->force_accumulator);
        // ...
    }
}

This brute-force N^2 check is inefficient for many objects, but it's the standard way to start. We will cover optimization with spatial partitioning in a later lesson. For now, create a few sphere entities and watch them bounce!


Conclusion

Excellent work today. You've taken a massive leap from simple static collisions to dynamic, physics-based responses. This is a cornerstone of any game with interesting physical interactions.

Key Takeaways:

  • Sphere collision detection is a simple distance check.
  • The math for a simple bounce off a static surface is V_out = V_in - 2 * proj(V_in, N).
  • Dynamic collisions between two moving objects require conserving momentum and are best solved by projecting to a 1D system along the collision normal.
  • The coefficient of restitution (e) is a simple and effective way to control the "bounciness" of collisions.
  • Resolving dynamic collisions often involves not only changing velocity but also applying a positional correction to move objects out of their overlapping state.

Preview of the Next Lesson:
Our physics simulation is getting quite good, but objects still slide around as if on ice. In the next lesson, we will apply friction as a force opposing an entity's velocity, which will add another layer of realism to your engine.