Assignment 5 Writeup
Students: Shantanu Das (shantand), Pepin Hazan (phazan)
For our assignment, we added irradiance caching to Scotty3D. Irradiance caching is a technique that accelerates rendering by caching indirect lighting on select points of diffuse surfaces. We then interpolate these cache entries at other locations rather than repeatedly carrying out the full ray tracing procedure. Our implementation is based on the original proposal of the technique by Ward et al. in their 1988 paper A Ray Tracing Solution to Diffuse Interreflection, with some modifications and enhancements that will be indicated throughout this writeup. This writeup is structured in roughly chronological order in terms of implementation.
I. Stratified Sampling
In order for irradiance caching to be effective, our cache entries must be as accurate as possible. So in order to reduce variance, we added stratified sampling to our cosine weighted hemisphere sampler. Essentially, we divide our hemisphere into a number of cells and then sample from each cell. Our implementation of stratified sampling can be found in StratifiedCosineWeightedHemisphereSampler3D::get_sample in sampler.cpp.
II. Irradiance Cache Data Structure
Our irradiance cache entries are stored in lists of structs which contain the following (see CacheEntry in pathtracer.h):
- position - position of hitpoint of parent ray
- normal - normal at hitpoint of parent ray
- irradiance - indirect lighting at hitpoint
- range - radius around cache point over which entry is potentially valid
- rotational gradient (more on this later)
We maintain a separate irradiance cache for each ray depth. These are indexed by depth in PathTracer::irradianceCache.
III. IRRADIANCE CACHING
Our caching algorithm works as follows: When calculating indirect lighting, we first check if any valid cache entries are present for interpolation. If not, we create a new entry.
We iterate through all cache entries with the same depth as our current ray. We check for valid entries by verifying that (1) our hitpoint is within the range of the entry and (2) our entry is not a "false positive" (that is, it's not oriented in front of our hitpoint in such a way that it's within range but should not be counted). This check can be found in PathTracer::is_entry_valid.
Each valid entry is weighted according to the following formula:
As described in , this is an alternative formulation to Ward et al.'s initial weighting formulation. The lower left left position-based error term ensures that cache entries closer to our hit point are given more weight, and the lower right normal-based error term ensures that cache entries with similar surface normals to our hit point are given more weight. Note that R is the range for our cache entry and kappa is a constant that affects cache accuracy vs. performance. Our cache interpolation implementation can be found in PathTracer::attempt_to_use_cache.
If no valid entries are found, we perform the full ray tracing procedure and create a new cache entry using our ray's position, normal, and the resulting irradiance. We determine our entry's range using the minimum distance-to-surface of the rays cast for our stratified sampling. Just as for our weighting calculation, this is an alternative range calculation to the original harmonic mean calculation described by Ward et al. Our range assignment ensures that we get more cash entries in crevices, where indirect lighting changes fastest. Our cache entry creation can be found in PathTracer::trace_ray.
Below is a comparison of the original Scotty3D implementation and our implementation of irradiance caching with 8x8 stratified sampling (both with parameters -l 16 -m 3 -t 8). We can see that stratified sampling leads to a much higher quality image, and irradiance caching keeps rendering time manageable. With stratified sampling enabled and no irradiance caching, we have never successfully completed this rendering. It is far too slow.
IV. Double Pass
An intrinsic problem with creating and interpolating our cache in a single pass is that later hit points have more cache entries to interpolate than earlier points. This can lead to visual discrepancies. In order to solve this issue, we render our scene twice. The second rendering is significantly faster, as it reuses the cache entries created during the first. And it allows for smoother interpolation overall. Our double pass implementation can be found in PathTracer::worker_thread.
V. ROTATIONAL Irradiance Gradients
After implementing irradiance caching with double pass, the output still contained image artifacts. We observed that these were occurring in sections where the normals were changing rapidly, for instance on spheres. In order to reduce these artifacts, we implemented rotational irradiance gradients. This gives us information on how irradiance changes according to rotation and helps in reducing interpolation error.
We calculate the rotational gradient along with our irradiance value calculations and store it as a field in our cache entries. It's calculated using the following formula:
The rotational gradient is a vector in the direction of fastest irradiance change, with magnitude reflecting the speed of change. We also modified our interpolation procedure. Rather than simply assigning a weight to each cache entry, we first apply our rotation gradient as follows:
Our implementation of rotational irradiance gradients can be found in PathTracer::trace_ray.
VI. Cache Visualization
In order to facilitate debugging, we added the ability to visualize cache entries. After rendering, each pixel that contains at least one cache entry is colored. Additionally, cache entries at each depth are colored differently (of course, each pixel will only get one color—even if it contains cache entries at multiple depths).
VII. Future Improvements
If we had more time, this project could be extended in a number of ways. Most importantly, we could use a more time-efficient data structure for storing our cache entries. Specifically, an octree is typically used in order to accelerate lookup time for cache entries. Additionally, we could use translation gradients in addition to our rotation gradients in order to further improve the interpolation.
The following command renders diffuse spheres using irradiance caching:
./scotty3d -i 0 -l 16 -m 3 -t 8 ../dae/sky/CBspheres_lambertian.dae
To show cache point visualization, use this command instead:
./scotty3d -i 1 -l 16 -m 3 -t 8 ../dae/sky/CBspheres_lambertian.dae
In general, irradiance caching with or without cache point visualization can be enabled using a command line parameter:
-i 0: use irradiance caching without visualization
-i 1: use irradiance caching with visualization
When irradiance caching is enabled, we automatically use 8x8 stratified sampling and render with two passes. When irradiance caching is disabled, behavior remains unchanged from the original implementation (no stratified sampling and no double pass).
Our source code is located at:
While our implementation generally works, we've found that rendering more than once (pressing 'r' then 'm' then 'r' again) can cause crashes when running from AFS. We've had no issues locally, but we suspect it's related to our double pass implementation. We recommend relaunching after each rendering if this occurs.