ARKit 2 in Xamarin.iOS
ARKit è cresciuto notevolmente dopo la sua introduzione l'anno scorso in iOS 11. In primo luogo, è ora possibile rilevare piani verticali e orizzontali, che migliora notevolmente la praticità delle esperienze di realtà aumentata di interni. Sono inoltre disponibili nuove funzionalità:
- Riconoscimento di immagini e oggetti di riferimento come giunzione tra il mondo reale e le immagini digitali
- Nuova modalità di illuminazione che simula l'illuminazione reale
- Possibilità di condividere e rendere persistenti gli ambienti AR
- Nuovo formato di file preferito per l'archiviazione del contenuto AR
Riconoscimento degli oggetti di riferimento
Una funzionalità di presentazione in ARKit 2 è la possibilità di riconoscere immagini e oggetti di riferimento. Le immagini di riferimento possono essere caricate dai normali file di immagine (descritti più avanti), ma gli oggetti di riferimento devono essere analizzati, usando l'oggetto incentrato sullo ARObjectScanningConfiguration
sviluppatore.
App di esempio: Analisi e rilevamento di oggetti 3D
L'esempio è una porta di un progetto Apple che illustra:
- Gestione dello stato dell'applicazione tramite
NSNotification
oggetti - Visualizzazione personalizzata
- Movimenti complessi
- Analisi degli oggetti
- Archiviazione di un oggetto
ARReferenceObject
L'analisi di un oggetto di riferimento è a batteria e a elevato utilizzo del processore e dei dispositivi meno recenti spesso avranno problemi a ottenere un tracciamento stabile.
Gestione dello stato tramite oggetti NSNotification
Questa applicazione usa una macchina a stati che esegue la transizione tra gli stati seguenti:
AppState.StartARSession
AppState.NotReady
AppState.Scanning
AppState.Testing
Usa inoltre un set incorporato di stati e transizioni quando in AppState.Scanning
:
Scan.ScanState.Ready
Scan.ScanState.DefineBoundingBox
Scan.ScanState.Scanning
Scan.ScanState.AdjustingOrigin
L'app usa un'architettura reattiva che invia notifiche di transizione stato a NSNotificationCenter
e sottoscrive queste notifiche. L'installazione è simile a questo frammento di codice da 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);
Un gestore di notifica tipico aggiornerà l'interfaccia utente ed eventualmente modificherà lo stato dell'applicazione, ad esempio questo gestore che aggiorna durante l'analisi dell'oggetto:
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})"));
}
}
Infine, Enter{State}
i metodi modificano il modello e l'esperienza utente in base al nuovo stato:
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();
}
Visualizzazione personalizzata
L'app mostra il "cloud punto" di basso livello dell'oggetto contenuto in un rettangolo delimitatore proiettato su un piano orizzontale rilevato.
Questo cloud di punti è disponibile per gli sviluppatori nella ARFrame.RawFeaturePoints
proprietà . La visualizzazione efficiente del cloud del punto può essere un problema difficile. Scorrere i punti, quindi creare e posizionare un nuovo nodo SceneKit per ogni punto ucciderà la frequenza dei fotogrammi. In alternativa, se fatto in modo asincrono, ci sarebbe un ritardo. L'esempio mantiene le prestazioni con una strategia in tre parti:
- Uso di codice unsafe per aggiungere i dati sul posto e interpretare i dati come buffer non elaborato di byte.
- Conversione del buffer non elaborato in un oggetto
SCNGeometrySource
e creazione di un oggetto "modello".SCNGeometryElement
- Unire rapidamente i dati non elaborati e il modello usando
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;
}
}
}
Il risultato è simile al seguente:
Movimenti complessi
L'utente può ridimensionare, ruotare e trascinare il rettangolo di selezione che circonda l'oggetto di destinazione. Esistono due aspetti interessanti nei riconoscitori di movimento associati.
In primo luogo, tutti i riconoscitori di movimento si attivano solo dopo il superamento di una soglia; ad esempio, un dito ha trascinato così tanti pixel o la rotazione supera un angolo. La tecnica consiste nell'accumulare lo spostamento fino a quando non viene superata la soglia, quindi applicarla in modo incrementale:
// 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;
}
}
}
La seconda cosa interessante da fare in relazione ai gesti è il modo in cui il rettangolo di selezione viene spostato in relazione ai piani reali rilevati. Questo aspetto è illustrato in questo post di blog di Xamarin.
Altre nuove funzionalità in ARKit 2
Altre configurazioni di rilevamento
È ora possibile usare uno dei seguenti elementi come base per un'esperienza di realtà mista:
- Solo l'accelerometro del dispositivo (
AROrientationTrackingConfiguration
, iOS 11) - Visi (
ARFaceTrackingConfiguration
, iOS 11) - Immagini di riferimento (
ARImageTrackingConfiguration
, iOS 12) - Scansione di oggetti 3D (
ARObjectScanningConfiguration
, iOS 12) - Odometria inerziale visiva (
ARWorldTrackingConfiguration
migliorata in iOS 12)
AROrientationTrackingConfiguration
, descritto in questo post di blog e nell'esempio F# è il più limitato e offre un'esperienza di realtà mista scarsa, in quanto inserisce solo oggetti digitali in relazione al movimento del dispositivo, senza tentare di collegare il dispositivo e lo schermo nel mondo reale.
ARImageTrackingConfiguration
Consente di riconoscere immagini 2D reali (dipinti, logo e così via) e di usarle per ancorare immagini digitali:
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;
Esistono due aspetti interessanti per questa configurazione:
- È efficiente e può essere usato con un numero potenzialmente elevato di immagini di riferimento
- L'immagine digitale è ancorata all'immagine, anche se tale immagine si sposta nel mondo reale (ad esempio, se viene riconosciuta la copertina di un libro, tiene traccia del libro come viene estratto dallo scaffale, disposto e così via).
L'oggetto ARObjectScanningConfiguration
è stato discusso in precedenza ed è una configurazione incentrata sullo sviluppatore per l'analisi degli oggetti 3D. Si tratta di un elevato utilizzo di processore e batteria e non deve essere usato nelle applicazioni degli utenti finali.
La configurazione di rilevamento finale, ARWorldTrackingConfiguration
, è il cavallo di battaglia della maggior parte delle esperienze di realtà mista. Questa configurazione usa "odometria inerziale visiva" per correlare "punti di funzionalità" reali alle immagini digitali. La geometria digitale o gli sprite sono ancorati rispetto ai piani orizzontali e verticali reali o relativi alle istanze rilevate ARReferenceObject
. In questa configurazione, l'origine mondiale è la posizione originale della fotocamera nello spazio con l'asse Z allineato alla gravità e gli oggetti digitali "rimangono sul posto" rispetto agli oggetti nel mondo reale.
Texturing ambientale
ARKit 2 supporta la "texturing ambientale" che usa immagini acquisite per stimare l'illuminazione e persino applicare evidenziazioni speculari a oggetti lucidi. La mappa del cubo ambientale viene creata dinamicamente e, una volta che la fotocamera ha guardato in tutte le direzioni, può produrre un'esperienza impressionantemente realistica:
Per utilizzare la descrizione ambientale:
- Gli
SCNMaterial
oggetti devono usareSCNLightingModel.PhysicallyBased
e assegnare un valore compreso nell'intervallo da 0 a 1 perMetalness.Contents
e eRoughness.Contents
- La configurazione di rilevamento deve impostare
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
};
Anche se la trama perfettamente riflettente mostrata nel frammento di codice precedente è divertente in un campione, la texturing ambientale è probabilmente meglio usata con moderazione che attiva una risposta "valle incanny" (la trama è solo una stima basata su ciò che la fotocamera ha registrato).
Esperienze ar condivise e persistenti
Un'altra importante aggiunta ad ARKit 2 è la ARWorldMap
classe , che consente di condividere o archiviare i dati di rilevamento globale. Si ottiene la mappa globale corrente con ARSession.GetCurrentWorldMapAsync
o 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());
}
Per condividere o ripristinare la mappa globale:
- Caricare i dati dal file,
- Unarchive in un
ARWorldMap
oggetto, - Usare tale valore come valore per la
ARWorldTrackingConfiguration.InitialWorldMap
proprietà :
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
};
L'unico ARWorldMap
oggetto contiene dati di rilevamento del mondo non visibili e gli ARAnchor
oggetti che non contengono asset digitali. Per condividere la geometria o l'immagine, è necessario sviluppare una strategia personalizzata appropriata per il caso d'uso, ad esempio archiviando o trasmettendo solo la posizione e l'orientamento della geometria e applicandola a oggetti statici SCNGeometry
o forse archiviando o trasmettendo oggetti serializzati. Il vantaggio di ARWorldMap
è che gli asset, una volta inseriti rispetto a un oggetto condiviso ARAnchor
, verranno visualizzati in modo coerente tra dispositivi o sessioni.
Formato di file Descrizione scena universale
La caratteristica principale finale di ARKit 2 è l'adozione di Apple del formato di file Universal Scene Description di Pixar. Questo formato sostituisce il formato DAE di Collada come formato preferito per la condivisione e l'archiviazione degli asset ARKit. Il supporto per la visualizzazione degli asset è integrato in iOS 12 e Mojave. L'estensione di file USDZ è un archivio ZIP non compresso e non crittografato contenente file USD. Pixar fornisce strumenti per l'uso di file USD, ma non è ancora disponibile un supporto di terze parti.
Suggerimenti per la programmazione ARKit
Gestione manuale delle risorse
In ARKit è fondamentale gestire manualmente le risorse. Non solo ciò consente frequenze di fotogrammi elevate, in realtà è necessario evitare un "blocco dello schermo confuso". Il framework ARKit è lazy per fornire un nuovo fotogramma della fotocamera (ARSession.CurrentFrame
. Fino a quando l'attuale ARFrame
non ha Dispose()
chiamato su di esso, ARKit non fornirà un nuovo frame! In questo modo il video viene "bloccato" anche se il resto dell'app è reattivo. La soluzione consiste nell'accedere ARSession.CurrentFrame
sempre con un using
blocco o chiamare Dispose()
manualmente su di esso.
Tutti gli oggetti derivati da sono e implementano il modello Dispose, pertanto in genere è consigliabile seguire questo modello per l'implementazione Dispose
in una classe derivata.NSObject
IDisposable
NSObject
Modifica delle matrici di trasformazione
In qualsiasi applicazione 3D si tratterà di matrici di trasformazione 4x4 che descrivono in modo compatto come spostare, ruotare ed esadecimare un oggetto attraverso lo spazio 3D. In SceneKit si tratta di SCNMatrix4
oggetti.
La SCNNode.Transform
proprietà restituisce la SCNMatrix4
matrice di trasformazione per l'oggetto SCNNode
come supportato dal tipo row-major simdfloat4x4
. Ad esempio:
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)"
Come si può notare, la posizione viene codificata nei primi tre elementi della riga inferiore.
In Xamarin il tipo comune per la modifica delle matrici di trasformazione è NVector4
, che per convenzione viene interpretato in modo principale della colonna. Ciò significa che il componente traslazione/posizione è previsto in M14, M24, M34, non M41, M42, M43:
Essere coerenti con la scelta dell'interpretazione della matrice è fondamentale per il comportamento corretto. Poiché le matrici di trasformazione 3D sono 4x4, gli errori di coerenza non produrranno alcun tipo di eccezione in fase di compilazione o persino di runtime, ma solo che le operazioni funzioneranno in modo imprevisto. Se gli oggetti SceneKit/ARKit sembrano essere bloccati, volano via o instabilità, una matrice di trasformazione non corretta è una buona possibilità. La soluzione è semplice: NMatrix4.Transpose
eseguirà un'analisi sul posto degli elementi.