Share via


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 ARObjectScanningConfigurationsviluppatore.

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:

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

Il risultato è simile al seguente:

point_cloud

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:

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:

immagine demo di texturing ambientale

Per utilizzare la descrizione ambientale:

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:

  1. Caricare i dati dal file,
  2. Unarchive in un ARWorldMap oggetto,
  3. 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.NSObjectIDisposableNSObject

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 SCNNodecome 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:

row-major e column-major

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.