Étude de cas - Développement des fonctionnalités de mappage spatial de HoloLens

Lors de la création de nos premières applications pour Microsoft HoloLens, nous étions impatients de voir jusqu’où nous pouvions repousser les limites du mappage spatial sur l’appareil. Jeff Evertt, ingénieur logiciel chez Microsoft Studios, explique comment une nouvelle technologie a été développée pour mieux contrôler la façon dont les hologrammes sont placés dans l’environnement réel d’un utilisateur.

Notes

HoloLens 2 implémente un nouveau runtime Scene Understanding, qui fournit aux développeurs Mixed Reality une représentation d’environnement structurée et de haut niveau conçue pour rendre intuitif le développement d’applications respectueuses de l’environnement.

Regarder la vidéo

Au-delà du mappage spatial

Pendant que nous travaillions sur Fragments et Young Conker, deux des premiers jeux pour HoloLens, nous avons constaté que lorsque nous effectuions le placement procédural des hologrammes dans le monde physique, nous avions besoin d’un niveau plus élevé de compréhension de l’environnement de l’utilisateur. Chaque jeu avait ses propres besoins de placement : dans Fragments, par exemple, nous voulions être en mesure de faire la distinction entre différentes surfaces( comme le sol ou une table) pour placer des indices dans des emplacements pertinents. Nous voulions également être en mesure d’identifier les surfaces sur lesquelles des caractères holographiques de taille réelle pourraient s’asseoir, comme un canapé ou une chaise. Dans Young Conker, nous voulions que Conker et ses adversaires puissent utiliser des surfaces surélevées dans la salle d’un joueur comme plateformes.

Asobo Studios, notre partenaire de développement pour ces jeux, a fait face à ce problème et a créé une technologie qui étend les fonctionnalités de mappage spatial de HoloLens. Grâce à cela, nous pouvons analyser la pièce d’un joueur et identifier les surfaces telles que les murs, les tables, les chaises et les planchers. Il nous a également donné la possibilité d’optimiser par rapport à un ensemble de contraintes afin de déterminer le meilleur emplacement pour les objets holographiques.

Le code de compréhension spatiale

Nous avons pris le code d’origine d’Asobo et créé une bibliothèque qui encapsule cette technologie. Microsoft et Asobo ont maintenant open sourcer ce code et l’ont mis à disposition sur MixedRealityToolkit pour que vous l’utilisiez dans vos propres projets. Tout le code source est inclus, ce qui vous permet de le personnaliser en fonction de vos besoins et de partager vos améliorations avec la communauté. Le code du solveur C++ a été encapsulé dans une DLL UWP et exposé à Unity avec un préfabriqué de dépôt contenu dans MixedRealityToolkit.

Il existe de nombreuses requêtes utiles incluses dans l’exemple Unity qui vous permettront de trouver des espaces vides sur les murs, de placer des objets au plafond ou sur de grands espaces sur le sol, d’identifier les emplacements où les personnages peuvent s’asseoir et d’une myriade d’autres requêtes de compréhension spatiale.

Bien que la solution de mappage spatial fournie par HoloLens soit suffisamment générique pour répondre aux besoins de l’ensemble des espaces de problèmes, le module de compréhension spatiale a été conçu pour prendre en charge les besoins de deux jeux spécifiques. Par conséquent, sa solution est structurée autour d’un processus spécifique et d’un ensemble d’hypothèses :

  • Espace de jeu de taille fixe : l’utilisateur spécifie la taille maximale de l’espace de jeu dans l’appel init.
  • Processus d’analyse unique : le processus nécessite une phase d’analyse discrète dans laquelle l’utilisateur se déplace, en définissant l’espace de jeu. Les fonctions de requête ne fonctionnent qu’une fois l’analyse finalisée.
  • « Peinture » de l’espace de jeu piloté par l’utilisateur : pendant la phase de numérisation, l’utilisateur se déplace et regarde autour de l’espace de jeu, peignant efficacement les zones qui doivent être incluses. Le maillage généré est important pour fournir des commentaires aux utilisateurs pendant cette phase.
  • Installation à l’intérieur de la maison ou du bureau : les fonctions de requête sont conçues autour de surfaces plates et de murs à angle droit. Il s’agit d’une limitation réversible. Toutefois, pendant la phase d’analyse, une analyse de l’axe principal est effectuée pour optimiser le pavage du maillage le long de l’axe principal et secondaire.

