HoloLens (1ª gen) Espacial 230: Mapeamento espacial

Importante

Os tutoriais da Mixed Reality Academy foram desenhados com HoloLens (1ª gen), Unidade 2017 e Auscultadores Imersivos de Realidade Mista em mente. Como tal, sentimos que é importante deixar estes tutoriais no lugar para os desenvolvedores que ainda estão à procura de orientação no desenvolvimento para esses dispositivos. Estes tutoriais não serão atualizados com os mais recentes instrumentos ou interações que estão a ser utilizados para HoloLens 2 e podem não ser compatíveis com versões mais recentes da Unidade. Serão mantidos para continuar a trabalhar nos dispositivos suportados. Uma nova série de tutoriais foi publicada para HoloLens 2.

O mapeamento espacial combina o mundo real e o mundo virtual, ensinando hologramas sobre o ambiente. No MR Spatial 230 (Project Planetário) aprenderemos a:

  • Digitalize o ambiente e transfira dados da HoloLens para a sua máquina de desenvolvimento.
  • Explore os shaders e aprenda a usá-los para visualizar o seu espaço.
  • Decompo a malha da sala em planos simples usando o processamento de malha.
  • Vá além das técnicas de colocação que aprendemos no MR Basics 101,e forneça feedback sobre onde um holograma pode ser colocado no ambiente.
  • Explore os efeitos de oclusão, por isso, quando o holograma está por trás de um objeto do mundo real, ainda pode vê-lo com visão de raio-X!

Suporte de dispositivos

Curso HoloLens Auscultadores imersivos
MR Spatial 230: Mapeamento espacial ✔️

Antes de começar

Pré-requisitos

Project ficheiros

  • Descarregue os ficheiros exigidos pelo projeto. Requer Unidade 2017.2 ou mais tarde.
    • Se ainda precisar de suporte da Unidade 5.6, utilize esta versão.
    • Se ainda precisar de suporte da Unidade 5.5, utilize esta versão.
    • Se ainda precisar de suporte unidade 5.4, utilize esta versão.
  • Desaprove os ficheiros para o seu ambiente de trabalho ou outro local de fácil acesso.

Nota

Se quiser ver o código fonte antes de descarregar, está disponível no GitHub.

Notas

  • "Enable Just My Code" em Visual Studio precisa de ser desativado(não verificado)em Ferramentas > Opções Depuração para atingir pontos de rutura no seu código.

Configuração da unidade

  • Iniciar a Unidade.
  • Selecione Novo para criar um novo projeto.
  • Nomeie o projeto Planetário.
  • Verifique se a definição 3D está selecionada.
  • Clique em Create Project (Criar Projeto).
  • Assim que a Unidade for lançada, vá ao Editar Project Definições > Player.
  • No painel do Inspetor, encontre e selecione o ícone verde Windows Store.
  • Expandir outros Definições.
  • Na secção de renderização, verifique a opção Virtual Reality Supported.
  • Verifique se Windows Holographic aparece na lista de SDKsde Realidade Virtual . Caso contrário, selecione o + botão na parte inferior da lista e escolha +.
  • Expandir Definições de Publicação.
  • Na secção Capacidades, consulte as seguintes definições:
    • InternetClientServer
    • PrivateNetworkClientServer
    • Microfone
    • Perceção Espacial
  • Ir para editar Project Definições > Qualidade
  • No painel inspetor, sob o ícone Windows Store, selecione a seta preta para baixo sob a linha 'Predefinição' e altere a definição predefinitiva para Very Low.
  • Vá para pacote personalizado pacote de importação de > ativos.
  • Navegue para a pasta ...\HolographicAcademy-Hologramas-230-SpatialMapping\Starting.
  • Clique em Planetrium.unitypackage.
  • Clique em Abrir.
  • Deve aparecer uma janela do Pacote de Unidade de Importação, clique no botão Import.
  • Aguarde que a Unidade importe todos os ativos que precisamos para concluir este projeto.
  • No painel hierarquia, elimine a Câmara Principal.
  • No painel Project,a pasta HoloToolkit-SpatialMapping-230\Utilities\Utilities\Prefabs, encontre o objeto da Câmara Principal.
  • Arraste e deixe cair o prefácio da Câmara Principal no painel da Hierarquia.
  • No painel hierarquia, elimine o objeto Luz Direcional.
  • No painel Project,Hologramas pasta, localize o objeto Cursor.
  • Arraste & o & pré-fabricado para a Hierarquia.
  • No painel hierarquia, selecione o objeto Cursor.
  • No painel inspetor, clique na camada de drop-down e selecione Editar Camadas....
  • Nome Camada de utilizador 31 como "SpatialMapping".
  • Salve a nova cena: Arquivo Salvar cena como...
  • Clique em Nova Pasta e nomeie as cenas dapasta .
  • Nomeie o ficheiro "Planetário" e guarde-o na pasta Cenas.

Capítulo 1 - Digitalização

Objetivos

  • Conheça o SurfaceObserver e como as suas definições impactam a experiência e o desempenho.
  • Crie uma experiência de digitalização de quartos para recolher as malhas do seu quarto.

Instruções

  • No painel ProjectHoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs, encontre o pré-fabricado SpatialMapping.
  • Arraste & o & para o painel da Hierarquia.

Construir e Implantar (parte 1)

  • In Unitity, selecione File Build Definições.
  • Clique em Adicionar Cenas Abertas para adicionar a cena planetária à construção.
  • Selecione Plataforma de Windows Universal na lista de plataformas e clique na Plataforma Switch.
  • Coloque o SDK no Universal 10 e no UWP Build Type para D3D.
  • Verifique projetos de unidade C#.
  • Clique em Construir.
  • Criar uma nova pasta chamada "App".
  • Clique na pasta App.
  • Prima o botão Desembargar Pasta.
  • Quando a Unidade terminar de construir, aparecerá uma janela do Explorador de Ficheiros.
  • Clique duas vezes na pasta app para abri-la.
  • Clique duas vezes no Planetário.sln para carregar o projeto em Visual Studio.
  • Na Visual Studio, utilize a barra de ferramentas superior para alterar a configuração para soltar.
  • Altere a Plataforma para x86.
  • Clique na seta para baixo à direita de 'Máquina Local' e selecione Máquina Remota.
  • Introduza o endereço IP do seu dispositivo no campo Address e altere o Modo de Autenticação para Universal (Protocolo Não Encriptado).
  • Clique em Debug - Comece sem depurar ou prima Ctrl + F5.
  • Observe o painel de saída em Visual Studio para construir e implementar o estado.
  • Uma vez implementada a sua aplicação, caminhe pela sala. Você verá as superfícies circundantes cobertas por malhas de arame preto e branco.
  • Digitalize o que o rodeia. Certifique-se de olhar para paredes, tetos e pisos.

Construir e Implantar (parte 2)

Agora vamos explorar como o Mapeamento Espacial pode afetar o desempenho.

  • Em Unidade, selecione Profiler de janela.
  • Clique em Adicionar GPU do Profiler.
  • Clique no Perfil Ativo.
  • Insira o endereço IP do seu HoloLens.
  • Clique em Ligar.
  • Observe o número de milissegundos necessários para a GPU renderizar uma moldura.
  • Impedir que a aplicação entre em funcionamento no dispositivo.
  • Volte a Visual Studio e abra o SpatialMappingObserver.cs. Você vai encontrá-lo na pasta HoloToolkit\SpatialMapping do projeto Assembly-CSharp (Universal Windows).
  • Encontre a função Despertar e adicione a seguinte linha de código: TrianglesPerCubicMeter = 1200;
  • Reexame o projeto no seu dispositivo e, em seguida, reconecte o perfil. Observe a alteração do número de milissegundos para fazer uma moldura.
  • Impedir que a aplicação entre em funcionamento no dispositivo.

Guardar e carregar na Unidade

Finalmente, vamos salvar a malha do nosso quarto e carregá-la na Unidade.

  • Volte a Visual Studio e remova a linha TrianglesPerCubicMeter que adicionou na função Despertar durante a secção anterior.
  • Recolocar o projeto no seu dispositivo. Devemos agora estar a correr com 500 triângulos por metro cúbico.
  • Abra um browser e introduza o seu HoloLens IPAddress para navegar no Portal do Dispositivo Windows.
  • Selecione a opção 3D Ver no painel esquerdo.
  • Em Reconstrução do Surface selecione o botão 'Actualizar'.
  • Observe como as áreas que digitalizou no seu HoloLens aparecem na janela do visor.
  • Para guardar a verificação do seu quarto, prima o botão Guardar.
  • Abra a pasta Downloads para encontrar o modelo de quarto guardado SRMesh.obj.
  • Copie SRMesh.obj para a pasta Ativos do seu projeto Unidade.
  • Em Unidade, selecione o objeto SpatialMapping no painel da Hierarquia.
  • Localize o componente Object Surface Observer (Script).
  • Clique no círculo à direita da propriedade Modelo de Quarto.
  • Encontre e selecione o objeto SRMesh e, em seguida, feche a janela.
  • Verifique se a propriedade do Modelo de Quarto no painel do Inspetor está agora definida para SRMesh.
  • Prima o botão Reprodução para entrar no modo de pré-visualização da Unidade.
  • O componente SpatialMapping carregará as malhas do modelo de quarto guardado para que possa usá-las em Unidade.
  • Mude para a vista de cena para ver todo o modelo do seu quarto exibido com o shader da estrutura de arame.
  • Prima novamente o botão Reproduzir para sair do modo de pré-visualização.

NOTA: Da próxima vez que introduzir o modo de pré-visualização em Unidade, carregará a malha da sala guardada por defeito.

Capítulo 2 - Visualização

Objetivos

  • Aprenda o básico dos shaders.
  • Visualize o que o rodeia.

Instruções

  • No painel da Hierarquia da Unidade, selecione o objeto SpatialMapping.
  • No painel do Inspetor, encontre o componente De Mapeamento Espacial (Script).
  • Clique no círculo à direita da propriedade Material de Superfície.
  • Encontre e selecione o material BlueLinesOnWalls e feche a janela.
  • Na pasta Project do painel Shaders, clique duas vezes nas BlueLinesOnWalls para abrir o shader em Visual Studio.
  • Este é um simples shader de pixel (vértice a fragmento), que realiza as seguintes tarefas:
    1. Converte a localização de um vértice para o espaço mundial.
    2. Verifica se o vértice é normal para determinar se um pixel é vertical.
    3. Define a cor do pixel para renderização.

Construir e Implementar

  • Regresso à Unidade e prima Reproduzir para entrar no modo de pré-visualização.
  • As linhas azuis serão renderizadas em todas as superfícies verticais da malha da sala (que automaticamente são carregadas a partir dos nossos dados de digitalização guardados).
  • Mude para o separador Cena para ajustar a sua visão da sala e veja como aparece toda a malha da sala na Unidade.
  • No painel Project, encontre a pasta De Materiais e selecione o material BlueLinesOnWalls.
  • Modifique algumas propriedades e veja como as mudanças aparecem no editor da Unidade.
    • No painel inspetor, ajuste o valor de Escala de Linha para que as linhas pareçam mais grossas ou mais finas.
    • No painel do Inspetor, ajuste o valor LinesPerMeter para alterar quantas linhas aparecem em cada parede.
  • Clique em Reproduzir novamente para sair do modo de pré-visualização.
  • Construa e implemente para o HoloLens e observe como a renderização de sombreadores aparece em superfícies reais.

A unidade faz um grande trabalho de pré-visualização de materiais, mas é sempre uma boa ideia fazer check-out no dispositivo.

Capítulo 3 - Processamento

Objetivos

  • Aprenda técnicas para processar dados de mapeamento espacial para utilização na sua aplicação.
  • Analise os dados de mapeamento espacial para encontrar planos e remover triângulos.
  • Use aviões para a colocação de hologramas.

Instruções

  • No painel Project da Unidade, Hologramas pasta, encontre o objeto SpatialProcessing.
  • Arraste & o objeto & para o painel da Hierarquia.

