top of page

SAMPLING AND AUTHORING A BxDF:

IMPLEMENTING GGX MICROFACET NORMAL DISTRIBUTION IN SMALLPT PATH-TRACER

noWeight_alpha05_300spp.png

Language:  C++

Date:  Fall 2018

Class:  Rendering and Shading (Graduate)

I customized the smallpt path-tracing renderer (http://www.kevinbeason.com/smallpt/) to include a microfacet material model based on Walter's 2007 microfacet-model paper.  This model determines microfacet normals of a surface based on the GGX / Trowbridge-Reitz normal distribution function and reflects the incoming radiance about this micronormal.  Additionally, I implemented the sample weighting based on the GGX shadow-masking function.  The code provided is my addition to the original renderer.

The render is a result of using a width parameter (alpha) of 0.5 and 300 samples per pixel.


//-----------------------------

// GGX Shadow-Masking Function
//     G1(v, m) = vis * (2 / (1 + sqrt(1 + width*width + tan^2(thetaVtoM)))
//     visibility vis(a) = 1 if a > 0, and 0 if a <= 0
double ggxShadowPart(Vec v, Vec m, Vec n, double width)
{
    double shadow, vDotM, dotRatio;
    double theta, tanTheta, ratio, denom;
    int vis;

    vDotM = v.dot(m);

    dotRatio = vDotM / v.dot(n);
    vis = dotRatio>0 ? 1 : 0;

    theta = acos(vDotM);
    tanTheta = tan(theta);

    ratio = 2.0 / (1.0 + sqrt(1.0 + width*width*tanTheta*tanTheta));
    shadow = vis * ratio;

    return shadow;
}

//-----------------------------

// Smith Shadow-Masking Approximation (using GGX shadow-masking)
// Determine a sample weight to multiply to radiance.
// Eq 41 in Walter 2007:  weight(o) = (abs(i.dot(m)) G(i, o, m)) / (abs(i.dot(n)) * abs(m.dot(n)))
// Shadow masking G as Smith and GGX: G(i, o, m) = G1(i, m) * G1(o, m)
double ggxSampleWeight(Vec i, Vec o, Vec m, Vec n, double width)
{
    double G1i, G1o;    // Shadow-masking functions
    double iDotM, iDotN, mDotN;
    double weight, numer, denom;

    G1i = ggxShadowPart(i, m, n, width);
    G1o = ggxShadowPart(o, m, n, width);
    iDotM = (i.dot(m) > 0.0) ? i.dot(m) : 0.0;
    iDotN = (i.dot(n) > 0.0) ? i.dot(n) : 0.0;
    mDotN = (m.dot(n) > 0.0) ? m.dot(n) : 0.0;
    //denom = (iDotN*mDotN == 0.0) ? 1.0 : iDotN*mDotN;

    denom = iDotN*mDotN;
    if (denom == 0.0)
    {
        return 0.0;
    }

    numer = iDotM * G1i * G1o;
    weight = numer / denom;

    return weight;
}

//-----------------------------

.....

<in radiance() function>

.....

  else if (obj.refl == GGX)
  {
      // Establish
      double width = 0.5;    // In (0,1)

      double rand0 = erand48(Xi);
      double theta = atan( (width * sqrt(rand0)) / sqrt(1-rand0) );   // Angle between microfacet normal m and macrosurface normal n.

      double rand1 = erand48(Xi);
      double azim = 2*M_PI * rand1;

    // Instead of sending rays in micronormal, send in reflection of viewing angle
      // A set of axes to make a hemisphere relative to:
      // w (original normal), u (selected based on w direction), v (cross product of the two)
      // This hemisphere is centered on the macronormal, and azim and theta determine the macronormal
      //    on this hemisphere.
      Vec w = nl;
      Vec u = ((fabs(w.x) >.1 ? Vec(0,1) : Vec(1))%w).norm();
      Vec v = w%u;

      // Micronormal sampled from hemisphere around macronormal.
      // Polar -> Cartesian coordinates:
      // x = rad * sin(theta) * cos(phi)
      // y = rad * sin(theta) * sin(phi)
      // z = rad * cos(theta)
      Vec m = ( u*sin(theta)*cos(azim) + v*sin(theta)*sin(azim) + w*cos(theta) ).norm();    

      // Reflected direction about micronormal from the opposite of the view direction.
      Vec negRD = r.d * -1;
      Vec d = m * 2 * (fmax(m.dot(negRD), 0.0)) - negRD;

      // Determine a sample weight to multiply to radiance.
      //double weight = ggxSampleWeight(negRD, d, m, n, width);
      double weight = 1.0;

    // Redirect ray by reflection of viewing angle and micronormal.
      return obj.e + f.mult( radiance(Ray(x, d), depth, Xi)*weight );
  }

.....

bottom of page