Fallstudie – Utöka funktionerna för rumslig mappning i HoloLens

När vi skapade våra första Microsoft HoloLens var vi otåliga att se hur långt vi kunde flytta gränserna för rumslig mappning på enheten. Jeff Evertt, programvaruutvecklare på Microsoft Studios, förklarar hur en ny teknik utvecklades på grund av behovet av mer kontroll över hur hologram placeras i en användares verkliga miljö.

Anteckning

HoloLens 2 implementerar en ny Scene Understanding Runtimesom ger Mixed Reality-utvecklare en strukturerad miljörepresentation på hög nivå som utformats för att göra utveckling för medvetna program intuitiv.

Titta på videon

Utöver rumslig mappning

Medan vi arbetade med Fragment och Young Conker, två av de första matcherna för HoloLens, upptäckte vi att när vi gjorde procedurplacering av hologram i den fysiska världen behövde vi en högre förståelse för användarens miljö. Varje spel hade sina egna specifika placeringsbehov: I fragment ville vi till exempel kunna skilja mellan olika ytor – till exempel golv eller en tabell – för att placera ledtrådar på relevanta platser. Vi ville också kunna identifiera ytor som holographic-tecken i livsstorlek kan finnas på, till exempel en couch eller ett barn. I Young Conker ville vi att Conker och han gärna skulle kunna använda upphöjda ytor i en spelares rum som plattformar.

Asobo Studios,vår utvecklingspartner för dessa spel, stötte på det här problemet och skapade en teknik som utökar funktionerna för rumslig mappning i HoloLens. Med hjälp av detta kan vi analysera en spelares rum och identifiera ytor som väggar, tabeller, golv och golv. Det gav oss också möjlighet att optimera mot en uppsättning begränsningar för att fastställa den bästa placeringen för holografiska objekt.

Koden för spatial förståelse

Vi tog Asobos ursprungliga kod och skapade ett bibliotek som kapslar in den här tekniken. Microsoft och Asobo har nu öppen källkod för den här koden och gjort den tillgänglig på MixedRealityToolkit så att du kan använda den i dina egna projekt. All källkod ingår, så att du kan anpassa den efter dina behov och dela dina förbättringar med communityn. Koden för C++-lösaren har omslutts i en UWP-DLL och exponerats för Unity med en drop-in-prefab i MixedRealityToolkit.

Det finns många användbara frågor i Unity-exemplet som gör att du kan hitta tomma utrymmen på väggar, placera objekt i taket eller på stora utrymmen på golv, identifiera platser där tecken ska finnas och en mängd andra rumsliga frågor.

Även om lösningen för rumslig mappning som tillhandahålls av HoloLens är utformad för att vara tillräckligt allmän för att uppfylla behoven i hela skalan av problemområden, har modulen för spatial förståelse skapats för att stödja behoven för två specifika spel. Lösningen är därför strukturerad kring en specifik process och en uppsättning antaganden:

  • Spelområde med fast storlek: Användaren anger den maximala spelrumsstorleken i init-anropet.
  • Genomsökningsprocess en gång: Processen kräver en diskret genomsökningsfas där användaren går runt och definierar spelområdet. Frågefunktionerna fungerar inte förrän genomsökningen har genomförts.
  • Användardrivet spelområde "painting": Under genomsökningsfasen flyttar och tittar användaren runt spelområdet och målar effektivt de områden som ska ingå. Det genererade nätet är viktigt för att ge användarfeedback under den här fasen.
  • Hus- eller kontorsinstallationer inomhus: Frågefunktionerna är utformade runt plana ytor och väggar i högra vinklar. Det här är en mjuk begränsning. Under genomsökningsfasen slutförs dock en primär axelanalys för att optimera nätmaskeringen längs större och mindre axel.

Genomsökningsprocess för rum

