rendering fractals in Unity5
Unity5 になってレンダリング機能の柔軟性が大きく増しました。
新たに追加された CommandBuffer により、Unity のレンダリングパスのほとんど任意のタイミングに任意の描画コマンドを差し込むことができるようになりました。また、deferred shading レンダリングパスが新たに追加されました。Unity4 にあったのは deferred lighting でしたが、今度のはリッチな G-Buffer を持ったちゃんとした deferred shading です。
CommandBuffer と deferred shading を併用することにより、Unity のレンダリングパスで用いられている G-Buffer を加工したり、ライティングを独自処理に差し替えたりといったことができるようになります。試しにこれらを用いてフラクタル図形をレンダリングしてみました。
https://github.com/i-saint/RaymarchingOnUnity5
http://primitive-games.jp/Unity/RaymarcherUnity5.html
本記事の内容のソースコードと WebPlayer ビルドはこちらになります。Unity5 からフリー版 (Personal Edition) でも全レンダリング機能を使えるようになったので、気軽に手元で試せるようになったんじゃないかと思います。
元ネタはしばらく前の この記事。distance function & raymarching を用いて G-Buffer を生成し、それに対してライティングを適用する、というものです。
deferred shading の場合、ポリゴンモデルは G-Buffer の生成のみに用いられ、ライティングの段階ではラスタライズされたデータである G-Buffer が用いられます。裏を返すと、G-Buffer さえ適切に生成できれば、どんなものに対しても適切にライティングを施すことができます。
3D モデル の表現はポリゴンモデル以外にもいくつかあり、それらも G-Buffer の生成に活用できます。今回用いる distance function もその 1 つです。
distance function & raymarching は demoscene の世界でよく使われているレンダリング手法で、ポリゴンモデルの代わりに数式で 3D モデルを表現してピクセルシェーダでそれを可視化する、というものです。この数式は図形との距離を返す関数で、故に distance function と呼ばれます。distance function を用い、ray を飛ばして段階的に図形との距離を求めて可視化していく手法が raymarching と呼ばれます。
下図のように ray が進んでいくことから、sphere tracing と呼ばれることもあります。ボリュームデータを可視化する手法も raymarching と呼ばれるので、区別するにはこちらの方がいいのかもしれません。
distance function や raymarching の詳細は元ネタ記事に委ねて、実装の詳細です。
demoscene の世界では大抵ライティング処理まで 1 パスでやってしまいますが、前述のように今回は G-Buffer の生成のみに distance function & raymarching を用いてライティングは Unity に委ねるという手法を取ります。これにより Unity の通常の 3D オブジェクトと一貫したライティングを行うのが狙いです。
実際の手順は単純で、CommandBuffer を用いて G-Buffer パスの後 (CameraEvent.AfterGBuffer) に fullscreen quad を一枚描く処理を追加し、ピクセルシェーダで G-Buffer を生成します。他は通常通りで、ライティングやポストエフェクトなどは標準機能を使います。
CommandBuffer の内容は以下になります。
CommandBuffer cb = new CommandBuffer(); cb.name = "Raymarcher"; cb.DrawMesh(quad, Matrix4x4.identity, material, 0, 1); camera.AddCommandBuffer(CameraEvent.AfterGBuffer, cb);
これだけです。ちなみに、実行タイミングが CameraEvent.BeforeGBuffer/AfterGBuffer であればレンダーターゲットは既に G-Buffer が指定されており、自分で指定する必要はないようです。
ピクセルシェーダが今回の肝で、distance function を raymarching するわけですが、distance function は今回はよく知られてるフラクタル図形の式を適当に見繕ってきました。
http://glslsandbox.com/e#23658
(map() 関数で式を選びます)
左: Tglad's formula と呼ばれているらしい式。右: Pseudo-Kleinian と呼ばれているらしい式。どちらもびっくりするくらい短い式で綺麗な絵が出てきます。
raymarching でレンダリングするオブジェクトと Unity のシーン上のオブジェクトの位置関係を正しくするには、当然ながら raymarching で用いる座標系と Unity のシーンの座標系を一致させる必要があります。このために必要な情報は、カメラの位置、正面方向、上方向、focal length ( 1.0/tan(fovy*0.5) ) になります。
これは知っていれば簡単で、全て view 行列と projection 行列から抽出できます。ただ、位置に関しては復元にちょっとばかし計算コストがかかるため、Unity が提供している組み込み変数 _WorldSpaceCameraPos を利用した方がたぶんいいです。結論としては以下のコードで必要な情報を抽出できます。
float3 GetCameraPosition() { return _WorldSpaceCameraPos; } float3 GetCameraForward() { return -UNITY_MATRIX_V[2].xyz; } float3 GetCameraUp() { return UNITY_MATRIX_V[1].xyz; } float3 GetCameraRight() { return UNITY_MATRIX_V[0].xyz; } float GetCameraFocalLength() { return abs(UNITY_MATRIX_P[1][1]); }
// 参考用、view 行列から復元する版 GetCameraPosition() float3 GetCameraPosition() { float3x3 view33 = float3x3(UNITY_MATRIX_V[0].xyz, UNITY_MATRIX_V[1].xyz, UNITY_MATRIX_V[2].xyz); float3 rpos= float3(UNITY_MATRIX_V[0].w, UNITY_MATRIX_V[1].w, UNITY_MATRIX_V[2].w); return mul(transpose(view33), -rpos); }
プラットフォームによって変わる可能性を考慮して関数にしています。右方向 (GetCameraRight()) に関しては正面方向と上方向の外積で算出できますが、せっかくそこにあるので使っています。
raymarching で ray の位置が求め、近隣ピクセルの勾配から法線を推測したら、それらを G-Buffer として出力します。G-Buffer を生成するためのピクセルシェーダの出力は以下のようになります。
struct ps_out { half4 diffuse : SV_Target0; // RT0: diffuse color (rgb), occlusion (a) half4 spec_smoothness : SV_Target1; // RT1: spec color (rgb), smoothness (a) half4 normal : SV_Target2; // RT2: normal (rgb), --unused, very low precision-- (a) half4 emission : SV_Target3; // RT3: emission (rgb), --unused-- (a) float depth : SV_Depth; // 今回は depth も独自に出力 };
注意点として、normal は符号なしデータ (RGBA 10, 10, 10, 2 bits) で格納されるため、格納時は *0.5+0.5、取得時は *2.0-1.0 してやる必要があります。
また、Stencil を有効にして 7 bit 目を立てる (128 を足す) 必要があります。Stencil の 7 bit 目が立っていないピクセルはライティングされません。
それと、今回のようなケースでは depth も独自に計算して出力する必要があります。通常はピクセルの位置の depth が自動的に出力されるので気にする必要はないのですが、今回はそれだと full screen quad の depth (=0) が出力されてしまいます。今回ほしいのは ray が到達した地点の depth です。
depth を独自に出力する場合、上記のようにピクセルシェーダの出力に SV_Depth セマンティクスつきのデータを加えればいいのですが、depth の算出方法は Direct3D と OpenGL 系で若干違うため、以下のような対応も必要になります。
float ComputeDepth(float4 clippos) { #if defined(SHADER_TARGET_GLSL) return (clippos.z / clippos.w) * 0.5 + 0.5; #else return clippos.z / clippos.w; #endif }
あとはピクセルシェーダの中で depth = ComputeDepth(mul(UNITY_MATRIX_VP, float4(ray, 1.0))); のようにすれば ok です。これにより、raymarching で生成した図形と Unity の 3D オブジェクトが正しく前後するようになり、ライティングも一貫します。SSAO の類をかけるとより馴染むでしょう。
ちなみに、今回は当たり判定まではやっていませんが、distance function はその名の通り図形との距離を算出する関数であるため、CPU 側コードで同内容の関数を書けば当たり判定も取れるはずです。GPU パーティクルなどであれば G-Buffer を見てスクリーンスペースで当たり判定を取るテクニックも使えます。
整合性確認のために (SD) Unity ちゃん にご登場頂きました。Unity ちゃんの隣の球も通常の Unity の 3D オブジェクトです。前後関係もライティングも一貫しているし、影もちゃんと落ちているのが見て取れると思います。
raymarching & distance function を使うと、ポリゴンベースの手法に見慣れている人ほど面食らう面白い絵を出せるんじゃないかと思います。
ただ、問題として、制御が難しくて細かい調整が大変なのと、激烈に重いというのがあります。
頂点負荷はほとんどないものの、ピクセルシェーダがものすごく重くなるため、GPU がしょぼいくせに解像度はやたら高い iOS / Android デバイスでは現状まともな速度を出すのは絶望的です。ノート PC 用の GPU でもかなり厳しいです。デスクトップ PC や PS4 で限定的な使用であればなんとか実用に耐える…といいな、というレベルです。
とはいえ、比較的少ない労力で超ディティールの絵を出せるので、この手法がごく普通に使えるまでにハードウェアが進化した時、特に小規模開発チームにとって強力な武器になりうるはずです。
Unity4 で今回のようなことをやりたい場合、標準のレンダリングパイプライン使うのは諦めて全部自力でやるしかなかったのですが、Unity5 になって、このように特殊な処理だけ自力でやって他は Unity に任せる、というのがやりやすくなりました。自分的にすごく嬉しい変化です。
また、今は CommandBuffer には DrawProcedural() がありませんが、近い将来追加されるそうです。そうなったら、インスタンシングで描いたオブジェクトを Unity の deferred パスでライティングする、ということができるようになります。これは自分的に長らく待ち望んでいたものです…!
CommandBuffer & deferred shading が加わったことで、raymarching 以外にも様々な G-Buffer 加工トリックが実現できるようになったはずです。近い先以前やった モデルの boolean 演算 などを使いやすい形にして移植してみる予定です。
[2016/03/09] 追記
下記の CommandBuffer.SetRenderTarget() の問題はいつの間にか修正されているようで、正確なタイミングは不明ですが少なくとも 5.2 系では RenderTarget 指定は機能するようです。
余談ですが、実装上の注意点というか自分がハマった点。
CommandBuffer.SetRenderTarget() には現状任意の RenderTexture を設定することはできないようです。builtin 型か、temporary なものに限られます。コードを見た方がどういうことか理解しやすいと思います。
var cb = new CommandBuffer(); { // これは ok var rt = new RenderTargetIdentifier[4] { BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer1, BuiltinRenderTextureType.GBuffer2, BuiltinRenderTextureType.GBuffer3, }; cb.SetRenderTarget(rt, BuiltinRenderTextureType.CameraTarget); } { // これも ok int rtid = Shader.PropertyToID("MyRenderTarget"); cb.GetTemporaryRT(rtid, -1, -1, 0, FilterMode.Point); cb.SetRenderTarget(rtid); } { // これは機能しない! var rt = new RenderTexture(1280, 720, 0); cb.SetRenderTarget(rt); }
幸い temporary なものも勝手に消されたりはしないようで、複数フレームにまたがる処理なども問題なく実装できました。必要であれば OnPostRender() とかの中で内容をコピーするシェーダを走らせれば RenderTexture に内容を移すこともできます。
CommandBuffer については 公式にいいサンプルが提供されており、導入には最適だと思われます。
また、いつも通り、込み入ったシェーダを書く場合、公式に提供されている Unity の builtin シェーダのソース が役に立つというか事実上必須になります。
また、Unity5 になって稀に Direct3D の時だけ挙動がおかしくなるシェーダがあるのですが、#pragma enable_d3d11_debug_symbols を入れると直るという現象に遭遇しています。気持ち悪いですが明確な原因は突き止められず、ひとまずこれで凌いでいます。
それと、どうも WebGL では deferred パスは使えないようです。たぶん WebGL 1.x には multi render target がないためで、だとすれば対応は WebGL 2.0 を待つしかない気がします…。