Dual Contouring

The basis for many model generators is an algorithm to turn an implicit surface into a mesh. I implemented different algorithms over the last years and found the famous Marching Cubes painful. There is a bunch of lookup tables and optimizing it creates even more lookup tables. It has to be said that I always try to create vertex and index buffer which means the algorithm must find adjacent triangles fast. However it took me 500 lines code to implement that. This year I discovered another algorithm: Dual Contouring (Marching Edges would fit as name too). I required 200 lines of code to implement it and the performance/quality is good. Dual Contouring creates nicer meshes (more regular triangles) but produces artefacts if objects become too small, which Marching Cubes also do.

The method was invented 2002 and published on the Siggraph: Dual Contouring of Hermite Data. They also compare this method to marching cubes. You might have a look at the pictures to see the differences.

The Idea

In simple words: span a grid, check each edge, if one vertex of the edge is inside and the other outside create a quad perpendicular to that edge, project quad to the isosurface.

Ok the first step is to take a search grid. This grid has some resolution in each direction and determines the detail of the resulting mesh. Whereby cells of that grid must not necessarily be cubes.

DualGridThen we just compute the signs for each grid point by sampling of the density/distance function. An edge cuts the isosurface if the two adjacent grid points have a different sign. In that case create 6 new vertices for two triangles spanning a quad. We will resolve the adjacency later. The quad is located at the edge's center and has the same size as the grid in the two perpendicular directions. Assume the green grid to be the edge set of the resulting quads. Rendering that mesh directly creates a voxel-like surface. As the grid image shows the created mesh and the search grid are dual graphs. That is where the name comes from.

DualCountourVoxels

Now, only the projection is required. Technically this is done by a gradient descent or conjugated gradient descent algorithm. The gradient might be given by the implicit description of the model or can be estimated by finite differences. Following code snipped computes a variant of the gradient with modified length. If  _bLocalLength == false  this method returns the normal vector for the vertex at this location. Otherwise the length is scaled by the density value which speeds up the whole gradient descent process by a large factor (~8).

Doing this on the created triangles directly would mean to compute everything 6 times in average, since at each corner of a "voxel" six triangles meet. Further we would like to have a Indexbuffer for that model to reduce mesh size and increase rendering performance. So there is one intermediate step to find adjacent triangles.

Find Adjacency

Again it just needs a simple idea: Take a hash map, put vertices into, on collision map indices because we have two equal vertices. The main problem is to find an hash function for vertices, which projects very similar vertices to the same value and different vertices to diffferent values. The former is required because there could be little floating-point mistakes. The best thing here is to round the position to three digits and hash the rounded values. Indeed my function looks much more wired because it failed a lot for other meshes (from explicit surfaces). The hashing is still not perfect.

The  MeldVertices  method takes less than a thousandth part of the whole surface extraction process and increases the performance for everything after that step. It could also be used as post process for marching cubes results and other methods.
It should be mentioned that the upper method will fail for more than 8 hash-collisions between different vertices. This small drawback comes from the fact that the implementation is designed for demo (exe files with less than a couple of Kilobytes). You might want to use  std::vector  for the bucket or even some tree or  std::unordered_map  for the whole hashing.

Now one last image of the beautiful mesh:

DualCountourProjected

 

4 thoughts on “Dual Contouring

    1. Johannes Post author

      Well, first of all I create the boxy mesh visible in the second picture and meld vertices with the same position. Than I compute the gradient at the position of each vertex via finite differences of my density function. Following the gradient is a search for the root leading to a position on the surface. This position is not necessarily the one with the smallest quadratic error. But this solution is quiet easy and simplicity of code was a major criteria to me. The normal itself is computed as the normalized gradient at the point on the surface at the end.

      Reply
    1. Johannes Post author

      The density function is a function f:R^3 -> R which is similar too a height map in 2D (f:R^2 -> R). Essentially it has areas with values below 0 and other with larger values.
      One of the best sources for functions of geometric primitives is: http://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm.
      Another interesting article is: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch01.html. In "1.3.3 Making an Interesting Density Function" a function similar to mine is implemented step by step.
      Hope that is what you have searched!

      Reply

Leave a Reply to BW Cancel reply

Your email address will not be published. Required fields are marked *