Ray Tracing in One Weekend: Part 7 Lights
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 lighting up our scene. We will do this with emissive materials, allowing us to create lights of any shape which do not bounce rays but instead just emit a constant color.
The following link is the commit in my Github repo that matches the code we will go over.
Material
The first thing we will need to do is expand our material trait to give it a new property — emitted.
pub trait Material: Send + Sync {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<ScatterRecord>;
fn emitted(&self, u: f64, v: f64, p: &Point3) -> Color {
Color::new(0.0, 0.0, 0.0)
}
}
All materials now have an emitted function, by default this will return black (i.e. no emitted light) so that all our old materials function as they did before with no changes necessary.
Diffuse Light Material
Our diffuse light material will simply have an albedo texture, which provides the color to emit as “light”.
pub struct DiffuseLight {
albedo: Box<dyn Texture>
}
impl DiffuseLight {
pub fn new(albedo: Box<dyn Texture>) -> Self {
DiffuseLight {
albedo
}
}
pub fn from_color(albedo_color: Color) -> Self {
let albedo = SolidColor::new(albedo_color);
DiffuseLight {
albedo: Box::new(albedo)
}
}
}
When a ray hits our light object, we do not scatter as we only emit light from this material and it should not receive colors from any other objects. We emit the color from our diffuse light’s texture when emitted is called.
impl Material for DiffuseLight {
fn scatter(&self, _r_in: &Ray, _rec: &HitRecord) -> Option<ScatterRecord> {
None
}
fn emitted(&self, u: f64, v: f64, p: &Point3) -> Color {
self.albedo.get_color(u, v, p)
}
}
Using Emitted Light in Camera Render
Our camera will now need to be updated to make use of materials which emit light. First off, we will add a background color to our camera, we lose the gradient effect we had before for a more simplified paramaterizable ambient color for renders.
pub struct Camera {
image_width: i32,
image_height: i32,
samples_per_pixel: i32,
max_depth: i32,
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
u: Vec3,
v: Vec3,
lens_radius: f64,
background: Color
}
The new function is updated accordingly, taking in background as a parameter.
The change in our rendering has to do with how we get a ray’s color.
fn ray_color(&self, ray: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
if let Some(hit_rec) = world.hit(ray, 0.001, common::INFINITY) {
let color_from_emission = hit_rec.mat.emitted(hit_rec.u, hit_rec.v, &hit_rec.p);
return match hit_rec.mat.scatter(ray, &hit_rec) {
Some(scatter_rec) => {
let color_from_scatter = scatter_rec.attenuation * self.ray_color(&scatter_rec.scattered, world, depth - 1);
return color_from_emission + color_from_scatter;
},
None => color_from_emission
};
} else {
return self.background;
}
}
Now, when there is a hit we sum up the emission and scatter — if present — colors. If there is no hit, we return our background color.
Rendering
We can now create a Cornell Box like world with a light at the top.
fn cornell_box() {
let mut world = HittableList::new();
let red = Lambertian::from_color(Color::new(0.65, 0.05, 0.05));
let white_one = Lambertian::from_color(Color::new(0.73, 0.73, 0.73));
let white_two = Lambertian::from_color(Color::new(0.73, 0.73, 0.73));
let white_three = Lambertian::from_color(Color::new(0.73, 0.73, 0.73));
let green = Lambertian::from_color(Color::new(0.12, 0.45, 0.15));
let light = DiffuseLight::from_color(Color::new(15.0, 15.0, 15.0));
world.add(Box::new(Quad::new(Point3::new(555.0,0.0,0.0), Vec3::new(0.0, 555.0, 0.0), Vec3::new(0.0, 0.0, 555.0), Arc::new(green))));
world.add(Box::new(Quad::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 555.0, 0.0), Vec3::new(0.0, 0.0, 555.0), Arc::new(red))));
world.add(Box::new(Quad::new(Point3::new(343.0, 554.0, 332.0), Vec3::new(-130.0, 0.0, 0.0), Vec3::new(0.0, 0.0, -105.0), Arc::new(light))));
world.add(Box::new(Quad::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(555.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 555.0), Arc::new(white_one))));
world.add(Box::new(Quad::new(Point3::new(555.0, 555.0, 555.0), Vec3::new(-555.0, 0.0, 0.0), Vec3::new(0.0, 0.0, -555.0), Arc::new(white_two))));
world.add(Box::new(Quad::new(Point3::new(0.0, 0.0, 555.0), Vec3::new(555.0, 0.0, 0.0), Vec3::new(0.0, 555.0, 0.0), Arc::new(white_three))));
let eye = Point3::new(278.0, 278.0, -800.0);
let lookat = Point3::new(278.0, 278.0, 0.0);
let up = Point3::new(0.0, 1.0, 0.0);
let dist_to_focus = (eye - lookat).length();
let aperture = 0.0;
let camera = Camera::new(600, 600, SAMPLES_PER_PIXEL, MAX_DEPTH, eye, lookat, up, 40.0, 1.0, aperture, dist_to_focus, Color::new(0.0, 0.0, 0.0));
camera.render(&world);
}
We have an ambient scene color of black so the only light in our scene is from the emissive quad we put in the top of the box.
Below is the resulting render (which could use a couple more samples per pixel to fully capture the light bounces).
Conclusion
In this article we added lights via an emissive material property, this allows us to create lights using any shape or texture that we want. We started laying out the structure to reproduce the Cornell box render which we will build upon in subsequent articles.