När du läser in modulen för spatial förståelse är det första du ska göra att genomsöka utrymmet, så att alla användbara ytor, till exempel golv, tak och väggar, identifieras och märks. Under genomsökningen tittar du runt i rummet och "färgar" de områden som ska ingå i genomsökningen.

Näten som visas under den här fasen är en viktig visuell feedback som talar om för användarna vilka delar av rummet som genomsöks. DLL-filen för modulen för spatial förståelse lagrar spelområdet internt som ett rutnät med 8 cm stora voxel-kuber. Under den första delen av genomsökningen slutförs en primär komponentanalys för att fastställa rummets axlar. Internt lagrar den voxel-utrymmet justerat mot dessa axlar. Ett nät genereras ungefär varje sekund genom att extrahera isosurfningen från voxel-volymen.

Spatialt kartnät i vitt och för att förstå spelområdesnät i grönt

Spatialt kartnät i vitt och för att förstå spelområdesnät i grönt

Den inkluderade filen SpatialUnderstanding.cs hanterar genomsökningsfasen. Den anropar följande funktioner:

  • SpatialUnderstanding_Init: Anropas en gång i början.
  • GeneratePlayspace_InitScan: Anger att genomsökningsfasen ska börja.
  • GeneratePlayspace_UpdateScan_DynamicScan: Anropade varje bildruta för att uppdatera genomsökningen. Kamerans position och orientering skickas och används för målningsprocessen i spelområdet, som beskrivs ovan.
  • GeneratePlayspace_RequestFinish: Anropas för att slutföra spelområdet. Då används områdena "avskärmade" under genomsökningsfasen för att definiera och låsa spelområdet. Programmet kan köra frågor mot statistik under genomsökningsfasen och köra frågor mot det anpassade nätet för att ge användarfeedback.
  • Import_UnderstandingMesh: Under genomsökningen frågar SpatialUnderstandingCustomMesh-beteendet som tillhandahålls av modulen och placeras på den förstånde prefab regelbundet det anpassade nät som genereras av processen. Dessutom görs detta igen när genomsökningen har genomförts.

Genomsökningsflödet, som drivs av beteendet SpatialUnderstanding, anropar InitScan och sedan UpdateScan varje bildruta. När statistikfrågan rapporterar rimlig täckning kan användaren airtap anropa RequestFinish för att indikera slutet av genomsökningsfasen. UpdateScan fortsätter att anropas tills dess returvärde anger att DLL-filen har slutfört bearbetningen.

Frågorna

När genomsökningen är klar kan du komma åt tre olika typer av frågor i gränssnittet:

  • Topologifrågor: Det här är snabba frågor som baseras på topologin i det skannade rummet.
  • Formfrågor: Dessa använder resultatet av dina topologifrågor för att hitta vågräta ytor som är en bra matchning till anpassade former som du definierar.
  • Objektplaceringsfrågor: Det här är mer komplexa frågor som hittar den plats som passar bäst baserat på en uppsättning regler och begränsningar för objektet.

Förutom de tre primära frågorna finns det ett raycasting-gränssnitt som kan användas för att hämta taggade yttyper och ett anpassat watertight-rumsnät kan kopieras ut.

Topologifrågor

I DLL-filen hanterar topologihanteraren etikettering av miljön. Som nämnts ovan lagras mycket av data i surfels, som finns i en voxel-volym. Dessutom används PlaySpaceInfos-strukturen för att lagra information om spelområdet, inklusive världsjustering (mer information om detta nedan), golv och takhöjd.

Heuristik används för att fastställa golv, tak och väggar. Till exempel betraktas den största och lägsta vågräta ytan med större än 1 m2-yta som golv. Observera att kamerasökvägen under genomsökningen också används i den här processen.

En delmängd av de frågor som exponeras av topologihanteraren exponeras via DLL-filen. De exponerade topologifrågorna är följande:

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

