凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity の Low-level Native Plugin Interface を調べてみた

Unity 5.2 以降について

Unity 5.2 より Low-Level Native Plugin Interface のお作法が少し変更になりましたので、最新の情報についてはドキュメントをご参照下さい。

はじめに

Unity では Low-level Native Plugin Interface というネイティブ(Direct3DOpenGL 等)の力を利用した描画を可能とする機構が備わっています。

調べてみましたのでメモしておきます。

サンプルのダウンロード

下の方に「ここでダウンロード出来ます 。」というリンクがあるので、そこからダウンロードします。RenderingPluginExample42/RenderingPlugin 下に RenderingPlugin.cppUnityPluginInterface.h があるのでこれらを見ると色々分かります。また、これを利用したサンプルプロジェクトは RenderingPluginExample42/UnityProject に入っています。実行すると以下の様なものが表示されます。

f:id:hecomi:20140119173943p:plain

概要

サンプルでは以下の2つが紹介されています。

  • レンダリングイベントを利用してネイティブからスクリーンに描画
    • サンプルの三角形
  • テクスチャのポインタを介してネイティブ側でプロシージャルテクスチャを生成
    • サンプルの白黒もやもや
    • Texture2D.GetNativeTexturePtr() でテクスチャのポインタを取得している

詳細

サンプルコード(Unity/C#

はじめに Unity 側のコードを見ておくとイメージが湧きやすいと思います。読みやすいようにちょっと改変しています。

using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;

public class UseRenderingPlugin : MonoBehaviour 
{
	// アニメーション用の時間を Unity 側からセット
	[DllImport ("RenderingPlugin")]
	private static extern void SetTimeFromUnity(float t);
	// Unity 側からテクスチャをネイティブに渡す(白黒もやもやを生成するため)
	[DllImport ("RenderingPlugin")]
	private static extern void SetTextureFromUnity(System.IntPtr texture);

	void Start() 
	{
		CreateTextureAndPassToPlugin();
		StartCoroutine("CallPluginAtEndOfFrames");
	}

	private void CreateTextureAndPassToPlugin()
	{
		// 白黒もやもや用に 256 x 256 のテクスチャを生成
		Texture2D tex = new Texture2D(256,256,TextureFormat.ARGB32,false);
		tex.filterMode = FilterMode.Point;
		tex.Apply();
		renderer.material.mainTexture = tex;
		
		// そのテクスチャのポインタをネイティブ側に渡す
		SetTextureFromUnity (tex.GetNativeTexturePtr());
	}
	
	private IEnumerator CallPluginAtEndOfFrames()
	{
		// 毎フレーム実行
		while (true) {
			// スクリーンへの描画の直前まで待機
			yield return new WaitForEndOfFrame();
			// アニメーション用に現時刻をセット
			SetTimeFromUnity(Time.timeSinceLevelLoad);
			// ネイティブ側のレンダリングイベントの呼び出し
			GL.IssuePluginEvent(1);
		}
	}
}
サンプルコード(ネイティブ側/C++

サンプルではネイティブ側から 4 つの関数を Unity 側へ晒しています。

extern "C" void EXPORT_API UnitySetGraphicsDevice (void* device, int deviceType, int eventType)
extern "C" void EXPORT_API UnityRenderEvent (int eventID);
extern "C" void EXPORT_API SetTimeFromUnity (float t);
extern "C" void EXPORT_API SetTextureFromUnity (void* texturePtr);

UnitySetGraphicsDeviceUnityRenderEvent が Unity 既定の関数です。SetTimeFromUnitySetTextureFromUnity は、引数をグローバル変数に登録しているユーザ定義の関数です。まとめると全体の流れとしては、

  1. UnitySetGraphicsDevice でデバイスの初期化・デバイスの種類をグローバル変数に登録
  2. SetTimeFromUnity / SetTextureFromUnity を適切なタイミングで呼んで描画に必要なパラメタをグローバル変数に登録
  3. GL.IssuePluginEvent して実行された UnityRenderEvent で登録されたグローバル変数を参照して描画

という形になります。

UnitySetGraphicsDevice - グラフィックドライバへのアクセス

これは先にリンクを貼ったマニュアル内にも書かれていますが、この関数名を晒すことにより、Unity 側からグラフィックの初期化や破棄時に自動的に呼ばれるようになります。引数はデバイスのポインタ、デバイスの種類、イベントの種類で、以下の様なものがあります。

deviceType

enum GfxDeviceRenderer {
	kGfxRendererOpenGL            = 0,  // OpenGL
	kGfxRendererD3D9              = 1,  // Direct3D 9
	kGfxRendererD3D11             = 2,  // Direct3D 11
	kGfxRendererGCM               = 3,  // Sony PlayStation 3 GCM
	kGfxRendererNull              = 4,  // "null" device (used in batch mode)
	kGfxRendererHollywood         = 5,  // Nintendo Wii
	kGfxRendererXenon             = 6,  // Xbox 360
	kGfxRendererOpenGLES          = 7,  // OpenGL ES 1.1
	kGfxRendererOpenGLES20Mobile  = 8,  // OpenGL ES 2.0 mobile variant
	kGfxRendererMolehill          = 9,  // Flash 11 Stage3D
	kGfxRendererOpenGLES20Desktop = 10, // OpenGL ES 2.0 desktop variant (i.e. NaCl)
};

eventType

enum GfxDeviceEventType {
	kGfxDeviceEventInitialize  = 0,
	kGfxDeviceEventShutdown    = 1,
	kGfxDeviceEventBeforeReset = 2,   // Direct3D 9 のみ
	kGfxDeviceEventAfterReset  = 3,   // Direct3D 9 のみ
};

これらを利用してプラットフォームごとに初期化を行います。例えば、Direct3D 11 なら、

if (deviceType == kGfxRendererD3D11) {
	g_DeviceType = deviceType;
	SetGraphicsDeviceD3D11 ((ID3D11Device*)device, (GfxDeviceEventType)eventType);
}

といった形です。

UnityRenderEvent - レンダリングスレッド上のプラグインコールバック

Unity 側で GL.IssuePluginEvent(eventID) するとコールされます。この eventID を利用して色んな描画の場合分けを行います。サンプルでは、プラットフォーム毎に描画方法(Direct3D 9/11、OpenGL など)を場合分けをしているのですが、ここでは簡単のために、OpenGL 用のコードをくっつけて見てみます。

extern "C" void EXPORT_API UnityRenderEvent(int eventID)
{
	// 三角形の頂点座標と色
	MyVertex verts[3] = {
		{ -0.5f, -0.25f,  0, 0xFFff0000 },
		{  0.5f, -0.25f,  0, 0xFF00ff00 },
		{  0,     0.5f ,  0, 0xFF0000ff },
	};

	// 三角形の回転
	float phi = g_Time;
	float cosPhi = cosf(phi);
	float sinPhi = sinf(phi);

	// 描画に必要なもろもろの行列
	float worldMatrix[16] = {
		cosPhi,-sinPhi,0,0,
		sinPhi,cosPhi,0,0,
		0,0,1,0,
		0,0,0.7f,1,
	};
	float identityMatrix[16] = {
		1,0,0,0,
		0,1,0,0,
		0,0,1,0,
		0,0,0,1,
	};
	float projectionMatrix[16] = {
		1,0,0,0,
		0,1,0,0,
		0,0,1,0,
		0,0,0,1,
	};

	// GL の設定
	glDisable (GL_CULL_FACE);
	glDisable (GL_LIGHTING);
	glDisable (GL_BLEND);
	glDisable (GL_ALPHA_TEST);
	glDepthFunc (GL_LEQUAL);
	glEnable (GL_DEPTH_TEST);
	glDepthMask (GL_FALSE);

	// Transformation matrices
	glMatrixMode (GL_MODELVIEW);
	glLoadMatrixf (worldMatrix);
	glMatrixMode (GL_PROJECTION);

	// Tweak the projection matrix a bit to make it match what identity
	// projection would do in D3D case.
	projectionMatrix[10] = 2.0f;
	projectionMatrix[14] = -1.0f;
	glLoadMatrixf (projectionMatrix);

	glVertexPointer (3, GL_FLOAT, sizeof(verts[0]), &verts[0].x);
	glEnableClientState (GL_VERTEX_ARRAY);
	glColorPointer (4, GL_UNSIGNED_BYTE, sizeof(verts[0]), &verts[0].color);
	glEnableClientState (GL_COLOR_ARRAY);

	glDrawArrays (GL_TRIANGLES, 0, 3);

	// 白黒もやもやの生成
	if (g_TexturePointer)
	{
		GLuint gltex = (GLuint)(size_t)(g_TexturePointer);
		glBindTexture (GL_TEXTURE_2D, gltex);
		int texWidth, texHeight;
		glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &texWidth);
		glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &texHeight);

		unsigned char* data = new unsigned char[texWidth*texHeight*4];
		// 白黒もやもやを作る関数(略)
		FillTextureFromCode (texWidth, texHeight, texHeight*4, data);
		glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, texWidth, texHeight, GL_RGBA, GL_UNSIGNED_BYTE, data);
		delete[] data;
	}
}

マニュアルにも書かれていますが、レンダリングスレッドは、MonoBehaviour とは独立したスレッドで動いており、複数レンダリングスレッドが干渉する可能性があるので注意が必要です。

これで最初のサンプルで行っていた、スクリーン上にネイティブから重畳して絵を描いたり、ネイティブ側でテクスチャを描画して、そのテクスチャを Unity 側で利用している仕組みが、お分かり頂けたのではないかと思います。

おわりに

ネイティブの力を利用すると、プラットフォームによる場合分けは大変ですが、既存の資産を利用したり表現の幅を広げたり出来て、とても有用だと思います。