Unity-Chan "Candy Rock Star"
以前の記事で触れたユニティちゃんライブステージが、ようやくプロジェクトデータが公開されました。是非手元で動かしてみてください。
https://github.com/unity3d-jp/unitychan-crs
これに合わせて、ステージの床のシェーダのメイキングを書きました。
例によって私の趣味全開で distance function で模様を生成しています。
https://github.com/unity3d-jp/unitychan-crs/wiki/Visualizer
また、@_kzr さんによるプロジェクトの開発は Unity で開発する上で参考になると思います。
https://github.com/unity3d-jp/unitychan-crs/wiki
introduction to mono
mono の API を使って C++ から直接 Unity の C# のオブジェクトを触る、という実験を最近やっていたので、その成果を書き残しておきます。
mono を使うことで pinning や marshalling などの余計な処理を介さず C# と C++ を連携させよう、という趣旨です。Unity のプラグインから使う前提で書いていますが、Unreal Engine の場合でもあてはまることは多いと思われます。
mono の組み込みは公式に入門ドキュメントが用意されており、これがいいとっかかりになってくれました。
http://www.mono-project.com/docs/advanced/embedding/
また、突っ込んだことをやろうとするとソースを読む根性が必要になると思われます。Unity の mono はカスタムが入ったものになっています。
https://github.com/Unity-Technologies/mono (本家: https://github.com/mono/mono )
C# から C++ の関数を呼ぶ
C++ の関数を C# に登録するのは何通りか方法があるようですが、ここでは P/Invoke と mono_add_internal_call() を使う 2 通りの方法について触れます。
P/Invoke は、dllexport な関数をそのまま C# 側に取り込むものです。mono の API を使う必要すらなく、一番お手軽です。
P/Invoke では C# -> C++ への暗黙のデータの変換 (marshalling) が行われます。例えば C# の string は内部的に char* に変換して C++ 関数に渡されます。この挙動は MarshalAs attribute を指定することで変更できます。(詳細)
// C++ side extern "C" __declspec(dllexport) void StaticMemberFunction() { ... } extern "C" __declspec(dllexport) void MemberFunction(MonoObject *this_cs) { ... } // C# side class MyClass { [DllImport("CppDll")] public static extern void StaticMemberFunction(); [DllImport("CppDll")] public extern void MemberFunction(); [DllImport("msvcrt.dll")] public static extern int puts(string m); }
mono_add_internal_call() は、dll やホストプログラム側から明示的に関数を登録する方法です。こちらは暗黙の marshalling は行われません。例えば C# の string はそのまま内部表現である MonoString* として C++ 関数に渡されます。mono の機能に直接アクセスしたい場合はこちらを用います。
以後この記事ではこちら方法で関数を登録している前提で説明します。
// C++ side void StaticMemberFunction() { ... } void MemberFunction(MonoObject *this_cs) { ... } ... mono_add_internal_call("MyClass::StaticMemberFunction", &StaticMemberFunction); mono_add_internal_call("MyClass::MemberFunction", &MemberFunction); // C# side class MyClass { [MethodImplAttribute(MethodImplOptions.InternalCall)] public static extern void StaticMemberFunction(); [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void MemberFunction(); }
class の情報を取得
アセンブリ名、namespace、class 名から型情報 (MonoClass*) を取得できます。これと mono_class_get_* 系 API を用いることでその class のさまざまな情報を取れます。メンバ関数、property、field (C++ でいうメンバ変数) あたりはよく使うことになると予想されます。
ちなみに mono の初期化やアセンブリの読み込みは Unity 本体側が既にやっているので自力でやる必要はありません。
// Transform のメンバ関数一覧を表示 MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine")); // UnityEngine.dll アセンブリを get MonoClass *mclass = mono_class_from_name(image, "UnityEngine", "Transform"); // UnityEngine.Transform の型情報を取得 MonoMethod *method = nullptr; gpointer iter = nullptr; while ((method = mono_class_get_methods(mclass, &iter))) { puts(mono_method_get_name(method)); // 全 method を巡回して名前を表示 }
C++ から C# の関数を呼ぶ
MonoClass* と mono_class_get_method_from_name() で関数 (MonoMethod*) を得られます。
MonoMethod*、this となるオブジェクト、および引数群を指定して mono_runtime_invoke() を呼ぶことで C# の関数を呼び出せます。
// Transform.position の get/set を呼ぶ例 MonoObject *trans; // Transform コンポーネントであると仮定 ... MonoClass *mclass = mono_object_get_class(trans); MonoMethod *setter = mono_class_get_method_from_name(mclass, "set_position", 1); // position プロパティの setter。最後の 1 は引数の数で、-1 だと don't care Vector3 pos = {1.0f, 2.0f, 3.0f}; void *args[] = {&pos}; mono_runtime_invoke(setter, trans, args, nullptr); // trans.position = new Vector3(1.0f, 2.0f, 3.0f) MonoMethod *getter = mono_class_get_method_from_name(mclass, "get_position", 0); // position プロパティの getter MonoObject *ret = mono_runtime_invoke(getter, trans, nullptr, nullptr); // ret = trans.position
引数は primitive 型や struct 型の場合、上記例のようにそのポインタを渡します。それ以外、オブジェクトの場合 MonoObject* をそのまま渡します。
mono_runtime_invoke() は内部的に複雑な処理をやってるようなのであまり多用したくないところです。冒頭の公式ドキュメントでは mono_method_get_unmanaged_thunk() という API が紹介されています。C++ から直接呼べる関数ポインタを返してくれる代物らしく、是非使いたいところですが、残念ながら dllexport されていないようで使用は困難です。
Object
C# のオブジェクトは C++ では MonoObject* 型です。
primitive 型や struct 型の場合、MonoObject の後ろにデータがくっつく形になっています。なので、mono_runtime_invoke() で呼んだ C# の関数は MonoObject* で戻り値を返しますが、以下のようにすることでデータを受け取れます。(実際には Mono から提供されている mono_object_unbox() を使った方がいいです。この API も (これの執筆時点では) 内部的に全く同じことをやっています)
// 上の Transform.position 例の続き MonoObject *ret = mono_runtime_invoke(getter, trans, nullptr, nullptr); Vector3 pos = *(Vector3*)(ret+1);
field (メンバ変数) にアクセスしたい場合、mono_class_get_field_from_name() で MonoClassField* を得て mono_field_get_value(), mono_field_set_value() を使います。
その field が primitive 型や struct 型であれば、直にメモリにアクセスすることもできます。MonoClassField* から mono_field_get_offset() で offset 値を取得し、MonoObject* のアドレスから offset 分先のアドレスに値があります。
// C# side class TestObject { public Vector3 test_field; } // C++ side MonoObject *obj; // TestObject と仮定 ... MonoClass *mclass = mono_object_get_class(obj); MonoClassField *field = mono_class_get_field_from_name(mclass, "test_field") guint32 offset = mono_field_get_offset(field); Vector3 value = *(Vector3*)((char*)obj + offset);
C++ 側で C# オブジェクトを new する場合、mono_object_new() で生成した後、自力でコンストラクタを呼びます。
// new Texture2D(128, 128) 相当処理 MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine")); MonoClass *ctex = mono_class_from_name(image, "UnityEngine", "Texture2D"); MonoObject *tex = mono_object_new(mono_domain_get(), ctex); int width = 128, height = 128; void *args[] = {&width, &height}; MonoMethod *ctor = mono_class_get_method_from_name(ctex, ".ctor", 2); // コンストラクタ mono_runtime_invoke(ctor, tex, args, nullptr);
生成したオブジェクトはそのままだと適当なタイミングで GC されてしまいます。これを抑止するには mono_gchandle_new() で GC のハンドルを生成します。メモリの固定化 (pinning) もこの API で行います。 開放してよくなったら mono_gchandle_free() でハンドルを開放します。
// 上のコードの続き guint32 gchandle = mono_gchandle_new(tex, TRUE); // 第二引数 TRUE で pinning // ... mono_gchandle_free(gchandle);
String
C# の string は C++ では MonoString* に相当します。
これは mono_string_to_utf8(), mono_string_to_utf16() で C++ の文字列に変換できます。
// C# side [MethodImplAttribute(MethodImplOptions.InternalCall)] public static extern void CppFunction(string str); ... CppFunction("hogehoge"); // C++ side void CppFunction(MonoString *str) { char *mbs = mono_string_to_utf8(str); puts(mbs); }
C++ から C# に文字列を渡す場合、mono_string_new() や mono_string_new_utf16() を用います。
// C++ から MonoBehaviour.name = "new_name" する例 MonoObject *this_cs; // MonoBehaviour を継承した何かと仮定 ... MonoClass *mclass = mono_object_get_class(this_cs); MonoMethod *set_name = mono_class_get_method_from_name(mclass, "set_name"); MonoString *new_name = mono_string_new(mono_domain_get(), "new_name"); void *args[] = { new_name }; mono_runtime_invoke(set_name, this_cs, args, nullptr); // this_cs.name = "new_name"
mono のワイド文字は uint16 に固定されており、たぶんコンパイラによっては wchar_t としては扱えない点に注意が必要です。
また、Mono の文字列は内部的にワイド文字で保持されるため、mono_string_new() や mono_string_to_utf8() よりも mono_string_new_utf16(), mono_string_to_utf16() の方がオーバーヘッドが少ない点も気に留めておいたほうがいいかもしれません。(悩ましい…)
Array
C# の配列は C++ では MonoArray* に相当します。
MonoArray::max_length がその配列の要素数、MonoArray::vector が先頭要素になっており、max_length 分要素が連続しています。よって、以下のようなコードで C# の配列を C++ で巡回できます。
// C# side [MethodImplAttribute(MethodImplOptions.InternalCall)] public static extern void CppFunction(Vector3[] v3a); ... Vector3[] v3a = new Vector3[] { Vector3.one*1.0f, Vector3.one*2.0f, Vector3.one*3.0f, }; CppFunction(v3a); // C++ side void CppFunction(MonoArray *v3a) { int size = v3a->max_length; Vector3 *data = (Vector3*)v3a->vector; for(int i=0; i<size; ++i) { printf("{%.2f, %.2f, %.2f}\n", data[i].x, data[i].y, data[i].z); } }
MonoArray は MonoObject を内包しており、型情報も含んでいるため、必要に応じて型チェックを行うこともできます。
C++ 側で配列を生成して C# に渡すには mono_array_new() 一族を用います。
// new Vector3[16] 相当処理 MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine")); MonoClass *cv3 = mono_class_from_name(image, "UnityEngine", "Vector3"); MonoArray *v3a = mono_array_new(mono_domain_get(), cv3, 16);
Generic Method & Generic Class
generic 関数 (C++ で言うところの template 関数) は実体化しないと使えません。実体化には mono_class_inflate_generic_method() を用います。
// GetComponent<Transform>() を呼ぶ例 MonoObject *this_cs; // MonoBehaviour を継承したオブジェクトと仮定 ... MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine")); MonoClass *ctransform = mono_class_from_name(image, "UnityEngine", "Transform"); // UnityEngine.Transform の型情報 MonoClass *cthis = mono_object_get_class(this_cs); MonoMethod *gmethod = mono_class_get_method_from_name(cthis, "GetComponent", 0); // GetComponent<T> MonoGenericInst *mgi = (MonoGenericInst*)malloc(sizeof(MonoGenericInst)); mgi->id = -1; mgi->is_open = 0; // must be zero! mgi->type_argc = 1; // generic のパラメータ数 mgi->type_argv[0] = mono_class_get_type(ctransform); // Transform を generic パラメータに MonoGenericContext ctx = { nullptr, mgi }; MonoMethod *imethod = mono_class_inflate_generic_method(gmethod, &ctx); // GetComponent<Transform> を実体化 MonoObject *transform = mono_runtime_invoke(imethod, this_cs, nullptr, nullptr); // this_cs.GetComponent<Transform>()
ここらへんは説明がほとんどなくて苦労しました。微妙に使い方間違ってる可能性もあります。
MonoGenericInst は実体化された関数が存在してる間は有効な領域にある必要があります。mono 側でスマートに管理してくれる機構があってもよさそうなもんですが見つけられなかったので、この例では自分でmalloc() した領域に置いています。
generic class も同様に mono_class_inflate_generic_class() を使えば実体化できそうに見えるんですが、なぜかこの API は dllexport されておらず、現状困難だと思われます…。
補足事項として、C# では fixed() などで固定していないオブジェクトは GC が勝手にメモリ上の位置を移動させることがある、という仕様がありますが、Unity ではこれが当てはまりません。
Unity に使われている mono には古い GC が使われており、fragmentation 対策がなされていないためです。これは C++ 側が C# 側オブジェクト (のアドレス) を保持しっぱなしでも問題ないということであり、今回に関しては都合がいい方向に働いてくれます。
Unreal Engine の場合、新しい GC が使われているはずなので保持しっぱなしだとたまにマズいことが起きる可能性が高いです。(未検証)
また、Unity では ゲームを開始する度に Assembly が更新されるようで、mono_domain_assembly_open(mono_domain_get(), "UnityEngine") とかで取得したイメージは保持しっぱなしではマズいです。スクリプト部分に関しては、御存知の通り、編集するたびに更新されるので生存期間はさらに短いです。これらは少なくともエディタから実行してる時は必要に応じて Assembly を取得しなおす必要があります。
現在 C++ を Unity のスクリプトとして使うプラグインを作ってるんですが、これのソースがより具体的な使い方の例になるんじゃないかと思います。このプラグインについては、一通り機能を実装してから後日詳細に紹介する予定です。
https://github.com/i-saint/UnityCppScript
mono を触ってみて、C# はスクリプト言語としてなかなか悪くないかもしれない、と思うようになりました。
[edit: 2014/10/27] P/Invoke についての説明に不足があったので追記 (暗黙の marshaling 関連)
[edit: 2014/12/10] Object の項目に GC を抑制する方法を追記。いくつか細かい補足追加
CEDEC 2014 & C86
CEDEC 2014 で "Live Coding in C++" と題して講演を行いました。以前の予告の通り、この blog に書いてきたことをまとめた内容になっています。上はその発表資料です。
予想に反して聴講しに来てくれた方は多く、この領域の情報を求めていた人まだこんなにいたんだ!と勇気づけられました。講演後も鋭い質問があったり (後にその方は web 上で面識があった方だと判明)、「"C++ は動的言語" で胸が熱くなりました!」とアツい声援を受けたり、楽しい一時でした。
正直話についてこれなかった方も数多くいるんじゃないかと思いますが、そういう方にも何かしらアイデアは示せたんじゃないかと思います。
内容に関して、State Save が最後まで満足行くレベルに到達できなくて、ここらへんはとても悔いが残る結果になってしまいました。ビデオカードのドライバやサウンド系のスレッドでたまにクラッシュするのがどうしても対応しきれず、今回のデモではそれらは一切保存も復元もしていません。
講演内では触れませんでしたが、信頼性の高い State Save を実現するにはもう一段深いレベルの実装が必要な気がしています。具体的には、簡易 VM 的なものを実装して exe を実行する処理まで自力で行い、WinAPI や DirectX もアプリケーションに見せるレイヤーは自力で実装して全てコントロールする感じです。丁度 wine がやってることをそのまま Windows 上でやるイメージです。当然それを実現するには巨大な労力とやる気が必要で、実際にやるかどうかは不明です…。
古い環境限定とはいえ State Save を実現した Hourglass は偉大だと思います。
また、この講演の実現はスクエニの元同僚の多大な助力に支えられました。この場を借りてお礼を申し上げます。
もう一件。
(リアルタイムレンダリング版。是非こちらも御覧ください)
C86 で公開された ユニティちゃんステージ の製作に携わりました。
具体的には床のシェーダは私が書いています。この床、私の趣味によりテクスチャ素材は一切使わず distance function を用いてシェーダだけでパターンを生成しています。(GLSL による実装例) また、反射は Y 方向反転したカメラで描いた結果を使って実現しています。
デザイン画や素材レベルでは用意が行われていたものはあったものの、Unity 上でそれらを組み立てる作業が行われたのは本当に直前で、公開直前 3 日間で超突貫で制作されました。金曜日に公開なのに月曜の時点では Unity 上にステージは影も形もなかったという無茶っぷりです。
私は「床を綺麗にしてくれ!」というオーダーが突然降ってきてすごい勢いで巻き込まれました。
@nD_ntny さんと @nyaa_toraneko さんがステージや Unity ちゃんの素材を仕上げ、@_kzr さんが Unity 上でのセットアップのほぼ全てを行い、俺が延々床をシェーダで綺麗にするという謎の製作体制。楽しゅうございました。
— i-saint (@i_saint) August 15, 2014
その無計画っぷりはさておき、こんなスピーディーな作業工程は過去の仕事では経験したことがなかったもので、ゲームエンジンが当たり前に使える時代だとその場のノリと勢いだけで結構なものが作れてしまうんだ、という新鮮な感覚がありました。
ちなみに、この Unity ちゃんステージは近い先に誰でも自由に使えるアセットとして公開される予定です。
追記:2014/10/06
小林さん (@nyaa_toraneko) による上記デモの Unity ちゃんのセットアップ解説
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 を使う意義はあるという実感があります。
ゲームエンジンが一般化するにつれてアーティストの作ったリソース勝負になってきそう、という恐れが漠然とあったんですが、実際に手を付けてみると、プログラマが単身創意工夫でなんとかする方式も意外とまだまだ通用するんじゃね?という気になれました。
render massive amount of cubes in Unity
MassParticle
パーティクルエンジンを Unity のプラグインとして再実装しています。
まだまだ発展途上ではあるものの、そこそこの物量と速度と使い勝手を両立できる目処が立ってきたので、いずれ Asset Store で公開しようと考えています。ソースに関しては今もこれからも github で公開中です。
実装にあたり、シミュレーションのコアは以前 ISPC で書いたのをそのまま利用。他オブジェクトとのインタラクションは、C# スクリプトからプラグイン側に Collider 情報を渡し、パーティクルデータを unsafe のポインタで返して衝突しているパーティクルから AddForce()、ですぐに実装できたのですが、描画処理が相当なクセモノで苦労させられました。
以下は Unity で大量の cube を描くためにこれまで辿った道筋です。想定しているゴールは、私のマシン (i7 970 3.2 x 6, GTX 660) で 100000 くらいの cube を現実的な速度で描画できるようにする、という感じです。
1. プラグイン内で hardware instancing
Unity はプラグインに描画デバイス (ID3D11Device* など) を提供します。なので、自然とまずはそれを使って描画しようという発想になります。
この方法に関しては公式にいいサンプルが用意されており、それを参考にプラグイン内でシェーダやバッファ類を独自に用意、カメラ情報などは C# スクリプトから毎フレーム送ってもらい DrawIndexedInstanced() で描いてみました。
注意すべき点として、Unity は projection 行列に独自の加工を施しており、それと同じことをプラグイン側でもやる必要があります (該当処理)。そうしないと depth が Unity が描画したものより手前にズレて前後関係がおかしくなります。
速度面に限ればおそらくこれが最良の方法で、このシーンで 90000 パーティクルくらいまで 60FPS を保てます。(完全に CPU bound なので、パーティクルが密集しないシーンであればもっともっと数出せます。Intel GPA によると 100000 cube 描画 draw call の所要時間は 2.7ms 程度でした)
しかしこれには致命的な問題があって、Unity の Material を一切使えず、シェーダーも完全に独自に書く必要があります。また、影を出すこともできません。 他の人にも使ってもらいたい場合この前提はさすがに無理があるので、他の方法を考えなければなりません。
というわけで、これ以下は Unity の描画パイプラインを使いつつ 1. に近い速度を出す方法の考察になります。
2. パーティクル群を 1 つの Mesh にまとめて描画
現状 Unity には instancing 描画機能がないので、大量のオブジェクトを一括描画するには工夫が必要です。
最初に検証したのが一番ストレートな方法で、プラグイン側でパーティクル群をまとめて 1 つの頂点データの塊にし、C# 側の Mesh オブジェクトにそれを流し込み、その Mesh を Unity 側で描いてもらう、というものです。Mesh として描くので Unity の Material を使うことができます。
正確には、Mesh は 1 つにつき 65000 頂点までしか格納できないので、65000 / (cube あたりの頂点数 24) ≒ 2700 cube が 1 draw call で描ける限界になります。なので、Mesh を描くだけのオブジェクトを パーティクル数 / 2700 個 生成して描くようにします。
頂点データは C# 側で事前に new Vector3[24*2700] のように確保しておき、それを C++ 側に渡して書いて貰います。
で、試してみたところ、遅すぎて使い物になりませんでした。しかも時間を食ってるのはプラグイン側の処理ではなく、C# 側のデータのコピーです。
プラグインから C# の配列にデータを注入し、それを Mesh.(vertex | normal | uv | triangles) にコピーする (これらは property であり、直接実データにアクセスすることができない)、というのを毎フレームやる必要があるのですが、要素数がとても多い上、C# はメモリ保護機能のせいかコピーが超遅いのでひどいことになります。10000 パーティクル足らずで 60FPS を割ってしまいました。
3. geometry shader でプリミティブ生成
C# のコピーが遅いならコピーが必要なデータを最小にしよう、ということで、次は geometry shader を使うアプローチを検証しました。パーティクル 1 個につき 1 頂点だけ出力し、geometry shader でプリミティブを生成して描画する、というものです。これならコピーが必要なデータ量は 2. の方法の 1/24 程度で済み、1 回の draw call で 65000 cube 描けるようになります。
この方法はそこそこいい感じに動き、60000 パーティクルくらいまで 60 FPS を保ってくれたのですが、やや痛い欠点があります。geometry shader を使うと surface shader が使えなくなるらしいのです。
surface shader は Unity の標準的なシェーディング処理で、所定の手続きを踏んで SurfaceOutput データを出力すればあとは影含む大部分のシェーディング処理をいい感じに仕上げてくれます。surface shader が使えなくなるということは、これらの処理を独自に実装しないといけないということになります。
なので、シェーディング不要な billboard なんかを大量にを出す場合には良さそうですが、きちんとシェーディングされたオブジェクトを出したい場合面倒なことになりそうです。
また、2. よりだいぶんマシになったとはいえこの方法でも C# 側のデータコピーがでかいロスになります。
(ちなみに 100000 billboard 生成して描画した場合の純粋な GPU 所要時間は 1 ms でした。GS は遅いと言われてるのをよく聞きますが、意外と実用性高いんじゃないかと思いました)
4. テクスチャにパーティクル情報を格納
Unity の Texture 一族には GetNativeTexturePtr() なるメンバ関数が用意されており、これを使うと D3D や OpenGL のテクスチャオブジェクト( ID3D11Texture2D* など) を取得できます。テクスチャにダイレクトアクセスできるのであれば、プラグイン側でパーティクルのデータをテクスチャに書くことで C# を一切介さず Unity 側にデータを渡せそうです。
描画には古き悪しき擬似 instancing テクニックを使います。1 つの頂点バッファに同じモデルデータをテクスチャ座標だけ少しずらしつつ連続配置しておき、vertex shader でテクスチャから各インスタンスの位置データを持ってきて頂点を移動させることで hardware instancing を擬似的に実現する、というものです。
本物の hardware instancing と比べると VRAM をバカ食いするのと、GPU によっては vertex shader 内の texture fetch が遅かったりする欠点がありますが、概ねうまく機能します。vertex shader に独自処理を追加する必要はあるものの、それ以外は Unity 側で用意された機能に任せることができるので、負担はそこまで大きくないはずです。
実装の際は、Unity の Texture2D が何故か float 系フォーマットをサポートしておらず、RenderTexture で代用する必要がある点に注意が必要です。また、影を出したり受けたりするには ShadowCaster & ShadowCollector Pass も書いて頂点移動処理をやる必要があります。
今回の例ではパーティクルは 1 つ 48 byte なので、uv 座標を 3 texel 分横にずらしつつ、1 つの Mesh に 2700 cube 分の頂点データを格納しています。(2. と同様 1 draw call 2700 cube の制限があります) あとは位置データ以外も容易に渡せるので、速度に応じて赤熱させたり寿命が尽きかけているものは縮小フェードアウトさせたりしています。
この方法はかなり上手い具合に機能していて、Unity の描画パイプラインに載せつつ 1. にかなり近い速度を出せました。geometry shader との併用もできます。これだけ数出しつつ影を出せるのは感動的です。
今のところこの方法を主軸にやっていこうという方針です。
今回の描画の検証以外に、パーティクルエンジンを pure C# で実装する、などの実験もやったんですが、これらの経験から痛感させられたのが、C# は本当に本当に、絶望的に遅いということでした。幸か不幸か、こういう HPC 的な領域ではローレベルプログラミング能力の需要は永遠になくならない気がしてきます。
あとは compute shader を使うアプローチをまだ全く検証していないので、そこに可能性が残っているかもしれません。
しかしこんなのバッドノウハウ以外の何物でもないので、Unity に instancing 描画が搭載されて抜本的に解決されるのが理想ではあります。
[Edit 2014/12/23] 続編。実は instancing 描画備わっていました。あと compute shader も試してみました。
render massive amount of cubes in Unity (2) - primitive: blog
CEDEC 2014 / Status Report
CEDEC 2014 で喋ることになりました。
Live Coding in C++
ここ 1 年くらい趣味でやってた活動をまとめたような内容にする予定で、実行時 C++ 編集ネタとプロセスの状態保存/復元の 2 つが主なトピックになります。タイトルから推測できる通り、色んな黒魔術を駆使して C++ で快適に開発しよう、という趣旨です。
あと、先月また転職して現在 Unity Technologies Japan に勤めています。Unity を自分のようなオールドタイプでスピード狂なプログラマーでも快適に扱えるゲームエンジンに鍛えるべく奮闘中です。(ちなみに上記 CEDEC の私の講演に Unity の話は出てきません)
今回転職にあたって色んな人のお世話になりました。相手してくださった方々本当にありがとうございます。
ここ数ヶ月在職中の鬱憤を晴らすべくあっちこっち遠出しまくってたので、以下適当な記録。誰得写真集。
4/4。雨上がりの夕刻にものすごく綺麗な夕日が見れた一日でした。
奥多摩 / 奥多摩湖 / 日原鍾乳洞
首都圏から手軽に行けて都会の喧騒を忘れられるいい場所で、度々足を運んでいます。
猿とか鹿とかに出くわすこともあり、クマ注意の看板まであって、東京都の違う側面が見られます。
戦場ヶ原 / 日光
自転車で行ける面白そうな場所を適当に探してたら目に付いたので行ってみたんですが、大当たりでした。雪の残る湿原はかなり見た目にファンタジー度が高く、周囲の地形も変化に富んでいて見てて飽きません。別の季節にまた訪れてみたいところです。
霧ヶ峰 / 八島ヶ原湿原
戦場ヶ原が素晴らしかったので他の湿原も見てみよう…と行ってみた場所。とにかくだだっ広い感じが素晴らしくて、眼前一面に広がる笹野原は圧巻でした。
ただ、観光地として整備されすぎてて雰囲気ぶち壊しになってる部分も多いのがやや残念でした。そこらじゅうに張り巡らされた柵と警告の看板とか…。
熊本 / 阿蘇
福岡に遊びに行ったついでに立ち寄り。火口付近は植物がほとんど無い荒涼とした景色が広がり、その外側には牧草地帯、更に外側には外輪山と、変化に富んだ植生が面白い場所でした。
上海
出張で行ってきました。中国自体初めてだったんですが、ネットに規制がかかってて Google とか Twitter とか有名なサービスの多くは遮断されるとか、みんな信号無視しまくりで横断歩道が恐怖そのものだったりとか、めちゃくちゃでかい街だけど細部が色々雑な感じとか、色々カルチャーショックを味わえました。
悪い方向の印象が強かったものの、寺院とか植生は見てて面白くて、中国の田舎の方を旅するのは楽しそう、と思わせてくれました。