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

När vi skapade våra första appar för Microsoft HoloLens var vi angelägna om att se hur långt vi kunde tänja på gränserna för rumslig mappning på enheten. Jeff Evertt, programvaruingenjör på Microsoft Studios, förklarar hur en ny teknik utvecklades ur behovet av mer kontroll över hur hologram placeras i en användares verkliga miljö.

Anteckning

HoloLens 2 implementerar en ny Scene Understanding Runtime som ger Mixed Reality utvecklare en strukturerad miljörepresentation på hög nivå som är utformad för att göra utveckling för miljömedvetna program intuitivt.

Titta på videon

Bortom rumslig mappning

Medan vi arbetade med Fragments och Young Conker, två av de första spelen för HoloLens, fann vi att när vi gjorde procedurplacering av hologram i den fysiska världen behövde vi en högre nivå av 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 golvet eller ett bord, för att placera ledtrådar på relevanta platser. Vi ville också kunna identifiera ytor som holografiska karaktärer i livsstorlek kunde sitta på, till exempel en soffa eller en stol. I Young Conker ville vi att Conker och hans motståndare 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 spatial mappning i HoloLens. Med detta kan vi analysera spelarens rum och identifiera ytor som väggar, bord, stolar 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 rumslig 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 omslutits till en UWP-DLL och exponerats för Unity med en drop-in prefab som finns 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å golvet, identifiera platser där tecken ska sitta och en mängd andra frågor om rumslig förståelse.

Även om den rumsliga mappningslösningen som tillhandahålls av HoloLens är utformad för att vara tillräckligt generisk för att uppfylla behoven i hela skalan av problemutrymmen, skapades modulen spatial understanding för att stödja behoven i två specifika spel. Lösningen är därför strukturerad kring en specifik process och en uppsättning antaganden:

  • Spelyta med fast storlek: Användaren anger den maximala spelytans storlek i init-anropet.
  • Engångsgenomsökningsprocess: Processen kräver en diskret genomsökningsfas där användaren går runt och definierar spelytan. Frågefunktioner fungerar inte förrän genomsökningen har slutförts.
  • Användardriven spelyta "målning": Under skanningsfasen rör sig användaren och tittar runt på spelytan 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.
  • Installation av inomhushem eller kontor: Frågefunktionerna är utformade runt plana ytor och väggar i rät vinkel. Detta är en mjuk begränsning. Under genomsökningsfasen slutförs dock en analys av primäraxeln för att optimera mesh-tessellationen längs huvudaxeln och mindre axeln.

Rumsgenomsökningsprocess

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

Nätet som visas under den här fasen är en viktig visuell feedback som låter användarna veta vilka delar av rummet som genomsöks. DLL-filen för modulen spatial understanding lagrar spelytan internt som ett rutnät med voxelkuber i 8 cm-storlek. Under den första delen av genomsökningen slutförs en primär komponentanalys för att fastställa rummets axlar. Internt lagrar den sitt voxelutrymme som är justerat till dessa axlar. Ett nät genereras ungefär varje sekund genom att isosurface extraheras från voxelvolymen.

Spatial mapping mesh in white and understanding playspace mesh in green

Nät för rumslig mappning i vitt och förstå spelytenä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 starta.
  • GeneratePlayspace_UpdateScan_DynamicScan: Anropade varje bildruta för att uppdatera genomsökningsprocessen. Kamerapositionen och orienteringen skickas in och används för spelytans målningsprocess, som beskrivs ovan.
  • GeneratePlayspace_RequestFinish: Anropas för att slutföra spelytan. Då används områdena "målade" under genomsökningsfasen för att definiera och låsa spelytan. Programmet kan fråga efter statistik under genomsökningsfasen och fråga det anpassade nätet för att ge feedback från användaren.
  • Import_UnderstandingMesh: Under genomsökningen frågar SpatialUnderstandingCustomMesh-beteendet som tillhandahålls av modulen och placeras på förståelseprefab regelbundet det anpassade nät som genereras av processen. Dessutom görs detta en gång till efter att genomsökningen har slutfö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 skicka en airtap för att anropa RequestFinish för att ange slutet på genomsökningsfasen. UpdateScan fortsätter att anropas tills returvärdet 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 topologifrågorna för att hitta vågräta ytor som är en bra matchning mot 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 vattentätt 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 spelytan, inklusive världsjusteringen (mer information om detta nedan), golv- och takhöjd.

Heuristik används för att bestämma golv, tak och väggar. Till exempel anses den största och lägsta vågräta ytan med större än 1 m2 yta vara golvet. 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öjdbredden & för önskad volym, minsta placeringshöjd ovanför golvet och den minsta mängden utrymme 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örallokerad matris med TopologyResult-strukturer . Parametern locationCount anger längden på den införda matrisen. Returvärdet rapporterar antalet returnerade platser. Det här talet är aldrig större än parametern för det skickade locationCount .

TopologiResult innehåller den returnerade volymens mittposition, den riktade riktningen (dvs. normal) och dimensionerna för det hittade utrymmet.

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

