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

今日の成果物 Rust

はじめに

お疲れ様です。いかがお過ごしでしょうか?最近雨も多くジメジメしていて、夏の到来を感じるなとしみじみ思っているところです。季節な変わり目は、体調崩しやすいので気をつけていきたいです。

前回は、カメラモデルの作成までを実施しました。今日は、画像出力部分までやれたらと思っています。具体的な作業ボリュームは下記の箇所を作っていきます。

src
└ image_output
   └ image_output.rs
   ┝ image_output_factory.rs
   └ ppm_output.rs

実装イメージは、image_outputで共通の部分を作成します。今回は、ppmだけですが、pngやjpegなど似たような実装で実装するための前準備です。image_output_factoryは、保存したい画像の種類をハンドリングするためのプログラムを組みます。今日は、ppmだけですが、png, jepgは挑戦したい人は挑戦してみると良いかもです。

ImageOutputの実装

こちらの役割は、レンダリングで得た出力を画像にどうやって保存するかをまとめるのが目的なので、主な機能は保存です。他に機能が必要になれば、適宜追加することにします。

use crate::camera::film::Film;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ImageType {
    PPM,
    PNG, // 今日は実装しない
    JPEG, // 今日は実装しない
}

pub trait ImageOutput {
    // 保存をここで
    fn save(&self, film: &Film, path: &str) -> Result<(), String>;
  // 念の為画像タイプを返すやつ
    fn get_type(&self) -> ImageType;
}

実際に中身を充実させるのは、PpmOutputの実装の方でやっていきます。この部分は、ひとまずこんな機能を実装するぞという設計書の役割をイメージするとわかりやすいかもしれないです。

PpmOutputの実装

今日のメインテーマです。画像の出力部分です!。その前に、PNGJPEGなどはよく聞きますが、ppmってなんぞやって感じだと思います。
一言で言うと、PPMの特徴は「圧縮をしない」ことです。画像をそのままのデータで扱うためシンプルで扱いやすく綺麗な画像になります。その一方でファイルサイズが大きくなってしまうという欠点があります。
一方で、PNGJPEGは圧縮してファイルサイズを小さくしている他、写真の共有やWebサイトを軽くすることには向いているが、画質が落ちてしまうこともあります。PNGは可逆圧縮方式とか使っていたと思うので画質はJPEGよりは良かったはずです。

今回は、シンプルにPPMを利用しようと思います。Windowsの方は、もしかするとPreviewで見れないかもしれないです、、、

ppmのファイル形式について補足です。


ppmの中身は下記で構成されています。
1行目:ppmのデータの形式(P3だとRGB形式、P6とかも使えそう)
2行目:横幅と高さ
3行目:色を表現する際の最大値
4行目以降:色〜

P3
400 400
255
0 0 0
0 0 255

use crate::camera::film::Film;
use super::image_output::ImageOutput;
use super::image_output::ImageType;

pub struct PpmOutput;

impl ImageOutput for PpmOutput {
    fn save(&self, film: &Film, path: &str) -> Result<(), String> {
        // ファイルの作成
        let mut file = match std::fs::File::create(path) {
            Ok(file) => file,
            Err(e) => return Err(format!("Failed to create file: {}", e)),
        };
    // header部分の追加
        let header = format!("P3\n{} {}\n255\n", film.get_width(), film.get_height());
        if let Err(e) = std::io::Write::write_all(&mut file, header.as_bytes()) {
            return Err(format!("Failed to write header: {}", e));
        }
    // ピクセルデータの挿入
        for color in film.get_pixcel() {
            let r = (color.get_r().clamp(0.0, 1.0) * 255.0).round() as u8;
            let g = (color.get_g().clamp(0.0, 1.0) * 255.0).round() as u8;
            let b = (color.get_b().clamp(0.0, 1.0) * 255.0).round() as u8;

            let pixel_str = format!("{} {} {}\n", r, g, b);
            if let Err(e) = std::io::Write::write_all(&mut file, pixel_str.as_bytes()) {
                return Err(format!("Failed to write pixel data: {}", e));
            }
        }

        Ok(())
    }

    fn get_type(&self) -> ImageType {
        ImageType::PPM
    }
}

よし、残るは、動作確認ダァ。

ImageOutputFactoryの実装

その前に、ハンドリングするやつを作ると宣言していたのを忘れていたので、作ってしまいます。

use super::{
    image_output::{ImageOutput, ImageType},
    ppm_output::PpmOutput,
};

pub struct ImageOutputFactory;

impl ImageOutputFactory {
    pub fn create(image_type: ImageType) -> Box<dyn ImageOutput> {
        match image_type {
            ImageType::PPM => Box::new(PpmOutput),
      _ => panic!("Oops! I can't handle this imageType..."),
        }
    }
}

Rustでmatchする時、網羅していないと怒られますのでお気をつけください。逆に網羅しているのに_があるとそれはそれで怒られます。

動作確認

main.rsで下記を追加して、実行します。

use std::error::Error;
use crate::camera::camera_model::CameraModel;
use crate::image_output::image_output::ImageOutput;
use crate::image_output::ppm_output::PpmOutput;
use crate::util::color::rgb_color::Rgb;

use crate::camera::film::Film;
use crate::camera::camera::Camera;
use crate::util::vector::vector3::{Point3, Vector3};

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

  // Rayを作成しつつピクセルごとの値を記録する
    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 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);

            let 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(())
}

下記の様な画像が出力されればひとまずオッケーです。お疲れ様です。
PPM形式だとアップロードできなかったので、PNGにエキスポートしました、、、

おわり

今日もお疲れ様でした。
次からは、平面とか球とか表示できたらいいなと思っています。
ベクトルの計算式と2時方程式の判別式の復習しとかないと、、、