O pré-teste de Processo Espacial inclui componentes para o processamento dos dados de mapeamento espacial. Os .cs SurfaceMeshesToPlanes encontrarão e gerarão aviões com base nos dados de mapeamento espacial. Usaremos aviões na nossa aplicação para representar paredes, pisos e tetos. Este prefácio também inclui RemoveSurfaceVertices.cs que pode remover vértices da malha de mapeamento espacial. Isto pode ser usado para criar buracos na malha, ou para remover triângulos em excesso que já não são necessários (porque os planos podem ser usados em vez disso).

  • No painel Project da Unidade, Hologramas pasta, encontre o objeto SpaceCollection.
  • Arraste e deixe cair o objeto SpaceCollection no painel da Hierarquia.
  • No painel hierarquia, selecione o objeto SpatialProcessing.
  • No painel inspetor, encontre o componente Play Space Manager (Script).
  • Clique duas vezes no PlaySpaceManager.cs para abri-lo em Visual Studio.

PlaySpaceManager.cs contém código específico para aplicações. Adicionaremos funcionalidade a este script para permitir o seguinte comportamento:

  1. Pare de recolher dados de mapeamento espacial depois de ultrapassarmos o prazo de digitalização (10 segundos).
  2. Processar os dados de mapeamento espacial:
    1. Utilize o SurfaceMeshesToPlanes para criar uma representação mais simples do mundo como aviões (paredes, pisos, tetos, etc.
    2. Utilize RemoveSurfaceVertices para remover triângulos superficiais que se enquadram nos limites do plano.
  3. Gere uma coleção de hologramas no mundo e coloca-os em planos de parede e chão perto do utilizador.

Complete os exercícios de codificação marcados no PlaySpaceManager.cs ou substitua o script pela solução acabada a partir de baixo:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;

/// <summary>
/// The SurfaceManager class allows applications to scan the environment for a specified amount of time 
/// and then process the Spatial Mapping Mesh (find planes, remove vertices) after that time has expired.
/// </summary>
public class PlaySpaceManager : Singleton<PlaySpaceManager>
{
    [Tooltip("When checked, the SurfaceObserver will stop running after a specified amount of time.")]
    public bool limitScanningByTime = true;

    [Tooltip("How much time (in seconds) that the SurfaceObserver will run after being started; used when 'Limit Scanning By Time' is checked.")]
    public float scanTime = 30.0f;

    [Tooltip("Material to use when rendering Spatial Mapping meshes while the observer is running.")]
    public Material defaultMaterial;

    [Tooltip("Optional Material to use when rendering Spatial Mapping meshes after the observer has been stopped.")]
    public Material secondaryMaterial;

    [Tooltip("Minimum number of floor planes required in order to exit scanning/processing mode.")]
    public uint minimumFloors = 1;

    [Tooltip("Minimum number of wall planes required in order to exit scanning/processing mode.")]
    public uint minimumWalls = 1;

    /// <summary>
    /// Indicates if processing of the surface meshes is complete.
    /// </summary>
    private bool meshesProcessed = false;

    /// <summary>
    /// GameObject initialization.
    /// </summary>
    private void Start()
    {
        // Update surfaceObserver and storedMeshes to use the same material during scanning.
        SpatialMappingManager.Instance.SetSurfaceMaterial(defaultMaterial);

        // Register for the MakePlanesComplete event.
        SurfaceMeshesToPlanes.Instance.MakePlanesComplete += SurfaceMeshesToPlanes_MakePlanesComplete;
    }

    /// <summary>
    /// Called once per frame.
    /// </summary>
    private void Update()
    {
        // Check to see if the spatial mapping data has been processed
        // and if we are limiting how much time the user can spend scanning.
        if (!meshesProcessed && limitScanningByTime)
        {
            // If we have not processed the spatial mapping data
            // and scanning time is limited...

            // Check to see if enough scanning time has passed
            // since starting the observer.
            if (limitScanningByTime && ((Time.time - SpatialMappingManager.Instance.StartTime) < scanTime))
            {
                // If we have a limited scanning time, then we should wait until
                // enough time has passed before processing the mesh.
            }
            else
            {
                // The user should be done scanning their environment,
                // so start processing the spatial mapping data...

                /* TODO: 3.a DEVELOPER CODING EXERCISE 3.a */

                // 3.a: Check if IsObserverRunning() is true on the
                // SpatialMappingManager.Instance.
                if(SpatialMappingManager.Instance.IsObserverRunning())
                {
                    // 3.a: If running, Stop the observer by calling
                    // StopObserver() on the SpatialMappingManager.Instance.
                    SpatialMappingManager.Instance.StopObserver();
                }

                // 3.a: Call CreatePlanes() to generate planes.
                CreatePlanes();

                // 3.a: Set meshesProcessed to true.
                meshesProcessed = true;
            }
        }
    }

    /// <summary>
    /// Handler for the SurfaceMeshesToPlanes MakePlanesComplete event.
    /// </summary>
    /// <param name="source">Source of the event.</param>
    /// <param name="args">Args for the event.</param>
    private void SurfaceMeshesToPlanes_MakePlanesComplete(object source, System.EventArgs args)
    {
        /* TODO: 3.a DEVELOPER CODING EXERCISE 3.a */

        // Collection of floor and table planes that we can use to set horizontal items on.
        List<GameObject> horizontal = new List<GameObject>();

        // Collection of wall planes that we can use to set vertical items on.
        List<GameObject> vertical = new List<GameObject>();

        // 3.a: Get all floor and table planes by calling
        // SurfaceMeshesToPlanes.Instance.GetActivePlanes().
        // Assign the result to the 'horizontal' list.
        horizontal = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Table | PlaneTypes.Floor);

        // 3.a: Get all wall planes by calling
        // SurfaceMeshesToPlanes.Instance.GetActivePlanes().
        // Assign the result to the 'vertical' list.
        vertical = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Wall);

        // Check to see if we have enough horizontal planes (minimumFloors)
        // and vertical planes (minimumWalls), to set holograms on in the world.
        if (horizontal.Count >= minimumFloors && vertical.Count >= minimumWalls)
        {
            // We have enough floors and walls to place our holograms on...

            // 3.a: Let's reduce our triangle count by removing triangles
            // from SpatialMapping meshes that intersect with our active planes.
            // Call RemoveVertices().
            // Pass in all activePlanes found by SurfaceMeshesToPlanes.Instance.
            RemoveVertices(SurfaceMeshesToPlanes.Instance.ActivePlanes);

            // 3.a: We can indicate to the user that scanning is over by
            // changing the material applied to the Spatial Mapping meshes.
            // Call SpatialMappingManager.Instance.SetSurfaceMaterial().
            // Pass in the secondaryMaterial.
            SpatialMappingManager.Instance.SetSurfaceMaterial(secondaryMaterial);

            // 3.a: We are all done processing the mesh, so we can now
            // initialize a collection of Placeable holograms in the world
            // and use horizontal/vertical planes to set their starting positions.
            // Call SpaceCollectionManager.Instance.GenerateItemsInWorld().
            // Pass in the lists of horizontal and vertical planes that we found earlier.
            SpaceCollectionManager.Instance.GenerateItemsInWorld(horizontal, vertical);
        }
        else
        {
            // We do not have enough floors/walls to place our holograms on...

            // 3.a: Re-enter scanning mode so the user can find more surfaces by
            // calling StartObserver() on the SpatialMappingManager.Instance.
            SpatialMappingManager.Instance.StartObserver();

            // 3.a: Re-process spatial data after scanning completes by
            // re-setting meshesProcessed to false.
            meshesProcessed = false;
        }
    }

    /// <summary>
    /// Creates planes from the spatial mapping surfaces.
    /// </summary>
    private void CreatePlanes()
    {
        // Generate planes based on the spatial map.
        SurfaceMeshesToPlanes surfaceToPlanes = SurfaceMeshesToPlanes.Instance;
        if (surfaceToPlanes != null && surfaceToPlanes.enabled)
        {
            surfaceToPlanes.MakePlanes();
        }
    }

    /// <summary>
    /// Removes triangles from the spatial mapping surfaces.
    /// </summary>
    /// <param name="boundingObjects"></param>
    private void RemoveVertices(IEnumerable<GameObject> boundingObjects)
    {
        RemoveSurfaceVertices removeVerts = RemoveSurfaceVertices.Instance;
        if (removeVerts != null && removeVerts.enabled)
        {
            removeVerts.RemoveSurfaceVerticesWithinBounds(boundingObjects);
        }
    }

    /// <summary>
    /// Called when the GameObject is unloaded.
    /// </summary>
    private void OnDestroy()
    {
        if (SurfaceMeshesToPlanes.Instance != null)
        {
            SurfaceMeshesToPlanes.Instance.MakePlanesComplete -= SurfaceMeshesToPlanes_MakePlanesComplete;
        }
    }
}

Construir e Implementar

  • Antes de se deslocar para o HoloLens, prima o botão Reproduzir unidade para entrar no modo de reprodução.
  • Depois de a malha da sala ser carregada a partir do ficheiro, aguarde 10 segundos antes de iniciar o processamento na malha de mapeamento espacial.
  • Quando o processamento estiver concluído, os aviões aparecerão para representar o chão, paredes, teto, etc.
  • Depois de todos os aviões terem sido encontrados, deve ver um sistema solar a aparecer numa mesa de andar perto da câmara.
  • Dois cartazes devem aparecer nas paredes perto da câmara também. Mude para o separador Cena se não conseguir vê-los no modo Jogo.
  • Prima novamente o botão Reproduzir para sair do modo de reprodução.
  • Construa e desloque-se para o HoloLens, como de costume.
  • Aguarde a verificação e o processamento dos dados de mapeamento espacial para completar.
  • Assim que virem aviões, tentem encontrar o sistema solar e cartazes no vosso mundo.

Capítulo 4 - Colocação

Objetivos

  • Determinar se um holograma caberá numa superfície.
  • Forneça feedback ao utilizador quando um holograma pode/não caber numa superfície.

Instruções

  • No painel hierarquia da Unidade, selecione o objeto SpatialProcessing.
  • No painel do Inspetor, encontre o componente de malhas de superfície para aviões (Script).
  • Altere a propriedade Draw Planes para Nada para limpar a seleção.
  • Mude a propriedade Draw Planes para Wall,de modo a que apenas os aviões de parede sejam renderizados.
  • No painel Project, a pasta Scripts, clique duas vezes no Placeable.cs para abri-la em Visual Studio.

O script Placeable já está anexado aos cartazes e caixa de projeção que são criados após a conclusão do avião. Tudo o que precisamos fazer é descomprimir algum código, e este script vai alcançar o seguinte:

  1. Determine se um holograma caberá numa superfície através de um raio do centro e quatro cantos do cubo de delimitação.
  2. Verifique se a superfície é normal para determinar se é suficientemente lisa para que o holograma se sente nivelado.
  3. Torne um cubo de delimitação em torno do holograma para mostrar o seu tamanho real enquanto está sendo colocado.
  4. Lance uma sombra por baixo/atrás do holograma para mostrar onde será colocada no chão/parede.
  5. Torne a sombra vermelha, se o holograma não puder ser colocado na superfície, ou verde, se puder.
  6. Re-orientar o holograma para alinhar com o tipo de superfície (vertical ou horizontal) a que tem afinidade.
  7. Coloque suavemente o holograma na superfície selecionada para evitar o salto ou o comportamento de estalar.

Descomprê-lo todo o código no exercício de codificação abaixo, ou utilize esta solução completa em Placeable.cs:

using System.Collections.Generic;
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Enumeration containing the surfaces on which a GameObject
/// can be placed.  For simplicity of this sample, only one
/// surface type is allowed to be selected.
/// </summary>
public enum PlacementSurfaces
{
    // Horizontal surface with an upward pointing normal.
    Horizontal = 1,

    // Vertical surface with a normal facing the user.
    Vertical = 2,
}

