11. カメラの移動
Ray Tracing in One Weekend (v3.2.3): 11 Positionable Camera / 1.11 カメラの移動
この章ではカメラを自由に配置・向き変更できるように拡張します。これまでカメラは原点固定・Z 軸方向固定でしたが,任意の位置から任意の方向を向けるようにします。あわせて垂直視野角(vfov)のパラメータ化も行います。
垂直視野角(vfov)
視野角(field of view, FOV)は,カメラが何度の範囲を映すかを表します。θ(垂直 FOV)とビューポートの高さの関係は次の式で表されます。
以前は viewport_height = 2.0 に固定していましたが,これは
| vfov | viewport_height | 35 mm 換算焦点距離 | |
|---|---|---|---|
| 90° | 2.0(以前の固定値) | 12 mm | |
| 20° | ≈ 0.353(望遠) | 68 mm | |
| 120° | ≈ 3.464(超広角) | 6.93 mm |
vfov を小さくするほどビューポートが狭くなり,被写体が拡大されます。
カメラの位置と向き
カメラの姿勢を決める 3 つのベクトルを用意します。
lookfrom— カメラ自身の位置(視点)lookat— 注視点(カメラが向く先)vup— 世界の「上」方向(ロール角を固定するために使う)
これら 3 つからカメラローカル座標系の正規直交基底
| ベクトル | 意味 |
|---|---|
| カメラの後方向(lookfrom から lookat への方向の逆) | |
| カメラの右方向 | |
| カメラの上方向 |
カメラは
vup に vup を傾けることでロール(横回転)を付けることもできます。
C++ の実装
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 は,原点から水平・垂直方向それぞれ半分ずつ引き,さらに - Vec3(0,0,focal_length) だった部分が - w に置き換わっています(w は単位ベクトルなので
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())。
cross と unit_vector のインポート
camera.rs から cross・unit_vector・degrees_to_radians を使うために,use 宣言を追加します。
// 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}; のみでしたが,cross・unit_vector の自由関数と degrees_to_radians を追加します。
C++ と Rust の比較
| 機能 | C++ | Rust |
|---|---|---|
| 三角関数 | tan(theta/2) | (theta / 2.0).tan() |
| tan の引数 | ラジアン(変換後) | ラジアン(変換後) |
| 構造体初期化 | コンストラクタ本体でメンバに代入 | フィールドを let で計算し最後に構造体リテラル |
| 引数型 | point3, vec3 | Point3, Vec3(型エイリアス,実体は同じ) |
common クレートへの変更
common/src/camera.rs
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」でしたので,同じ引数を渡します。
// 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 を用意します。
[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
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-zoom は r111-camera とほぼ同じで,vfov を 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, // Just change from 90° to 20°
aspect_ratio,
);vfov が 90° → 20° に変わることで,
WASM エクスポート
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()
}