2017.07.10

iOS ARKitのUnity Pluginを使ってみましょう


ARKit

こんにちは。GMOインターネットの次世代システム研究室のJ.Lです。
iOS 11にてついにAR開発Frameworkが発表されました。名前はどストレートの「ARKit」!
今回はARKitを利用して簡単なARアプリを作って見たいと思います。

 

ARKitについて

ARKitで実装する方法は以下の3通りです。

1. SpriteKit(2D)もしくはSceneKit(3D)を利用
2. Metalを直接利用
3. Unity pluginを利用(内部的にはMetalに変換される)

簡単なアプリケーションを作成するなら1の方法を利用すると簡単に作成できますが、複雑な3Dアプリケーションを作成するなら、やはりUnityを利用するのが便利ですね。
とのことで、今回はUnity pluginについて少しだけ詳しく紹介したいと思います。

準備

まずはXcode 9のBeta版とiOS 11がインストールされている端末が必要です。そして、バージョン「5.6.1p1」以上のUnityとARKitのUnity pluginが必要です。
ARKit Unity PluginはここからCloneできます。
それでは、UnityからCloneしたソースを開いてみましょう。

読み込むと以下のような画面になると思います。

unity-init

 

Projectルートの方にいくつかのSceneが見られます。6/27現在サンプルとして4つのSample Sceneが用意されてますので、それらを見ながらこのPluginの使い方を把握していきたいと思います。
(SceneはUnityでWorldを定義するための要素です。詳細はUnityのマニュアルなどを参照してください。)

ちなみにそれぞれのSceneは以下の内容を持ってます。
  1. UnityParticlePainter : 空中に選択した色のParticleを生成する。
  2. UnityARKitScene : 平面認識機能とHittestのサンプル(画面タップするとその立方体を表示する)
  3. UnityARShadows : 平面認識機能とHittestのサンプル(画面タップするとその眠り人形を表示する)
  4. UnityARBallz : 平面認識機能とHittestのサンプル(画面タップするとその位置にボールを生成す)

ARカメラ設定と起動

それでは、まずはカメラを設定して見ましょう。新しいSceneをProjectに追加して開いてください。
そして、以下の手順で必要なComponentの追加と設定を行ってください。
  1. Main Cameraを選択
  2. Inspectorの「Clear Flags」を「Depth only」に設定
  3. 「Add Component」をクリック
  4. Pluginの「Unity AR Video」Componentを追加
  5. UnityARVideo Componentの「Clear Material」にpluginの「YUVMaterial」を設定
今度はiOS機器のカメラを起動して見ましょう。
以下の手順でカメラを起動できます。
  1. HierarchyにEmpty Objectを追加(Create→Create Empty)
  2. 上で追加した要素の名前を「CameraManager」に変更
  3. CameraManagerのInspectorから「Add Component」をクリック
  4. pluginの「Unity AR Camera Manager」Componentを追加
  5. UnityARCameraManagerの「Camera」に「Main Camera」を設定
上記設定後、iOS機器でアプリを起動するとカメラが起動されて画面に映像が映るのを確認できます。
ちなみに、作成した内容をiOS機器にインストールする方法は検索するとすぐに見つけると思いますので、ここでは割愛します。

HitTestを利用し画面タップされた場所にオブジェクトを設置

それでは以下の手順でHitTestを利用して、画面をタップするとその場所(3D空間)に球を設置して見ましょう。
  1. HierarchyにSphereを追加(Create→3D Object→Sphere)
  2. SphereのInspectorのTransform→Scaleのx,y,z全部を「0.03」を設定
  3. SphereオブジェクトをProjectにDrag & Dropし、Prefab化(PrefabについてはUnityのマニュアルなどを参照)
  4. Hierarchyから「Sphere」を削除
  5. HierarchyにEmpty Objectを追加(Create→Create Empty)
  6. 名前を「ObjectMaker」に設定
  7. Projectに「C# Script」を追加(Create→C# Script)
  8. 作成したスクリプトの名前を「ObjectMaker」に設定
  9. ObjectMaker Scriptをダブルクリックし、以下の「ObjectMaker.cs」を参照し、コーデイングする
  10. HierarchyのObjectMakerオブジェクトを選択
  11. Inspectorの「Add Component」をクリックし、上で作成した「ObjectMaker」を選択する
  12. ObjectMakerのobjectに「Sphere」を設定
ObjectMaker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.iOS;

public class ObjectMaker : MonoBehaviour
{
	public GameObject obj;

	void CreateObj(Vector3 atPosition)
	{
		GameObject newBall = Instantiate(obj, atPosition, Quaternion.identity);
	}

