Fallstudie: Erweitern der Räumlichen Zuordnungsfunktionen von HoloLens

Beim Erstellen unserer ersten Apps für Microsoft HoloLens waren wir gespannt, wie weit wir die Grenzen der räumlichen Zuordnung auf dem Gerät verschieben können. Jeff Evertt, Softwareentwickler bei Microsoft Studios, erklärt, wie eine neue Technologie entwickelt wurde, um mehr Kontrolle darüber zu erhalten, wie Hologramme in der realen Umgebung eines Benutzers platziert werden.

Hinweis

HoloLens 2 implementiert eine neue Scene Understanding Runtime, die Mixed Reality Entwicklern eine strukturierte, allgemeine Umgebungsdarstellung bietet, die die Entwicklung für umweltbewusste Anwendungen intuitiv gestalten soll.

Video ansehen

Über die räumliche Zuordnung hinaus

Während wir an Fragmenten und Young Conkerarbeiteten, zwei der ersten Spiele für HoloLens, stellten wir fest, dass wir bei der prozeduralen Platzierung von Hologrammen in der physischen Welt ein höheres Maß an Verständnis über die Umgebung des Benutzers benötigten. Jedes Spiel hatte seine eigenen spezifischen Platzierungsanforderungen: In Fragmenten wollten wir beispielsweise in der Lage sein, zwischen verschiedenen Oberflächen – wie dem Boden oder einem Tisch – zu unterscheiden, um Hinweise an relevanten Stellen zu platzieren. Wir wollten auch Oberflächen identifizieren können, auf denen lebensgroße holografische Zeichen wie eine Couch oder einen Stuhl sitzen könnten. In Young Conker wollten wir, dass Conker und seine Gegner in der Lage sind, erhöhte Oberflächen in einem Spielerzimmer als Plattformen zu verwenden.

Asobo Studios, unser Entwicklungspartner für diese Spiele, stellte sich diesem Problem und entwickelte eine Technologie, die die räumlichen Kartenfunktionen von HoloLens erweitert. Damit könnten wir den Raum eines Spielers analysieren und Oberflächen wie Wände, Tische, Stühle und Böden identifizieren. Es gab uns auch die Möglichkeit, eine Reihe von Einschränkungen zu optimieren, um die beste Platzierung für holografische Objekte zu bestimmen.

Der Code des räumlichen Verständnisses

Wir haben den ursprünglichen Code von Asobo übernommen und eine Bibliothek erstellt, die diese Technologie kapselt. Microsoft und Asobo haben diesen Code nun als Open-Source-Code bereitgestellt und in MixedRealityToolkit zur Verfügung gestellt, damit Sie ihn in Ihren eigenen Projekten verwenden können. Der gesamte Quellcode ist enthalten, sodass Sie ihn an Ihre Anforderungen anpassen und Ihre Verbesserungen mit der Community teilen können. Der Code für den C++-Solver wurde in eine UWP-DLL umschlossen und unity mit einem Drop-In-Prefab in MixedRealityToolkit verfügbar gemacht.

Das Unity-Beispiel enthält viele nützliche Abfragen, mit denen Sie leere Leerzeichen an Wänden finden, Objekte an der Decke oder in großen Räumen auf dem Boden platzieren, Orte identifizieren können, an denen Zeichen sitzen können, und eine Vielzahl anderer Abfragen des räumlichen Verständnisses.

