| Sebastian's profileA random walk through ge...BlogLists | Help |
|
July 05 Ray tracing signed distance functionsSigned Distance Functions are a pretty simple concept. Basically for each point in the world, you return a distance to the nearest surface, negative distances are inside geometry. One neat application of them to represent scene geometry, and ray trace into the SDF. Why would anyone do such a thing? Well, it turns out that when marching along a ray looking for intersections (which is obviously not the only way to trace rays), it’s jolly useful to know a minimum bound on when you might expect to encounter a surface. This way you can keep taking large jumps along the ray, using the SDF to figure out how large, until you finally reach some threshold distance. Here’s the end result of the code I’m presenting in this blog post.
Note the cool twisted pillar, and the nice soft ambient occlusion around contact points. Both of these features are simple to do when tracing SDFs, but much harder with other methods. So basically we represent our scene as a SDF, but in order to have varying materials we need this function to not only return the distance to the closest surface, but the material of that surface too, so we end up with the following Haskell type for our scenes: type Scene = Vec3 -> (Double, Material) In order to define a simple scene we just need to write a function which returns the distance to the surfaces, for example here’s a unit sphere: sphere pt = (mag pt - 1.0, defaultMaterial ) In other words, we take the magnitude of the position (i.e. distance from the origin) and subtract 1.0 since this is our radius. Simple! To build larger scenes from simple scenes such as this, we need a way to combine two scenes. This is done by just taking the minimum of the distances, and picking the corresponding material. mergeScenes :: Scene -> Scene -> Scene Since our scenes are just functions from position to distance, it’s very easy to manipulate the geometry by warping the inputs to the SDF – for example, in order to build the twisted column shown in the screenshot above we first build a regular column, and then twist it by rotating the input by varying amounts (related to the y component): column pt@(Vec3 x y z) = (mag pt - mag closestPt, defaultMaterial ) There are many other possibilities for these kinds of modifications, for example we could add some random noise to the distance function to get bumpy surfaces etc. Once we have a scene to ray trace against, we simply march through the SDF until we reach a distance small enough to consider “on” the surface: type Ray = (Vec3, Vec3) -- origin, direction rayTrace :: Scene -> Double -> Ray -> Maybe Color The first equation of rayTrace simply checks if we’ve reached the far plane, in which case we return Nothing, since there was no hit. The second guard checks if we’ve reached a surface, and if so shades the point using our light and the normal is computed by looking at the distances in the neighbourhood of the point. The whole thing is modulated by our ambient occlusion factor (which may not be strictly correct, I know). This ambient occlusion factor is computed by looking at a small number of samples “above” our surface point and comparing the distance to these samples with the distances in the SDF for those samples. If the sampled distances are smaller than the distance to the surface, then that means there’s some amount of “occlusion” for that sample (since there’s evidently some object other than the surface nearby). We weight these occlusion estimates and sum them to get the final ambient occlusion. This is pretty hacky with several magic numbers to tweak, but it works really well, giving smooth 3D ambient occlusion with very little cost. Here’s the full code listing. I’ve implemented a small vector library with the stuff we need in order to keep it reasonably self-contained. You will need some way of actually displaying/saving the final image, though - the code below uses the ppm package (”cabal install ppm” should do it). This code is not intended to be an example of “nice” Haskell code – it’s pretty much just a copy of the code I ended up with after experimenting with SDFs, so there’s lots of hard coded values and other limitations (e.g. just one light source) which are, erm, ”left as an exercise to the reader”. {-# LANGUAGE ParallelListComp #-} type Material = (Vec3, Double, Double) -- Color, Specular power, gloss
colorize :: Color -> Scene -> Scene red, green :: Scene -> Scene
checker x y = checker1D x `xor` checker1D y -- the scenes column pt@(Vec3 x y z) = (mag pt - mag closestPt, (Vec3 0.4 0.7 1, 2, 0.5) ) mergeScenes :: Scene -> Scene -> Scene getNormal :: Double -> Scene -> Position -> Direction
getAO :: Scene -> Position -> Direction -> Double rayTrace :: Scene -> Double -> Ray -> Maybe Color toRGB24 (Vec3 r g b) = (exposure r, exposure g, exposure b) writePPM fname img = do
-- quick and dirty vector maths instance Num Vec3 where (Vec3 x y z) `dot` (Vec3 x' y' z') = x*x' + y*y' + z*z' Side note: the outer loop for our ray tracer uses “parMap” for almost perfect linear scaling (I get 1.9x on my dual core machine, and I suspect a lot of the overhead is just the serial business of actually writing the image to disk). It’s interesting to note that this was added pretty much as an afterthought; while playing with the code and adding more and more stuff the runtime got slower and slower, so as a quick’n’dirty speedup I changed a “map” to “parMap rnf” and it ran twice as fast. This wasn’t a carefully considered optimization pass, it was more like “This is slow, I’ll just hack in some parallelization real quick”. I dare say I’ll never utter that sentence when writing C – even if it had a “parMap” equivalent, I’d be very careful about using it since there’s no way of knowing that there won’t be any hidden side effects somewhere. Comments (1)
TrackbacksThe trackback URL for this entry is: http://sebastiansylvan.spaces.live.com/blog/cns!4469F26E93033B8C!173.trak Weblogs that reference this entry
|
|
|