0
$\begingroup$

I've been trying to importance sample the GGX NDF of my Cook-Torrance BRDF for some time now but I can't get it right.

I've been following this article.

Here's my code for the importance sampling part. It samples a direction and returns it. It also computes the corresponding probability for that direction and stores it in the pdf argument of cook_torrance_sample_direction. The PerRayData argument is only used to generate uniform random numbers.

inline vec3f __device__ cook_torrance_sample_direction(PerRayData& prd, vec3f& view_direction, vec3f& surface_normal, float roughness, float& pdf)
{
    float roughness2 = roughness * roughness;

    //rand_theta in [0.0, 1.0]
    float rand_theta = prd.random();

    float theta = atan(roughness * sqrtf(rand_theta / (1.0f - rand_theta)));
    float phi = prd.random() * 2.0f * M_PI;//Isotropic case

    float cos_theta = cos(theta);
    float sin_theta = sin(theta);
    float cos_phi = cos(phi);
    float sin_phi = sin(phi);

    //+0.00001f to avoid dividing by 0
    float denom = (roughness2 - 1.0f) * cos_theta * cos_theta + 1.0f + 0.00001;
    float pdf_half_vector = (2 * roughness2 * cos_theta * sin_theta) / (denom * denom);

    vec3f sampled_normal_local = vec3f(cos_phi * sin_theta,
                                       sin_phi * sin_theta,
                                       cos_theta);

    vec3f view_direction_local = world_to_local(surface_normal, view_direction);

    //Section "One Extra Step" of the article
    pdf = pdf_half_vector / (4 * dot(view_direction_local, sampled_normal_local) + 0.00001f);

    vec3f surface_normal_local = vec3f(0.0f, 0.0f, 1.0f);
    vec3f reflected_direction_local = normalize(-view_direction_local - 2 * dot(-view_direction_local, sampled_normal_local) * sampled_normal_local);
    vec3f reflected_dir_world = local_to_world(surface_normal, reflected_direction_local);

    return reflected_dir_world;
}

The sampled direction is then passed to the BRDF evaluation function (as the light_direction argument) which returns a color. I then divide this color by the PDF:

inline vec3f __device__ cook_torrance_brdf(const CookTorranceMaterial& material, const vec3f& view_dir, const vec3f& light_direction, const vec3f& normal)
{
    vec3f halfway_vector = normalize(view_dir + light_direction);

    float NoV = max(dot(normal, view_dir),            0.0f);
    float NoL = max(dot(normal, light_direction),  0.0f);
    float NoH = max(dot(normal, halfway_vector),      0.0f);
    float VoH = max(dot(view_dir, halfway_vector),    0.0f);

    vec3f F0 = (0.16f * (material.reflectance * material.reflectance));
    //F0 for metals is equal to the albedo.
    //We're going to lerp between the albedo and 0.04
    //(previous value of F0) depending on
    //the metalness of the material. A fully metalic material
    //will thus have a F0 equal to its albedo
    F0 = (1.0f - material.metallic) * F0 + material.metallic * material.albedo;

    //Fresnel: reflected portion of the light (1 - transmitted)
    vec3f F = schlick_approximation(VoH, F0);
    float NDF = GGX_NDF(NoH, material.roughness);
    float G = geometry_Smith(NoV, NoL, material.roughness);

    vec3f kS = F;
    vec3f kD = vec3f(1.0f) - kS;
    kD *= 1.0 - material.metallic;

    vec3f numerator = NDF * G * F;
    //+0.0001f to avoid dividng by zero if the dot products are 0
    float denominator = 4.0f * NoV * NoL + 0.0001f;
    vec3f specular = numerator / denominator;

    // add to outgoing radiance Lo
    return (kD * material.albedo / (float)M_PI + specular);
}

inline vec3f __device__ schlick_approximation(float cos_theta, const vec3f& F0)
{
    return F0 + (1.0f - F0) * powf((1.0f - cos_theta), 5.0f);
}

inline float __device__ GGX_NDF(float NoH, float roughness)
{
    float alpha = roughness * roughness;
    float alpha2 = alpha * alpha;
    float NoH2 = NoH * NoH;

    float num = alpha2;
    float denom = (NoH2 * (alpha2 - 1.0f) + 1.0f);
    denom = (float)M_PI * denom * denom;

    return num / denom;
}

inline float __device__ geometry_Schlick_GGX(float NoX, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float num = NoX;
    float denom = NoX * (1.0 - k) + k;

    return num / denom;
}

inline float __device__ geometry_Smith(float NoV, float NoL, float roughness)
{
    float ggx2 = geometry_Schlick_GGX(NoV, roughness);
    float ggx1 = geometry_Schlick_GGX(NoL, roughness);

    return ggx1 * ggx2;
}

However, this is giving me results brighter than expected for low roughness values. Here is a roughness of 0.05:

Roughness = 0.05

And for roughness lower than 0.013, black pixels start appearing (and eventually cover the entire image):

Roughness = 0.012

I noticed that for the too-bright case of the roughness = 0.05, my evaluation function returns color vectors whose RGB values are over 200 (sometimes way more than that, reaching thousands). I suppose this is okay and expected as long as the PDF counter balances it (which it doesn't because in those cases, the value of the PDF is only between ~5.0 and ~15.0)?

$\endgroup$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.