レイトレ合宿6 参加報告 前編(準備編)

これはレイトレ合宿6の参加報告の記事の前編です。 記事が長くなったので、前編と後編の2つに分けました。

この記事では前編(準備編)ということで、自作レンダラーに実装した機能や手法の紹介を行います。

後編(当日編)はこちらです。


9月1日(土)~9月2日(日)に神津島で開催されたレイトレ合宿6に参加しました。

レイトレ合宿は完全自作のレイトレーサーを走らせて画像の美しさを競うイベントです。

参加者の中には、Arnold RendererやRadeon ProRenderといった商用のレンダラーの開発者、 SIGGRAPH 2017で発表された研究者など、グラフィック分野の最先端で活躍されている方々もいらっしゃり、大変刺激を受けました。

私は今年で3回目の参加になります。過去の参加報告はこちらです。

Rustで開発したパストレーシングによる自作の物理ベースレンダラー(Hanamaru Renderer)をバージョンアップして、 こんな感じの画像を123秒でレンダリングしました。

今回は19人中10位だったので、ギリギリ入賞圏内に潜り込めました!

result

↑リンクをクリックするとオリジナルの可逆圧縮の画像になります。

ソースコードは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倍速になりました! メジャーバージョンを上げると劇的にパフォーマンスが変わることがあるのですね! 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実装後の結果を比較しました。ノイズを劇的に軽減できました!

NEEの理論と実装についての詳細については、ShockerさんPocolさんの資料を参考にさせていただきました。

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 * cospdf が打ち消すケースが多いので、このように定義
    • 具体例を挙げると、完全拡散面で cos に応じた重点的サンプリングを行うと bsdf * cos / pdf = 1.0 となる
  • bsdf()
    • 名前の通り bsdf を返す関数
    • NEEの計算の中で bsdf が必要になるので定義
  • nee_available()
    • NEEに対応しているかどうか返す関数
    • 実質的にはスペキュラー面でないなら true を返す関数

インターフェースを統一できてコードが綺麗になった気がします。

トーンマッピングの実装

去年の実装では HDR で計算した結果を LDR に変換するときに単純に clamp(x, 0, 1) していました。

このままでは 1.0 を超える明るい箇所の画素がすべて白色に潰れてしまいます。

この問題を解決するためにトーンマッピングを実装しました。

Tonemap

今回はトーンマッピングの中でも最も単純そうな「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」を実装しました。

Denoise

Bilateral Filterを簡単に解説します。

平滑化フィルター(ぼかしフィルター)として有名なアルゴリズム「Gaussian Blur」があります。 Gaussian Blurは「空間的な重み」に基づいて周囲のピクセルを混ぜ合わせて平滑化を行います。

Gaussian Blurでは全体的にぼやけてしまうので、 重みを変化させてエッジ部分を保持するようにしたものがBilateral Filterです。

Bilateral Filterでは「空間的な重み」と「ピクセル値の差による重み」を掛け合わせたものを用います。

Bilateral Filter on a Height Field

$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

HoudiniでStanford Bunnyのモデルをワイヤーフレーム化したメッシュに加工しました。

PolyWire というノードを使うことで簡単に実現できます。

額縁

額縁はBox同士をブーリアン演算で切り抜いた形状を PolyBevel というノードで角を丸めて作りました。

フラクタルの試作

本番では使いませんでしたが、フラクタルも試作しました。

Windows環境で36コアしか使えない問題

今年のレイトレ合宿の本番マシンは72コアを持つEC2インスタンスでした。

Windowsの仕様によってシステムに 64コアを超える論理プロセッサーが搭載されていると、 プロセッサーはプロセッサー・グループに分割されてしまうらしく、私のレンダラーも36コアしか利用できませんでした。 しかも締切前日に発覚しました。

C++であれば、 SetThreadGroupAffinity() でスレッドグループを指定することで対処可能のようでしたが、Rustだと対処困難でした。

去年のレイトレ合宿からWindowsとAmazon Linuxの2つから好きなOSを選択できるようにルール改定がありました。 そこで、急遽Linux用のバイナリを作成してAmazon Linuxで走らせたところ、72コアをフルに利用できるようになりました!

レイトレ合宿のルールには次の文言があります。

何もインストールしていないまっさらなマシン上で動作するようにしてください。

これは動的ライブラリに依存せずに動作する必要があることを意味します。

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のリソースフォークには実行権限がついているので、リソースフォークを除外して圧縮しました。

まとめ

最終的にはなんとか締切に間に合いましたが、来年はもう少し余裕を持って開発したいですね。

業務が忙しかったりSIGGRAPHのために海外出張したりして、今年は準備の着手が遅くなってしまいました(言い訳)。

本格的に準備に着手したのはSIGGRAPH帰国後になってしまい、合宿まで残り2週間を切っていました😇 しかもSIGGRAPH帰国後だったので、時差ボケに苦しみながらの実装でした😵 ともあれ、なんとか無事に提出に間に合ってよかったです!

TODO管理としてGitHubのissuesを利用しました。 私がどういう機能を実装しようとして、何を諦めたのか興味がある人は読むと良いでしょう。

合宿当日

合宿当日の様子については、後編(当日編)の記事に続きます。

comments powered by Disqus

gam0022.net

Qiita