# Mimicking Blender's “roughness” in my path tracer

Recently I asked a question regarding how to mix a glossy and diffuse shader in my path tracer: Mix shader looks wrong on my path tracer. I thought it was incorrect, but a comparison between mine and Blender's seems correct. However, now I am lost in how I would approach making the surface of a mirror look rough as this example shows:

I have done some research, and discovered that there are different types of reflectance models such as Beckmann and GGX. I've found some explanations of how to find the BRDF such as this site: Specular BRDF, but I can't find any explanation on how to do the reflection ray with explicit lighting. As shown in the pseudo code of my path tracer (below), for every object I shoot a ray towards a light and use the surface transport rendering equation to incorporate the BRDF. I am assuming this is where the GGX/Beckmann BRDF would be plugged in. (I'm guessing it's not quite that simple though and some probability must be involved). What really gets me though is that reflection ray. For diffuse it's easy because I just send off a random ray anywhere in the hemisphere of the surface normal. However, for specular, there's a more sharp bump in the BRDF. How would I translate that into a reflected ray? If I just jittered the ideal reflection ray a little, that wouldn't relate to how the microfacets are modeled in the GGX/Beckmann reflection.

Explicit lighting equation from Peter Shirley's Realistic Ray Tracing:

$$\large L_S(\mathbf x,\mathbf k_o)\approx\frac{\rho(\mathbf k_i,\mathbf k_o)L_e(\mathbf x',\mathbf x-\mathbf x')v(\mathbf x,\mathbf x')\cos\theta_i\cos\theta'}{p(\mathbf x')\left\lVert \mathbf x-\mathbf x'\right\rVert^2}$$

Where $p(\mathbf x)$ is the density function of the light triangle $1/\text{Area}$

$\mathbf x'$ is a random point on the light triangle

$\mathbf x$ is the hit point on the object

The cosines are the angles between the light's normal and the light ray, and the object's normal and the light ray

$\rho$ is the BRDF ($1/\pi$ for ideal diffuse)

And $v$ is either $1$ or $0$ depending on if it's in shadow

Pseudo code:

rayColor(ray r, depth, int E=1)
{
if(r doesn't hit triangle)
return 0
if(r hit is a light)
{
if(E)
return light_emission
else
return 0
}

vector x = r.origin + r.direction*t // x is point where r hit tri
vector n = normal where ray hit triangle
n.normalize()
vector nl = n.dot(r.d) < 0 ? n : n*(-1) // properly orient normal

if(++depth > 5) return 0 // max bounces

float triangle_area   = area of emitting triangle
vector x_light_random = random point on emitting triangle
vector light_normal   = normal of emitting triangle

vector d = x_light_random = x_converted;

if(light_normal.dot(d) > 0) light_normal *= -1; // make it emit
// both sides

object_normal.normalize();
light_normal.normalize();

BRDF = 1/PI // perfect diffuse
light_emitted = 1 // emission of 1
vector light_out = 0
if(ray starting at x towards d hits light (i.e. not in shadow))
{
light_out = BRDF*light_emitted*(object_normal.dot(d))*
(-1*light_normal.dot(d)*triangle_area)/
(d.length*d.length*d.length*d.length)
}
vector direct_light = color_of_object_triangle*light_out;

//----SPECULAR-----
vector d2 = r.dir-n*2*n.dot(r.dir); // ideal reflection
vector light_color = 1 // white since dialectics don't change spec
vector specular = light_color*rayColor(createRay(x, d2),depth)

float P = 0.5; // 50/50 chance of mirror/diffuse
if(erand(Xi) < P)
return direct_light/P
else
return spec/(1-P)


For explicit light sampling: yep, you just evaluate the BRDF for that incoming direction and the output direction back toward the camera. There's probability involved in the case of an area light: you have to randomly choose a point on the light source, and include a factor to convert from probability density over light source area to a density over solid angle at the receiver point. It looks like you've got that already (that's the $\cos \theta' / (p(\mathbf{x}')\|\mathbf{x}-\mathbf{x}'\|^2)$ in the formula you mentioned).

For sampling the reflection ray, you could just pick a random ray from the hemisphere and weight it by the BRDF, the same as you'd do for diffuse. But that won't be very efficient; it'll take a huge number of samples to converge to a low-noise result.

What you really want to do is importance-sample the BRDF. This means choosing a random ray not uniformly over the hemisphere, but using a probability distribution that matches the shape of the BRDF (as much as possible). Then you'll automatically get rays that are clustered around the sharp reflection peak in the BRDF when the roughness is low, and spread out more over the hemisphere when the roughness is high.

So how do you do it? The paper Microfacet Models for Refraction through Rough Surfaces by Walter et al (which popularized the GGX distribution in graphics) handily gives importance-sampling formulas for several common BRDFs in section 5.2. Their approach is to importance-sample the normal distribution of the BRDF, then leave the Fresnel and geometry factors to be factored into the path weight.

For example, their GGX importance-sampling formula is (equations 35–36): \begin{aligned}\theta_m &= \arctan \left( \frac{\alpha_g\sqrt{\xi_1}}{\sqrt{1-\xi_1}} \right) \\ \phi_m &= 2\pi\xi_2\end{aligned} where $\alpha_g$ is the GGX roughness parameter, and $\xi_1, \xi_2$ are a pair of uniform random numbers in [0, 1]. The outputs $\theta_m, \phi_m$ are the polar coordinates (relative to the surface normal) of the sampled microfacet normal.

So to use this, you'd choose random inputs $\xi_1, \xi_2$, then evaluate the above equations, and use the resulting normal to compute the reflection ray, as well as the Fresnel and geometry factors from the BRDF.

• Thank you so, so much! Now everything makes a lot more sense to me. That paper is a wonderful resource too as my next step will be implementing glass after I have mirror done. – Henry Bergin Jan 8 '17 at 21:23