A Ray Tracer in C#3.0

Ray tracers are a lot of fun.  When I was in middle school, I discovered POV-Ray and was so excited about the cool graphics it could create that I would often leave my 286 on overnight rendering ray-traced scenes and movies.  When I was in high school, I discovered Computer Graphics: Principles and Practice and spent weeks working through it's ray tracing algorithms trying to implement my own ray tracer in C++.  When I was in college, taking an introductory course which used Scheme, I re-wrote the raytracer in Scheme to show some friends that you really could write programs longer than 20 lines in Scheme!  And then when I joined the C# team 3 years ago, I of course had to re-write my raytracer in C#. 

More recently, I took a pass through the code to update it to use C#3.0.  It's not a particularly fancy or efficient ray tracer - but then again, it's only 400 lines of code.  Below are a few of the interesting uses of C#3.0 from the ray tracer code.

C#3.0 Without the Databases and XML

Although we often demo C#3.0 using databases and XML to show off LINQ - it turns out that the new language features really are also great for applications which have little to do with querying.  Ray tracing, for example, is certainly not one of the prototypical scenario for query and transformation of data.  Nonetheless, I found quite a few places in the code where C#3.0 and LINQ to Objects really improved the code - making it easier to express what the program was doing.

LINQ to Objects

Here's one example of the method which computes the intersections of a ray with the objects in a scene.   

         private IEnumerable<ISect> Intersections(Ray ray, Scene scene)
        {
            return scene.Things
                        .Select(obj => obj.Intersect(ray))
                        .Where(inter => inter != null)
                        .OrderBy(inter => inter.Dist);
        }

The method intersects each object in the scene with the ray, then throws away those that didn't intersect, and finally orders the rest by the distance from the source of the ray. The first object in this result is the closest object hit by the ray.  If there are no elements in the result, there were no intersections.  The LINQ to Objects query methods provide a nice way to describe this, and lambdas make it easy to write the code which processes the objects and intersections.

Object and Collection Initializers 

Here's the code that describes the scene rendered in the image above.

         internal readonly Scene DefaultScene =
            new Scene() {
                    Things = new SceneObject[] { 
                                new Plane() {
                                    Norm = Vector.Make(0,1,0),
                                    Offset = 0,
                                    Surface = Surfaces.CheckerBoard
                                },
                                new Sphere() {
                                    Center = Vector.Make(0,1,0),
                                    Radius = 1,
                                    Surface = Surfaces.Shiny
                                },
                                new Sphere() {
                                    Center = Vector.Make(-1,.5,1.5),
                                    Radius = .5,
                                    Surface = Surfaces.Shiny
                                }},
                    Lights = new Light[] { 
                                new Light() {
                                    Pos = Vector.Make(-2,2.5,0),
                                    Color = Color.Make(.49,.07,.07)
                                },
                                new Light() {
                                    Pos = Vector.Make(1.5,2.5,1.5),
                                    Color = Color.Make(.07,.07,.49)
                                },
                                new Light() {
                                    Pos = Vector.Make(1.5,2.5,-1.5),
                                    Color = Color.Make(.07,.49,.071)
                                },
                                new Light() {
                                    Pos = Vector.Make(0,3.5,0),
                                    Color = Color.Make(.21,.21,.35)
                                }},
                    Camera = Camera.Create(Vector.Make(3,2,4), Vector.Make(-1,.5,0))
                };

The scene consists of a collection of things, a collection of lights, and a camera, all of which are initialized in one statement using object and collection initializers.  This makes it quite a bit easier to describe the scene.  This format also helps to make the structure of the scene clear.

Lambdas

Here's the code describing the two surface textures used in the image above:

     static class Surfaces {
        // Only works with X-Z plane.
        public static readonly Surface CheckerBoard  = 
            new Surface() {
                    Diffuse = pos => ((Math.Floor(pos.Z) + Math.Floor(pos.X)) % 2 != 0) 
                                       ? Color.Make(1,1,1) 
                                       : Color.Make(0,0,0),
                    Specular = pos => Color.Make(1,1,1),
                    Reflect = pos => ((Math.Floor(pos.Z) + Math.Floor(pos.X)) % 2 != 0) 
                                       ? .1 
                                       : .7,
                    Roughness = 150
                };
            

        public static readonly Surface Shiny  = 
            new Surface() {
                    Diffuse = pos => Color.Make(1,1,1),
                    Specular = pos => Color.Make(.5,.5,.5),
                    Reflect = pos => .6,
                    Roughness = 50
                };
    }

These two static fields are initialized with the surface styles for the checkerboard and shiny textures on the objects in the scene above.  The surfaces are created with object initializers, and lambda expressions are used to provide the functions to compute diffuse, specular and reflection values based on the position on the surface.  The combination makes it possible to do something similar to prototype objects.

Try Out the Code

Want to try it out yourself?  Just compile RayTracer.cs with the Orcas C# compiler.  Then run.

File iconRayTracer.cs

kick it on DotNetKicks.com