11. Positionable Camera
Ray Tracing in One Weekend (v3.2.3): 11 Positionable Camera
This chapter extends the camera to allow free positioning and orientation. Until now the camera was fixed at the origin pointing in the negative Z direction. The updated camera can be placed anywhere and aimed in any direction. The vertical field of view (vfov) also becomes a parameter.
Vertical Field of View (vfov)
The field of view (FOV) describes how wide an angle the camera captures. The relationship between θ (vertical FOV) and the viewport height is:
Previously, viewport_height was fixed at 2.0, which corresponds to
| vfov | viewport_height | |
|---|---|---|
| 90° | 2.0 (previous fixed value) | |
| 20° | ≈ 0.353 (telephoto) | |
| 120° | ≈ 3.464 (ultra wide-angle) |
A smaller vfov narrows the viewport and magnifies the subject.
Camera Position and Orientation
Three vectors define the camera's pose:
lookfrom— the camera's own position (eye point)lookat— the point the camera looks at (target)vup— the world's "up" direction (used to fix the roll angle)
These three vectors form the orthonormal basis
| Vector | Meaning |
|---|---|
| Camera backward direction (opposite to lookfrom → lookat) | |
| Camera right direction | |
| Camera up direction |
The camera looks toward
Passing vup keeps the camera level; tilting vup adds a roll (sideways rotation).
C++ Implementation
camera(point3 lookfrom, point3 lookat, vec3 vup,
double vfov, double aspect_ratio) {
auto theta = degrees_to_radians(vfov);
auto h = tan(theta / 2);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
auto w = unit_vector(lookfrom - lookat);
auto u = unit_vector(cross(vup, w));
auto v = cross(w, u);
origin = lookfrom;
horizontal = viewport_width * u;
vertical = viewport_height * v;
lower_left_corner = origin - horizontal/2 - vertical/2 - w;
}lower_left_corner is the point reached by starting at the origin, subtracting half the horizontal and vertical extents, and then stepping one unit in the - Vec3(0,0,focal_length) where focal_length=1; this is now replaced by - w (which is a unit vector, so
Rust Implementation
use crate::vec3::{Point3, Vec3, cross, unit_vector};
use crate::ray::Ray;
use crate::utils::degrees_to_radians;
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
pub fn new(
lookfrom: Point3,
lookat: Point3,
vup: Vec3,
vfov: f64,
aspect_ratio: f64,
) -> Self {
let theta = degrees_to_radians(vfov);
let h = (theta / 2.0).tan();
let viewport_height = 2.0 * h;
let viewport_width = aspect_ratio * viewport_height;
let w = unit_vector(lookfrom - lookat);
let u = unit_vector(cross(vup, w));
let v = cross(w, u);
let origin = lookfrom;
let horizontal = viewport_width * u;
let vertical = viewport_height * v;
let lower_left_corner = origin - horizontal / 2.0 - vertical / 2.0 - w;
Camera { origin, lower_left_corner, horizontal, vertical }
}
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
Ray::new(
self.origin,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin,
)
}
}C++'s tan(theta/2) is written as (theta / 2.0).tan() in Rust. In Rust, floating-point operations are method calls (x.tan() rather than f64::tan(x)).
Importing cross and unit_vector
To use cross, unit_vector, and degrees_to_radians from camera.rs, add them to the use declarations:
// Top of camera.rs
use crate::vec3::{Point3, Vec3, cross, unit_vector};
use crate::ray::Ray;
use crate::utils::degrees_to_radians;Previously, only Point3 and Vec3 were imported; the use declaration now also includes the free functions cross, unit_vector, and degrees_to_radians.
Differences Between C++ and Rust
| Feature | C++ | Rust |
|---|---|---|
| Trigonometry | tan(theta/2) | (theta / 2.0).tan() |
tan argument | Radians (after conversion) | Radians (after conversion) |
| Struct initialization | Assign to members in constructor body | Compute with let-bindings, then struct literal |
| Argument types | point3, vec3 | Point3, Vec3 (type aliases; same underlying type) |
Updating the common Crate
common/src/camera.rs (complete replacement)
use crate::vec3::{Point3, Vec3, cross, unit_vector};
use crate::ray::Ray;
use crate::utils::degrees_to_radians;
/// A positionable camera with configurable field of view.
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
/// `lookfrom`: camera origin, `lookat`: point the camera looks at, `vup`: up-direction vector.
/// `vfov`: vertical field of view (degrees), `aspect_ratio`: width/height ratio.
pub fn new(
lookfrom: Point3,
lookat: Point3,
vup: Vec3,
vfov: f64,
aspect_ratio: f64,
) -> Self {
let theta = degrees_to_radians(vfov);
let h = (theta / 2.0).tan();
let viewport_height = 2.0 * h;
let viewport_width = aspect_ratio * viewport_height;
let w = unit_vector(lookfrom - lookat);
let u = unit_vector(cross(vup, w));
let v = cross(w, u);
let origin = lookfrom;
let horizontal = viewport_width * u;
let vertical = viewport_height * v;
let lower_left_corner = origin - horizontal / 2.0 - vertical / 2.0 - w;
Camera { origin, lower_left_corner, horizontal, vertical }
}
/// Returns a ray for the given `(s, t)` viewport coordinates in [0, 1].
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
Ray::new(
self.origin,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin,
)
}
}Updating Existing Crates
Because the Camera::new() signature changed, all crates from r107 through r110 need updating. The old camera was at the origin, looking in the
// Before (used by r107 through r110)
let camera = Camera::new();
// After (reproducing the same appearance)
let camera = Camera::new(
Point3::new(0.0, 0.0, 0.0), // lookfrom: origin
Point3::new(0.0, 0.0, -1.0), // lookat: negative Z direction
Point3::new(0.0, 1.0, 0.0), // vup: positive Y direction
90.0, // vfov: 90° = equivalent to the old fixed value
aspect_ratio,
);Implementing the r111-camera Crate
Set up r111-camera/Cargo.toml:
[package]
name = "r111-camera"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }This chapter reuses the four-sphere scene from Chapter 10 (diffuse, ground, metal, glass) and changes only the camera.
r111-camera/src/lib.rs
use std::sync::Arc;
use common::{
Camera, Color, Dielectric, Hittable, HittableList, Lambertian, Metal, Point3,
Sphere, unit_vector, write_color_gamma, Ray, random_double,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
if let Some(mat) = &rec.mat {
if let Some((attenuation, scattered)) = mat.scatter(r, &rec) {
return attenuation * ray_color(&scattered, world, depth - 1);
}
}
return Color::new(0.0, 0.0, 0.0);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
pub fn render_image() -> String {
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 384_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 50_i32;
let max_depth = 50_i32;
let mut world = HittableList::new();
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, 0.0, -1.0), 0.5,
Arc::new(Lambertian::new(Color::new(0.1, 0.2, 0.5))),
)));
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -100.5, -1.0), 100.0,
Arc::new(Lambertian::new(Color::new(0.8, 0.8, 0.0))),
)));
world.add(Box::new(Sphere::with_material(
Point3::new(1.0, 0.0, -1.0), 0.5,
Arc::new(Metal::new(Color::new(0.8, 0.6, 0.2), 0.0)),
)));
world.add(Box::new(Sphere::with_material(
Point3::new(-1.0, 0.0, -1.0), 0.5,
Arc::new(Dielectric::new(1.5)),
)));
// Wide-angle camera looking down from above (vfov=90°)
let camera = Camera::new(
Point3::new(-2.0, 2.0, 1.0),
Point3::new(0.0, 0.0, -1.0),
Point3::new(0.0, 1.0, 0.0),
90.0,
aspect_ratio,
);
let mut output = String::new();
output.push_str("P3\n");
output.push_str(&format!("{} {}\n", image_width, image_height));
output.push_str("255\n");
for j in (0..image_height).rev() {
for i in 0..image_width {
let mut pixel_color = Color::new(0.0, 0.0, 0.0);
for _ in 0..samples_per_pixel {
let u = (i as f64 + random_double()) / (image_width - 1) as f64;
let v = (j as f64 + random_double()) / (image_height - 1) as f64;
let r = camera.get_ray(u, v);
pixel_color += ray_color(&r, &world, max_depth);
}
output.push_str(&format!("{}\n", write_color_gamma(pixel_color, samples_per_pixel)));
}
}
output
}Implementing the r111-camera-zoom Crate (Telephoto)
r111-camera-zoom is nearly identical to r111-camera, with only vfov changed to 20°.
// Same position and direction, but telephoto (vfov=20°)
let camera = Camera::new(
Point3::new(-2.0, 2.0, 1.0),
Point3::new(0.0, 0.0, -1.0),
Point3::new(0.0, 1.0, 0.0),
20.0, // Changed from 90° to 20°
aspect_ratio,
);Changing vfov from 90° to 20° makes
WASM Export
Add to raytracing-demos/src/lib.rs:
// Chapter 1.11: Positionable Camera
#[wasm_bindgen]
pub fn render_camera() -> String {
r111_camera::render_image()
}
#[wasm_bindgen]
pub fn render_camera_zoom() -> String {
r111_camera_zoom::render_image()
}