Ray Tracing in One Weekend: Part 12 Expanding PDFs
Introduction
I have been reading through this book series on Raytracing which walks the reader through the creation of a Raytracer using C++. In this series of articles, I will be going through this book and implementing the lessons in Rust instead and diving deep into the pieces.
In this article we will be completing our PDF implementation, adding the ability to mix multiple PDFs in the scene and add PDF components to spheres and hittable lists.
The following link is the commit in my Github repo that matches the code we will go over.
Mixture of PDFs
In our scene we will likely have multiple important PDFs we want to sample from and not just a single lights quad. To support this case, we are going to allow ourselves to split importance sampling between our important light and a cosine PDF.
pub struct MixturePdf {
p: Arc<dyn Pdf>,
q: Arc<dyn Pdf>
}
impl MixturePdf {
pub fn new(p: Arc<dyn Pdf>, q: Arc<dyn Pdf>) -> MixturePdf {
MixturePdf { p, q }
}
}
impl Pdf for MixturePdf {
fn value(&self, direction: Vec3) -> f64 {
0.5 * self.p.value(direction) + 0.5 * self.q.value(direction)
}
fn generate(&self) -> Vec3 {
if random_double() < 0.5 {
self.p.generate()
} else {
self.q.generate()
}
}
}
Our mixture PDF just takes in two other PDFs. When we want to generate a Vec3, we randomly pick one of the PDFs to sample from, and delegate to its generate function. When we generate the probability value, we do a 50–50 weighted sum of the likelihood that the vector comes from each of the PDFs.
Using the mixture pdf looks like this
let light_pdf = HittablePdf::new(hit_rec.p, lights.clone());
let cosine_pdf = CosinePdf::new(hit_rec.normal);
let mixture_pdf = MixturePdf::new(Arc::new(light_pdf), Arc::new(cosine_pdf));
Then we just use the mixture pdf in places where we previously used the lights PDF, giving us the render below.
Completing Hittable PDF Implementations
Currently, we have implemented PDF function for our Quads, now we will complete our coverage of PDF support by implementing PDF support for Hittable Lists and Spheres.
Hittable List PDF
Our Hittable List PDF will contain multiple hittable objects and generate a PDF for when we hit them.
fn pdf_value(&self, origin: Point3, direction: Vec3) -> f64 {
let weight = 1.0 / self.objects.len() as f64;
self.objects.iter()
.map(|object| weight * object.pdf_value(origin, direction))
.sum()
}
fn random(&self, origin: Point3) -> Vec3 {
let index: usize = random_int_range(0, (self.objects.len() - 1) as i32) as usize;
self.objects[index].random(origin)
}
To sample a ray we randomly pick one of the objects and delegate the ray generation job to that object. For getting the probability of a given Vec3 being from the distribution, we just take a weighted sum. This approach is really just the generalized version of our mixture of PDFs implementation we did in the last section for an arbitrary number of PDFs.
Sphere PDF
Now let’s build out our Sphere PDF solution.
We are going to create a helper sphere function which will give us a random vector which hits the sphere of a given sphere radius and distance away.
fn random_to_sphere(radius: f64, distance_squared: f64) -> Vec3 {
let r1 = random_double();
let r2 = random_double();
let z = 1.0 + r2 * (f64::sqrt(1.0 - (radius * radius)/distance_squared) - 1.0);
let phi = 2.0 * f64::consts::PI * r1;
let x = f64::cos(phi) * f64::sqrt(1.0 - z * z);
let y = f64::sin(phi) * f64::sqrt(1.0 - z * z);
Vec3::new(x, y, z)
}
Now for our actual Sphere PDF trait implementations
fn pdf_value(&self, origin: Point3, direction: Vec3) -> f64 {
if let None = self.hit(&Ray::new(origin, direction, 0.0), 0.001, f64::INFINITY) {
return 0.0;
}
let dist_squared = (self.center.at(0.0) - origin).length_squared();
let cos_theta_max = f64::sqrt(1.0 - self.radius * self.radius / dist_squared);
let solid_angle = 2.0 * f64::consts::PI * (1.0 - cos_theta_max);
1.0 / solid_angle
}
fn random(&self, origin: Point3) -> Vec3 {
let direction = self.center.at(0.0) - origin;
let distance_squared = direction.length_squared();
let uwu = Onb::new(&direction);
uwu.transform(Sphere::random_to_sphere(self.radius, distance_squared))
}
We use our helpful sphere associated function to give us a sampled ray which hits the sphere from our origin, and we create our pdf_value function which will return the probability of the given ray occurring given the PDF.
Rendering
Now we can add spheres into our render setups — and extra lights if we wanted to. Below is the result from doing our final render which includes a dielectric Sphere.
Note: we did a slight refactor to our material struct seen here to store an optional PDF instead of simply storing a pdf_value.
Conclusion
This concludes our exploration into Ray Tracing in Rust. We have built a good basis for building out more advanced Ray Tracing features and from here there are many more directions we could go. Two cool next steps I would be interested in going at this point are:
- We could add support for triangles and create a glTF ray tracing application
- We could put this logic on the GPU and allow us to build a more realtime Raytracer (See this Work in Progress book for that)