# PA5: Sampling lights

Use the Samplers and Integrators interfaces from the previous assignment to build powerful new rendering techniques that rendering with much less noise.

This assignment consists of three parts. In the first, you will implement sampling routines for different geometries, and visualize the resulting points. Then you will modify the existing shapes in darts to allow sampling them and evaluate their PDFs. Finally, you will implement new integrators that make use of the ability to sample emissive geometry in the scene.

## Task 1: Sampling Geometry

In this part, you will implement (in `include/`

) a few new functions for sampling basic geometry, which will make the later parts of the assignment easier. Similar to last time, you should write a standalone program to generate and save a few hundred points from your functions, and visualize them using your favorite plotting tool (we will show screenshots from plot.ly). You can reuse the same program you used in the previous assignment.

### Triangles

Implement a function `sample_triangle()`

that produces points uniformly at random on a triangle with the specified coordinates. There are different ways you can do this; the easiest way is to first generate random barycentric coordinates (alpha, beta, gamma), and then return the corresponding interpolated point `v0*alpha + v1*beta + v2*gamma`

.

Also implement the corresponding `sample_triangle_pdf()`

function.

Visualize points from your function for a triangle with corners (0,0,0), (1,1,1) and (0.5, 1, 2). You should get points like the ones shown in the image here.

### Spherical Cap

Now implement the function `sample_sphere_cap()`

to sample points uniformly at random from a spherical cap. You've already (implicitly) implemented this method for your previous assignment: Sampling the sphere and the hemisphere are just special cases of sampling different sections of the sphere. Copy-paste the code for sampling the hemisphere from your previous assignment; all that needs to change is the computation of `cos_theta`

: It should be distributed uniformly between 1 and `cos_theta_max`

; this is the cosine of the largest angle that points are allowed to have with the normal. In pseudo code, `cos_theta = lerp(cos_theta_max, 1.0f, <uniform random number>)`

.

Also implement the corresponding `sample_sphere_cap_pdf()`

function.

Visualize your code for an angle of pi/4 (Hint: You pass the cosine of the angle to this function, not the angle directly). It should look something like the image shown here.

## Task 2: Making Lights Sampleable

In this task, you will extend the surface base class in darts with a new sampling interface. Open `surface.h`

. You should see a new struct `EmitterRecord`

, and three new methods: `Surface::sample()`

, `Surface::pdf()`

, and `Surface::is_emissive()`

(we'll discuss `Surface::sample_child()`

and `Surface::child_prob()`

further down). Read their documentation.

`Surface::sample()`

will generate a random *direction* that points from `rec.o`

towards the surface. The direction is guaranteed to hit the surface from `rec.o`

. Usually, you will do this by first generating a point on the surface (using the functions from Task 1) and then computing the direction from `rec.o`

to the point. However, for certain shapes (like the sphere), we will sample directions directly.

`Surface::pdf()`

will return the probability density of generating a certain direction `v`

, seen from point `o`

. Usually, you would implement this by tracing a ray from `o`

in direction `v`

and checking if it hits the surface (and returning 0 if not - we never generate directions not towards the surface); the PDF is then the PDF of generating the hitpoint (usually 1/surface area) times the geometry factor (squared distance to `o`

divided by the cosine of `v`

with the surface normal at the hit point).

Similar to last assignment, we provide a new set of tests for your sampling code. The implementation is available in `src/tests/surface_sample_test.cpp`

. Add it to your project by modifying the `CMakeLists.txt`

file like you did in the previous assignment.

### Rectangles

To help you get started with sampling surfaces, we provide working implementations of `Quad::sample()`

and `Quad::pdf()`

in `quad.cpp`

.

`Quad::sample()`

, generates a random point on the rectangle, stores it in `rec.hit.p`

, and computes the normalized direction from `rec.o`

to this point, along with the PDF and other members of the `EmitterRecord`

. It then returns the emitted color of that surface point divided by the PDF.

`Quad::pdf()`