Während die von HoloLens bereitgestellte Lösung für die räumliche Zuordnung generisch genug ist, um die Anforderungen der gesamten Bandbreite von Problemräumen zu erfüllen, wurde das Modul für räumliches Verständnis entwickelt, um die Anforderungen von zwei spezifischen Spielen zu unterstützen. Daher ist die Lösung nach einem bestimmten Prozess und einer Reihe von Annahmen strukturiert:

  • Playspace mit fester Größe: Der Benutzer gibt die maximale Größe des Playspaces im Initialisierungsaufruf an.
  • Einmaliger Scanprozess: Der Prozess erfordert eine diskrete Überprüfungsphase, in der der Benutzer herumläuft und den Playspace definiert. Abfragefunktionen funktionieren erst, nachdem die Überprüfung abgeschlossen wurde.
  • User driven playspace "painting": Während der Scanphase bewegt sich der Benutzer im Spielraum und sieht sich im Spielraum um und malt so effektiv die Bereiche, die einbezogen werden sollen. Das generierte Gitter ist wichtig, um während dieser Phase Feedback vom Benutzer zu geben.
  • Einrichtung von Innen- oder Büroobjekten: Die Abfragefunktionen sind um flache Oberflächen und Wände im rechten Winkel ausgelegt. Dies ist eine weiche Einschränkung. Während der Überprüfungsphase wird jedoch eine Analyse der primären Achse abgeschlossen, um die Netz-Tesselation entlang der Haupt- und Nebenachse zu optimieren.

Prozess der Raumüberprüfung

Wenn Sie das Modul für räumliches Verständnis laden, scannen Sie zunächst Ihren Raum, damit alle nutzbaren Oberflächen – z. B. Boden, Decke und Wände – identifiziert und beschriftet werden. Während des Scanvorgangs schauen Sie sich in Ihrem Raum um und "malen" die Bereiche, die in den Scan einbezogen werden sollen.

Das Gitter, das während dieser Phase zu sehen ist, ist ein wichtiger Teil des visuellen Feedbacks, mit dem Benutzer wissen, welche Teile des Raums gescannt werden. Die DLL für das Spatial Understanding-Modul speichert den Playspace intern als Raster von 8cm großen Voxelwürfeln. Während des ersten Teils der Überprüfung wird eine Primäre Komponentenanalyse abgeschlossen, um die Achsen des Raums zu bestimmen. Intern speichert er den Voxelraum, der an diesen Achsen ausgerichtet ist. Ein Gitter wird etwa jede Sekunde durch Extrahieren der Isosurface aus dem Voxelvolumen erzeugt.

Räumliches Zuordnungsgitter in Weiß und Verstehen des Playspace-Gitters in Grün

Räumliches Zuordnungsgitter in Weiß und Verstehen des Playspace-Gitters in Grün

Die enthaltene Datei SpatialUnderstanding.cs verwaltet den Prozess der Überprüfungsphase. Es ruft die folgenden Funktionen auf:

  • SpatialUnderstanding_Init: Wird einmal am Anfang aufgerufen.
  • GeneratePlayspace_InitScan: Gibt an, dass die Überprüfungsphase beginnen soll.
  • GeneratePlayspace_UpdateScan_DynamicScan: Jeder Frame wurde aufgerufen, um den Scanvorgang zu aktualisieren. Die Kameraposition und -ausrichtung wird übergeben und für den oben beschriebenen Spielraummalprozess verwendet.
  • GeneratePlayspace_RequestFinish: Wird aufgerufen, um den Playspace abzuschließen. Dabei werden die Bereiche verwendet, die während der Überprüfungsphase "gezeichnet" wurden, um den Playspace zu definieren und zu sperren. Die Anwendung kann Statistiken während der Überprüfungsphase abfragen und das benutzerdefinierte Netz abfragen, um Benutzerfeedback zu geben.
  • Import_UnderstandingMesh: Während der Überprüfung fragt das vom Modul bereitgestellte und im Understanding-Prefab platzierte SpatialUnderstandingCustomMesh-Verhalten in regelmäßigen Abständen das vom Prozess generierte benutzerdefinierte Gitter ab. Darüber hinaus erfolgt dies erneut, nachdem die Überprüfung abgeschlossen wurde.

Der Scanflow, der durch das Verhalten SpatialUnderstanding gesteuert wird, ruft InitScan und dann UpdateScan für jeden Frame auf. Wenn die Statistikabfrage eine angemessene Abdeckung meldet, kann der Benutzer requestFinish aufrufen, um das Ende der Überprüfungsphase anzugeben. UpdateScan wird weiterhin aufgerufen, bis der Rückgabewert angibt, dass die DLL die Verarbeitung abgeschlossen hat.

Die Abfragen

