I have a renderer where the BxDF interface is Sample(), PDF(), and Eval(). The Lambertian BRDF is working well, and I believe I have properly implemented Eval for GGX based on another user's question, but I'm struggling to find a resource I can understand that explains how to generate samples, and then calculate the corresponding PDFs, for a GGX distribution.
I've found some of the original papers, but unlike Lambert the Sampling and PDF seem based on spherical coordinates (which I don't really understand - 3D Vectors are about the limit of my math).
I would really love to know how to translate "A Simpler and Exact Sampling Routine for the GGX Distribution of Visible Normals" into separate Sample() and PDF() functions.
Here's what I have for Lambert:
type Lambert struct {
R, G, B float64
}
func (l Lambert) Sample(out, normal geom.Direction, rnd *rand.Rand) geom.Direction {
return normal.RandHemiCos(rnd)
}
func (l Lambert) PDF(in, normal geom.Direction) float64 {
return in.Dot(normal) * math.Pi
}
func (l Lambert) Eval(in, out, normal geom.Direction) rgb.Energy {
return rgb.Energy{l.R, l.G, l.B}.Scaled(math.Pi * in.Dot(normal))
}
And here's my in-progress work for Microfacets:
func (m Microfacet) Sample(out, normal geom.Direction, rnd *rand.Rand) geom.Direction {
// TODO: better sampling
return normal.RandHemi(rnd)
}
func (m Microfacet) PDF(in, normal geom.Direction) float64 {
// TODO: PDF that matches a better sampling distribution
return 1 / (2 * math.Pi)
}
// https://computergraphics.stackexchange.com/questions/130/trying-to-implement-microfacet-brdf-but-my-result-images-are-wrong
// https://schuttejoe.github.io/post/ggximportancesamplingpart2/
func (m Microfacet) Eval(in, out, normal geom.Direction) rgb.Energy {
F := schlick2(in, normal, m.F0.Mean()) // The Fresnel function
D := ggx(in, out, normal, m.Roughness) // The NDF (Normal Distribution Function)
G := smithGGX(out, normal, m.Roughness) // The Geometric Shadowing function
r := (F * D * G) / (4 * normal.Dot(in) * normal.Dot(out))
return m.F0.Scaled(r)
}
The GGX BSDF relies on several physics functions I've also implemented:
// Schlick's approximation of Fresnel
func schlick2(in, normal geom.Direction, f0 float64) float64 {
return f0 + (1-f0)*math.Pow(1-normal.Dot(in), 5)
}
// GGX Normal Distribution Function
// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
func ggx(in, out, normal geom.Direction, roughness float64) float64 {
m := in.Half(out)
a := roughness * roughness
nm2 := math.Pow(normal.Dot(m), 2)
return (a * a) / (math.Pi * math.Pow(nm2*(a*a-1)+1, 2))
}
// Smith geometric shadowing for a GGX distribution
// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
func smithGGX(out, normal geom.Direction, roughness float64) float64 {
a := roughness * roughness
nv := normal.Dot(out)
return (2 * nv) / (nv + math.Sqrt(a*a+(1-a*a)*nv*nv))
}
based on spherical coordinates (which I don't really understand - 3D Vectors are about the limit of my math)
you should really look into this - once you get the hang of it, it seems pretty obvious. with cartesian coordinates, you simply describe the extend on each axis for a point. with spherical coordinates, you rather specify the direction, in which the point lies. imagine you look straight up. the polar angle determines, how far you rotate your head down (y axis, y is up). the azimuthal angle determines, how far you rotate your body around (around the y-axis). that's the gist of it $\endgroup$