これはレイトレ合宿6の参加報告の記事の前編です。 記事が長くなったので、前編と後編の2つに分けました。
この記事では前編(準備編)ということで、自作レンダラーに実装した機能や手法の紹介を行います。
後編(当日編)はこちらです。
9月1日(土)~9月2日(日)に神津島で開催されたレイトレ合宿6に参加しました。
レイトレ合宿は完全自作のレイトレーサーを走らせて画像の美しさを競うイベントです。
参加者の中には、Arnold RendererやRadeon ProRenderといった商用のレンダラーの開発者、 SIGGRAPH 2017で発表された研究者など、グラフィック分野の最先端で活躍されている方々もいらっしゃり、大変刺激を受けました。
私は今年で3回目の参加になります。過去の参加報告はこちらです。
Rustで開発したパストレーシングによる自作の物理ベースレンダラー(Hanamaru Renderer)をバージョンアップして、 こんな感じの画像を123秒でレンダリングしました。
今回は19人中10位だったので、ギリギリ入賞圏内に潜り込めました!
↑リンクをクリックするとオリジナルの可逆圧縮の画像になります。
ソースコードはGitHubに公開しています(スターください)。
こちらは合宿当日のプレゼン資料です。
去年やったこと
レンダラーとしての基本機能は去年のレイトレ合宿5の時点で完成していました。
- パストレーシング(BSDFによる重点的サンプリングあり)
- オブジェクトとして Sphere, Polygon Mesh, AABB に対応
- BVHによる衝突判定の高速化
- マテリアル
- 完全拡散反射
- 完全鏡面反射
- 金属面(GGXの法線分布モデル)
- ガラス面(GGXの法線分布モデル)
- Image Based Lighting(IBL)
- テクスチャによる albedo / roughness / emission の指定
- 薄レンズモデルによる被写界深度(レンズのピンぼけ)
今年は足りない機能・個人的に実装したかった機能を付け足す形で実装を行いました。
今年やったこと
最終的に以下のような機能の実装や作業を行いました。
- Rust環境の最新化
- Next Event Estimation(NEE)の実装
- 処理のリファクタリング
- トーンマッピングの実装
- デノイズの実装
- Houdiniによるシーン作成
- 各種トラブルシューティング
それぞれについて、簡単に手法の紹介を交えつつ説明していきます。
Rust環境の最新化
Rustは新しい言語だけあって、取り巻く環境の進化も非常に速い印象です。
一年前は最新の環境でしたが、すっかり古くなってしまったので、Rust環境の最新化を行いました。
Rustコンパイラのバージョンアップ
初手としてRustのコンパイラを最新化したのは大正解でした。
Rustのバージョンを上げただけで、自作レンダラーの速度が3.3倍速になってすごい 😆
— がむ (@gam0022) 2018年8月19日
cargo 0.20.0 (a60d185c8 2017-07-13): sampled: 9x4 spp.
cargo 1.28.0 (96a2c7d16 2018-07-13): sampled: 30x4 spp.
Rustのコンパイラを最新化したところ、コードをまったく書き換えずに3倍速になりました! メジャーバージョンを上げると劇的にパフォーマンスが変わることがあるのですね! SIMDの最適化などが賢くなったのかなぁという気はしていますが、どういった理由で高速化できたのか詳しい調査はできていません。
Rustを最新の安定バージョンに上げる手順は非常に簡単でしたので、備忘録をかねて紹介します。
# 最新の安定バージョンに上げる
$ rustup update
# バージョンの確認
$ rustc -V
依存ライブラリのバージョンアップ
Rustの高速化に味を占めて依存ライブラリも最新化したのですが、特に速度は変化ありませんでした。
ライブラリをバージョンアップする手順も簡単にメモしておきます。 ここでは Rayon という並列化のライブラリのバージョンを上げる例を紹介します。
まず Cargo.toml
を編集します。
-rayon = "0.8.2"
+rayon = "1.0"
そして次のコマンドを叩くと、 Cargo.toml
で指定された中で最新のバージョンに依存ライブラリがアップデートされます。
$ cargo update
cargo-outdated
依存ライブラリの最新バージョンを調べるときに cargo-outdated
というツールが役に立ちました。
次のコマンドからインストールできます。
$ cargo install cargo-outdated
cargo outdated
を実行すると、
- Project: 現在のバージョン
- Compat: 現在の
Cargo.toml
のままでcargo update
からインストール可能なバージョン - Latest: 最新バージョン
が一発で分かります。
$ cargo outdated
Name Project Compat Latest Kind Platform
---- ------- ------ ------ ---- --------
fuchsia-zircon->bitflags 1.0.4 --- Removed Normal ---
fuchsia-zircon->fuchsia-zircon-sys 0.3.3 --- Removed Normal ---
rand 0.3.22 --- 0.5.5 Normal ---
rand->fuchsia-zircon 0.3.3 --- Removed Normal cfg(target_os = "fuchsia")
rand->libc 0.2.43 --- Removed Normal cfg(unix)
rand->rand 0.4.3 --- Removed Normal ---
rand->winapi 0.3.5 --- Removed Normal cfg(windows)
winapi->winapi-i686-pc-windows-gnu 0.4.0 --- Removed Normal i686-pc-windows-gnu
winapi->winapi-x86_64-pc-windows-gnu 0.4.0 --- Removed Normal x86_64-pc-windows-gnu
IntelliJ IDEA
去年の参加報告にも書きましたが、IntelliJ IDEAに次のプラグインを入れるとRustの神IDEが完成します。
IntelliJ IDEAのバージョンも 2017.2.3 -> 2018.2.1
に上げました。
Next Event Estimation(NEE)の実装
Next Event Estimation(NEE)と呼ばれるパストレーシングのサンプリングを効率化する手法を実装しました。
光源が小さいシーンの場合、BSDFによる重点的サンプリングだけではなかなか光源にヒットしません。 レイトレ合宿のように制限時間が短い場合はノイズだらけの結果になってしまいます。
そこで、光源の表面上の点を明示的にサンプリングして光転送経路を生成します。これがNEEです。
同じサンプリング数でNEE実装前とNEE実装後の結果を比較しました。ノイズを劇的に軽減できました!
NEEの理論と実装についての詳細については、Shockerさん、Pocolさんの資料を参考にさせていただきました。
- パストレーシング - Computer Graphics - memoRANDOM
- パストレーシング / Path Tracing - Speaker Deck
- レイトレ再入門 – ☆PROJECT ASURA☆
Sphereの光源をNEEに対応させるために必要な「球面上に一様分布した点を選ぶ処理」は次の資料の「2.4 単位球面に一様分布する点」を参考にさせていただきました。
極座標ではなく $(z, \phi)$ で球面を表現するとシンプルに計算できます。
$$ 0 \le z \le 1, \quad 0 \le \phi \le 2 \pi $$
$$ x = \sqrt{1 - z^2}cos \phi $$
$$
y = \sqrt{1 - z^2}sin \phi
$$
$$ z = z $$
処理のリファクタリング
マテリアル側の次のような関数を持たせるようにリファクタリングしました。
sample()
- サンプリング方向 +
bsdf * cos / pdf
を返す関数 - 重点的サンプリングを行うと
bsdf * cos
をpdf
が打ち消すケースが多いので、このように定義 - 具体例を挙げると、完全拡散面で
cos
に応じた重点的サンプリングを行うとbsdf * cos / pdf = 1.0
となる
- サンプリング方向 +
bsdf()
- 名前の通り
bsdf
を返す関数 - NEEの計算の中で
bsdf
が必要になるので定義
- 名前の通り
nee_available()
- NEEに対応しているかどうか返す関数
- 実質的にはスペキュラー面でないなら
true
を返す関数
インターフェースを統一できてコードが綺麗になった気がします。
トーンマッピングの実装
去年の実装では HDR で計算した結果を LDR に変換するときに単純に clamp(x, 0, 1)
していました。
このままでは 1.0 を超える明るい箇所の画素がすべて白色に潰れてしまいます。
この問題を解決するためにトーンマッピングを実装しました。
今回はトーンマッピングの中でも最も単純そうな「Reinhard Tonemapping」を実装しました。
Reinhard Tonemappingでは
$$f(x) = \frac{x}{x + 1}$$
という式で画素値を変換することで、画素値が無限大になっても 1.0 に漸近させることができます。
この式をそのまま各RGBの要素に適用すると、各要素は1.0を超えないようになりますが、RGBすべてが 1.0 を大きく超える画素では結局白に潰れてしまいます。 そこで、分母の $x$ は RGB からの計算した輝度(スカラー)として、分子の $x$ はRGB(ベクター)として実装しました。
さらに、単純なReinhard Tonemappingだと無限大の輝度値をもつ画素値しか白に漸近してくれずに不便なので、 任意の輝度値 $L_w$ を白に漸近させるポイントとして指定できる改良版のアルゴリズムを利用しました。
$$f(x) = \frac{x}{x + 1} \left(1 + \frac{x}{L_w^2} \right) $$
詳しくは以下のPDFが参考になるでしょう。
デノイズの実装
レイトレ合宿の制限時間は年々短くなっています。
イベント名 | 制限時間 |
---|---|
レイトレ合宿! | 1時間 |
レイトレ合宿2!! | 30分 |
レイトレ合宿3!!! | 15分 |
レイトレ合宿4!? | 5分 |
レイトレ合宿5‽ | 273秒 |
レイトレ合宿6 | 123秒 |
さらに出力解像度のハードルも年々上がっていて、今年はほとんどの参加者がフルHDで出力しており、4Kで出力する猛者もいました。
制限時間の短縮と高解像度化によって、1ピクセルあたりにかけられるサンプリング数がどんどん少なくなっているため、デノイズの重要性は高まっていると言えるでしょう。
今回はデノイズの中でも最も単純そうな「Bilateral Filter」を実装しました。
Bilateral Filterを簡単に解説します。
平滑化フィルター(ぼかしフィルター)として有名なアルゴリズム「Gaussian Blur」があります。 Gaussian Blurは「空間的な重み」に基づいて周囲のピクセルを混ぜ合わせて平滑化を行います。
Gaussian Blurでは全体的にぼやけてしまうので、 重みを変化させてエッジ部分を保持するようにしたものがBilateral Filterです。
Bilateral Filterでは「空間的な重み」と「ピクセル値の差による重み」を掛け合わせたものを用います。
$G_{\sigma_s}(||p - q||)$ は距離をパラメータとしたガウス関数なので「空間的な重み」となります。
$G_{\sigma_r}(|I_p - I_q|)$ は画素値の差をパラメータとしたガウス関数なので「ピクセル値の差による重み」です。
この2つの重みを組み合わせることでエッジ部分を保持しながら平滑化ができます。
1つ補足すると、上の式の $\sigma_r$ を無限大にするとガウス関数の性質上、「ピクセル値の差による重み」が一様な分布になります。 つまり$\sigma_r$ を無限大にするとGaussian Blurになります。 このような知識を念頭に置いておくと、パラメータ調整に役に立つでしょう。
上の画像は “A Gentle Introduction to Bilateral Filtering and its Applications” SIGGRAPH 2007 の “Fixing the Gaussian Blur”: the Bilateral Filter という資料の7ページ目から引用しました。
以下のブログの説明も分かりやすかったです。
今回は簡単なデノイズを実装しましたが、余裕があればもっと凄いデノイズをやりたいですね。 レイトレ合宿の主催のqさんよると、次の手法がオススメだそうです。
Houdiniによるシーン作成
Assetの一部はHoudiniを利用して作成しました。
Wired Bunny
Wired Bunny #Houdini pic.twitter.com/aZO2CHSS31
— がむ (@gam0022) 2018年8月27日
HoudiniでStanford Bunnyのモデルをワイヤーフレーム化したメッシュに加工しました。
PolyWire
というノードを使うことで簡単に実現できます。
額縁
#Houdini の Boolean と PolyBevel でつくった額縁 pic.twitter.com/ycwK9SJw5K
— がむ (@gam0022) 2018年8月31日
額縁はBox同士をブーリアン演算で切り抜いた形状を PolyBevel
というノードで角を丸めて作りました。
フラクタルの試作
本番では使いませんでしたが、フラクタルも試作しました。
#Houdini でフラクタル図形(Menger Sponge)を作ってみた。
— がむ (@gam0022) 2018年5月5日
少しずつSOPとVEXを理解してきた気がする。 pic.twitter.com/Wip7DbkGN8
立方体の向きを揃える処理を省くと複雑な形状になって面白い
— がむ (@gam0022) 2018年5月5日
MengerSpongeの亜種になる #Houdini pic.twitter.com/OKfd29TtNn
スケールの計算がバグっていたので修正。
— がむ (@gam0022) 2018年8月27日
良い感じになった。 #Houdini pic.twitter.com/4hyjzhwNlc
Dodecahedron(正十二面体)の Fractal 😉 #Houdini pic.twitter.com/26iIRVefLL
— がむ (@gam0022) 2018年8月27日
Windows環境で36コアしか使えない問題
今年のレイトレ合宿の本番マシンは72コアを持つEC2インスタンスでした。
Windowsの仕様によってシステムに 64コアを超える論理プロセッサーが搭載されていると、 プロセッサーはプロセッサー・グループに分割されてしまうらしく、私のレンダラーも36コアしか利用できませんでした。 しかも締切前日に発覚しました。
C++であれば、 SetThreadGroupAffinity()
でスレッドグループを指定することで対処可能のようでしたが、Rustだと対処困難でした。
去年のレイトレ合宿からWindowsとAmazon Linuxの2つから好きなOSを選択できるようにルール改定がありました。 そこで、急遽Linux用のバイナリを作成してAmazon Linuxで走らせたところ、72コアをフルに利用できるようになりました!
Linux用のバイナリをクロスコンパイルして72コア使えました ☺️
— がむ (@gam0022) 2018年8月29日
クロスコンパイルが比較的簡単にできるのでRustは神。https://t.co/UpqCC66wmV pic.twitter.com/hIX2GO4Yn1
レイトレ合宿のルールには次の文言があります。
何もインストールしていないまっさらなマシン上で動作するようにしてください。
これは動的ライブラリに依存せずに動作する必要があることを意味します。
Rustで動的ライブラリに依存しないバイナリを作成する場合は x86_64-unknown-linux-musl
を target にしてビルドすればOKです。
以下の記事を参考にしてmacOSでLinux用のバイナリをクロスコンパイルして最終提出しました。
同じくRustで参加されたxyz600の参加報告によると全く同じ手段で解決されていました。
制限時間や出力解像度のコマンドライン引数対応
制限時間や出力解像度をコマンドラインで引数で指定する機能を締切前日くらいに実装しました。
こんな基本的な機能をなぜ実装しなかった疑問に思うかもしれませんが、単純に時間的余裕が無かっただけです。
本番環境と開発環境ではスペックでは性能差があるため、 開発環境では性能差を考慮して長めの制限時間に変更するコード修正が必要でしたが、 これによってコマンドライン引数からコード修正なしに設定を変更できるようになりました。
コマンドライン引数のパースには getopts というクレートを利用しました。
# レイトレ合宿6のレギュレーションで実行
cargo run --release
# 制限時間を1047秒に設定し、60秒ごとに途中結果を出力しながら実行
cargo run --release -- -t 1047 -i 60
# 低解像度・サンプリング数を1で実行
cargo run --release -- -w 480 -h 270 -s 1
# デバッグモードで実行(被写界深度の焦点面を可視化)
cargo run --release -- -d
# ヘルプを表示
cargo run --release -- --help
Usage: hanamaru-renderer [options]
Options:
--help print this help menu
-d, --debug use debug mode
-w, --width WIDTH output resolution width
-h, --height HEIGHT output resolution height
-s, --sampling SAMPLING
sampling limit
-t, --time TIME time limit sec
-i, --interval INTERVAL
report interval se
リソースフォークを除外しながら圧縮
tar
コマンドを使ってリソースフォークを除外しながら圧縮する方法を学びました。
$ COPYFILE_DISABLE=1 tar zcvf 圧縮先.tar.gz --exclude ".DS_Store" 圧縮元のディレクトリ
運営側でレンダラーをスクリプトから自動実行しているそうなのですが、実行ファイルが複数あると自動実行できなくなるそうです。
Macのリソースフォークには実行権限がついているので、リソースフォークを除外して圧縮しました。
Macでtarコマンドを使うときには次のオプションをつけるとリソースフォークが除外されるという知見を得た。
— がむ (@gam0022) 2018年8月30日
COPYFILE_DISABLE=1 tar zcvf 圧縮先.tar.gz –exclude ".DS_Store" 圧縮元のディレクトリhttps://t.co/mKWLxCZ8Hn
まとめ
最終的にはなんとか締切に間に合いましたが、来年はもう少し余裕を持って開発したいですね。
サブミット完了。
— がむ (@gam0022) 2018年8月30日
業務が忙しかったりSIGGRAPHのために海外出張したりして、今年は準備の着手が遅くなってしまいました(言い訳)。
本格的に準備に着手したのはSIGGRAPH帰国後になってしまい、合宿まで残り2週間を切っていました😇 しかもSIGGRAPH帰国後だったので、時差ボケに苦しみながらの実装でした😵 ともあれ、なんとか無事に提出に間に合ってよかったです!
TODO管理としてGitHubのissuesを利用しました。 私がどういう機能を実装しようとして、何を諦めたのか興味がある人は読むと良いでしょう。
合宿当日
合宿当日の様子については、後編(当日編)の記事に続きます。