blockの巣

Rustでコンパイル時レイトレーシング1

2022/01/21 22:28 公開
2022/01/20 11:24

コード修正 & レンダリング結果修正

2022/01/21 23:52

レンダリング結果追加

2022/02/16 01:07

レンダリング結果の上下が逆だったのを修正

Compile Time Ray Tracing in Rust Ray Tracing Rust

Rustでコンパイル時レイトレーシングはじめました。
リポジトリはGitHubで公開しています。
記事の内容はコミットID82481b974f2684a73a4940a85f92de74f9f05b7eのものです。

Ray Tracing in One Weekendの内容に合わせて実装していきます。
この記事では「2. Output an Image」のx,yによって色を変えて出力する部分までの解説を行います。
まだレイトレーシングは登場しません。

レンダリング結果
レンダリング結果

Ray Tracing in One Weekendとの変更点

元コードではPPMファイルを書き出していますが、環境によってはチェックしづらいのでpngで書き出すように変更しています。
また元コードはC++で書かれていますが、私はRustで書いています。

何をコンパイル時に行い何を実行時に行うか

「コンパイル時」とついていますがすべての処理をコンパイル時に行うわけではなく、 最終結果の各ピクセルの色の計算はコンパイル時に行い画像として出力する処理は実行時に行います。

使用している言語バージョン

後述するconst_fn_floating_point_arithmeticを有効にするためv1.59.0-nightly(Nightlyビルド)を使用しています。

コード解説

今回解説するコードは下記のものです。 リポジトリ最初のコミットを見ていただいても大丈夫です。

コード全文
#![feature(const_fn_floating_point_arithmetic)] // const fnの中で浮動小数点演算を行うために必要

use image::ImageFormat;

const IMAGE_WIDTH: usize = 256;
const IMAGE_HEIGHT: usize = 256;
const PIXEL_COUNT: usize = IMAGE_WIDTH * IMAGE_HEIGHT;

fn main() -> anyhow::Result<()> {
    const PIXEL_COLORS: [(u8, u8, u8); PIXEL_COUNT] = ray_trace();
    const BUFFER: [u8; PIXEL_COUNT * 3] = pixels_to_buffer(PIXEL_COLORS);
    let img = image::RgbImage::from_raw(IMAGE_WIDTH as u32, IMAGE_HEIGHT as u32, BUFFER.to_vec())
        .unwrap();
    img.save_with_format("./result.png", ImageFormat::Png)?;

    Ok(())
}

const fn ray_trace() -> [(u8, u8, u8); PIXEL_COUNT] {
    let mut pixel_colors = [(0, 0, 0); PIXEL_COUNT];
    let mut i = 0;
    let mut j = (IMAGE_HEIGHT - 1) as i32;
    let mut pixel_index = 0;
    // forが使えないのでwhileで代用
    while 0 <= j {
        while i < IMAGE_WIDTH {
            let r = (i as f64) / ((IMAGE_WIDTH - 1) as f64);
            let g = j as f64 / (IMAGE_HEIGHT - 1) as f64;
            let b = 0.25;
            let (r, g, b) = (
                (r * 255.999) as u8,
                (g * 255.999) as u8,
                (b * 255.999) as u8,
            );
            pixel_colors[pixel_index] = (r, g, b);
            i += 1;
            pixel_index += 1;
        }
        i = 0;
        j -= 1;
    }
    pixel_colors
}

const fn pixels_to_buffer(pixels: [(u8, u8, u8); PIXEL_COUNT]) -> [u8; PIXEL_COUNT * 3] {
    //let buf: Vec<_> = PIXEL_COLORS
    //    .iter()
    //    .map(|&(r, g, b)| [r, g, b])
    //    .flatten()
    //    .collect();
    //buf

    // 上と同じことをしている
    let mut buf = [0; PIXEL_COUNT * 3];
    let mut i = 0;
    while i < PIXEL_COUNT {
        buf[i * 3 + 0] = pixels[i].0;
        buf[i * 3 + 1] = pixels[i].1;
        buf[i * 3 + 2] = pixels[i].2;
        i += 1;
    }
    buf
}

解説

まず最初に書いてある#![feature(const_fn_floating_point_arithmetic)]ですが、コメントにあるようにコンパイル時にconst fnの中で浮動小数点演算を行うために必要になります。 これがないとconst fnの中で浮動小数の処理を行えないため有効にしています。


fn main() -> anyhow::Result<()> {
    const PIXEL_COLORS: [(u8, u8, u8); PIXEL_COUNT] = ray_trace();
    const BUFFER: [u8; PIXEL_COUNT * 3] = pixels_to_buffer(PIXEL_COLORS);
    let img = image::RgbImage::from_raw(IMAGE_WIDTH as u32, IMAGE_HEIGHT as u32, BUFFER.to_vec())
        .unwrap();
    img.save_with_format("./result.png", ImageFormat::Png)?;

    Ok(())
}

main()ではray_trace()の結果をimage crateで書き出す形に変換するところままでをコンパイル時に行い、実行時にはresult.pngというファイルを書き出します。


const fn ray_trace() -> [(u8, u8, u8); PIXEL_COUNT] {
    let mut pixel_colors = [(0, 0, 0); PIXEL_COUNT];
    let mut i = 0;
    let mut j = (IMAGE_HEIGHT - 1) as i32;
    let mut pixel_index = 0;
    // forが使えないのでwhileで代用
    while 0 <= j {
        while i < IMAGE_WIDTH {
            let r = (i as f64) / ((IMAGE_WIDTH - 1) as f64);
            let g = j as f64 / (IMAGE_HEIGHT - 1) as f64;
            let b = 0.25;
            let (r, g, b) = (
                (r * 255.999) as u8,
                (g * 255.999) as u8,
                (b * 255.999) as u8,
            );
            pixel_colors[pixel_index] = (r, g, b);
            i += 1;
            pixel_index += 1;
        }
        i = 0;
        j -= 1;
    }
    pixel_colors
}

ray_trace()関数では実際にレイを飛ばして各ピクセルの色の計算を行います。(今回はまだレイは飛ばしていません)
通常の関数(非const fn)と比べるとforが使えないためwhileで代用している部分が大きな変更点だと思います。
カウンタ用の変数を自分で定義/インクリメント/リセットしなければならないので面倒ですが、それさえ忘れなければforを使うのと大差はないです。


const fn pixels_to_buffer(pixels: [(u8, u8, u8); PIXEL_COUNT]) -> [u8; PIXEL_COUNT * 3] {
    //let buf: Vec<_> = PIXEL_COLORS
    //    .iter()
    //    .map(|&(r, g, b)| [r, g, b])
    //    .flatten()
    //    .collect();
    //buf

    // 上と同じことをしている
    let mut buf = [0; PIXEL_COUNT * 3];
    let mut i = 0;
    while i < PIXEL_COUNT {
        buf[i * 3 + 0] = pixels[i].0;
        buf[i * 3 + 1] = pixels[i].1;
        buf[i * 3 + 2] = pixels[i].2;
        i += 1;
    }
    buf
}

pixels_to_buffer()関数はピクセルの色の配列をimage::RgbImage::from_rawに渡すための形式に変換するための関数です。
const fnでiteratorを使用した処理が書けないのでwhileループで処理しています。

まとめ

今回はのx,yによって色を変えて出力する部分までの処理なのでレイトレーシングっぽいことは特にありませんでした。
ループ処理でforが使えない、iteratorを用いた処理が行えない、というあたりが注意しなければいけないところだと思います。