ACADEMIC RESEARCH AND PUBLICATIONS
during Graduate and Undergraduate Studies
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 );
}
​
.....