Sponza By Morning

Rendered in Danaan by Jerome Ko

Introduction

My final project is a rendering of the Sponza atrium, lit by nice morning sunlight. In the beginning I wanted to do some sort of architectural scene which would effectively show off global illumination. As I understood it, this was the big step towards realism that had yet to be tackled in the course. My idea finally settled on using the publicly available Sponza model since I lack skills in using modeling programs. The final image is a rendering of a dusty Sponza scene lit by morning sunlight.

Although I enjoy calling the ray tracer my own, at its core is the miro skeleton code, though it does little more than display wireframe models. But since everyone else does it, I named my renderer like some sort of lovable pet. Its name is Danaan. Using this, I implemented several algorithms, the most important of which I will highlight here:

Area Lights and Sampling Methods

The first step in moving up from a naive point light was to implement area lighting, which in turn would give soft shadows. Initially I implemented sampling of a square planar area light. The challenge here was finding a good sampling method that would reduce noise as much as possible in the shadows. I tested numerous sampling methods and tweaked parameters. I decided to use different methods depending on the specific algorithm I was implementing, as there are many instances where I needed to use Monte Carlo methods. For just computing direct lighting, I settled on using the VanDerCorput method in one dimension and the Radical Inverse method in the other. This was done to avoid correlation between the two dimensions.

Stratified sampling is always a challenge, as different methods might not necessarily produce smoother noise but rather a different pattern. While jittered supersampling produced high frequency "sandy" noise, experimentation with VanDerCorput produced a more uniform "brushed metal" sort of pattern, where the noise was made up of diagonal lines. See the reference images below. I decided to stick with VanDerCorput since for my particular scene it seemed to look better. Combining it with the Radical Inverse method helped to reduce the regularity of the pattern. In addition, successive calls to the VanDerCorput random number generator were done with an offset as far away as possible from being a multiple of the resolution. This was done to avoid any sort of correlation between adjacent pixels in the same row but more importantly in the same column.

In addition to generating well distributed random numbers, you have to use them in a "well-distributed" way in order to make them useful. Rather than uniformly distributing rays in the hemisphere around a hitpoint, importance sampling is used so that we cast more rays in "important" directions. Due to knowledge of the function we are trying to integrate beforehand, we can use the pdf of the function to correctly weight the intensity of the gathered irradiance. In calculating cosine-weighted ray directions, which is done all over the place, I used Malley's method. The algorithm states that if you generate points uniformly on a disk and then project them up to the unit sphere, you will have a cosine-weighted sampling of a hemisphere. In order to generate the direct illumination using a dome light, this method was used. If a generated ray did not intersect any geometry, that means it reached the dome light (which encompasses the entire scene). The details of implementation were found in Physically Based Rendering, a book by Matt Pharr and Greg Humphreys. To uniformly sample a disk, I used the method proposed by Shirley and Chiu in A Low Distortion Map Between Disk and Square, which details a concentric mapping from the unit square to the unit circle, thus reducing distortion.


4 jittered samples

4 VanDerCorput samples

25 jittered samples

25 VanDerCorput samples

Here is also a render from the early stages of the project of a dragon model, just to see the effects of using advanced sampling methods on a more complex model. A small planar area light was used to illuminate the scene. VanDerCorput was used for one dimension with Sobol for the other:


25 VanDerCorput samples

Some test renders of the Sponza model comparing differences between jittered and VanDerCorput sequence sampling. First we observe comparisons of 4 jittered samples and 4 VanDerCorput samples. Notice the improvement using VanDerCorput due to the diagonal pattern of the noise, which does not stand out as much as random noise:


4 jittered samples

4 VanDerCorput samples

I also tested the effects of anti-aliasing. Although render time grows linearly with the number of samples, notice that we can reduce the number of shadow samples and achieve good results. Although we shoot 16 rays per pixel in each of these images (16 shadow rays versus 4 shadowrays 4 primary rays), the anti-aliased image is smoother and the noise is reduced. The final image made use of this win-win situation to save time while also getting anti-aliasing effects:


