AR (augmented-reality、拡張現実) を Xamarin と iOS 11 で試してみました

iOS 11 の主な新機能の 1 つとして、A9 以降の新しいチップ (6S およびそれ以降の iPhone、2017 年リリースの iPad、iPad Pro) で動作するデバイス上で AR (Augment Reality、拡張現実) モードを利用できる ARKit があります。ARKit を使用すると、ユーザーはデバイスを持って、現実世界の水平面に “張り付いた” ように見えるビデオ フィードやコンピューターで生成した画像 (2D SpriteKit または 3D SceneKit) の合成ビデオを見ることができます。

ARKit の簡単な概要

ARKit を利用した Lion の役割は、デバイスの裏側で行われます。基本的に、ARSession オブジェクトがデバイスのモーション データ、カメラの物理的特性 (焦点距離、ピクセルサイズなど) 、および計算ジオメトリを組み合わせてビデオ ストリームのインプットから追跡可能な “特徴点” を検出し、固定の世界座標系をもとに探し出し、現実世界とコンピューターで生成した画像を繋ぐ ARAnchor オブジェクトを作成します。

これには制限があります。前述のように、ARKit は古い iOS デバイスをサポートしません。さらに、ARKit は水平面しか検出できません (ARPlaneDetection は Horizontal のみを定義する列挙型ですが、制限の原因とみられるおかしな動作は Apple 社がすぐに修正すると予測されます) 。最後に、AR のシーン内のコンピューター画像はビデオ上でレンダリングされ、それを遮断する現実世界のオブジェクト上に表示されます。

ARKit には、5 つの重要なコンセプトがあります:

  • すべては世界座標系をもとに定義され、ARSession を実行開始直後に初期化されます。
  • 画像処理技術は、フレームごとに現実世界でのハイコントラストな点を探し出します。これらの “特徴点” は、開発者によって利用可能な中間結果ですが、主にシステムによる ARPlaneAnchor オブジェクトの検出を通知します。ARKit が水平面を検出する前に、比較的多くのフレームでたくさんの特徴点が必要です。当然のことながら、明るい照明やテクスチャ加工されたサーフェスは、均等に照らされたマットなサーフェスよりも多くの追跡可能な特徴点を生成します。
  • 画像処理が続行されると、水平面が作成、削除、および合成されます。それらの範囲は、ワールド トラッキングに応じて、シフト、拡大、および縮小します。
  • コンピューター画像は、これらの水平面を支えとし、実際の場所や方角にあるようにスクリーン上でレンダリングされます。
  • AR の処理は 1 秒につき 60 回発生するため、パフォーマンスの最適化には、メモリやこれ以上必要のないリソースを削除することが重要です。Xamarin’s Profiler をご活用ください!

次の 2 つの画像は、これらのコンセプトを表します。最初の画像は、現実世界の “空間に浮かぶ” 軸、黄色の点の集まりとして表された現在の特徴点、ARKit が ARPlaneAnchor を配置した小さな赤い立方体を示しています。

2 つ目の画像は、世界座標系、カメラ/iOS デバイス、およびアンカーの概念図を表すとともに、ARPlaneAnchor によって定義された水平面、世界座標系と水平面に基づいて配置された 3D SceneKit ジオメトリを表します。

ARKit で最も簡単に AR 拡張現実を作成

開発者は、実際に ARKit でのレンダリングを完全にコントロールできますが、ほとんどの場合、SceneKit (3D) と SpriteKit (2D) に基づいた、AR (augmented-reality) コンテンツを提供する事前に定義した ARSCNView と ARSKView を使用します。

次の ARKit プログラムで SceneKit ジオメトリ (キューブ) を配置することで、水平面に “張り付いた” ように見えます。その前に、ARKit を初期化し、水平面を認識できるまでの十分な期間、実行する必要があります。

ARKit を、ViewDidLoad と ViewWillAppear の 2 つのメソッドで初期化します。初めに、ViewDidLoad を実行します。

public class ARKitController : UIViewController
{		
	ARSCNView scnView; 		
	public ARKitController() : base(){ }	
	
