読者です 読者をやめる 読者になる 読者になる

rendering fractals in Unity5

pseudo_kleinian_with_unitychan
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 と呼ばれるので、区別するにはこちらの方がいいのかもしれません。
http://http.developer.nvidia.com/GPUGems2/elementLinks/08_displacement_05.jpg

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() 関数で式を選びます)
tglat_formulakleinian
左: 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 を見てスクリーンスペースで当たり判定を取るテクニックも使えます。

rendering_fractal_in_Unity5
rendering_fractal_in_Unity5b
整合性確認のために (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 を待つしかない気がします…。

blue impulse / TokyoDemoFest2015


webplayer: http://primitive-games.jp/Unity/blue_impulse/
source: https://github.com/i-saint/BlueImpulse

TokyoDemoFest2015 にて PC demo を発表しました。その映像、バイナリ、ソース一式を公開します。
今回も Unity 製です。会場のマシン (Windows & GeForce GTX 780) で動けばいいやという想定で作ってるので相当重いです。Direct3D11 必須。

今まで積み上げてきたリソースを組み合わせて短期間で映像作品に仕立ててみよう、というチャレンジのもとに制作に挑みました。水面やポストエフェクトの追加などレンダリングエンジンへの機能追加に約 1 週間、シーンデータの作成に約 1 週間で、製作期間は 2 週間くらいです。
今回もサウンドは カワノさん が担当しており、本格的にシーン制作にとりかかるよりも先に求めていたイメージずばりな曲を上げてきてくれて、制作のはずみになりました。

レンダリングエンジンは独自のもの (exception reboot と同じ) であり、Unity 標準のシェーダは一切使っていません。また、意識して縛りプレイしたわけではないですが、結果的に今回も使ったモデルは Cube と Quad だけになりました。
ゲームエンジンを使う場合、今回みたいに レンダリングエンジンだけ他のプロジェクトに流用し、そのフィードバックを元プロジェクトに返す、みたいなことが簡単にできるのが 1 つの強みである、というのが今回得られた実感でした。レンダリングエンジンを鍛えるためにも、今回みたいな試みはたまにやっていきたいところです。


今作はいつも以上にポストエフェクトに依存した絵作りになっています。
GBuffer 書き出してライティングした状態。
blue_impulse_breakdown1

視線->法線の角度が浅いところを明るくするエフェクト、ブルーム、スクリーンスペース反射を加えた状態。
blue_impulse_breakdown2

水面&コースティクスエフェクトを加えた状態。
これらを実装するにあたって鍵となるノイズ関数は、こちらに使われてるものを拝借いたしました。一般的な perlin ノイズより軽量で結果は十分良好なように見えます。
blue_impulse_breakdown3

トーンマッピング、指向性ブルーム、走査線、周辺減光を加えて最終イメージ図。
blue_impulse_breakdown4

パーティクルは以前実装した GPU パーティクルです。trail も compute shader で頂点を生成しています。
blue_impulse

一部の要素は過去の記事で詳細な解説がなされています。
deferred shading in Unity - primitive: blog
Temporal Screen Space Reflections - primitive: blog
render massive amount of cubes in Unity (2) - primitive: blog
pseudo-instanced drawing in Unity - primitive: blog


TokyoDemoFest には毎年行っていますが、今回からはオーガナイザの一人として運営の方にも関わっています。
今回は間違いなく過去最高に盛り上がった回で、投稿作品も多く、レベルも高く、いろんな方と技術トークできて幸せなひとときでした。
特によっしんさん (@yosshin4004) の Optical Circuit が信じられないくらいすごくて、初めて elevated を見た時級の衝撃を受けました。魔法って実在するんだ、というのを改めて実感しました。

バイナリ

TokyoDemoFest は来年の開催に向けてスポンサーをやってくれる方 / 企業様を募集しております。

pseudo-instanced drawing in Unity


Unity で大量のオブジェクトを描画するスクリプトを書きました。
https://github.com/i-saint/BatchRenderer
簡単な使用例

いつも通り弾幕やらパーティクルやらを描くのを想定した代物ですが、割と簡単に使えてポータブルな作りになっています。Windows で D3D11, D3D9, OpenGL モードでいずれも動作。Android でも動作を確認しています。
スクリプトの使い方については上記ページを参照していただくとして、この記事ではこのスクリプトを作る過程で得られた (バッド) ノウハウ群を書き残しておこうと思います。過去の記事と内容が被ってる部分が多くありますが、その多くは本記事でより洗練されています。


まずこれを作った背景。
OpenGL や Direct3D では、大量のオブジェクトを描画するにはインスタンシング描画を用いるのが一般的です。Unity にも一応 インスタンシング描画機能は備わっているのですが (Graphics.DrawProcedural())、残念ながらこれは Unity の描画パイプラインに組み込むことができません。
Unity の MeshRenderer は描画リクエストをキューに突っ込み、優先順位に基いて実際の描画処理を行うようになっています。一方、Graphics.DrawProcedural() は呼んだその場で描画処理を行います。なので、描画キューに入っているリクエスト全てが処理される前か後にしか描くことができず、不透明オブジェクトパスで描画、みたいなよくある要求を満たせません。
さらに、surface shader も使えなくなるため、Unity のシェーダと一貫したライティング処理を行うのも難しくなります。おまけに現状 Direct3D11 が使える環境か PS4 でしか使えません。
よって、Unity の描画パイプラインを使いつつポータブルに大量のオブジェクトを描画したい場合、他の手を考える必要があります。BatchRenderer はそういう要求のもとに作られました。


多数のオブジェクトを描画したい場合、一番ストレートな方法は描きたいオブジェクトの数だけ GameObject (MeshRenderer) を用意する、というものですが、まあ、遅くて現実的ではありません。アクティブな GameObject は存在するだけで結構な負担になります。
他に使えそうな手として、Graphics.DrawMesh() という API があります。これは Mesh を描画するリクエストをキューに突っ込む、というもので、つまり MeshRenderer がやってることを GameObject なしで実現できる API になります。
GameObject なしで Unity のレンダリングパイプラインを使う描画処理をやるにはたぶんこの API を使うしかないのですが、単純に描きたいオブジェクトの数だけ DrawMesh() を呼ぶ、というも遅くてやや厳しいです。描画キューを処理するにあたって内部的に複雑な前処理が行われるため、1 つの drawcall が OpenGL や Direct3D の世界よりずっと重くのしかかります。
数千くらいなら個別 Graphics.DrawMesh() でもなんとかなるかもしれませんが、それ以上になるともう一歩踏み込んだ方法が必要になります。

より大量のオブジェクト描画を実現するため、BatchRenderer では擬似インスタンシングを用いています。これは 1 つの Mesh に多数のモデルを格納し、頂点シェーダで各モデルを動かすことで 1 回の Graphics.DrawMesh() で多数のオブジェクトを描く、というものです。
Mesh オブジェクトは 1 つにつき 65000 頂点までしか格納できないため、65000 / モデルの頂点数 が 1 回の Graphics.DrawMesh() で描ける限界になります。 例えば cube の場合、24 頂点なので 2708 個が 1 つの Mesh に収まる数になります。2708 個、頂点の位置も法線も同じ cube がぎっしり重なって格納されているイメージです。後述のモデル ID だけがそれぞれのモデルで異なります。

より詳細に BatchRenderer の内部で行われていることを説明すると、まずユーザーが指定した Mesh のデータをコピーして、上記のような多数のモデルが格納された Mesh を生成します (65000 / モデルの頂点数 分)。この時、Mesh.uv2 にはそのモデルが何番目かという ID を格納します。(元の uv2 は失われますが、あまり使われないので妥当な格納場所と判断)
次に、ユーザーが指定した Material の複製を作ります。1 回の Graphics.DrawMesh() で描ききれない場合、複数回 Graphics.DrawMesh() を呼ぶ必要がありますが、その回数分の Material を必要に応じて生成します。各 Material には、オブジェクトの ID が何番目から始まるか、という情報を付与します。この開始 ID + モデルの ID でオブジェクトの ID を得られるようにするわけです。
このオブジェクト ID を元に頂点シェーダ内でオブジェクトの位置や回転などの情報を取得し、変形処理を行って描画します。
(例えば cube を 10000 個描きたい場合、10000/2708 で 4 回の Graphics.DrawMesh() が必要なため、Material の複製も 4 個作ります。各マテリアルの開始 ID は 0, 2708, 5416, 8124 となります。 ちなみに 10000 以降のモデルは頂点シェーダで vertex.xyz = 0.0 として画面に出ないようにします)


頂点シェーダからオブジェクトの位置などの情報にアクセスできるようにするには、事前にスクリプト側でデータを集めて GPU 側にアップロードしておく必要があります。これがなかなか厄介で、今回の話の主題はここになります。
頂点シェーダからアクセスできる任意長のデータは、テクスチャ (sampler2D) かバッファ (StructureBuffer) の 2 種類になります。

バッファの場合話は単純で、スクリプトから ComputeBuffer オブジェクトを生成し、それを Material.SetBuffer() でアタッチし、、以降 ComputeBuffer.SetData() でデータを更新するだけです。
とても簡単でまあまあ速くて任意の構造 (struct) のデータを格納でき、あとフリー版でも使えるというメリットもあります。しかしながら、現状 Direct3D11 が使える環境か PS4 でしか使えません。
近年はモバイルデバイスもハードウェア的にはこの機能はサポートしており、将来的には今回のような用途にはバッファを使うのが定石になると思われますが、残念ながらポータビリティを考慮すると現状テクスチャを使う方が有力な選択肢になります。

テクスチャの場合 Texture2D …を使いたいところですが、Texture2D はなぜか float や half のフォーマットをサポートしていないため、これらをサポートしている RenderTexture で代用する必要があります (よって、フリー版では使えません)。そして RenderTexture にはなぜか SetPixels() がないため、面倒なことをする必要があります。
RenderTexture に任意のデータを書き込む現実的な方法はたぶん 2 通りで、1 つはプラグインを用いる方法、もう 1 つは Mesh にデータを格納して Graphics.DrawMeshNow() で書き込む方法です。

プラグインを用いる方法。
Texture 一族は GetNativeTexturePtr() という、ネイティブ API (OpenGL や Direct3D) のテクスチャオブジェクト (ID3D11Texture2D* など) を取得するメンバ関数を提供しています。なので、ネイティブ API を用いてテクスチャにデータを書き込むプラグインを作り、スクリプトからテクスチャオブジェクトとデータポインタを渡せば目的を達成できます。
この方法はデータのコピーに mono を一切介さないため速いというメリットがあります。
当然ながら、プラグインはプラットフォームごとに個別の処理を書いてビルドする必要があります。プラットフォームによってはプラグインを組み込むのが大変で、実装難度が高くなります。また、WebPlayer や WebGL (Unity5) だと使えないというポータビリティ面での問題もあります。
また、環境によっては float のテクスチャはサポートしておらず、half のみだったりするので、どちらでも対応できるようにしておいたほうがいいでしょう。

Mesh & 描画による方法。
Mesh.vertices などにデータを格納し、データを書き込むシェーダを用意し、データ用 RenderTexture をレンダーターゲットに設定して Graphics.DrawMeshNow() で描くことで、Mesh に格納したデータを RenderTexture に移すという方法です。
ほとんどのプラットフォームで使えてスクリプトだけで完結するポータビリティが高い方法ですが、まず Mesh にデータを格納するのが遅い上、それを GPU にアップロードして drawcall を発行してそれが完了するのを待たないといけず、他の方法と比べて致命的に遅いという問題もあります。
実装の際の注意点として、プラットフォームによっては Mesh の normals, tangents, colors は内部的に正規化されたり clamp されたりするようです。(少なくとも Android では。ハマりました…) よって、ポータビリティを考えるとデータの輸送には vertices, uv, uv2 しか使えません。その分 Mesh にデータを書き込んで Graphics.DrawMeshNow()、を何度も繰り返すことになります…。

テクスチャを使うアプローチ共通の注意点として、格納できるデータは float/half の羅列に限られるので、任意の構造体を格納できるバッファに比べると柔軟性は落ちます。とはいえ、ビットフラグとかを格納するのでなければあんまり問題にはならないとは思われますが。
また、古い環境では同時に使えるテクスチャの数の上限が大きな障害になります。(Direct3D9 / ShaderModel 3.0 だと 4 つ。4 つ!!)
ちなみに頂点シェーダでテクスチャにアクセスするには tex2Dlod() を用います。

まとめると、任意のデータを GPU に転送してシェーダからアクセスできるようにする方法は大きく以下の 3 通りになると思われます。

1. ComputeBuffer
pros
・実装が非常に楽
・任意のデータ構造を格納可能
・フリー版でも使用可能
cons
・現状 Direct3D11 が使える環境か PS4 でしか使えない

2. RenderTexture にプラグインからデータを書き込む
pros
・速い
cons
・プラットフォーム毎の個別対応が必要
・WebPlayer や WebGL では使えない

3. RenderTexture に Mesh 経由でデータを書き込む
pros
・とてもポータブル
cons
・とても遅い

速度: RenderTexture&プラグイン > ComputeBuffer >>>>>>>> RenderTexture&Mesh
ポータビリティ: RenderTexture&Mesh > RenderTexture&プラグイン > ComputeBuffer
実装容易性: ComputeBuffer > RenderTexture&Mesh > RenderTexture&プラグイン

どれもすごく一長一短で悩ましいです。
BatchRenderer はこの 3 種類を全て実装し、ComputeBuffer か RenderTexture&プラグイン が使えなければ RenderTexture&Mesh にフォールバックするようにしています。


以下余談。
GeometryShader を使うと状況によっては一回の Graphics.DrawMesh() で描ける数をさらに増やせるのですが、今回はそこまでやっていません。理由はいっぱいあって、Unity の GeometryShader は Direct3D11 が使える環境でしか使えないこと (今は他に使える環境があるかもしれませんが、いずれにせよポータビリティは低いです)、surface shader と併用できないこと、ビルボードなどの本当に単純なモデルにしか適用できないこと、Unity の GeometryShader サポートがいまいちやる気が感じられないこと、などです。

また、BatchRenderer ではやっていませんが、応用として Graphics.DrawProcedural() の Unity のレンダリングパイプラインに載せられるバージョンに近いものも実現できます。
まず実際のモデルデータを ComputeBuffer に格納。それとは別に vertices に頂点 ID のみを格納した Mesh を用意。描画の際、頂点シェーダは 頂点 ID をインデックスとして StructuredBuffer から実際のモデルの頂点データを得てそれを出力する、みたいなやり方です。
ComputeShader でモデルを生成してそれを Unity のレンダリングパイプラインに載せて描きたいような場合、現状こういう方法を取る他ないと思われます。

バッドノウハウ以外の何物でもないですが、Direct3D11 世代機能は Unity にはあとづけなのでしょうがない…と納得する他ありません。たぶんきっと将来的に改善されていくことでしょう。

after C87

C87、スペースに立ち寄ってくださった方々、ありがとうございました。

exception reboot 体験版ですが、Visual Studio 2013 ランタイムが入ってないとパーティクルや背景が出ないというミスをやらかしてしまいました…。同現象に見舞われている方はこちらをインストールしてお試しください。
http://www.microsoft.com/ja-JP/download/details.aspx?id=40784 (vcredist_x86.exe の方)
また、ビデオカードによってはエフェクトが正常に出ないようです。今後調査して直していきます。

今回 1 ステージだけなので意識的に難しめにしています、が、ちょっとやりすぎたかもしれません…。コントローラ対応が中途半端だったり、自機を見失いやすいなどのプレイアビリティに関しては追々改善していきます。
最終的に売り物にしつつもソースは公開、Unity インストールして自力でビルドすれば無料で遊べて、自分でステージ作って他のプレイヤーと共有することもできる、みたいな形を目指しています。2015 年中の完成が目標です。

あとソースに関して、readme に書き忘れたんですが、ビルドには Unity 4.6.1 p1 以降が必要になります。これ未満 (現時点の public release 含む) だとコンパイルエラーが出てしまうと思います。
Assets/Scenes/Title.unity を開くと Unity のエディタ上で今作がプレイできるはずです。



以下、冬コミ版の後書きとか Unity 使ってみた雑感とか。

今回の冬コミ版、パーティクルエンジンとレンダリングエンジン以外の純粋なゲーム部分は、大体 10 日くらいで作っています。ボス作るのが楽しく大部分の時間リソースをそっちに割いてしまい、ステージ道中はかなりお粗末になってしまいました…。メニュー類やシーンの遷移についてはコミケ前日の 1 日で仕上げました。Unity なしでは成り立たない無茶な進行だったと思います。
当初はもうちょっと余裕がある進行にする予定だったんですが、パーティクルとレンダリングの方に時間を取られすぎてしまいました。

自分的今作のハイライト
boss


この半年くらい Unity 絡みの仕事したり、Unity 上で動くデモプログラムを作ったりはしてきましたが、ゲームらしいゲームを作るのは今回が初めてでした。やってみた感じ、やはりこれまでより大分楽で、ステージ作る能率は無印 exception の時より 3 倍以上上がったと思います。

自分で意外だったのが、Unity を使い始めてから、逆に独自のレンダリングエンジンにこだわるようになりました。
Unity を使いはじめる以前は、レンダリングエンジンとか最も既存のゲームエンジンに任せたい部分、と思っていたんですが、いざ実際にゲームエンジンを使ってみると、プログラマー兼アーティストの自分の場合それなりの独自性と最低限のクオリティを両立した絵を出そうと思ったらレンダリングエンジン独自に書くのが一番早い、という結論に至りました。
Unity だとシェーダとレンダリングに絡むコードの変更をゲームに反映させるサイクルが速く、以前よりシェーダ書くのが楽しくなったというのも要因としてあります。

なんか会場で 5 人くらいに「これ Unity 上で動いてはいるけど Unity の機能使ってないですよね?」的なことを言われて意外だったんですが、シェーダとパーティクルが独自なだけで他はちゃんと Unity です。
今どきレンダリングエンジンはゲームエンジンのほんの 1 機能に過ぎないので、自力でやるモチベーションがある部分はフルコントロールして、そうでない部分はゲームエンジンに委ねる、というやり方でいいんじゃないかと思います。自分の場合、パーティクルとレンダリングはそれなりにこだわりがあるものの、エディタ類とかには極力労力を割きたくないのでこういう作り方にしました。自分的にゲームエンジンはレベルエディタとして優秀であればそれで十分です。

今回は Rigidbody、Animation、uGUI あたりを大いに活用しています。
Collider と Rigidbody コンポーネントを追加すれば物理挙動を実現できるお手軽さは素晴らしくて、物理を活用したギミックは今後も増やしていきたいところです。
Animation はステージのスクロールやボスの砲台の制御なんかに使っています。タイムラインエディタはもちろん、任意のタイミングで任意の関数を呼ぶ機能 (Animation Event) が重宝しています。ボスのビームの発射の機能や、ステージのセクションの切り替えは Animation Event による制御です。
uGUI。4.6 から入った UI システムですが、趣味と仕事で過去 3 回くらいインゲーム UI システム書いた身としては、こんな使いやすいのが標準にあることに感動です。UI システムは最低限の機能が揃えばあとは比較的 誰が作っても最終的な見た目にそこまで差は出ない 部分だと思うので、デファクトスタンダードとなりうるものが出てくることには大きな意義あるんじゃないかと思います。

逆に今回 Unity 使ってて不満だった点も挙げると、
C#。Unity というか mono が原因ですが。去年この blog に何度も書いてきましたが、C# で巨大な配列を巡回して処理を行うのは死ぬほど遅くて、この手の処理についてはプラグインか ComputeShader 書く他ありません。pure C# で 10 万パーティクル相互衝突とかたぶん 10 年未来のマシンでも 60 FPS 出すのは無理でしょう。
裏を返すと、プログラマーがゲームデザインをする際最もアドバンテージがあるのがこの領域、とも言えると思います。

スクリプトの実行時編集。スクリプトが編集されるとオブジェクトをシリアライズして、アセンブリをリロードして、オブジェクトをデシリアライズする、という手順を踏みますが、このへんそれなりにちゃんと理解していないと、ちょっとプロジェクトが複雑になると破綻するんじゃないかと思います。static オブジェクトの扱い、Action などのシリアライズ非対応型など、色々罠があります。ComputeBuffer など UnityEngine のオブジェクトなのにシリアライズ非対応な連中も存在していてなかなか厄介です。
ちゃんと対処するのはめんどくさそうだったので、今回は途中から実行時編集は諦めました。代わりにエディタ上でステージの途中から開始することでイテレーション速度を保つようにしています。
過去に頑張って C++ で同等機能を実装してた身からすると、なんというか予想外に親しみが湧くリアルワールドな世界でした。


今回の冬コミ版の制作を通して、自分のようなオールドタイププログラマーでも Unity とはよろしくやっていけそう、Unity で自分の作りたいゲームは十分作れそう、という確固とした実感を得られました。

C87 / exception reboot


冬コミ参加します。3日目西え25a。
Unity で exception の続編的ゲームを作ろう、という実験も兼ねた挑戦をやっていて、その経過の作品になります。またしても体験版です…。
100 円、いつも通りソースコードも同梱します。また、Direct3D11 が使える GPU が必須になります。
今回もカワノさん (@pocomo) が曲を書いてくださいました。当日は二人で売り子している予定です。

ちなみにソースコードについては、パーティクルエンジンは これ、レンダリングエンジンは これ なので、今回のパッケージに技術的な見どころは特になかったりします…。
パーティクルは CPU 実装ですが、背景のキューブの群れは移動から描画まで GPU で完結するようにしていたりします。
以前の作品 が重すぎた反省から、今回はモバイル GPU でも快適に動くように気を使いながら開発していて、私の Core i7 2.3 Ghz x4 & GetForce GT 750M なノートでも、720p の標準設定であればほぼ 60 FPS で動作するようになっています。(経験的に、M がつく Geforce は、M がつかない同レベル型番の半分以下のパワーです…)

ここ数年ずっとゲームを完成させられずにいるので、来年これを完成させる、というのが当面の死守すべき目標になりそうです。

render massive amount of cubes in Unity (2)

以前 検証した Unity で大量の cube を描く方法の続きです。あれからまたいくつか検証を重ねてきたのでまとめておこうと思います。

まず前提として、今回解説する方法は全て Graphics.DrawProcedual() を使うアプローチです。以前の記事を書いた時点では気づいていなかったのですが、Graphics.DrawProcedual() を使うと instancing 描画ができます。
残念ながらこの API、現状 OpenGL 系のプラットフォームでは使えません。つまり Android & iOS では使えず、PC でも Windows のみとなってしまいます。しかし近い将来いろんなプラットフォームでサポートされていくはずです。(近年のモバイルデバイスはハードウェア的には既に instancing 描画をサポートしています)

この Graphics.DrawProcedual()、D3D11 の DrawInstanced() とかとはやや違う不思議な仕様になっていて、vertex shader の入力に頂点データを渡せません。頂点 ID (SV_VertexID)、インスタンス ID (SV_InstanceID) のみが入力となります。頂点データは自力で ComputeBuffer を生成&アサインする必要があります。
また、この API を使う場合、たぶん Unity の surface shader は使えず、ライティング処理なども自力で書く必要があります。(Unity 5 であれば G-Buffer 書き出しだけ独自に行い、ライティングは共通処理を使えると思いますが、未検証)
総じて、遺憾ながら、汎用性や既存の Unity の機能との親和性では、以前の記事の擬似 instancing の方に分があると言わざるをえません。しかし使える環境であればこちらの方が実装はスマートかつ VRAM 使用量がずっと少なく済みます。


前回の記事同様、C++ でパーティクルの計算を行い、パーティクルデータを Unity 側のテクスチャに書き込みます。
立方体の頂点データは ComputeBuffer を用意してその中に格納しておきます。前回の擬似 instancing と違い、一個分の頂点で十分です。
そして DrawProcedual() で描きます。頂点シェーダの インスタンス ID からそのパーティクルのデータがあるテクスチャ座標を計算して取得、頂点を動かしたりなどの処理を行います。
(以下は Core i7 2.3 Ghz x4 & GetForce GT 750M での結果)
ss
ソース&プロジェクト一式

ComputeBuffer であれば任意の構造のデータを格納できるので、パーティクルのデータもテクスチャではなく ComputeBuffer に入れたいところですが、ComputeBuffer.SetData() に毎フレームでかいデータを渡すと超遅くて話にならないので諦めました。
実は、以前の Mono の API を使って C++ から C# の機能にアクセスする 方法の検証、あれの目的の半分くらいは ComputeBuffer.SetData() を C++ から呼ぶことで高速に GPU にデータを転送することでした。しかし、実際にやってみたところ全然速くなくて、もう Mono を介した時点で毎フレーム大量のデータをやりとりするのは無理がある、ということがわかりました。

可能性がありそうなまだ検証できていないアプローチとして、ComputeBuffer.SetData() の先にある Unity の内部 API を C++ から直接呼ぶ、というものがあります。
このアプローチの障害として、該当 API は dllexport すらされていないので、まともな方法ではアクセスできません。Development ビルドであれば生成されるデバッグ情報に該当 API の情報が残っているので dbghelp の API を用いることでアクセスできるはずですが、非 Development ビルドでそれをどうやるかは目下考え中です。
また、ComputeBuffer の内部変数からプラットフォーム固有オブジェクト (ID3DBuffer* など) を取れないか調べてみましたが、ComputeBuffer が保持してるのはハンドルであり、結局 Unity の内部 API を呼ぶ必要があるので諦めました。
とはいえ、これらの未検証アプローチも速度的には今回の テクスチャにインスタンスデータ置いて DrawProcedual() で描くのとほとんど差はないはずで、互換性重視の前回の擬似 instancing & もうちょっとスマートで低 VRAM 使用量の今回の汚い instancing が現実的な落とし所ではありそうな気がします。


あとは前回放り投げた compute shader を使うアプローチを試してみました。Unity の compute shader は程よく抽象化されてて、既に GPGPU プログラミングをやったことある人であればかなり楽に感じられるんじゃないかと思います。このスレッドの Aras のサンプルプログラムを見れば大体の使い方はわかるでしょう。
パーティクルを compute shader で実装する場合、あらかじめパーティクルデータを ComputeBuffer で最大個数分確保しておき、compute shader でぶん回します。レンダリングの際はその ComputeBuffer をそのまま Material.SetBuffer() で設定し、vertex shader でインタンス ID からパーティクルデータを取得してよろしくやるだけです。

csparticle
ごく普通にパーティクル生成。いつも通り hash grid + bitonic sort で高速化してパーティクルの相互衝突を実現しています。

GBCollision
G-Buffer 見てスクリーンスペースで当たり判定を取るというアレ。床の穴は boolean で開けています。こういうのは GPU particle ならではという感じです。

VectorField
vector field。空間を適当に等間隔グリッドで切り、各グリッドに乱数で適当なベクトルを与え、それをパーティクルの動きに加算しています。実装は簡単ながら、劇的な効果が得られました。

TestException
exception ぽい簡単なゲーム。GPU <-> CPU 間データ転送は遅いとはいえ、今どきの PC ならこれくらいは割と動いてくれる感触です。

fluid
SPH。簡単なゲーム仕立て。SPH 自体は DirectX SDK のサンプルについてるもののほぼベタ移植です。

ソース&プロジェクトは これ に含まれています。

やってみたところ、これまでの記事で解説したような、C++ でプラグイン書いてヘンな方法で GPU にデータ渡すよりもずっと楽でした。WebPlayer で動くのも素晴らしいです。なかなかいいプログラマのおもちゃなんじゃないかと思います。
ただ、個人的な実感として、compute shader を使うアプローチはデスクトップ GPU 環境であれば十分ありだと思いますが、モバイル系の非力な GPU まで考慮すると慎重にならざるを得ない、という感触です。
今どき CPU パワーは大抵余ってると思いますが、GPU に関しては未だにちょっと凝った絵出そうとしたら簡単にパワー使い果たしてしまいます。その上 GPU は CPU と比べるとスペックの上限下限の幅が広く、オンボード GPU とデスクトップ向けハイエンド GPU では 10 倍とかのオーダーでパワーに差があります。
ゲームロジックには関係ないエフェクト類であれば、重ければ無効化すればいいので好きに実装していいのですが、例えば上の exception 的ゲームの例のようにゲームロジックに直結する機能を GPGPU で実装すると、GPU が非力な環境では GPGPU だけでほぼ手一杯になってグラフィックにパワーが割けなくなってしまいます。(atomic を作ってた時これを実感してパーティクルの実装を GPU から CPU に戻したという経緯があるのですが、今も同じ感触です)
ただ、PS4 とかだと逆に GPU がパワフルで CPU がしょぼいので、重いゲームロジックを実装する場合、PC とコンソールで大きくアプローチを変える必要が出てくるかもしれません…。


あとは、pixel shader でパーティクルの計算を行う方法 (=compute shader 登場以前の GPGPU) も試そう…と思ったのですが、RenderTexture には GetPixel() がなく、GPU -> CPU へのデータ転送が超面倒だとわかって結局やらずじまいになっています。
手順だけ解説すると、例えば私のパーティクルエンジンの場合、パーティクルデータは一つ 48 byte、float 12 個分です。なので、ARGB float フォーマットの RenderTexture を 3 枚用意し、各 pixel を 1 つのパーティクルとみなして計算を行い multi render target でこの 3 枚にデータを出力することで、pixel shader でパーティクルを計算 & 結果を保持することができます。
前時代的な方法ですが、遺憾ながらポータビリティを考えると今でも有用と言わざるを得ません。GPU 内で完結する純粋なエフェクト用パーティクルであればこの方法でも十分行けるとは思います。AssetStore を見てると、たぶんこの方法で実装してると思われるパーティクルエンジンが散見されます。

Temporal Screen Space Reflections


Temporal Screen Space Reflection

以前諦めた真面目なスクリーンスペース反射の実装に再挑戦して、今回は割といい成果を出せました。
この成果に至る過程が長く苦しくもおもしろかったので書き残しておこうと思います。

スクリーンスペース反射はその名の通り、スクリーンスペースで反射の映り込みを実現するもので、G-Buffer の法線と深度を見てレイマーチでトラバースすることで実現します。
画面内にあるものしか映せないという致命的な欠点こそあるものの、本来レイトレが必要なはずの任意面反射を破格のコストで実現することができ、Crysis2 が採用して以降、近年の大型タイトルではもはやデファクトスタンダードとなっています。


まずは素朴な実装で、G-Buffer の深度と法線から、カメラ -> pixel 位置に対する反射ベクトルを求め、深度バッファをレイマーチしてみました。
しかし、実際にやってみるとこれだけでは綺麗な結果は得られないことがわかります。


TSSR wip1

画面中央下部のような、ヘンな階段状の隙間が出てしまうのです。静止画だとわかりにくいですが、実際にカメラを動かしてみると許容しがたいレベルで気になると思います。
これはレイがオブジェクトの端を貫通してしまうために起きる現象です。レイマーチングではレイは非連続なため、レイを進める際、間に何かあっても衝突を検出できずに突き抜けてしまいます。その結果このようなアーテファクトが出てしまいます。
レイを進める間隔を極限まで小さくすれば起きなくなるはずですが、そうすると超でかいステップ数にしないと必要な距離をトラバースできず、重くて現実的ではありません。
前回諦めたのはこれにうまく対処する方法がわからなかったためでした。


こういう問題には大抵乱数を用いてごまかす対処方法が用いられるそうです。
レイマーチの際、乱数で適当に下駄を履かせます。具体的には各レイを事前に 1ステップ分の距離 * 0.0~1.0の乱数 だけあらかじめ進めておく感じです。
そうすると以下のような結果が得られます。(乱数は現在 pixel の位置をハッシュする方式)


TSSR wip2

ノイジーになりましたが、階段模様よりはマシになった気がします…が、これもカメラが動くとちらつきまくって許容できないレベルで気になります。しかし本番はここからで、これを最近流行りの temporal というやつでいい感じにします。
乱数を毎フレーム結果が変わるようにして (位置*現在の時間とかをハッシュする方式に変える)、過去のフレームの結果を累積して平均値を結果として出します。これによりノイズが平均化され、レイが貫通しにくいところはそのまま、レイが貫通しやすいところは色が薄くなります。


result

劇的な改善が見て取れると思います。実装してて感動した部分でした。
結果が安定するのに数フレーム時間がかかる (=それまでノイズが目立つ) のが難点ですが、大抵はその欠点を補って余りある効果が出せるんじゃないかと思います。
(もちろん temporal の代わりに 1 フレーム中に気が済むまでループを回すことで即座に良好な結果を得られますが、重すぎて現実的ではありません…。累計値が低いところは 1 フレームに複数回回す、のようなアダプティブ方式はありかもしれません。)

累積する一方だとカメラが移動した時残像が残ってしまうので、移動量などに応じて累積結果を減らす処理も入れます。
前フレームの深度バッファとカメラ行列を保存しておき、現在の pixel の位置を前フレームのカメラ行列でリプロジェクションして、そのスクリーン座標の前フレームの深度バッファから位置を取得し、現在 pixel 位置との差分 (=移動量) を求めます。これの大きさによって累積中の結果を適当に減衰させます。

ここまでを実装したのが今回の成果になります。
今回の例では減衰はやや弱めにしており、やや残像感が出てしまっています。その代わりカメラがゆっくり移動する時のノイズが低減されています。ここらへんは悩ましいトレードオフです…。
あと、映り込むオブジェクトは変化しても累積結果を減らしていないので、変化が映りこみに反映されるまでにラグが発生します。例えば今回の例だと光の溝の変化が映り込むまでにラグがあるのが見て取れます。たぶんそんなに気にならないだろうということで今は対処していません。


実装に際しては GPU Pro5 を参考にしました。もっとも、GPU Pro5 の記事の主題は "Hi-Z Screen-Space Cone-Traced Reflections" という、もう一歩アドバンスドな方法だったんですが、そっちはよくわからず実装には至っていません。今後の課題です。しかしこの記事はスクリーンスペース反射が基礎からフォローされてて悪くはない内容だったと思います。
また、Hi-Z ... の概要については hanecci さんが詳細にまとめられておられます

余談ですが、今回のデモの光の溝は box folding パターンというやつを用いています。最近詳細を知ったんですが、プログラマー兼アーティスト的に心強い味方となってくれそうです。
http://glslsandbox.com/e#21290.5