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

5. 球のレンダリング

Ray Tracing in One Weekend (v3.2.3): 5 Adding a Sphere / 1.5 球のレンダリング

この章では,初めて「物体」をシーンに配置します。題材は最もシンプルな立体である球です。レイと球の交差判定を数学的に導出し,当たった部分を赤く塗ります。

球とレイの交差判定

球の方程式

中心 C=(Cx,Cy,Cz),半径 r の球上の点 P は次の条件を満たします:

(PC)(PC)=r2

これは「PC の距離の二乗が r2 に等しい」ことを内積で表したものです。

レイを代入する

レイ P(t)=A+tb を球の方程式に代入し,oc=AC とおくと:

(oc+tb)(oc+tb)=r2

展開すると:

t2(bb)+2t(boc)+(ococ)r2=0

これは t についての 二次方程式 at2+bt+c=0 で,各係数は:

変数
abb
b2(boc)
cococr2

判別式

二次方程式の判別式 Δ=b24ac は実数解の個数を決定します:

判別式意味交差の様子
Δ>02個の実数解レイが球を貫通
Δ=01個の実数解レイが球に接する
Δ<0実数解なしレイが球を外れる

この章では Δ>0 かどうかだけを判定します。

hit_sphere 関数

C++ の実装です。

cpp
bool hit_sphere(const point3& center, double radius, const ray& r) {
    vec3 oc = r.origin() - center;
    auto a = dot(r.direction(), r.direction());
    auto b = 2.0 * dot(oc, r.direction());
    auto c = dot(oc, oc) - radius*radius;
    auto discriminant = b*b - 4*a*c;
    return (discriminant > 0);
}

Rust の実装です。

rust
fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool {
    let oc = r.origin() - center;
    let a = dot(r.direction(), r.direction());
    let b = 2.0 * dot(oc, r.direction());
    let c = dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;
    discriminant > 0.0
}

C++ と Rust の違い

最後の式が戻り値になる

Rust の関数では,最後に置いたセミコロンなしの式がそのまま戻り値になります。

rust
fn hit_sphere(...) -> bool {
    // ...
    discriminant > 0.0   // No semicolon → this bool value is the return value
}

return discriminant > 0.0; と書いても同じですが,Rust では慣例として関数末尾の return を省略します。セミコロンの有無が「式(値を生成する)」か「文(値を捨てる)」かを区別するのが Rust の文法上の特徴です。

一方,ray_color のように途中で早期リターンする場合は明示的に return が必要です。

rust
fn ray_color(r: &Ray) -> Color {
    if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
        return Color::new(1.0, 0.0, 0.0);  // Early return
    }
    // ...
    (1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
    // The trailing expression is the return value
}

浮動小数点との比較

C++ の discriminant > 0doubleint の比較ですが,暗黙の型変換により問題なくコンパイルされます。

cpp
return (discriminant > 0);  // double > int: implicit conversion is fine

Rust では異なる型の比較はコンパイルエラーになります。discriminantf64 なので,比較対象も f64 でなければなりません。

rust
discriminant > 0.0  // f64 > f64:OK
// discriminant > 0  // f64 > i32: compile error

引数の値渡しと参照渡し

C++ の引数と Rust での対応を整理します。

cpp
bool hit_sphere(const point3& center, double radius, const ray& r)
rust
fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool
  • center: Point3Point3(= Vec3)は Copy を実装しているため,値渡しでもコストなくコピーされます。C++ の const point3& と実質同等です。
  • radius: f64f64Copy なので値渡しで問題ありません。
  • r: &RayRayCopy を実装していないため,所有権を渡さず &Ray で借用します。

if のブレース必須

C++ では if の本体が一文の場合,ブレースを省略できます。Rust では常にブレースが必要です。

cpp
// C++: braces can be omitted
if (hit_sphere(point3(0,0,-1), 0.5, r))
    return color(1, 0, 0);
rust
// Rust: braces are required
if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
    return Color::new(1.0, 0.0, 0.0);
}

r105-sphere クレートの実装

r105-sphere/Cargo.toml を用意します。

r105-sphere/Cargo.toml
toml
[package]
name = "r105-sphere"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

r105-sphere/src/lib.rs に完全な実装を置きます。カメラ設定とレンダリングループは前章の r104-ray-camera-background と同一で,hit_sphere 関数の追加と ray_color 関数の更新が変更点です。

r105-sphere/src/lib.rs
rust
use common::{Color, Point3, Vec3, dot, unit_vector, write_color, Ray};

fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool {
    let oc = r.origin() - center;
    let a = dot(r.direction(), r.direction());
    let b = 2.0 * dot(oc, r.direction());
    let c = dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;
    discriminant > 0.0
}

fn ray_color(r: &Ray) -> Color {
    if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
        return Color::new(1.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 viewport_height = 2.0_f64;
    let viewport_width = aspect_ratio * viewport_height;
    let focal_length = 1.0_f64;

    let origin = Point3::new(0.0, 0.0, 0.0);
    let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
    let vertical = Vec3::new(0.0, viewport_height, 0.0);
    let lower_left_corner =
        origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);

    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 u = i as f64 / (image_width - 1) as f64;
            let v = j as f64 / (image_height - 1) as f64;
            let r = Ray::new(
                origin,
                lower_left_corner + u * horizontal + v * vertical - origin,
            );
            let pixel_color = ray_color(&r);
            output.push_str(&format!("{}\n", write_color(pixel_color)));
        }
    }

    output
}

WASM エクスポート

raytracing-demos/src/lib.rs に追記します。

raytracing-demos/src/lib.rs
rust
// Chapter 1.5: Rendering a Sphere
#[wasm_bindgen]
pub fn render_sphere() -> String {
    r105_sphere::render_image()
}