	public override bool ShouldAutorotate() => true;		
	public override void ViewDidLoad()	
	{			
		base.ViewDidLoad();			
		scnView = new ARSCNView()		
		{				
			Frame = this.View.Frame,			
			Delegate = new ARDelegate(),				
			DebugOptions = ARSCNDebugOptions.ShowFeaturePoints | ARSCNDebugOptions.ShowWorldOrigin,				
			UserInteractionEnabled = true			
		};		
		
		this.View.AddSubview(scnView);	
	}
//... code continues ... 

これは、一般的な iOS UIViewController のサブクラスです。新しく ARSCNView (SceneKit 3D ジオメトリにおける簡単な使用ルート) を作成し、デリゲート オブジェクトを ARDelegate (後程説明あり) と呼ばれるクラスのインスタンスへ設定して、デバッグを視覚化し、タッチに応答するビューを有効化し、ビューの階層に ARSCNView を追加します。

初期化の第 2 部は、ViewWillAppear の間で発生します (これは ViewDidLoad の間に実行できる可能性がありますが、iOS の高度でステートフルなビュー初期化プロセスに常に注意します)。

public override void ViewWillAppear(bool animated)
{	
	base.ViewWillAppear(animated);	
	
	// Configure ARKit 	
	var config = new ARWorldTrackingConfiguration();	
	config.PlaneDetection = ARPlaneDetection.Horizontal;
		
	// This method is called subsequent to ViewDidLoad so we know scnView is instantiated	
	scnView.Session.Run(config, ARSessionRunOptions.RemoveExistingAnchors);
}

これは、単純に先程作成した ARSCNView の ARSession を構成し、処理を開始します。

この時点で、ARKit はバックグラウンドで処理を開始します。スクリーン上では、ユーザーのカメラによるフィードが表示され、数秒後にデバッグの視覚化が始まり、フィーチャー クラウドと世界座標系の起点 (先程のスクリーンショットに似ている) を表示します。

しばらくすると、ARKit は水平面を追跡するのに十分な同一平面上の特徴点を発見します。発見した場合、ARKit は自動的にワールド トラッキングへ ARPlaneAnchor を追加します。この追加は、ARSCNView オブジェクトのデリゲート オブジェクト上の DidAddNode メソッドをトリガーします。今回の例では、ARDelegate になります。


public class ARDelegate : ARSCNViewDelegate
{	
	public override void DidAddNode(ISCNSceneRenderer renderer, SCNNode node, ARAnchor anchor)	
	{
		if (anchor != null && anchor is ARPlaneAnchor)
		{	
			PlaceAnchorNode(node, anchor as ARPlaneAnchor);
		}
	}	
				
	void PlaceAnchorNode(SCNNode node, ARPlaneAnchor anchor)	
	{
		var plane = SCNPlane.Create(anchor.Extent.X, anchor.Extent.Z);
		plane.FirstMaterial.Diffuse.Contents = UIColor.LightGray;
		
		var planeNode = SCNNode.FromGeometry(plane);
 
		//Locate the plane at the position of the anchor
		planeNode.Position = new SCNVector3(anchor.Extent.X, 0.0f, anchor.Extent.Z);
 
		//Rotate it to lie flat
		planeNode.Transform = SCNMatrix4.CreateRotationX((float) (Math.PI / 2.0));
		node.AddChildNode(planeNode);
 
		//Mark the anchor with a small red box
		var box = new SCNBox 
		{ 
			Height = 0.1f, 
			Width = 0.1f, 
			Length = 0.1f 
		};
		box.FirstMaterial.Diffuse.ContentColor = UIColor.Red;
		var anchorNode = new SCNNode 
		{ 
			Position = new SCNVector3(0, 0, 0), 
			Geometry = box 
		};
		planeNode.AddChildNode(anchorNode);	
	}	
	
	public override void DidUpdateNode(ISCNSceneRenderer renderer, SCNNode node, ARAnchor anchor)	
	{
		if (anchor is ARPlaneAnchor)
		{	
			var planeAnchor = anchor as ARPlaneAnchor;	
			System.Console.WriteLine($"The (updated) extent of the anchor is [{planeAnchor.Extent.X} , {planeAnchor.Extent.Y} , {planeAnchor.Extent.Z} ]");
		}
	}
} 

DidAddNode は、ノードがビューに追加された場合に呼び出されますが、ARKit が水平面の内部モデルに追加している状態を示す ARPlaneAnchor であるかという特別な処理に注目します。状態をテストし、true の場合、this.PlaceAnchorCube を呼び出します。次にそのメソッドは、水平面のジオメトリを維持し、アンカーと同様に世界座標系に配置されたノードと、ARPlaneAnchor の視覚的なインジケーターである小さな赤い箱といったいくつかの SceneKit ジオメトリ作成します。SceneKit は、scene-graph アーキテクチャを使用するため、anchorNode 位置 [0,0,0] は、世界座標系に配置された planeAnchor に基づく親 planeNode の位置からの相対位置であることに注意してください。

このメソッドが呼び出されたら、上記のスクリーンショットとほとんど同じものが表示されます。

ヒットテスト

少なくとも 1 つの水平面が検出および追跡されたら、スクリーンにタッチして追加のジオメトリを配置できます。ViewController クラスに戻ります。

// This snippet is part of:
public class ARKitController : UIViewController
{
	//… code shown previously …
	public override void TouchesBegan(NSSet touches, UIEvent evt)
	{
		base.TouchesBegan(touches, evt);
		var touch = touches.AnyObject as UITouch;
		if (touch != null)
		{
			var loc = touch.LocationInView(scnView);
			var worldPos = WorldPositionFromHitTest(loc);
			if (worldPos != null)
			{
				PlaceCube(worldPos.Item1);
			}
		}
	}
	private SCNVector3 PositionFromTransform(NMatrix4 xform)
	{	
		return new SCNVector3(xform.M14, xform.M24, xform.M34);
	}
 
