これは KLab Advent Calendar 2016 の13日目の記事です。
はじめに
Rust で簡単なレイトレーシングのプログラムを作成しました。
レイトレーシングとは、光源から発する光線の挙動をシミュレーションすることによって画像の各画素における色を計算し、写実的な画像を描画することができる技術のことです。光は本来電球などの光源から発し私たちの目に届きますが、光源から発する光線はその多くが視界には入らないため、視界から逆向き(backward)に光線を発し、各画素の色を計算するのがレイトレーシングの普通のやり方です。
今回レイトレのプログラムを作成してみようと考えたきっかけとしては、社内の一部でレイトレが流行っていることや、自分がCGの技術にかなり疎かったことなどがあります。また、Rust の LT 会 に参加し Rust の機運が高まっていたため、実装は Rust でやることにしました。
開発
プログラムを作成する上で、以下の筑波大学の授業資料を参考にしました。
また、gam0022さんにいろいろとアドバイスをいただきました。ありがとうございます。
今回の実装で対応している図形は球と平面、反射は拡散反射と鏡面反射(減衰はなし)のみです。光源に関しては点光源と平行光源に対応していますが、比率などはプログラム中にべた書きしています。また、点光源は見栄えの良いように減衰します。
性能
球が8個、平面が5つの以下の画像を出力するのにデバッグビルドで 3.4秒、リリースビルドで 0.55 秒かかりました(再帰の深さは7)。鏡面反射の物体が増えるとそれだけ時間がかかります。
Rust について
Rust はまったく初心者だったのですが、OCaml の経験があったせいで普通の関数型言語(に trait などの機能を付け加えたもの)と思って開発を進めてしまいました。概ね違和感なく書けてしまい、しかも borrow checker や lifetime などに足を踏み入れることもなかったため、やや消化不良気味です。3次元ベクトル構造体での演算子のオーバーロードや、物体に対する trait の実装などができたのが、Rust の機能を活用していると言えるかもしれません。
trait について
trait関連で1点はまったことがありました。まず、光線を与えたときに、その光線と衝突するかどうかを返す hit 関数を要求する Hit トレイトを作成し、球と平面それぞれで実装しています。これを、環境中で光線を飛ばし一番最初に衝突する物体を探す処理(ここらへん)で使えると思ったのですが、Rust では closure で generics が使えない?らしく、半端な抽象化しかできませんでした。
該当の処理は以下のようになっています。HitRecord は光線が衝突したときの情報を保持する構造体で、衝突しなかった場合は None を返すため Option です。
let take_min =
|a: Option<obj::HitRecord>,
b: Option<obj::HitRecord>| match (a, b) {
(Some(a), Some(b)) => if a.t < b.t { Some(a) } else { Some(b) },
(Some(a), None) => Some(a),
(None , Some(b)) => Some(b),
(None , None) => None
};
let hit = self.spheres.iter().fold(
None,
|acc, sphere| take_min(acc, sphere.hit(ray))
);
let hit = self.planes.iter().fold(
hit,
|acc, plane| take_min(acc, plane.hit(ray))
);
これを、次のように書きたかったのでした。
let update = |a, b: <T: Hit>| match (a, b.hit(ray)) {
// take_min 相当の処理
}
let hit = self.spheres.iter().fold(None, update);
let hit = self.planes.iter().fold(hit, update);
update は closure かつ第2引数が Hit を実装していることを要求したいため generics になっていますが、これが書けない・・・
PRらしきもの はありますが、そのうち入るのでしょうか?
imgcat
画像を出力するレイトレのようなプログラムを開発する際は、プログラムを修正するたびに結果を確認する必要があり、簡単にそれができると便利です。mac の iTerm2 ではターミナルに画像を表示する機能があるため、今回はこれを利用しました。具体的には、開発中は次のコマンドでプログラムの出力を確認していました。
$ cargo run 2>/dev/null | convert - png:- | imgcat
プログラム自体は PGM 形式で画像を出力するようにしたため、それを一旦 png に変換してから imgcat にパイプで渡しています。
ところで、imgcat はどうも tmux と相性が悪いようで、そのままだとうまく表示されません。以下のURLを参考にして、tmux 上でも画像が表示されるように imgcat を修正して使っていました。
最初に空行を10行出力し、10行戻った後10行分のサイズの画像を出力して、さらに10行進むとうまく表示される(謎):
https://gitlab.com/gnachman/iterm2/issues/3898#note_14097715
画像のサイズを取得するようにしたバージョン:
https://gist.github.com/krtx/533d33d6cc49ecbbb8fab0ae871059ec
ただし、上記の工夫をしてもコピーモードに入ったりすると画像の表示が消えてしまいます。なので、以前の出力との比較がそんなに簡単にはできず、画像をドラッグして画像を保存したりする必要があります。いろいろ面倒なので、ターミナル内に画像を表示させたかったら tmux を諦めるのが良いのかもしれません。
感想
考えてみれば、幾何学的な物体というのはプログラムを学習するときによく出てくる課題なのではないでしょうか。円や正方形を定義し、それらの面積を求めるメソッドを作ったりというのは、いかにも入門として扱われそうな題材だと思います。あるいは、今回作成した3次元ベクトル構造体というのも、演算子をオーバーロードする例として何度も実装されたものでしょう。幾何学的な物体は抽象的な対象であるためにプログラムの上で扱いやすいのだと思います。レイトレーシングはそのような抽象的な物体を扱いながら、写実的でわかりやすい結果が得られるという点で優れたエクササイズだと言えるのではないでしょうか。画像処理といっても PBM 形式のような単純なフォーマットであればただ print するだけで済みますし、サーバなんかも必要ではないのでスタンドアロンで簡潔する手軽さがあります。
色、屈折、アンチエイリアシングなど今回できなかった話題はいくつもあります。個人的には部屋に鏡面反射の球と光が屈折するガラス玉がある画像(cornell box というのでしょうか?)が好きなので、屈折にはぜひとも挑戦してみたいです。