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 konnten. Jeff Evertt, ein Softwareingenieur bei Microsoft Studios, erklärt, wie eine neue Technologie aus der Notwendigkeit entwickelt wurde, mehr Kontrolle darüber zu haben, wie Hologramme in der realen Umgebung eines Benutzers platziert werden.

Hinweis

HoloLens 2 implementiert eine neue Szenenverständnis-Runtime, die Mixed Reality entwicklern eine strukturierte, hohe Umgebungsdarstellung bietet, die die Entwicklung für umweltbewusste Anwendungen intuitiv macht.

Video ansehen

Über räumliche Zuordnung hinaus

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

Asobo Studios, unser Entwicklungspartner für diese Spiele, stellte sich diesem Problem vor und schuf eine Technologie, die die räumlichen Zuordnungsfunktionen von HoloLens erweitert. Mithilfe dieser Vorgehensweise können wir den Raum eines Spielers analysieren und Oberflächen wie Wände, Tische, Stühle und Böden identifizieren. Es hat uns auch die Möglichkeit gegeben, gegen eine Reihe von Einschränkungen zu optimieren, um die beste Platzierung für holografische Objekte zu bestimmen.

Der räumliche Verständniscode

Wir haben asobos ursprünglichen Code genommen und eine Bibliothek erstellt, die diese Technologie kapselt. Microsoft und Asobo haben diesen Code jetzt open-sourced gemacht und auf MixedRealityToolkit zur Verfügung gestellt, damit Sie sie 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 die C++-Solver wurde in eine UWP-DLL umschlossen und unity mit einem Drop-In-Prefab in MixedRealityToolkit verfügbar gemacht.

Im Unity-Beispiel sind viele nützliche Abfragen enthalten, mit denen Sie leere Leerzeichen auf Wänden finden, Objekte auf der Decke oder auf großen Räumen auf dem Boden platzieren, Orte identifizieren, an denen Zeichen sitzen können, und eine Vielzahl anderer Raumverständnisabfragen.

Während die von HoloLens bereitgestellte Raumzuordnungslösung generisch genug ist, um den Anforderungen der gesamten Bandbreite von Problemräumen gerecht zu werden, wurde das Raumverständnismodul erstellt, um die Anforderungen von zwei spezifischen Spielen zu unterstützen. Ihre Lösung ist beispielsweise um einen bestimmten Prozess und eine Reihe von Annahmen strukturiert:

  • Playspace mit fester Größe: Der Benutzer gibt die maximale Wiedergaberaumgröße im Init-Aufruf an.
  • Einmaliger Scanvorgang: Der Prozess erfordert eine separate Scanphase, in der der Benutzer durchläuft und den Playspace definiert. Abfragefunktionen funktionieren erst, nachdem die Überprüfung abgeschlossen wurde.
  • Benutzergesteuertes Playspace "Malen": Während der Scanphase bewegt sich der Benutzer und sieht sich um den Playspace herum, und bemalt effektiv die Bereiche, die eingeschlossen werden sollen. Das generierte Gitter ist wichtig, während dieser Phase Benutzerfeedback bereitzustellen.
  • Innen- oder Büroeinrichtung: Die Abfragefunktionen sind um flache Oberflächen und Wände in rechten Winkeln ausgelegt. Dies ist eine weiche Einschränkung. Während der Überprüfungsphase wird jedoch eine Primärachsenanalyse abgeschlossen, um die Gittertesselation entlang der Haupt- und Nebenachse zu optimieren.

Raumscanprozess

Wenn Sie das Raumverständnismodul laden, wird der erste Schritt, den Sie tun, ihren Raum scannen, sodass alle verwendbaren Oberflächen wie Boden, Decke und Wände identifiziert und gekennzeichnet werden. Während des Scanvorgangs betrachten Sie Ihren Raum und "zeichnen" die Bereiche, die im Scan enthalten sein sollten.

Das Gitter, das während dieser Phase gesehen wird, ist ein wichtiger Teil des visuellen Feedbacks, mit dem Benutzer wissen können, welche Teile des Raums gescannt werden. Die DLL für das Raumverständnismodul speichert intern den Playspace als Raster von 8cm-Voxelwürfeln. Während des ersten Teils des Scannens wird eine Primäre Komponentenanalyse abgeschlossen, um die Achsen des Raums zu bestimmen. Intern speichert er seinen Voxelraum, der an diese Achsen ausgerichtet ist. Ein Gitter wird ungefähr jede Sekunde generiert, indem das Isosurface aus dem Voxelvolumen extrahiert wird.

Spatial mapping mesh in white and understanding playspace mesh in green

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