Processus d’analyse de salle

Lorsque vous chargez le module de compréhension spatiale, la première chose que vous allez faire est d’analyser votre espace, afin que toutes les surfaces utilisables, telles que le plancher, le plafond et les murs, soient identifiées et étiquetées. Pendant le processus de numérisation, vous regardez autour de votre pièce et « peignez » les zones qui doivent être incluses dans le balayage.

Le maillage observé pendant cette phase est un élément important de commentaires visuels qui permet aux utilisateurs de savoir quelles parties de la pièce sont analysées. La DLL du module de compréhension spatiale stocke en interne l’espace de jeu sous la forme d’une grille de cubes voxel de 8 cm. Pendant la partie initiale de l’analyse, une analyse des composants principaux est effectuée pour déterminer les axes de la salle. En interne, il stocke son espace voxel aligné sur ces axes. Un maillage est généré environ toutes les secondes en extrayant l’isosurface du volume voxel.

Maillage de mappage spatial en blanc et compréhension du maillage d’espace de jeu en vert

Maillage de mappage spatial en blanc et compréhension du maillage d’espace de jeu en vert

Le fichier SpatialUnderstanding.cs inclus gère le processus de phase d’analyse. Il appelle les fonctions suivantes :

  • SpatialUnderstanding_Init : appelé une fois au début.
  • GeneratePlayspace_InitScan : indique que la phase d’analyse doit commencer.
  • GeneratePlayspace_UpdateScan_DynamicScan : a appelé chaque image pour mettre à jour le processus d’analyse. La position et l’orientation de la caméra sont passées et sont utilisées pour le processus de peinture de l’espace de jeu, décrit ci-dessus.
  • GeneratePlayspace_RequestFinish : appelé pour finaliser l’espace de jeu. Cela utilisera les zones « peintes » pendant la phase de balayage pour définir et verrouiller l’espace de jeu. L’application peut interroger les statistiques pendant la phase d’analyse et interroger le maillage personnalisé pour fournir des commentaires aux utilisateurs.
  • Import_UnderstandingMesh : pendant l’analyse, le comportement SpatialUnderstandingCustomMesh fourni par le module et placé sur le préfabriqué de compréhension interroge régulièrement le maillage personnalisé généré par le processus. En outre, cette opération est effectuée une fois de plus une fois l’analyse finalisée.

Le flux d’analyse, piloté par le comportement SpatialUnderstanding , appelle InitScan, puis UpdateScan chaque image. Lorsque la requête de statistiques signale une couverture raisonnable, l’utilisateur peut airtaper pour appeler RequestFinish pour indiquer la fin de la phase d’analyse. UpdateScan continue d’être appelé jusqu’à ce que sa valeur de retour indique que le traitement de la DLL est terminé.

Les requêtes

Une fois l’analyse terminée, vous pourrez accéder à trois types de requêtes différents dans l’interface :

  • Requêtes de topologie : il s’agit de requêtes rapides basées sur la topologie de la salle analysée.
  • Requêtes de forme : elles utilisent les résultats de vos requêtes de topologie pour trouver des surfaces horizontales qui correspondent bien aux formes personnalisées que vous définissez.
  • Requêtes de placement d’objet : il s’agit de requêtes plus complexes qui trouvent l’emplacement le mieux adapté en fonction d’un ensemble de règles et de contraintes pour l’objet.

En plus des trois requêtes principales, il existe une interface de raycasting qui peut être utilisée pour récupérer les types de surface marqués et un maillage de salle étanche personnalisé peut être copié.

Requêtes de topologie

Dans la DLL, le gestionnaire de topologie gère l’étiquetage de l’environnement. Comme mentionné ci-dessus, la plupart des données sont stockées dans des surfels, qui sont contenus dans un volume voxel. En outre, la structure PlaySpaceInfos est utilisée pour stocker des informations sur l’espace de jeu, y compris l’alignement du monde (plus d’informations à ce sujet ci-dessous), le plancher et la hauteur du plafond.

Les heuristiques sont utilisées pour déterminer le plancher, le plafond et les murs. Par exemple, la surface horizontale la plus grande et la plus basse avec une surface supérieure à 1 m2 est considérée comme le plancher. Notez que le chemin de la caméra pendant le processus d’analyse est également utilisé dans ce processus.

