The Minkowski collison detection algorithm is beautiful! The implementation is very compact and very robust and also provides accurate contact information
Interactive demo to demonstrate and provide a minimal working example on implementing the Minkowski algorithm for collision detection.
While the algorithm itself isn't very parallel - we'll implement a single implementation on a single thread to detect the collision between two objects. The concept can be scaled up so that it can be run repeatedly for all the object pair collision calculations.
Key points
• WGSL Implementation (Run on GPU)
• Array of triangles
• Uses the vertex positions (doesn't depend on the order or layout - even handle broken shapes)
• Convex shape
• Gets contact information (contact points, contact normal and penetration distance)
How it works
The minkowski collision detection algorithm is in two parts - first we calculate the difference between the two shapes. This produces a new set of vertices. These vertices give us the minkowski shape - if this new shape has the origin inside it - then we have a hit!
The contact and distance information is found by calculating the nearest point on the surface to the origin.
We calculate the minkowski difference by subtracting every point of one shape from every point on the other. Now, this can produce a lot of points and can be very computationally expensive!
Imagine we have a cube with 36 vertex positions and a capsule with 600 points - this would produce 36x600=21600 vertex points! As every vertex on shape A is subtracted from every vertex on shape B and vice versa.
Instead of doing this the brute force way - we'll a support point algorithm using direction. We won't store the minkowski difference points - instead we'll loop over the points and calculate it as needed.
Tetrahedrons and Bookkeeping
Start by building a tetrahedron that encapsulates the origin (0,0,0).
• v0 - Calculate the centroid (use the centroid from shape A to shape B)
• v1 - From the centroid (v0) to the origin (0,0,0) - find the support point v1
• v2 - With the origin we have 3 points - take the cross product to find a third
• v3 - v0v1v2 gives us a triangle and a normal which can be used to find v3 (need to do an extra check to make sure the origin is on the correct side)
The code below shows the start of the algorithm for finding the starting* tetrahedron.
var v0 = MeshSupportPoint( (centroids.A - centroids.B), 0 );
var v1 = GetSupportVertex( origin - v0.p ); var v2 = GetSupportVertex( cross(origin-v0.p, origin-v1.p) ); var dir3 = cross(v1.p-v0.p, v2.p-v0.p);
Minkowski difference and tetrahedron to find the closest point on the surface.
We need to reduce this tetrahedron as much as possible - so that we have a triangle for the surface.
A very very important point - when calculating the vertices using the support point algorithm - we keep track of which vertex in the original shape the vertex came from (both shape A and shape B).
Once we've found the closest triangle to the origin - we can map the information back to the original coordinates to find the position and distance on the original shapes.
Implementation Example
The output for the implementation is shown in the figure below - the green circles are used to show the triangles on both shape A and shape B from the tetrahedron.
The line and the red/white sphere mark the point on the surface and the closest point between the two shapes. When the distance is positive the shapes are intersecting, when it's negative it provides the shortest distance.
Minkowski collision detection algorithm example (Minkowski difference)
Below gives the WGSL compute code. We pass the vertex positions for the two shapes. We run the algorithm as a single thread (
workgroup(1,1,1)
).
// Transform for each chape struct Transforms { matrixA : mat4x4<f32>, matrixB : mat4x4<f32>, };
// Results we read back from the calculation struct Results { data0: vec4<f32>, outContactPoint : vec4<f32>, outContactNormal : vec4<f32>, outPenetrationDepth : vec4<f32>, outContactPointB : vec4<f32>, // 4
X : vec4<f32>, // 5 -> Y : vec4<f32>, Z : vec4<f32>, W : vec4<f32>,
A : vec4<f32>, // B : vec4<f32>, C : vec4<f32>, D : vec4<f32>, }
// binding information for the in/out from the comopute shader @group(0) @binding(0) var<uniform> transforms : Transforms; @group(0) @binding(1) var<storage, read_write> shapeA : array< f32 >; // use f32 to avoid any alignment issues @group(0) @binding(2) var<storage, read_write> shapeB : array< f32 >; // tighly packed to 4 byte @group(0) @binding(3) var<storage, read_write> results : Results; @group(0) @binding(4) var<storage, read_write> tmpg : f32;
/* Instead of passing an array of 'vec3<f32>' - we pass a single array of floats - every 3 floats is a vertex position.
This was a workaround for the alignment - compute shader likes to pad structures to 16 bytes (4 floats). */ fn GetVertex( shapeId:u32, indx:u32 ) -> vec3<f32> { var p : vec3<f32> = vec3<f32>(0); if ( shapeId == 0 ) { p.x=shapeA[indx*3+0]; p.y=shapeA[indx*3+1]; p.z=shapeA[indx*3+2]; p = (transforms.matrixA * vec4<f32>(p,1.0) ).xyz; }; if ( shapeId == 1 ) { p.x=shapeB[indx*3+0]; p.y=shapeB[indx*3+1]; p.z=shapeB[indx*3+2]; p = (transforms.matrixB * vec4<f32>(p,1.0) ).xyz; }; return p; }
struct Centroids { A: vec3<f32>, B: vec3<f32> }
fn GetCentroids() -> Centroids {
var centroids: Centroids;
centroids.A = vec3<f32>(0); for (var i:u32=0; i< u32(arrayLength(&shapeA)/3); i++) { let pA = GetVertex( 0, i ); centroids.A += pA; } centroids.A = centroids.A * 1.0/f32( arrayLength(&shapeA)/3 );
fn GetMeshSupportVertex( shapeId:u32, n:vec3<f32>) -> MeshSupportPoint { var numShapePoints:u32 = 0; if ( shapeId==0 ) { numShapePoints=arrayLength(&shapeA)/3; }; if ( shapeId==1 ) { numShapePoints=arrayLength(&shapeB)/3; };
var supportPoint : vec3<f32>; var mmax = -1000000.0; var ip:u32 = 0; for (var i:u32=0; i<numShapePoints; i++) { var p : vec3<f32> = GetVertex(shapeId, i); var val = dot( p, n );
if ( val > mmax ) { supportPoint = p; ip = i/3; mmax = val; } } return MeshSupportPoint(supportPoint, ip); }
fn GetSupportVertex( n:vec3<f32>) -> SupportPoint { var v0 = GetMeshSupportVertex( 0, n ); var v1 = GetMeshSupportVertex( 1, -n ); return SupportPoint(v0.p - v1.p, n, v0, v1); }// End GetSupportVertex(..)
fn MapPointOrigin(A:vec3<f32>, B:vec3<f32>, C:vec3<f32>, tA:vec3<f32>, tB:vec3<f32>, tC:vec3<f32> ) -> vec3<f32> { let origin = vec3<f32>(0,0,0); let n = normalize( cross( A-B, A-C ) ); let p = origin + n * dot( n, A - origin );
var tt = length( cross(p-C, A-C) ); var tb = length( cross(B-C, A-C) );
var st = length( cross(p-C, B-C) ); var sb = length( cross(A-C, B-C) );
----------------------------------------------------------------------------- */ @compute @workgroup_size(1, 1) fn main(@builtin(global_invocation_id) globalId : vec3<u32>, @builtin(local_invocation_id) localId : vec3<u32>, @builtin(workgroup_id) workgroupId : vec3<u32>, @builtin(num_workgroups) workgroupSize : vec3<u32> ) { // These are the objectives - what we want to find! - fill them with debug values var outPenetrationDepth : f32 = 1.23456; var outContactPoint : vec3<f32> = vec3<f32>(1.23456); var outContactNormal : vec3<f32> = vec3<f32>(1.23456);
let centroids = GetCentroids();
var origin = vec3<f32>(0,0,0); // our target!
var v0 = SupportPoint(centroids.A - centroids.B, vec3<f32>(0), MeshSupportPoint(vec3<f32>(0),0), MeshSupportPoint(vec3<f32>(0),0) );
var v1 = GetSupportVertex( origin - v0.p ); var v2 = GetSupportVertex( cross(origin-v0.p, origin-v1.p) ); var dir3 = cross(v1.p-v0.p, v2.p-v0.p);
// v0, v1, v2 and v3 form a tetrahedron - however, we need to do // a bit of looping around to 'ensure' the tetrahedron encloses the origin for (var nn=0; nn<100; nn++) { if ( dot( cross(v1.p-v0.p, v3.p-v0.p), v0.p - origin ) < 0 ) { v2 = v3; v3 = GetSupportVertex( cross(v1.p-v0.p, v3.p-v0.p) ); continue; }
// Use for with uppper limit instead of a while - avoid chance of an ininit loop // reduce to control fidelity/performance factors for (var nn=0; nn<10; nn++) { // Compute normal of the wedge face var v4 = GetSupportVertex( cross(v2.p - v1.p, v3.p - v1.p) );
// Is our new point already on the plane of our triangle, // if so, we're already as close as we can get to the target var delta = dot( (v4.p - v3.p), v4.n ); if ( abs(delta) < 0.01 ) { var n = normalize( v4.n ); // Compute distance from origin to wedge face outPenetrationDepth = dot(n, v1.p-origin);
// we're inside the wedge and have a collision // break and return closest point, normal and peneration/distance break; }
// We'll create three baby tetrahedrons and decide // which one will replace the current tetrahedron // v1v2 - v2v3 - v3v1 - current tetrahedron edges // leads to 3 new tetrahedrons: // v1v2v4 // v2v3v4 // v3v1v4 // Use for with uppper limit instead of a while - avoid chance of an ininit loop //while ( true ) for (var gg=0; gg<100; gg++) { var na = -cross( v2.p-v0.p, v4.p-v0.p ); // Normal to the triangle v2v4v0 var nb = -cross( v4.p-v0.p, v1.p-v0.p ); // Normal to the triangle v4v1v0 if ( dot( na, v0.p-origin ) > 0 && dot( nb, v0.p-origin ) > 0 ) { // Inside this tetrahedron v3 = v4; break; }
na = -cross( v3.p-v0.p, v4.p-v0.p ); // Normal to the triangle v3v4v0 nb = -cross( v4.p-v0.p, v2.p-v0.p ); // Normal to the triangle v4v2v0 if ( dot( na, v0.p-origin ) > 0 && dot( nb, v0.p-origin ) > 0 ) { // Inside this tetrahedron v1 = v4; break; }
na = -cross( v1.p-v0.p, v4.p-v0.p ); // Normal to the triangle v1v4v0 nb = -cross( v4.p-v0.p, v3.p-v0.p ); // Normal to the triangle v4v3v0 if ( dot( na, v0.p-origin ) > 0 && dot( nb, v0.p-origin ) > 0 ) { // Inside this tetrahedron v2 = v4; break; }
// Should never get here - as it must be in // one of the children! // assert - fix algorithm!! break; }
}// End loop
// Barycentric coordinates to map from the minkowski // difference onto the original shape outContactPoint = MapPointOrigin( v1.p, v2.p, v3.p, v1.a.p, v2.a.p, v3.a.p );
// store extra debug information - triangles on each shape results.A = vec4<f32>( v1.a.p, 0.0); results.B = vec4<f32>( v2.a.p, 0.0); results.C = vec4<f32>( v3.a.p, 0.0);
The implementation is available online - that you can test out in real-time. It's a WebGPU application so it runs in a webpage without having to install anything (links at the end).