2022 November 12,

CS 311: Ray Tracing

The third part of CS 311 deals with a different algorithm: ray tracing. We don't have time to do as much as I'd like, but we do learn the basics at least. We definitely see how ray tracing can produce results that, compared to triangle rasterization, are more realistic and more computationally costly.

You work with your newly assigned partner. Except where otherwise noted, each homework assignment is due at the start of the next class meeting. Remember our late policy: If you don't have an assignment finished on time, then don't panic, but do e-mail me about it, so that we can formulate a plan for getting back on track.

Day 23

Today we implement the core algorithm of ray tracing, specialized to the case in which all scene bodies are spheres of uniform color.

640ray.c: Study. This file grows over time.

640mainSpheres.c: Study what's there. Implement getIntersection, getSceneColor, and three chunks of render. The getSceneColor function should call getIntersection on each body in the scene. My implementation of render is greatly helped by my thorough understanding of 300isometry.c and 300camera.c. The user interface lets the user switch between perspective and orthographic cameras, so make sure to handle both correctly. The running program should look like this:

640mainSpheres screenshot

Clean up and hand in 640mainSpheres.c on the COURSES file server. Also hand in your four helper files: 250vector.c, 280matrix.c, 300isometry.c, and 300camera.c.

Day 24

Today we re-instate texture mapping. We introduce an abstraction for the material that makes up a surface (at a particular point of that surface). We also introduce an abstraction for bodies in the scene. That may sound like a lot of work, but each part is fairly short.

Texture

650vector.c: Into a copy of 250vector.c, paste the following function.

/* Partial inverse to vec3Spherical. Always outputs 0 <= rho, 0 <= phi <= pi, 
and 0 <= theta <= 2 pi. */
void vec3Rectangular(
        const double v[3], double *rho, double *phi, double *theta) {
    *rho = sqrt(vecDot(3, v, v));
    if (*rho == 0.0) {
        /* The point v is near the origin. */
        *phi = 0.0;
        *theta = 0.0;
    } else {
        *phi = acos(v[2] / *rho);
        double rhoSinPhi = *rho * sin(*phi);
        if (rhoSinPhi == 0.0) {
            /* The point v is near the z-axis. */
            if (v[2] >= 0.0) {
                *rho = v[2];
                *phi = 0.0;
                *theta = 0.0;
            } else {
                *rho = -v[2];
                *phi = M_PI;
                *theta = 0.0;
            }
        } else {
            /* This is the typical case. */
            *theta = atan2(v[1], v[0]);
            if (*theta < 0.0)
                *theta += 2.0 * M_PI;
        }
    }
}

650mainTexturing.c: In a copy of 640mainSpheres.c, implement the following function. Then, in getSceneColor, instead of just using the uniform color of the winning body, get the winning body's texture coordinates (S, T) and normal n, and modulate the uniform color with n or (S, T, 1), as a test.

/* Given the sphere that just produced the given rayIntersection. Outputs the 
sphere's texture coordinates at the intersection point. Also outputs the 
sphere's unit outward-pointing normal vector there, in world coordinates. */
void getTexCoordsAndNormal(
        double r, const isoIsometry *isom, const double p[3], const double d[3], 
        const rayIntersection* inter, double texCoords[2], double normal[3])

