Intersecting Rays with Spheres

Chapter5

github.com

Ray

始点と方向のみ。

pub struct Ray {
    origin: Point3D,
    direction: Vector3D,
}

始点が同じ Ray を何度も作成することが頻繁にあるから、direction はともかく origin は reference にした方が良かったかも。必要なら後で変える。

Ray 上の任意の点  \vec{p} は媒介変数  t を用いて  \vec{p} = t\vec{direction} + \vec{origin} で表される。以降簡便のため  \vec{direction} \vec{d} \vec{origin} \vec{o} として扱う。

Sphere

ローカル座標において常に原点中心で半径 1 の単位球であるとする。これだけだと色々な位置に様々な大きさの球を配置することができない。とはいえ、ワールド座標でそれができれば良いのであるから、ワールド座標上での球は Transform を用いて表現する。つまり、ローカル座標 -> ワールド座標の変換行列を Sphere に持たせる。その変換で位置や大きさを決める。

pub struct Sphere {
    /// 球に対して適用する変換
    transform: Transform,
}

Intersecting Rays with Spheres

Intersection の計算は球のローカル座標で行う。こうすることで、原点中心の単位球との Intersection のみを考えれば良くなる。その際、Ray を Sphere のローカル座標で表現するために ワールド -> ローカル の変換、つまり Sphere が持つ transform の inverse を適用する。

原点に位置する単位球は [tex: x2 + y2 + z2 - 1 = 0] で表される。これに上記 Ray を代入すると


p_x^2 + p_y^2 + p_z^2 -1  = \\
(t d_x + o_x)^2 + (t d_y + o_y)^2 + (t d_z + o_z)^2 - 1 = \\
\vec{d}^{2}t^2 + \vec{o}^2 + 2(d_{x}o_{x} + d_{y}o_{y} + d_{z}o_{z})t -1 = \\
\vec{d}^{2}t^2 + 2\vec{d}\cdot\vec{o}t + \vec{o}^2 - 1 = 0

となり、 a = \vec{d}^{2} b = 2\vec{d}\cdot\vec{o} および  c = \vec{o}^2 - 1 とおけば [tex: ax2 + bx + c = 0] という二次方程式になる。なので解の公式がそのまま使え、

        let r = self.transform.inv() * ray;
        let o = r.origin();
        let d = r.direction();
        let sphere_to_ray = o - &Point3D::ZERO;

        let a = d.dot(&d);
        let b = 2.0 * d.dot(&sphere_to_ray);
        let c = sphere_to_ray.dot(&sphere_to_ray) - 1.0;

        let discriminant = b * b - 4.0 * a * c;
        if discriminant < 0.0 {
            // no intersection
            return vec![];
        }

        let t1 = (-b - discriminant.sqrt()) / (2.0 * a);
        let t2 = (-b + discriminant.sqrt()) / (2.0 * a);

で Ray と Sphere の交点を求めることができる。なお、sphere_to_ray は球の中心から Ray の始点方向へのベクトルを表している。単位球の中心が原点にあるなら、これは常に  \vec{o} に等しい。

Rendering

今のところ Camera が存在していないため、Rendering の処理において簡易的な Camera を実現する。必要な情報は以下の通り。

  • 出力する画像の大きさ(pixel 単位  I_w * tex: I_h)
  • 撮影する平面(ワールド座標における大きさ  w * h)
  • 撮影する平面までの距離

またCamera はワールド座標 (0, 0, -5) 位置し、z 軸正方向を向いているとする。

目的とする画像を得るには各 pixel が何色であるかを知らなければならない。そのため、各 pixel に対応する Ray を生成する。

上記を図示したのが以下。

f:id:mtXTJocj:20201114210222p:plain

ここで  I_w w,  I_y y, が対応しているとわかる。よって、x 方向は 1 pixel あたりの大きさはワールド座標系において  w / I_w になり、同様に y 方向は  h / I_h になる。今回は出力画像サイズを縦横ともに 100 pixel とし、撮影する平面は縦横ともに 7.0 とした。

    let wall_size = 7.0;
    let canvas_pixels: usize = 100;
    let size_per_pixel = wall_size / canvas_pixels as f32;

よって pixel 単位でループする際に size_per_pixel ずつ変化させる

    for y in 0..canvas.height() {
        let world_y = half - (size_per_pixel * y as f32);

        for x in 0..canvas.width() {
            let world_x = -half + (size_per_pixel * x as f32);    

これと撮影する平面までの距離を合わせればワールド座標系での 1 点が決まり、カメラ位置とその点を結ぶ線が求める Ray の方向になる。

            let mut direction = &position - &ray_origin;
            direction.normalize();

            let r = Ray::new(ray_origin.clone(), direction); 

こうして各 pixel に対して生成した Ray と Sphere とで交差判定を行い、交差する場合(赤)としない場合(黒)とで異なる色を出力すると以下のような画像が得られる。

f:id:mtXTJocj:20201114202025p:plain

まだ全然 3D らしくないネ。

f:id:mtXTJocj:20200118114540p:plain