This is an archive of a past semester of this course. Go to the current semester.
Project 3-2, Part 3: Environment Map Lights

This image took about 10 minutes to render (using importance sampling). It has 4 samples/pixel, 256 samples/light, and max ray depth equal to 5.

In this task you will implement a new type of light source: an infinite environment light. An environment light is a light that supplies incident radiance from all directions on the sphere. The source is thought to be "infinitely far away" and is representative of realistic lighting environments in the real world. As a result, rendering using environment lighting can be quite striking.

The intensity of incoming light from each direction is defined by a texture map parameterized by phi and theta, as shown below.

You'll be implementing EnvironmentLight::sample_L() method in static_scene/environment_light.cpp. You'll start with uniform direction sampling to get things working and then move to a more advanced implementation that uses importance sampling to significantly reduce variance in rendered images.

We recommend testing using the unlit bunny pedestal scenes to better see the effect of your environment map on the illumination. These are bunny_unlit.dae and bunny_microfacet_cu_unlit.dae. Don't test on Cornell box scenes since the object will not be exposed to the environment!

Task 1: EnvironmentLight::sample_dir()

Fill in this function. Use the helpers dir_to_theta_phi and theta_phi_to_xy to get coordinates that can be used to index into the envMap variable, then use bilinear interpolation to sample a Spectrum from envMap at that location.

In est_radiance_global_illumination, if no intersection with the scene occurs and there is an environment map, we want to then render from the environment map. If envLight is non-null, we then return the result of calling sample_dir with the view ray, otherwise we return the empty spectrum as before.

In at_least_one_bounce_radiance, we also want to make a similar modification that if the sampled ray does not intersect with the scene and there is an envLight, we use the result of sample_dir as the sampled radiance, applying the proper multiplicative factors as we did before.

After this, you should see an environment map in the background of your scene when you use the -e command line flag to load an environment map, as in:

./pathtracer -t 8 -e ../exr/grace.exr ../dae/sky/bunny_unlit.dae

High dynamic range environment maps can be large files, we have not included them in the starter code repo. You can download a set of environment maps here.

Task 2: Uniform sampling

To get things working, your first implementation of EnvironmentLight::sample_L() will be quite simple. You should generate a random direction on the sphere (with uniform $\frac{1}{4\pi}$ probability with respect to solid angle) using the sampler_uniform_sphere, convert this direction to coordinates $(\phi, \theta)$ and then look up the appropriate radiance value in the texture map using bilinear interpolation. Don't forget to set the distance to the light to INF_D and the pdf to $\frac{1}{4\pi}$.

Tips:

  • envMap->data contains the pixels of the environment map.
  • The size of the environment texture is given by envMap->w and envMap->h.
  • We have provided a few helper functions such as dir_to_theta_phi() that will be useful here.

Task 3: Importance sampling

Much like light in the real world, most of the energy provided by an environment light source is concentrated in the directions toward bright light sources. Therefore, it makes sense to bias selection of sampled directions towards the directions for which incoming radiance is the greatest. In this task you will implement an importance sampling scheme for environment lights. For environment lights with large variation in incoming light intensities, good importance sampling will significantly decrease the noise of your renderings.

The basic idea is that you will assign a probability to each pixel in the environment map based on the total flux passing through the solid angle it represents. This will be our strategy for sampling the environment map based distribution:

  • Sample a row of the environment map using the marginal distribution $p(y)$.
  • Sample a pixel within that row using the condition distribution $p(x|y)$.
  • Convert that $(x,y)$ to a direction vector and return the appropriate radiance and pdf values.

You will be able to use the probability_debug.png image that is saved out by save_probability_debug() to debug the correctness of your marginal and conditional distributions. For example, you should get something like this when loading the field.exr map after implementing steps 2-3 (we provide you with step 1 to compute the pdf):

Step 1: Compute the pdf

As a free gift, we provide most of this part of the code to you in EnvironmentLight::init().

In order to compute the pdf, we need to convert the environment map into a 2D probability density function on the domain $[0,w]\times[0,h]$. We will get samples $(i,j)$ from this distribution that we will be mapping to a direction vector with spherical coordinates $(\theta,\phi) = (\pi \frac j h, 2\pi \frac i w)$.

This pdf will have to satisfy the property

$ \int_0^w \int_0^h p(x,y) dx dy = 1. $

