blockの巣

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

2022/04/07 13:03 公開
Compile Time Ray Tracing in Rust Ray Tracing Rust

前回記事
リポジトリ
記事の内容はコミットID80bd3b43d920d02c989dad6a7723400ab18e8bacのものです。

今回はアンチエイリアシング処理を追加しました。
1ピクセルのレンダリングのために乱数で少しずつ散らしたレイを100本飛ばしています。
手元の環境で前回まではコンパイルに2分かからなかったですが今回からコンパイル時間が10時間を超えるようになりました。

レンダリング結果
レンダリング結果
前回の結果(下の画像)と比べると球の周囲のジャギーが減っているのがわかると思います。
前回のレンダリング結果

この記事の内容を実装したときのRustのバージョンは1.61.0-nightly (3d6970d50 2022-02-28)です。

今回の変更点

今回の変更点は

です。

Camera構造体を追加

今まではカメラの位置やビューポートの情報はmain.rs内で変数として持っていましたが今回からはCamera構造体にまとめられています。
Cameraの変数にuvを渡すことで対応するレイを取得する関数があるのでmain.rsでレイを取得するためのコードが少しシンプルになりました。
newget_rayconst fnになっているくらいで参考元と大きな違いはありません。const fn特有の処理もないです。

use crate::{Point3, Ray, Vec3};

pub struct Camera {
    origin: Point3,
    lower_left_corner: Vec3,
    horizontal: Vec3,
    vertical: Vec3,
}

impl Camera {
    pub const fn new() -> Self {
        let aspect_ratio = 16.0 / 9.0;
        let viewport_height = 2.0;
        let viewport_width = aspect_ratio * viewport_height;
        let focal_length = 1.0;

        let origin = Point3::new(0.0, 0.0, 0.0);
        let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
        let vertical = Vec3::new(0.0, viewport_height, 0.0);
        let lower_left_corner =
            origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);
        Self {
            origin,
            horizontal,
            vertical,
            lower_left_corner,
        }
    }

    pub const fn get_ray(&self, u: f64, v: f64) -> Ray {
        Ray::new(
            &self.origin,
            &(self.lower_left_corner + u * self.horizontal + v * self.vertical - self.origin),
        )
    }
}

乱数生成の仕組みを追加

乱数生成の仕組みとしてXorshiftを追加しました。
こちらも各関数がconst fnになっていますが#![feature(const_mut_refs)]を有効にしているため実行時用の処理と大きな違いはありません。
最初はstableでも使用できるXorshiftを実装しようと思っていたのですがgen_f64の最終行の浮動小数点の減算がコンパイル時に解決できずに諦めました…
今回追加したXorshiftが生成する乱数はu32型の値です。そのためu64を取得する場合はu32を2回生成してそれぞれ上位ビットと下位ビットに挿入しています。 f64が欲しい場合はu64を生成した後にその値をシフト演算でずらしたものを仮数部ととして扱い、指数部と符号を適切な数値で埋めた後にf64に変換してやると1.0~2.0の乱数ができるのでそこから1.0を引くことで0.0~1.0の乱数を取得しています。

pub struct Xorshift {
    x: u32,
    y: u32,
    z: u32,
    w: u32,
}

impl Xorshift {
    pub const fn new(seed: u32) -> Self {
        Self {
            x: 123456789,
            y: 362436069,
            z: 521288629,
            w: seed,
        }
    }

    pub const fn gen_u32(&mut self) -> u32 {
        let t = self.x ^ (self.x << 11);
        let x = self.y;
        let y = self.z;
        let z = self.w;
        let w = (self.w ^ (self.w >> 19)) ^ (t ^ (t >> 8));
        *self = Self { x, y, z, w };
        w
    }

    pub const fn gen_u64(&mut self) -> u64 {
        let v1 = self.gen_u32();
        let v2 = self.gen_u32();
        let v = unsafe { std::mem::transmute::<[u32; 2], u64>([v1, v2]) };
        v
    }

