ARKit 2 dans Xamarin.iOS

ARKit a considérablement évolué depuis son introduction l’an dernier dans iOS 11. Tout d’abord, vous pouvez désormais détecter les plans verticaux et horizontaux, ce qui améliore considérablement la fonctionnalité des expériences de réalité augmentée en intérieur. En outre, il existe de nouvelles fonctionnalités :

  • Reconnaissance d’images et d’objets de référence comme jonction entre le monde réel et l’imagerie numérique
  • Un nouveau mode d’éclairage qui simule l’éclairage réel
  • La possibilité de partager et de conserver des environnements AR
  • Nouveau format de fichier préféré pour le stockage du contenu AR

Reconnaissance des objets de référence

Une fonctionnalité de démonstration dans ARKit 2 est la possibilité de reconnaître des images et des objets de référence. Les images de référence peuvent être chargées à partir de fichiers image normaux (abordés plus loin), mais les objets de référence doivent être analysés, à l’aide du fichier dédié aux ARObjectScanningConfigurationdéveloppeurs .

Exemple d’application : analyse et détection d’objets 3D

L’exemple Analyse et détection d’objets 3D est un port d’un projet Apple qui illustre :

  • Gestion de l’état des applications à l’aide d’objets NSNotification
  • Visualisation personnalisée
  • Mouvements complexes
  • Analyse des objets
  • Stockage d’un ARReferenceObject

L’analyse d’un objet de référence nécessite beaucoup de batterie et de processeur, et les appareils plus anciens auront souvent des difficultés à obtenir un suivi stable.

Gestion des états à l’aide d’objets NSNotification

Cette application utilise une machine à états qui effectue la transition entre les états suivants :

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

Et utilise également un ensemble incorporé d’états et de transitions dans AppState.Scanning:

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

L’application utilise une architecture réactive qui publie des notifications de transition d’état et NSNotificationCenter s’y abonne. Le programme d’installation ressemble à cet extrait de code de 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 gestionnaire de notifications classique met à jour l’interface utilisateur et peut éventuellement modifier l’état de l’application, tel que ce gestionnaire qui se met à jour au fur et à mesure que l’objet est analysé :

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

Enfin, Enter{State} les méthodes modifient le modèle et l’expérience utilisateur en fonction du nouvel état :

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

Visualisation personnalisée

L’application affiche le « nuage de points » de bas niveau de l’objet contenu dans un cadre englobant projeté sur un plan horizontal détecté.

Ce cloud de points est disponible pour les développeurs dans la ARFrame.RawFeaturePoints propriété . La visualisation efficace du cloud de points peut être un problème délicat. L’itération sur les points, puis la création et le placement d’un nœud SceneKit pour chaque point tue la fréquence d’images. Sinon, s’il est effectué de manière asynchrone, il y aurait un décalage. L’exemple maintient les performances avec une stratégie en trois parties :

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

Le résultat ressemble à ceci :

point_cloud

Mouvements complexes

L’utilisateur peut mettre à l’échelle, faire pivoter et faire glisser le cadre englobant l’objet cible. Il existe deux éléments intéressants dans les modules de reconnaissance de mouvements associés.

Tout d’abord, tous les modules de reconnaissance de mouvements ne s’activent qu’une fois qu’un seuil a été dépassé ; par exemple, un doigt a fait glisser tant de pixels ou la rotation dépasse un certain angle. La technique consiste à accumuler le déplacement jusqu’à ce que le seuil ait été dépassé, puis à l’appliquer de manière incrémentielle :

// 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 deuxième chose intéressante en ce qui concerne les mouvements est la façon dont le cadre englobant est déplacé par rapport aux plans du monde réel détectés. Cet aspect est abordé dans ce billet de blog Xamarin.

Autres nouvelles fonctionnalités dans ARKit 2

Autres configurations de suivi

Maintenant, vous pouvez utiliser l’un des éléments suivants comme base d’une expérience de réalité mixte :

AROrientationTrackingConfiguration, abordé dans ce billet de blog et l’exemple F#, est le plus limité et offre une mauvaise expérience de réalité mixte, car il place uniquement des objets numériques par rapport au mouvement de l’appareil, sans essayer de lier l’appareil et l’écran au monde réel.

Le ARImageTrackingConfiguration vous permet de reconnaître des images 2D réelles (peintures, logos, etc.) et de les utiliser pour ancrer des images numériques :

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;

Cette configuration présente deux aspects intéressants :

  • Il est efficace et peut être utilisé avec un nombre potentiellement important d’images de référence
  • L’imagerie numérique est ancrée à l’image, même si cette image se déplace dans le monde réel (par exemple, si la couverture d’un livre est reconnue, elle suit le livre lorsqu’il est retiré de l’étagère, posé, etc.).

Le ARObjectScanningConfiguration a été abordé précédemment et est une configuration centrée sur les développeurs pour l’analyse d’objets 3D. Il est très gourmand en processeur et en batterie et ne doit pas être utilisé dans les applications des utilisateurs finaux. L’exemple Analyse et détection d’objets 3D illustre l’utilisation de cette configuration.