Nach Abschluss der Überprüfung können Sie auf drei verschiedene Arten von Abfragen in der Schnittstelle zugreifen:

  • Topologieabfragen: Dies sind schnelle Abfragen, die auf der Topologie des gescannten Raums basieren.
  • Shape-Abfragen: Diese verwenden die Ergebnisse Ihrer Topologieabfragen, um horizontale Oberflächen zu finden, die eine gute Übereinstimmung mit benutzerdefinierten Shapes sind, die Sie definieren.
  • Objektplatzierungsabfragen: Dies sind komplexere Abfragen, die den am besten geeigneten Speicherort basierend auf einer Reihe von Regeln und Einschränkungen für das Objekt finden.

Zusätzlich zu den drei primären Abfragen gibt es eine Raycasting-Schnittstelle, mit der markierte Oberflächentypen abgerufen werden können, und ein benutzerdefiniertes wasserdichtes Raumgitter kann kopiert werden.

Topologieabfragen

Innerhalb der DLL übernimmt der Topologie-Manager die Bezeichnung der Umgebung. Wie bereits erwähnt, werden viele der Daten in Surfeln gespeichert, die in einem Voxel-Volume enthalten sind. Darüber hinaus wird die PlaySpaceInfos-Struktur verwendet, um Informationen über den Playspace zu speichern, einschließlich der Weltausrichtung (weitere Details dazu unten), des Bodens und der Deckenhöhe.

Heuristik wird verwendet, um Boden, Decke und Wände zu bestimmen. Beispielsweise wird die größte und niedrigste horizontale Fläche mit einer Fläche von mehr als 1 m2 als Boden betrachtet. Beachten Sie, dass der Kamerapfad während des Scanvorgangs auch in diesem Prozess verwendet wird.

Eine Teilmenge der Abfragen, die vom Topologie-Manager verfügbar gemacht werden, werden über die DLL verfügbar gemacht. Die verfügbar gemachten Topologieabfragen sind wie folgt:

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

Jede der Abfragen verfügt über einen Satz von Parametern, die für den Abfragetyp spezifisch sind. Im folgenden Beispiel gibt der Benutzer die Mindesthöhe & Breite des gewünschten Volumens, die mindeste Platzierungshöhe über dem Boden und den mindesten Abstand vor dem Volume an. Alle Messungen sind in Metern.

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)

Jede dieser Abfragen verwendet ein vorab zugeordnetes Array von TopologyResult-Strukturen . Der parameter locationCount gibt die Länge des übergebenen Arrays an. Der Rückgabewert gibt die Anzahl der zurückgegebenen Speicherorte an. Diese Zahl ist niemals größer als der übergebene locationCount-Parameter .

TopologyResult enthält die Mittelposition des zurückgegebenen Volumes, die richtungsorientierte Richtung (d. h. normal) und die Dimensionen des gefundenen Raums.

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

Beachten Sie, dass im Unity-Beispiel jede dieser Abfragen mit einer Schaltfläche im Bereich der virtuellen Benutzeroberfläche verknüpft ist. Im Beispiel werden die Parameter für jede dieser Abfragen hart in vernünftige Werte codieren. Weitere Beispiele finden Sie im Beispielcode unter SpaceVisualizer.cs .

Shape-Abfragen

Innerhalb der DLL verwendet das Shape Analyzer (ShapeAnalyzer_W) das Topologieanalysetool, um mit benutzerdefinierten, vom Benutzer definierten Formen abzugleichen. Das Unity-Beispiel enthält einen vordefinierten Satz von Formen, die im Abfragemenü auf der Registerkarte Form angezeigt werden.

Beachten Sie, dass die Formanalyse nur auf horizontalen Flächen funktioniert. Eine Couch wird beispielsweise durch die flache Sitzfläche und die flache Oberseite des Couchrückens definiert. Die Shape-Abfrage sucht nach zwei Oberflächen mit einer bestimmten Größe, Höhe und einem bestimmten Seitenbereich, wobei die beiden Oberflächen ausgerichtet und verbunden sind. Bei Verwendung der APIs-Terminologie sind der Couchsitz und die obere Rückseite der Couch Formkomponenten, und die Ausrichtungsanforderungen sind Einschränkungen für Formkomponenten.