/// <summary>
/// The Placeable class implements the logic used to determine if a GameObject
/// can be placed on a target surface. Constraints for placement include:
/// * No part of the GameObject's box collider impacts with another object in the scene
/// * The object lays flat (within specified tolerances) against the surface
/// * The object would not fall off of the surface if gravity were enabled.
/// This class also provides the following visualizations.
/// * A transparent cube representing the object's box collider.
/// * Shadow on the target surface indicating whether or not placement is valid.
/// </summary>
public class Placeable : MonoBehaviour
{
    [Tooltip("The base material used to render the bounds asset when placement is allowed.")]
    public Material PlaceableBoundsMaterial = null;

    [Tooltip("The base material used to render the bounds asset when placement is not allowed.")]
    public Material NotPlaceableBoundsMaterial = null;

    [Tooltip("The material used to render the placement shadow when placement it allowed.")]
    public Material PlaceableShadowMaterial = null;

    [Tooltip("The material used to render the placement shadow when placement it not allowed.")]
    public Material NotPlaceableShadowMaterial = null;

    [Tooltip("The type of surface on which the object can be placed.")]
    public PlacementSurfaces PlacementSurface = PlacementSurfaces.Horizontal;

    [Tooltip("The child object(s) to hide during placement.")]
    public List<GameObject> ChildrenToHide = new List<GameObject>();

    /// <summary>
    /// Indicates if the object is in the process of being placed.
    /// </summary>
    public bool IsPlacing { get; private set; }

    // The most recent distance to the surface.  This is used to 
    // locate the object when the user's gaze does not intersect
    // with the Spatial Mapping mesh.
    private float lastDistance = 2.0f;

    // The distance away from the target surface that the object should hover prior while being placed.
    private float hoverDistance = 0.15f;

    // Threshold (the closer to 0, the stricter the standard) used to determine if a surface is flat.
    private float distanceThreshold = 0.02f;

    // Threshold (the closer to 1, the stricter the standard) used to determine if a surface is vertical.
    private float upNormalThreshold = 0.9f;

    // Maximum distance, from the object, that placement is allowed.
    // This is used when raycasting to see if the object is near a placeable surface.
    private float maximumPlacementDistance = 5.0f;

    // Speed (1.0 being fastest) at which the object settles to the surface upon placement.
    private float placementVelocity = 0.06f;

    // Indicates whether or not this script manages the object's box collider.
    private bool managingBoxCollider = false;

    // The box collider used to determine of the object will fit in the desired location.
    // It is also used to size the bounding cube.
    private BoxCollider boxCollider = null;

    // Visible asset used to show the dimensions of the object. This asset is sized
    // using the box collider's bounds.
    private GameObject boundsAsset = null;

    // Visible asset used to show the where the object is attempting to be placed.
    // This asset is sized using the box collider's bounds.
    private GameObject shadowAsset = null;

    // The location at which the object will be placed.
    private Vector3 targetPosition;

    /// <summary>
    /// Called when the GameObject is created.
    /// </summary>
    private void Awake()
    {
        targetPosition = gameObject.transform.position;

        // Get the object's collider.
        boxCollider = gameObject.GetComponent<BoxCollider>();
        if (boxCollider == null)
        {
            // The object does not have a collider, create one and remember that
            // we are managing it.
            managingBoxCollider = true;
            boxCollider = gameObject.AddComponent<BoxCollider>();
            boxCollider.enabled = false;
        }

        // Create the object that will be used to indicate the bounds of the GameObject.
        boundsAsset = GameObject.CreatePrimitive(PrimitiveType.Cube);
        boundsAsset.transform.parent = gameObject.transform;
        boundsAsset.SetActive(false);

        // Create a object that will be used as a shadow.
        shadowAsset = GameObject.CreatePrimitive(PrimitiveType.Quad);
        shadowAsset.transform.parent = gameObject.transform;
        shadowAsset.SetActive(false);
    }

    /// <summary>
    /// Called when our object is selected.  Generally called by
    /// a gesture management component.
    /// </summary>
    public void OnSelect()
    {
        /* TODO: 4.a CODE ALONG 4.a */

        if (!IsPlacing)
        {
            OnPlacementStart();
        }
        else
        {
            OnPlacementStop();
        }
    }

