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

はじめに

今日は、カメラモデルを作っていきたいです。よろしくお願いします。

今日は下記の部分を作っていきます。

src
┝ camera
│ ┝ camera_model.rs
│ ┝ film.rs
│ └ camera.rs
└ data
   └ ray.rs

光線の定義

カメラから光線を飛ばしてシミュレートするので、まず初めに光線の定義をしていきます。

Ray(t) = 始点 + t * 方向

で定義します。
イメージは左の図のような感じです。
これを元に簡単に作ります。

// ray.rs
use crate::util::vector::vector3::{Vector3, Point3};

#[derive(Debug)]
pub struct Ray {
    origin: Point3,
    direction: Vector3,
}

impl Ray {
    pub fn new(origin: Point3, direction: Vector3) -> Self {
        Ray {origin, direction}
    }

    pub fn at(&self, t: f64) -> Point3 {
        self.origin + t * self.direction
    }

    pub fn get_direction(&self) -> Vector3 {
        self.direction
    }
 
    pub fn get_origin(&self) -> Point3 {
        self.origin
    }
}

at関数を今後よく使います。

Filmの実装

CameraModelを実装してみると結構パラメータが多くなって見通しが悪くなるんですよね。なので、少しデータをまとめたいなと思ってこれを作ります。get~ を作っていますが、だるい時はstrcutを定義する際にpub heightのようにすれば、直で取得できます。ここら辺は好みでしょうか、、

また、今回のFilmは現実のカメラで言うフィルムやデジタルセンサーに相当する役割を意識して実装します。カメラから発射した光線が3D空間を行き来し集めた光の情報を、最終的に2次元の出力(画像)に落とし込む役割を担っています。

次になるとは思いますが、Filmを利用して画像保存traitを作ると思います。

// film.rs
use crate::util::color::color::ColorSystem;
use crate::util::color::rgb_color::Rgb;

/// Filmのデータ構造
#[derive(Debug)]
pub struct Film {
    color_system: ColorSystem,
    height: usize,
    width: usize,
    aspect_ratio: f64,
    inverse_height: f64,
    inverse_width: f64,
    pixels: Vec<Rgb>,
}

impl Film {
    #[allow(dead_code)]
    pub fn new(height: usize, width: usize) -> Self {
        let aspect_ratio = (width as f64) / (height as f64);

        Film {
            width,
            height,
            aspect_ratio,
            inverse_width: 1.0 / (width as f64),
            inverse_height: 1.0 / (height as f64),
            color_system: ColorSystem::None,
            pixels: vec![Rgb::zero(); width*height],
        }
    }
    pub fn set_pixel(&mut self, x: usize, y: usize, color: Rgb) {
        if x >= self.width || y >= self.height {
            return;
        }
        let index = y*self.height + x;
        self.pixels[index] = self.pixels[index] + color;
    }
    pub fn set_color_system(&mut self, color_system: ColorSystem) {
        self.color_system = color_system
    }
    pub fn get_height(&self) -> usize {
        self.height
    }
    pub fn get_width(&self) -> usize {
        self.width
    }
    pub fn get_inverse_height(&self) -> f64 {
        self.inverse_height
    }
    pub fn get_inverse_width(&self) -> f64 {
        self.inverse_width
    }
    pub fn get_aspect_ratio(&self) -> f64 {
        self.aspect_ratio
    }
    pub fn get_color_system(&self) -> ColorSystem {
        self.color_system
    }
    pub fn get_pixcel(&self) -> &Vec<Rgb> {
        &self.pixels
    }
}

実装はひとまずこんな感じでしようと思いますが、Rgbと特定の色空間ですが、将来的には汎用的なものにしたいですね。XYZLabなど他にも色空間がるので、できたらいいなぁと思っています。

CameraModelの実装

CameraModelのトレイトを作ります。画像を出力する上でどのようにシーンを見るかを定義するために必要な重要な要素となります。現実世界と同じように、位置、向き、画角、被写体深度、、、などのパラメータを持ち3D空間内の視点を表現していきます。
CameraModelトレイトは位置情報などからえられるものからさらに拡張させ情報を得るためのアルゴリズムをまとめます。例えば、レイの生成方法やFilmのpixcelの状態などです。

