Moving Camera Code Into Its Own Class
Before continuing, now is a good time to consolidate our camera and scene-render code into a single new class: the camera
class. The camera class will be responsible for two important jobs:
- Construct and dispatch rays into the world.
- Use the results of these rays to construct the rendered image.
In this refactoring, we'll collect the ray_color()
function, along with the image, camera, and render sections of our main program. The new camera class will contain two public methods initialize()
and render()
, plus two private helper methods get_ray()
and ray_color()
.
Ultimately, the camera will follow the simplest usage pattern that we could think of: it will be default constructed no arguments, then the owning code will modify the camera's public variables through simple assignment, and finally everything is initialized by a call to the initialize()
function. This pattern is chosen instead of the owner calling a constructor with a ton of parameters or by defining and calling a bunch of setter methods. Instead, the owning code only needs to set what it explicitly cares about. Finally, we could either have the owning code call initialize()
, or just have the camera call this function automatically at the start of render()
. We'll use the second approach. 1
After main creates a camera and sets default values, it will call the render()
method. The render()
method will prepare the camera
for rendering and then execute the render loop.
Here's the skeleton of our new camera
class:
use crate::{hittable::Hittable, prelude::*};
pub struct Camera {
/// Ratio of image width over height
pub aspect_ratio: f64,
/// Rendered image width in pixel count
pub image_width: i32,
/// Rendered image height
image_height: i32,
/// Camera center
center: Point3,
/// Location of pixel 0, 0
pixel00_loc: Point3,
/// Offset to pixel to the right
pixel_delta_u: Vec3,
/// Offset to pixel below
pixel_delta_v: Vec3,
}
impl Default for Camera {
fn default() -> Self {
Self {
aspect_ratio: 1.0,
image_width: 100,
image_height: Default::default(),
center: Default::default(),
pixel00_loc: Default::default(),
pixel_delta_u: Default::default(),
pixel_delta_v: Default::default(),
}
}
}
impl Camera {
pub fn with_aspect_ratio(mut self, aspect_ratio: f64) -> Self {
self.aspect_ratio = aspect_ratio;
self
}
pub fn with_image_width(mut self, image_width: i32) -> Self {
self.image_width = image_width;
self
}
pub fn render(&mut self, world: &impl Hittable) -> std::io::Result<()> {
self.initialize();
println!("P3");
println!("{} {}", self.image_width, self.image_height);
println!("255");
for j in 0..self.image_height {
info!("Scanlines remaining: {}", self.image_height - j);
for i in 0..self.image_width {
let pixel_center = self.pixel00_loc
+ (i as f64) * self.pixel_delta_u
+ (j as f64) * self.pixel_delta_v;
let ray_direction = pixel_center - self.center;
let r = Ray::new(self.center, ray_direction);
let pixel_color = Self::ray_color(r, world);
write_color(std::io::stdout(), pixel_color)?;
}
}
info!("Done.");
Ok(())
}
fn initialize(&mut self) {
self.image_height = {
let image_height = (self.image_width as f64 / self.aspect_ratio) as i32;
if image_height < 1 { 1 } else { image_height }
};
self.center = Point3::new(0.0, 0.0, 0.0);
// Determine viewport dimensions.
let focal_length = 1.0;
let viewport_height = 2.0;
let viewport_width =
viewport_height * (self.image_width as f64) / (self.image_height as f64);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
let viewport_u = Vec3::new(viewport_width, 0.0, 0.0);
let viewport_v = Vec3::new(0.0, -viewport_height, 0.0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
self.pixel_delta_u = viewport_u / self.image_width as f64;
self.pixel_delta_v = viewport_v / self.image_height as f64;
// Calculate the location of the upper left pixel.
let viewport_upper_left =
self.center - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
self.pixel00_loc = viewport_upper_left + 0.5 * (self.pixel_delta_u + self.pixel_delta_v);
}
fn ray_color(r: Ray, world: &impl Hittable) -> Color {
if let Some(rec) = world.hit(r, Interval::new(0.0, INFINITY)) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
}
let unit_direction = unit_vector(r.direction());
let a = 0.5 * (unit_direction.y() + 1.0);
(1.0 - a) * Color::new(1.0, 1.0, 1.0) + a * Color::new(0.5, 0.7, 1.0)
}
}
Listing 37: [camera.rs] The camera class skeleton
To begin with, let's fill in the ray_color()
function from main.cc
:
use crate::{hittable::Hittable, prelude::*};
pub struct Camera {
/// Ratio of image width over height
pub aspect_ratio: f64,
/// Rendered image width in pixel count
pub image_width: i32,
/// Rendered image height
image_height: i32,
/// Camera center
center: Point3,
/// Location of pixel 0, 0
pixel00_loc: Point3,
/// Offset to pixel to the right
pixel_delta_u: Vec3,
/// Offset to pixel below
pixel_delta_v: Vec3,
}
impl Default for Camera {
fn default() -> Self {
Self {
aspect_ratio: 1.0,
image_width: 100,
image_height: Default::default(),
center: Default::default(),
pixel00_loc: Default::default(),
pixel_delta_u: Default::default(),
pixel_delta_v: Default::default(),
}
}
}
impl Camera {
pub fn with_aspect_ratio(mut self, aspect_ratio: f64) -> Self {
self.aspect_ratio = aspect_ratio;
self
}
pub fn with_image_width(mut self, image_width: i32) -> Self {
self.image_width = image_width;
self
}
pub fn render(&mut self, world: &impl Hittable) -> std::io::Result<()> {
self.initialize();
println!("P3");
println!("{} {}", self.image_width, self.image_height);
println!("255");
for j in 0..self.image_height {
info!("Scanlines remaining: {}", self.image_height - j);
for i in 0..self.image_width {
let pixel_center = self.pixel00_loc
+ (i as f64) * self.pixel_delta_u
+ (j as f64) * self.pixel_delta_v;
let ray_direction = pixel_center - self.center;
let r = Ray::new(self.center, ray_direction);
let pixel_color = Self::ray_color(r, world);
write_color(std::io::stdout(), pixel_color)?;
}
}
info!("Done.");
Ok(())
}
fn initialize(&mut self) {
self.image_height = {
let image_height = (self.image_width as f64 / self.aspect_ratio) as i32;
if image_height < 1 { 1 } else { image_height }
};
self.center = Point3::new(0.0, 0.0, 0.0);
// Determine viewport dimensions.
let focal_length = 1.0;
let viewport_height = 2.0;
let viewport_width =
viewport_height * (self.image_width as f64) / (self.image_height as f64);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
let viewport_u = Vec3::new(viewport_width, 0.0, 0.0);
let viewport_v = Vec3::new(0.0, -viewport_height, 0.0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
self.pixel_delta_u = viewport_u / self.image_width as f64;
self.pixel_delta_v = viewport_v / self.image_height as f64;
// Calculate the location of the upper left pixel.
let viewport_upper_left =
self.center - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
self.pixel00_loc = viewport_upper_left + 0.5 * (self.pixel_delta_u + self.pixel_delta_v);
}
fn ray_color(r: Ray, world: &impl Hittable) -> Color {
if let Some(rec) = world.hit(r, Interval::new(0.0, INFINITY)) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
}
let unit_direction = unit_vector(r.direction());
let a = 0.5 * (unit_direction.y() + 1.0);
(1.0 - a) * Color::new(1.0, 1.0, 1.0) + a * Color::new(0.5, 0.7, 1.0)
}
}
Listing 38: [camera.rs] The camera::ray_color function
Now we move almost everything from the main()
function into our new camera class. The only thing remaining in the main()
function is the world construction. Here's the camera class with newly migrated code:
use crate::{hittable::Hittable, prelude::*};
pub struct Camera {
/// Ratio of image width over height
pub aspect_ratio: f64,
/// Rendered image width in pixel count
pub image_width: i32,
/// Rendered image height
image_height: i32,
/// Camera center
center: Point3,
/// Location of pixel 0, 0
pixel00_loc: Point3,
/// Offset to pixel to the right
pixel_delta_u: Vec3,
/// Offset to pixel below
pixel_delta_v: Vec3,
}
impl Default for Camera {
fn default() -> Self {
Self {
aspect_ratio: 1.0,
image_width: 100,
image_height: Default::default(),
center: Default::default(),
pixel00_loc: Default::default(),
pixel_delta_u: Default::default(),
pixel_delta_v: Default::default(),
}
}
}
impl Camera {
pub fn with_aspect_ratio(mut self, aspect_ratio: f64) -> Self {
self.aspect_ratio = aspect_ratio;
self
}
pub fn with_image_width(mut self, image_width: i32) -> Self {
self.image_width = image_width;
self
}
pub fn render(&mut self, world: &impl Hittable) -> std::io::Result<()> {
self.initialize();
println!("P3");
println!("{} {}", self.image_width, self.image_height);
println!("255");
for j in 0..self.image_height {
info!("Scanlines remaining: {}", self.image_height - j);
for i in 0..self.image_width {
let pixel_center = self.pixel00_loc
+ (i as f64) * self.pixel_delta_u
+ (j as f64) * self.pixel_delta_v;
let ray_direction = pixel_center - self.center;
let r = Ray::new(self.center, ray_direction);
let pixel_color = Self::ray_color(r, world);
write_color(std::io::stdout(), pixel_color)?;
}
}
info!("Done.");
Ok(())
}
fn initialize(&mut self) {
self.image_height = {
let image_height = (self.image_width as f64 / self.aspect_ratio) as i32;
if image_height < 1 { 1 } else { image_height }
};
self.center = Point3::new(0.0, 0.0, 0.0);
// Determine viewport dimensions.
let focal_length = 1.0;
let viewport_height = 2.0;
let viewport_width =
viewport_height * (self.image_width as f64) / (self.image_height as f64);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
let viewport_u = Vec3::new(viewport_width, 0.0, 0.0);
let viewport_v = Vec3::new(0.0, -viewport_height, 0.0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
self.pixel_delta_u = viewport_u / self.image_width as f64;
self.pixel_delta_v = viewport_v / self.image_height as f64;
// Calculate the location of the upper left pixel.
let viewport_upper_left =
self.center - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
self.pixel00_loc = viewport_upper_left + 0.5 * (self.pixel_delta_u + self.pixel_delta_v);
}
fn ray_color(r: Ray, world: &impl Hittable) -> Color {
if let Some(rec) = world.hit(r, Interval::new(0.0, INFINITY)) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
}
let unit_direction = unit_vector(r.direction());
let a = 0.5 * (unit_direction.y() + 1.0);
(1.0 - a) * Color::new(1.0, 1.0, 1.0) + a * Color::new(0.5, 0.7, 1.0)
}
}
Listing 39: [camera.rs] The working camera class
And here's the much reduced main:
diff --git a/src/main.rs b/src/main.rs
index a8d3932..27377f1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,74 +1,15 @@
-use code::{hittable::Hittable, hittable_list::HittableList, prelude::*, sphere::Sphere};
-
-fn ray_color(r: Ray, world: &impl Hittable) -> Color {
- if let Some(rec) = world.hit(r, Interval::new(0.0, INFINITY)) {
- return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
- }
-
- let unit_direction = unit_vector(r.direction());
- let a = 0.5 * (unit_direction.y() + 1.0);
- (1.0 - a) * Color::new(1.0, 1.0, 1.0) + a * Color::new(0.5, 0.7, 1.0)
-}
-
-fn main() -> Result<(), Box<dyn std::error::Error>> {
- // Image
-
- const ASPECT_RATIO: f64 = 16.0 / 9.0;
- const IMAGE_WIDTH: i32 = 400;
-
- // Calculate the image height, and ensure that it's at least 1.
- const IMAGE_HEIGHT: i32 = {
- let image_height = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as i32;
- if image_height < 1 { 1 } else { image_height }
- };
-
- // World
+use code::{camera::Camera, hittable_list::HittableList, prelude::*, sphere::Sphere};
+fn main() -> std::io::Result<()> {
let mut world = HittableList::new();
world.add(Rc::new(Sphere::new(Point3::new(0.0, 0.0, -1.0), 0.5)));
world.add(Rc::new(Sphere::new(Point3::new(0.0, -100.5, -1.0), 100.0)));
- // Camera
-
- let focal_length = 1.0;
- let viewport_height = 2.0;
- let viewport_width = viewport_height * (IMAGE_WIDTH as f64) / (IMAGE_HEIGHT as f64);
- let camera_center = Point3::new(0.0, 0.0, 0.0);
-
- // Calculate the vectors across the horizontal and down the vertical viewport edges.
- let viewport_u = Vec3::new(viewport_width, 0.0, 0.0);
- let viewport_v = Vec3::new(0.0, -viewport_height, 0.0);
-
- // Calculate the horizontal and vertical delta vectors from pixel to pixel.
- let pixel_delta_u = viewport_u / IMAGE_WIDTH as f64;
- let pixel_delta_v = viewport_v / IMAGE_HEIGHT as f64;
-
- // Calculate the location of the upper left pixel.
- let viewport_upper_left =
- camera_center - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
- let pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
-
- // Render
-
env_logger::init();
- println!("P3");
- println!("{IMAGE_WIDTH} {IMAGE_HEIGHT}");
- println!("255");
-
- for j in 0..IMAGE_HEIGHT {
- info!("Scanlines remaining: {}", IMAGE_HEIGHT - j);
- for i in 0..IMAGE_WIDTH {
- let pixel_center =
- pixel00_loc + (i as f64) * pixel_delta_u + (j as f64) * pixel_delta_v;
- let ray_direction = pixel_center - camera_center;
- let r = Ray::new(camera_center, ray_direction);
-
- let pixel_color = ray_color(r, &world);
- write_color(std::io::stdout(), pixel_color)?;
- }
- }
- info!("Done.");
- Ok(())
+ Camera::default()
+ .with_aspect_ratio(16.0 / 9.0)
+ .with_image_width(400)
+ .render(&world)
}
Listing 40: [main.rs] The new main, using the new camera
Running this newly refactored program should give us the same rendered image as before.
-
The idiomatic Rust solution for this type of problem is the builder pattern. Important parameters can be set either directly with struct access or via a convenient method chain in the owning code. ↩