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

11. カメラの移動

Ray Tracing in One Weekend (v3.2.3): 11 Positionable Camera / 1.11 カメラの移動

この章ではカメラを自由に配置・向き変更できるように拡張します。これまでカメラは原点固定・Z 軸方向固定でしたが,任意の位置から任意の方向を向けるようにします。あわせて垂直視野角(vfov)のパラメータ化も行います。

垂直視野角(vfov)

視野角(field of view, FOV)は,カメラが何度の範囲を映すかを表します。θ(垂直 FOV)とビューポートの高さの関係は次の式で表されます。

h=tan(θ2)viewport_height=2h

以前は viewport_height = 2.0 に固定していましたが,これは θ=90° に相当します。

vfovh=tan(θ/2)viewport_height35 mm 換算焦点距離
90°tan45°=1.02.0(以前の固定値)12 mm
20°tan10°0.176≈ 0.353(望遠)68 mm
120°tan60°1.732≈ 3.464(超広角)6.93 mm

vfov を小さくするほどビューポートが狭くなり,被写体が拡大されます。

カメラの位置と向き

カメラの姿勢を決める 3 つのベクトルを用意します。

  • lookfrom — カメラ自身の位置(視点)
  • lookat — 注視点(カメラが向く先)
  • vup — 世界の「上」方向(ロール角を固定するために使う)

これら 3 つからカメラローカル座標系の正規直交基底 (u,v,w) を構築します。

w=e^fromlookat反転=lookfromlookat|lookfromlookat|u=vup×w|vup×w|v=w×u
ベクトル意味
wカメラの後方向(lookfrom から lookat への方向の逆)
uカメラの右方向
vカメラの上方向

カメラは w 方向を向いています。以前の固定カメラでは w=(0,0,1)(Z 軸正方向)に相当し,カメラは Z 方向を向いていました。

vup(0,1,0) を渡すと,カメラが水平に保たれます。vup を傾けることでロール(横回転)を付けることもできます。

C++ の実装

cpp
class camera {
public:
  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;
  }

  ray get_ray(double u, double v) const {
    return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
  }

private:
  point3 origin;
  point3 lower_left_corner;
  vec3 horizontal;
  vec3 vertical;
};

lower_left_corner は,原点から水平・垂直方向それぞれ半分ずつ引き,さらに w 方向へ 1 単位進んだ点です。以前の固定カメラでは - Vec3(0,0,focal_length) だった部分が - w に置き換わっています(w は単位ベクトルなので |w|=1=focal_length)。

Rust の実装

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++ の tan(theta/2) は Rust では (theta / 2.0).tan() と書きます。Rust では浮動小数点演算がメソッド呼び出しになります(f64::tan(x) ではなく x.tan())。

crossunit_vector のインポート

camera.rs から crossunit_vectordegrees_to_radians を使うために,use 宣言を追加します。

rust
// Top of camera.rs
use crate::vec3::{Point3, Vec3, cross, unit_vector};
use crate::ray::Ray;
use crate::utils::degrees_to_radians;

以前は use crate::vec3::{Point3, Vec3}; のみでしたが,crossunit_vector の自由関数と degrees_to_radians を追加します。

C++ と Rust の比較

機能C++Rust
三角関数tan(theta/2)(theta / 2.0).tan()
tan の引数ラジアン(変換後)ラジアン(変換後)
構造体初期化コンストラクタ本体でメンバに代入フィールドを let で計算し最後に構造体リテラル
引数型point3, vec3Point3, Vec3(型エイリアス,実体は同じ)

common クレートへの変更

common/src/camera.rs

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 thin-lens depth of field.
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,
        )
    }
}

既存クレートの更新

Camera::new() のシグネチャが変わったため,r107〜r110 の既存クレートも更新が必要です。旧カメラは「原点,-Z 方向,90° FOV」でしたので,同じ引数を渡します。

rust
// Before (shared 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,
);

r111-camera クレートの実装

r111-camera/Cargo.toml を用意します。

r111-camera/Cargo.toml
toml
[package]
name = "r111-camera"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

r111-camera/src/lib.rs の完全な実装です。10 章のシーン(4 球:拡散・地面・金属・ガラス)を流用し,カメラだけ変更します。

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
}

r111-camera-zoom クレートの実装(望遠)

r111-camera-zoomr111-camera とほぼ同じで,vfov を 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,           // Just change from 90° to 20°
    aspect_ratio,
);

vfov が 90° → 20° に変わることで,h=tan(10°)0.176 となりビューポートが大幅に縮小します。同じ解像度(384×216)に映像が収まるため,被写体が約 5 倍に拡大されて見えます。

WASM エクスポート

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()
}