Var och en av frågorna har en uppsättning parametrar som är specifika för frågetypen. I följande exempel anger användaren den minsta höjden & bredden på önskad volym, den minsta placeringshöjden ovanför golv och minsta längd framför volymen. Alla mått är i meter.

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)

Var och en av dessa frågor tar en för allokerad matris med TopologyResult-strukturer. Parametern locationCount anger längden på den inmatade matrisen. Returvärdet rapporterar antalet returnerade platser. Det här talet är aldrig större än parametern passed-in locationCount.

TopologyResult innehåller den returnerade volymens mittposition, riktningen (dvs. normal) och dimensionerna för det påträffade utrymmet.

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

Observera att i Unity-exemplet länkas var och en av dessa frågor till en knapp i panelen för det virtuella användargränssnittet. Exemplet hårdkodar parametrarna för var och en av dessa frågor till rimliga värden. Fler exempel finns i SpaceVisualizer.cs i exempelkoden.

Forma frågor

Inuti DLL-filen använder formanalysatorn (ShapeAnalyzer_W) topologianalysen för att matcha mot anpassade former som definierats av användaren. Unity-exemplet har en fördefinierad uppsättning former som visas i frågemenyn på fliken Form.

Observera att formanalysen endast fungerar på vågräta ytor. En couch definieras till exempel av den platta platsytan och den platta överdelen av soffan. Formfrågan söker efter två ytor av en viss storlek, höjd och bredd– med de två ytorna justerade och anslutna. Med hjälp av API:erna-terminologin är couch seat och överdelen av soffen formkomponenter och anpassningskraven är formkomponentbegränsningar.

En exempelfråga som definierats i Unity-exemplet (ShapeDefinition.cs), för "sittable"-objekt är följande:

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

Varje formfråga definieras av en uppsättning formkomponenter, var och en med en uppsättning komponentbegränsningar och en uppsättning formbegränsningar som visar beroenden mellan komponenterna. Det här exemplet innehåller tre begränsningar i en enda komponentdefinition och inga formbegränsningar mellan komponenter (eftersom det bara finns en komponent).

Däremot har soffformen två formkomponenter och fyra formbegränsningar. Observera att komponenterna identifieras med hjälp av indexet i användarens komponentlista (0 och 1 i det här exemplet).

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

Wrapper-funktioner finns i Unity-modulen för att enkelt skapa anpassade formdefinitioner. En fullständig lista över komponent- och formbegränsningar finns i SpatialUnderstandingDll.cs i strukturerna ShapeComponentConstraint och ShapeConstraint.

Den blå rektangeln visar resultatet av frågan med formen shape.

Den blå rektangeln visar resultatet av frågan med formen shape.

Objektplaceringslösare

Objektplaceringsfrågor kan användas för att identifiera idealiska platser i det fysiska rummet där objekten ska placeras. Lösenen hittar den plats som passar bäst med tanke på objektregler och begränsningar. Dessutom kvarstår objektfrågor tills objektet tas bort med Solver_RemoveObject eller Solver_RemoveAllObjects anrop, vilket tillåter begränsad placering av flera objekt.

Objektplaceringsfrågor består av tre delar: placeringstyp med parametrar, en lista över regler och en lista över begränsningar. Om du vill köra en fråga använder du följande 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)

Den här funktionen tar ett objektnamn, en placeringsdefinition och en lista över regler och begränsningar. C#-omslutningar tillhandahåller konstruktionshjälpfunktioner som gör det enkelt att skapa regler och begränsningar. Placeringsdefinitionen innehåller frågetypen , det vill säga något av följande:

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

Var och en av placeringstyperna har en uppsättning parametrar som är unika för typen. Strukturen ObjectPlacementDefinition innehåller en uppsättning statiska hjälpfunktioner för att skapa dessa definitioner. Om du till exempel vill hitta en plats för att placera ett objekt på golv kan du använda följande funktion:

public static ObjectPlacementDefinition Create_OnFloor(Vector3 halfDims)