	void Update()
	{
		// タッチ入力確認
		if (Input.touchCount > 0)
		{
			var touch = Input.GetTouch(0);
			if (touch.phase == TouchPhase.Began)
			{
				var screenPosition = Camera.main.ScreenToViewportPoint(touch.position);
				ARPoint point = new ARPoint
				{
					x = screenPosition.x,
					y = screenPosition.y
				};
				// スクリーンの座標をWorld座標に変換
				List<ARHitTestResult> hitResults = UnityARSessionNativeInterface.GetARSessionNativeInterface().HitTest(point, ARHitTestResultType.ARHitTestResultTypeFeaturePoint);
				if (hitResults.Count > 0)
				{
					foreach (var hitResult in hitResults)
					{
						Vector3 position = UnityARMatrixOps.GetPosition(hitResult.worldTransform);
						CreateObj(new Vector3(position.x, position.y, position.z));
						break;
					}
				}
			}
		}
	}
}

Sphere

設定が終わったらiOS機器にインストールし、起動して見てください。周辺環境の認識まで数秒かかりますので、数秒後、画面をタップするとその場所の適当な3D空間に球が表示されると思います。
上のソースで注目すべき箇所は30行目です。HitTestを行い画面の座標からWorld座標を取得する処理です。

平面認識

では、最後に平面認識機能を使って見ましょう。
以下の手順で設定を行ってください。
  1. HierarchyにEmpty Objectを追加(Create→Create Empty)
  2. 名前を「PlaneDetector」に設定
  3. Projectに「C# Script」を追加(Create→C# Script)
  4. 作成したスクリプトの名前を「PlaneDetector」に設定
  5. PlaneDetector Scriptをダブルクリックし、以下の「PlaneDetector.cs」を参照し、コーデイングする
  6. HierarchyのPlaneDetectorオブジェクトを選択
  7. Inspectorの「Add Component」をクリックし、上で作成した「PlaneDetector」を選択する
  8. PlaneDetectorのplanePrefabにpluginの「debugPlanePrefab」を設定
  9. Projectの上で作成した「ObjectMaker.cs」を開き「ARHitTestResultTypeFeaturePoint」を「ARHitTestResultTypeExistingPlaneUsingExtent」に変更
PlaneDetector.cs (UnityARAnchorManager.csを真似てます)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.XR.iOS;

public class PlaneDetector : MonoBehaviour {
	public GameObject planePrefab;
	// 認識した平面を管理するため
	private Dictionary<string, ARPlaneAnchorGameObject> planeAnchorMap;

	void Start ()
	{
		planeAnchorMap = new Dictionary<string,ARPlaneAnchorGameObject> ();
		// 各イベントを受け取るメソッド設定
		UnityARSessionNativeInterface.ARAnchorAddedEvent += AddAnchor;
		UnityARSessionNativeInterface.ARAnchorUpdatedEvent += UpdateAnchor;
		UnityARSessionNativeInterface.ARAnchorRemovedEvent += RemoveAnchor;
	}

	private GameObject CreatePlaneInScene(ARPlaneAnchor arPlaneAnchor)
	{
		// 新しい平面オブジェクトを生成
		GameObject newPlane;
		if (planePrefab != null)
		{
			newPlane = Instantiate(planePrefab);
		}
		else
		{
			newPlane = new GameObject();
		}

		newPlane.name = arPlaneAnchor.identifier;
		// 生成した平面オブジェクトをAnchorに合わせる
		return UpdatePlaneWithAnchorTransform(newPlane, arPlaneAnchor);
	}

	private GameObject UpdatePlaneWithAnchorTransform(GameObject plane, ARPlaneAnchor arPlaneAnchor)
	{
		// ARKit座標をUnity座標に変換
		plane.transform.position = UnityARMatrixOps.GetPosition(arPlaneAnchor.transform);
		plane.transform.rotation = UnityARMatrixOps.GetRotation(arPlaneAnchor.transform);

		MeshFilter mf = plane.GetComponentInChildren<MeshFilter>();

		if (mf != null)
		{
			//since our plane mesh is actually 10mx10m in the world, we scale it here by 0.1f
			mf.gameObject.transform.localScale = new Vector3(arPlaneAnchor.extent.x * 0.1f, arPlaneAnchor.extent.y * 0.1f, arPlaneAnchor.extent.z * 0.1f);
			//convert our center position to unity coords
			mf.gameObject.transform.localPosition = new Vector3(arPlaneAnchor.center.x, arPlaneAnchor.center.y, -arPlaneAnchor.center.z);
		}

		return plane;
	}

