# Importance Sampling path tracer, Different pdfs result in different luminance?

I'm having a lot of trouble implementing Importance Sampling path tracer.

If I understand it correctly, what I need to evaluate for a path tracer is this:
$$\int_\Omega L_{in}(L) * fr(L, V) * \cos(\Theta_L) d\omega_L$$

Which for an Importance sampling PT is estimated by sampling $$n$$ times $$L_i$$ according to some pdf $$p(L)$$, i.e.:
$$\frac{1}{M} * \sum_1^n L_{in}(L) * \frac{fr(L, V) * \cos(\Theta_L)}{p(L_i)}$$

Which in it's simplest form, for a single sample of a recursive reverse Path Tracer using an environment map, translates to roughly this pseudocode:

trace(scene, ray, throughput, depth) =
if(depth > ...)
return 0
else
intersection, normal, material = intersect(ray, scene)
if(intersection)
new_ray = material.sample_direction(normal, ray)
new_throughput = material.brdf(normal, ray, new_ray)
* cos(normal, new_ray)
/ material.pdf(normal, ray, new_ray)
return trace(scene, new_ray, new_throughut, depth + 1)
else
return throughput * environment(ray)


Usually the $$brdf * cos(theta)$$ and the $$pdf$$ would be similar. In my case for Lambertian importance sampling, they are equal.

What I think this means is that if, instead of importance sampling the material, I sample a random direction in the "$$normal$$-oriented" hemisphere, and use constant $$\frac{1}{2 * PI}$$ for the pdf, the radiance (for a sufficient number of samples) should be equal (up to some trivial variance):

trace(scene, ray, throughput, depth) =
if(depth > ...)
return 0
else
intersection, normal, material = intersect(ray, scene)
if(intersection)
new_ray = sample_uniform_hemisphere(normal, ray)
new_throughput = material.brdf(normal, ray, new_ray)
* cos(normal, new_ray)
/ (1/(2 * PI))
return trace(scene, new_ray, new_throughut, depth + 1)
else
return throughput * environment(ray)


Instead, even for trivial Lambertian materials, IF i switch the sampling pdfs in a checkerboard pattern, it shows a large difference in luminance: Switching sampling PDFs in a checkerboard pattern shows a difference in luminance

For more complex materials the problem is MUCH worse.

The actual Lambert implementation is (note the albedo is separated out):

glm::vec3 lambert::sample(glm::vec3 n, glm::vec3 v) const
{
glm::vec2 xi{drand48(), drand48()};
float th = acosf(sqrtf(xi.x));
float ph = 2 * M_PIf32 * xi.y;
glm::vec3 dir{sinf(th) * cosf(ph), sinf(th) * sinf(ph), cosf(th)};
return local_to_normal(dir, n);
}

float lambert::bxdf(glm::vec3 n, glm::vec3 v, glm::vec3 l) const
{
return M_1_PIf32;
}

float lambert::pdf(glm::vec3 n, glm::vec3 v, glm::vec3 l) const
{
return glm::dot(n, l) * M_1_PIf32;
}


The actual trace routine is:

auto trace(ray r, scene const &s)
{
float throughput = 1.0f;
for(int num_bounces = 0; num_bounces < 6; ++num_bounces) {
auto &&[i, hit] = intersect_scene(r, s);
if(!i) {
return throughput * sample_environment(r.dir, s.environment);
}
auto out_direction = hit.mat->sample(hit.x_norm, -r.dir);
throughput *= hit.mat->albedo * hit.mat->bxdf(hit.normal, -r.dir, out_direction)  * glm::dot(hit.normal, out_direction)/ hit.mat->pdf(hit.normal, -r.dir, out_direction);
r.src = hit.pos + hit.normal * 1e-4f;
r.dir = out_direction;
}
return 0.f;
}


For the uniform hemispherical sampling I use this:

    auto const r = sqrtf(1.f - u1 * u1);
auto const phi = 2 * M_PIf32 * u2;
return {r * cosf(phi), r * sinf(phi), u1};


And then change the basis to the intersection normal again.