Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

How to Dynamically Slice a Convex Shape

$
0
0

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:

TriangleSplit

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:

TriangleSplitDiagram

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 along cutting planes dynamically via user interaction.
Splitting triangles along cutting planes dynamically via user interaction.

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:

Splitting a polygon via Sutherland-Hodgman by user interaction. Polygons are triangulated as a triangle fan.
Splitting a polygon dynamically via Sutherland-Hodgman by user interaction. Polygons are triangulated as a triangle fan.

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!

Example of a visible crack between triangles as a result of inconsistent clipping (image inspired by Ericson's Real-Time Collision Detection book).
Example of a visible crack between triangles as a result of inconsistent clipping (image inspired by Ericson's Real-Time Collision Detection book).

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


Viewing all articles
Browse latest Browse all 728

Trending Articles