ARKit 2 dans Xamarin.iOS
ARKit a considérablement mûri depuis son introduction l’année dernière dans iOS 11. Tout d’abord, vous pouvez désormais détecter des plans verticaux et horizontaux, ce qui améliore considérablement la pratique des expériences de réalité augmentée intérieure. En outre, il existe de nouvelles fonctionnalités :
- Reconnaissance des images de référence et des objets comme la jonction entre le monde réel et l’imagerie numérique
- Un nouveau mode d’éclairage qui simule l’éclairage réel
- Possibilité de partager et de conserver des environnements AR
- Un nouveau format de fichier préféré pour le stockage du contenu AR
Reconnaissance des objets de référence
Une fonctionnalité de présentation 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 développeur ARObjectScanningConfiguration
.
Exemple d’application : Analyse et détection d’objets 3D
L’exemple 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 d’objets
- Stockage d’un
ARReferenceObject
L’analyse d’un objet de référence est batterie et processeur intensive et les appareils plus anciens ont souvent des difficultés à obtenir un suivi stable.
Gestion de l’état à l’aide d’objets NSNotification
Cette application utilise une machine d’état qui passe 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 vers NSNotificationCenter
et s’abonne à ces notifications. La configuration ressemble à cet extrait de code à partir 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 notification classique met à jour l’interface utilisateur et peut éventuellement modifier l’état de l’application, tel que ce gestionnaire qui met à jour l’objet à 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 de l’état suivant :
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 point est disponible pour les développeurs dans la ARFrame.RawFeaturePoints
propriété. La visualisation efficace du cloud de point peut être un problème difficile. Itérer sur les points, puis créer et placer un nouveau nœud SceneKit pour chaque point tuerait la fréquence d’images. Sinon, si c’est fait de façon asynchrone, il y aurait un décalage. L’exemple gère les performances avec une stratégie en trois parties :
- Utilisation d’un code non sécurisé pour épingler les données en place et interpréter les données en tant que mémoire tampon brute d’octets.
- Conversion de cette mémoire tampon brute en objet
SCNGeometrySource
« template » et création d’un objet « template ».SCNGeometryElement
- « Assembler rapidement » les données brutes et le modèle à l’aide de
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;
}
}
}
Le résultat ressemble à ceci :
Mouvements complexes
L’utilisateur peut mettre à l’échelle, faire pivoter et faire glisser la zone englobante qui entoure l’objet cible. Il existe deux choses intéressantes dans les modules de reconnaissance de mouvement associés.
Tout d’abord, tous les modules de reconnaissance de mouvement s’activent uniquement une fois qu’un seuil a été passé ; par exemple, un doigt a fait glisser tant de pixels ou la rotation dépasse un 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 réels détectés. Cet aspect est abordé dans ce billet de blog Xamarin.
Autres nouvelles fonctionnalités dans ARKit 2
Autres configurations de suivi
À présent, vous pouvez utiliser l’un des éléments suivants comme base pour une expérience de réalité mixte :
- Seul l’accéléromètre de l’appareil (
AROrientationTrackingConfiguration
iOS 11) - Visages (
ARFaceTrackingConfiguration
iOS 11) - Images de référence (
ARImageTrackingConfiguration
iOS 12) - Analyse des objets 3D (
ARObjectScanningConfiguration
iOS 12) - odométrie de l’inertie visuelle (
ARWorldTrackingConfiguration
améliorée dans iOS 12)
AROrientationTrackingConfiguration
, abordé dans cet exemple de billet de blog et F#, est le plus limité et fournit une expérience de réalité mixte médiocre, car il place uniquement les objets numériques par rapport au mouvement de l’appareil, sans essayer de lier l’appareil et l’écran dans le monde réel.
Vous ARImageTrackingConfiguration
pouvez reconnaître des images 2D réelles (peintures, logos, etc.) et les utiliser pour ancrer l’imagerie numérique :
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;
Il existe deux aspects intéressants de cette configuration :
- 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, il suit le livre tel qu’il est retiré de l’étagère, posé, etc.).
L’objet ARObjectScanningConfiguration
a été abordé précédemment et est une configuration centrée sur le développeur pour l’analyse des objets 3D. Il est hautement processeur et gourmand en batterie et ne doit pas être utilisé dans les applications de l’utilisateur final.
La configuration finale du suivi, ARWorldTrackingConfiguration
est le cheval de bataille de la plupart des expériences de réalité mixte. Cette configuration utilise la « odométrie de l’inertie visuelle » pour lier les « points de caractéristique » réels à 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 dans le monde réel.
Texturing environnemental
ARKit 2 prend en charge le « texturing environnemental » qui utilise des images capturées pour estimer l’éclairage et même appliquer des surbrillances spéculaires aux objets brillants. Le cubemap environnemental est construit dynamiquement et, une fois que l’appareil photo a regardé dans toutes les directions, peut produire une expérience impressionnantement réaliste :
Pour utiliser le texturing environnemental :
- Vos
SCNMaterial
objets doivent utiliserSCNLightingModel.PhysicallyBased
et affecter une valeur de la plage de 0 à 1 pourMetalness.Contents
etRoughness.Contents
- Votre configuration de suivi doit être définie
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
};
Bien que la texture parfaitement réfléchissante indiquée dans l’extrait de code précédent soit amusante dans un exemple, le texturing environnemental est probablement mieux utilisé avec retenue, il déclenche une réponse « noncanny valley » (la texture n’est qu’une estimation basée sur ce que l’appareil photo a enregistré).
Expériences AR 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 mondiale 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 mondiale :
- Chargez les données à partir du fichier,
- Annuler l’archivage dans un
ARWorldMap
objet, - 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 mondial non visibles et les ARAnchor
objets, il ne contient pas de ressources numériques. Pour partager la géométrie ou l’imagerie, vous devrez développer votre propre stratégie approprié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 est ARWorldMap
que les ressources, une fois placées par rapport à un partage ARAnchor
, apparaissent de manière cohérente entre les appareils ou les sessions.
Format de fichier Description de scène universelle
La dernière fonctionnalité d’ARKit 2 est l’adoption par Apple du format de fichier Universal Scene Description 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 autorise-t-il des fréquences d’images élevées, il est en fait nécessaire d’éviter un « gel de l’écran » confus. L’infrastructure ARKit est différée sur l’approvisionnement d’un nouveau cadre de caméra (ARSession.CurrentFrame
. Jusqu’à ce que le courant ARFrame
ait Dispose()
appelé, ARKit ne fournira pas de nouveau cadre ! Cela provoque le « gel » de la vidéo, même si le reste de l’application est réactif. La solution consiste à toujours accéder ARSession.CurrentFrame
à l’aide d’un using
bloc ou d’un appel Dispose()
manuel dessus.
Tous les objets dérivés de sont et implémentent le modèle Dispose. Vous devez donc généralement suivre ce modèle pour l’implémentation Dispose
sur une classe dérivée.NSObject
IDisposable
NSObject
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 écailler un objet à travers l’espace 3D. Dans SceneKit, il s’agit d’objets SCNMatrix4
.
La SCNNode.Transform
propriété retourne la SCNMatrix4
matrice de transformation pour l’objet SCNNode
tel qu’il est soutenu par le type principal simdfloat4x4
de ligne. Par exemple :
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 commun de manipulation des matrices de transformation est NVector4
, qui, par convention, est interprété de manière majeure par colonne. Autrement dit, le composant traduction/position est attendu dans M14, M24, M34, et non M41, M42, M43 :
Être cohérent avec le choix de l’interprétation de matrice est essentiel au comportement approprié. Étant donné que les matrices de transformation 3D sont 4 x 4, les erreurs de cohérence ne produisent pas de type d’exception de compilation ou même d’exécution , c’est simplement que les opérations agissent de manière inattendue. Si vos objets SceneKit / ARKit semblent être bloqués, volants ou gigues, une matrice de transformation incorrecte est une bonne possibilité. La solution est simple : NMatrix4.Transpose
effectuera une transpose sur place d’éléments.