Monday, February 5, 2007

Doo-Sabin in Haskell

To get more experience with Haskell, I went back to my graphics textbooks and decided to implement the Doo-Sabin algorithm for subdivision surfaces in Haskell. The Doo-Sabin algorithm refines a polygon mesh to a smooth surface. The following outline of the algorithm is from Farin 2000:

1 - For each face in the mesh, form new vertices as follows:
  1. Form the face's centroid - average of the vertices
  2. Form the edge midpoints of the face
  3. Each new vertex is the average of a face vertex, the centroid, and the two adjacent edge midpoints.
2 - Form new faces from the new vertices
  1. F-faces are constructed by joining new vertices within a face
  2. E-faces are constructed by joining new vertices as the edges of neighboring old faces
  3. V-faces are constructed by joining all new vertices around an old vertex

My initial thought is to represent a 3D vertex as a list. A face is then a list of vertices. I'm hijacking a couple of routines I wrote for vector math to help compute the centroid.

> addVec :: RealFloat a => [a] -> [a] -> [a]
> addVec v w = [ a + b | (a, b) <- zip v w]

> scaleVec :: RealFloat a => [a] -> a -> [a]
> scaleVec v c = [a * c | a <- v]

> centroid :: RealFloat a => [[a]] -> [a]
> centroid p = scaleVec (foldl1 addVec p) (1/fromIntegral (length p))

Time for a clever hack. I need to compute the midpoints of each edge of a face. Imagine that we have a triangular face with vertices v1, v2, v3. I need to compute the midpoint of v1v2, v2v3 and v3v1. By zipping a shifted version of my vertex list with itself, I'll have the desired vertices paired up.

> shiftr :: [a] -> [a]
> shiftr [] = []
> shiftr (x:y) = y ++ [x]

> midPoint :: RealFloat a => [a] -> [a] -> [a]
> midPoint x y = [(a+b)/2 | (a,b) <- zip x y]

> midPoints :: RealFloat a => [[a]] -> [[a]]
> midPoints p = [midPoint a b | (a,b) <- zip p (shiftr p)]

Now that I have the centroid and midpoints, I just need to compute the new vertices. The trick here is to get the midpoints adjacent to a face vertex. Another shift is in order, but in the opposite direction. My initial routine was this:

> shiftl :: [a] -> [a]
> shiftl [] = []
> shiftl x = [last x] ++ init x

I don't like the fact that this routine makes two passes though the list. I posted this code to the haskell-cafe list looking for better ideas. I got this really nifty routine as a response.

> shiftl (x1:x2:xs) = last:x1:init
> where last:init = shiftl (x2:xs)
> shiftl xs = xs

So, here is the routine to compute the new vertices:

> dooSabinNewVertices :: RealFloat a => [[a]] -> [[a]]
> dooSabinNewVertices n = [ centroid [c, x, y, z] | (x,(y,z)) <-
> zip n (zip m (shiftl m))]
> where
> c = centroid n
> m = midPoints n

Next up is how to compute the new faces.

No comments: