Unityの低レベルネイティブプラグインインターフェースのサンプルを細かく読んでみる その1 - C#側

はじめに

最近Unityの低レベルネイティブプラグインインターフェース(Low-Level Native Plugin Interface)を使ってみようと四苦八苦してるので、忘れないうちに記録として残しておこうと思い、記事を書くことにしました。
UnityもC#も素人、かつC++に至っては触ったことすらないので、内容が寄り道だらけになってしまうと思いますがご了承ください。


内容

今回読むサンプルについては、凹みさんが既に解説してくださっています。
tips.hecomi.com
一連の記事を読んだ後「よし、使ってみよう」と思ったものの、何もできなかったので「じゃあUnityのサンプルをもうちょっと掘り下げて読んでみよう」という内容です。
具体的に言うと、その1(今回)ではC#のコードを読んでいき、その2(次回)ではDLL内のC++のコードを読んでいきます。
ただC#のコードで凹みさんの記事と重複する部分は面倒なので書きません。
Unityのサンプルはこちらのページ下部の「ここでダウンロードできます」リンクからダウンロードしてください。
低レベルネイティブプラグインインターフェース - Unity マニュアル


動作環境

Windows 10
Unity 5.6.5f1 (サンプルプロジェクトと同じ)
Visual Studio Community 2017 (15.6.6)


サンプルプロジェクトを開く

NativeRenderingPlugin\UnityProjectをUnityで開き、Assets内のsceneを開くとこんな感じのシーンがあると思います。
f:id:alupaca1363:20180424222016p:plain
Playを押すとカラフルな三角形のポリゴンと白黒の波がうねうね動きます。

UseRenderingPlugin.csを読む

シーン中の_PlaneThatCallsIntoPluginにアタッチされているUseRenderingPlugin.csで、RenderingPlugin.dllから使われているのは主にこの4つの関数です。

// C++側に時刻をセットする。 今回はt=[Sceneがロードされてからの時間]
//としてコルーチンで呼び出し、波などの関数に使う
private static extern void SetTimeFromUnity(float t);

// Unityであらかじめ作ったTextureのポインタをC++側にセットする
private static extern void SetTextureFromUnity(
    System.IntPtr texture, 
    int w, 
    int h
);

// (今回はScene中に存在している)MeshのポインタをC++側にセットする
private static extern void SetMeshBuffersFromUnity (
    IntPtr vertexBuffer, 
    int vertexCount, 
    IntPtr sourceVertices, 
    IntPtr sourceNormals, 
    IntPtr sourceUVs
);

// ネイティブ(C++)で定義した関数の関数ポインタを返す。
// GL.IssuePluginEvent(IntPtr callback, int eventID); のコールバック関数にセットする。
private static extern IntPtr GetRenderEventFunc();

GL.IssuePluginEvent()はネイティブプラグインレンダリングスレッドから呼び出すための関数で、こいつにネイティブ側で呼びたいメソッド(の関数ポインタ)を与えることでレンダリングスレッドから発動させることができます。
サンプルには①「うねうね動く波っぽいやつ」と②「回転するカラフルな三角形」の二種類の例が載っており、それぞれどんな仕組みで動いているかというと

① Textureの作成はUnity側で行い、初期化時にTextureのポインタ(アドレス)とMeshのポインタをC++側に渡し、そっちでMeshの変形とTextureの更新を行ってもらう

② 処理はすべてネイティブ側で行い、描画も(Windowsの場合)DirectXで直接描画する(?)

C#側には、②についての処理は見受けられませんでした。(強いて言うならレンダリングスレッドのコールバック関数の中にネイティブの②のコードがあることぐらい)
コードをさらっと見た感じ、DirectXでごりごり処理を書いているように見えたので今は目を背けました。

IEnumerator Start()

UseRenderingPlugin.csで、Start()まわりの処理は以下のようになっています。(少し省略)

IEnumerator Start()
{
	// _PlaneThatCallsIntoPluginのTextureとMeshのポインタをC++に与える
	CreateTextureAndPassToPlugin();
	SendMeshBuffersToPlugin();

	// ネイティブプラグインのメソッドを毎フレーム呼び出す
	yield return StartCoroutine(CallPluginAtEndOfFrames());
}
private IEnumerator CallPluginAtEndOfFrames()
{
	while (true) {
		// その他のレンダリングが終わるまで待つ
		yield return new WaitForEndOfFrame();

		// ネイティブ側に時間をセットする
		SetTimeFromUnity (Time.timeSinceLevelLoad);

		// ネイティブの関数をレンダリングスレッドから呼び出す
		GL.IssuePluginEvent(GetRenderEventFunc(), 1);
	}
}


IEnumerator Start() <- 誰だお前!

ということで調べたところ、どうやらvoid Start()IEnumerator Start()とすることで、Start() (Unityのコールバック関数)そのものをコルーチン化できるらしい。
tsubakit1.hateblo.jp
へぇ~と思いましたが、正直今回のサンプルではvoid Start()でもいい気がします。実際に書き換えてみても普通に動いていました。


WaitForEndOfFrame()