Un sous-ensemble des requêtes exposées par le gestionnaire de topologie sont exposées via la DLL. Les requêtes de topologie exposées sont les suivantes :

  • QueryTopology_FindPositionsOnWalls
  • QueryTopology_FindLargePositionsOnWalls
  • QueryTopology_FindLargestWall
  • QueryTopology_FindPositionsOnFloor
  • QueryTopology_FindLargestPositionsOnFloor
  • QueryTopology_FindPositionsSittable

Chacune des requêtes a un ensemble de paramètres, spécifiques au type de requête. Dans l’exemple suivant, l’utilisateur spécifie la hauteur minimale & largeur du volume souhaité, la hauteur minimale de placement au-dessus du plancher et la quantité minimale de dégagement devant le volume. Toutes les mesures sont exprimées en mètres.

EXTERN_C __declspec(dllexport) int QueryTopology_FindPositionsOnWalls(
          _In_ float minHeightOfWallSpace,
          _In_ float minWidthOfWallSpace,
          _In_ float minHeightAboveFloor,
          _In_ float minFacingClearance,
          _In_ int locationCount,
          _Inout_ Dll_Interface::TopologyResult* locationData)

Chacune de ces requêtes utilise un tableau pré-alloué de structures TopologyResult . Le paramètre locationCount spécifie la longueur du tableau transmis. La valeur de retour indique le nombre d’emplacements retournés. Ce nombre n’est jamais supérieur au paramètre locationCount transmis.

TopologyResult contient la position centrale du volume retourné, la direction de face (normale) et les dimensions de l’espace trouvé.

struct TopologyResult
     {
          DirectX::XMFLOAT3 position;
          DirectX::XMFLOAT3 normal;
          float width;
          float length;
     };

Notez que dans l’exemple Unity, chacune de ces requêtes est liée à un bouton dans le panneau de l’interface utilisateur virtuelle. L’exemple code en dur les paramètres de chacune de ces requêtes sur des valeurs raisonnables. Pour plus d’exemples, consultez SpaceVisualizer.cs dans l’exemple de code.

Requêtes de forme

À l’intérieur de la DLL, l’analyseur de forme (ShapeAnalyzer_W) utilise l’analyseur de topologie pour correspondre aux formes personnalisées définies par l’utilisateur. L’exemple Unity a un ensemble prédéfini de formes qui sont affichées dans le menu de requête, sous l’onglet Forme.

Notez que l’analyse des formes fonctionne uniquement sur les surfaces horizontales. Un canapé, par exemple, est défini par la surface du siège plat et le haut plat du dos du canapé. La requête de forme recherche deux surfaces d’une taille, d’une hauteur et d’une plage d’aspects spécifiques, avec les deux surfaces alignées et connectées. Selon la terminologie des API, le siège du canapé et le haut du dos du canapé sont des composants de forme et les exigences d’alignement sont des contraintes de composant de forme.

Voici un exemple de requête défini dans l’exemple Unity (ShapeDefinition.cs) pour les objets « sittable » :

shapeComponents = new List<ShapeComponent>()
     {
          new ShapeComponent(
               new List<ShapeComponentConstraint>()
               {
                    ShapeComponentConstraint.Create_SurfaceHeight_Between(0.2f, 0.6f),
                    ShapeComponentConstraint.Create_SurfaceCount_Min(1),
                    ShapeComponentConstraint.Create_SurfaceArea_Min(0.035f),
               }),
     };
     AddShape("Sittable", shapeComponents);

Chaque requête de forme est définie par un ensemble de composants de forme, chacun avec un ensemble de contraintes de composant et un ensemble de contraintes de forme qui répertorie les dépendances entre les composants. Cet exemple inclut trois contraintes dans une définition de composant unique et aucune contrainte de forme entre les composants (car il n’y a qu’un seul composant).

En revanche, la forme du canapé a deux composants de forme et quatre contraintes de forme. Notez que les composants sont identifiés par leur index dans la liste des composants de l’utilisateur (0 et 1 dans cet exemple).

shapeConstraints = new List<ShapeConstraint>()
        {
              ShapeConstraint.Create_RectanglesSameLength(0, 1, 0.6f),
              ShapeConstraint.Create_RectanglesParallel(0, 1),
              ShapeConstraint.Create_RectanglesAligned(0, 1, 0.3f),
              ShapeConstraint.Create_AtBackOf(1, 0),
        };

