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 :

Naive version

Explicit light sampling version :

enter image description here

Diff :

Diff with tweaked contrast

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;
    // 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;

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.

  • $\begingroup$ I gave it a quick look and I don't see any checks that account for double dipping the light source contribution. Like the answer said, since you are sampling light sources explicitly now, you either need to divide by 2 if you are double dipping or simply just ignore the contribution when you hit a light source by random sampling the hemisphere $\endgroup$ – gallickgunner Mar 30 '19 at 17:38
  • $\begingroup$ I only add the light emission if the ray hits a light on the first bounce. Otherwise, the light emission only comes from the direct lighting part. $\endgroup$ – Aulaulz Mar 30 '19 at 18:07
  • $\begingroup$ okay, btw did you took both images at the same samples? cuz then it wouldn't be a fair comparison. You said something about 30 bounces and bounces-1 I don't understand why -1 The NEE version is substantially higher (in image quality) So you should be comparing something like a 4k sample naive image with a maybe 100 sample NEE image. $\endgroup$ – gallickgunner Mar 30 '19 at 23:01
  • $\begingroup$ I tested it with 10k samples with the naive version and 400 with the explicit one. Don't mind the -1 thing, it was just a test. $\endgroup$ – Aulaulz Apr 1 '19 at 20:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.