Caso prático – Expandir as capacidades de mapeamento espacial do HoloLens

Ao criar as nossas primeiras aplicações para Microsoft HoloLens, estávamos ansiosos para ver até onde poderíamos ultrapassar os limites do mapeamento espacial no dispositivo. Jeff Evertt, engenheiro de software da Microsoft Studios, explica como uma nova tecnologia foi desenvolvida devido à necessidade de mais controlo sobre a forma como os hologramas são colocados no ambiente real de um utilizador.

Nota

HoloLens 2 implementa um novo Scene Understanding Runtime, que fornece aos programadores Mixed Reality uma representação de ambiente estruturada e de alto nível concebida para tornar intuitivo o desenvolvimento de aplicações com conhecimento ambiental.

Ver o vídeo

Além do mapeamento espacial

Enquanto estávamos a trabalhar em Fragments e Young Conker, dois dos primeiros jogos para o HoloLens, descobrimos que quando estávamos a fazer a colocação processual de hologramas no mundo físico, precisávamos de um nível mais elevado de compreensão sobre o ambiente do utilizador. Cada jogo tinha as suas próprias necessidades de colocação específicas: em Fragmentos, por exemplo, queríamos ser capazes de distinguir entre diferentes superfícies ( como o chão ou uma tabela) para colocar pistas em localizações relevantes. Também queríamos ser capazes de identificar superfícies em que os carateres holográficos em tamanho real pudessem sentar-se, como um sofá ou uma cadeira. No Young Conker, queríamos que o Conker e os seus adversários pudessem usar superfícies elevadas no quarto de um jogador como plataformas.

A Asobo Studios, nossa parceira de desenvolvimento para estes jogos, enfrentou este problema de frente e criou uma tecnologia que expande as capacidades de mapeamento espacial do HoloLens. Com isto, poderíamos analisar o quarto de um jogador e identificar superfícies como paredes, mesas, cadeiras e pisos. Também nos deu a capacidade de otimizar um conjunto de restrições para determinar a melhor colocação para objetos holográficos.

O código de compreensão espacial

Pegamos no código original do Asobo e criámos uma biblioteca que encapsula esta tecnologia. A Microsoft e o Asobo já abriram este código e disponibilizaram-no no MixedRealityToolkit para que possa utilizar nos seus próprios projetos. Todo o código fonte está incluído, permitindo-lhe personalizá-lo às suas necessidades e partilhar as suas melhorias com a comunidade. O código do solver C++ foi moldado numa DLL UWP e exposto ao Unity com uma pré-fabricada pendente contida no MixedRealityToolkit.

Existem muitas consultas úteis incluídas no exemplo do Unity que lhe permitirão encontrar espaços vazios nas paredes, colocar objetos no teto ou em espaços grandes no chão, identificar locais para os carateres se sentarem e uma miríade de outras consultas de compreensão espacial.

Embora a solução de mapeamento espacial fornecida pelo HoloLens tenha sido concebida para ser genérica o suficiente para satisfazer as necessidades de toda a gama de espaços problemáticos, o módulo de compreensão espacial foi criado para suportar as necessidades de dois jogos específicos. Como tal, a sua solução está estruturada em torno de um processo específico e de um conjunto de pressupostos:

  • Área de reprodução de tamanho fixo: o utilizador especifica o tamanho máximo da área de jogo na chamada init.
  • Processo de análise único: o processo requer uma fase de análise discreta em que o utilizador anda por aí, definindo a área de reprodução. As funções de consulta só funcionarão depois de a análise ter sido finalizada.
  • "pintura" da área de reprodução orientada pelo utilizador: durante a fase de análise, o utilizador move-se e olha para a área de reprodução, pintando efetivamente as áreas que devem ser incluídas. A malha gerada é importante para fornecer feedback dos utilizadores durante esta fase.
  • Configuração de casa ou escritório interior: as funções de consulta são concebidas em torno de superfícies planas e paredes em ângulos rectos. Esta é uma limitação suave. No entanto, durante a fase de análise, é concluída uma análise do eixo primário para otimizar a tessellação de malha ao longo do eixo principal e secundário.

