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 ARObjectScanningConfiguration
gescannt 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.Scanning
Fä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. Im Beispiel wird die Leistung mit einer dreiteiligen Strategie beibehalten:
- Verwenden von unsicherem Code zum Anheften der Daten und Interpretieren der Daten als unformatierter Bytepuffer.
- Konvertieren des rohen Puffers in ein
SCNGeometrySource
"template"-Objekt und Erstellen eines "template"SCNGeometryElement
-Objekts. - Schnelles "Zusammenfügen" der Rohdaten und der Vorlage mit
SCNGeometry.Create(SCNGeometrySource[], SCNGeometryElement[])
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 = &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:
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:
- Nur der Gerätebeschleigungsmesser (
AROrientationTrackingConfiguration
iOS 11) - Gesichter (
ARFaceTrackingConfiguration
iOS 11) - Referenzimages (
ARImageTrackingConfiguration
iOS 12) - Scannen von 3D-Objekten (
ARObjectScanningConfiguration
iOS 12) - Visuelle Inertial-Odometrie (
ARWorldTrackingConfiguration
verbessert in iOS 12)
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:
Zur Verwendung von Umgebungstexturierung:
- Ihre
SCNMaterial
Objekte müssen einen Wert im Bereich von 0 bis 1 verwendenSCNLightingModel.PhysicallyBased
undRoughness.Contents
zuweisen undMetalness.Contents
- Ihre Nachverfolgungskonfiguration muss folgendes festlegen
EnvironmentTexturing
=AREnvironmentTexturing.Automatic
:
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:
- Laden sie die Daten aus der Datei,
- Heben Sie die Archivierung in einem
ARWorldMap
Objekt auf, - 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 ARAnchor
Objekt 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 SCNNode
vom 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 NVector4
der 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:
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.