16 VanDerCorput samples

4 VanDerCorput samples and 2x2 anti-aliasing

Photon Mapping

Global illumination can be done several different ways, but for my project I decided to implement photon mapping. I used Henrik Wann Jensen's book Realistic Image Synthesis Using Photon Mapping as a reference. In it, he provides an implementation of a Kd-tree structure for storing photons as well as methods for irradiance estimation. This saved me a lot of time, as I simply had to code up the photon emission to make it work. I initially emitted photons from a point light source to save time, but I later discovered that the indirect illumination estimation was not looking very realistic. I decided to change the light source to a dome light, just as I did for the direct illumination. This helped to better distribute the photons in the scene. Final gathering was done to reduce the splotchy artifacts characteristic of photon mapping. In order to sample the directions for photon emission, Li et al. (2003) showed that we can uniformly distribute ray directions in a sphere by connecting two uniformly chosen points on the surface of the sphere. I used this method to shoot direct photons into the scene. Once the direct photons landed on a surface, directions were calculated using a cosine distribution for indirect photons.

Besides splotchy artifacts, another common problem with photon mapping is light seemingly "leaking" into areas of the scene. Because the irradiance estimation function simply searches for photons in the sphere around the hitpoint while ignoring any geometry, it is possible that you gather photons from surfaces that have no radiance contribution to the current hitpoint. Naturally this occurs mostly at corners or thin surfaces. To remedy this problem, I referenced Per H. Christensen's Photon Mapping Tricks which suggest using a secondary gather method at points where you have these light leaks. Instead of shooting the same number of final gather rays, he suggests using a reduced number of "secondary gather rays" which help to save time.

Here is an early comparison of photon mapping using 200,000 photons versus 2 million photons. Notice that in a scene like this where there is mostly gradual variation in lighting, the difference is minimal. The direct illumination in these is done with a small planar area light, and photons are emitted from a point light for indirect illumination.


Global illumination with 200,000 photons

Global illumination with 2 million photons

As an example image, this is what happens when you don't use final gathering and instead lookup the photon map directly at each hitpoint:


This is much faster than doing final gathering,
but who wants this as their estimation of indirect illumination?

Initially I wanted to implement irradiance caching to speed up photon mapping, but in estimating the value of time saved versus image quality, I decided to go with image quality, as irradiance caching in fact does nothing for the quality of the image rendered. Although time in the end played a large role in the quality of the indirect illumination (ie it never quite reached the quality I wanted), I'm not sure that using the time to implement irradiance caching would have actually made up for the time lost.

Volumetric Scattering

If there is any sort of highlight or focus to my final render, it would have to be the volumetric scattering effects which produce the beams of light coming down from the sky. These were done using a single scattering effect, which affects light in two ways: direct light in-scattered in the volume towards the eye, and absorption of diffusely reflected radiance from a hitpoint. The in-scattering of light is what actually increases the radiance and produces the beam effect; absorption on the other hand only adds a foggy look to the scene. To actually calculate the in-scattering effect, I used ray marching and sampled the light at each point along the ray. A total of 160 steps were taken for each primary ray in rendering the beams of light for the final image.

The default Sponza model is open-air, thus there is nothing to actually break up light and create the beams. To achieve the desired effect, I modeled a simple grid-like structure made up of planes which is supposed to emulate the tiling of windows. Since the planes block out some light in some areas and let light through in others, "beams" are created. The resulting look was very pleasing.

Here is a visualization of the beams of light as they would appear in the Sponza scene, but without the architecture in the background:


A definite heavenly feeling here. Northern lights anyone?

Creating the Final Image