Processo de Análise de Salas

Quando carrega o módulo de compreensão espacial, a primeira coisa que vai fazer é digitalizar o seu espaço, para que todas as superfícies utilizáveis , como o chão, o tecto e as paredes, sejam identificadas e etiquetadas. Durante o processo de análise, vai procurar no seu quarto e "pintar" as áreas que devem ser incluídas na análise.

A malha vista durante esta fase é uma parte importante do feedback visual que permite aos utilizadores saber que partes da sala estão a ser analisadas. O DLL para o módulo de compreensão espacial armazena internamente a área de jogo como uma grelha de cubos voxel de tamanho de 8cm. Durante a parte inicial da análise, é concluída uma análise do componente principal para determinar os eixos da sala. Internamente, armazena o seu espaço voxel alinhado com estes eixos. Uma malha é gerada aproximadamente a cada segundo ao extrair o isosurface do volume voxel.

Malha de mapeamento espacial em branco e compreensão da malha da área de jogo a verde

Malha de mapeamento espacial em branco e compreensão da malha da área de jogo a verde

O ficheiro spatialUnderstanding.cs incluído gere o processo de fase de análise. Chama as seguintes funções:

  • SpatialUnderstanding_Init: chamada uma vez no início.
  • GeneratePlayspace_InitScan: indica que a fase de análise deve começar.
  • GeneratePlayspace_UpdateScan_DynamicScan: chamou cada frame para atualizar o processo de análise. A posição e a orientação da câmara são transmitidas e utilizadas para o processo de pintura da área de jogos, descrito acima.
  • GeneratePlayspace_RequestFinish: Chamada para finalizar a área de jogo. Esta ação utilizará as áreas "pintadas" durante a fase de análise para definir e bloquear a área de reprodução. A aplicação pode consultar estatísticas durante a fase de análise, bem como consultar a malha personalizada para fornecer feedback dos utilizadores.
  • Import_UnderstandingMesh: Durante a análise, o comportamento SpatialUnderstandingCustomMesh fornecido pelo módulo e colocado no prefáb de compreensão irá consultar periodicamente a malha personalizada gerada pelo processo. Além disso, isto é feito mais uma vez depois de a análise ter sido finalizada.

O fluxo de análise, impulsionado pelo comportamento SpatialUnderstanding chama InitScan e, em seguida, UpdateScan cada frame. Quando a consulta de estatísticas comunica uma cobertura razoável, o utilizador pode executar um airtap para chamar RequestFinish para indicar o fim da fase de análise. UpdateScan continua a ser chamado até que o valor devolvido indique que a DLL concluiu o processamento.

As consultas

Assim que a análise estiver concluída, poderá aceder a três tipos diferentes de consultas na interface:

  • Consultas de topologia: estas são consultas rápidas baseadas na topologia da sala analisada.
  • Consultas de formas: estas utilizam os resultados das consultas de topologia para encontrar superfícies horizontais que correspondem às formas personalizadas que define.
  • Consultas de colocação de objetos: estas são consultas mais complexas que encontram a localização mais adequada com base num conjunto de regras e restrições para o objeto.

Além das três consultas primárias, existe uma interface de raycasting que pode ser utilizada para obter tipos de superfície etiquetados e uma malha de sala estanque personalizada pode ser copiada.

Consultas de topologia

Na DLL, o gestor de topologia processa a etiquetagem do ambiente. Conforme mencionado acima, grande parte dos dados são armazenados em surfels, que estão contidos num volume voxel. Além disso, a estrutura PlaySpaceInfos é utilizada para armazenar informações sobre a área de jogos, incluindo o alinhamento do mundo (mais detalhes sobre isto abaixo), o piso e a altura do teto.

A heurística é utilizada para determinar o chão, o tecto e as paredes. Por exemplo, a superfície horizontal maior e mais baixa com uma área de superfície superior a 1 m2 é considerada o piso. Tenha em atenção que o caminho da câmara durante o processo de análise também é utilizado neste processo.

