Basic Dual Contouring Theory
Asked Answered
M

1

15

I've been searching on google, but cannot find anything basic. In it's most basic form, how is dual contouring (for a voxel terrain) implememted? I know what it does, and why, but cannot understand how to do it. JS or C# (preferably) is good.Has anyone used Dual contouring before and can explain it briefly?

Minefield answered 26/6, 2011 at 18:31 Comment(5)
You could try reading the original paper: frankpetterson.com/publications/dualcontour/dualcontour.pdf What have you tried so far?Wince
Read that, I get slightly lost in the rhetoric and the equations. I also saw [link] (procworld.blogspot.com/2010/11/from-voxels-to-polygons.html) which has some stuff on it, but again is slightly confusing. I read another paper which was quite similar as well. So any more basic explanation?Minefield
What part of them confused you?Wince
I couldn't actually find a desciption of what happens. Like, how the mesh is created, from what points. So I guess the formulae confused me.Minefield
Is there just a basic explanation of what happens? Like 'these points are calculated, joined together to form a mesh, and then the original voxels are removed'? Has anyone used dual contouring before?Minefield
W
49

Ok. So I got bored tonight and decided to give implementing dual contouring myself a shot. Like I said in the comments, all the relevant material is in section 2 of the following paper:

In particular, the topology of the mesh is described in part 2.2 in the following section, quote:

  1. For each cube that exhibits a sign change, generate a vertex positioned at the minimizer of the quadratic function of equation 1.

  2. For each edge that exhibits a sign change, generate a quad connecting the minimizing vertices of the four cubes containing the edge.

That's all there is to it! You solve a linear least squares problem to get a vertex for each cube, then you connect adjacent vertices with quads. So using this basic idea, I wrote a dual contouring isosurface extractor in python using numpy (partly just to satisfy my own morbid curiosity on how it worked). Here is the code:

import numpy as np
import numpy.linalg as la
import scipy.optimize as opt
import itertools as it

#Cardinal directions
dirs = [ [1,0,0], [0,1,0], [0,0,1] ]

#Vertices of cube
cube_verts = [ np.array([x, y, z])
    for x in range(2)
    for y in range(2)
    for z in range(2) ]

#Edges of cube
cube_edges = [ 
    [ k for (k,v) in enumerate(cube_verts) if v[i] == a and v[j] == b ]
    for a in range(2)
    for b in range(2)
    for i in range(3) 
    for j in range(3) if i != j ]

#Use non-linear root finding to compute intersection point
def estimate_hermite(f, df, v0, v1):
    t0 = opt.brentq(lambda t : f((1.-t)*v0 + t*v1), 0, 1)
    x0 = (1.-t0)*v0 + t0*v1
    return (x0, df(x0))

#Input:
# f = implicit function
# df = gradient of f
# nc = resolution
def dual_contour(f, df, nc):

    #Compute vertices
    dc_verts = []
    vindex   = {}
    for x,y,z in it.product(range(nc), range(nc), range(nc)):
        o = np.array([x,y,z])

        #Get signs for cube
        cube_signs = [ f(o+v)>0 for v in cube_verts ]

        if all(cube_signs) or not any(cube_signs):
            continue

        #Estimate hermite data
        h_data = [ estimate_hermite(f, df, o+cube_verts[e[0]], o+cube_verts[e[1]]) 
            for e in cube_edges if cube_signs[e[0]] != cube_signs[e[1]] ]

        #Solve qef to get vertex
        A = [ n for p,n in h_data ]
        b = [ np.dot(p,n) for p,n in h_data ]
        v, residue, rank, s = la.lstsq(A, b)

        #Throw out failed solutions
        if la.norm(v-o) > 2:
            continue

        #Emit one vertex per every cube that crosses
        vindex[ tuple(o) ] = len(dc_verts)
        dc_verts.append(v)

    #Construct faces
    dc_faces = []
    for x,y,z in it.product(range(nc), range(nc), range(nc)):
        if not (x,y,z) in vindex:
            continue

        #Emit one face per each edge that crosses
        o = np.array([x,y,z])   
        for i in range(3):
            for j in range(i):
                if tuple(o + dirs[i]) in vindex and tuple(o + dirs[j]) in vindex and tuple(o + dirs[i] + dirs[j]) in vindex:
                    dc_faces.append( [vindex[tuple(o)], vindex[tuple(o+dirs[i])], vindex[tuple(o+dirs[j])]] )
                    dc_faces.append( [vindex[tuple(o+dirs[i]+dirs[j])], vindex[tuple(o+dirs[j])], vindex[tuple(o+dirs[i])]] )

    return dc_verts, dc_faces

