ACADEMIC RESEARCH AND PUBLICATIONS
during Graduate and Undergraduate Studies
Doctoral Candidate in Computer Science
Visual Computing Division, Digital Production Arts
Clemson University | School of Computing
SAMPLING AND AUTHORING A BxDF:
IMPLEMENTING GGX MICROFACET NORMAL DISTRIBUTION IN SMALLPT PATH-TRACER
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 );
}
.....