Les fonctions wrapper sont fournies dans le module Unity pour faciliter la création de définitions de formes personnalisées. La liste complète des contraintes de composant et de forme se trouve dans SpatialUnderstandingDll.cs au sein des structures ShapeComponentConstraint et ShapeConstraint .

Le rectangle bleu met en évidence les résultats de la requête de forme de la chaise.

Le rectangle bleu met en évidence les résultats de la requête de forme de la chaise.

Solveur de placement d’objets

Les requêtes de placement d’objets peuvent être utilisées pour identifier les emplacements idéaux dans la salle physique pour placer vos objets. Le solveur trouvera l’emplacement le mieux adapté en fonction des règles et contraintes de l’objet. En outre, les requêtes d’objet persistent jusqu’à ce que l’objet soit supprimé avec des appels Solver_RemoveObject ou Solver_RemoveAllObjects , ce qui permet un placement multi-objets limité.

Les requêtes de placement d’objets se composent de trois parties : le type de placement avec des paramètres, une liste de règles et une liste de contraintes. Pour exécuter une requête, utilisez l’API suivante :

public static int Solver_PlaceObject(
                [In] string objectName,
                [In] IntPtr placementDefinition,	// ObjectPlacementDefinition
                [In] int placementRuleCount,
                [In] IntPtr placementRules,     	// ObjectPlacementRule
                [In] int constraintCount,
                [In] IntPtr placementConstraints,	// ObjectPlacementConstraint
                [Out] IntPtr placementResult)

Cette fonction prend un nom d’objet, une définition de placement et une liste de règles et de contraintes. Les wrappers C# fournissent des fonctions d’assistance de construction pour faciliter la construction des règles et des contraintes. La définition de placement contient le type de requête, c’est-à-dire l’un des éléments suivants :

public enum PlacementType
                {
                    Place_OnFloor,
                    Place_OnWall,
                    Place_OnCeiling,
                    Place_OnShape,
                    Place_OnEdge,
                    Place_OnFloorAndCeiling,
                    Place_RandomInAir,
                    Place_InMidAir,
                    Place_UnderFurnitureEdge,
                };

Chacun des types de placement a un ensemble de paramètres propres au type. La structure ObjectPlacementDefinition contient un ensemble de fonctions d’assistance statiques pour la création de ces définitions. Par exemple, pour trouver un emplacement où placer un objet sur le sol, vous pouvez utiliser la fonction suivante :

public static ObjectPlacementDefinition Create_OnFloor(Vector3 halfDims)

En plus du type de placement, vous pouvez fournir un ensemble de règles et de contraintes. Les règles ne peuvent pas être violées. Les emplacements de placement possibles qui répondent au type et aux règles sont ensuite optimisés par rapport à l’ensemble de contraintes pour sélectionner l’emplacement de placement optimal. Chacune des règles et contraintes peut être créée par les fonctions de création statique fournies. Un exemple de fonction de construction de règle et de contrainte est fourni ci-dessous.

public static ObjectPlacementRule Create_AwayFromPosition(
                    Vector3 position, float minDistance)
               public static ObjectPlacementConstraint Create_NearPoint(
                    Vector3 position, float minDistance = 0.0f, float maxDistance = 0.0f)

La requête de placement d’objet ci-dessous recherche un emplacement pour placer un cube d’un demi-mètre sur le bord d’une surface, loin des autres objets précédemment placés et près du centre de la pièce.

List<ObjectPlacementRule> rules = 
          new List<ObjectPlacementRule>() {
               ObjectPlacementRule.Create_AwayFromOtherObjects(1.0f),
          };

     List<ObjectPlacementConstraint> constraints = 
          new List<ObjectPlacementConstraint> {
               ObjectPlacementConstraint.Create_NearCenter(),
          };

     Solver_PlaceObject(
          “MyCustomObject”,
          new ObjectPlacementDefinition.Create_OnEdge(
          new Vector3(0.25f, 0.25f, 0.25f), 
          new Vector3(0.25f, 0.25f, 0.25f)),
          rules.Count,
          UnderstandingDLL.PinObject(rules.ToArray()),
          constraints.Count,
          UnderstandingDLL.PinObject(constraints.ToArray()),
          UnderstandingDLL.GetStaticObjectPlacementResultPtr());