, checks if the given direction hits the Quad from the given origin `o`

. If not, it returns 0; otherwise, it returns 1/area times the geometry factor.

You can test this code using the sample tester tool by running:

darts scenes/assignment5/test_surfaces.json

You should get images like these for `quad-pdf.png`

and `quad-sampled.png`

:

### Triangles

Now add and implement `Triangle::sample()`

and `Triangle::pdf()`

(we've already implemented `Triangle::is_emissive()`

for you). Use exactly the same recipe as you did for the quad - all that needs to change is how to get a point on the surface, and the computation of the surface area. Look at `Triangle::`

for inspiration for how to get the three corners of the triangle (`p0`

, `p1`

and `p2`

).

### Spheres

Open `sphere.h`

and `sphere.cpp`

, and add and implement `Sphere::sample()`

and `Sphere::pdf()`

(we already provide you `XformedSurfaceWithMaterial::is_emissive()`

, which `Sphere`

inherits).

We will use a different approach for sampling the sphere: Instead of sampling a point on the sphere first and computing the direction towards it, we will directly sample the cone of directions that all point towards the sphere. To do this, you can use your function for sampling spherical caps from the first task. However, to use this function you need to first figure out `cos_theta_max`

, the angular extent of the sphere as seen from `rec.o`

. You can compute it with `cos_theta_max = sqrt(d*d - r*r)/d`

where `d`

is the distance from the center of sphere to `rec.o`

, and `r`

is the radius of the sphere. Hint: Can this formula fail? When does this happen? What should the value of `cos_theta_max`

be in that case? Note that the sphere may be transformed by a `Transform`

, so `r`

may not simply be `Sphere::`

. You may assume that the `Transform`

contains only isotropic scaling and rotation. How can you compute the resulting radius in that case?

The sampling function you implemented in Task 1 assumes the spherical cap is aligned with the z axis. You should use your `ONB`

class to transform the sampled direction, so that the spherical cap points from `rec.o`

towards the center of the sphere instead.

For `Sphere::pdf()`

, you should first check if the ray with the given origin/direction intersects with the sphere. If it does, the PDF is simply 1/solid angle of the spherical cap (no geometry factor needed - we generated directions directly!). The solid angle of a spherical cap with opening angle of `theta_max`

is `2*pi*(1-cos(theta_max))`

.

Test your code with the sample tester tool by running the scene `scenes/assignment5/test_surfaces.json`

again. You should get images like these:

### Surface Groups

Open `surface_`

and `surface_`

and implement the sampling interface for the `SurfaceGroup`

class. This class represents a collection of shapes (e.g. the list of all lights in the scene), which are stored in the array `m_surfaces`

.

In your implementation of `SurfaceGroup::sample()`

, we should first pick one of the children uniformly at random, and then call that child surface's `sample()`

function. We already give you functions that provide some of this functionality. Read the documentation for the functions `Surface::sample_child()`

and `Surface::child_prob()`

in `surface.h`

. For a single surface, these functions default to return the surface itself. We also provide a specialization of this function for `SurfaceGroup`

. Look at its implementation in `surface_`

. This function selects one of the child surfaces at random, and returns a pointer to it, along with the probability it was chosen. You can call this function in your implementation of `SurfaceGroup::sample()`

. Since you are only returning the color of one of the children with some probability, you will need to adjust the color returned by the child's `Surface::sample()`

function. Also, before returning, make sure that the `EmitterRecord::pdf`

data member accounts for both the probability of selecting a particular light (returned by `SurfaceGroup::sample_child()`

), and the probability density within that light (returned by `Surface::sample()`

).

For `pdf()`

, the Shirley book suggests calling the `pdf()`

method of each surface in the list and returning the average of all the PDF evaluations. We already provide you an implementation of `SurfaceGroup::pdf()`

in `surface_`

. However, while this will work, it requires us to iterate over all emitters in the scene, which takes time. This will work fine for a few emitters, but once you turn an entire `Mesh`

