# Multiple Importance Sampling with more than one light

(Note: This has been cross posted from my ompf2 post.)

Recently I've implemented Multiple Importance Sampling for the sampling of surfaces in my ray tracer. I do this by, on each intersection, sampling both a random direction from the BRDF and also sampling a random light, then combining both using the power heuristic.

When I have only a single light, the results are beautiful. Whereas previously I had this:

Now I have this, with the same amount (1024 per pixel) of samples:

So the algorithm seems to work well. The problem is when I add a second light source to the scene, the noise comes back! This is a render with a second light source added:

I am probably using the technique incorrectly. Perhaps I need to sample all light sources at once, instead of randomly picking one like I'm doing now? That doesn't seem like it would scale well as the number of lights go up, however. The implementation was loosely based on pbrt's DirectLightingIntegrator, and I suspect the reason why it doesn't work properly is that I also have multi-bounce indirect lighting. This is my core integration function:

vec3 calc_light_incidence(const Scene& scene, Rng& rng, const Ray& ray, int depth) {
float ray_weight = 1.0f;

depth += 1;
if (depth > 2) {
const float live_probability = 0.75f;

if (rng.canonical() > live_probability) {
return vec3_0;
} else {
ray_weight = 1.0f / live_probability;
}
}

vec3 color = vec3_0;

const Optional<Intersection> surface_hit = find_nearest_intersection(scene, ray);
if (surface_hit) {
const vec3 out_dir = -normalized(ray.direction);

const size_t light_index = (size_t)(rng.canonical() * scene.lights.size());
const SceneObject* light = &scene.objects[scene.lights[light_index]];
const float light_weight = (float)scene.lights.size();

// Sample BRDF
{
const vec3 in_dir = cosine_weighted_point_on_hemisphere(rng.canonical(), rng.canonical(), surface_hit->normal);
const vec3 reflectance = surface_hit->object->material.diffuse_brdf(*surface_hit, in_dir, out_dir);
const float cos_term = dot(in_dir, surface_hit->normal);

const float brdf_pdf = cos_term / pi;

const Optional<Intersection> light_hit = light->shape->intersect(ray_from_surface(*surface_hit, in_dir));
const float light_pdf = (light_hit ? light->shape->areaPdf(surface_hit->position, in_dir) : 0.f);

if (brdf_pdf != 0.f && reflectance != vec3_0) {
const vec3 illuminance = calc_light_incidence(scene, rng, ray_from_surface(*surface_hit, in_dir), depth);
color += (1.0f / brdf_pdf) * cos_term * reflectance * illuminance * power_heuristic(brdf_pdf, light_pdf);
}
}

// Sample lights
{
const ShapeSample light_sample = light->shape->sampleArea(rng, surface_hit->position);
const vec3 light_vec = light_sample.point - surface_hit->position;
const vec3 in_dir = normalized(light_vec);
const float cos_term = vmax(0.f, dot(in_dir, surface_hit->normal));

const Optional<Intersection> light_hit = find_nearest_intersection(scene, ray_from_surface(*surface_hit, light_vec));
bool occluded = !light_hit || light_hit->object != light;
const float light_pdf = light_weight * light_sample.pdf;

const float brdf_pdf = cos_term / pi;

const vec3 reflectance = surface_hit->object->material.diffuse_brdf(*surface_hit, in_dir, out_dir);
if (!occluded && light_pdf != 0.f && reflectance != vec3_0) {
const vec3 illuminance = calc_light_incidence(scene, rng, ray_from_surface(*surface_hit, in_dir), depth);
const float differential_area = -dot(light_hit->normal, in_dir) / length_sqr(light_vec);
color += light_weight * (1.0f / light_pdf) * differential_area * cos_term * reflectance * illuminance * power_heuristic(light_pdf, brdf_pdf);
}
}

if (dot(out_dir, surface_hit->normal) >= 0.f) {
color += surface_hit->object->material.emmision->getValue(*surface_hit);
}
} else {
//color = lerp(mvec3(1.0f, 0.2f, 0.0f), mvec3(0.35f, 0.9f, 1.0f), 1.0f - std::pow(1.0f - vmax(0.0f, dot(ray.direction, vec3_y)), 2)) * 0.5f;
color = lerp(mvec3(0.02f, 0.06f, 0.36f), mvec3(0.0f, 0.0f, 0.0f), 1.0f - std::pow(1.0f - vmax(0.0f, dot(ray.direction, vec3_y)), 2)) * 0.5f;
}

return color * ray_weight;
}

• Do you get a similarly smooth image from only the second light? i.e. it's definitely an artifact of multiple lights, not just the geometry of the second light? Aug 12 '15 at 3:07
• Yes, using any kind of single light gives a smooth image. The noise only comes back when there are multiple lights to choose from. (And I suppose that the 3rd example image is somewhat misleading, but when there are 2 lights, the image looks almost as bad as the first one which uses the naive algorithm if I use only 1024 spp.) Aug 12 '15 at 3:36
• To me, that suggests a bug. But it's rather difficult to debug code just by reading it. I'd try disabling pieces of code—for example, in this scene the importance weight of the BRDF sample should be extremely small since light sampling is far more likely to hit the light, so you should be able to set brdf_pdf to 0 and only use light sampling and see if the noise goes away. Aug 12 '15 at 3:39
• @JohnCalsbeek Looks like you gave me a good lead! This is how it looks like if I ignore BRDF sampling: i.imgur.com/4c7bRCo.png I'll try to further track down the issue later when I have more time. It seems like the issue is that the impact of the BRDF is overestimated, causing it to overwhelm the light contribution in some cases. Aug 12 '15 at 5:54
• You should actually add the significant parts of your code into the question instead of linking them elsewhere. Aug 12 '15 at 9:26