Eine im Unity-Beispiel (ShapeDefinition.cs) definierte Beispielabfrage für "sittable"-Objekte lautet wie folgt:

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

Jede Shape-Abfrage wird durch eine Reihe von Shape-Komponenten definiert, die jeweils einen Satz von Komponenteneinschränkungen und einen Satz von Shape-Einschränkungen enthalten, die Abhängigkeiten zwischen den Komponenten auflisten. Dieses Beispiel enthält drei Einschränkungen in einer einzelnen Komponentendefinition und keine Formeinschränkungen zwischen Komponenten (da es nur eine Komponente gibt).

Im Gegensatz dazu weist die Couchform zwei Formkomponenten und vier Formeinschränkungen auf. Beachten Sie, dass Komponenten durch ihren Index in der Komponentenliste des Benutzers (0 und 1 in diesem Beispiel) identifiziert werden.

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

Wrapperfunktionen werden im Unity-Modul zur einfachen Erstellung benutzerdefinierter Shape-Definitionen bereitgestellt. Die vollständige Liste der Komponenten- und Shape-Einschränkungen finden Sie in SpatialUnderstandingDll.cs in den Strukturen ShapeComponentConstraint und ShapeConstraint .

Das blaue Rechteck hebt die Ergebnisse der Stuhlformabfrage hervor.

Das blaue Rechteck hebt die Ergebnisse der Stuhlformabfrage hervor.

Objektplatzierungslöser

Objektplatzierungsabfragen können verwendet werden, um die idealen Standorte im physischen Raum zu identifizieren, um Ihre Objekte zu platzieren. Der Solver findet den am besten geeigneten Standort unter Berücksichtigung der Objektregeln und -einschränkungen. Darüber hinaus bleiben Objektabfragen bestehen, bis das Objekt mit Solver_RemoveObject - oder Solver_RemoveAllObjects-Aufrufen entfernt wird, sodass eine eingeschränkte Platzierung mehrerer Objekte möglich ist.

Objektplatzierungsabfragen bestehen aus drei Teilen: Platzierungstyp mit Parametern, einer Liste von Regeln und einer Liste von Einschränkungen. Verwenden Sie zum Ausführen einer Abfrage die folgende API:

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)

Diese Funktion übernimmt einen Objektnamen, eine Platzierungsdefinition und eine Liste von Regeln und Einschränkungen. Die C#-Wrapper bieten Konstruktionshilfsfunktionen, um das Erstellen von Regeln und Einschränkungen zu vereinfachen. Die Platzierungsdefinition enthält den Abfragetyp, d. a. eine der folgenden:

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

Jeder Der Platzierungstypen verfügt über einen Satz von Parametern, die für den Typ eindeutig sind. Die ObjectPlacementDefinition-Struktur enthält einen Satz statischer Hilfsfunktionen zum Erstellen dieser Definitionen. Um beispielsweise einen Ort zu finden, an dem ein Objekt auf dem Boden platziert werden kann, können Sie die folgende Funktion verwenden:

public static ObjectPlacementDefinition Create_OnFloor(Vector3 halfDims)

Zusätzlich zum Platzierungstyp können Sie eine Reihe von Regeln und Einschränkungen bereitstellen. Regeln dürfen nicht verletzt werden. Mögliche Platzierungsorte, die den Typ und die Regeln erfüllen, werden dann für den Satz von Einschränkungen optimiert, um den optimalen Platzierungsort auszuwählen. Jede der Regeln und Einschränkungen kann von den bereitgestellten statischen Erstellungsfunktionen erstellt werden. Unten finden Sie eine Beispielfunktion für regel- und einschränkungskonstruktion.

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

Die folgende Objektplatzierungsabfrage sucht nach einem Ort, an dem ein Würfel mit einem halben Meter am Rand einer Oberfläche platziert werden kann, entfernt von anderen zuvor platzierten Objekten und in der Nähe der Mitte des Raums.

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