It is not very fast because it uses the SciPy's generic non-linear root finding methods to find the edge points on the isosurface. However, it does seem to work reasonably well and in a fairly generic way. To test it, I ran it using the following test case (in the Mayavi2 visualization toolkit):

import enthought.mayavi.mlab as mlab

center = np.array([16,16,16])
radius = 10

def test_f(x):
    d = x-center
    return np.dot(d,d) - radius**2

def test_df(x):
    d = x-center
    return d / sqrt(np.dot(d,d))

verts, tris = dual_contour(f, df, n)

mlab.triangular_mesh( 
            [ v[0] for v in verts ],
            [ v[1] for v in verts ],
            [ v[2] for v in verts ],
            tris)

This defines a sphere as an implicit equation, and solves for the 0-isosurface by dual contouring. When I ran it in the toolkit, this was the result:

enter image description here

In conclusion, it appears to be working.

Wince answered 27/6, 2011 at 10:48 Comment(13)
Wow! Your dedication is pretty awesome. I don't know any Python, but I should be able to convert it with some time. Thanks though. The bit which confuses me is formula 1 in the paper.Minefield
Oh, that's just a typo. It should be a sum. They just use it to compute a linear-least-squares solution to find the vertex per face. If you read on a bit they explain that pretty thoroughly in section 2.3 (where they also discuss how to solve a linear least squares problem in more detail than strictly necessary :) ).Wince
@Wince What actually happens? What I mean is what points are actually put together to form the mesh?Minefield
The vertices are just the linear least squares solutions to the intersections of the planes defined by the hermite data on each edge. The faces just given indexed by the edges. The connectivity data is described in section 2.2, if you look there is an enumerated list about halfway down the right column at the start of that section that describes how the vertices and faces are defined.Wince
I updated the solution and added the direct quote from the relevant section. It is really all there. I recommend you read all of section 2 again, but more carefully this time, and try to understand what it is saying.Wince
@Wince I'm sorry, I've read through it and translated it into English I can understand. But in the quote you gave, what is a minimizer (and the minimizing vertices)? This is probably the hardest rep you ever earned.Minefield
That is in section 2.3. What it means is that it is trying to find the vertex which is the best approximation of the intersection of all the Hermite planes on the edges. Because it is an overdetermined system, they regularize by taking a least squares estimate.Wince
@Wince - Sorry to keep bothering you, but what is a 'contour' in this case? I'm assuming it's not a marking on a map.Minefield
It is the same idea. Just a 0-level set of an implicit function (aka what marching cubes and dual contouring both spit out). en.wikipedia.org/wiki/Level_set The problem of finding contours is a basically a higher dimensional analog of root finding (and it is the basic problem that people solve when computing these sorts of surfaces).Wince
one more thing...what is the sign change it talks about?Minefield
Sorry to keep pestering you about this - but how would you arrange the cubes before dual contouring them?Minefield
Dead quesion, why are you bringing this up?Minefield
@Wince - Thanks for the chunk of code. However, there are a few kinks: * The orientation of the faces changes (which can easily be fixed). * The algorithm produces non-manifold edges. I'm currently trying to fix theses issues and post the result here.Allier

© 2022 - 2024 — McMap. All rights reserved.