Die enthaltene Datei "SpatialUnderstanding.cs" verwaltet den Überprüfungsphasesprozess. Es ruft die folgenden Funktionen auf:

  • SpatialUnderstanding_Init: Einmal am Anfang aufgerufen.
  • GeneratePlayspace_InitScan: Gibt an, dass die Scanphase beginnen soll.
  • GeneratePlayspace_UpdateScan_DynamicScan: Ruft jeden Frame auf, um den Scanvorgang zu aktualisieren. Die Kameraposition und -ausrichtung wird übergeben und wird für den Spielraum-Gemäldeprozess verwendet, der oben beschrieben wird.
  • GeneratePlayspace_RequestFinish: Wird aufgerufen, um den Playspace abzuschließen. Dadurch werden die Bereiche "bemalt" während der Scanphase verwendet, um den Playspace zu definieren und zu sperren. Die Anwendung kann Statistiken während der Überprüfungsphase abfragen sowie das benutzerdefinierte Gitter abfragen, um Benutzerfeedback bereitzustellen.
  • Import_UnderstandingMesh: Während des Scannens wird das vom Modul bereitgestellte SpatialUnderstandingCustomMesh-Verhalten und das grundlegende Prefab regelmäßig das vom Prozess generierte benutzerdefinierte Gitter abfragen. Darüber hinaus erfolgt dies erneut, nachdem die Überprüfung abgeschlossen wurde.

Der Scanfluss, der durch das SpatialUnderstanding-Verhalten gesteuert wird, ruft InitScan auf und aktualisiert dann jeden Frame. 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

Sobald der Scan abgeschlossen ist, 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 einer guten Übereinstimmung mit benutzerdefinierten Shapes entsprechen, 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, die verwendet werden kann, um markierte Oberflächentypen abzurufen, und ein benutzerdefiniertes Wasserraumgitter kann kopiert werden.

Topologieabfragen

Innerhalb der DLL behandelt der Topologie-Manager die Bezeichnung der Umgebung. Wie oben erwähnt, werden viele der Daten in Surfels gespeichert, die in einem Voxelvolumen 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), Boden- und Deckenhöhe.

Heuristiken werden zum Bestimmen von Boden, Decke und Wänden verwendet. Beispielsweise gilt die größte und niedrigste horizontale Oberfläche mit größer als 1 m2 Fläche als Boden. 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 Abfrage verfügt über eine Reihe von Parametern, die für den Abfragetyp spezifisch sind. Im folgenden Beispiel gibt der Benutzer die mindesthöhenbreite & des gewünschten Volumens, die mindestplatzierungshöhe über dem Boden und den mindesten Abstand vor dem Volumen an. Alle Messungen befinden sich 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 LocationCount-Parameter gibt die Länge des übergebenen Arrays an. Der Rückgabewert meldet die Anzahl der zurückgegebenen Speicherorte. Diese Zahl ist nie größer als der übergebene LocationCount-Parameter .

Das TopologyResult enthält die mittlere Position des zurückgegebenen Volumes, die gerichtete Richtung (z. B. normal) und die Abmessungen 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 auf angemessene Werte festgelegt. Weitere Beispiele finden Sie unter SpaceVisualizer.cs im Beispielcode.

Shape-Abfragen

Innerhalb der DLL verwendet die Shape-Analyse (ShapeAnalyzer_W) die Topologieanalyse, um mit benutzerdefinierten Shapes abzugleichen, die vom Benutzer definiert werden. Das Unity-Beispiel verfügt über einen vordefinierten Satz von Shapes, die im Abfragemenü auf der Registerkarte "Form" angezeigt werden.

Beachten Sie, dass die Shape-Analyse nur auf horizontalen Oberflächen funktioniert. Eine Couch, z. B. wird durch die flache Sitzfläche und die flache Oberseite der Couch zurück definiert. Die Shape-Abfrage sucht nach zwei Oberflächen einer bestimmten Größe, Höhe und Seitenbereich, wobei die beiden Oberflächen ausgerichtet und verbunden sind. Bei Verwendung der APIs-Terminologie sind der Couchsitz und der Obere der Rückseite der Couch Formkomponenten und die Ausrichtungsanforderungen sind Formkomponenten.

Eine beispielabfrage, die im Unity-Beispiel (ShapeDefinition.cs) für "sittable"-Objekte definiert ist, 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, wobei jeweils eine Gruppe von Komponenteneinschränkungen und eine Reihe von Shape-Einschränkungen aufgeführt werden, 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 nur eine Komponente vorhanden ist).

Im Gegensatz dazu verfügt die Couchform über zwei Formenkomponenten und vier Formeneinschränkungen. 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 bereitgestellt, um benutzerdefinierte Shapedefinitionen einfach zu erstellen. Die vollständige Liste der Komponenten- und Shape-Einschränkungen finden Sie in SpatialUnderstandingDll.cs innerhalb der ShapeComponentConstraint - und ShapeConstraint-Strukturen .

The blue rectangle highlights the results of the chair shape query.

Das blaue Rechteck hebt die Ergebnisse der Stuhlformabfrage hervor.

Objektplatzierungslöser

