The ability to dynamically split a convex shape into two separate shapes is a very valuable skill or tool to have in your gamedev arsenal. This splitting allows for advanced types of simulation, such as binary space partitions for graphics or physics, dynamically destructive environments (brittle fracturing!), ocean or water simulation, collision resolution for physics engines, binary spatial partioning, and the list just goes on. One great example is the game Fruit Ninja for Kinect.
What exactly does it mean to split a convex shape? In two dimensions, we refer to a shape as a polygon; in three dimensions, we refer to a shape as a polyhedron. (Polyhedra is the word used to reference more than one polyhedron.)
Tip: In general, convex polygons and polyhedra simplify many aspects of volume or mesh manipulation and management. Due to this simplification, the entire article assumes convex polygons and convex polyhedra. Concave shapes are not apart of any discussion here. In general complicated concave shapes are simulated as a collection of joined convex shapes.
Prerequisites
In order to make sense of the ideas presented in this article, you'll need a working knowledge of some programming language, and a simple understanding of the dot product.
One great way to split shapes in both two and three dimensions is to make use of the Sutherland-Hodgman clipping routine. This routine is quite simple and very efficient, and can also be extended ever so slightly to account for numerical robustness. If you're unfamiliar with the algorithm, check out my previous article on the subject.
An understanding of planes in either two or three dimensions is also a must. It should be noted that a two dimensional plane could be thought of as a projection of a three dimensional plane into two dimensions—or, in other words, a line.
Please understand that a plane can also be thought of as a half-space. Computing the distance or intersection of points to half-spaces is a required prerequisite: see the last section of How to Create a Custom 2D Physics Engine: The Core Engine for information on this.
Demo Source
Please refer to the demonstration source (also on Bitbucket) that I have created as you read through this tutorial. I used this demo for creating all the GIF images in the article. The source code is in C++ (and should be cross-platform compatible), but is written in a way that can easily be ported to any programming language.
Triangle Splitting
Before tackling the problem of splitting an entire polygon, let's take a look at the problem of splitting a triangle through a cutting plane. This will form the basis of understanding for moving on to a robust and generalized solution.
The nice thing about shape splitting is that, often, algorithms in 2D can be extended without much trouble directly into 3D. Here, I'll present a very simple triangle splitting algorithm for both two and three dimensions.
When a triangle intersects with a splitting plane, three new triangles should be generated. Here's an image showing a triangle before and after splitting along a plane:
Given a splitting plane, three new triangles are output during the slicing operation. Let's throw some code into the open, assuming that the three vertices of a triangle are { a, b, c }
in counter-clockwise (CCW) order, and that we know that the edge ab
(edge of vertices a to b) have intersected the splitting plane:
// Clip a triangle against a plane knowing that // a to b crosses the clipping plane // Reference: Exact Bouyancy for Polyhedra by // Erin Catto in Game Programming Gems 6 void SliceTriangle( std::vector& out, const Vec2& a, // First point on triangle, CCW order const Vec2& b, // Second point on triangle, CCW order const Vec2& c, // Third point on triangle, CCW order f32 d1, // Distance of point a to the splitting plane f32 d2, // Distance of point b to the splitting plane f32 d3 // Distance of point c to the splitting plane ) { // Calculate the intersection point from a to b Vec2 ab = a + (d1 / (d1 - d2)) * (b - a); Triangle tri; if(d1 < 0.0f) { // b to c crosses the clipping plane if(d3 < 0.0f) { // Calculate intersection point from b to c Vec2 bc = b + (d2 / (d2 - d3)) * (c - b); tri.Set( b, bc, ab ); out.push_back( tri ); tri.Set( bc, c, a ); out.push_back( tri ); tri.Set( ab, bc, a ); out.push_back( tri ); } // c to a crosses the clipping plane else { // Calculate intersection point from a to c Vec2 ac = a + (d1 / (d1 - d3)) * (c - a); tri.Set( a, ab, ac ); out.push_back( tri ); tri.Set( ab, b, c ); out.push_back( tri ); tri.Set( ac, ab, c ); out.push_back( tri ); } } else { // c to a crosses the clipping plane if(d3 < 0.0f) { // Calculate intersection point from a to c Vec2 ac = a + (d1 / (d1 - d3)) * (c - a); tri.Set( a, ab, ac ); out.push_back( tri ); tri.Set( ac, ab, b ); out.push_back( tri ); tri.Set( b, c, ac ); out.push_back( tri ); } // b to c crosses the clipping plane else { // Calculate intersection point from b to c Vec2 bc = b + (d2 / (d2 - d3)) * (c - b); tri.Set( b, bc, ab ); out.push_back( tri ); tri.Set( a, ab, bc ); out.push_back( tri ); tri.Set( c, a, bc ); out.push_back( tri ); } } }
Hopefully the above code scared you a little. But fear not; all we're doing here is calculating some intersection points in order to know how to generate three new triangles. If one had examined the previous image carefully, the intersection points' locations might be obvious: they are where the dotted line meets the splitting plane, and where the edge ab
intersects the splitting plane. Here's a diagram for convenience:
From this diagram, it is easy to see that output triangles should contain the vertices { a, ac, ab }
, { ac, c, b }
, and { ab, ac, b }
. (But not necessarily in this exact format; for example, { a, b, c }
would be the same triangle as { c, b, a }
because vertices were simply shifted to the right.)
In order to determine which vertices contribute to which of the three new triangles, we must determine whether the vertex a
and vertex c
lay above or below the plane. Since we are assuming that the edge ab
is known to be intersecting, we can deduce implicitly that b
is on the opposite side of the clipping plane from a
.
If the convention of a negative distance from a splitting plane means penetrating the plane, we can formulate a predicate to determine if a point intersects a halfspace: #define HitHalfspace( distance ) ((distance) < 0.0f)
. This predicate is used within each if statement to check and see whether a point is within the halfspace of the clipping plane.
There are four cases that exist of combinations of a
and b
hitting the halfspace of the clipping plane:
a | T T F F ----------- b | T F T F
Since our function requires that both a
and b
be on opposite sides of the plane, two of these cases are dropped. All that is left is to see on which side c
lays. From here, the orientation of all three vertices are known; intersection points and output vertices can be directly computed.
Finding the Initial Edge
In order to use the SliceTriangle()
function, we must find an intersecting edge of a triangle. The below implementation is efficient, and can be used upon all triangles in the simulation to be potentially split:
// Slices all triangles given a vector of triangles. // A new output triangle list is generated. The old // list of triangles is discarded. // n - The normal of the clipping plane // d - Distance of clipping plane from the origin // Reference: Exact Bouyancy for Polyhedra by // Erin Catto in Game Programming Gems 6 void SliceAllTriangles( const Vec2& n, f32 d ) { std::vector out; for(uint32 i = 0; i < g_tris.size( ); ++i) { // Grab a triangle from the global triangle list Triangle tri = g_tris[i]; // Compute distance of each triangle vertex to the clipping plane f32 d1 = Dot( tri.a, n ) - d; f32 d2 = Dot( tri.b, n ) - d; f32 d3 = Dot( tri.c, n ) - d; // a to b crosses the clipping plane if(d1 * d2 < 0.0f) SliceTriangle( out, tri.a, tri.b, tri.c, d1, d2, d3 ); // a to c crosses the clipping plane else if(d1 * d3 < 0.0f) SliceTriangle( out, tri.c, tri.a, tri.b, d3, d1, d2 ); // b to c crosses the clipping plane else if(d2 * d3 < 0.0f) SliceTriangle( out, tri.b, tri.c, tri.a, d2, d3, d1 ); // No clipping plane intersection; keep the whole triangle else out.push_back( tri ); } g_tris = out; }
After computing the signed distance of each triangle vertex to the splitting plane, multiplication can be used to see whether two distinct points lay on opposite sides of a plane. Since the distances will be of a positive and negative float within a pair, the product of the two multiplied together must be negative. This allows for the use of a simple predicate to see if two points lay on either side of a plane: #define OnOppositeSides( distanceA, distanceB ) ((distanceA) * (distanceB) < 0.0f)
.
Once any edge is found to be intersecting with the splitting plane, the triangle vertices are renamed and shifted and immediately passed along to the interior SliceTriangle
function. In this way, the first intersecting edge found is renamed to ab
.
Here is what a final working product may look like:
Splitting triangles in this manner accounts for each triangle independently, and the algorithm presented here extends, without any additional authoring, from two to three dimensions. This form of triangle clipping is ideal when adjacency info of triangles is not required, or when clipped triangles are not stored anywhere in memory. This is often the case when computing volumes of intersections, as in buoyancy simulation.
The only problem with splitting triangles in isolation is that there is no information about triangles that are adjacent to one another. If you examine the above GIF carefully, you can see that many triangles share collinear vertices, and as a result can be collapsed into a single triangle in order to be rendered efficiently. The next section of this article addresses this problem with another, more complex, algorithm (which makes use of all the tactics present in this section).
Sutherland-Hodgman
Now for the final topic. Assuming a working understanding of the Sutherland-Hodgman algorithm, it is quite easy to extend this understanding to split a shape with a plane and output vertices on both sides of the plane. Numerical robustness can (and should) also be considered.
Let's briefly examine the old Sutherland-Hodgman clipping cases:
// InFront = plane.Distance( point ) > 0.0f // Behind = plane.Distance( point ) < 0.0f Vec2 p1, p2; ClipPlane plane; case p1 InFront and p2 InFront push p2 case p1 InFront and p2 Behind push intersection case p1 Behind and p2 InFront push intersection push p2
These three cases work decently, but don't actually take into account the thickness of the splitting plane. As a result, numerical error can drift in when objects are moving, causing low temporal frame coherence. This sort of numerical instability can result in a corner being clipped one frame and not in another frame, and this sort of jittering can be quite ugly visually, or unacceptable for physical simulation.
Another benefit of this thick plane test is that points lying very near the plane can actually be considered as being on the plane, which makes the clipped geometry slightly more useful. It is entirely possible for a computed intersection point to numerically lay on the wrong side of a plane! Thick planes avoid such weird problems.
By using thick planes for intersection tests, a new type of case can be added: a point laying directly on on a plane.
Sutherland-Hodgman should be modified like so (with a floating point EPSILON
to account for thick planes):
// InFront = plane.Distance( point ) > EPSILON // Behind = plane.Distance( point ) < -EPSILON // OnPlane = !InFront( dist ) && !Behind( dist ) Vec2 p1, p2; ClipPlane plane; case p1 InFront and p2 InFront push p2 case p1 InFront and p2 Behind push intersection case p1 Behind and p2 InFront push intersection push p2 case any p1 and p2 OnPlane push p2
However, this form of Sutherland-Hodgman only outputs vertices on one side of the splitting plane. These five cases can easily be extended to a set of nine to output vertices on either side of a splitting plane:
// InFront = plane.Distance( point ) > EPSILON // Behind = plane.Distance( point ) < -EPSILON // OnPlane = !InFront( dist ) && !Behind( dist ) Vec2 p1, p2; Poly front, back; ClipPlane plane; case p1 InFront and p2 InFront front.push( p2 ) case p1 OnPlane and p2 InFront front.push( p2 ) case p1 Behind and p2 InFront front.push( intersection ) front.push( p2 ) back.push( intersection ) case p1 InFront and p2 OnPlane front.push( p2 ) case p1 OnPlane and p2 OnPlane front.push( p2 ) case p1 Behind and p2 OnPlane front.push( p2 ) back.push( p2 ) case p1 InFront and p2 Behind front.push( intersection ) back.push( intersection ) back.push( p2 ) case p1 OnPlane and p2 Behind back.push( p1 ) back.push( p2 ) case p1 Behind and p2 Behind back.push( p2 )
An implementation of these nine cases might look like the following (derived from Ericson's Real-Time Collision Detection):
// Splits a polygon in half along a splitting plane using a clipping algorithm // call Sutherland-Hodgman clipping // Resource: Page 367 of Ericson (Real-Time Collision Detection) void SutherlandHodgman( const Vec2& n, f32 d, const Poly *poly, std::vector *out ) { Poly frontPoly; Poly backPoly; uint32 s = poly->vertices.size( ); Vec2 a = poly->vertices[s - 1]; f32 da = Dot( n, a ) - d; for(uint32 i = 0; i < s; ++i) { Vec2 b = poly->vertices[i]; f32 db = Dot( n, b ) - d; if(InFront( b )) { if(Behind( a )) { Vec2 i = Intersect( b, a, db, da ); frontPoly.vertices.push_back( i ); backPoly.vertices.push_back( i ); } frontPoly.vertices.push_back( b ); } else if(Behind( b )) { if(InFront( a )) { Vec2 i = Intersect( a, b, da, db ); frontPoly.vertices.push_back( i ); backPoly.vertices.push_back( i ); } else if(On( a )) backPoly.vertices.push_back( a ); backPoly.vertices.push_back( b ); } else { frontPoly.vertices.push_back( b ); if(On( a )) backPoly.vertices.push_back( b ); } a = b; da = db; } if(frontPoly.vertices.size( )) out->push_back( frontPoly ); if(backPoly.vertices.size( )) out->push_back( backPoly ); }
Here is an example of Sutherland-Hodgman in action:
It's worth noting that the final polygons can be rendered as a vertex list with the format of triangle fan.
Numerical Robustness
There is one problem that you should be aware of: when computing an intersection point of ab
and a splitting plane, this point suffers from numerical quantization. This means that any intersection value is an approximation of the actual intersection value. It also means that the intersection point ba
is not numerically the same; tiny numerical drift will actually result in two different values!
A naive clipping routine can make a big mistake of computing intersection points blindly, which can result in T-junctions or other gaps in geometry. To avoid such a problem, a consistent intersection orientation must be used. By convention, points should be clipped from one side of a plane to another. This strict intersection ordering ensures that the same intersection point is calculated, and will resolve potential gaps in geometry (as shown in the image above, there's a consistent clipping result on the right).
UV Coordinates
In order to actually render textures over split shapes (perhaps when splitting sprites), you'll need an understanding of UV coordinates. A complete discussion of UV coordinates and texture mapping is way beyond the scope of this article, but if you already have this understanding, it should be easy to transform intersection points into UV space.
Please understand that a transformation from world space to UV space requires a change of basis transform. I'll leave UV transformations as an exercise for the reader!
Conclusion
In this tutorial, we looked at some simple linear algebra techniques for tackling the problem of dynamically splitting generic shapes. I also addressed some numerical robustness issues. You should now be able to implement your own shape splitting code, or use the demonstration source, to achieve many advanced and interesting effects or features for general game programming.
References
- Preview image: A modified Pear by Edward Boatman from the Noun Project