I'm in the process of making a ray tracer. This project has been a lot of fun to work on. We've been talking about ray tracing in my Computer Graphics II class, which I'm auditing. The rest of the students in the class have a couple of assignments where they need to do ray tracing, but since I'm auditing the class, I don't have to do the assignments. I'm working on this mostly for fun and for the experience. It is kind of nice to be able to work on this without having to follow a strict set of guidelines that I will be graded on. I can work on anything that I am interested in. It really is the ideal way to work on a project, but I have to admit that for most people, and for most projects, if we weren't required to meet certain requirements, we wouldn't do anything with it at all. Ray tracing has been fun and interesting for me, so I think I've managed to do everything the others have been assigned to do, and more.
With this project, I've been uploading screenshots after each major task is completed. It is actually extremely interesting to see the development of the project over time. I'd recommend this for anyone else, and it is something that I'm planning on doing in the future. It keeps you motivated and helps you see how far you've come.
My ray tracer how has the following features:
- Multiple primitive types
- infinite planes
- 3D models (built out of triangles)
- The basic lighting techniques
- ambient lighting
- diffuse lighting
- specular lighting
- hard shadows
- soft shadows
- Set shadow properties for an object: receive shadows, cast shadows, and self shadows
- depth of field
- 2D images on all primitives
- 3D checkerboard pattern on all primitives
- basic support for affine transformations
After a couple of hours of programming, I have the basic ray tracing going, with simple coloring. It's nothing fancy, but here's a rendered image:
Basically this is an infinite plane that is green and kind of looks like the ground, two infinite planes that are black and look like walls (which converge in the distance), and a red sphere. For some reason, there is an empty dot right in the middle. Also, if it doesn't intersect anything, it defaults to good old cornflower blue.
OK, a couple of hours later, I've got the materials working as well as diffuse lighting. I have it set up to read all of the scene information from a file, so that I don't have it embedded in code. For example, below is the file that I used to generate the image below:
Material Name=Red DiffuseR=1.0 DiffuseG=0.1 DiffuseB=0.1 Material Name=Green DiffuseR=0.1 DiffuseG=1.0 DiffuseB=0.1 Material Name=Blue DiffuseR=0.1 DiffuseG=0.1 DiffuseB=1.0 Material Name=Yellow DiffuseR=1.0 DiffuseG=1.0 DiffuseB=0.1 Sphere CenterX=0 CenterY=0 CenterZ=10 Radius=3 Material=Red Sphere CenterX=5 CenterY=5 CenterZ=40 Radius=8 Material=Green Sphere CenterX=5 CenterY=20 CenterZ=80 Radius=16 Material=Yellow Plane LocationX=0 LocationY=-20 LocationZ=0 NormalX=0 NormalY=1 NormalZ=0 Material=Blue Plane LocationX=-30 LocationY=-30 LocationZ=0 NormalX=1 NormalY=0 NormalZ=0 Material=Green Plane LocationX=30 LocationY=-30 LocationZ=0 NormalX=-1 NormalY=0 NormalZ=0 Material=Yellow DirectionalLight DirectionX=-1.0 DirectionY=1.0 DirectionZ=0.0 DiffuseR=0.8 DiffuseG=0.0 DiffuseB=0.0 DirectionalLight DirectionX=0.0 DirectionY=1.0 DirectionZ=0.0 DiffuseR=0.75 DiffuseG=0.75 DiffuseB=0.75 DirectionalLight DirectionX=1.0 DirectionY=1.0 DirectionZ=-1.0 DiffuseR=0.5 DiffuseG=0.5 DiffuseB=0.5
It's pretty simple, but so far, things seem to be working, and I'm pretty excited about it so far.
It shouldn't have even taken this long to get this going, but I ran into a bug where I was accidentally sending my rays backwards, instead of into the scene. But the way it was set up, it was hard to see that. Anyway, it was a simple fix, and now it's going!
Only a short time later, my ambient and specular lighting was going. Of course, it helps that I just made a specular lighting shader tutorial, so the math and stuff was pretty fresh in my mind….
Notice that each material can specify its own specular power coefficient.
I got the anti-aliasing going. There's a couple of choices, in the program. You can either do random sampling, or sampling on a grid. Both give similar results, and I can't imagine they are much different in performance, as long as they both generate the same number of starting rays. The random one, however, doesn't look as good at a low level of sampling, like, say, 1.
Of course, now performance is starting to be a bigger issue, since I'm doing lots of rays for each pixel.
I've just implemented the shadow rays stuff. Wow… that was the easiest part so far. I just grabbed the code from before for intersections and created a shadow ray, and that's all it took, really. I've also gotten rid of a couple of my infinite planes in my scene, simply because they were blocking out all of the light, now that shadows are working. Here's the latest image!
I've now added point lights to the ray tracer. They look pretty nice! I have two images below, one that should be similar to the previous image, but with a point light instead of directional lights. But now that I'm using different lights, I've added my side walls back in for the heck of it.
Well, I've made it pretty far for only working on this intermittently for one day. The next step might be to add in triangles, which will ultimately lead to being able to import 3D models and all kinds of textures, like normal diffuse textures, and bump maps.
OK, somehow, all of the work I did the other day disappeared. I've looked everywhere on my computer, including the recycling bin for the code that I wrote and I can't find it anywhere. Oh, well. So I've rewritten the program from scratch now. Of course, it is better this time through, because I've learned from my mistakes, though actually, the code that I wrote was very nice.
Anyway, I've gotten it back to where it was, and I added in reflection. The math for this was pretty simple. Much simpler than refraction will be. I'm a little disappointed in how it looks though. I was hoping to get something that looked nicer, but I guess that mostly comes from all of the fancy texturing that is being done in a nice ray tracing program, like Bryce. So far, mine only has colors, not textures. So it looks kind of plain. But the reflection is working out nicely! Oh, and by the way, you can specify the reflectivity and the metallicity of a material. The reflectivity indicates how much of the light comes from the reflection, and how much comes from the normal lighting model. The metallicity indicates how much the color of the object affects the reflected color. To illustrate, look at a metal object, and you will see that the reflection is tinted the color of the metal. If you have red metal, all of the reflection will be tinted red.
I've been messing around with refraction for a while now, and its giving me some trouble. I get refraction looking images, but things don't seem to quite look right. I thought it might be a total internal reflection thing, so I had the code determine whether or not the ray was intersecting the surface at an amount greater than the critical angle, but then I get crazy reflections all over inside of it. So I'm putting the refraction on hold for now and moving on to triangles.
The triangle stuff was pretty easy to do, once I found a reasonable algorithm online. I guess I didn't have complete notes from our discussion of triangle/ray intersection in CS 6400, so I had to look elsewhere. I got my idea from http://www.codeproject.com/KB/graphics/Simple_Ray_Tracing_in_C_2.aspx. The basic idea is to check to see whether the ray crosses the plane that the triangle is in, so my Triangle class is a subclass of my InfinitePlane class. That made this pretty simple, and within a few minutes I had triangles rendering!
OK, I've been doing some work now with models. I've made it so you could load models in the .obj format, which is a commonly used format, so its good for you, and it is extremely easy to parse, so it is good for me. Perhaps some day I'll add other formats, but for the near future, I have no plans in doing that, since it is likely I'm the only one who will ever use it. The model stuff works now, with a few notable exceptions. First, the file name can not contain any spaces in it, and you can't put it in quotes or anything to get it to work. The path for the file, whether it is full or relative, must not contain spaces. Second, you'll see in the image that only flat shading is working right now. This is by far the easiest, so it is what I did first. I've toyed around with a couple of methods for doing smooth shading, but so far, I'm just getting black. So I'll still have to work that out. Also, you can set a model's position, but not it's scale. It has weird side effects. And there is nothing in there for rotations or anything yet. Those are all things that I'm planning on fixing before too long.
Additionally, I've spent quite a bit of time trying to improve the performance. I now use a bounding box around each model and each triangle, which tremendously increased the performance, but it is still slower than I would like. So I might have to move over to some sort of hierarchical model, to get it to work really well. Also, the bounding box may have some problems associated with scaling the triangle.
Its been pretty exciting to see this program come together, and I like seeing the images from earlier versions. It's been a couple of weeks since I've had time to work on this program, but today I added in support for smooth shading.
Well I spent some more time working on refraction, and I think I've got it working, but I still don't have the total internal reflection thing working, but that's OK for now. Below is an image of my refraction. I believe the object has an index of refraction of 1.5, roughly equivalent to glass.
I've been working on texturing the last little while. I've got a simple checkerboard 3D texture. Right now, the texture is relative to the world, not the object that it is texturing. Ideally, you'd be able to choose between a 3D texture that is relative to the world and one that is relative to the object. So right now, as an object moves around, the texture will change. If it were relative to the object itself, this wouldn't happen because the coordinate system of the texture would be translated as well. But it looks pretty nice. My next step is going to be a 2D bitmap texture that it determined by the UV-coordinates of a model.
Well I believe my 2D texturing is working now. Here is a screenshot with a simple tank model I made for a game the other day. The walls have the 3D checkerboard texture applied to them.
I've gotten my texturing to work on infinite planes and spheres now. It was actually quite a bit of work to do for the infinite planes, because of what I wanted to do with it. The main program that I use that performs ray tracing is Bryce, and one of my biggest complaints about Bryce is that it seems like when you use an infinite plane in Bryce, if you rescale the infinite plane (if that's even possible…), the texture coordinates don't change at all. They draw a little finite plane that you can move around and resize, but it appears to have no effects at all on the texture coordinates. Of course, I don't think I know everything there is to know about Bryce, so maybe there's something you can do, but it seems like even when you set the texture coordinates to a different mode, like "world" or anything, it has no effect. Anyway, I wanted to have my planes be resizeable, and allow you to have the uv-coordinates of the texture running in the direction of your choice. I think I've gotten that now, but it took some time. Spheres weren't too bad. Below is a screenshot of my infinite planes, spheres, and a bug I fixed with the model texturing.
I've added a SphereLight primitive to my ray tracer, which is essentially a point light with a radius, which allows you to have soft shadows. Unfortunately, I'm needing to have something like 200 shadow rays before it stops looking like a bunch of dots. I improved my sampling algorithm to use what they talked about on this website:
It seemed to help, but it still requires an enormous amount of samples, and slows things down a lot. The biggest problem is that even when I have a small number of simple objects in the scene, it is still slow, so any sort of acceleration data structure, like a kD tree won't really help with this problem. (OK, it will help a lot, but that many shadow rays is so many, that it will be slow anyway. I'll have to come up with another way to improve it. Anyway, here's a screenshot of my soft shadows:
OK, one of the things that I've wanted to do with my ray tracer, ever since I first started looking at it, is a depth of field thing. Normally, a simple ray tracer or real time graphics will emulate a pinhole camera, which means everything will be in perfect focus, no matter how close or far away it is. This is actually sort of ideal, and is normally what people want. However, perfect focus doesn't look photorealistic, because that's not how a camera, or the human eye works. So many of the more advanced ray tracers will add in depth of field and produce a much more photorealistic image. The basic idea is that you need to add in an aperture size for your camera lens, and a focal length, which is the distance that things should be in focus. Then for each pixel you look at, you shoot a ray into the scene until it crosses the focal plane and figure out what the point is that it crosses at. This was easy for me to do because I create the ray, unitize the direction vector, then lookup a t value equal to the focal length on the ray. This gives the focal plane intersection point. Then you sample repeatedly with rays that start at the original camera location, with a random offset of up to the aperture size. You average the samples and you get your final result.
Performance is a huge issue, once you add in a combination of soft shadows, antialiasing, and depth of field. The ray tracer creates tons of rays for just a single pixel, and even in my simple scene below, was taking too long, so I turned off the soft shadows. My wife was saying that perhaps the camera was too blurry in the image below, but I was really only doing it like this to show the depth of field blur in action. She's probably right. It's overly blurry.
Well over the last little bit, I've added support for a couple of new primitive types. I wanted to add support for cylinders, and along with that, came support for circles. So now I have two new primitive types. Additionally, I laid a lot of ground work for fully transformable objects, which may be my next step. By that I mean being able to scale, rotate, and translate an object in any way that you can dream up.
Below are several images that I created as I implemented cylinders. I first created the cylinder as an infinite cylinder. Then I cut off the ends so it was basically a hollow tube. Then I implemented the circles, which are shown in one of the images below. Then I added the top and bottom circles to the cylinders. Finally, I got the texturing going on the cylinders and circles.
Transformations are working now, but it took some time. My one recommendation for anyone who wants to allow affine transforms of objects should do that from the start. It was a pretty big hassle to convert everything over. It would have been easier if I had just done that from the beginning. Spheres where the first thing I did, and they took some time because they were the first. Planes took a while too, because they were such an odd creature. I had to come up with a method of rotating the plane's normal into another vector (the +Y axis). There are a few algorithms online for doing this, as long as you know what to search for. Try "rotate a vector into another". It had a lot of special conditions too. At any rate, it's working now, so I'm relieved. Below are some images from this process. In a file you can specify scaling and translation values, but not rotations yet. In these images I've added in a rotation value to show that rotations work as well.
- Model paths must be one string
Some things I'd like to do:
- Distributed ray tracing over the network to improve render times