The sphere above is at location (0, 0, 2.0) with a radius of 0.5 and a color of 0.8. It looks like a circle at this point because we haven't added any lighting effects.
The program that produced the above image consisted of ~150 lines of code with four functions:
main - loops through pixels, computes initial ray, calls intersect_sphere
normalize - normalizes a given vector
define_objs - assigns the sphere (as defined above) to the first element of the object
array
intersect_sphere - performs ray/sphere intersection test
Now we can see the 3D shape of the sphere. The portion of the sphere away from the light is completely dark since it receives no light.
In the code, we have added some new functions:
do_lighting - computes the diffuse light contribution at a given intersection point
vector_subtract - subtracts one vector from another and assigns the result to a third vector
dot_product - computes the dot product of two vectors (giving us the cos of the angle between
the two vectors)
The image above looks more like natural lighting by adding in the ambient component (light that exists everywhere in the environment).
The ambient computation requires only a small code addition to do_lighting.
The highlight on the sphere in the above image results from the specular component.
In the code, we added/modified these functions:
do_lighting - added code to compute the specular lighting component
vector_multiply - multiplies a vector by a scalar and assigns the result to a third vector
Now that we have lighting working for our sphere, we can move it to a less prominent position where it will become part of a larger scene.
The center for our sphere is now at (0.5, 0.8, 4.0); radius and color are the same.
The image above shows a second sphere. We can't tell exactly where it is in 3D space because we have no additional lighting or depth cues to help us out.
The define_objects function needs to be modified to include the new sphere, with center at (-0.5, 0.15, 4.2), radius equal to 0.6, and color 0.8.
We need to modify the main routine to keep track of the smallest t during the object intersection tests. To determine this depth, you can add a function called vector_length. We will perform lighting calculations for the closest intersection point only.
Our scene is starting to take shape now that we've added a ground plane. Note that the code written to compute lighting for the spheres also works on the plane.
For simplification, the ground plane will be at y = -1 (we'll generalize this later to handle planes anywhere in the environment). You'll need to add an intersect_plane function to test for plane/ray intersections. It can return TRUE/FALSE in the same way that intersect_sphere does. You will also need to modify main to call the correct intersect function based on the object type. Additionally, you'll be able to tell for sure whether or not you're computing the closest t correctly since this is the first image where objects in the scene actually occlude (or hide) each other.
Our ground plane looks much more interesting with a checkerboard. Note that there are some "jaggies" (or pixels that make our straight lines in the checkerboard appear to be jagged). We'll fix those later with antialiasing.
To produce the checkerboard, you can simply add some code to the beginning of do_lighting to find out what the color of the object should be if it's a plane at the given intersection point.
Now that we have shadows, we can tell how the objects are related to each other in space. The imperfections in the shadows above result from jpg compression and should not be evident in your ppm images.
To get shadows to work, you'll need to write a new function, shadow_test, that will be called from do_lighting. The function will shoot a ray from the current intersection point towards the light and test this ray for an intersection with any object in the scene (except itself). If any intersection is found, the function returns TRUE and we will use ambient shading only.
The program producing the above image contains about 450 lines of code.