[Rust] レイトレーシング – 5

Rust

はじめに

お疲れ様です。最近、蒸し暑くなんだか嫌になってしまっています。窓を開けても無風、、、嫌になってしまいます。梅雨はどこに行ったのやら、今年の農業への影響がないか心配だと思う今日この頃です。

本題に入りますが、何か図形が表示される様にしていきたいと思います。今日の目標は下記の作成です。

/src
  └ /object
     ┝ Shape.rs
     ┝ Intersection.rs
     └ Plane.rs  

簡単な関係図は下記です。こんなイメージで作ろうかと思います。

PlantUML Syntax:</p>



<p>interface Shape {</p>



<p>+intersect(ray: &Ray, t_min: f64, t_max: f64)->Option<Intersection></p>



<p>}</p>



<p>class Intersection {</p>



<p>+t: f64</p>



<p>+point: Point3</p>



<p>+normal: Vector3</p>



<p>+front_face: bool</p>



<p>+set_face_normal(ray: &Ray, outward_normal: Vector3)</p>



<p>}</p>



<p>class Plane {</p>



<p>+point: Point3</p>



<p>+normal: Vector3</p>



<p>}</p>



<p>Shape <|.. Plane</p>



<p>Shape ..> Intersection</p>



<p>

IntersectionとShapeの実装

まず、Shapeですが交差点を求めるためのアルゴリズムを書いていく箇所の予定です。各図形で交差点を求めるアルゴリズムを求めたいので各々で実装する感じです。

pub trait Shape {
    fn intersect(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<Intersection>
}

Intersectionは、光線がどこで交差したのか?交差点はどこなのかを記録します。また、交差点の法線も記録することで表面に当たっているのか裏側から当たっているのかを算出します。

pub struct Intersection {
    pub t: f64,
    pub point: Point3,
    pub normal: Vector3,
    pub front_face: bool,
}

impl Intersection {
    pub fn set_face_normal(&mut self, ray: &Ray, outward_normal: Vector3) {
        self.front_face = ray.get_direction().dot(&outward_normal) < 0.0;
        self.normal = if self.front_face {
            outward_normal
        } else {
            -1.0 * outward_normal
        };
    }
}

Planeの実装

まず、基本の部分を書き出します。平面を表現するには、何があれば表現できるかを考えます。

上の図から、考えると最低限平面上の点を一つ面の法線があれば定義できます。これを元に平面のデータ構造を定義します。

#[derive(Debug)]
pub struct Plane {
    pub point: Point3, //平面上の点
    pub normal: Vector3, //法線
}

impl Plane {
    pub fn new(point: Point3, normal: Vector3) -> Self {
        Plane { point, normal: normal.unit() }
    }
}

まず、平面上に点があるとはどういうことか考えていいきます。
第一に、Rayがまず平面と並行の時は論外です。この条件はRayと法線の内積が0であるかを確認します。

$$ (\vec{Ray}, \vec{n}) = 0 $$

次に、交差点の計算をします。
Ray(t)が平面上にある時、点pから点Ray(t)のベクトルと法線ベクトルが垂直になるので下記が成立します。

$$(\vec{n}, \vec{Ray(t)} – \vec{p}) = 0$$$$(\vec{n}, \vec{o} + t * \vec{d} – \vec{p}) = 0$$$$(\vec{n}, \vec{o} – \vec{p}) + t * (\vec{n}, \vec{d}) = 0$$$$t = \frac{(\vec{n}, (\vec{p} – \vec{o}))}{(\vec{n}, \vec{d})}$$

これを元に実装を進めていく。0かどうかは、f64::EPSILONは数学的に正しいかもですが、丸め誤差的なものでうまく計算できない可能性があるかもしれないです。コンピュータの中では、完全な実数を扱えているわけではないので、どうしても誤差は出てしまうからです。
なので、下記の実装では、f64::EPSILONでしていますが、1e-8とかでも問題ないと思います。

impl Shape for Plane {
    fn intersect(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<Intersection> {
        let denom = ray.get_direction().dot(&self.normal);

        //Rayと平面がほぼ並行
        if denom.abs() < f64::EPSILON {
            return None;
        }

        //交差点までの距離の計算
        let v = self.point - ray.get_origin();
        let t = v.dot(&self.normal) / denom;

        // 距離が範囲外
        if t < t_min || t > t_max {
            return None;
        }

        // 交差点の座標
        let point = ray.at(t);

        // 法線向き
        let mut intersection = Intersection {
            t,
            point,
            normal: self.normal,
            front_face: false,
        };
        intersection.set_face_normal(ray, self.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, 1.0, 1.0);
    let look_at = Point3::new(0.0, 0.0, -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 plane = Plane::new(
        Point3::new(0.0, 0.0, 0.0), //平面上の点
        Vector3::new(0.0, 1.0, 0.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) =  plane.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/test.ppm").unwrap();

    Ok(())
}

上記の実装した上で、下記の出力を確認できればOKです。平面だけなので、面白みがないですが、一歩前進ですかね??また、着色は、法線をそのまま色に変換しているので、まだ自由はきかないですが、今後頑張っていきます。

おわりに

お疲れ様です。今日は、平面が追加される様にしました。次回は、ひとまず球を追加していくと思います。また、今のままでは一つの光線に複数の物体と交差した時、実装しづらいのでその部分も改善できたら良いかと思います。良い週末を!