レイトレーシング 8-1

Rust

はじめに

こんにちは、お疲れ様です。
最近暑くて、食欲がない今日この頃です。夏バテに気をつけていきましょう。

本日は、円柱でも作ろうかなと思っています。明らかに量が多くなりそうなので、前半後半に分ける前提で作業をしようかと思っています。よろしくお願いいたします。

簡単に、円柱って側面と二つの底面で構成されているので、前半を側面パート後半を底面パートにしていこうかと思っています。

/src
 └ /object
      └ cylinder.rs

クラス図

円柱(Cylinder)を下記のイメージで作っていこうと思います。交差判定を今まで通りintersectに詰め込むと大変なことになりそうなので、分けて実装しようかと思います。また、交差点における法線の算出も割と複雑になりそうなので、今回は分けることにします。normal_atに関しては、Shapeの方で定義しておいてもいい気がします。

Cylinderの実装

何はともあれ、初めにデータ構造を定義しないと始まらないので定義していきます。絵心ないのは許して欲しいですが、円柱を描写する時に必要なデータ構造はおそらく、半径高さ円柱の中心円柱の軸の4つかなと思います。

上記をもとに、データ構造を定義すると下記のような感じになるかと思います。

pub struct Cylinder {
    pub center: Point3, // 円柱の軸の中央に位置する点
    pub axis:  Vector3,
    pub radius: f64,
    pub height: f64,
}

impl Cylinder {
    pub fn new(center: Point3, axis: Vector3, radius: f64, height: f64) -> Self {
        let normalized_axis = axis.unit();
        Self {
            center,
            axis: normalized_axis,
            radius,
            height,
        }
    }