650mainTexturing.c: Include 150texture.c. In initializeArtwork, initialize a texture. (Don't forget to finalize it in the appropriate place too.) In getSceneColor, use the texture coordinates to sample the texture, and modulate that color with the uniform color. When the program runs, the spheres should visibly rotate, because of the animation in the time step handler. Here's mine (using a photograph of this gentleman):

650mainTexturing screenshot

Material

A material has lots of features, most of which we ignore in today's work. Today's work with materials implements only ambient lighting.

660ray.c: Study.

660mainMaterial.c: Make a copy of 650mainTexturing.c. At the top of the artwork section, implement the following "material shader". Like all shaders, it is supposed to be specific rather than general. It describes a particular material, which is affected by ambient light, with the diffuse surface color sampled from the 0th texture. The material does not reflect diffusely or specularly, and it has no mirroring or transmission. It does not depend on any material uniforms. (We use the uniforms in later work.)

/* Based on the uniforms, textures, rayIntersection, and texture coordinates, 
outputs a material. */
void getMaterial(
        int unifDim, const double unif[], int texNum, const texTexture *tex[], 
        const rayIntersection *inter, const double texCoords[2], 
        rayMaterial *material)

660mainMaterial.c: Delete the global variable that stores the body colors. Create and initialize a global variable to store the scene-wide ambient light color cambient. In getSceneColor, after you obtain the texture coordinates, use getMaterial to obtain the material. Set the outgoing RGB color as directed by that material. Here's mine, with gray ambient light:

660mainMaterial screenshot

Body

A body consists of a geometry and a kind of material. (Right now the only geometry we have is the sphere, but later we have other geometries.) These parts are independent of each other; you can pair any geometry with any kind of material. In this chunk of work, we reproduce 660mainMaterial.c using this body abstraction.

670body.c: Study the bodyBody definition and skim the rest.

670sphere.c: Implement the two functions by copying and pasting from 660mainMaterial.c and using the 0th uniform as the sphere's radius.

670mainBody.c: Make a copy of 660mainMaterial.c. Delete the sphere section, which contains the two functions that you just moved to 670sphere.c. There are two global variables for storing the four spheres. Replace these two global variables with an array of four bodyBodys. In initializeArtwork, initialize these bodies to be spheres, with the only getMaterial available, no material uniforms, and one texture each. Set their radii, textures, and translations. Finalize the bodies where appropriate. In the time step handler, update the isometries that are inside the bodies. Finally, change getSceneColor to have the following interface. Update its implementation to render the bodies that it's given instead of the old global variables. To clarify, it should call bodyGetIntersection on each body, then call bodyGetTexCoordsAndNormal and bodyGetMaterial on the winning body. Test.

void getSceneColor(
        int bodyNum, const bodyBody bodies[], const double p[3], 
        const double d[3], double rgb[3])

You have six files to hand in: 650vector.c, 650mainTexturing.c, 150texture.c, 660mainMaterial.c, 670sphere.c, and 670mainBody.c. Clean them up and submit one copy to COURSES as usual.

Day 25

Because of the exam on Friday, this work is due Monday rather than Friday. We implement Phong lighting based on arbitrarily many abstracted lights. In each step, try to code carefully, because you aren't really able to test until around the fourth step.

One Light

680light.c: Study.

680mainLights.c: Make a copy of 670mainBody.c. Include 680light.c. Change getSceneColor to match the specification below. Elsewhere, where you call getSceneColor, pass it the ambient light color and zero lights. Test. Your program should work exactly as it did earlier, but now this crucial function uses no global variables.

void getSceneColor(
        int bodyNum, const bodyBody bodies[], const double cAmbient[3], 
        int lightNum, const lightLight lights[], const double p[3], 
        const double d[3], double rgb[3])

680mainLights.c: Edit getSceneColor so that, if the material has diffuse or specular reflection, then the function loops over all lights, asking each light for its lighting effect and then adding its diffuse and/or specular terms to the ambient term (if any). You need ucamera; it should be computed as the normalization of -d rather than anything involving the camera object. Check your logic carefully. For example, does your logic work if the material has specular reflection but not ambient or diffuse? Test. There are still no lights, so the program should work exactly as it did earlier.

680mainLights.c: Edit getMaterial so that there is diffuse and specular reflection. Let's agree that the specular color is the first three uniforms and the shininess is the fourth uniform. In your body initializations, give each body four material uniforms set with reasonable values (white with shininess 64?). Test. There are still no lights.

680mainLights.c: Add a global variable to hold an array of lightLights. For now, let's say that there's just one light, and it's directional. In initializeArtwork, initialize that light with three uniforms and the getDirectionalLighting function that you're about to write. Set the light's isometry's rotation. Elsewhere in the artwork section, finalize the light, and write the getDirectionalLighting function. Use the uniforms for clight. Get ulight by rotating the direction (0, 0, 1) by the light's isometry. The distance should be rayINFINITY. Pass the light array to getSceneColor. You should now have a lighting effect.

680mainLights.c: By editing getMaterial, try various combinations of ambient, diffuse, and specular. For example, try a material that has diffuse and specular but no ambient. Make sure all combinations work.

Two Lights

690mainLights.c: In a copy of 680mainLights.c, add a second light. Make it positional. Again there are three uniforms for clight. You need a new getPositionalLighting function. In it, compute ulight based on the light's position and x. The distance should also be correct. You are not expected to implement attenuation. The material should have ambient, diffuse, and specular, but here's mine with specular only:

690mainLights screenshot

In the usual way, clean up 680mainLights.c and 690mainLights.c and submit them to COURSES.

Optional: Work Ahead

Feel free to get started on shadows. They don't take a ton of work to implement.

Day 26

Because of the exam, no new work is assigned today.

Day 27

Today we implement shadows and mirrors. They both require casting extra rays into the scene, but our general ray-tracing machinery makes them pretty simple to implement.

Shadows

700mainShadows.c: Make a copy of 690mainLights.c. Implement the function below. I wrote my version by copying getSceneColor and then stripping it down. Use it to test whether each fragment is in shadow from each light. If it is, then that light's diffuse and specular contributions should not be included at that fragment.

/* Casts the ray x(t) = p + t d into the scene. Returns 0 if it hits no body or 
1 if it hits any body. Used to determine whether a fragment is in shadow. */
int getSceneShadow(
        int bodyNum, const bodyBody bodies[], const double p[3], 
        const double d[3])

Here's a screenshot of mine. I've moved the spheres farther apart, to make the shadows easier to see.

700mainShadows screenshot

Mirrors

710mainMirrors.c: Make a copy of 700mainShadows.c. Rename getMaterial to getPhongMaterial. Make a function getMirrorMaterial that returns a perfect mirror (white cmirror and no lighting). In initializeArtwork, make one of the satellite spheres mirrored. In getSceneColor, implement mirroring if the material hasMirror. The mirror effect requires a recursive call to getSceneColor. The recursively computed color is modulated with cmirror and then added into the fragment's overall color.

710mainMirrors.c: There is a danger of infinite recursion — not in this scene right now, but in general. So insert a new argument int recDepth at the very beginning of getSceneColor's argument list. If recDepth is zero, then no recursive calls are allowed, which means that mirroring (and, later, transmission) are disabled. If recDepth is positive, then the recursive call for mirroring is made with a value of recDepth - 1. To test your code, make a second satellite sphere mirrored. Here's mine with a top-level recDepth of 3:

710mainMirrors screenshot

At this point, your code should assume that every material's hasTransmission is 0, but it should be able to handle every other feature of rayMaterial correctly.

Planes

720plane.c: Implement the two functions.

720mainPlanes.c: Make a copy of 710mainMirrors.c. Make a fifth body that is a plane with Phong lighting. The plane should be horizontal and below the spheres, and the lights should be somewhere above the spheres, so that the spheres cast shadows onto the plane. The plane should be stationary — not rotated in the time step handler. Here's mine:

720mainPlanes screenshot

In the usual way, clean up and hand in your four files — 700mainShadows.c, 710mainMirrors.c, 720plane.c, and 720mainPlanes.c — to COURSES.

Day 28

On this final day of our project, we write a program that ray-traces a mesh. To some extent we proceed by baby steps.

Preparation

First we must complete a bunch of small, simple tasks.

730ray.c: In a copy of 660ray.c, add an int index member to the end of rayIntersection. For reshes, it stores the index of the intersected triangle (if any). Spheres and planes ignore it.

730body.c: Study the bodyBody definition and skim the rest. What's new?

730sphere.c: In a copy of 670sphere.c, update the functions to their new interfaces, even though they don't use the geometry data.

730plane.c: In a copy of 720plane.c, update the functions to their new interfaces, even though they don't use the geometry data.

730mesh.c: In a copy of 190mesh.c (or any mesh.c from our first project), delete meshRender (and its helper functions).

730mainMeshes.c: In a copy of 720mainPlanes.c, include the five "730" files just discussed. Also include 250mesh3D.c. Update the material shaders to their new interfaces, even though they don't use the material data. Test. Your program should behave exactly as it did previously.

Meshes

Let's agree that a mesh packaged for ray-tracing is a "resh" (sigh).

740resh.c: Study. The first function has been implemented for you. The third function has a placeholder implementation, that must be fixed later. For now, just implement the second function. I recommend that you use the first function as a helper.

740mainMeshes.c: In a copy of 730mainMeshes, initialize and finalize a mesh. Don't make it very detailed, because meshes with many triangles are slow to render. Add a sixth body, that's a resh using that mesh, to the scene. Test. It should render to the correct pixels but with incorrect texture coordinates and normals.

740resh.c: Implement the third function for real. I recommend that you use the first function as a helper.

740mainMeshes.c: Test. Everything should work now. Here's mine, running at about 23 s/frame with a mesh of 112 triangles:

740mainMeshes screenshot

You have five "730" files and two "740" files to hand in. In the usual way, clean them up and submit one copy to COURSES.

Optional: Optimization

Here is a first step in speeding up the wretched slowness of ray-tracing meshes.

750resh.c: In a copy of 740resh.c, change reshUNIFDIM to 1. The one uniform is the radius of the tightest sphere, centered on the origin in local coordinates, that bounds the mesh. In reshGetIntersection, test whether the ray intersects the bounding sphere at any time between -rayINFINITY and rayINFINITY. (You need to copy code from sphGetIntersection and customize it for this purpose.) If so, then intersect with the mesh as usual. If not, then there is no need to intersect with the mesh.

750mainOptimization.c: Make a copy of 740mainMeshes.c. In initializeArtwork, when you attach the mesh to its body, compute the bounding radius of the mesh and store it in the body's geometry uniform. Test. You should get exactly the same imagery, as in the previous version, but it should be faster. My program sped up from 23 s/frame to 9 s/frame.