	(SCNVector3, ARAnchor) WorldPositionFromHitTest (CGPoint pt)
	{	
		//Hit test against existing anchors	
		var hits = scnView.HitTest(pt, ARHitTestResultType.ExistingPlaneUsingExtent);	
		if (hits != null && hits.Length > 0)	
		{		
			var anchors = hits.Where(r => r.Anchor is ARPlaneAnchor);		
			if (anchors.Count() > 0)		
			{			
				var first = anchors.First();			
				var pos = PositionFromTransform(first.WorldTransform);			
				return (pos, (ARPlaneAnchor)first.Anchor);		
			}	
		}	
		
		return null;
	}
		
	private SCNMaterial[] LoadMaterials()
	{	
		Func<string, SCNMaterial> LoadMaterial = fname =>	
		{		
			var mat = new SCNMaterial();		
			mat.Diffuse.Contents = UIImage.FromFile(fname);		
			mat.LocksAmbientWithDiffuse = true;		return mat;	
		};	
		
		var a = LoadMaterial("msft_logo.png");	
		var b = LoadMaterial("xamagon.png");	
		var c = LoadMaterial("fsharp.png"); 
 
		// This demo was originally in F# 🙂 	
		return new[] { a, b, a, b, c, c };
	}
		
	SCNNode PlaceCube(SCNVector3 pos)
	{	
		var box = new SCNBox 
		{ 
			Width = 0.10f, 
			Height = 0.10f, 
			Length = 0.10f 
		};	
		
		var cubeNode = new SCNNode 
		{ 
			Position = pos, 
			Geometry = box 
		};	
		
		cubeNode.Geometry.Materials = LoadMaterials();	
		scnView.Scene.RootNode.AddChildNode(cubeNode);	
		return cubeNode;
	}
}

TouchesBegan メソッドへスキップすると、ハイレベルかつとても簡単であることが分かります。タッチの位置を把握して、水平面のヒットテストを行います。ヒットテストが成功した場合、最初の水平面の位置をタプル型 (SCNVector3ARPlaneAnchor) として返します。ヒットテストは、最も近いものから順序付けされ、AR 拡張現実 “世界” の外側へ光線を投影し、交差するあらゆる平面上のアンカーを含む配列を返すビルトインの ARSceneView.HitTest(SCNVector3, ARHitTestResultType) メソッドを使用して行われます。配列が空でない場合、1 番目を取得し、位置を SCNVector3 (アンカーの NMatrix4 行列の適切なコンポ―ネントから抽出) として返します。(ヒストリカル ノート: これらの行列に使用されるタイプは、iOS 11 のベータ期間中に Xamarin バインディングで行優先と列優先が切り替わりました。ベータ期間中に書かれたコードをレビューすると、回転や変換が転位したように見えることがあります。)

PlaceCube メソッドは、横 10cm の箱を作成し、上記のとおり WorldPositionFromHitTest によって返された SCNVector3  の値を持つpos で AR 拡張現実 “世界” の中に配置します。

結果はこのようになります。

さらに詳しく

この夏、Twitter のハッシュタグ #MadeWithARKit で、仮想ポータル、空間に配置されたサウンド ノード、および ARKit とビデオ フィルターの組み合わせなどの素晴らしいコンセプトがデモされ、インスピレーションの多大な源となりました。

iPhone X の発表後、Apple 社は ARFaceAnchor と ARFaceGeometry を含む、顔認証とマッピングに関する新しい API を公開しました。

このサンプルで使用したすべてのコードは、こちらから入手できます。

Xamarin Developer Center では、iOS 11 の紹介ガイドをご覧いただけます。

弊社では、Xamarin に関するさまざまな日本語のドキュメントを用意しています。ご興味のある方は、こちらの Xamarin 日本語ドキュメント ページをご覧ください。

Xamarin のパートナー一覧や、トレーニング情報はこちらのページにて随時更新中です。

記事参照:
2017 年 9月 17日 Larry O’Brien
© 2017 Xamarin Inc.
「Augment Reality with Xamarin and iOS 11」

シェアする

  • このエントリーをはてなブックマークに追加

フォローする