CallPluginAtEndOfFrames()の中では毎フレームWaitForEndOfFrame()レンダリング処理が終わるのを待ってからネイティブの処理を実行しています。
yield return new WaitForEndOfFrame();の挙動については以下の記事が参考になりました。
robamemo.hatenablog.com

CreateTextureAndPassToPlugin()SendMeshBuffersToPlugin()

この二つのメソッドの中身はこんな感じです

private void CreateTextureAndPassToPlugin()
{
	// テクスチャを生成
	Texture2D tex = new Texture2D(
		256,
		256,
		TextureFormat.ARGB32,
		false
	);

	// ピクセルの大きさが変わった際に、ぼかさずにはっきり表示する
	tex.filterMode = FilterMode.Point;

	// テクスチャをGPUにアップロード
	tex.Apply();

	// _PlaneThatCallsIntoPluginのテクスチャを生成したものに設定
	GetComponent<Renderer>().material.mainTexture = tex;

	// ネイティブにテクスチャ関係のアドレスを渡す
	SetTextureFromUnity (
		tex.GetNativeTexturePtr(), 
		tex.width, 
		tex.height
	);
}
private void SendMeshBuffersToPlugin ()
{
	var filter = GetComponent<MeshFilter> ();
	var mesh = filter.mesh;
	// 何回も変形させるメッシュなので、Dynamicモードに設定
	mesh.MarkDynamic ();

	// メッシュの頂点、法線、UV座標のポインタをGC
	// (ガベージコレクション)に回収されないよう
	// 固定し、ネイティブに渡す。
	var vertices = mesh.vertices;
	var normals = mesh.normals;
	var uvs = mesh.uv;
	GCHandle gcVertices = 
	    GCHandle.Alloc(vertices, GCHandleType.Pinned);
	GCHandle gcNormals = 
	    GCHandle.Alloc(normals, GCHandleType.Pinned);
	GCHandle gcUV = 
	    GCHandle.Alloc(uvs, GCHandleType.Pinned);

	// メッシュのポインタ、各情報をネイティブにセット
	SetMeshBuffersFromUnity (
		mesh.GetNativeVertexBufferPtr (0), 
		mesh.vertexCount, 
		gcVertices.AddrOfPinnedObject (), 
		gcNormals.AddrOfPinnedObject (), 
		gcUV.AddrOfPinnedObject ()
	);

	// 使い終わった固定メモリはリークしないよう開放する
	gcVertices.Free ();
	gcNormals.Free ();
	gcUV.Free ();
}

CreateTextureAndPassToPlugin() の方は凹みさんの記事にあるので割愛します

SendMeshBuffersToPlugin () GCHandleという見慣れない型がいたので調べてみると、メモリがマネージドな(GCに管理されている)C#でメモリを固定するために使われているようです。
目的としては、「ネイティブ側から頻繁にアクセスがあるので、ネイティブヒープを使わずマネージドヒープを直接ネイティブ側から参照することで無駄なコピーを減らす」ためではないかと思われます。(間違ってたら教えてください)
そのため、メッシュの情報をキャッシュしたタイミングとSetMeshBuffersFromUnity ()が発動する間のタイミングでGCが発動してもメモリが回収(解放)されてしまわないようGCHandle.Alloc()で固定メモリを与えているのではないでしょうか。
正直、ここら辺には自信がありません。
ここいらの話は以下のスライドが非常に参考になりました。

www.slideshare.net

あと、「SetMeshBuffersFromUnity()の直後にメモリを開放すると、C#側でメモリを参照しているものが何もないうえメモリが固定されてないので、じきにGCで解放されてしまうのでは?」とか思いましたが、悩んだ結果「ネイティブ側から直接参照しているので回収の対象にならないのでは」という結論に至りました。
間違ってるかもしれませんが、そもそもGCの知識が十分にない状態で考えてもわからないと思ったので、頭の隅に置いておくに留めることにします。


おわりに

C#側のコードだけでも割と長くなってしまいました。
近いうちにC++側を読んだものも投稿しようと思います。

VR Gesture and Signature を使ってみる

VR Gesture and Signature とは?

VR Gesture and Signature (for Oculus Rift) とは、Oculus Touchを使ってVR空間でお絵かきを出来るようにしたり、Touchのモーションを認識することのできる無料のスクリプトアセットです。

f:id:alupaca1363:20171108234628p:plain

https://www.assetstore.unity3d.com/jp/#!/content/101504

Oculus以外にもViveなどのHMDにも専用のアセットが別々で公開されています。

 

今回の内容について

このブログは自分用にVR Gesture and Signatureのドキュメントの一部を読み解いたものが含まれます。英語は素人なので語弊があるかもしれません。英語が大丈夫な方は以下の公式ドキュメントを読んだほうが良いかと思います。

http://www.airsig.com/doc/sdk/oculus/en/?utm_source=unity&utm_medium=asset_store&utm_campaign=oculus_doc

 

動作環境

Windows10

Unity5.6.2f1

Oculus Utilities v1.15.0, OVRPlugin v1.14.1, SDK v1.20.0

 

機能(2017/11/09現在)

