raymarching for games

demoscene の世界では近年 raymarching というレンダリング手法がよく用いられています。ポリゴンモデルは使わず、モデルデータは数式の図形としてシェーダコードの中で表現し、pixel shader で図形との距離を求めて可視化していく、というものです。
demoscene (4k/64k intro) の厳しい容量制限の中綺麗な絵を出すために生み出された手法ですが、従来のポリゴンベースの手法では難しい独特の絵を出すことができるという副次的効果があります。

raymarching の代表的な作品群


この手法は PS4 世代以降、小規模ゲーム開発チーム向けの有用なツールになるんじゃないかと考えていて、atomic では試しに背景にこの手法を用いています。以下はその過程や考察です。

まず、raymarching の基本については demoscene.jp の人たちが素晴らしい解説を書いてくれています。
http://www.demoscene.jp/?p=811
raymarching の基本的な図形の式は、iq 氏 (elevated の作者) のサイトにまとめられています。
http://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
上 2 つを知っていればとりあえず raymarching で絵を出すには十分です。
ついでに、folding や fractal など、より高次のテクニックを知りたい人は Mikael H Christensen 氏の記事が役立ちます。
http://blog.hvidtfeldts.net/index.php/2011/06/distance-estimated-3d-fractals-part-i/
また、shadertoy.comglslsandbox.com には様々な作品が投稿されています。


atomic の背景は非常に単純で、Box の繰り返しと後処理だけで実現されています。順を追って説明すると以下の様な感じです。
atomic_bg0
1. 箱を出す & 距離と ray を march した回数に応じて適当に色を足す (WebGL で実行)
これだけだとよくわからん感じですが、XZ 軸方向の 2 次元 Box です。
ray を march した回数というのは、ray が図形に到達するまで (もしくは打ち切られるまで) にかかった step 数で、オブジェクトの輪郭スレスレを通りつつ交差しなかった場合とかに数が多くなります。この数に応じて色を足すと、4k intro でよく見るオブジェクトの輪郭付近が発光してオーラが出てるようなエフェクトを実現できます。

atomic_bg1
2. 箱を反復させる (WebGL で実行)
平面との and をとって y 方向に空間を作りつつ、折りたたみや mod() で図形を繰り返します。

atomic_bg2
3. もっと反復させる (WebGL で実行)
いい感じに見えるように調整します。

atomic_bg3
4. 箱の位置に乱数を加える (WebGL で実行)
面白く見えるようにします。乱数は位置を hash する方式です。位置をベースにした乱数を使う場合、次の step の正確な距離を予測できなくなるため、ray が図形を突き抜けるケースが出てくることに注意が必要です。(手前のちらついてる部分はそれが原因。誤魔化す方法はありそうな気がしますが、うまくいきませんでした)

atomic_bg4
5. 法線を求め、カメラ->法線の角度が浅いところを光らせる&輪郭を光らせる (WebGL で実行)
法線は近隣 pixel の位置を求めてその勾配から推測します。普通にライティングしてもいいんですが、かっこよく見えるならそれを最優先です。

atomic_bg6
6. おなじみ光の溝エフェクト&走査線効果 (WebGL で実行)
あとはなんでもいいのでかっこよく見えるエフェクトをぶちこみましょう


実際にやってみて得られたのは、超人的なプログラムを書かなくても見せ方次第でそれなりに綺麗に見える絵を出すことはできる、という感触でした。
しかし raymarching にも色々デメリットがあります。モデルの形状の融通の効かなさもなかなかツラいですが、一番問題になるのは、重いことだと思われます。pixel shader の中で図形との距離計算を何十回もやることになるため、図形の複雑さと描画面積に比例してすごい勢いで重くなっていきます。

demoscene の場合、容量制限の都合上シェーダレベル以上の最適化はまず行われませんが、ゲームの場合より高次の最適化を行う余地があります。pixel shader が重くなるのは不可避なので、レンダリング解像度を減らす最適化が効果大です。
この手の最適化として代表的なものに adaptive sampling という手法があります。事前に 1/4 とか 1/16 とかの解像度でレンダリングしておき、高解像度でレンダリングする際、低解像度バッファの近隣 pixel が似たような結果であればそれをそのまま使用、そうでなければ真面目に計算する、というものです。
adaptive_sampling1adaptive_sampling2adaptive_sampling3
実際に試してみた図。左: 最適化なし 中央: 最適化あり 右: 1/4 の解像度でレンダリングした領域を赤く表示。品質とのトレードオフになりますが、シーンによっては有用そうな感触です。
他にも、ある程度以上暗いところは低解像度でレンダリングするとか、縦半分の解像度でレンダリングして残り半分は前フレームの結果をインターレースで混ぜて補うとか、ゲーム向けの小細工は色々ありそうな気がします。

また、応用として、deferred 系のレンダリング手法の場合、raymarching で G Buffer だけ書き出してライティングはポリゴンモデルと一括で行う、みたいにすることもできます。raymarching で生成したヘンテコオブジェクトと通常のポリゴンモデルで一貫したライティング処理を行うことができるため、面白い効果を得られるかもしれません。

raymarching のゲームへの応用はまだ前例が少なく、自分も試行錯誤している段階で、成果らしい成果が出せたら今後も経過を書き残していく予定です。