Förutom placeringstypen kan du ange en uppsättning regler och begränsningar. Regler kan inte överträdas. Möjliga placeringsplatser som uppfyller typen och reglerna optimeras sedan mot uppsättningen begränsningar för att välja den optimala placeringsplatsen. Var och en av reglerna och begränsningarna kan skapas av de tillhandahållna funktionerna för statiskt skapande. En exempelfunktion för regel- och begränsningskonstruktion finns nedan.

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

Objektplaceringsfrågan nedan letar efter en plats där du kan placera en halv meter-kub på kanten av en yta, bort från andra tidigare placera objekt och nära mitten av rummet.

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

Om det lyckas returneras en ObjectPlacementResult-struktur som innehåller placeringsposition, dimensioner och orientering. Dessutom läggs placeringen till i DLL-filens interna lista över placerade objekt. Efterföljande placeringsfrågor tar hänsyn till det här objektet. Filen LevelSolver.cs i Unity-exemplet innehåller fler exempelfrågor.

De blå rutorna visar resultatet från tre Place On Floor-frågor med regler för "bort från kameraposition".

De blå rutorna visar resultatet från tre Place On Floor-frågor med regler för "bort från kameraposition".

Tips:

  • När du löser placeringsplatsen för flera objekt som krävs för en nivå eller ett programscenario måste du först lösa problem och stora objekt för att maximera sannolikheten för att ett utrymme kan hittas.
  • Placeringsordningen är viktig. Om objektplaceringar inte kan hittas kan du prova mindre begränsade konfigurationer. Det är viktigt att ha en uppsättning återställningskonfigurationer för att stödja funktioner i många rumskonfigurationer.

Ray-apparat

Förutom de tre primära frågorna kan ett ray casting-gränssnitt användas för att hämta taggade yttyper och ett anpassat vattentight playspace-nät kan kopieras ut När rummet har genomsökts och slutförs genereras etiketter internt för ytor som golv, tak och väggar. Funktionen PlayspaceRaycast tar en röta och returnerar om rocken krockar med en känd yta och i så fall information om den ytan i form av en 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;
     };

Internt beräknas raycast mot den beräknade 8cm kubiska voxel-representationen av spelområdet. Varje voxel innehåller en uppsättning ytelement med bearbetade topologidata (även kallade surfels). Surfellerna som finns i den överdegerade voxelcellen jämförs och den bästa matchningen som används för att leta upp topologiinformationen. Dessa topologidata innehåller etiketterna som returneras i form av SurfaceTypes-uppräkning, samt den överdrundade ytans yta.

I Unity-exemplet kastar markören en båge varje bildruta. För det första, mot Unitys kollisioner; för det andra, mot förståelsen av modulens världsrepresentation; och slutligen mot gränssnittselementen. I det här programmet prioriteras användargränssnittet, sedan förståelseresultatet och slutligen Unitys krockare. SurfaceType rapporteras som text bredvid markören.

Raycast-resultatrapporteringsskärningspunkt med golv.

Raycast-resultatrapporteringsskärningspunkt med golv.

Hämta koden

Koden med öppen källkod är tillgänglig i MixedRealityToolkit. Berätta för oss på HoloLens Forum för utvecklare om du använder koden i ett projekt. Vi kan inte vänta på att se vad du gör med det!

Om författaren

Jeff Evertt, Software Engineering Lead at Microsoft Jeff Evertt är en utvecklingsledning som har arbetat med HoloLens tidigare, från inlärning till erfarenhet av utveckling. Innan HoloLens arbetade han med Xbox Kinect och i spelbranschen på en mängd olika plattformar och spel. Jeff brinner för robotteknik, grafik och saker med prålig belysning som går som ett pip. Han tycker om att lära sig nya saker och att arbeta med programvara, maskinvara och särskilt inom den plats där de två ligger mitt i varandra.

Se även