This image took about 10 minutes to render on bunny_microfacet_cu_unlit.dae. It has 4 samples/pixel, 256 samples/light, and max ray depth equal to 5.
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 static_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 0: Refactoring 3-1 Code
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 return the result of calling sample_dir
with the view ray. Otherwise there is no environment light, so we return the empty spectrum as before.
If you're using your code from 3-1, you'll need to make the following change in PathTracer::est_radiance_global_illumination
. It should look something like this, depending on your variable names and implementation.
if (!bvh->intersect(r, &isect))
return envLight ? envLight->sample_dir(r) : L_out;
If you're using the staff library, these changes have already been made for you.
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.
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.
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 probability with respect to solid angle) using the sampler_uniform_sphere
. Convert this direction to coordinates , 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 .
Tips:
envMap->data
contains the pixels of the environment map.- The size of the environment texture is given by
envMap->w
andenvMap->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 .
- Sample a pixel within that row using the condition distribution .
- Convert that 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 . We will get samples from this distribution that we will map to a direction vector with spherical coordinates .
This pdf will have to satisfy the property
.
Our pdf will be piecewise constant on each rectangle of the form . We can break our integral up into a sum:
Thus, we want to satisfy the constraint . If the environment map were being pasted onto a flat texture, we might want to do something like (the symbol means "proportional to"). But since we are pasting 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 for . 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:
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 is defined as 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
and store it in the 1D array marginal_y
.
Step 3: Compute the conditional distributions
Remember that the conditional density is equal to by Bayes' rule. The cumulative conditional distribution function is equal to
Remember that marginal_y
does not store the marginal density but rather the marginal distribution , 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 is stored from conds_y+j*w
to conds_y+(j+1)*w
in memory.
Step 4: Update sample_L
to do importance sampling
- Get a uniform 2D sample on from
sampler_uniform2d
. - Use the inversion method combined with your marginal and conditional distributions plus the
std::upper_bound()
function in order to get sampled from the pdf you calculated in Step 1. - Convert these values to a direction using our helper functions and store it in
*wi
. Set the distance to light toINF_D
. - Calculate the
pdf
value to be returned by querying thepdf_envmap
at your (integer valued) point , which you will also multiply by . - Return the environment map value at
The additional multiplicative factor in step 4 comes from the switch between probability densities. Switching from the environment map's domain to the domain provides the term (the ratio of the domain's areas). Switching from spherical sampling to solid angle sampling provides the term (remember from class that ).
This Part's Writeup
Pick one .exr file to use for all subparts here. Include a converted .jpg of it in your website so we know what map you are using.
- In a few sentences, explain the ideas behind environment lighting (i.e. why we do it/how it works).
- Show the probability_debug.png file for the .exr file you are using, generated using the
save_probability_debug()
helper function after initializing your probability distributions. - Use the
bunny_unlit.dae
scene and your environment map .exr file and render two pictures, one with uniform sampling and one with importance sampling. Use 4 samples per pixel and 64 samples per light in each. Compare noise levels. - Use the
bunny_microfacet_cu_unlit.dae
and your environment map .exr file and render two pictures, one with uniform sampling and one with importance sampling. Use 4 samples per pixel and 64 samples per light in each. Compare noise levels.