Tuesday, October 14, 2008

Raytracing for Dummies

A little while ago, I posted about NVidia's CUDA; this is a follow-up on what I've been doing with it.

Most people, I noticed, seem to start out by writing yet another Matrix Multiplication for parallel processing. I don't know about the rest of you, but I never seem to need a faster matrix multiplication... Heck, I rarely even need a slow one.

So instead, I decided to write a raytracer. I wrote one in Java just for fun a few months ago, so this isn't completely out of the blue; Java, simply by being Java, is not exactly the fastest language for rendering. So this time I decided to focus on performance, and add visual features after I was happy with that.

About the title of this post: I plan to describe how a raytracer works from a programmer's perspective. Here's where that starts:

Raytracing is done by shooting rays from the viewer (camera) into the scene to see what they hit. An image is generated by using the results of these ray-object intersections to color pixels.

To make things more complicated, at each ray-object intersection, multiple rays may be shot in other directions to detect light sources, reflections, and refractions. All of these rays return color data, which is somehow combined into a single color for a pixel.

This is the kind of description that you can find anywhere. The problem that I have with it is that there are no details. How do you shoot the rays? What directions? How do you combine the colors?

I'm going to answer some of this, and I'm going to be specific.

Firstly, you need to understand how everything is laid out. To start, you will need a camera and a viewport. Think of the camera as the eye of the person viewing your scene, and the viewport as the screen displaying the image.

You will need the following data: Position of the camera (x, y, z), direction the camera is facing (x, y, z), distance from the camera to the viewport, dimensions of the viewport (width, height) both in pixels and in scene units.

Scene units? Your scene should be defined using floating-point coordinates in 3-space. You can choose whatever size these units should represent, just be consistant within your scene. I like to think of 1 unit = 1 foot.

Now what about shooting those rays into the scene? This is actually relatively simple now: Shoot one ray from the camera through each pixel of the viewport. That will give you a starting point and a direction, which is exactly how rays are defined. Then all you need to do is search your scene objects for intersections.

This involves a significant amount of 3-D trigonometry, however, which wasn't exactly stressed in my High School. But I've figured out some calculations that should help:

Firstly, you need to imagine your viewing plane as being a screen in front of the camera. You need to know the positions of each of the pixels in this screen so you can determine the directions for all of your rays. Here is how to compute the position of each pixel:

Variables used here that you must define (a vector has .x, .y, and sometimes .z coordinates):

  • cameraPos = The position of the camera; a floating-point 3-D vector
  • cameraDir = A point that the camera is looking at directly; a floating-point 3-D vector
  • cameraDist = The distance between the camera and the viewport; a floating-point number
  • viewportSize = The size of the viewport in scene units; a floating-point 2-D vector
  • screenSize = The size of the screen in pixels; an integral 2-D vector

The function 'normalize()' on Vector3 objects returns a vector pointing in the same direction that is 1 unit long.

So if anyone is trying to write their own raytracer, grab that 'angle' variable and use it as the direction of each ray you try to shoot.

Lighting is one of the simplest features you can add to a raytracer. Each time a ray hits an object, loop through all of your light sources and try to reach it from the point where the ray hit the object; if you can reach the light source without striking another object in between (including the one that the original ray hit), that point is lit by that light source.

One thing to be careful of, though: Any time you have a hitpoint and you want to shoot a new ray from there, add just a little bit of the normal for the surface at that hitpoint to the starting point for the new ray, so you don't immediately strike the object you are bouncing off of.

Here is a simple ray-object intersection equation that you may find useful; it is for a sphere, and was originally taken from the WildMagic foundation library of intersections (GPL code):

Requires:

  • center = center of the sphere; floating-point 3-D vector
  • radius = radius of the sphere; floating-point number
  • origin = starting point of the ray; floating-point 3-D vector
  • direction = direction of the ray; floating-point 3-D vector

Outputs:

  • hitpoint: the first point where the ray hits the sphere, or null if it does not; floating-point 3-D vector
  • normal: the normal of the sphere surface where the ray struck it; floating-point 3-D vector

A note about ZERO_TOLERANCE: Since floating-point calculations are not exact (if you have variables A and B which both contain numeric results that should be equal but were achieved using different calculations, A == B may come back false), ZERO_TOLERANCE is used to put a small boundary around 0 that is considered equal to 0. You should probably set it to something like 0.00001.

Combining colors is a much more difficult concept, I've discovered. Here is what I have been doing, although I am not satisfied with it:

Colors are represented as Vector3 objects; x = red, y = green, z = blue. Each color channel should be in the range [0, 1] with 0 = 0 and 1 = 255.

I simply add the color of each object struck by the ray to the colors of any lights shining on those hitpoints, and either clamp (bound x, y, and z to the range [0, 1] independently of each other), or scale (scale all components back by the largest one, if any are above 1) the colors. I haven't found a better way to do this yet, although I am planning to try scaling back the influence of each color as I traverse more and more rays to reach it.

Hope I got someone interested in this stuff; it's a cool way to generate 3-D scenes.

No comments: