Share via


Xamarin.iOS の ARKit 2

ARKit は、昨年 iOS 11 で導入されて以来、大幅に成熟しました。 まず第一に、水平面だけでなく垂直面も検出できるようになり、屋内拡張現実エクスペリエンスの有効性が大幅に向上しました。 さらに、以下の新しい機能が搭載されています。

  • 現実世界とデジタル画像の接点としての参照画像とオブジェクトの認識
  • 現実世界の照明をシミュレートする新しい照明モード
  • AR 環境を共有および永続化する機能
  • AR コンテンツの保存に適した新しいファイル形式

参照オブジェクトの認識

ARKit 2 の魅力的な機能の 1 つに、参照画像とオブジェクトを認識する機能があります。 参照画像は通常の画像ファイルから読み込むことができますが (後述)、参照オブジェクトは開発者向けの ARObjectScanningConfiguration を使用してスキャンする必要があります。

サンプル アプリ: 3D オブジェクトのスキャンと検出

このサンプルは、Apple プロジェクトのポートです。

  • NSNotification オブジェクトを使用したアプリケーション状態の管理
  • カスタムの視覚化
  • 複雑なジェスチャ
  • オブジェクトのスキャン
  • ARReferenceObject の保存

参照オブジェクトをスキャンするとバッテリーとプロセッサに負荷がかかり、古いデバイスでは、安定した追跡が困難なことがよくあります。

NSNotification オブジェクトを使用した状態管理

このアプリケーションでは、次の状態間を遷移する状態機械を使用します。

  • AppState.StartARSession
  • AppState.NotReady
  • AppState.Scanning
  • AppState.Testing

さらに、AppState.Scanning の場合に、埋め込まれた一連の状態と遷移を使用します。

  • Scan.ScanState.Ready
  • Scan.ScanState.DefineBoundingBox
  • Scan.ScanState.Scanning
  • Scan.ScanState.AdjustingOrigin

このアプリは、NSNotificationCenter に状態遷移通知をポストし、これらの通知を登録する反応型のアーキテクチャを使用しています。 セットアップは、ViewController.cs の次のスニペットのようになります。

// Configure notifications for application state changes
var notificationCenter = NSNotificationCenter.DefaultCenter;

notificationCenter.AddObserver(Scan.ScanningStateChangedNotificationName, State.ScanningStateChanged);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxCreatedNotificationName, State.GhostBoundingBoxWasCreated);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxRemovedNotificationName, State.GhostBoundingBoxWasRemoved);
notificationCenter.AddObserver(ScannedObject.BoundingBoxCreatedNotificationName, State.BoundingBoxWasCreated);
notificationCenter.AddObserver(BoundingBox.ScanPercentageChangedNotificationName, ScanPercentageChanged);
notificationCenter.AddObserver(BoundingBox.ExtentChangedNotificationName, BoundingBoxExtentChanged);
notificationCenter.AddObserver(BoundingBox.PositionChangedNotificationName, BoundingBoxPositionChanged);
notificationCenter.AddObserver(ObjectOrigin.PositionChangedNotificationName, ObjectOriginPositionChanged);
notificationCenter.AddObserver(NSProcessInfo.PowerStateDidChangeNotification, DisplayWarningIfInLowPowerMode);

オブジェクトのスキャン時に更新される以下のハンドラーのように、一般的な通知ハンドラーは UI を更新し、場合によってはアプリケーション状態を変更します。

private void ScanPercentageChanged(NSNotification notification)
{
    var pctNum = TryGet<NSNumber>(notification.UserInfo, BoundingBox.ScanPercentageUserKey);
    if (pctNum == null)
    {
        return;
    }
    double percentage = pctNum.DoubleValue;
    // Switch to the next state if scan is complete
    if (percentage >= 100.0)
    {
        State.SwitchToNextState();
    }
    else
    {
        DispatchQueue.MainQueue.DispatchAsync(() => navigationBarController.SetNavigationBarTitle($"Scan ({percentage})"));
    }
}

最後に、Enter{State} メソッドは、新しい状態に応じてモデルと UX を適切に変更します。

internal void EnterStateTesting()
{
    navigationBarController.SetNavigationBarTitle("Testing");
    navigationBarController.ShowBackButton(false);
    loadModelButton.Hidden = true;
    flashlightButton.Hidden = false;
    nextButton.Enabled = true;
    nextButton.SetTitle("Share", UIControlState.Normal);

    testRun = new TestRun(sessionInfo, sceneView);
    TestObjectDetection();
    CancelMaxScanTimeTimer();
}

