object space raymarching
夏コミ版 exception reboot で用いた、オブジェクトスペースでレイマーチする手法について解説してみます。(ここで言うレイマーチは厳密には sphere tracing のことですが、面倒なのでレイマーチで統一します)
アイデア自体は特に新しくも難しくもなく、シーン内に単純なポリゴンモデルを配置し、そのモデルの中でレイマーチを行うというものです。レイマーチにより G-Buffer を生成し、あとは通常通りライティングを行います。
レイマーチについては過去にこの blog でも簡単な入門を書きました。最近では日本語でも結構情報が出てくるくらい知名度が上がってきているように見受けられます。
raymarching for games - primitive: blog
レイマーチで G-Buffer を生成する手法もしばらく前にこの blog で紹介しました。
rendering fractals in Unity5 - primitive: blog
実装の詳細の大部分は既にこれらの記事中に書かれており、本記事ではそれらとかぶってる部分は省略します。また、成果物はこちらになります。上の画像のシーンがそのまま含まれています。(ProceduralModeling.unity)
https://github.com/i-saint/Unity5Effects
ちなみに作例は Unity で作られていますが、レンダリング方式が deferred shading かつ自作シェーダを動かせる環境であればどこでもこの手法は使えるはずです。
レイマーチで distance function を描く場合、fullscreen quad で画面全体を描くことが多いですが、それを小分けにしてオブジェクトスペースでやろうというのが今回の主題です。これにより、レイマーチの欠点である柔軟性のなさと計算量の多さの緩和を狙います。
レイマーチでは、シェーダに式を書くことで図形を表現するため、図形の変更はつまりシェーダソースの変更になります。外部パラメータ化によりある程度変更可能にはできますが、例えば特定の位置に図形を追加、みたいな変更に耐える柔軟性を持たせるのは結構大変です。今回のオブジェクトスペースのやり方であれば、オブジェクトの配置や位置の変更はゲームエンジンのエディタ上でオブジェクトを追加したり TRS をいじることで簡単に実現できます。
画面全体をレイマーチする場合、レイの開始点は常にカメラ位置 (もしくは view plane) であり、図形に到達するまでに相応のステップ数が必要になります。一方オブジェクトスペースの場合、レイの開始点がそれなりに目的の図形の近くであることが期待でき、ステップ数削減が見込めます。
以下実装の手順。
まず、描画に使うポリゴンモデルは立方体か球だと都合がいいです。これらの場合ピクセルシェーダ内の単純な計算でレイがオブジェクトの内側にいるかが判定できます。もっと複雑なモデルでも適用は可能ですが、その場合内外判定に裏面の depth の情報が必要になります (最大マーチ距離などで適当に近似するのもアリではありますが) 。内外判定を怠ると オブジェクトの裏面=無限遠 と扱われることになり、シーンによっては顕著にヘンな結果になります。
シェーダ内の処理。頂点シェーダの出力に、頂点のワールド座標と法線を追加します。このワールド座標 (=ポリゴン表面位置) をレイマーチの開始位置とするわけです。
ピクセルシェーダでレイマーチを行いますが、この中でレイをオブジェクトのローカル座標系に変換する処理を挟みます。具体的には、レイをオブジェクトの TRS の逆行列で変換し、scale だけ掛けなおします (TR の逆行列でもいいと思われますが。Unity の場合 TRS の逆行列はエンジン側が提供しているのでこうしました)。レイの方向は normalize(頂点のワールド座標-カメラ位置) で算出できます。
注意すべき点として、レイの開始点が既に図形の内側だった場合の対処が必要です。これを怠ると法線が反転した面が出てきてライティングがおかしくなります。対処法としては、レイマーチの結果得られた距離が負 (=レイは図形の内側) である場合、元ポリゴンモデルの法線を出力する、というものになります。
あとは通常通り distance function を捏ねていい感じに見える形状を構築します。distance function 以外は定型的な処理なので、distance function だけ書いてパラメータを設定すればいい感じにレンダリングできるようなフレームワークを作るといいでしょう。私の場合この 2 つがフレームワークになっています:ProceduralModeling.cginc Framework.cginc
レイがオブジェクトの外に行ってしまったら discard、とすることで、元ポリゴンモデルと輪郭が異なる形状を表現できます。(前述の 立方体か球だと都合がいい のはこの内外判定が容易なため)
SV_Depth を用いてレイの到達点の depth を出力すれば、影や立体交差も正しく表現できるようになり、SSAO などもいい感じに載るようになります。ただし、early Z culling が効かなくなるため顕著に重くなります。また、元ポリゴンモデルとの形状がかけ離れるほど、正しく形状を表現するのに多くのステップ数が必要になり、重くなります。ここらへんはバランスを考える必要がありそうです。
例:元ポリゴンモデルとの形状の差が激しい状況。左は元ポリゴンモデルの depth をそのまま出力したもの。右は SV_Depth でレイの到達位置を出力したもの。見ての通り左は交差部分がおかしいとか色々エラーがあります。が、右よりだいぶん速いです。
作例:
レイマーチを試したことがある方なら一目で式が分かると思います。使い古された形状ですが、画面密度を増やすには割と効果的なんじゃないかと。経過時間で縦方向の座標を動かせばそれっぽくアニメーションになります。
みんな大好き六角形。歯抜けにしたりランダム高低差をつけたりすると見た目の華やかさが上がります。六角形の中心が元図形の範囲外だったら描画しない、とすることで、元ポリゴンモデルをスケールするだけで六角模様が追随するようになります (=元モデルの境界で六角形がぶった切られない)。
メタボール。これはレイマーチだとすっごく単純な式で実現できます (実装のコア部分)。ただし、これだとボールの数に比例してすごい勢いで重くなっていきます。数百や数千を想定する場合もっと高次のアルゴリズムを考える必要がありそうです。
冒頭の画像のシーンは、これらにポストエフェクトやパーティクルを盛りまくることでできあがりました。
Unity ちゃん以外の背景オブジェクトは全て立方体をピクセルシェーダで加工したものです。
下図はシェーダによる差を示したもので、両方とも同じシーンで左はシェーダを通常の Standard Shader に差し替えたものになります。
ここまでやってみた実感として、レンダリング結果や編集のしやすさは良好なんですが、この方法でも図形が複雑になると重くて、もう一工夫入れないとちょっと実用に耐えられない印象です。上記画像のシーンは六角模様の床が明らかに危険域の重さになっています…。
いくつか最適化のネタはあるものの、まだ検証中の段階でそちらの詳細はまた別の機会に書く予定です。まあ adaptive sub-sampling、temporal、screen space normals、early-Z culling モドキ など、ネタとしてはごくありふれたものになりそうです。
(ちなみに夏コミ版 exception reboot には時間がなくて一つも入れられませんでした…。つまり最終的に今よりは描画負荷は軽くなる見込みです)
今後ハードウェアの進化に伴ってこういうポリゴン以外の 3D 形状の表現がだんだん市民権を得ていくんじゃないかと思います。
ただ、今回みたいなプロシージャル的なネタはなかなか一般化が難しく、タイトル固有の実装にならざるを得ないところが多いです。逆に言うと、ゲームエンジンが当たり前に使える時代にプログラマーが最も輝けるのがこういう領域なのかもしれません。