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)
}