Observera att i Unity-exemplet är var och en av dessa frågor länkad till en knapp i den virtuella användargränssnittspanelen. 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 formanalyseraren (ShapeAnalyzer_W) topologianalysen för att matcha mot anpassade former som definieras av användaren. Unity-exemplet har en fördefinierad uppsättning former som visas på frågemenyn på formfliken.

Observera att formanalysen endast fungerar på vågräta ytor. En soffa definieras till exempel av den platta sittytan och den platta toppen av soffans rygg. Formfrågan söker efter två ytor med en viss storlek, höjd och breddintervall, med de två ytorna justerade och anslutna. Med hjälp av API:ernas terminologi är soffsätet och soffans överkant formkomponenter och justeringskraven är formkomponentbegränsningar.

En exempelfråga som definieras 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 listar beroenden mellan komponenterna. Det här exemplet innehåller tre begränsningar i en enskild komponentdefinition och inga formbegränsningar mellan komponenter (eftersom det bara finns en komponent).

Soffformen har däremot två formkomponenter och fyra formbegränsningar. Observera att komponenter identifieras av deras index 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),
        };

Omslutningsfunktioner finns i Unity-modulen för att enkelt skapa anpassade formdefinitioner. Den fullständiga listan över komponent- och formbegränsningar finns i SpatialUnderstandingDll.cs i ShapeComponentConstraint och ShapeConstraint-strukturerna .

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

Den blå rektangeln visar resultatet av frågan om stolsformen.

Lösning för objektplacering

Objektplaceringsfrågor kan användas för att identifiera idealiska platser i det fysiska rummet för att placera dina objekt. Lösningslösaren hittar den plats som passar bäst med tanke på objektregler och begränsningar. Dessutom bevaras 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#-omslutningarna tillhandahåller bygghjälpfunktioner för att göra regel- och begränsningskonstruktionen enkel. Placeringsdefinitionen innehåller frågetypen , det vill säger 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. ObjectPlacementDefinition-strukturen 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å golvet 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 brytas. Möjliga placeringsplatser som uppfyller typen och reglerna optimeras sedan mot den uppsättning begränsningar som krävs för att välja den optimala placeringsplatsen. Var och en av reglerna och begränsningarna kan skapas av de tillhandahållna statiska skapandefunktionerna. Nedan visas en exempelregel och begränsningskonstruktionsfunktion.

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 för att placera en halvmeterkub på kanten av en yta, bort från andra tidigare platsobjekt 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 placeringspositionen, dimensionerna och orienteringen. Dessutom läggs placeringen till i DLL:ens 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.

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

De blå rutorna visar resultatet från tre place on floor-frågor med regler "borta från kameraposition".

Tips:

  • När du löser placeringsplatsen för flera objekt som krävs för ett nivå- eller programscenario löser du först oumbärliga och stora objekt för att maximera sannolikheten för att ett blanksteg kan hittas.
  • Placeringsordning är viktigt. Om det inte går att hitta objektplaceringar kan du prova mindre begränsade konfigurationer. Att ha en uppsättning återställningskonfigurationer är avgörande för att stödja funktioner i många rumskonfigurationer.

Strålegjutning

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 vattentätt spelområdesnät kan kopieras ut När rummet har skannats och slutförts genereras etiketter internt för ytor som golv, tak och väggar. Funktionen PlayspaceRaycast tar en stråle och returnerar om strålen kolliderar 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 voxel-representationen på 8 cm för spelytan. Varje voxel innehåller en uppsättning ytelement med bearbetade topologidata (kallas även surfels). Surfels som finns i den korsande voxelcellen jämförs och den bästa matchningen som används för att slå upp topologiinformationen. Dessa topologidata innehåller etiketter som returneras i form av SurfaceTypes-uppräkningen , samt ytan på den korsande ytan.

I Unity-exemplet kastar markören en stråle varje bildruta. För det första, mot Unitys kolliderare; för det andra, mot förståelsemodulens världsrepresentation; och slutligen mot gränssnittselementen. I det här programmet får användargränssnittet prioritet, sedan förståelseresultatet och slutligen Unitys kolliderare. SurfaceType rapporteras som text bredvid markören.

Raycast result reporting intersection with the floor.

Raycast resultat rapportering skärningspunkt med golvet.

Hämta koden

Koden med öppen källkod är tillgänglig i MixedRealityToolkit. Meddela oss på HoloLens Developer Forums om du använder koden i ett projekt. Vi längtar efter att få se vad du gör med det!

Om författaren

Jeff Evertt, Software Engineering Lead at Microsoft Jeff Evertt är en programvaruingenjör som har arbetat med HoloLens sedan början, från inkubation till erfarenhet av utveckling. Innan HoloLens arbetade han på Xbox Kinect och i spelbranschen på en mängd olika plattformar och spel. Jeff brinner för robotteknik, grafik och saker med flashiga lampor som hörs pipa. Han tycker om att lära sig nya saker och arbeta med programvara, maskinvara och särskilt i det utrymme där de två korsar varandra.

Se även