    pub const fn gen_f64(&mut self) -> f64 {
        const A: u16 = 0x0001;
        const B: [u8; 2] = unsafe { std::mem::transmute::<u16, [u8; 2]>(A) };
        const IS_LE: bool = B[0] == 0x01;
        let v = self.gen_u64();
        let v = if IS_LE {
            let v = (v >> 12) | 0x3ff0000000000000;
            unsafe { std::mem::transmute::<u64, f64>(v) }
        } else {
            let v = [
                ((v >> 56) & 0xff) as u8,
                ((v >> 48) & 0xff) as u8,
                ((v >> 40) & 0xff) as u8,
                ((v >> 32) & 0xff) as u8,
                ((v >> 24) & 0xff) as u8,
                ((v >> 16) & 0xff) as u8,
                ((v >> 8) & 0xff) as u8,
                (v & 0xff) as u8,
            ];
            unsafe { std::mem::transmute::<[u8; 8], f64>(v) }
        };
        v - 1.0
    }
}

コードを読んで気づくかもしれませんがgen_f64は途中からリトルエンディアンとビッグエンディアンで処理が別れています。しかし手元にビッグエンディアンの環境がなかったのでチェックできていません。

アンチエイリアシング処理を追加

1ピクセルのレンダリングのために乱数で少しずつ散らしたレイを100本飛ばしその平均を取ることでアンチエイリアシング処理を行っています。
ray_trace関数の中でray_colorの返り値にlet scale = 1.0 / SAMPLES_PER_PIXEL as f64;をかけてやったものをSAMPLES_PER_PIXEL回行いそれらを合計することで少しずつずれたレイトレースの結果の平均を取っています。

/* 省略 */
const SAMPLES_PER_PIXEL: usize = 100;
/* 省略 */

const fn ray_trace() -> [Color; PIXEL_COUNT] {
    /* 省略 */

    let cam = Camera::new();

    let mut rng = Xorshift::new(include!("rand_seed.txt"));
    let mut pixel_colors = [Color::ZERO; PIXEL_COUNT];
    let mut i = 0;
    let mut j = (IMAGE_HEIGHT - 1) as i32;
    let mut pixel_index = 0; let scale = 1.0 / SAMPLES_PER_PIXEL as f64;
    // forが使えないのでwhileで代用
    while 0 <= j {
        while i < IMAGE_WIDTH {
            let mut s = 0;
            let mut pixel_color = Color::ZERO;
            while s < SAMPLES_PER_PIXEL {
                let u = (i as f64 + rng.gen_f64()) / (IMAGE_WIDTH - 1) as f64;
                let v = (j as f64 + rng.gen_f64()) / (IMAGE_HEIGHT - 1) as f64;
                let r = cam.get_ray(u, v);
                pixel_color = pixel_color + ray_color(&r, &world) * scale;
                s += 1;
            }
            pixel_colors[pixel_index] = pixel_color;
            i += 1;
            pixel_index += 1;
        }
        i = 0;
        j -= 1;
    }
    pixel_colors
}

Xorshiftのseed値について

今回追加したXorshiftXorshift::new(seed)のようにシード値を渡すことができるようになっています。
上記のコードではXorshift::new(include!("rand_seed.txt"))rand_seed.txtというテキストファイルの中身をそのまま渡しており、このテキストファイルの中身はu32型の整数値が入っています。
rand_seed.txtの中身を自分で決めたくなかったので下記のようなビルドスクリプトを用意してコンパイル時に生成するようにしています。値は現在時刻を元に決めています。

use anyhow::Result;

fn main() -> Result<()> {
    use std::fs::File;
    use std::io::Write as _;
    use std::time::SystemTime;

    let n = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
    let seed = (n.as_secs() & 0xffffffff) as u32;

    let mut file = File::options()
        .write(true)
        .append(false)
        .create(true)
        .open("./src/rand_seed.txt")?;
    write!(file, "{}", seed)?;

    Ok(())
}

あとがき

Xorshiftがstable Rustのconst fnで使えるものにできなかったのが少し悔しいです。整数型限定ならできるのですが…

手元の環境でコンパイル時間が10時間を超えるようになり「コンパイル時レイトレーシング感」が増してきました。 そろそろ分割してコンパイルすることを検討する必要がありそうです。