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. We now allow it to be placed anywhere and aimed in any direction. We also parameterize the vertical field of view (vfov).
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 we fixed viewport_height = 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
We use three vectors to 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)
From these three we build 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, we 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; we now add 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 }We reuse the four-sphere scene from chapter 10 (diffuse, ground, metal, glass) and only change 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()
}