deferred shading in Unity
Unity は標準で deferred rendering をサポートしていますが、これは light pre-pass とか deferred lighting と呼ばれるもので、deferred shading とはちょっと違います。私が必要としているのは deferred shading の方なので、これを Unity で実装してみました。
https://github.com/i-saint/DeferredShading
deferred shading 自体の詳しい解説はここでは省略しますが、 大雑把には render target を複数用意し、ポリゴン描画パスではそれらに法線、位置、diffuse 色などシェーディングに使う情報を格納 (これらは geometry buffer、略して G-Buffer と呼ばれます)、その後 G-Buffer を利用してポストエフェクト的にシェーディング処理を行う、というものです。より詳しく知りたい場合 西川善司氏の Killzone2 の解説や wikipedia の記事 が参考になると思います。
一般的には deferred shading の強みはライトがいっぱい置けることと、シェーディングの際 pixel shader が走る回数を最小限にできることあたりだと思いますが、G-Buffer を利用したジオメトリ変形やポストエフェクトなどのテクニックが使える、というのもあります。自分的にこの G-Buffer を利用したテクニックが小規模ゲーム開発において強力な武器になると考えていて、今回実装しようと思ったのもそれが目的です。
まずは G-Buffer 書いてポイントライトでシェーディング。(G-Buffer シェーダ ポイントライトシェーダ)
この絵が出るまで C# もシェーダも 150 行程度。アルゴリズムを理解していれば deferred shading はコア部分はすぐに実装できます。
これを G-Buffer を利用したトリックを用いて面白く見えるようにしていきます。
・screen space shadow (ポイントライトシェーダの該当処理部分)
G-Buffer を使うと簡単に影が出せます。光源の中心から現在の pixel 位置まで適当な間隔で ray を飛ばし、途中で遮られたらライティングしない (discard)、とすれば自然と影ができる、というものです。
ただ、重大な欠点があって、G-Buffer は 2D のデータであるため、立体交差を正常に処理できません。その pixel の一番手前にあるポリゴンから奥は全部埋まってるものとして扱われてしまいます。このため、光源自体が遮られたら真っ暗になってしまったりなど、視点によっては簡単に破綻してしまいます。しかし実装は簡単かつ shadow map 方式より概ね高速、しかも shadow map 方式では面倒な全方位影を簡単に出せるため、実用度はそれなりに高いはずです。
・輪郭付近を明るく (該当処理のソース)
お手軽にかっこよく見えるエフェクト代表その 1
カメラ -> 法線の角度が浅いところを適当に明るくするだけ。これだけでなんかメタリックでクールな絵が得られます。あとこの例では輪郭を強調する処理も入れています。輪郭抽出は近隣 pixel の法線を見て差が一定値以上かどうかで判定しています。
・光の溝 (該当処理のソース)
お手軽にかっこよく見えるエフェクト代表その 2
ワールド座標を mod() で区切って端っこを光らせるとグリッド模様になります。それを時間でアニメーションさせています。単なるグリッドだと単調なので間引きパターンも入れていますが、かっこ良く見えるパターン作るのは難しくて、今現在も模索中です…。
・bloom
お手軽にかっこよく見えるエフェクト代表その 3
縮小バッファを用意してぼかして加算で重ねる、至って普通の bloom です。bloom は Unity 標準にもありますが、今回 G-Buffer に発光色用バッファを用意してそれを元に光らせたかったので、独自に用意する必要がありました。
・反射 (該当処理のソース)
最近のゲームではデファクトになりつつある screen space 反射エフェクト。正確な反射を実装するのは結構大変だと思いますが、ぱっと見それっぽく綺麗に見える絵を出したい程度であればそんなに大変でもないです。
G-Buffer から現在の pixel の位置と法線を得て、カメラ->pixel 位置 のベクトルと法線の反射ベクトルを求めます。そして反射ベクトルを適当に伸ばした先のフレームバッファの色を加算すれば反射っぽく見えます。
今回の例は反射距離は常に一定という超手抜き実装で、その代わりランダムに散らしたレイを 8 本ほど飛ばして (=ぼかしを入れて) 綺麗に見えるようにごまかしています。あと、結構重い処理になるので、縦横半分にしたバッファで反射色を出してそれを加算しています。
もっと真面目な実装もやろうとしたんですが、正確さと速さの両立が難しい上、上記手抜き実装の方が綺麗に見えたりするので今回は断念しました…。この辺を詳しく知りたい方は local reflections とかをキーワードに調べてみるといいでしょう。
そして上記要素を一通り盛り込んだものがこちら、WebPlayer によるデモ。
また、上記要素を使って前回のパーティクル群を描いてみました。なかなか綺麗に見える絵が出せてるんじゃないかと思います。
注意すべき点として、モバイル環境などの非力な GPU では G-Buffer を書くのが非常に遅く、こういう over draw が多いシーンでは depth pre-pass (先に depth だけを書き、ZTest Equal でシェーディングすることで pixel shader 負荷を下げる古いテクニック) 入れた方が速くなりました。
light pre-pass 避けたかった理由の一つにジオメトリ 2 回処理するのがイヤだからというのもあったんですが、deferred shading でもそうした方が速くなったりするというのはなんとも皮肉な話です。
あとはついでに、ジオメトリ変形処理を実装してみました。
WebPlayer によるデモ
・モデルの減算 (Sub) 処理
モデルを引き算して穴開けたりする、いわゆる boolean 演算というやつです。G-Buffer を加工すればこんなこともできます。
やってることは以下のような内容です。
1. 減算される側で普通に G-Buffer を書く
2. 減算する側のオブジェクトを stencil だけ書いてマスクを作る
3. 減算する側のオブジェクトを、2 で作ったマスクを使いつつ、面を反転して、ZTest Greater で書いて G-Buffer を更新する
直感的には理解しづらいですが、表面で stencil を書き、そのマスクを使いつつ裏面で ZTest Greater で書くと、交差している部分だけが描かれます。交差した部分に減算する側の反転したメッシュを書いて G-Buffer を更新すれば凹んだように見せかけることができるわけです。
ただ、これだけだと穴開けるなどの輪郭が変わる変更ができません。前述のように G-Buffer は 2D のデータであり、最前面以外の情報が失われているためです。これを何とかするため、この例では、事前に減算される側のモデルを反転して depth をテクスチャに書き、それを貫通判定に使っています。減算する側の面の depth > 減算される側の反転面の depth であれば貫通している、と判断できるので、その pixel は G-Buffer を初期化して穴を開けているわけです。
しかし、これも減算される側同士に立体交差があったら破綻します。より正確にやりたい場合、減算される側モデルを一個書く -> 減算する側モデルを全部書く を繰り返す必要があります。(この例ではそこまでやっていないので、たまに不自然な結果が見えます)
pixel 毎にリンクリストを作って奥の面の情報を記録 (Order Independent Transparency で使われてる方法) すればスマートに解決できそうな気がしますが、それが実用レベルに達するにはもうしばらくハードウェアの進化を待つ必要がありそうです。
WebPlayer によるデモ
・モデルの And 処理
減算ができるなら And もやらねば、ということで、boolean 演算その 2。モデルが重なった部分だけを可視化しています。
こちらは減算より難度が高いです。重なった部分を判定しないといけないので、And する側される側両方の、表面裏面両面の情報が必要になります。手順としては以下のような感じです。
1. And する側の G-Buffer と裏面 depth をテクスチャに書く
2. And される側の裏面 depth をテクスチャに書く
3. And される側の G-Buffer を書く
- この際、And する側される側どちらかの表面 depth が相手の裏面 depth より大きかったら交差してないので discard
- そして And する側される側、depth が大きい方の G-Buffer を書き出す
重い上に使いどころが難しいですが、珍しい視覚効果なのでプレイヤーの目を引くことができるかもしれません。
もはや Unity とはほとんど関係ない内容になっていますが、Unity だとコードの変更から動作の確認までが速いので、シェーダ書くのが楽しくなります。エディタだけでも十分 Unity を使う意義はあるという実感があります。
ゲームエンジンが一般化するにつれてアーティストの作ったリソース勝負になってきそう、という恐れが漠然とあったんですが、実際に手を付けてみると、プログラマが単身創意工夫でなんとかする方式も意外とまだまだ通用するんじゃね?という気になれました。