Both of these images were rendered using 256 samples/pixel, 4 samples/light, and max ray depth equal to 7. It took about 2 minutes each.


In this part, you're going to implement the Microfacet model. Specifically, isotropic rough conductors that only reflect. You don't have to implement refractions here.

After finishing, you'll be able to render the following scenes:

  • CBbunny_microfacet_cu.dae (copper)
  • CBdragon_microfacet_au.dae (gold)
  • CBspheres_microfacet_al_ag.dae (aluminum and silver)
  • bunny_microfacet_cu.dae (copper)

Task 1: Microfacet BRDF

Implement the BRDF evaluation function MicrofacetBSDF::f() (not F() function!):

f=F(ωi)G(ωo,ωi)D(h)4(nωo)(nωi),f=\frac{F(\omega_i) * G(\omega_o, \omega_i) * D(h)}{4 * (n\cdot\omega_o) * (n\cdot\omega_i)},

where FF is the Fresnel term, GG is the shadowing-masking term, and DD is the normal distribution function (NDF). nn is the macro surface normal, which is always (0,0,1)(0,0,1) in local coordinates. hh is the half vector as before.

We have some code pre-written for these functions. So, in this step, you just have to call them correctly. You will need to modify them in the following steps.

Task 2: Normal Distribution Function (NDF)

Implement the normal distribution function (NDF) MicrofacetBSDF::D(). The pre-implemented code for this function is fake, and needs to be overwritten.

The NDF defines how the microfacets' normals are distributed. Given the light incident and outgoing directions ωi\omega_i and ωo\omega_o, we know that only those microfacets whose normals are exactly along the half vector hh are able to reflect ωi\omega_i to ωo\omega_o, because the microfacets are assumed to be perfectly specular. In other words, we need to query the NDF at hh.

The NDF can be defined in various kinds of distribution functions. Here we adopt Beckmann distribution, which is similar to a Gaussian distribution. Specifically,


where α\alpha is the roughness of the macro surface, θh\theta_h is the angle between hh and the macro surface normal nn.

For a reasonable range of α\alpha, you can usually set it between 0.0050.005 and 0.50.5. The smaller α\alpha is, the smoother the macro surface will be. In other words, the macro surface tends to be diffuse when α\alpha is large and glossy when α\alpha is small.

Task 3: Fresnel Term

Implement the Fresnel term MicrofacetBSDF::F(). The pre-implemented code for this function needs to be overwritten.

First of all, you cannot use the Fresnel term or the Schlick's approximation from previous parts! Those are for air-dielectric interfaces but here we need air-conductor. Moreover, air-dielectric Fresnel terms are not wavelength-dependent, which means that you can calculate the Fresnel term, then multiply some color with it. However, the air-conductor Fresnel term is wavelength-dependent, which means that it contains color information, thus has type Vector3D here.

Theoretically, to be 100% accurate, we need to calculate the Fresnel term for every possible wavelength, then convert the sampled spectrum to an RGB color. But that'll be too complicated. So, here we make a simplification. We calculate the Fresnel terms for R, G, B channels respectively, assuming that each channel has a fixed wavelength.

To calculate the air-conductor Fresnel term, you can use the following approximation:




where η\eta and kk are used together to represent indices of refraction for conductors. Both of them are Vector3D values, recording the scalar η\eta and kk values at wavelengths 614614 nm (red), 549549 nm (green) and 466466 nm (blue).

This website provides a collection of refraction indices for different materials at various wavelengths. You can easily query any conductor's η\eta (nn on the website) and kk values at our fixed wavelengths, and replace them in our .dae file to get your own metal. You'll find it useful for one of the deliverables of this part.

Task 4: Importance Sampling

Implement the BRDF sampling function MicrofacetBSDF::sample_f(). The pre-implemented code is cosine hemisphere sampling, which is perfect for importance sampling diffuse BRDFs, but not suitable for Beckmann distribution. Your task here is to importance sample the microfacet BRDF according to the shape of the Beckmann NDF. Thus, this function must be overwritten.

The general idea is that, you can sample θh\theta_h and ϕh\phi_h according to some pdfs pθp_\theta and pϕp_\phi respectively, then combine them to get the sampled microfacet normal hh and its pdf. Then, we can reflect the given wow_o according to the sampled normal hh to get the sampled light incident direction ωi\omega_i.

Based on the importance sampling theory, the more these pdfs pθp_\theta and pϕp_\phi resemble D(h)D(h), the less noise you will have. So, for the Beckmann NDF, we provide a good pair of pdfs pθp_\theta and pϕp_\phi:



To sample them, we use the inversion method by integrating and inversing those pdfs. This process can be difficult, so we provide the results:

θh=arctanα2ln(1r1),\theta_h=\arctan \sqrt{-\alpha^2\ln(1-r_1)},

ϕh=2πr2,\phi_h=2\pi r_2,

where r1r_1 and r2r_2 are two random numbers uniformly distributed within [0,1)[0, 1).

After getting θ\theta and ϕ\phi, you immediately have the sampled microfacet normal hh. Reflecting ωo\omega_o according to hh gives you the sampled ωi\omega_i.

Now, the tricky part: what is the pdf of sampling ωi\omega_i w.r.t. solid angle? There are two steps to get it.

Since we originally had the pdfs of sampling hh w.r.t. θ\theta and ϕ\phi, we first calculate the pdf of sampling hh w.r.t. solid angle. This is given by

pω(h)=pθ(θh)pϕ(ϕh)sin(θh).p_\omega(h)=\frac{p_\theta(\theta_h)\cdot p_\phi(\phi_h)}{\sin(\theta_h)}.

Then, we calculate the final pdf of sampling ωi\omega_i w.r.t. solid angle as:

pω(ωi)=pω(h)4(ωih),p_\omega(\omega_i)=\frac{p_\omega(h)}{4 (\omega_i\cdot h)},

This website provides a very good derivation of these pdfs.

Some implementation notes:

  • You should fill in the sampled direction *wi and the corresponding *pdf, and return the sampled BRDF value same as before (not just the NDF value!).
  • You can use sampler.get_sample() to get two uniform random numbers within [0,1)[0, 1).
  • Although the cosine hemisphere sampling method provided by default here is inefficient, it is still correct! It'll take a long time to converge to a noise-free result, but you can use it to verify your importance sampling.
  • Check if ωi\omega_i and ωo\omega_o are valid (both nωin \cdot \omega_i and nωon \cdot \omega_o should be greater than zero) at the beginning of the BRDF evaluation function MicrofacetBSDF::f(). If not, return zero.
  • Check if your sampled ωi\omega_i is valid. If not, return zero pdf and zero BRDF.
  • If you have any black spots or regions, check for numerical errors! Is your pdf zero or negative? Are you dividing by zero such that you get NaNs or Infs? Do you have negative values under a square root?

This Part's Writeup

  • Show a sequence of 4 images of scene CBdragon_microfacet_au.dae rendered with α\alpha set to 0.005, 0.05, 0.25 and 0.5. The other settings should be at least 128 samples per pixel and 1 samples per light. The number of bounces should be at least 5. Describe the differences between different images. Note that, to change the α\alpha, just open the .dae file and search for microfacet.
  • Show two images of scene CBbunny_microfacet_cu.dae rendered using cosine hemisphere sampling (default) and your importance sampling. The sampling rate should be fixed at 64 samples per pixel and 1 samples per light. The number of bounces should be at least 5. Briefly discuss their difference.
  • Show at least one image with some other conductor material, replacing eta and k. Note that you should look up values for real data rather than modifying them arbitrarily. Tell us what kind of material your parameters correspond to