How to set equivalent PDFs for cosine-weighted and uniform-sampled hemispheres? 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 &gt;= 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 &lt;= 0 {
        return emit
    }
    indirect := color(NewRay(hit.Pt, in, r.T), s, depth+1, rnd).Times(bsdf).Scaled(1 / pdf)
    return emit.Plus(indirect)
}
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.