into an emitter, it will be a significant bottleneck.

To make this faster (and O(1) complexity!) we will not use `SurfaceGroup::pdf()`

at all. When needed, we will instead compute the effective PDF as the product of the child selection probability (`SurfaceGroup::child_prob()`

) and the pdf returned by the selected child's `Surface::pdf()`

function.

Test your code with the sample tester tool by running the scene `scenes/assignment5/test_surfaces.json`

again. You should get images like these:

## Task 3: Integrating Lights

In this task, we will implement better integrators for direct lighting. You'll use the sampling interfaces you implemented in the earlier task to explicitly guide rays towards light sources.

Note that the algorithm described in this assignment is slightly different from the pseudo-code shown in class: Both algorithms will work, and you are free to implement either version, as long as it gives you the correct image.

We will begin by focusing on direct lighting, and then slowly extend our integrators to account for global illumination.

### Direct Lighting Material Integrator

First make sure your `PathTracerMats`

integrator from the last assignment can successfully render `scenes/assignment4/veach_mats.json`

to produce a direct illumination image like the one shown here when ‘"max bounces"’ is set to 1.

Most of this image looks extremely noisy, especially the background and the reflections in the lower right. We can do better than this!

### Creating a list of scene emitters

Instead of asking the `Material`

to generate ray directions for us, we could instead generate ray directions by sampling points on all surfaces in the scene using our new `Surface::sample()`

function. As it is now, however, this would not be so useful, since most surfaces in the scene do not emit light. We'd like to sample rays towards surfaces that emit light. To do that we first need to add some plumbing to our code to allow a `Scene`

to maintain a list of only the emissive surfaces.

First, add a `SurfaceGroup m_emitters;`

data member to the `Scene`

class. Then, take a look at `Scene::`

. Currently it just calls `m_surfaces->add_child()`

. After doing so, check whether the surface is emissive (call the `is_emissive()`

method on `surface`

) and if it is, also add `surface`

to `m_emitters`

by calling its `add_child`

method.

### Next Event Integrator

Add a new integrator class called `PathTracerNEE`

, and register it with the factory using the string `"path tracer nee"`

. The "NEE" part stands for *Next Event Estimation*, which is a fancy name for sampling light sources directly. We will start by implementing direct lighting with no recursion first.

On a basic level, this integrator looks very similar to the material-based integrator from the previous subtask (and you can start by copy-pasting that code and removing the recursion). However, instead of calling `Material::sample()`

to produce a direction, it should call `scene.emitters().sample()`

. `scene.emitters()`

is a `SurfaceGroup`

that contains all emissive shapes in the scene. Similarly, instead of dividing by `Material::pdf()`

, you should divide by the `EmitterRecord::pdf`

field populated by `scene.emitters().sample()`

.

This (for the first time) shows the flexibility of the Monte Carlo approach: All you had to change was the sampling and PDF routine, and you get a noise-free image much faster! This would not have been possible with the renderer you wrote in Assignment 1. Convince yourself that this integrator has much less noise by running your new code on `scenes/assignment5/veach_nee.json`

. You should get an image looking like the one shown here.

This looks *much* better than the `PathTracerMats`

integrator for most of the image... except for the upper left, where the NEE estimator does much worse. Just like discussed in class, we'll fix this by combining both estimators with MIS in the next section. Also beware that this new estimator will not (yet) work correctly if you set `"max bounces"`

> 1. We'll handle that further in this assignment.

#### Additional verification

But before we do, we've designed a few more scenes to help you debug and verify your implementation.

If the Veach scene isn't rendering correctly, it can be difficult to isolate the issue because it tests multiple features simultaneously. We provide a simpler set of scenes to help you debug your sphere lights and integrators. They are `scenes/assignment5/sphere_light_[small|medium|large]_XXX.json`

. These scenes rely on the principle that two fully visible sphere lights will produce identical illumination on a diffuse surface even if their radii are different, as long as their power is the same.

