0
$\begingroup$

I'm trying to add BRDFs to a very basic path tracer. Starting out, I'd like to convert just the Lambertian material, with two different sampling methods, to ensure that everything is working right. Theoretically, a uniform sample and a cosine-weighted hemispherical sample should both render similar images with different amounts of noise.

However, I'm finding that the uniform sample yields a significantly darker image than the cosine-weighted sample. This makes sense as the uniform set will always be scaled by 1 whereas the weighted set will usually be scaled by more than one - but I don't know exactly how I should fix that. Maybe someone can enlighten me as to how these PDFs should work?

Here is the relevant material code (edited based on the most recent):

func (l *Lambert) Scatter(_, n geom.Unit, uv, p geom.Vec, rnd *rand.Rand) (in geom.Unit, bsdf Color, pdf float64) {
    in, pdf = sampleUniform(n, rnd) // should be able to swap this out, right?
    bsdf = l.texture.Map(uv, p).Scaled(in.Dot(n))
    return
}

func sampleUniform(n geom.Unit, rnd *rand.Rand) (sample geom.Unit, pdf float64) {
    sample = n.RandInHemisphere(rnd)
    pdf = 0.5
    return
}

func sampleCosWeighted(n geom.Unit, rnd *rand.Rand) (sample geom.Unit, pdf float64) {
    sample = n.RandInHemisphereCos(rnd)
    pdf = n.Dot(sample)
    return
}

func sampleReflected(out, n geom.Unit, rnd *rand.Rand) (sample geom.Unit, pdf float64) {
    sample = reflect(out, n)
    pdf = 1
    return
}

And the relevant render loop code:

func color(r Ray, s Surface, depth int, rnd *rand.Rand) Color {
    if depth >= 50 {
        return black
    }
    hit := s.Hit(r, bias, math.MaxFloat64, rnd)
    if hit == nil {
        return black
    }
    out := r.Dir.Inv()
    emit := hit.Mat.Emit(hit.UV, hit.Pt)
    in, bsdf, pdf := hit.Mat.Scatter(out, hit.Norm, hit.UV, hit.Pt, rnd)
    if pdf <= 0 {
        return emit
    }
    indirect := color(NewRay(hit.Pt, in, r.T), s, depth+1, rnd).Times(bsdf).Scaled(1 / pdf)
    return emit.Plus(indirect)
}
$\endgroup$
  • $\begingroup$ Removing the π and using 1/2 and cos(theta) respectively appears to work. I wish I understood why, though. $\endgroup$ – hunterloftis Feb 11 at 9:05
2
$\begingroup$

So, for Uniform sampling the PDF is $1/2π$, For Cos-weighted its $cos(θ)/π$. The Lambertian BRDF has a $\pi$ term as well in the denominator for energy conservation.

When not optimizing things you should be dividing by $\pi$ during the BRDF calculation, then dividing by the proper PDFs mentioned above.

Considering all the factors into account, for uniform sampling you'd have

$L_i*brdf*\cos(\theta) * \displaystyle{\frac{1}{pdf}}$

$L_i * \frac{color}{\pi} * \cos(\theta) \displaystyle{\frac{1}{\frac{1}{2\pi}}}$

$L_i * {color} * \cos(\theta) * 2$

Similarly for Cosine-weighted hemispherical sampling you'd have

$L_i*brdf*\cos(\theta) * \displaystyle{\frac{1}{pdf}}$

$L_i * \frac{color}{\pi} * \cos(\theta) \displaystyle{\frac{1}{\frac{\cos(\theta)}{pi}}}$

$L_i * {color} $

Since you weren't dividing by $\pi$ in the BRDF, the division by the PDF should have been $1/2$ and $\cos(\theta)$ as you suggested but couldn't understand since the actual pdfs have a factor of $\pi$ in them.

$\endgroup$
1
$\begingroup$

You're simply not normalizing correctly, since you've picked the pdf for uniform to be $1$ which it is not, and for cosine to be $\cos\theta$ which it is not. The pdf for uniformly distributed points on the upper hemisphere is: $p_U(\theta, \phi) = \frac{\sin\theta}{2\pi}$. The pdf for cosine distributed points on the upper hemisphere is: $p_C(\theta, \phi) = \frac{\cos\theta\sin\theta}{\pi}$. The integrand of the rendering equation (formulated in terms of sampling the hemisphere and not the area formulation) is: $$brdf(\theta_i, \phi_i, x, \theta_o, \phi_o)L_i(x,\theta_i,\phi_i)\cos\theta_i\sin\theta_i$$ Since the estimator is the integrand divided by the pdf, then in the case of uniform sampling the sine cancels and you get: $$2\pi \cdot brdf(\theta_i, \phi_i, x, \theta_o, \phi_o)L_i(x,\theta_i,\phi_i)\cos\theta_i$$ In the case of cosine sampling the sine and cosine cancel and you get: $$\pi \cdot brdf(\theta_i, \phi_i, x, \theta_o, \phi_o)L_i(x,\theta_i,\phi_i)$$

Note that for a Lambertian material you should use cosine sampling since it produces lower variance.

$\endgroup$
  • $\begingroup$ Comments are not for extended discussion; this conversation has been moved to chat. $\endgroup$ – Dan Hulme Feb 13 at 22:28

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.