This image took about 15 minutes to render on bunny_microfacet_cu_unlit.dae. It has 4 samples/pixel, 256 samples/light, and max ray depth equal to 7. The command takes the form ./pathtracer -s 256 -l 4 -m 7 -e ../exr/field.exr ../dae/sky/bunny_microfacet_cu_unlit.dae

Introduction

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 with 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 the EnvironmentLight::sample_L() method in scene/environment_light.cpp. Start with uniform direction sampling to get things working, 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 won't be exposed to the environment.

Also, neither the uniform hemisphere sampling from Project 3-1's PathTracer::estimate_direct_lighting_hemisphere nor the delta BSDFs from Part 1 will work, as environment lights are like point lights in that they do not have a primitive in our scene and cannot be intersected by a ray.

Your image may have a couple white specks on it like the reference image above. If you're using the staff library you can try switching to your own Project 3-1 code and see if the problem persists, or implement some sort of filtering on the incoming spectrums. Neither of these is required for this part of the project, and should not affect scenes without environment lighting.

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 Vector3D representing the spectrum from envMap at that location.

How to get .exr files: High dynamic range environment maps can be large files, so we have not included them in the starter code repo. You can download a set of environment maps here, create a exr folder at the repo root, and place them into the exr folder (for example: <repo>/exr/grace.exr).

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

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 14π\frac{1}{4\pi} probability with respect to solid angle) using the sampler_uniform_sphere. Convert this direction to coordinates (ϕ,θ)(\phi, \theta), 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 14π\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)p(y).
  • Sample a pixel within that row using the condition distribution p(xy)p(x|y).
  • Convert that (x,y)(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

We have 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]×[0,h][0,w]\times[0,h]. We will get samples (i,j)(i,j) from this distribution that we will map to a direction vector with spherical coordinates (θ,ϕ)=(πjh,2πiw)(\theta,\phi) = (\pi \frac j h, 2\pi \frac i w).

This pdf will have to satisfy the property

0w0hp(x,y)dxdy=1\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]×[j,j+1][i,i+1] \times [j,j+1]. We can break our integral up into a sum:

0w0hp(x,y)dxdy=i=0w1j=0h1p(i,j)jj+1ii+1dxdy=i=0w1j=0h1p(i,j).\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 i=0w1j=0h1p(i/w,j/h)=1\sum_{i=0}^{w-1} \sum_{j=0}^{h-1} p(i/w,j/h) = 1. If the environment map EE were being pasted onto a flat texture, we might want to do something like p(i,j)E[i,j]p(i,j) \propto E[i,j] (the \propto symbol means "proportional to"). But since we are pasting EE 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)E[i,j]sinθp(i,j) \propto E[i,j] \sin \theta for θ=πjh\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)=E[x,y]sin(πy/h)i,jE[i,j]sin(πj/h).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)p(y) is defined as p(y)=0wp(x,y)dx=i=0w1p(i,y).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)=j=0ji=0w1p(i,j)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(xy)p(x|y) is equal to p(x,y)p(y)\frac{p(x,y)}{p(y)} by Bayes' rule. The cumulative conditional distribution function is equal to

F(ij)=P(x<iy=j)=0ip(xy=j)dx=0ip(x,j)p(j)dx=i=0i1p(i,j)p(j).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)p(j) but rather the marginal distribution F(j)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(j)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[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)(x,y) sampled from the pdf you calculated in Step 1.
  3. Convert these (x,y)(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)(x,y), which you will also multiply by wh2π2sinθ\frac{wh}{2\pi^2 \sin \theta}.
  5. Return the environment map value at (x,y)(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]×[0,h][0,w]\times [0,h] to the (θ,ϕ)(\theta,\phi) domain [0,π]×[0,2π][0,\pi]\times [0,2\pi] provides the wh2π2\frac{wh}{2\pi^2} term (the ratio of the domain's areas). Switching from spherical (θ,ϕ)(\theta,\phi) sampling to solid angle dωd\omega sampling provides the 1sinθ\frac{1}{\sin\theta} term (remember from class that sinθdθdϕ=dω\sin \theta d\theta d\phi = d\omega).