Um subconjunto das consultas expostas pelo gestor de Topologia é exposto através da DLL. As consultas de topologia expostas são as seguintes:

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

Cada uma das consultas tem um conjunto de parâmetros, específico do tipo de consulta. No exemplo seguinte, o utilizador especifica a altura mínima & largura do volume pretendido, a altura mínima de colocação acima do piso e a quantidade mínima de desalfandegamento à frente do volume. Todas as medições estão em metros.

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)

Cada uma destas consultas utiliza uma matriz pré-alocada de estruturas TopologyResult . O parâmetro locationCount especifica o comprimento da matriz transmitida. O valor devolvido comunica o número de localizações devolvidas. Este número nunca é maior do que o parâmetro locationCount transmitido.

A TopologyResult contém a posição central do volume devolvido, a direção voltada (ou seja, normal) e as dimensões do espaço encontrado.

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

Tenha em atenção que, no exemplo do Unity, cada uma destas consultas está ligada a um botão no painel de IU virtual. O exemplo codifica os parâmetros de cada uma destas consultas para valores razoáveis. Veja SpaceVisualizer.cs no código de exemplo para obter mais exemplos.

Consultas de formas

Dentro da DLL, o analisador de formas (ShapeAnalyzer_W) utiliza o analisador de topologia para corresponder às formas personalizadas definidas pelo utilizador. O exemplo do Unity tem um conjunto predefinido de formas que são apresentadas no menu de consulta, no separador forma.

Tenha em atenção que a análise da forma funciona apenas em superfícies horizontais. Um sofá, por exemplo, é definido pela superfície plana do assento e pela parte superior plana do sofá para trás. A consulta da forma procura duas superfícies de tamanho, altura e intervalo de aspetos específicos, com as duas superfícies alinhadas e ligadas. Com a terminologia das APIs, o assento do sofá e a parte superior da parte de trás do sofá são componentes da forma e os requisitos de alinhamento são restrições de componentes da forma.

Uma consulta de exemplo definida no exemplo do Unity (ShapeDefinition.cs), para objetos "sittable" é a seguinte:

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

Cada consulta de forma é definida por um conjunto de componentes de forma, cada um com um conjunto de restrições de componentes e um conjunto de restrições de formas que lista dependências entre os componentes. Este exemplo inclui três restrições numa única definição de componente e nenhuma restrição de forma entre componentes (uma vez que existe apenas um componente).

Em contraste, a forma do sofá tem dois componentes de forma e quatro restrições de forma. Tenha em atenção que os componentes são identificados pelo respetivo índice na lista de componentes do utilizador (0 e 1 neste exemplo).

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

As funções wrapper são fornecidas no módulo do Unity para facilitar a criação de definições de formas personalizadas. A lista completa de restrições de componentes e formas pode ser encontrada em SpatialUnderstandingDll.cs nas estruturas ShapeComponentConstraint e ShapeConstraint .

O retângulo azul realça os resultados da consulta da forma da cadeira.

O retângulo azul realça os resultados da consulta da forma da cadeira.

Solucionador de colocação de objetos

As consultas de colocação de objetos podem ser utilizadas para identificar as localizações ideais na sala física para colocar os objetos. O solver encontrará a localização mais adequada, dadas as regras e restrições do objeto. Além disso, as consultas de objetos persistem até que o objeto seja removido com Solver_RemoveObject ou Solver_RemoveAllObjects chamadas, permitindo a colocação restrita de vários objetos.

As consultas de colocação de objetos consistem em três partes: tipo de posicionamento com parâmetros, uma lista de regras e uma lista de restrições. Para executar uma consulta, utilize a seguinte 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)

Esta função utiliza um nome de objeto, uma definição de colocação e uma lista de regras e restrições. Os wrappers C# fornecem funções auxiliares de construção para facilitar a construção de regras e restrições. A definição de colocação contém o tipo de consulta , ou seja, um dos seguintes:

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

Cada um dos tipos de colocação tem um conjunto de parâmetros exclusivos do tipo. A estrutura ObjectPlacementDefinition contém um conjunto de funções auxiliares estáticas para criar estas definições. Por exemplo, para encontrar um local para colocar um objeto no chão, pode utilizar a seguinte função:

