GPU Rigid Body Physics

Utilizing the GPU for rigid body physics calculations.

In our game we are developing the majority of the gameplay within a simulation which is run on the GPU and because of this the physics and collisions calculations also need to be done on the GPU (or would we have to constantly transfer huge amounts of data back and forth). Unfortunately this means we can’t make use of Unity’s built in physics solution, as this is CPU based, but it does mean we get to delve into the maths in details and get a full understanding of the collision systems!

Just like a CPU based approach each object will have a location, a rotation, a size and importantly a collider shape. The collider shapes defines which counts as ‘in’ the object, and what is outside. There are several different types of common colliders. For simple applications basic primitive shapes are used to roughly estimate the shape of objects, however for more detailed requirements colliders which align closer to a meshes shape can be used in exchange for performance. jopo

The table below illustrates some common collider types along with their strengths and weaknesses:

Collider Type Comparison

/

A comparison of primative and mesh collider types

For our game, to keep things simply and get going quickly we are initially going to make use of spherical and point colliders. Our drone units can be broadly considered to be spheres, and our projectiles can exist at a specific collision point (in theory the back of a projectile won’t cause a collision, but this is unlikely to be noticeable).

Now, there are a number of things we expect to need to do with our colliders, this will vary depending on your game. Ours are listed below:

  • Detect collisions between objects of the same or different collider type
  • Perform raycast collisions (where we don’t know which object we are going to hit)
  • Determine the surface distance in a particular direction

Fortunately for us these are relatively simple for the sphere and point colliders. The distance from the centre to the edge of a collider will always be equal to the spheres radius (and equal to 0 for a point). Therefore we don’t have to worry around any rotations (phew – no matrix maths for now), and if we want to check if any two units collide can use the following simple code.

HLSL
  float3 posAdjacent = CombinedBuffer[adjacentIndex].CBpositionBuffer;
  float3 offset = posAdjacent - posThis;
  float distAdjancent = length(offset);
  
  if(distAdjancent > ((CombinedBuffer[adjacentIndex].unitSize.x + CombinedBuffer[thisIndex].unitSize.x) /2.0)) {
      //COLLIDES
  }
acryptum.com

Easy! Now within the GPU simulation each unit effectively gets processed in its own parallel thread. In a very basic physics engine you would check all objects against all other objects and find out what collisions exist. This quickly becomes unmanageable with a significant number of units and processing requirements scale with N2. This can be avoided by using a broad phase collision detection in addition to the above which could be considered a precise phase collision detection. For our purpose we implement a spatial grid for our main broad phase algorithm to dramatically limit the number of units any other units needs to check itself against. For more details on this – CHECK OUT THE SPATIAL GRID POST

For now we can sleep easy as we have implemented a basic collision detection on the GPU!

Simulation Collision Checking

The process to run the collision checking is far from straight forward. We went though a number of iterations to get the code working correctly in a parallel nature. In the end we got two compute functions:

  • 1) Detect Collisions – Calculate all collisions for every object & update ‘working’ variables [position & velocity]
  • 2) Update Objects – Process and apply these final position & velocity variables

I’ll cover these at a high level first, starting with 1)

HLSL
[numthreads(64, 1, 1)]
void DetectCollisions(uint3 id : SV_DispatchThreadID) {
    uint iz = id.x + (id.y * 1) + (id.z * 1);

    if (iz >= numUnits) {
        return;
    }
    
    float3 iz_pos = combinedObBuffer[iz].positionsBuffer;
    float3 iz_vel = combinedObBuffer[iz].velocitiesBuffer;
    float3 iz_ext = combinedObBuffer[iz].extentsBuffer;
    float3 iz_rot = combinedObBuffer[iz].rotationsBuffer;
    int iz_shape = combinedObBuffer[iz].shapeType;
    float iz_mass = combinedObBuffer[iz].massesBuffer;
    int iz_static = combinedObBuffer[iz].isStaticBuffer;
    
    combinedObBuffer[iz].positionsWorkBuffer = iz_pos;
    combinedObBuffer[iz].velocitiesWorkBuffer = iz_vel;
  
    //Check every other object not yet processed - reduces check by 50% but runs into race conditions
    //for (int u = iz+1; u < numUnits; u++) {
    
    //Check every other object    
    for (int u = 0; u < numUnits; u++) {   
        if (iz == u) {
            continue;
        } else {
            float3 iz_pos2 = combinedObBuffer[u].positionsBuffer;
            float3 iz_vel2 = combinedObBuffer[u].velocitiesBuffer;
            float3 iz_ext2 = combinedObBuffer[u].extentsBuffer;
            float3 iz_rot2 = combinedObBuffer[u].rotationsBuffer;
            int iz_shape2 = combinedObBuffer[u].shapeType;
            float iz_mass2 = combinedObBuffer[u].massesBuffer;
            int iz_static2 = combinedObBuffer[u].isStaticBuffer;
            SimulateCollisions(iz, u, iz_pos, iz_ext, iz_rot, iz_shape, iz_static, iz_mass, iz_pos2, iz_ext2, iz_rot2, iz_shape2, iz_static2, iz_mass2);
        }
    }
}
acryptum.com

Fundamentally for each thread (or object) – we then loop over every other object in the simulation – calling our Simulate Collisions function. We’ll come onto that in a minute, but essentially that just updates the working position and velocity of the current object, based on any detected positions.

You may notice the alternative for loop which is commented out. This

Updates!

(Dec-24) Part 2 – Raycast Targets

(Mar-25) Part 3 – Full Primitive Collisions Detection

Scroll to Top