La configuration de suivi finale, ARWorldTrackingConfiguration , est le cheval de bataille de la plupart des expériences de réalité mixte. Cette configuration utilise l'« odométrie inertielle visuelle » pour établir un lien entre les « points de caractéristique » réels et l’imagerie numérique. La géométrie numérique ou les sprites sont ancrés par rapport aux plans horizontaux et verticaux réels ou par rapport aux instances détectées ARReferenceObject . Dans cette configuration, l’origine du monde est la position d’origine de la caméra dans l’espace avec l’axe Z aligné sur la gravité, et les objets numériques « restent en place » par rapport aux objets du monde réel.

Texturing environnemental

ARKit 2 prend en charge la « texturation environnementale » qui utilise des images capturées pour estimer l’éclairage et même appliquer des surbrillances spéculaires à des objets brillants. Le cubemap environnemental est construit dynamiquement et, une fois que la caméra a regardé dans toutes les directions, peut produire une expérience incroyablement réaliste :

image de démonstration de texturing environnemental

Pour utiliser la texturation environnementale :

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

Bien que la texture parfaitement réfléchissante indiquée dans l’extrait de code précédent soit amusante dans un exemple, la texturation environnementale est probablement mieux utilisée avec retenue de peur qu’elle ne déclenche une réponse « vallée étrange » (la texture n’est qu’une estimation basée sur ce que la caméra a enregistré).

Expériences DE RÉALITÉ AUGMENTÉE partagées et persistantes

Un autre ajout majeur à ARKit 2 est la ARWorldMap classe, qui vous permet de partager ou de stocker des données de suivi mondial. Vous obtenez la carte du monde actuelle avec ARSession.GetCurrentWorldMapAsync ou 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());
}

Pour partager ou restaurer la carte du monde :

  1. Chargez les données à partir du fichier,
  2. Désarchiver dans un ARWorldMap objet,
  3. Utilisez cette valeur comme valeur pour la ARWorldTrackingConfiguration.InitialWorldMap propriété :
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
};

Le ARWorldMap seul contient des données de suivi du monde non visibles et les ARAnchor objets, il ne contient pas de ressources numériques. Pour partager la géométrie ou l’imagerie, vous devez développer votre propre stratégie adaptée à votre cas d’usage (peut-être en stockant/transmettant uniquement l’emplacement et l’orientation de la géométrie et en l’appliquant à des objets statiques SCNGeometry ou peut-être en stockant/transmettant des objets sérialisés). L’avantage de est ARWorldMap que les ressources, une fois placées par rapport à un partagé ARAnchor, s’affichent de manière cohérente entre les appareils ou les sessions.

Format de fichier Description de scène universelle

La dernière fonctionnalité principale d’ARKit 2 est l’adoption par Apple du format de fichier Description de scène universelle de Pixar. Ce format remplace le format DAE de Collada comme format préféré pour le partage et le stockage des ressources ARKit. La prise en charge de la visualisation des ressources est intégrée à iOS 12 et Mojave. L’extension de fichier USDZ est une archive zip non compressée et non chiffrée contenant des fichiers USD. Pixar fournit des outils pour travailler avec des fichiers USD , mais il n’existe pas encore beaucoup de prise en charge tierce.

Conseils de programmation ARKit

Gestion manuelle des ressources

Dans ARKit, il est essentiel de gérer manuellement les ressources. Non seulement cela permet des fréquences d’images élevées, mais il est en fait nécessaire d’éviter un « gel de l’écran » confus. Le framework ARKit est paresseux quant à la fourniture d’un nouveau cadre de caméra (ARSession.CurrentFrame. Tant que le courant ARFrame n’a pas fait Dispose() appel à lui, ARKit ne fournira pas de nouveau cadre! Cela entraîne le « gel » de la vidéo, même si le reste de l’application est réactif. La solution consiste à toujours y accéder ARSession.CurrentFrame avec un using bloc ou à l’appeler Dispose() manuellement.

Tous les objets dérivés de NSObject sont IDisposable et NSObject implémentent le modèle Disposer. Vous devez donc généralement suivre ce modèle pour l’implémentation Dispose sur une classe dérivée.

Manipulation de matrices de transformation

Dans n’importe quelle application 3D, vous allez traiter des matrices de transformation 4x4 qui décrivent de manière compacte comment déplacer, faire pivoter et cisailler un objet dans l’espace 3D. Dans SceneKit, il s’agit d’objets SCNMatrix4 .

La SCNNode.Transform propriété retourne la matrice de SCNMatrix4 transformation pour le SCNNodecomme adossé au type row-major simdfloat4x4 . Par conséquent, pour instance :

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

Comme vous pouvez le voir, la position est encodée dans les trois premiers éléments de la ligne inférieure.

Dans Xamarin, le type courant pour la manipulation de matrices de transformation est NVector4, qui est interprété par convention de manière à être interprété de manière à colonne principale. Autrement dit, le composant de traduction/position est attendu dans M14, M24, M34, et non M41, M42, M43 :

row-major vs column-major

Il est essentiel d’être cohérent avec le choix de l’interprétation de la matrice pour un comportement correct. Étant donné que les matrices de transformation 3D sont 4x4, les erreurs de cohérence ne produisent aucun type d’exception au moment de la compilation ou même de l’exécution . C’est simplement que les opérations agissent de manière inattendue. Si vos objets SceneKit/ARKit semblent être bloqués, s’envolent ou giguent, une matrice de transformation incorrecte est une bonne possibilité. La solution est simple : NMatrix4.Transpose effectuera une transposition sur place des éléments.