	// 新しい平面が検出された場合
	public void AddAnchor(ARPlaneAnchor arPlaneAnchor)
	{
		// Anchorに合わせて新しい平面オブジェクト生成
		GameObject go = CreatePlaneInScene(arPlaneAnchor);
		// 生成した平面オブジェクトを管理用Listに登録
		ARPlaneAnchorGameObject arpag = new ARPlaneAnchorGameObject();
		arpag.planeAnchor = arPlaneAnchor;
		arpag.gameObject = go;
		planeAnchorMap.Add(arPlaneAnchor.identifier, arpag);
	}

	// 平面がなくなった場合
	public void RemoveAnchor(ARPlaneAnchor arPlaneAnchor)
	{
		if (planeAnchorMap.ContainsKey(arPlaneAnchor.identifier))
		{
			ARPlaneAnchorGameObject arpag = planeAnchorMap[arPlaneAnchor.identifier];
			Destroy(arpag.gameObject);
			planeAnchorMap.Remove(arPlaneAnchor.identifier);
		}
	}

	// 平面が更新された場合
	public void UpdateAnchor(ARPlaneAnchor arPlaneAnchor)
	{
		if (planeAnchorMap.ContainsKey(arPlaneAnchor.identifier))
		{
			ARPlaneAnchorGameObject arpag = planeAnchorMap[arPlaneAnchor.identifier];
			UpdatePlaneWithAnchorTransform(arpag.gameObject, arPlaneAnchor);
			arpag.planeAnchor = arPlaneAnchor;
			planeAnchorMap[arPlaneAnchor.identifier] = arpag;
		}
	}

	public void Destroy()
	{
		foreach (ARPlaneAnchorGameObject arpag in GetCurrentPlaneAnchors())
		{
			Destroy(arpag.gameObject);
		}

		planeAnchorMap.Clear();
	}

	// 外部から平面を利用させるため
	public List<ARPlaneAnchorGameObject> GetCurrentPlaneAnchors()
	{
		return planeAnchorMap.Values.ToList();
	}
}

PlaneDetection

上で行なっているのは、「UnityARSessionNativeInterface」に平面認識イベントを受け取れるようにイベント処理を登録し、イベントを受け取ったら、「debugPlanePrefab」オブジェクトを追加、更新、削除を行なっています。
さらに追加したオブジェクトは「planeAnchorMap」で管理されます。
そして、hitTestのタイプを認識した平面を考慮した位置が取れるように設定しました。
上の処理を実行するとアプリが平面を認識し、画面をタップすると平面の上に球が表示されると思います。

応用してみましょう

平面の更新による球の管理

認識された平面は周辺の情報を収集する際に平面の位置や広さが更新されます。(カメラを移動すると認識された平面が広くなったり、少し移動してしまったりします。)ただし、上の実装だと球オブジェクトと平面を表示しているオブジェクトに関係性がないため、平面が更新される際に、平面が移動されその上に乗っていた球が平面の下になってしまう場合もあります。
それを防ぐためにはまず、HitTestの結果から取得できる「anchorIdentifier」の値から「PlaneDetector.cs」の「planeAnchorMap」に保存されている平面オブジェクトを探します。
そして、取得した平面オブジェクトと球オブジェクトを親子関係にすると平面が移動された際に球オブジェクトも一緒に移動されるはずです。

平面認識の停止

上の処理は平面を認識した分Planeを作ってしまいますので、一つ以上必要ではない場合は適当なタイミングで平面認識を止めることもできます。
やり方は「CameraManager」オブジェクトに設定したUnityARCameraManagerをやめ同じようなC# Scriptを追加します(コピペーで良いです)。
そして、新たに作成したComponentをCameraManagerに設定します。
ここまでだと同じ動作をするはずです。そこで、Start()関数をよく見るとconfig.planeDetectionに「UnityARPlaneDetection.Horizontal」が設定されているのがわかります。
そして、「m_session.RunWithConfig(config)」を呼び出すことでARを起動してます。
カメラ起動中に新しい「config」を設定し「RunWithConfig」を実行するとモードが切り替えられますので、「planeDetection」を設定してないconfigを設定し「RunWithConfig」を実行するように変えれば平面認識を中断することができます。

まとめ

iOS 11のARKitとUnityを利用してどう実装するかを紹介しました。割と簡単にAR起動が実現できるので、とても便利です。まだ水平面の認識しかできないとか平面認識の精度がイマイチの問題、物体認識などGoogleのTangoに比べるとARの制度が落ちます。
ただし、有料のARライブラリを使わずに多くのデバイス(iPhone 6s以上)で実現できるのでこれから多くのARアプリが登場するのではないかと思います。技術は使えば使うほど発展するので、今後のARKitがどのように変わっていくかが楽しみです。

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。