これを作っておくことで、ピンホールカメラや魚眼レンズ、パノラマカメラなど様々なカメラのタイプを同じインターフェースを用いて実装が可能になります。

// camera_model.rs
use crate::data::ray::Ray;
use crate::util::vector::vector3::{Vector3, Point3};
use crate::util::color::rgb_color::Rgb;
use super::film::Film;

/// Camera Model
pub trait CameraModel {
    fn generate_ray(&self, u: f64, v: f64) -> Ray;
    fn record_pixel(&mut self, x: usize, y: usize, color: Rgb);
    fn get_position(&self) -> Point3;
    fn get_look_at(&self) -> Point3;
    fn get_direction(&self) -> Vector3;
    fn get_film(&self) -> &Film;
}

Cameraの実装

今回は薄レンズカメラっぽいのを作ります。被写体深度やボケ効果はちょっと面倒なので、簡易化して実装します。時間のある時に実装を追加するかもしれないです。


$$\vec{forward} = \vec{look\_at} – \vec{origin}$$
$$\vec{right} = \vec{forward} \times \vec{up}$$
$$\vec{up\_dir} = \vec{right} \times \vec{forward}$$

左の図をイメージしつつ考えていきます。
positionからlook_atを見るイメージです。
upは上をどこに据えるかという認識です。

画像として出力されるものは左の図の四角部分をイメージしながら考えるといいと思います。四角い部分に対してピクセルに相当とする部分を計算する必要があるのですが、これを計算する際にrightベクトルとup_dirベクトルがあると計算がしやすくなります。

これを計算するには、外積が役に立ちます。
実際に計算してみると下記です。

\(\vec{forward}\)を事前計算しておきます。
これは、originlook_atの向きのベクトルです。
これと\(\vec{up}\)の外積から\(\vec{right}\)を左のように計算できます。
また、\(\vec{up\_dir}\)も同様に計算できます。

上記をもとに少し実装を進めます。上記の計算では、単位ベクトルにしていませんでしたが、実際に実装する際は、単位ベクトルにしておくと計算が楽です。

// camera.rs
use super::camera_model::CameraModel;
use super::film::Film;
use crate::data::ray::Ray;
use crate::util::vector::vector3::{Point3, Vector3};
use crate::util::color::rgb_color::Rgb;

#[derive(Debug)]
pub struct Camera {
    position: Point3,
    look_at: Point3,
    fov: f64,
    film: Film,
    forward: Vector3,
    right: Vector3,
    up_dir: Vector3,
}

impl Camera {
    pub fn new(position: Point3, look_at: Point3, up: Vector3, fov: f64, film: Film) -> Camera {
        let forward = (look_at - position).unit();
        let right = forward.cross(&up).unit();
        let up_dir = right.cross(&forward).unit();

        Camera {
            position,
            look_at,
            fov,
            film,
            forward,
            right,
            up_dir,
        }
    }
}

次に、uvの部分に向かう光線を作る部分を作っていきます。この光線を求めるために、左上を基準に計算できると計算しやすいです。そのため、まず左上をまず求めてしまいます。

左上:\(\vec{forward} \\
– half\_width * \vec{right} \\
+ half\_height * \vec{up\_dir}\)

で求めることができます。

uvを加味して実装をすると下記です。

impl CameraModel for Camera {
    fn generate_ray(&self, u: f64, v: f64) -> Ray {
        let half_width = (self.fov / 2.0).tan();
        let half_height = half_width / self.film.get_aspect_ratio();

        let film_x = (2.0 * u - 1.0) * half_width;
        let film_y = (1.0 - 2.0 * v) * half_height;

        let direction = (self.forward + film_x * self.right + film_y * self.up_dir).unit();

        Ray::new(
            self.position,
            direction,
        )
    }
    fn record_pixel(&mut self, x: usize, y: usize, color: Rgb) {
        self.film.set_pixel(x, y, color);
    }
    fn get_position(&self) -> Point3 {
        self.position
    }
    fn get_look_at(&self) -> Point3 {
        self.look_at
    }
    fn get_direction(&self) -> Vector3 {
        self.forward
    }
    fn get_film(&self) -> &Film {
        &self.film
    }
}

動作確認しようかと思いましたが、次の回で画像出力部分作ろうかと思っているのでそこでいっぺんにやろうかと思います。

さいごに

結構手探りで実装している感が半端ないです、、、