The Veach scene uses sphere lights, but let's also test triangle lights. Render the `scenes/assignment5/odyssey_triangle_mats.json`

and `scenes/assignment5/odyssey_triangle_nee.json`

and scenes. This scene is identical to the one from the previous assignment, but the rectangular light source is composed of two `Triangle`

s instead of a single `Quad`

. Compare these triangle versions to the quad versions in `scenes/assignment4/odyssey_mats.json`

and `scenes/assignment5/odyssey_nee.json`

.

### MIS Integrator

Add a new integrator class called `PathTracerMIS`

, and register it with the factory using the string `"path tracer mis"`

. The MIS part stands for *Multiple Importance Sampling*, which is a technique for efficiently combining multiple integration techniques.

Begin by copy-pasting either your `PathTracerMats`

or your `PathTracerNEE`

integrator from before. Instead of always sampling from the material, or always from the light source, you should sample from a *mixture* of both of them. With probability of 0.5, generate a direction by sampling the material; otherwise, generate a direction by sampling the lights. After you've generated a direction, trace a ray and evaluate the emission and material like before; however, instead of dividing by just the material pdf or the light PDF, divide by the *average* of the two PDFs (since you randomly sample from either one).

To compute the average PDF you will need to evaluate both PDFs for either sample you generate. `Material::pdf()`

allows you to evaluate the material sampling PDF for any direction. For a sample generated with `SurfaceGroup::sample()`

, we also get its PDF in the `EmitterRecord::pdf`

field. The potential remaining tricky part is evaluating the emitter PDF for the ray sampled by the material. If you are an undergraduate, you can use our provided `SurfaceGroup::pdf()`

function. Graduate students (and undergraduates for extra credit) should avoid this function. Instead, you can compute this by noting that our `SurfaceGroup::sample()`

procedure works in two steps: first sample a child surface, and then sample a point on the child surface. The probability of the first we can obtain by calling `scene.emitters()->child_prob()`

. Once we hit a surface with our material-sampled ray, we can call its `Surface::pdf()`

function to account for the probability density of the second step.

Test your integrator on `scenes/assignment5/veach_mis.json`

and compare to the NEE and material sampling variants. You should get images like those below:

The integrator now works robustly in all parts of the scene, and combines the good traits of both material sampling and light sampling.

Also test your integrators on `scenes/assignment5/odyssey_triangle_XXX.json`

. Here is our comparison:

### Full Path Tracing

Now extend the `PathTracerMIS`

integrator from the previous subtask to do not just direct lighting, but all direct and indirect lighting. Your code currently finds the closest hit point along the given ray, estimates direct lighting at that location and exits. All you need to do in order to support indirect light is to add recursion before exiting: After computing direct lighting, sample the material to obtain the next ray along the path, and call the function recursively (or in a loop) with the new ray to obtain an estimate of indirect lighting. Divide that estimate by the material sampling PDF, and add this estimate to the direct lighting estimate. That's it!

**Optional**: You could complete your `PathTracerNEE`

integrator to compute full global illumination in exactly the same way. Just generate a recursive ray by sampling the material in addition to the (non-recursive) shadow ray towards the lights.

**Grad Students: Optimize your PathTracerMIS integrator**. In the current version, you sample the material and trace a ray once for direct lighting, and a second time for the recursive ray. This is wasteful - they can both use the same ray and intersection (intersecting the scene is expensive!). Optimize your implementation to reuse the material ray and intersection for the recursive call. You can check the lecture slides for ideas on how to do this.

Test your new integrators on `scenes/assignment5/jensen_box_XXX.json`

.

## Task 4: Interesting scene

Now create an interesting scene (or scenes) showcasing the features you've implemented. Be creative.

## What to submit

In your report, make sure to include:

- Visualized point sets for all distributions from Task 1
- Rendered images of all the scenes in
`scenes/assignment5`

and your interesting scene

Then submit according to the instructions in the Submitting on Canvas section of Getting started guide.