En cas de réussite, une structure ObjectPlacementResult contenant la position de placement, les dimensions et l’orientation est retournée. En outre, le placement est ajouté à la liste interne d’objets placés de la DLL. Les requêtes de placement suivantes prendront cet objet en compte. Le fichier LevelSolver.cs de l’exemple Unity contient d’autres exemples de requêtes.

Les zones bleues affichent le résultat de trois requêtes Place On Floor avec des règles de « position loin de la caméra ».

Les zones bleues affichent le résultat de trois requêtes Place On Floor avec des règles de « position loin de la caméra ».

Conseils :

  • Lors de la résolution de l’emplacement de plusieurs objets requis pour un scénario de niveau ou d’application, résolvez d’abord les objets indispensables et volumineux pour optimiser la probabilité qu’un espace soit trouvé.
  • L’ordre de placement est important. Si les emplacements d’objets sont introuvables, essayez des configurations moins contraintes. Disposer d’un ensemble de configurations de secours est essentiel à la prise en charge des fonctionnalités dans de nombreuses configurations de salle.

Casting de rayons

En plus des trois requêtes principales, une interface de diffusion de rayons peut être utilisée pour récupérer les types de surfaces étiquetées et un maillage d’espace de jeu étanche personnalisé peut être copié une fois que la salle a été analysée et finalisée, des étiquettes sont générées en interne pour les surfaces telles que le plancher, le plafond et les murs. La fonction PlayspaceRaycast prend un rayon et retourne si le rayon entre en collision avec une surface connue et, le cas échéant, des informations sur cette surface sous la forme d’un RaycastResult.

struct RaycastResult
     {
          enum SurfaceTypes
          {
               Invalid,	// No intersection
               Other,
               Floor,
               FloorLike,         // Not part of the floor topology, 
                                  //     but close to the floor and looks like the floor
               Platform,          // Horizontal platform between the ground and 
                                  //     the ceiling
               Ceiling,
               WallExternal,
               WallLike,          // Not part of the external wall surface, 
                                  //     but vertical surface that looks like a 
                                  //	wall structure
               };
               SurfaceTypes SurfaceType;
               float SurfaceArea;	// Zero if unknown 
                                        //	(i.e. if not part of the topology analysis)
               DirectX::XMFLOAT3 IntersectPoint;
               DirectX::XMFLOAT3 IntersectNormal;
     };

En interne, le raycast est calculé sur la représentation voxel cube de 8 cm calculée du playspace. Chaque voxel contient un ensemble d’éléments de surface avec des données de topologie traitées (également appelées surfels). Les surfels contenus dans la cellule voxel croisée sont comparés et la meilleure correspondance est utilisée pour rechercher les informations de topologie. Ces données de topologie contiennent l’étiquetage retourné sous la forme de l’énumération SurfaceTypes , ainsi que la surface de surface de la surface croisée.

Dans l’exemple Unity, le curseur projette un rayon chaque image. Premièrement, contre les collisionneurs d’Unity ; deuxièmement, à l’encontre de la représentation mondiale du module de compréhension ; et enfin, par rapport aux éléments d’interface utilisateur. Dans cette application, l’interface utilisateur obtient la priorité, puis le résultat de compréhension et enfin, les collisionneurs d’Unity. Le SurfaceType est signalé sous forme de texte en regard du curseur.

Intersection du rapport de résultat Raycast avec le sol.

Intersection du rapport de résultat Raycast avec le sol.

Obtenir le code

Le code open source est disponible dans MixedRealityToolkit. Faites-nous savoir sur les forums des développeurs HoloLens si vous utilisez le code dans un projet. Nous avons hâte de voir ce que vous en faites !

À propos de l’auteur

Jeff Evertt, responsable de l’ingénierie logicielle chez Microsoft Jeff Evertt est un responsable de l’ingénierie logicielle qui travaille sur HoloLens depuis ses débuts, de l’incubation au développement d’expérience. Avant HoloLens, il a travaillé sur Xbox Kinect et dans l’industrie des jeux sur une grande variété de plateformes et de jeux. Jeff est passionné par la robotique, les graphismes et les choses avec des lumières flashy qui bip. Il aime apprendre de nouvelles choses et travailler sur les logiciels, le matériel, et particulièrement dans l’espace où les deux se croisent.

Voir aussi