Rustでコンパイル時レイトレーシング7
2022/04/07 13:03 | 公開 |
前回記事
リポジトリ
記事の内容はコミットID80bd3b43d920d02c989dad6a7723400ab18e8bac
のものです。
今回はアンチエイリアシング処理を追加しました。
1ピクセルのレンダリングのために乱数で少しずつ散らしたレイを100本飛ばしています。
手元の環境で前回まではコンパイルに2分かからなかったですが今回からコンパイル時間が10時間を超えるようになりました。
レンダリング結果
前回の結果(下の画像)と比べると球の周囲のジャギーが減っているのがわかると思います。
この記事の内容を実装したときのRustのバージョンは1.61.0-nightly (3d6970d50 2022-02-28)
です。
今回の変更点
今回の変更点は
Camera
構造体を追加- 乱数生成の仕組みを追加
- アンチエイリアシング処理を追加
です。
Camera構造体を追加
今まではカメラの位置やビューポートの情報はmain.rs
内で変数として持っていましたが今回からはCamera
構造体にまとめられています。
Camera
の変数にuvを渡すことで対応するレイを取得する関数があるのでmain.rs
でレイを取得するためのコードが少しシンプルになりました。
new
やget_ray
がconst 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値について
今回追加したXorshift
はXorshift::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時間を超えるようになり「コンパイル時レイトレーシング感」が増してきました。 そろそろ分割してコンパイルすることを検討する必要がありそうです。