Irradiance Caching

Assignment 5 Writeup

Students: Shantanu Das (shantand), Pepin Hazan (phazan)

Overview

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.

We divide our hemisphere into cells and sample from each cell (Source [3])

We divide our hemisphere into cells and sample from each cell (Source [3])

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.

This is a "false positive" as described in [3], where pi is a cache entry and p is our hit point.

This is a "false positive" as described in [3], where pi is a cache entry and p is our hit point.

Each valid entry is weighted according to the following formula:

Weight calculation for interpolating cache entries as described in [3]

Weight calculation for interpolating cache entries as described in [3]

As described in [3], 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.

Original Scotty3D rendering (time 8.1711s)

Original Scotty3D rendering (time 8.1711s)

Irradiance caching and 8x8 stratified sampling (time 10.9915s)

Irradiance caching and 8x8 stratified sampling (time 10.9915s)

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.

Irradiance caching with 8x8 stratified sampling and double pass (first pass time 11.3646s, second pass time 6.9207s)

Irradiance caching with 8x8 stratified sampling and double pass (first pass time 11.3646ssecond pass time 6.9207s)

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:

Formula for rotational irradiance gradient as described in [3]

Formula for rotational irradiance gradient as described in [3]

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:

Modification of interpolated cache entries as described in [3]. The right term applies our rotational gradient.

Modification of interpolated cache entries as described in [3]. The right term applies our rotational gradient.

Our implementation of rotational irradiance gradients can be found in PathTracer::trace_ray.

Irradiance caching with 8x8 stratified sampling, double pass, and rotational irradiance gradients (first pass time 12.2244s, second pass time 7.0216s)

Irradiance caching with 8x8 stratified sampling, double pass, and rotational irradiance gradients (first pass time 12.2244s, second pass time 7.0216s)

 
Without rotational irradiance gradients

Without rotational irradiance gradients

With rotational irradiance gradients

With rotational irradiance gradients

 

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).

Rendering with cache entry visualization

Rendering with cache entry visualization

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.

VIII. Usage

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:

/afs/cs/academic/class/15462-f16-users/phazan/asst5/

IX. Issues

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.

X. References

[1] Irradiance Caching: Part 1

[2] Physically Based Rendering: From Theory to Implementation

[3] Practical Global Illumination with Irradiance Caching

[4] A Ray Tracing Solution to Diffuse Interreflection