Putting all these techniques together is itself an artistic challenge. I initially had a different camera angle which only showed one wall of Sponza, but I decided on showing more of the architecture. One requirement was to allow full view of the beams of light, which stretched all the way from the highest point in sponza down to the floor. In addition, several images were composited to produce the final image. Global illumination was split up into two steps, namely one image rendered with just path tracing (direct illumination) and one image rendered with photon mapping (indirect illumination). Furthermore, the direct illumination from the dome light was composited with a rendering of just the beams of light over a black background as in the image above; this was done so I could separately control the intensity of the light beams. The globally illuminated image with the beams of light was then further composited with a directional light, representing the sun.

Although the direct illumination using a skydome was experimented with the Sobol, Hammersley and Halton sequences, I ultimately settled on using the Radical Inverse method in base 3 for the second dimension. In all cases VanDerCorput was used for the first dimension. For photon emission, 4 random numbers needed to be created (2 for each of the 2 uniformly sampled points on the sphere). I again used the Radical Inverse method with bases of 2, 3, 5, 7 in the 4 dimensions. Anti-aliasing was done using jittered supersampling.

All in all there are 3 light objects used in the scene: a dome light representing the sky for direct illumination, a dome light emitting photons for indirect illumination, and a directional light for representing the sun, which can be seen shining on the right wall in the final image. The final image is a composite of 4 separate images, as detailed in the paragraph above. The direct illumination was calculated with 400 shadowrays per hit and 2x2 anti-aliasing, taking about 9 hours to render. The indirect illumination was calculated with 500,000 photons and with 100 final, 8 secondary gather rays and 2x2 anti-aliasing took nearly 3 hours to complete. Finally the computation of the beams of light took about an hour at 160 shadow rays per primary ray (remember we march along the ray, shooting a shadowray at each step).

Lessons, Failures, Regrets

My biggest disappointment was the quality of the indirect illumination rendering using photonmaps. I vastly underestimated the time needed to perfect the look, as well as the visual importance to the overall "realistic" feeling of the final image. In the end, given the limited time remaining, it came down to implementing light scattering effects or trying to perfect the photon mapping. Since there are many examples of Sponza globally illuminated on the web, I decided to tackle a more unique idea in giving Sponza a dusty atmosphere and creating visible beams of light that I felt would add a nice visual effect to the scene.

Initially I also wanted to implement high dynamic range imaging due to the fact that it's a current hot subject and that it was very applicable to bright sunlit scenes. To this day I am not sure how much my final render could have benefitted from implementing this technique (although I do simple gamma adjustments and clamp-to-white methods).

My most time consuming struggle was probably dealing with Monte Carlo sampling. Whenever there is any sort of integration needed in computer graphics, Monte Carlo estimation is used to break it down into an evaluation of a finite number of samples. In the end I performed extensive testing of several different random sequences using various numbers of samples before settling on the final settings for the render. Noise patterns and general noisiness of images became my worst enemy. Although these can usually be solved by taking an exorbitant amount of samples, the render time increases significantly, especially given the fact that the variance of the Monte Carlo estimator converges at a rate of 1/sqrt(n). This means that in order to get twice the quality, you need four times as many samples. When you are looking at render times on the scale of hours, efficiency becomes of great importance. In the end, I am happy with the amount of knowledge I gained from this experience, which will undoubtedly prove to be useful in any future computer graphics projects I decide to undertake.

Acknowledgments

I would like to use this section to recognize the tremendous help I received from various people in doing this project. First off, Professor Matthias Zwicker and the TA for the class Wojciech Jarosz proved invaluable countless times whenever I was missing something conceptually or had a nasty bug in my program that I could not fix. They made themselves available for help for many hours each week, and without their knowledge and experience I would probably still be stuck on some problem as of now. Actually, knowing me I probably would have given up already, but who knows. Secondly I received much help from my dad, whose faster computer not only helped me churn out test renders, but whose graphics experience also helped me to fully understand the algorithms I was implementing. Lastly I would like to thank my friends for providing extra pairs of eyes on test renders and for pushing me to play computer games in order to take a break from this project (thanks guys for the peer pressure).

Questions? Comments?

Send me an email at jcko AT ucsd DOT edu.