カスタムの視覚化

アプリは、検出された水平面に投影された境界ボックス内に含まれるオブジェクトの低レベルの “ポイント クラウド“ を表示します。

このポイント クラウドは、ARFrame.RawFeaturePoints プロパティの開発者が使用できます。 ポイント クラウドを効率的に視覚化すると、難しい問題が発生する可能性があります。 ポイントを反復処理し、各ポイントに新しい SceneKit ノードを作成して配置すると、フレーム レートが低下します。 一方、非同期で行うと、遅延が発生します。 サンプルでは、次の 3 つの部分で構成される方法でパフォーマンスを維持しています。

internal static SCNGeometry CreateVisualization(NVector3[] points, UIColor color, float size)
{
  if (points.Length == 0)
  {
    return null;
  }

  unsafe
  {
    var stride = sizeof(float) * 3;

    // Pin the data down so that it doesn't move
    fixed (NVector3* pPoints = &amp;points[0])
    {
      // Important: Don't unpin until after `SCNGeometry.Create`, because geometry creation is lazy

      // Grab a pointer to the data and treat it as a byte buffer of the appropriate length
      var intPtr = new IntPtr(pPoints);
      var pointData = NSData.FromBytes(intPtr, (System.nuint) (stride * points.Length));

      // Create a geometry source (factory) configured properly for the data (3 vertices)
      var source = SCNGeometrySource.FromData(
        pointData,
        SCNGeometrySourceSemantics.Vertex,
        points.Length,
        true,
        3,
        sizeof(float),
        0,
        stride
      );

      // Create geometry element
      // The null and bytesPerElement = 0 look odd, but this is just a template object
      var template = SCNGeometryElement.FromData(null, SCNGeometryPrimitiveType.Point, points.Length, 0);
      template.PointSize = 0.001F;
      template.MinimumPointScreenSpaceRadius = size;
      template.MaximumPointScreenSpaceRadius = size;

      // Stitch the data (source) together with the template to create the new object
      var pointsGeometry = SCNGeometry.Create(new[] { source }, new[] { template });
      pointsGeometry.Materials = new[] { Utilities.Material(color) };
      return pointsGeometry;
    }
  }
}

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

point_cloud

複雑なジェスチャ

ユーザーは、ターゲット オブジェクトを囲む境界ボックスを拡大縮小、回転、ドラッグできます。 関連付けられたジェスチャ認識エンジンには、2 つの興味深い点があります。

まず、すべてのジェスチャ認識エンジンは、しきい値を超えた後にのみアクティブになります。たとえば、指が特定のピクセル分ドラッグされた場合や、回転が特定の角度を超えた場合などです。 この手法では、しきい値を超えるまで移動を累積し、増分方式で適用します。

// A custom rotation gesture recognizer that fires only when a threshold is passed
internal partial class ThresholdRotationGestureRecognizer : UIRotationGestureRecognizer
{
    // The threshold after which this gesture is detected.
    const double threshold = Math.PI / 15; // (12°)

    // Indicates whether the currently active gesture has exceeded the threshold
    private bool thresholdExceeded = false;

    private double previousRotation = 0;
    internal double RotationDelta { get; private set; }

    internal ThresholdRotationGestureRecognizer(IntPtr handle) : base(handle)
    {
    }

    // Observe when the gesture's state changes to reset the threshold
    public override UIGestureRecognizerState State
    {
        get => base.State;
        set
        {
            base.State = value;

            switch(value)
            {
                case UIGestureRecognizerState.Began :
                case UIGestureRecognizerState.Changed :
                    break;
                default :
                    // Reset threshold check
                    thresholdExceeded = false;
                    previousRotation = 0;
                    RotationDelta = 0;
                    break;
            }
        }
    }

    public override void TouchesMoved(NSSet touches, UIEvent evt)
    {
        base.TouchesMoved(touches, evt);

        if (thresholdExceeded)
        {
            RotationDelta = Rotation - previousRotation;
            previousRotation = Rotation;
        }

        if (! thresholdExceeded && Math.Abs(Rotation) > threshold)
        {
            thresholdExceeded = true;
            previousRotation = Rotation;
        }
    }
}

ジェスチャに関して実行されている 2 つ目の興味深い点は、検出された現実世界の平面に対して境界ボックスが移動する方法です。 この点については、この Xamarin のブログ記事を参照してください。