Our pdf will be piecewise constant on each rectangle of the form $[i,i+1] \times [j,j+1]$. So we can break our integral up into a sum:

$ \int_0^w \int_0^h p(x,y) dx dy = \sum_{i=0}^{w-1} \sum_{j=0}^{h-1}p(i,j) \int_{j}^{j+1} \int_{i }^{i+1} dx dy = \sum_{i=0}^{w-1} \sum_{j=0}^{h-1} p(i,j).$

Thus we want to satisfy the constraint $ \sum_{i=0}^{w-1} \sum_{j=0}^{h-1} p(i/w,j/h) = 1$. If the environment map $E$ were being pasted onto a flat texture, we might want to do something like $p(i,j) \propto E[i,j]$ (the $\propto$ symbol means "proportional to"). But since we are pasting $E$ onto a sphere, we want to create a density such that a uniform environment map would result in uniform sphere sampling. This means we need to take

$ p(i,j) \propto E[i,j] \sin \theta $

for the $\theta=\pi \frac j h$. This will give us a base case of uniform spherical sampling.

Our three constraints (piecewise constant, sums to 1, proportional to sine-weighted environment map) combine to give us a pdf:

$ p(x,y) = \frac{E[\lfloor x \rfloor,\lfloor y \rfloor] \sin (\pi \lfloor y \rfloor/h)}{\sum_{i',j'} E[i',j'] \sin (\pi j'/h)}.$

Most of the code for this part has already been provided. The variable pdf_envmap in the function EnvironmentLight::init() given holds the weights for each pixel -- you must first normalize pdf_envmap so that it sums to 1 as desired. As usual, we use typical row-major 2D indexing where the 2D index [i,j] corresponds to 1D index [j*w+i].

Step 2: Compute the marginal distribution

Your task starts from here: step 2.

Remember that the marginal density $p(y)$ is defined as $ p(y) = \int_0^w p(x,y) dx = \sum_{i=0}^{w-1} p(i,\lfloor y \rfloor).$ This density will be piecewise constant. In order to sample from the marginal distribution, we actually want its cumulative distribution function, which we will store as an array. In this step you should calculate the cumulative marginal distribution

$ F(j) = \sum_{j'=0}^j \sum_{i=0}^{w-1} p(i,j') $

and store it in the 1D array marginal_y.

Step 3: Compute the conditional distributions

Remember that the conditional density $p(x|y)$ is equal to $\frac{p(x,y)}{p(y)}$ by Bayes' rule. The cumulative conditional distribution function is equal to

$ F(i|j) = P(x < i | y = j) = \int_0^i p(x|y=j) dx = \int_0^i \frac{p(x,j)}{p(j)} dx = \sum_{i'=0}^{i-1} \frac{p(i',j)}{p(j)} .$

Remember that marginal_y does not store the marginal density $p(j)$ but rather the marginal distribution $F(j)$, so you can't directly use it in the denominator for your calculations here. However, you probably can structure your loops for the first three steps in a way that reduces computation a bit. (Don't worry too much about it, it's a trivial amount of computation compared to tracing rays!)

Store the conditional distributions in row major form in the array conds_y, so that the distribution for $F(\cdot | j)$ is stored from conds_y+j*w to conds_y+(j+1)*w in memory.

Step 4: Update sample_L to do importance sampling

  1. Get a uniform 2D sample on $[0,1]^2$ from sampler_uniform2d.
  2. Use the inversion method combined with your marginal and conditional distributions plus the std::upper_bound() function in order to get $(x,y)$ sampled from the pdf you calculated in Step 1.
  3. Convert these $(x,y)$ values to a direction using our helper functions and store it in *wi. Set the distance to light to INF_D.
  4. Calculate the pdf value to be returned by querying the pdf_envmap at your (integer valued) point $(x,y)$, which you will also multiply by $\frac{wh}{2\pi^2 \sin \theta}$.
  5. Return the environment map value at $(x, y)$

The additional multiplicative factor in step 4 comes from the switch between probability densities. Switching from the environment map's domain $[0,w]\times [0,h]$ to the $(\theta,\phi)$ domain $[0,\pi]\times [0,2\pi]$ provides the $\frac{wh}{2\pi^2}$ term (the ratio of the domain's areas). Switching from spherical $(\theta,\phi)$ sampling to solid angle $d\omega$ sampling provides the $\frac{1}{\sin\theta}$ term (remember from class that $\sin \theta d\theta d\phi = d\omega$).