I am trying to understand why my explicit light sampling implemetation does not produce the same results as my naive path tracer. Here are my results :
Naive Version :
Explicit light sampling version :
Diff :
It's not that different, but the top and back planes have a stronger "glow" in the explicit light version. The shadow also seems softer in the explicit light version.
What I made for the explicit light sampling, based on PBRT and this answer :
vec3 pathTrace( const Ray& ray, int bounces, const std::vector< std::pair< Plane, Material > >& planes, const std::vector< std::pair< Sphere, Material > >& spheres, bool firstBounce = false )
{
if( bounces == 0 )
return vec3( 0.0 );
Hit hit = sceneIntersects( ray, planes, spheres );
if( !hit._any )
return vec3( 0.0 );
// Cast a new ray from the hit position to a random direction on the hemisphere
Ray secondRay( hit._position, sampleHemisphereUniform( hit._normal ) );
vec3 radiance = pathTrace( secondRay, bounces - 1, planes, spheres );
vec3 brdf = hit._material._albedo / Pi;
double pdf = 1.0 / ( 2.0 * Pi );
#ifdef NAIVE
return hit._material._emissive + radiance * brdf * dot( hit._normal, secondRay._direction ) / pdf;
#else
// Remove the emission since its taken into account in the directLighting part
vec3 indirectLighting = radiance * brdf * dot( hit._normal, secondRay._direction ) / pdf;
if( firstBounce && hit._isLight )
{
// Unlesss we hit a light at first bounce, to avoid black lights
indirectLighting += hit._material._emissive;
}
vec3 directLighting;
// For testing purposes, the sphere area light is hardcoded
const vec3 lightPosition( 0.0, 8.0, -2.5 );
const double lightRadius = 1.1;
const vec3 lightEmission( 4.0 );
// Sample a random point on an hemisphere oriented to the point instead of a sphere
vec3 lightSample = sampleHemisphereUniform( normalize( hit._position - lightPosition ) );
lightSample *= lightRadius;
lightSample += lightPosition;
vec3 lightDir = normalize( lightSample - hit._position );
// Cast a ray to the light
Hit hitLight = sceneIntersects( Ray( hit._position, lightDir ), planes, spheres );
if( !hit._isLight && hitLight._any && hitLight._isLight )
{
// Use the area form of the rendering equation
double G = dot( hit._normal, lightDir ) * dot( hitLight._normal, -lightDir ) / length2( hitLight._position - hit._position );
// The light is sampled using an hemisphere, so the area is the area of an hemisphere
double lightArea = 2.0 * Pi * 1.1 * 1.1;
double lightPdf = 1.0 / lightArea;
directLighting = lightEmission * brdf * G / lightPdf;
}
return directLighting + indirectLighting;
#endif
}
This answer says that because I am sampling twice, I should divide my twice the number of samples, but it gives me even more different results so I'm not sure if I to do that or not. Anyway I test both every time.
I was also wondering if it was because 1 bounce of the naive path tracer is not equal to one bounce of the explicit light sampling version (The first bounce of naive will only output color when it hits a light, while the explicit light version will output direct lighting), so I tested with 30 bounces, and bounces - 1, and got almost no difference. Same thing with samples. I am just not able to get the light sampling version with the naive version, and to me it's not a convergence time issue, since the naive version doesn't change much after a 3 min render (other than the noise, of course), so I'm pretty sure the images converges to a different result.
I don't know if I need multiple importance sampling here, I'd say no because I am not trying to cast a light ray based on the brdf (for now) but I'm again not sure.
I'm also not sure if I can sample the sphere using an hemisphere. I did that because I had the hemisphere sampling code ready. But intuitively it seems good to me, because the spherical light would have twice of the samples occluded, which is taken into account when dividing by the pdf of an hemisphere instead of the pdf of a sphere.