Skip to content
Copied!
published on 2026-04-05

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:

h=tan(θ2)viewport_height=2h

Previously we fixed viewport_height = 2.0, which corresponds to θ=90°.

vfovh=tan(θ/2)viewport_height
90°tan45°=1.02.0 (previous fixed value)
20°tan10°0.176≈ 0.353 (telephoto)
120°tan60°1.732≈ 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 (u,v,w) of the camera local coordinate system:

w=lookfromlookat|lookfromlookat|u=vup×w|vup×w|v=w×u
VectorMeaning
wCamera backward direction (opposite to lookfrom → lookat)
uCamera right direction
vCamera up direction

The camera looks toward w. The old fixed camera corresponded to w=(0,0,1) (positive Z axis), with the camera looking in the Z direction.

Passing (0,1,0) as vup keeps the camera level; tilting vup adds a roll (sideways rotation).

C++ Implementation

cpp
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 w direction. The old fixed camera used - Vec3(0,0,focal_length) where focal_length=1; this is now replaced by - w (which is a unit vector, so |w|=1=focal_length).

Rust Implementation

rust
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:

rust
// 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

FeatureC++Rust
Trigonometrytan(theta/2)(theta / 2.0).tan()
tan argumentRadians (after conversion)Radians (after conversion)
Struct initializationAssign to members in constructor bodyCompute with let-bindings, then struct literal
Argument typespoint3, vec3Point3, Vec3 (type aliases; same underlying type)

Updating the common Crate

common/src/camera.rs (complete replacement)

common/src/camera.rs
rust
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 Z direction, with a 90° FOV, so we pass equivalent arguments:

rust
// 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:

r111-camera/Cargo.toml
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
rust
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°.

rust
// 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 h=tan(10°)0.176, greatly shrinking the viewport. Since the same resolution (384×216) still covers the now-narrow viewport, the subject appears magnified by a factor of about 5.

WASM Export

Add to raytracing-demos/src/lib.rs:

raytracing-demos/src/lib.rs
rust
// 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()
}