Objektplatzierungsabfragen können verwendet werden, um ideale Standorte im physischen Raum zu identifizieren, um Ihre Objekte zu platzieren. Der Solver findet die optimale Position im Rahmen der Objektregeln und Einschränkungen. Darüber hinaus bleiben Objektabfragen beibehalten, bis das Objekt mit Solver_RemoveObject oder Solver_RemoveAllObjects Aufrufen entfernt wird, wodurch eingeschränkte Multiobjektplatzierung ermöglicht wird.

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 verwendet einen Objektnamen, eine Platzierungsdefinition und eine Liste der Regeln und Einschränkungen. Die C#-Wrapper bieten Bauhilfefunktionen, um Regel- und Einschränkungsbau einfach zu gestalten. Die Platzierungsdefinition enthält den Abfragetyp – also eine der folgenden:

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

Jede der Platzierungstypen verfügt über einen Satz von Parametern, die für den Typ eindeutig sind. Die ObjectPlacementDefinition-Struktur enthält eine Reihe statischer Hilfsfunktionen zum Erstellen dieser Definitionen. Um beispielsweise einen Ort zu finden, an dem ein Objekt auf dem Boden platziert werden soll, 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 können nicht verletzt werden. Mögliche Platzierungsorte, die den Typ und die Regeln erfüllen, werden dann für die Gruppe von Einschränkungen optimiert, um die optimale Platzierungsposition auszuwählen. Jede der Regeln und Einschränkungen kann durch die bereitgestellten statischen Erstellungsfunktionen erstellt werden. Eine Beispielregel- und Einschränkungsbaufunktion wird unten bereitgestellt.

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 nachstehende Objektplatzierungsabfrage sucht nach einem Platz zum Platzieren eines halb meterigen Würfels am Rand einer Oberfläche, 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 , die die Platzierungsposition, Dimensionen und Ausrichtung enthält, zurückgegeben. 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.

The blue boxes show the result from three Place On Floor queries with

Die blauen Felder zeigen das Ergebnis aus drei "Place On Floor"-Abfragen mit "abseits der Kameraposition"-Regeln 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 weniger eingeschränkte Konfigurationen. Bei einer Reihe von Fallbackkonfigurationen ist es wichtig, Funktionen in vielen Raumkonfigurationen zu unterstützen.

Ray-Guss

Zusätzlich zu den drei primären Abfragen kann eine Ray-Casting-Schnittstelle verwendet werden, um markierte Oberflächentypen abzurufen und ein benutzerdefiniertes Spielraumgitter zu kopieren, nachdem der Raum gescannt und abgeschlossen wurde, werden Etiketten intern für Oberflächen wie Boden, Decke und Wände generiert. Die PlayspaceRaycast-Funktion nimmt einen Ray und gibt zurück, wenn der Strahl mit einer bekannten Oberfläche kollidiert und falls ja, Informationen zu dieser 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-Voxel-Darstellung des Playspace berechnet. Jede Voxel enthält eine Reihe von Oberflächenelementen mit verarbeiteten Topologiedaten (auch als Surfel bezeichnet). Die surfel, die in der intersecierten Voxelzelle enthalten sind, werden verglichen und die besten Übereinstimmungen verwendet, um die Topologieinformationen nachzuschlagen. Diese Topologiedaten enthalten die Bezeichnung, die in Form der SurfaceTypes-Aufzählung zurückgegeben wird, sowie den Oberflächenbereich der durchschneidenden Oberfläche.

Im Unity-Beispiel wird der Cursor jeden Frame mit einem Ray gecastet. Zuerst gegen die Kollidierungen von Unity; zweitens gegen die Weltdarstellung des Moduls; und schließlich gegen die UI-Elemente. In dieser Anwendung erhält die Benutzeroberfläche Priorität, dann das Verständnisergebnis und schließlich die Kollisionen von Unity. Der SurfaceType wird als Text neben dem Cursor gemeldet.

Raycast result reporting intersection with the floor.

Raycast-Ergebnisberichtsabschnitt mit dem Boden.

Abrufen des Codes

Der Open-Source-Code ist in MixedRealityToolkit verfügbar. Informieren Sie uns über die HoloLens Entwicklerforen, wenn Sie den Code in einem Projekt verwenden. Wir können nicht warten, was Sie mit ihnen tun!

Informationen zum Autor

Jeff Evertt, Software Engineering Lead at Microsoft Jeff Evertt ist ein Softwaretechnik-Leiter, der seit den frühen Tagen von der Inkubation bis zur Entwicklung an HoloLens gearbeitet hat. Vor HoloLens arbeitete er an der Xbox Kinect und in der Spieleindustrie auf einer Vielzahl von Plattformen und Spielen. Jeff ist leidenschaftlich für Robotik, Grafiken und Dinge mit blitzenden Lichtern, die beepen. Er hat spaß daran, neue Dinge zu lernen und an Software, Hardware und insbesondere im Raum zu arbeiten, in dem sich die beiden überschneiden.

Siehe auch