CSE168 Rendering Final Project
I originally intended to produce a recreation of Henrik Wann Jensen's Cornell box scene progression using full global illumination, but was informed that I might want to focus on something with a little more "eye candy". Alright, the Cornell scene is admittedly a little bland, but what if the folks at Cornell were careless one night and their precious little box was...flooded? With that thought in mind, I decided to scrap full global illumination in lieu of something more candy-like: the caustic patterns formed on a diffuse surface due to the refraction through a wavy water surface.
Features of the final product include: caustic photon mapping with projection maps, area lights and soft shadows with adaptive sampling, jittered sampling with Gaussian filtering, absorption with Beer's law, two procedural textures, and an animation of the caustic patterns. To appease those of you with a shorter attention span, I'll start by displaying one of the final images:
2. Area Lights and Soft Shadows:
Rectangular area lights in my program are defined by two light basis vectors, which are modified by matrix operations specified in the driver text file and span the area of the light. With adaptive sampling enabled, each hit point samples the 4 corners and center of the light. If all 5 samples hit the light, then the radiance from those samples are used. If all 5 miss, then no contribution from that light is used. Otherwise, n random samples are chosen over the area of the light for the radiance estimate. 50 samples are used for most of my images (x16 for the antialiased ones), but more are necessary for larger area lights and other scene factors.
From left to right: 1 light sample, 15 samples, and 150 samples
3. Antialiasing and Filtering:
Two sampling methods are available in my renderer: uniform multisampling and jittering. Uniform multisampling casts multiple rays uniformly across each pixel, while jittering chooses a random location in each uniform area within a pixel. Jittering reduces the aliasing due to a low sampling frequency by converting it into noise, but this noise can be very variant at edges unless an adequate number of samples is used. To reconstruct the sampled image, two filters are available: a standard (lazy/crummy) box filter, and nice smooth Gaussian filter. Most of the images here used 16 jittered samples and the Gaussian filter. I'm sure you've all seen a hundred pictures illustrating the differences between aliased and antialiased images, so I'll save myself the time and spare you the pain...
4. Caustics and Project Mapping:
One of the largest optimizations for caustic maps is projection mapping, which ensures that caustic photons are only cast in the direction of caustic objects. Rather than casting photons across the entire hemisphere, projection mapping estimates the hemispherical coverage of caustic objects at the light. A large portion of the work on my final project was focused towards this end. The ultimate goal of the projection mapping phase is to create a grid in 2-D canonical random variable (henceforth CRV) space and store the cells that might map to the direction of a caustic object.
Noticing that rectangles in spherical coordinates map to rectangles in CRV space, my first approach was the following:
1: Obtain the bounding spheres for each caustic object
2: Determine the circle created by projecting the bounding sphere onto the unit hemisphere at the light point by computing the spherical coordinates of the direction to the center, and the angle to the edge
3: Compute the rectangle in spherical coordinates that bounds the projected circle
4: Use the inverse uniform hemispherical sampling transformation on two opposite corners to determine the mapped rectangle in CRV space.
All well and good in theory, but after implementing it I realized that I hadn't accounted for the case where the projected circle crosses the peak of the hemisphere. At that point the theta value in spherical coordinates becomes negative, but the uniform sampling transformation from CRV space only maps to positive theta values. The only easy solution was to use 2*PI for the phi value and the maximum theta value, greatly overestimating the coverage of caustics that cross the peak. After some thinking, I settled on a less direct approach:
1: Map the center of each bounding volume to CRV space and record the radius of the projected circle on the hemisphere
2: Loop through the grid corners, apply the uniform sampling transformation to obtain the vector direction for each.
3: Determine the distance from the vector to the projected center and test whether it's less than the radius
4: If so, mark the 4 surrounding grid cells as active.
After a grid has been created, the renderer loops through each active grid cell and chooses a random location within it to cast a caustic photon. After the photon casting phase, the radiance of all photons must be modulated by the ratio of active cells to total cells. With several optimizations, the projection mapping phase with 2500 grid cells in CRV space and multiple caustic objects takes less than about 1/2 a second (estimated with the ever-accurate human clock)
Below is a simple scene with a caustic object, and next to it is an ASCII output of the corresponding projection map (in CRV space, with spherical mappings noted) for the light in the scene. This scene was rendered with 10,000 photons, but the same scene without projection mapping would have required 343,000 photons for the same coverage!
5. Water Absorption:
Crystal-clear water is great for drinking, but sort of less-than-awesome for rendering. Radiant absorption by volumes with varying spatial density can get quite tricky, but adding a uniform amount of dye to our water isn't too hard at all. Beer's law for calculating uniform absorption in a medium simply yields A=a*l*c*L, where 'a' is an absorption coefficient (a Vector in our case), 'l' is the length travelled in the medium, 'c' is the concentration of absorbing species, and 'L' is the radiance at the hit point. a and c can be combined into a single vector, and the product of the two is the amount of radiance absorbed per unit length travelled in the medium. For example, a value of a*c = (.1,.3,.3) would eat 3 times as much blue and green light than red light, and would absorb all radiance after 10 meters. Here are some obligatory examples:
From left to right: a*c = (0,0,0), a*c = (.1,.1,.05), and a*c = (.4,.4,.1)
6. Water Surface:
One of the most crucial components of this project was the water surface texture (I have a feeling that the caustic patterns wouldn't have been too spectacular with a calm water surface). Despite the resulting visual complexity, the texture itself was fairly easy to produce. The surface is an ideally refractive flat surface with a variant of fractal Brownian motion applied as a bump map. Fractal Brownian motion (fBm) is implemented as repeated applications of Perlin's noise function, using each result as a seed for the following calculation. The applications of fBm are widespread - a Google search for "Fractal Brownian Motion" shows applications and observations of fBm in medical imaging, island generation, pressure sensors, cloud creation, and even flood prediction.
From left to right: A polygonal field displaced by an fBm texture (somewhat similar to how my water surface would look displacement-mapped), a multi-fractal landscape, and an animation of fBm
For those interested, the specific parameters used in the water texture were octaves=2.0, lacunarity=2.0 (the most common value in fBm applications), and increment(H)=3.0. To animate the water surface, the hit location was increased in the lateral direction a specified increment every frame.
Here's a rendering of the scene using the fBm as a texture, rather than a bump map:
7. Procedural tiles:
Nobody wants boring textures, and there's no fun in using someone else's, so I decided to create a procedural tile texture for the floor of my Cornell box. The texture was inspired by the following image from Steve Worley:
This image appears in virtually every introduction to cellular & Worley noise, but I've never seen the slightest hint of an implementation detail so I decided to see if I could mimic it. After playing with it for a while, I came up with the following:
+User Parameters: Low (grout) color, High (tile) color, low threshold, high threshold.
+Base texture: The flat texture itself is an implementation of Worley noise (duh), where the F2-F1 values are basically interpreted as the height of the tiles at each point. Any height under the low threshold is the 'grout', and is evaluated as the low color blended with Perlin noise. Any value above that gets the high color.
+Coloring: To create the varying colors, the high color is altered by a small percentage calculated using random numbers seeded by the index of the nearest point in the cellular grid(to ensure identical coloring per tile)
+Shading: The same cellular texture is evaluated as the bump map, with the added exception that any points above the low threshold and below the high threshold return a linear interpolation between the low and high colors.
+Consistency: To allow re-use of the same texture, I also added a parser parameter to output the random cellular grid values to a file, and a similar one to read them back in. Here are a few more samples with varying thresholds/colors:
7. The big finale:
Of course, the coolest thing about caustics due to waves is the animation thereof. Click the picture below for a (1MB, wmv-format) animation of 66 frames of caustic-y goodness.
I think my project ultimately displayed the intended caustic effects very nicely. Personally, my favorite component is the interior pattern and warping of the caustic from the glass ball in the animation. However, there's still a lot more that can be done. With more time, one could explore:
+Displacement mapping of the fBm water texture for a more convincing water surface and interreflections
+A better projection mapping algorithm allowing for different bounding volumes (OABBs/AABBs for instance)
+Volumetric photon mapping using ray marching for beams of light in the water
+Optimization strategies to allow re-use of parts of the caustic photon map from frame-to-frame
+Bumpmapping of the tile surfaces for tiles looking less-new.
Thanks for watching/reading!