Bei erfolgreicher Ausführung wird eine ObjectPlacementResult-Struktur zurückgegeben, die die Platzierungsposition, die Dimensionen und die Ausrichtung enthält. Darüber hinaus wird die Platzierung der internen Liste der platzierten Objekte der DLL hinzugefügt. Nachfolgende Platzierungsabfragen berücksichtigen dieses Objekt. Die Datei LevelSolver.cs im Unity-Beispiel enthält weitere Beispielabfragen.

Die blauen Felder zeigen das Ergebnis von drei Place On Floor-Abfragen mit Regeln für die Kameraposition entfernt an.

Die blauen Felder zeigen das Ergebnis von drei Place On Floor-Abfragen mit Regeln für die Kameraposition entfernt an.

Tipps:

  • Beim Lösen der Platzierungsposition mehrerer Objekte, die für ein Level- oder Anwendungsszenario erforderlich sind, lösen Sie zunächst unverzichtbare und große Objekte, um die Wahrscheinlichkeit zu maximieren, dass ein Raum gefunden werden kann.
  • Die Platzierungsreihenfolge ist wichtig. Wenn Objektplatzierungen nicht gefunden werden können, versuchen Sie es mit weniger eingeschränkten Konfigurationen. Eine Reihe von Fallbackkonfigurationen ist für die Unterstützung der Funktionalität in vielen Raumkonfigurationen von entscheidender Bedeutung.

Ray-Casting

Zusätzlich zu den drei primären Abfragen kann eine Ray Casting-Schnittstelle verwendet werden, um markierte Oberflächentypen abzurufen, und ein benutzerdefiniertes wasserdichtes Spielraumgitter kann kopiert werden Nachdem der Raum gescannt und abgeschlossen wurde, werden Intern Bezeichnungen für Oberflächen wie Boden, Decke und Wände generiert. Die PlayspaceRaycast-Funktion nimmt einen Strahl auf und gibt zurück, wenn der Strahl mit einer bekannten Oberfläche kollidiert, und wenn ja, Informationen über diese Oberfläche in Form eines 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;
     };

Intern wird der Raycast mit der berechneten 8cm-Voxeldarstellung des Playspace berechnet. Jeder Voxel enthält einen Satz von Oberflächenelementen mit verarbeiteten Topologiedaten (auch als Surfel bezeichnet). Die in der sich überschneidenden Voxelzelle enthaltenen Surfel werden verglichen und die beste Übereinstimmung zum Suchen der Topologieinformationen verwendet. Diese Topologiedaten enthalten die in Form der SurfaceTypes-Enumeration zurückgegebenen Bezeichnungen sowie die Oberfläche der sich überschneidenden Oberfläche.

Im Unity-Beispiel wandelt der Cursor jeden Frame einen Ray um. Erstens gegen die Collider von Unity; zweitens, gegen die Weltdarstellung des Verständnismoduls; und schließlich für die Benutzeroberflächenelemente. In dieser Anwendung erhält die Benutzeroberfläche Priorität, dann das Verständnisergebnis und schließlich die Collider von Unity. SurfaceType wird als Text neben dem Cursor gemeldet.

Schnittmenge des Raycast-Ergebnisses mit dem Boden.

Schnittmenge des Raycast-Ergebnisses mit dem Boden.

Abrufen des Codes

Der Open-Source-Code ist in MixedRealityToolkit verfügbar. Informieren Sie uns in den HoloLens-Entwicklerforen , wenn Sie den Code in einem Projekt verwenden. Wir können es kaum erwarten, zu sehen, was Sie damit machen!

Informationen zum Autor

Jeff Evertt, Software Engineering Lead bei Microsoft Jeff Evertt ist ein Softwareentwicklungsleiter, der seit den frühen Tagen an HoloLens gearbeitet hat, von der Inkubation bis zur Erfahrungsentwicklung. Vor HoloLens arbeitete er an der Xbox Kinect und in der Spieleindustrie auf einer Vielzahl von Plattformen und Spielen. Jeff ist begeistert von Robotik, Grafiken und Dingen mit auffälligen Leuchten, die piepen. Er liebt es, Neues zu lernen und an Software, Hardware und besonders in dem Raum zu arbeiten, in dem sich die beiden überschneiden.

Siehe auch