はじめに
おつかれささまです。先週は、用事があり作業ができず更新できませんでした。前回は、平面を追加したので、今日は、球を追加できたらいいかと思っています。今日の作業としては、球を追加することと複数のオブジェクトをレンダリングするためのScene
を作ってまとめて管理できるようにしたいと思います。よろしくお願いいたします。
今日の追加分は、下記の予定です。
/src
└ /object
┝ sphere.rs
└ scene.rs
クラス図
ひとまず、クラス図的なものを書くと下記のような感じになります。

Sceneの作成
Shape
のデータ構造を、Vec
型で保持していて、Shape
の中にあるintersect
を呼び出し光線との交差判定をしていきます。上記のクラス図にもありますが、Scene
の機能は以下三つです。
- new():初期化
- add():Shapeの追加
- itersect(): 光線との交差判定
Scene
のデータ構造は下記で定義します。
pub struct Scene {
pub objects: Vec<Box<dyn Shape>>,
}
中身は下記で実装していきます。intersect
部分は、Shape
内のintersect
で求めた交差点を取得します。
さらに近い交差点があれば、都度更新していくというような内容となっています。
impl Scene {
pub fn new() -> Self {
Scene { objects: Vec::new(), }
}
pub fn add(&mut self, object: Box<dyn Shape>) {
self.objects.push(object);
}
pub fn intersect(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<(usize, Intersection)> {
let mut closest_so_far = t_max;
let mut closest_intersection = None;
let mut closest_index = 0;
for (i, object) in self.objects.iter().enumerate() {
if let Some(intersection) = object.intersect(ray, t_min, closest_so_far) {
closest_so_far = intersection.t;
closest_intersection = Some(intersection);
closest_index = i;
}
}
closest_intersection.map(|intersection| (closest_index, intersection))
}
}
Sphereの実装
ひとまず、球を表現する上で、何が必要か書き出しておきます。
絵心ないですが、中心の点と、半径があれば、定義できます。

ということで、簡単な定義部分は埋めていきます。
pub struct Sphere {
pub center: Point3,
pub radius: f64,
}
impl Sphere {
pub fn new(center: Point3, radius: f64) -> Self {
Sphere { center, radius }
}
}
次に、交差判定部分を作っていきます。基本的に、平面の交差判定をした時と似た方針です。光線\(Ray(t)=\vec{origin} + t * \vec{dir} \)が球の表面上にあるときの方程式を考えて、t
について解いていくような感じです。
球の場合は、判別式の判定に持っていきます。
$$|Ray(t) – \vec{center}|^2 = r^2$$$$|\vec{origin} + t*\vec{dir} – \vec{center}|^2 = r^2$$$$|\vec{oc} + t*\vec{dir}|^2 = r^2$$$$|\vec{oc}|^2 + 2*t*(\vec{oc}, \vec{dir}) + t^2 * |\vec{dir}|^2 = r^2$$
ここまで、計算するとt
の2次方程式になっているので、t
が解を持つことと光線が球と交差することは同じ意味となります。というわけで、判別式を解いていきます。
$$\Delta=(2*(\vec{oc}, \vec{dir}))^2 – 4*(|\vec{dir}|^2)*(\vec{oc}|^2-r^2)$$
\(\Delta < 0\)の時は、交差しないです。それ以外の時は、交差します。
交差点(\(point\))における法線は、\(\vec{point} – \vec{center}\)のような計算で十分かと思います。
これまでのことを実装していきます。
impl Shape for Sphere {
fn intersect(&self, ray: &crate::data::ray::Ray, t_min: f64, t_max: f64) -> Option<Intersection> {
let oc = ray.get_origin() - self.center;
let a = ray.get_direction().length_squared();
let half_b = oc.dot(&ray.get_direction());
let c = oc.length_squared() - self.radius * self.radius;
let d = half_b * half_b - a * c;
// 交点有無を求める
if d < 0.0 {
return None;
}
// 解を求める。手前の交点から確認していく。
let sqrtd = d.sqrt();
let mut root = (-half_b - sqrtd) / a;
if root < t_min || t_max < root {
root = (-half_b + sqrtd) / a;
if root < t_min || t_max < root {
return None;
}
}
// 法線の算出と、Intersectionの作成
let t = root;
let point = ray.at(t);
let outward_normal = (1.0 / self.radius) * (point - self.center);
let mut intersection = Intersection {
t,
point,
normal: outward_normal,
front_face: false
};
intersection.set_face_normal(ray, outward_normal);
Some(intersection)
}
}
動作確認
さいごに動作確認をして終了です。簡単に下記で確認できると思います。
pub fn main() -> Result<(), Box<dyn Error>> {
let width = 256;
let height = 256;
let film = Film::new(width, height);
let pos = Point3::new(0.0, 0.5, 1.0);
let look_at = Point3::new(0.0, 0.5, -1.0);
let up = Vector3::new(0.0, 1.0, 0.0);
let fov = 90.0_f64.to_radians();
let mut cam = Camera::new(pos, look_at, up, fov, film);
// sceneの追加
let mut scene = Scene::new();
scene.add(Box::new(Plane::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
)));
scene.add(Box::new(Sphere::new(
Point3::new(0.0, 0.5, -0.0),
0.5
)));
for y in 0..height {
for x in 0..width {
let u = x as f64 / (width - 1) as f64;
let v = y as f64 / (height - 1) as f64;
let ray = cam.generate_ray(u, v);
let mut color = Color3::zero();
// sceneを使って、交差判定する
if let Some((_, intersection)) = scene.intersect(&ray, f64::EPSILON, f64::INFINITY) {
let normal = intersection.normal;
color = Rgb::new(
0.5 * (normal.get_x() + 1.0),
0.5 * (normal.get_y() + 1.0),
0.5 * (normal.get_z() + 1.0),
);
} else {
let unit_direction = ray.get_direction().unit();
let t = 0.5 * (unit_direction.get_y() + 1.0);
let white = Rgb::new(1.0, 1.0, 1.0);
let blue = Rgb::new(0.5, 0.7, 1.0);
color = white*(1.0 - t) + blue*t;
}
cam.record_pixel(x, y, color);
}
}
let ppm_output = PpmOutput;
ppm_output.save(cam.get_film(), "build/sphere.ppm").unwrap();
Ok(())
}
上記の出力として、下記が出力されればオッケーだと思います。本日もありがとうございました。

さいごに
本日は、球を追加しました。これからも他の図形を出力できるようにしていきたいと思います。