Integrating BRDF using Importance Sampling

I read some articles about BRDF integration with and without importance sampling and I don't understand one thing in equations.

• If we integrate a BRDF over hemisphere with uniform sample direction distribution, we evaluate BRDF at all generated directions, sum all values, divide the sum by N (number of samples) and then multiply the result by 2π, because we need to multiply the result by integration area, and 2π is solid angle of hemisphere
• If we integrate a BRDF over hemisphere using importance sampling, we generate sample directions from inverted CDF (Cumulative Distribution Function), evaluate BRDF at these samples dividing each value by PDF (Probability Distribution Function) of a corresponding sample, then sum values and divide by N and that's it, no 2π multiplication

I confirmed it numerically integrating Cook-Torrance BRDF and simple Lambertian cosine BRDF, but I don't get why 2π multiplication disappears when we use importance sampling from math perspective?

Below is example with just cosine function integration. In importance sampling cosine function is also used as its own PDF. Also is my integration code for both cases correct?

float f(Vec3f sampleDir) { return sampleDir.z; }
float pdf(Vec3f sampleDir) { return sampleDir.z; } // cosine function is also its PDF

void uniformIntegrationCos()
{
double sum = 0.0;
const int N = 1000;
for (int i = 0; i < N; ++i)
{
Vec3f sampleDir = randomHemisphere(i, N); // fibonacci hemisphere spiral

float value = f(sampleDir);
float weight = sampleDir.z; // NdotL == pdf(sample)
sum += value * weight / PI / N; // divide by PI to normalize cosine-weighted PDF

}
const float HEMISPHERE_SOLID_ANGLE = 2.f * PI; // integration area
sum *= HEMISPHERE_SOLID_ANGLE;

std::cout << "Sum = " << sum << std::endl;
}

void importanceIntegrationCos()
{
double sum = 0.0;
const int N = 1000;
for (int i = 0; i < N; ++i)
{
// generate sample direction using inversed CDF of cosine BRDF
Vec2f r = randomHammersley(i, N);
float phi = 2.f * PI * r.x;
float sinTheta = sqrt(r.y);
float cosTheta = sqrtf(1.f - sinTheta * sinTheta);

Vec3f sampleDir;
sampleDir.x = cos(phi) * sinTheta;
sampleDir.y = sin(phi) * sinTheta;
sampleDir.z = cosTheta;

float value = f(sampleDir);
sum += value / N; // BRDF is divided by it's PDF, so no NdotL weight and no PDF normalization by dividing by PI
}

//const float HEMISPHERE_SOLID_ANGLE = 2.f * PI; // integration area
//sum *= HEMISPHERE_SOLID_ANGLE; // Not needed, why ???

std::cout << "Sum = " << sum << std::endl;
}

void main()
{
uniformIntegrationCos();
importanceIntegrationCos();
}

• It's change of variables in integration, a different Jacobian arises which leads to a different term. Commented Nov 2, 2021 at 9:12
• @lightxbulb but my code above is correct for both cases, right? Commented Nov 2, 2021 at 10:09
• I didn't read your code and I don't plan to. It was just an answer to why $2\pi$ 'disappears'. Commented Nov 2, 2021 at 10:11

Think of this way: when integrating uniformly over the hemisphere, it's like you are importance-sampling with a constant pdf of $$1/2\pi$$.
The multiplication by $$2\pi$$ at the end, then, can be seen as the division by the pdf, factored out of the sum because it's constant.
The constant pdf is $$1/2\pi$$ (as opposed to some other number) because it needs to be normalized to integrate to 1 over the hemisphere, which it does because the area of the hemisphere is $$2\pi$$.
Your code looks reasonable from what I can see, but I would define the function pdf to include the normalization factor of $$1/\pi$$ for the cosine pdf:
float cosinePDF(Vec3f sampleDir) { return sampleDir.z / PI; }