    fn intersect_side(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<f64> {
         //TODO
    }

    fn intersect_caps(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<f64> {
         //TODO
    }
   
    fn nomal_at(&self, point: &Point3) -> Vector3 {
        //TODO
    }
}

円柱の側面の交差判定

円柱の側面の条件は、下記2つを満たせば良いのかと思います。
 ・1: 軸からの距離が半径r上にある。
 ・2: 円柱の高さに収まっている。

1について、\( \vec{v_{\perp}} \)の長さが、半径rであることの確認。

2について、\( \vec{v_{||}} \)がCを中心に高さheight以内に収まっていることの確認。

上記を達成すれば、うまく描写されるはずです。

1: 軸からの距離が半径r上にある。

\( \vec{axis}\)が単位ベクトルとし、\( \vec{v_{||}} = (\vec{v}, \vec{axis}) * \vec{axis} \)で算出できるので、\( \vec{v_{\perp}} = \vec{v} – \vec{v_{||}} = \vec{v} – (\vec{v}, \vec{axis}) * \vec{axis} \)で計算できます。
これをもとに、\( vec{v} = \vec{R(t)} – \vec{C} \)と半径がrであることを用いて下記のtの方程式をとければ良い良いのかと思います。
(\(R(t) = \vec{O} + t * \vec{D}\), Oは光線ベクトルの始点、Dは光線の方向ベクトル)

$$
d(t)^2 = || (\vec{R(t)} – \vec{C}) – (\vec{R(t)} – \vec{C}, \vec{axis}) * \vec{axis} ||^2 = r^2
$$

こちらを頑張って展開していきます。\(R(t) = \vec{O} + t * \vec{D} \)を実際に代入して、\( \vec{CO} = \vec{O} – \vec{C} \)としつつ計算進めると下記になります。

$$
r^2 = || \vec{CO_{\perp}} – t * \vec{D_{\perp}}||^2 \\\ = || \vec{CO_{\perp}}||^2 + 2t(\vec{CO_{\perp}},\vec{D_{\perp}}) + t^2 ||\vec{D_{\perp}} ||^2
$$

tの2次元の方程式に落とし込めたので、判別式\(\Delta\)を用いて解の有無で交差判定をしていきます。

$$
\Delta = (2 (\vec{CO_{\perp}},\vec{D_{\perp}})) ^2 – 4 ||\vec{D_{\perp}} ||^2 (|| \vec{CO_{\perp}}||^2 – r^2)
$$

補足

$$
\vec{CO_{\perp}} = \vec{CO} – (\vec{CO}, \vec{axis}) \vec{axis}
$$

$$
\vec{D_{\perp}} = \vec{D} – (\vec{D}, \vec{axis}) \vec{axis}
$$

2: 円柱の高さに収まっている。

上記を読み進めることができたのであれば、ここの判定は容易です。
上記にある\( \vec{v_{||}} = (\vec{v}, \vec{axis}) * \vec{axis} \)をベースに考えればすぐです。
\(p\)をヒットしたとし\( \vec{v} = \vec{p} – \vec{c} \)とすると、\( \vec{cp_{||}} = (\vec{cp}, \vec{axis}) * \vec{axis} \)となります。
\(\vec{cp}\)の\(\vec{axis}\)への射影であるので、この値が、\( height/2\)以下であることを確認すればOKです。

実装するぞー

上記をもとに実装を進めます。

    fn intersect_side(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<f64> {
        let co = ray.get_origin() - self.center;

        let dir_dot_axis = ray.get_direction().dot(&self.axis);
        let co_dot_axis = co.dot(&self.axis);

        let dir_perp = ray.get_direction() - dir_dot_axis * self.axis;
        let co_perp = co - co_dot_axis * self.axis;

        let a = dir_perp.length_squared();

        // aが0だとそもそも2次方程式の会の公式の分母が0になるので計算できない。
        if a < f64::EPSILON {
            return None;
        }

        let b = 2.0 * co_perp.dot(&dir_perp);
        let c = co_perp.length_squared() - self.radius * self.radius;

        let d = b * b - 4.0 * a * c;
    
    // 判別式dが0未満の場合、解なし=交差点が存在しない
        if d < 0.0 {
            return None;
        }

        let sqrt_d = d.sqrt();
        let mut t1 = (- b - sqrt_d) / (2.0 * a);
        let mut t2 = (- b + sqrt_d) / (2.0 * a);
    
    //tの範囲のチェック
        if t1 > t_max || t2 < t_min {
            return None;
        }

        if t1 < t_min {
            t1 = t_min;
        }

        if t2 > t_max {
            t2 = t_max;
        }
    
    // 円柱の高さ以内に収まっているか?
        let check_height = |t: f64| -> bool {
            let hit_point = ray.at(t);
            let hit_height = (hit_point - self.center).dot(&self.axis);

            hit_height.abs() <= self.height / 2.0
        };

        if check_height(t1) {
            Some(t1)
        } else if check_height(t2) {
            Some(t2)
        } else {
            None
        }
    }

法線ベクトルの算出

こちらもあまり、難しくはないです。交差した点をPとして\(\vec{CP_{\perp}\)を求めることができればOKです。つまり、下記で求めればおおけーです。これに加えて高さに気をつければ良いでしょう。

$$
\vec{PC_{\perp}} = \vec{PC} – (\vec{PC}, \vec{axis}) \vec{axis}
$$

fn normal_at(&self, point: &Point3) -> Vector3 {
    // 側面の法線求める
    let pc = *point - self.center;
    let axis_projection = pc.dot(&self.axis);
    let axis_component = axis_projection * self.axis;
  
  // 今回は底面の交差判定をしていないが、底面にある時はここに入る。
    if (axis_projection.abs() - self.height / 2.0).abs() < 1e-8 {
        if axis_projection > 0.0 {
            return self.axis;
        } else {
            return -self.axis;
        }
    }

    let normal = pc - axis_component;
    normal.unit()
}

Shapeのintersectを実装する

このまま動作確認と行きたいですが、下記を追加しないと怒られます(泣)。

impl Shape for Cylinder {
    fn intersect(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<Intersection> {
        let side_intersection = self.intersect_side(ray, t_min, t_max);

        let t = match(side_intersection) {
            (Some(t_side)) => t_side,
            (None) => return None,
        };

        if t < t_min || t > t_max {
            return None;
        }

        let hit_point = ray.at(t);
        let normal = self.normal_at(&hit_point);

        let mut intersection = Intersection {
            t,
            point: hit_point,
            normal,
            front_face: false,
        };
        intersection.set_face_normal(ray, normal);

        Some(intersection)
    }
}

動作確認

動作確認用に下記を用意しました。足りない、パッケージは、追加してください。

動作確認用main.rs
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, 3.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);

    let mut scene = Scene::new();
    scene.add(Box::new(Cylinder::new(
                Point3::new(1.5, 1.0, -1.0),
                Vector3::new(0.0, 0.25, -1.0),
                1.0,
                2.0,
            )));
    scene.add(Box::new(Cylinder::new(
                Point3::new(-1.5, 1.0, -1.0),
                Vector3::new(0.0, 1.0, 0.0),
                1.0,
                2.0,
            )));


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

            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/cylinder_test.ppm").unwrap();

    Ok(())
}

上記を実行して、下記が出力されればOKです。
今回は、底面を実装していないので、空洞であることの確認と、側面からの確認ですね!
お疲れ様でした。

さいごに

お疲れ様でした。次は、底面でも追加しようかと思います。
よろしくお願いいたします。