Share via


ARKit 2 in Xamarin.iOS

ARKit hat sich seit seiner Einführung im letzten Jahr in iOS 11 erheblich reift. In erster Linie können Sie jetzt vertikale und horizontale Ebenen erkennen, was die Praktischkeit von Indoor Augmented Reality-Erfahrungen erheblich verbessert. Darüber hinaus gibt es neue Funktionen:

  • Erkennen von Referenzbildern und Objekten als Verbindung zwischen der realen Welt und digitalen Bildern
  • Ein neuer Beleuchtungsmodus, der die reale Beleuchtung simuliert
  • Die Möglichkeit zum Freigeben und Beibehalten von AR-Umgebungen
  • Ein neues Dateiformat, das zum Speichern von AR-Inhalten bevorzugt wird

Erkennen von Referenzobjekten

Ein Showcase-Feature in ARKit 2 ist die Möglichkeit, Referenzbilder und Objekte zu erkennen. Referenzbilder können aus normalen Bilddateien geladen werden (weiter unten erläutert), aber Referenzobjekte müssen mit dem Entwicklerfokus ARObjectScanningConfigurationgescannt werden.

Beispiel-App: Scannen und Erkennen von 3D-Objekten

Das Beispiel ist ein Port eines Apple-Projekts , das zeigt:

  • Anwendungsstatusverwaltung mithilfe von NSNotification Objekten
  • Benutzerdefinierte Visualisierung
  • Komplexe Gesten
  • Objektüberprüfung
  • Speichern eines ARReferenceObject

Das Scannen eines Referenzobjekts ist akku- und prozessorintensiv, und ältere Geräte haben häufig Probleme beim Erreichen einer stabilen Nachverfolgung.

Zustandsverwaltung mit NSNotification-Objekten

Diese Anwendung verwendet einen Zustandsautomat, der zwischen den folgenden Zuständen wechselt:

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

Außerdem wird ein eingebetteter Satz von Zuständen und Übergängen in folgenden AppState.ScanningFällen verwendet:

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

Die App verwendet eine reaktive Architektur, die Statusübergangsbenachrichtigungen an diese Benachrichtigungen sendet und diese Benachrichtigungen NSNotificationCenter abonniert. Das Setup sieht wie dieser Codeausschnitt aus 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);

Ein typischer Benachrichtigungshandler aktualisiert die Benutzeroberfläche und ändert möglicherweise den Anwendungszustand, z. B. diesen Handler, der aktualisiert wird, während das Objekt gescannt wird:

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} Schließlich ändern Methoden das Modell und die UX entsprechend dem neuen Zustand:

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();
}

Benutzerdefinierte Visualisierung

Die App zeigt die "Punktwolke" des Objekts auf niedriger Ebene an, das in einem umgebenden Feld enthalten ist, das auf eine erkannte horizontale Ebene projiziert ist.

Diese Punktwolke steht Entwicklern in der ARFrame.RawFeaturePoints Eigenschaft zur Verfügung. Eine effiziente Visualisierung der Punktwolke kann ein schwieriges Problem sein. Das Durchlaufen der Punkte und das Erstellen und Platzieren eines neuen SceneKit-Knotens für jeden Punkt würde die Framerate beenden. Alternativ wäre es bei asynchroner Asynchroner Vorgang zu einer Verzögerung kommen. Das Beispiel Standard die Leistung mit einer dreiteiligen Strategie:

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;
    }
  }
}

Das Ergebnis sieht wie folgt aus:

point_cloud

Komplexe Gesten

Der Benutzer kann das umgebende Feld skalieren, drehen und ziehen, das das Zielobjekt umgibt. Es gibt zwei interessante Dinge in den zugehörigen Gestikerkennungen.

Zunächst werden alle Gestenerkennungen erst aktiviert, nachdem ein Schwellenwert überschritten wurde; Beispielsweise hat ein Finger so viele Pixel gezogen, oder die Drehung überschreitet einen Winkel. Die Technik besteht darin, die Verschiebung anzusammeln, bis der Schwellenwert überschritten wurde, und dann inkrementell anzuwenden:

// 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;
        }
    }
}

Das zweite interessante, was in Bezug auf Gesten getan wird, ist die Art und Weise, wie das Begrenzungsfeld in Bezug auf erkannte reale Flugzeuge verschoben wird. Dieser Aspekt wird in diesem Xamarin-Blogbeitrag erläutert.

Weitere neue Features in ARKit 2

Weitere Nachverfolgungskonfigurationen

Jetzt können Sie eine der folgenden Elemente als Grundlage für eine Mixed-Reality-Erfahrung verwenden:

AROrientationTrackingConfiguration, die in diesem Blogbeitrag und F#-Beispiel erläutert wird, ist die eingeschränktste und bietet eine schlechte Mixed-Reality-Erfahrung, da es nur digitale Objekte in Bezug auf die Bewegung des Geräts platziert, ohne das Gerät und den Bildschirm in die reale Welt zu binden.

Dies ARImageTrackingConfiguration ermöglicht Es Ihnen, reale 2D-Bilder (Gemälde, Logos usw.) zu erkennen und diese zu verwenden, um digitale Bilder zu verankern:

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;

Für diese Konfiguration gibt es zwei interessante Aspekte:

  • Es ist effizient und kann mit einer potenziell großen Anzahl von Referenzbildern verwendet werden.
  • Die digitale Bildwelt ist an das Bild verankert, auch wenn dieses Bild in der realen Welt verschoben wird (z. B. wenn das Cover eines Buchs erkannt wird, wird es das Buch verfolgen, wie es aus dem Regal gezogen, festgelegt usw.).

Dies ARObjectScanningConfiguration wurde zuvor erörtert und ist eine entwicklerorientierte Konfiguration für das Scannen von 3D-Objekten. Es ist sehr prozessor- und akkuintensiv und sollte nicht in Endbenutzeranwendungen verwendet werden.

Die endgültige Tracking-Konfiguration, ARWorldTrackingConfiguration ist die Arbeitshorde der meisten Mixed-Reality-Erfahrungen. Diese Konfiguration verwendet "visuelle Inertial-Odometrie", um echte "Featurepunkte" mit digitalen Bildern zu verknüpfen. Digitale Geometrie oder Sprites werden relativ zu horizontalen und vertikalen Ebenen der realen Welt oder relativ zu erkannten ARReferenceObject Instanzen verankert. In dieser Konfiguration ist der Weltursprung die ursprüngliche Position der Kamera im Raum mit der Z-Achse, die an die Schwerkraft ausgerichtet ist, und digitale Objekte "bleiben an Ort und Stelle" relativ zu Objekten in der realen Welt.

Texturierung der Umgebung

ARKit 2 unterstützt "Umgebungstexturierung", die aufgenommene Bilder verwendet, um Beleuchtung zu schätzen und sogar Glanzlichter auf glänzende Objekte anzuwenden. Die Umgebungs-Cubemap wird dynamisch aufgebaut und kann, sobald die Kamera in alle Richtungen geschaut hat, eine beeindruckende realistische Erfahrung erzeugen:

Demobild für Umgebungstexturierung

Zur Verwendung von Umgebungstexturierung:

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
};

Obwohl die im vorherigen Codeausschnitt gezeigte perfekt reflektierende Textur in einem Beispiel spaßig ist, wird die Umgebungstexturierung wahrscheinlich besser mit Zurückhaltung verwendet, da sie eine "unsinnliche Tal"-Antwort auslöst (die Textur ist nur eine Schätzung basierend auf dem, was die Kamera aufgezeichnet hat).

Freigegebene und persistente AR-Erfahrungen

Eine weitere wichtige Ergänzung zu ARKit 2 ist die Klasse, mit der ARWorldMap Sie World Tracking-Daten freigeben oder speichern können. Sie erhalten die aktuelle Weltkarte mit ARSession.GetCurrentWorldMapAsync oder 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());
}

So teilen oder wiederherstellen Sie die Weltkarte:

  1. Laden sie die Daten aus der Datei,
  2. Heben Sie die Archivierung in einem ARWorldMap Objekt auf,
  3. Verwenden Sie dies als Wert für die ARWorldTrackingConfiguration.InitialWorldMap Eigenschaft:
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
};