現在の主な機能は以下の二つです。

1. ユーザーの署名・サインを登録し、それを認識する機能

 ...TrainUserモードとIdentifyUserモード

2. ユーザー固有のハンドジェスチャーを登録し、それを認識する機能

 ...AddCustomGestureモードとIdentifyCustomGestureモード

 

ユーザーが登録する必要のない、開発者による事前のジェスチャー登録の機能については開発中のようです。 

また、上記の(開発者による)事前登録ジェスチャーと、(ユーザーによる)固有のジェスチャー登録(練習)の組み合わせによる認識精度の向上についても開発中だと示されていました。

 

デモシーンを使ってみる

VR Gesture and Signatureをダウンロード、インポートし、Assets/AirSig/DemoScene/Main を使うには、いくつかの手順が必要になります。まずはプロジェクトをOculus Rift+Touch対応にセットアップしましょう。僕はセットアップ方法は以下のサイトを参考にさせていただきました。

framesynthesis.jp

その後Edit->Project Settings->PlayerのVirtual Reality Supportedにチェックを入れ、SDKにOculusを設定しましょう。これでプロジェクトがVR対応になりました。

 

次にジェスチャー用データベースのディレクトリを設定します。方法はAssets/AirSig/ の中のStreamingAssetsフォルダをコピーしてAssets/ 以下に張り付けるだけです。つまりAssets/SereamingAssets/ というディレクトリが出来上がることになります。

 

次はTouchのボタンを押した際、手の先からParticleが出てくるようにします。

シーン内のLocalAvatar/hand_right の子としてParticle Systemを作成します。Hierarchyビューでhand_rightを右クリック->Particle Systemで作成可能です。

作成したParticle SystemをGameManagerのtrackの項目にドラック&ドロップすればボタンを押した際にParticleが放出されます。

...と言いたいところなのですが、11/09時点でGestureHandle.cs内にバグのようなものがあります。Update()の中なのですが、Touchのボタンを押してる間、毎フレームParticleSystem.Play()を呼び出すというコードになっています。本来Play()はボタンが押された瞬間だけ発動すれば良いはずです。具体的には、僕は以下のように修正しました。


    //修正前
    void Update(){
        ...
        float triggerKeyValue = OVRInput.Get(OVRInput.RawAxis1D.RHandTrigger);
        if (triggerKeyValue > 0.8f) {
            track.Play();
        } else if (triggerKeyValue < 0.1f) {
            track.Stop();
        }
        ...
    }

    //修正後
    private bool OnPlayFlg = false;
    
    ...
    
    void Update(){
        ...
        float triggerKeyValue = OVRInput.Get(OVRInput.RawAxis1D.RHandTrigger);
        if (triggerKeyValue > 0.8f && !OnPlayFlg) {
            track.Play();
            OnPlayFlg = true;
        } else if (triggerKeyValue < 0.1f) {
            track.Stop();
            OnPlayFlg = false;
        }
        ...
    }

 

やっているのは、単純にFlagを使ってボタンを押した直後にだけtrack.Play()が発動するようにしているだけです。OVRInput.Get()ではなくGetDown()やGetUp()を使えばもうちょっとスマートに書けますが、まあ動いているのでいいでしょう(妥協)。

 

ここまで来ればほとんど終わったようなものです。あとは好きなようにParticleSystemの設定をしましょう。ParticleSystemについては、以下のサイトが非常に参考になりました。

http://marupeke296.com/UNI_PT_No1_Shuriken.html

唯一、Simulation SpaceはWorldに設定したほうが良いと思います。

 

ここまでやると、以下の動画のようなことが可能になります。

Particleは割と適当に設定してるので、もうちょっと文字などを書きやすいように設定したほうが良いと思います。

 

 

Touchのジェスチャー認識開始のボタンを変更する

ちょっとしたことですが、デフォルトではジェスチャーの認識開始のボタンが中指(Gripボタン)に設定されています。それが個人的にちょっと気持ち悪かったので、ボタンを人差し指に変更しました。やり方は簡単で、

GestureHandle.cs の中のUpdate()と、AirSigManager.csの中のUpdate()にあるOVRInput.Get()の引数を、OVRInput.RawAxis1D.RHandTrigger からOVRInput.RawAxis1D.RIndexTrigger に変更するだけです。

これでジェスチャー開始ボタンを人差し指に変更できました。

 

最後に

VR Gesture and Signature についての情報が少なかったのと、ブログを書く練習がしたかったのでこの記事を書きました。Methodについては、今後調べてから更新しようと思っています。

ちょうど「あったらいいな」と思っていたアセットだったので、殆どコーディングせずにVRでジェスチャー認識ができるのは本当に便利だと感じました。ただいくつかの機能を使ってみて、「まだ開発途中である」ということを少々強く感じました。ジェスチャーの認識も、ある程度は認識できましたがあまり良いとは感じられませんでした。10月に出たばかりの新しいアセットですので、今後どんどん機能が増え、精度も上がってくれればと思っています。

そのため、ジェスチャーの認識については、以下のページで紹介されているアセットの利用も検討しながら実現したいと考えています。

2vr.jp