public static ObjectPlacementDefinition Create_OnFloor(Vector3 halfDims)

Além do tipo de colocação, pode fornecer um conjunto de regras e restrições. As regras não podem ser violadas. As possíveis localizações de colocação que satisfaçam o tipo e as regras são depois otimizadas em relação ao conjunto de restrições para selecionar a localização de colocação ideal. Cada uma das regras e restrições pode ser criada pelas funções de criação estática fornecidas. Segue-se uma regra de exemplo e uma função de construção de restrição.

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

A consulta de colocação de objetos abaixo está à procura de um local para colocar um cubo de meio metro na extremidade de uma superfície, longe de outros objetos anteriormente colocados e perto do centro da sala.

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

Se for bem-sucedido, é devolvida uma estrutura ObjectPlacementResult que contém a posição de posicionamento, as dimensões e a orientação. Além disso, o posicionamento é adicionado à lista interna de objetos colocados da DLL. As consultas de colocação subsequentes levarão este objeto em consideração. O ficheiro LevelSolver.cs no exemplo do Unity contém mais consultas de exemplo.

As caixas azuis mostram o resultado de três consultas Place On Floor com regras

As caixas azuis mostram o resultado de três consultas Place On Floor com regras "longe da posição da câmara".

Sugestões:

  • Ao resolver a localização de colocação de múltiplos objetos necessários para um cenário de nível ou aplicação, resolva primeiro objetos grandes e imprescindíveis para maximizar a probabilidade de um espaço ser encontrado.
  • A ordem de colocação é importante. Se não for possível localizar colocações de objetos, experimente configurações menos restritas. Ter um conjunto de configurações de contingência é fundamental para suportar funcionalidades em muitas configurações de sala.

Casting de raios

Além das três consultas primárias, pode ser utilizada uma interface de conversão de raios para obter tipos de superfície etiquetados e uma malha de área de jogo estanque personalizada pode ser copiada Depois de a sala ter sido analisada e finalizada, as etiquetas são geradas internamente para superfícies como o chão, o teto e as paredes. A função PlayspaceRaycast faz um raio e regressa se o raio colidir com uma superfície conhecida e, em caso afirmativo, informações sobre essa superfície sob a forma de um 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;
     };

Internamente, o raycast é calculado com base na representação computada de voxel em cubos de 8cm da área de jogo. Cada voxel contém um conjunto de elementos de superfície com dados de topologia processados (também conhecidos como surfels). Os surfels contidos na célula voxel interseccionada são comparados e a melhor correspondência utilizada para procurar as informações da topologia. Estes dados de topologia contêm a etiquetagem devolvida sob a forma da enumeração SurfaceTypes , bem como a área de superfície da superfície interseccionada.

No exemplo unity, o cursor lança um raio em cada moldura. Primeiro, contra os colisores do Unity; segundo, contra a representação mundial do módulo understanding; e, por fim, contra os elementos da IU. Nesta aplicação, a IU tem prioridade, depois o resultado de compreensão e, por fim, os colisores do Unity. O SurfaceType é reportado como texto junto ao cursor.

Raycast result reportando interseção com o chão.

Raycast result reportando interseção com o chão.

Obter o código

O código open source está disponível no MixedRealityToolkit. Informe-nos nos Fóruns de Programadores do HoloLens se utilizar o código num projeto. Mal podemos esperar para ver o que fazes com ele!

Acerca do autor

Jeff Evertt, Líder de Engenharia de Software na Microsoft Jeff Evertt é um líder de engenharia de software que trabalha no HoloLens desde os primeiros tempos, desde a incubação até à experiência de desenvolvimento. Antes do HoloLens, trabalhou no Xbox Kinect e na indústria de jogos numa grande variedade de plataformas e jogos. Jeff é apaixonado por robótica, gráficos e coisas com luzes vistosas que vão bip. Gosta de aprender coisas novas e de trabalhar em software, hardware e, em particular, no espaço onde os dois se cruzam.

Ver também