    /// <summary>
    /// Called once per frame.
    /// </summary>
    private void Update()
    {
        /* TODO: 4.a CODE ALONG 4.a */

        if (IsPlacing)
        {
            // Move the object.
            Move();

            // Set the visual elements.
            Vector3 targetPosition;
            Vector3 surfaceNormal;
            bool canBePlaced = ValidatePlacement(out targetPosition, out surfaceNormal);
            DisplayBounds(canBePlaced);
            DisplayShadow(targetPosition, surfaceNormal, canBePlaced);
        }
        else
        {
            // Disable the visual elements.
            boundsAsset.SetActive(false);
            shadowAsset.SetActive(false);

            // Gracefully place the object on the target surface.
            float dist = (gameObject.transform.position - targetPosition).magnitude;
            if (dist > 0)
            {
                gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, targetPosition, placementVelocity / dist);
            }
            else
            {
                // Unhide the child object(s) to make placement easier.
                for (int i = 0; i < ChildrenToHide.Count; i++)
                {
                    ChildrenToHide[i].SetActive(true);
                }
            }
        }
    }

    /// <summary>
    /// Verify whether or not the object can be placed.
    /// </summary>
    /// <param name="position">
    /// The target position on the surface.
    /// </param>
    /// <param name="surfaceNormal">
    /// The normal of the surface on which the object is to be placed.
    /// </param>
    /// <returns>
    /// True if the target position is valid for placing the object, otherwise false.
    /// </returns>
    private bool ValidatePlacement(out Vector3 position, out Vector3 surfaceNormal)
    {
        Vector3 raycastDirection = gameObject.transform.forward;

        if (PlacementSurface == PlacementSurfaces.Horizontal)
        {
            // Placing on horizontal surfaces.
            // Raycast from the bottom face of the box collider.
            raycastDirection = -(Vector3.up);
        }

        // Initialize out parameters.
        position = Vector3.zero;
        surfaceNormal = Vector3.zero;

        Vector3[] facePoints = GetColliderFacePoints();

        // The origin points we receive are in local space and we 
        // need to raycast in world space.
        for (int i = 0; i < facePoints.Length; i++)
        {
            facePoints[i] = gameObject.transform.TransformVector(facePoints[i]) + gameObject.transform.position;
        }

        // Cast a ray from the center of the box collider face to the surface.
        RaycastHit centerHit;
        if (!Physics.Raycast(facePoints[0],
                        raycastDirection,
                        out centerHit,
                        maximumPlacementDistance,
                        SpatialMappingManager.Instance.LayerMask))
        {
            // If the ray failed to hit the surface, we are done.
            return false;
        }

        // We have found a surface.  Set position and surfaceNormal.
        position = centerHit.point;
        surfaceNormal = centerHit.normal;

        // Cast a ray from the corners of the box collider face to the surface.
        for (int i = 1; i < facePoints.Length; i++)
        {
            RaycastHit hitInfo;
            if (Physics.Raycast(facePoints[i],
                                raycastDirection,
                                out hitInfo,
                                maximumPlacementDistance,
                                SpatialMappingManager.Instance.LayerMask))
            {
                // To be a valid placement location, each of the corners must have a similar
                // enough distance to the surface as the center point
                if (!IsEquivalentDistance(centerHit.distance, hitInfo.distance))
                {
                    return false;
                }
            }
            else
            {
                // The raycast failed to intersect with the target layer.
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Determine the coordinates, in local space, of the box collider face that 
    /// will be placed against the target surface.
    /// </summary>
    /// <returns>
    /// Vector3 array with the center point of the face at index 0.
    /// </returns>
    private Vector3[] GetColliderFacePoints()
    {
        // Get the collider extents.  
        // The size values are twice the extents.
        Vector3 extents = boxCollider.size / 2;

        // Calculate the min and max values for each coordinate.
        float minX = boxCollider.center.x - extents.x;
        float maxX = boxCollider.center.x + extents.x;
        float minY = boxCollider.center.y - extents.y;
        float maxY = boxCollider.center.y + extents.y;
        float minZ = boxCollider.center.z - extents.z;
        float maxZ = boxCollider.center.z + extents.z;

        Vector3 center;
        Vector3 corner0;
        Vector3 corner1;
        Vector3 corner2;
        Vector3 corner3;

        if (PlacementSurface == PlacementSurfaces.Horizontal)
        {
            // Placing on horizontal surfaces.
            center = new Vector3(boxCollider.center.x, minY, boxCollider.center.z);
            corner0 = new Vector3(minX, minY, minZ);
            corner1 = new Vector3(minX, minY, maxZ);
            corner2 = new Vector3(maxX, minY, minZ);
            corner3 = new Vector3(maxX, minY, maxZ);
        }
        else
        {
            // Placing on vertical surfaces.
            center = new Vector3(boxCollider.center.x, boxCollider.center.y, maxZ);
            corner0 = new Vector3(minX, minY, maxZ);
            corner1 = new Vector3(minX, maxY, maxZ);
            corner2 = new Vector3(maxX, minY, maxZ);
            corner3 = new Vector3(maxX, maxY, maxZ);
        }

        return new Vector3[] { center, corner0, corner1, corner2, corner3 };
    }

    /// <summary>
    /// Put the object into placement mode.
    /// </summary>
    public void OnPlacementStart()
    {
        // If we are managing the collider, enable it. 
        if (managingBoxCollider)
        {
            boxCollider.enabled = true;
        }

        // Hide the child object(s) to make placement easier.
        for (int i = 0; i < ChildrenToHide.Count; i++)
        {
            ChildrenToHide[i].SetActive(false);
        }

        // Tell the gesture manager that it is to assume
        // all input is to be given to this object.
        GestureManager.Instance.OverrideFocusedObject = gameObject;

        // Enter placement mode.
        IsPlacing = true;
    }

    /// <summary>
    /// Take the object out of placement mode.
    /// </summary>
    /// <remarks>
    /// This method will leave the object in placement mode if called while
    /// the object is in an invalid location.  To determine whether or not
    /// the object has been placed, check the value of the IsPlacing property.
    /// </remarks>
    public void OnPlacementStop()
    {
        // ValidatePlacement requires a normal as an out parameter.
        Vector3 position;
        Vector3 surfaceNormal;

        // Check to see if we can exit placement mode.
        if (!ValidatePlacement(out position, out surfaceNormal))
        {
            return;
        }

        // The object is allowed to be placed.
        // We are placing at a small buffer away from the surface.
        targetPosition = position + (0.01f * surfaceNormal);

        OrientObject(true, surfaceNormal);

        // If we are managing the collider, disable it. 
        if (managingBoxCollider)
        {
            boxCollider.enabled = false;
        }

        // Tell the gesture manager that it is to resume
        // its normal behavior.
        GestureManager.Instance.OverrideFocusedObject = null;

        // Exit placement mode.
        IsPlacing = false;
    }

    /// <summary>
    /// Positions the object along the surface toward which the user is gazing.
    /// </summary>
    /// <remarks>
    /// If the user's gaze does not intersect with a surface, the object
    /// will remain at the most recently calculated distance.
    /// </remarks>
    private void Move()
    {
        Vector3 moveTo = gameObject.transform.position;
        Vector3 surfaceNormal = Vector3.zero;
        RaycastHit hitInfo;

        bool hit = Physics.Raycast(Camera.main.transform.position,
                                Camera.main.transform.forward,
                                out hitInfo,
                                20f,
                                SpatialMappingManager.Instance.LayerMask);

        if (hit)
        {
            float offsetDistance = hoverDistance;

            // Place the object a small distance away from the surface while keeping 
            // the object from going behind the user.
            if (hitInfo.distance <= hoverDistance)
            {
                offsetDistance = 0f;
            }

            moveTo = hitInfo.point + (offsetDistance * hitInfo.normal);

            lastDistance = hitInfo.distance;
            surfaceNormal = hitInfo.normal;
        }
        else
        {
            // The raycast failed to hit a surface.  In this case, keep the object at the distance of the last
            // intersected surface.
            moveTo = Camera.main.transform.position + (Camera.main.transform.forward * lastDistance);
        }

        // Follow the user's gaze.
        float dist = Mathf.Abs((gameObject.transform.position - moveTo).magnitude);
        gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, moveTo, placementVelocity / dist);

        // Orient the object.
        // We are using the return value from Physics.Raycast to instruct
        // the OrientObject function to align to the vertical surface if appropriate.
        OrientObject(hit, surfaceNormal);
    }

    /// <summary>
    /// Orients the object so that it faces the user.
    /// </summary>
    /// <param name="alignToVerticalSurface">
    /// If true and the object is to be placed on a vertical surface, 
    /// orient parallel to the target surface.  If false, orient the object 
    /// to face the user.
    /// </param>
    /// <param name="surfaceNormal">
    /// The target surface's normal vector.
    /// </param>
    /// <remarks>
    /// The alignToVerticalSurface parameter is ignored if the object
    /// is to be placed on a horizontalSurface
    /// </remarks>
    private void OrientObject(bool alignToVerticalSurface, Vector3 surfaceNormal)
    {
        Quaternion rotation = Camera.main.transform.localRotation;

        // If the user's gaze does not intersect with the Spatial Mapping mesh,
        // orient the object towards the user.
        if (alignToVerticalSurface && (PlacementSurface == PlacementSurfaces.Vertical))
        {
            // We are placing on a vertical surface.
            // If the normal of the Spatial Mapping mesh indicates that the
            // surface is vertical, orient parallel to the surface.
            if (Mathf.Abs(surfaceNormal.y) <= (1 - upNormalThreshold))
            {
                rotation = Quaternion.LookRotation(-surfaceNormal, Vector3.up);
            }
        }
        else
        {
            rotation.x = 0f;
            rotation.z = 0f;
        }

        gameObject.transform.rotation = rotation;
    }

    /// <summary>
    /// Displays the bounds asset.
    /// </summary>
    /// <param name="canBePlaced">
    /// Specifies if the object is in a valid placement location.
    /// </param>
    private void DisplayBounds(bool canBePlaced)
    {
        // Ensure the bounds asset is sized and positioned correctly.
        boundsAsset.transform.localPosition = boxCollider.center;
        boundsAsset.transform.localScale = boxCollider.size;
        boundsAsset.transform.rotation = gameObject.transform.rotation;

        // Apply the appropriate material.
        if (canBePlaced)
        {
            boundsAsset.GetComponent<Renderer>().sharedMaterial = PlaceableBoundsMaterial;
        }
        else
        {
            boundsAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableBoundsMaterial;
        }

        // Show the bounds asset.
        boundsAsset.SetActive(true);
    }

    /// <summary>
    /// Displays the placement shadow asset.
    /// </summary>
    /// <param name="position">
    /// The position at which to place the shadow asset.
    /// </param>
    /// <param name="surfaceNormal">
    /// The normal of the surface on which the asset will be placed
    /// </param>
    /// <param name="canBePlaced">
    /// Specifies if the object is in a valid placement location.
    /// </param>
    private void DisplayShadow(Vector3 position,
                            Vector3 surfaceNormal,
                            bool canBePlaced)
    {
        // Rotate and scale the shadow so that it is displayed on the correct surface and matches the object.
        float rotationX = 0.0f;

        if (PlacementSurface == PlacementSurfaces.Horizontal)
        {
            rotationX = 90.0f;
            shadowAsset.transform.localScale = new Vector3(boxCollider.size.x, boxCollider.size.z, 1);
        }
        else
        {
            shadowAsset.transform.localScale = boxCollider.size;
        }

        Quaternion rotation = Quaternion.Euler(rotationX, gameObject.transform.rotation.eulerAngles.y, 0);
        shadowAsset.transform.rotation = rotation;

        // Apply the appropriate material.
        if (canBePlaced)
        {
            shadowAsset.GetComponent<Renderer>().sharedMaterial = PlaceableShadowMaterial;
        }
        else
        {
            shadowAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableShadowMaterial;
        }

        // Show the shadow asset as appropriate.
        if (position != Vector3.zero)
        {
            // Position the shadow a small distance from the target surface, along the normal.
            shadowAsset.transform.position = position + (0.01f * surfaceNormal);
            shadowAsset.SetActive(true);
        }
        else
        {
            shadowAsset.SetActive(false);
        }
    }

    /// <summary>
    /// Determines if two distance values should be considered equivalent. 
    /// </summary>
    /// <param name="d1">
    /// Distance to compare.
    /// </param>
    /// <param name="d2">
    /// Distance to compare.
    /// </param>
    /// <returns>
    /// True if the distances are within the desired tolerance, otherwise false.
    /// </returns>
    private bool IsEquivalentDistance(float d1, float d2)
    {
        float dist = Mathf.Abs(d1 - d2);
        return (dist <= distanceThreshold);
    }

    /// <summary>
    /// Called when the GameObject is unloaded.
    /// </summary>
    private void OnDestroy()
    {
        // Unload objects we have created.
        Destroy(boundsAsset);
        boundsAsset = null;
        Destroy(shadowAsset);
        shadowAsset = null;
    }
}

Construir e Implementar

  • Como antes, construir o projeto e implantar para o HoloLens.
  • Aguarde a verificação e o processamento dos dados de mapeamento espacial para completar.
  • Quando vir o sistema solar, olhe para a caixa de projeção abaixo e realize um gesto selecionado para movê-lo. Enquanto a caixa de projeção é selecionada, um cubo de delimitação será visível em torno da caixa de projeção.
  • Mova-se para olhar para um local diferente na sala. A caixa de projeção deve seguir o seu olhar. Quando a sombra por baixo da caixa de projeção fica vermelha, não é possível colocar o holograma nessa superfície. Quando a sombra abaixo da caixa de projeção ficar verde, pode colocar o holograma executando outro gesto selecionado.
  • Encontre e selecione um dos cartazes holográficos na parede para movê-lo para um novo local. Note que não pode colocar o cartaz no chão ou no teto, e que fica corretamente orientado para cada parede à medida que se desloca.

Capítulo 5 - Oclusão

Objetivos

  • Determine se um holograma é obstruído pela malha de mapeamento espacial.
  • Aplique diferentes técnicas de oclusão para obter um efeito divertido.

Instruções

Primeiro, vamos permitir que a malha de mapeamento espacial oclude outros hologramas sem impedir o mundo real:

  • No painel hierarquia, selecione o objeto SpatialProcessing.
  • No painel inspetor, encontre o componente Play Space Manager (Script).
  • Clique no círculo à direita da propriedade Material Secundário.
  • Encontre e selecione o material de oclusão e feche a janela.

Em seguida, vamos adicionar um comportamento especial à Terra, para que tenha um destaque azul sempre que se torna obstruído por outro holograma (como o sol), ou pela malha de mapeamento espacial:

  • No painel Project, na pasta Hologramas, expanda o objeto SolarSystem.
  • Clique na Terra.
  • No painel do Inspetor, encontre o material da Terra (componente inferior).
  • No shader drop-down,mude o shader para Custom OclusionRim. Isto fará um destaque azul em torno da Terra sempre que for obstruído por outro objeto.

Finalmente, vamos permitir um efeito de visão de raio-X para planetas no nosso sistema solar. Teremos de editar a .cs PlanetOcclusion (encontrada na pasta Scripts\SolarSystem) para conseguir o seguinte:

  1. Determine se um planeta é obstruído pela camada de SpatialMapping (malhas de sala e planos).
  2. Mostre a representação da estrutura de arame de um planeta sempre que é oclusa pela camada SpatialMapping.
  3. Esconda a representação da estrutura de arame de um planeta quando não estiver bloqueada pela camada EspacialMapping.

Siga o exercício de codificação no PlanetOcclusion.cs ou utilize a seguinte solução:

using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Determines when the occluded version of the planet should be visible.
/// This script allows us to do selective occlusion, so the occlusionObject
/// will only be rendered when a Spatial Mapping surface is occluding the planet,
/// not when another hologram is responsible for the occlusion.
/// </summary>
public class PlanetOcclusion : MonoBehaviour
{
    [Tooltip("Object to display when the planet is occluded.")]
    public GameObject occlusionObject;

    /// <summary>
    /// Points to raycast to when checking for occlusion.
    /// </summary>
    private Vector3[] checkPoints;

    // Use this for initialization
    void Start()
    {
        occlusionObject.SetActive(false);

        // Set the check points to use when testing for occlusion.
        MeshFilter filter = gameObject.GetComponent<MeshFilter>();
        Vector3 extents = filter.mesh.bounds.extents;
        Vector3 center = filter.mesh.bounds.center;
        Vector3 top = new Vector3(center.x, center.y + extents.y, center.z);
        Vector3 left = new Vector3(center.x - extents.x, center.y, center.z);
        Vector3 right = new Vector3(center.x + extents.x, center.y, center.z);
        Vector3 bottom = new Vector3(center.x, center.y - extents.y, center.z);

        checkPoints = new Vector3[] { center, top, left, right, bottom };
    }

    // Update is called once per frame
    void Update()
    {
        /* TODO: 5.a DEVELOPER CODING EXERCISE 5.a */

        // Check to see if any of the planet's boundary points are occluded.
        for (int i = 0; i < checkPoints.Length; i++)
        {
            // 5.a: Convert the current checkPoint to world coordinates.
            // Call gameObject.transform.TransformPoint(checkPoints[i]).
            // Assign the result to a new Vector3 variable called 'checkPt'.
            Vector3 checkPt = gameObject.transform.TransformPoint(checkPoints[i]);

            // 5.a: Call Vector3.Distance() to calculate the distance
            // between the Main Camera's position and 'checkPt'.
            // Assign the result to a new float variable called 'distance'.
            float distance = Vector3.Distance(Camera.main.transform.position, checkPt);

            // 5.a: Take 'checkPt' and subtract the Main Camera's position from it.
            // Assign the result to a new Vector3 variable called 'direction'.
            Vector3 direction = checkPt - Camera.main.transform.position;

            // Used to indicate if the call to Physics.Raycast() was successful.
            bool raycastHit = false;

            // 5.a: Check if the planet is occluded by a spatial mapping surface.
            // Call Physics.Raycast() with the following arguments:
            // - Pass in the Main Camera's position as the origin.
            // - Pass in 'direction' for the direction.
            // - Pass in 'distance' for the maxDistance.
            // - Pass in SpatialMappingManager.Instance.LayerMask as layerMask.
            // Assign the result to 'raycastHit'.
            raycastHit = Physics.Raycast(Camera.main.transform.position, direction, distance, SpatialMappingManager.Instance.LayerMask);

            if (raycastHit)
            {
                // 5.a: Our raycast hit a surface, so the planet is occluded.
                // Set the occlusionObject to active.
                occlusionObject.SetActive(true);

                // At least one point is occluded, so break from the loop.
                break;
            }
            else
            {
                // 5.a: The Raycast did not hit, so the planet is not occluded.
                // Deactivate the occlusionObject.
                occlusionObject.SetActive(false);
            }
        }
    }
}

Construir e Implementar

  • Construa e implemente a aplicação para HoloLens, como de costume.
  • Aguarde a conclusão da verificação e processamento dos dados de mapeamento espacial (deve ver linhas azuis nas paredes).
  • Encontre e selecione a caixa de projeção do sistema solar e, em seguida, coloque a caixa ao lado de uma parede ou atrás de um balcão.
  • Você pode ver oclusão básica escondendo-se atrás de superfícies para espreitar no cartaz ou caixa de projeção.
  • Procure a Terra, deve haver um efeito de destaque azul sempre que vai atrás de outro holograma ou superfície.
  • Veja como os planetas se movem atrás da parede ou de outras superfícies na sala. Agora tens visão de raio-X e consegues ver os esqueletos da armação de arame!

Fim

Parabéns! Já completou mr Spatial 230: Mapeamento espacial.

  • Sabe como digitalizar o seu ambiente e carregar dados de mapeamento espacial para a Unidade.
  • Compreende o básico dos shaders e como os materiais podem ser usados para re visualizar o mundo.
  • Soubes de novas técnicas de processamento para encontrar planos e remover triângulos de uma malha.
  • Conseguiste mover-te e colocar hologramas em superfícies que fizessem sentido.
  • Experimentou diferentes técnicas de oclusão e aproveitou o poder da visão de raio-X!