3. ベクトルクラス
Ray Tracing in One Weekend (v3.2.3): 3 The vec3 Class / 1.3 vec3クラス
原典では,色・位置・方向のすべてに使う vec3 クラスを C++ で定義します。本稿ではこれを Rust の構造体として実装します。Vec3 は以降のすべてのチャプターで使用するため,ワークスペースの common クレートに配置します。
C++ と Rust の違い
構造体定義と impl ブロックの分離
C++ ではメンバ関数をクラス内に定義しますが,Rust では構造体(データの型)と impl ブロック(メソッドの実装)が分離されます。
// C++
class vec3 {
public:
double e[3];
double x() const { return e[0]; } // Member function defined inside the class
};// Rust
struct Vec3 {
e: [f64; 3], // Data definition
}
impl Vec3 {
pub fn x(self) -> f64 { self.e[0] } // Methods go in the impl block
}Copy trait によるコピー動作の明示
C++ では double を持つクラスは暗黙的にコピーコンストラクタが定義されますが,Rust ではコピー動作を #[derive(Clone, Copy)] で明示的に宣言します。Copy を実装した型は,変数への代入や関数の引数渡しで自動的にコピーされます。Copy は Clone のサブ trait であるため,両方を同時に derive します。
演算子オーバーロードと trait
C++ では operator+ をメンバ関数または自由関数として定義しますが,Rust では標準ライブラリが提供する trait(Add,Sub 等)を実装することで演算子が使えるようになります。
// C++
vec3 operator+(const vec3& u, const vec3& v) { ... } // Defined as a free function// Rust: implement the std::ops::Add trait
impl std::ops::Add for Vec3 {
type Output = Vec3;
fn add(self, v: Vec3) -> Vec3 { ... }
}どの型がどんな演算子をサポートするかが,trait の実装として明示的に宣言されます。
コンストラクタはただの関連関数
C++ のコンストラクタは言語が特別扱いする構文ですが,Rust に言語レベルのコンストラクタはありません。Vec3::new(...) のような関連関数(associated function)を慣習的に定義します。引数なしのゼロ初期化は Default trait で実現します。
// C++
vec3() : e{0,0,0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}// Rust
impl Default for Vec3 {
fn default() -> Self {
Vec3 { e: [0.0, 0.0, 0.0] }
}
}
impl Vec3 {
pub fn new(e0: f64, e1: f64, e2: f64) -> Self {
Vec3 { e: [e0, e1, e2] }
}
}Vec3 の実装
common/src/vec3.rs を新規作成します。以下が完全な実装です。
common/src/vec3.rs
use std::ops::{
Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub,
};
/// A 3D vector with `f64` components.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vec3 {
/// Components stored as `[x, y, z]`.
pub e: [f64; 3],
}
/// Type alias for a 3D point.
pub type Point3 = Vec3;
/// Type alias for an RGB color (components in [0, 1]).
pub type Color = Vec3;
// --- Constructors and Accessors ---
impl Vec3 {
/// Creates a new `Vec3` from three `f64` components.
pub fn new(e0: f64, e1: f64, e2: f64) -> Self {
Vec3 { e: [e0, e1, e2] }
}
/// Returns the x component.
pub fn x(self) -> f64 { self.e[0] }
/// Returns the y component.
pub fn y(self) -> f64 { self.e[1] }
/// Returns the z component.
pub fn z(self) -> f64 { self.e[2] }
/// Returns the squared length (dot product with itself).
pub fn length_squared(self) -> f64 {
self.e[0] * self.e[0] + self.e[1] * self.e[1] + self.e[2] * self.e[2]
}
/// Returns the Euclidean length.
pub fn length(self) -> f64 {
self.length_squared().sqrt()
}
}
impl Default for Vec3 {
fn default() -> Self {
Vec3 { e: [0.0, 0.0, 0.0] }
}
}
// --- Output ---
impl std::fmt::Display for Vec3 {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
}
}
// --- Operator Overloads ---
impl Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
Vec3::new(-self.e[0], -self.e[1], -self.e[2])
}
}
impl Index<usize> for Vec3 {
type Output = f64;
fn index(&self, i: usize) -> &f64 { &self.e[i] }
}
impl IndexMut<usize> for Vec3 {
fn index_mut(&mut self, i: usize) -> &mut f64 { &mut self.e[i] }
}
impl AddAssign for Vec3 {
fn add_assign(&mut self, v: Vec3) {
self.e[0] += v.e[0];
self.e[1] += v.e[1];
self.e[2] += v.e[2];
}
}
impl MulAssign<f64> for Vec3 {
fn mul_assign(&mut self, t: f64) {
self.e[0] *= t;
self.e[1] *= t;
self.e[2] *= t;
}
}
impl DivAssign<f64> for Vec3 {
fn div_assign(&mut self, t: f64) {
*self *= 1.0 / t;
}
}
impl Add for Vec3 {
type Output = Vec3;
fn add(self, v: Vec3) -> Vec3 {
Vec3::new(self.e[0] + v.e[0], self.e[1] + v.e[1], self.e[2] + v.e[2])
}
}
impl Sub for Vec3 {
type Output = Vec3;
fn sub(self, v: Vec3) -> Vec3 {
Vec3::new(self.e[0] - v.e[0], self.e[1] - v.e[1], self.e[2] - v.e[2])
}
}
impl Mul for Vec3 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 {
Vec3::new(self.e[0] * v.e[0], self.e[1] * v.e[1], self.e[2] * v.e[2])
}
}
impl Mul<f64> for Vec3 {
type Output = Vec3;
fn mul(self, t: f64) -> Vec3 {
Vec3::new(self.e[0] * t, self.e[1] * t, self.e[2] * t)
}
}
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 { v * self }
}
impl Div<f64> for Vec3 {
type Output = Vec3;
fn div(self, t: f64) -> Vec3 { self * (1.0 / t) }
}
// --- Utility Functions ---
/// Computes the dot product of two vectors.
pub fn dot(u: Vec3, v: Vec3) -> f64 {
u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]
}
/// Computes the cross product of two vectors.
pub fn cross(u: Vec3, v: Vec3) -> Vec3 {
Vec3::new(
u.e[1] * v.e[2] - u.e[2] * v.e[1],
u.e[2] * v.e[0] - u.e[0] * v.e[2],
u.e[0] * v.e[1] - u.e[1] * v.e[0],
)
}
/// Returns the unit vector in the same direction as `v`.
pub fn unit_vector(v: Vec3) -> Vec3 {
v / v.length()
}実装の解説
#[derive(...)] マクロ
#[derive(Debug, Clone, Copy, PartialEq)]#[derive] マクロはボイラープレートな trait 実装を自動生成します。
Debug:{:?}フォーマット指定子でデバッグ出力できるようになります。println!("{:?}", v)のように使います。Clone:v.clone()で明示的なコピーが作れます。Copyを実装するにはCloneも実装している必要があります。Copy:let v2 = v1;のような代入や,関数の引数渡しで暗黙的にコピーされる動作を有効にします。f64の配列[f64; 3]はCopyなので,Vec3もCopyにできます。PartialEq:==演算子を使えるようにします。テストでのassert_eq!(v1, v2)等に必要です。
アクセサメソッドの self 値渡し
pub fn x(self) -> f64 { self.e[0] }C++ の const メンバ関数と異なり,Rust のメソッドは self,&self,&mut self のいずれかで受け取ります。ここでは self(値渡し)を使っています。通常,値渡しは所有権を移動させますが,Copy を実装しているため self.x() を呼んでも self は消費されず,呼び出し後も使い続けられます。
Default trait
impl Default for Vec3 {
fn default() -> Self {
Vec3 { e: [0.0, 0.0, 0.0] }
}
}C++ のデフォルトコンストラクタ vec3() : e{0,0,0} {} に相当します。Vec3::default() で呼べるほか,Vec3 { ..Default::default() } のような構造体更新構文でも使われます。
Display trait による出力
impl std::fmt::Display for Vec3 {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
}
}C++ の operator<< に相当します。Display を実装すると {} フォーマット指定子が使えるようになります。Debug ({:?}) は derive で自動生成できますが,Display ({}) はユーザー向けのフォーマットとして手動実装が必要です。
複合代入演算子の型パラメータ
impl AddAssign for Vec3 { ... } // Vec3 += Vec3
impl MulAssign<f64> for Vec3 { ... } // Vec3 *= f64
impl DivAssign<f64> for Vec3 { ... } // Vec3 /= f64AddAssign は両辺が同じ Vec3 型なので型パラメータが不要です。MulAssign<f64> は右辺が f64 なので型パラメータで指定します。
DivAssign は MulAssign に委譲しています。*self *= 1.0 / t の *self は自分自身への参照外しで,MulAssign の実装が呼ばれます。
Vec3 * Vec3 はアダマール積
impl Mul for Vec3 { ... } // Component-wise multiplicationVec3 * Vec3 は内積ではなく成分ごとの乗算(アダマール積)です。内積は後述の dot() 関数を使います。アダマール積は後のチャプターでテクスチャカラーの合成等に使われます。
f64 * Vec3 のための逆向き実装
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 { v * self }
}Rust の trait 実装は実装対象の型 (impl ... for Type) を起点に単方向にのみ適用されます。そのため 2.0 * v を使えるようにするには,Vec3 * f64 と別に f64 * Vec3 を実装する必要があります。
Index と IndexMut trait
impl Index<usize> for Vec3 { ... }
impl IndexMut<usize> for Vec3 { ... }Index を実装すると v[0] のような読み取りアクセスが,IndexMut を実装すると v[0] = 1.0 のような書き込みアクセスができます。
ユーティリティ関数
dot,cross,unit_vector は impl Vec3 の外に置いた自由関数です(C++ の自由関数と同様)。Rust では impl ブロック内の関連関数と外の自由関数のどちらに置いても機能に差はありませんが,dot(u, v) のような二項演算は自由関数として書く方が自然な呼び出し形になります。
cross は行列式展開による実装です:
write_color 関数
原典は color.h に write_color() を定義します。C++ 版は std::ostream に直接書き込みますが,本稿では前章と同様に String を返す形にします。Color は Vec3 の型エイリアスなので,Vec3 の値をそのまま受け取ります。
common/src/vec3.rs に追記します。
pub fn write_color(pixel_color: Color) -> String {
let ir = (255.999 * pixel_color.x()) as i32;
let ig = (255.999 * pixel_color.y()) as i32;
let ib = (255.999 * pixel_color.z()) as i32;
format!("{} {} {}", ir, ig, ib)
}type Color = Vec3 の意味
pub type Point3 = Vec3;
pub type Color = Vec3;C++ と同様に,Rust の type エイリアスはコンパイラによる型チェックが行われません。Color と Point3 は完全に同じ型として扱われ,混在させてもコンパイルエラーにはなりません。型の混在を防ぎたい場合は newtype パターン を使いますが,原典の設計を尊重してここでは型エイリアスにとどめます。
common クレートへの組み込み
Vec3 を common クレートから公開するため,common/src/lib.rs を作成します。
// common/src/lib.rs
pub mod vec3;
pub use vec3::{Color, Point3, Vec3, cross, dot, unit_vector, write_color};前章のコードを書き直す
原典は main.cc を color 型と write_color() を使って書き直します。本稿では r103-vec3 クレートを新規作成します。
common クレートを依存関係に追加するため,r103-vec3/Cargo.toml を用意します。
[package]
name = "r103-vec3"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r103-vec3/src/lib.rs に render_image() を実装します。
use common::{Color, write_color};
pub fn render_image() -> String {
let mut output = String::new();
let image_width = 256;
let image_height = 256;
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 {
for i in 0..image_width {
let pixel_color = Color::new(
i as f64 / (image_width - 1) as f64,
j as f64 / (image_height - 1) as f64,
0.0,
);
output.push_str(&format!("{}\n", write_color(pixel_color)));
}
}
output
}r,g,b の個別変数がなくなり,Color::new(r, g, b) で直接ピクセルカラーを構築しています。write_color に色計算を委譲したことで,render_image は「どんな色を生成するか」に集中した構造になります。