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

h=tan(θ2)viewport_height=2h

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

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

w=lookfrom−lookat|lookfrom−lookat|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, 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; the use declaration now also includes 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 each crate passes 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 }

This chapter reuses the four-sphere scene from Chapter 10 (diffuse, ground, metal, glass) and changes only 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()
}