ARKit 2 のその他の新機能

その他の追跡構成

複合現実エクスペリエンスの基盤として、次のいずれかを使用できるようになりました。

このブログ記事と F# サンプルで説明されている AROrientationTrackingConfiguration は非常に限定的で、デバイスとスクリーンを現実世界に関連付けるのではなく、デジタル オブジェクトをデバイスの動きに関連付けるだけであり、複合現実エクスペリエンスが低下します。

ARImageTrackingConfiguration では、現実世界の 2D 画像 (絵画、ロゴなど) を認識し、それらを使用してデジタル画像を固定できます。

var imagesAndWidths = new[] {
    ("cover1.jpg", 0.185F),
    ("cover2.jpg", 0.185F),
     //...etc...
    ("cover100.jpg", 0.185F),
};

var referenceImages = new NSSet<ARReferenceImage>(
    imagesAndWidths.Select( imageAndWidth =>
    {
      // Tuples cannot be destructured in lambda arguments
        var (image, width) = imageAndWidth;
        // Read the image
        var img = UIImage.FromFile(image).CGImage;
        return new ARReferenceImage(img, ImageIO.CGImagePropertyOrientation.Up, width);
    }).ToArray());

configuration.TrackingImages = referenceImages;

この構成には、次の 2 つの興味深い点があります。

  • 効率性が高く、多数の参照画像で使用できます
  • デジタル画像は、現実世界でその画像が移動しても、その画像に固定されます (たとえば、本の表紙が認識されると、本が棚から取り出されたり、横にして置かれたりしても追跡します)。

ARObjectScanningConfiguration以前に説明しましたが、3D オブジェクトをスキャンするための開発者向けの構成です。 プロセッサとバッテリーに非常に負荷がかかるため、エンドユーザー アプリケーションでは使用しないでください。

最後の追跡構成である ARWorldTrackingConfiguration は、ほとんどの複合現実エクスペリエンスで中心的な役割を果たします。 この構成では、"視覚慣性オドメトリ" を使用して、現実世界の "特徴点" をデジタル画像に関連付けます。 デジタル ジオメトリまたはスプライトは、現実世界の水平および垂直面、または検出された ARReferenceObject インスタンスを基準にして固定されます。 この構成では、ワールドの原点はカメラの空間内の元の位置になり、Z 軸は重力に沿って配置され、デジタル オブジェクトは現実世界のオブジェクトを基準にして "その位置を維持" します。

環境テクスチャリング

ARKit 2 は、キャプチャした画像を使用して照明を推定し、光沢のあるオブジェクトに鏡面ハイライトも適用できる "環境テクスチャリング" をサポートしています。 環境キューブ マップは動的に構築され、カメラをあらゆる方向に向けることで、驚くほど現実的なエクスペリエンスを生み出すことができます。

環境テクスチャのデモ画像

環境テクスチャリングを使用するには:

var sphere = SCNSphere.Create(0.33F);
sphere.FirstMaterial.LightingModelName = SCNLightingModel.PhysicallyBased;
// Shiny metallic sphere
sphere.FirstMaterial.Metalness.Contents = new NSNumber(1.0F);
sphere.FirstMaterial.Roughness.Contents = new NSNumber(0.0F);

// Session configuration:
var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
    LightEstimationEnabled = true,
    EnvironmentTexturing = AREnvironmentTexturing.Automatic
};

前のコード スニペットで示されている完全に反射するテクスチャは例としては面白いですが、"不気味の谷" 反応を引き起こす可能性があるため、環境テクスチャリングは控えめに使用した方がよいでしょう (テクスチャは、カメラが記録した内容に基づく推定にすぎません)。

共有および永続的な AR エクスペリエンス

ARKit 2 に追加されたもう 1 つの重要な機能は ARWorldMap クラスで、ワールド追跡データを共有または保存することができます。 ARSession.GetCurrentWorldMapAsync または GetCurrentWorldMap(Action<ARWorldMap,NSError>) を使用して、現在のワールド マップを取得できます。

// Local storage
var PersistentWorldPath => Environment.GetFolderPath(Environment.SpecialFolder.Personal) + "/arworldmap";

// Later, after scanning the environment thoroughly...
var worldMap = await Session.GetCurrentWorldMapAsync();
if (worldMap != null)
{
    var data = NSKeyedArchiver.ArchivedDataWithRootObject(worldMap, true, out var err);
    if (err != null)
    {
        Console.WriteLine(err);
    }
    File.WriteAllBytes(PersistentWorldPath, data.ToArray());
}