Die ARWorldMap einzige enthält nicht sichtbare Weltverfolgungsdaten und die ARAnchor Objekte, sie enthält keine digitalen Objekte. Um Geometrie oder Bilder freizugeben, müssen Sie Ihre eigene Strategie entwickeln, die für Ihren Anwendungsfall geeignet ist (z. B. indem Sie nur den Standort und die Ausrichtung der Geometrie speichern/übertragen und auf statische SCNGeometry oder vielleicht durch Speichern/Übertragen serialisierter Objekte anwenden). Der Vorteil ARWorldMap besteht darin, dass Ressourcen, die einmal relativ zu einem freigegebenen ARAnchorObjekt platziert wurden, konsistent zwischen Geräten oder Sitzungen angezeigt werden.

Universal Scene Description-Dateiformat

Das letzte Schlagzeilenfeature von ARKit 2 ist apples Einführung des Universal Scene Description-Dateiformats von Pixar. Dieses Format ersetzt das DAE-Format von Collada als bevorzugtes Format für das Freigeben und Speichern von ARKit-Objekten. Unterstützung für die Visualisierung von Ressourcen ist in iOS 12 und Mojave integriert. Die USDZ-Dateierweiterung ist ein unkomprimiertes und unverschlüsseltes ZIP-Archiv mit USD-Dateien. Pixar bietet Tools für das Arbeiten mit USD-Dateien , aber es gibt noch keine große Unterstützung von Drittanbietern.

ARKit-Programmiertipps

Manuelle Ressourcenverwaltung

In ARKit ist es wichtig, Ressourcen manuell zu verwalten. Dies lässt nicht nur hohe Frameraten zu, sondern es ist tatsächlich notwendig , eine verwirrende "Bildschirmfrierung" zu vermeiden. Das ARKit-Framework ist faul über die Bereitstellung eines neuen Kamerarahmens (ARSession.CurrentFrame. Bis der aktuelle ARFrame Aufruf dazu aufgerufen wurde Dispose() , liefert ARKit keinen neuen Frame! Dies bewirkt, dass das Video "fixiert" wird, auch wenn der Rest der App reaktionsfähig ist. Die Lösung besteht darin, immer mit einem using Block oder manuell Dispose() darauf zuzugreifenARSession.CurrentFrame.

Alle von NSObject diesem abgeleiteten Objekte sind IDisposable und NSObject implementieren das Dispose-Muster. Daher sollten Sie in der Regel diesem Muster folgen, um eine abgeleitete Klasse zu implementierenDispose.

Bearbeiten von Transformationsmatrizen

In jeder 3D-Anwendung befassen Sie sich mit 4x4-Transformationsmatrizen, die komprimiert beschreiben, wie sie ein Objekt durch den 3D-Raum verschieben, drehen und scheren. In SceneKit sind SCNMatrix4 dies Objekte.

Die SCNNode.Transform Eigenschaft gibt die SCNMatrix4 Transformationsmatrix für die SCNNodevom Zeilen-Haupttyp simdfloat4x4 gesicherte Matrix zurück. So zum Beispiel:

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)"

Wie Sie sehen können, wird die Position in den ersten drei Elementen der unteren Zeile codiert.

In Xamarin ist NVector4der allgemeine Typ für die Bearbeitung von Transformationsmatrizen , die in der Konvention auf spaltenweise interpretiert wird. Das heißt, die Übersetzungs-/Positionskomponente wird in M14, M24, M34, nicht M41, M42, M43 erwartet:

Zeilen-Haupt- und Spalten-Hauptversion

Die Übereinstimmung mit der Wahl der Matrixinterpretation ist für das richtige Verhalten von entscheidender Bedeutung. Da 3D-Transformationsmatrizen 4x4 sind, erzeugen Konsistenzfehler keine Art von Kompilierungszeit- oder sogar Laufzeit-Ausnahme – es ist nur, dass Vorgänge unerwartet funktionieren. Wenn Ihre SceneKit/ARKit-Objekte scheinbar hängen bleiben, wegfliegen oder jittern, ist eine falsche Transformationsmatrix eine gute Möglichkeit. Die Lösung ist einfach: NMatrix4.Transpose wird eine direkte Umsetzung von Elementen durchführen.