ワールド マップを共有または復元するには:

  1. ファイルからデータを読み込みます。
  2. これを ARWorldMap オブジェクトにアーカイブ解除します。
  3. これを ARWorldTrackingConfiguration.InitialWorldMap プロパティの値として使用します。
var data = NSData.FromArray(File.ReadAllBytes(PersistentWorldController.PersistenWorldPath));
var worldMap = (ARWorldMap)NSKeyedUnarchiver.GetUnarchivedObject(typeof(ARWorldMap), data, out var err);

var configuration = new ARWorldTrackingConfiguration
{
    PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
    LightEstimationEnabled = true,
    EnvironmentTexturing = AREnvironmentTexturing.Automatic,
    InitialWorldMap = worldMap
};

ARWorldMap には、表示されないワールド追跡データと ARAnchor オブジェクトのみが含まれ、デジタル資産は含まれません。 ジオメトリまたは画像を共有するには、ユース ケースに適した独自の方法を開発する必要があります (ジオメトリの位置と向きのみを保存/送信して静的な SCNGeometry に適用するか、シリアル化されたオブジェクトを保存/送信します)。 ARWorldMap の利点は、共有された ARAnchor を基準にして配置された資産が、デバイスまたはセッション間で一貫して表示されることです。

Universal Scene Description ファイル形式

ARKit 2 の最後の主な機能は、Apple による Pixar の Universal Scene Description ファイル形式の採用です。 この形式は、Collada の DAE 形式に代わり、ARKit 資産の共有と保存を行う推奨形式です。 資産の視覚化のサポートは、iOS 12 と Mojave に組み込まれています。 USDZ ファイル拡張子は、USD ファイルを含み、圧縮と暗号化が行われていない zip アーカイブです。 Pixar では USD ファイルを操作するためのツールが用意されていますが、まだサードパーティのサポートは多くありません。

ARKit プログラミングのヒント

手動リソース管理

ARKit では、リソースを手動で管理することが重要です。 これにより、フレーム レートが高くなるだけでなく、混乱を招く "画面のフリーズ" を回避するために必要不可欠です。ARKit フレームワークでは、新しいカメラ フレームの提供が十分に行われません (ARSession.CurrentFrame。 現在の ARFrameDispose() が呼び出されるまで、ARKit は新しいフレームを提供しません。 これにより、アプリの残りの部分が応答している場合でも、ビデオが "フリーズ" します。 解決するには、常に using ブロックを使用して ARSession.CurrentFrame にアクセスするか、手動で Dispose() を呼び出します。

NSObject から派生したすべてのオブジェクトは IDisposable であり、NSObject破棄パターンを実装します。このため、通常、派生クラスで Dispose を実装するにはこのパターンを適用する必要があります。

変換行列の操作

3D アプリケーションでは、オブジェクトを 3D 空間でどのように移動、回転、せん断するかを簡単に記述する 4x4 変換行列を扱うことになります。 SceneKit では、これらは SCNMatrix4 オブジェクトです。

SCNNode.Transform プロパティは、行優先の simdfloat4x4 型によってサポートされているSCNNodeSCNMatrix4 変換行列を返します。 以下に例を示します。

var node = new SCNNode { Position = new SCNVector3(2, 3, 4) };  
var xform = node.Transform;
Console.WriteLine(xform);
// Output is: "(1, 0, 0, 0)\n(0, 1, 0, 0)\n(0, 0, 1, 0)\n(2, 3, 4, 1)"

ご覧のように、位置は最下行の最初の 3 つの要素にエンコードされています。

Xamarin では、変換行列を操作するための一般的な型は NVector4 であり、慣例に従って列優先で解釈されます。 つまり、M41、M42、M43 ではなく、M14、M24、M34 に移動/位置要素が含まれることが期待されます。

row-major と column-major

行列の解釈方法に一貫性を持たせることが、適切な動作に不可欠です。 3D 変換行列は 4x4 であるため、一貫性が確保できていない場合、コンパイル時または実行時の例外が生成されません。操作によって予期しない動作が行われるだけです。 SceneKit/ARKit オブジェクトが動かなくなった、画面から消失した、またはジッターが発生した場合、変換行列が正しくない可能性があります。 解決策は簡単で、NMatrix4.Transpose で要素のインプレース転置を実行します。