HoloLens (1. generacja) Spatial 230: Mapowanie przestrzenne

Ważne

Samouczki Mixed Reality Academy zostały zaprojektowane z myślą o urządzeniach HoloLens (1. generacji), Unity 2017 i Mixed Reality immersywnych zestawów słuchawkowych. W związku z tym uważamy, że ważne jest pozostawienie tych samouczków w miejscu dla deweloperów, którzy nadal szukają wskazówek dotyczących opracowywania dla tych urządzeń. Te samouczki nie zostaną zaktualizowane przy użyciu najnowszych zestawów narzędzi ani interakcji używanych do HoloLens 2 i mogą nie być zgodne z nowszymi wersjami aparatu Unity. Będą one utrzymywane w celu kontynuowania pracy na obsługiwanych urządzeniach. Opublikowano nową serię samouczków dla HoloLens 2.

Mapowanie przestrzenne łączy świat rzeczywisty i wirtualny ze sobą, ucząc hologramy dotyczące środowiska. W narzędziu MR Spatial 230 (Project Planetarium) dowiesz się, jak wykonywać następujące działania:

  • Przeskanuj środowisko i przenieś dane z urządzenia HoloLens na maszynę deweloperską.
  • Eksploruj cieniowanie i dowiedz się, jak używać ich do wizualizowania przestrzeni.
  • Podziel siatkę pomieszczenia na proste płaszczyzny przy użyciu przetwarzania siatki.
  • Zapoznaj się z technikami umieszczania poznanymi w artykule MR Basics 101 i przekaż opinię na temat miejsca, w którym można umieścić hologram w środowisku.
  • Eksploruj efekty okluzji, więc gdy hologram znajduje się za rzeczywistym obiektem, nadal możesz zobaczyć go za obrazem rentgenowskim!

Obsługa urządzeń

Kurs HoloLens Immersyjne zestawy nagłowne
MR Spatial 230: Mapowanie przestrzenne ✔️

Przed rozpoczęciem

Wymagania wstępne

Pliki projektu

  • Pobierz pliki wymagane przez projekt. Wymaga aparatu Unity 2017.2 lub nowszego.
    • Jeśli nadal potrzebujesz obsługi aparatu Unity 5.6, skorzystaj z tej wersji.
    • Jeśli nadal potrzebujesz obsługi aparatu Unity 5.5, skorzystaj z tej wersji.
    • Jeśli nadal potrzebujesz obsługi aparatu Unity 5.4, skorzystaj z tej wersji.
  • Cofnij archiwizowanie plików na pulpicie lub innych łatwych w dotarciu do lokalizacji.

Uwaga

Jeśli chcesz przejrzeć kod źródłowy przed pobraniem, jest on dostępny w witrynie GitHub.

Uwagi

  • Opcja "Włącz tylko mój kod" w programie Visual Studio musi być wyłączona (niezaznaczone) w obszarze Narzędzia > Opcje > Debugowanie w celu trafienia punktów przerwania w kodzie.

Konfiguracja aparatu Unity

  • Uruchom aparat Unity.
  • Wybierz pozycję Nowy , aby utworzyć nowy projekt.
  • Nadaj projektowi nazwę Planetarium.
  • Sprawdź, czy wybrano ustawienie 3D .
  • Kliknij pozycję Create Project (Utwórz projekt).
  • Po uruchomieniu aparatu Unity przejdź do pozycji Edytuj > odtwarzacz ustawień > projektu.
  • Na panelu Inspector (Inspektor ) znajdź i wybierz zieloną ikonę Sklepu Windows .
  • Rozwiń pozycję Inne ustawienia.
  • W sekcji Renderowanie zaznacz opcję Obsługiwana rzeczywistość wirtualna .
  • Sprawdź, czy na liście zestawów SDK rzeczywistości wirtualnej jest wyświetlana platforma Windows Holographic. Jeśli nie, wybierz + przycisk w dolnej części listy i wybierz pozycję Windows Holographic.
  • Rozwiń węzeł Ustawienia publikowania.
  • W sekcji Możliwości sprawdź następujące ustawienia:
    • InternetClientServer
    • PrivateNetworkClientServer
    • Mikrofon
    • SpatialPerception
  • Przejdź do pozycji Edytuj > jakość ustawień > projektu
  • W panelu Inspector (Inspektor ) w obszarze ikony Sklep Windows wybierz czarną strzałkę listy rozwijanej w wierszu "Default" (Domyślny) i zmień ustawienie domyślne na Bardzo niskie.
  • Przejdź do pozycji Zasoby > importuj pakiet > niestandardowy.
  • Przejdź do folderu ...\HolographicAcademy-Holograms-230-SpatialMapping\Starting .
  • Kliknij planetarium.unitypackage.
  • Kliknij przycisk Otwórz.
  • Powinno zostać wyświetlone okno Importowanie pakietu aparatu Unity , kliknij przycisk Importuj .
  • Poczekaj, aż aparat Unity zaimportuje wszystkie zasoby, które będą potrzebne do ukończenia tego projektu.
  • Na panelu Hierarchia usuń kamerę główną.
  • Na panelu ProjektholoToolkit-SpatialMapping-230\Utilities\Prefabs znajdź obiekt Main Camera .
  • Przeciągnij i upuść prefabryk aparatu głównego do panelu Hierarchy .
  • Na panelu Hierarchia usuń obiekt Directional Light .
  • W panelu Project (Projekt) znajdź obiekt Cursor w folderze Holograms.
  • Przeciągnij & upuść prefabryk kursora do hierarchii.
  • W panelu Hierarchy (Hierarchia ) wybierz obiekt Cursor .
  • Na panelu Inspektor kliknij listę rozwijaną Warstwa i wybierz pozycję Edytuj warstwy....
  • Nazwij warstwę użytkownika 31 jako "SpatialMapping".
  • Zapisz nową scenę: Zapisz scenę jako > ...
  • Kliknij pozycję Nowy folder i nadaj folderowi nazwę Sceny.
  • Nazwij plik "Planetarium" i zapisz go w folderze Sceny .

Rozdział 1 — Skanowanie

Cele

  • Dowiedz się więcej o serwerze SurfaceObserver i sposobie, w jaki jego ustawienia wpływają na środowisko i wydajność.
  • Utwórz środowisko skanowania pomieszczeń, aby zebrać siatki pomieszczenia.

Instrukcje

  • W panelu ProjektuHoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs znajdź prefab spatialMapping .
  • Przeciągnij & upuść prefab SpatialMapping do panelu Hierarchy (Hierarchy ).

Kompilowanie i wdrażanie (część 1)

  • W akrocie Unity wybierz pozycję Ustawienia kompilacji pliku>.
  • Kliknij pozycję Dodaj otwarte sceny , aby dodać scenę Planetarium do kompilacji.
  • Wybierz platforma uniwersalna systemu Windows na liście Platforma, a następnie kliknij pozycję Przełącz platformę.
  • Ustaw zestaw SDK na typ kompilacji uniwersalnej wersji 10 i platformy UWP na D3D.
  • Sprawdź projekty języka C# aparatu Unity.
  • Kliknij pozycję Kompiluj.
  • Utwórz nowy folder o nazwie "Aplikacja".
  • Jednym kliknięciem folderu Aplikacja .
  • Naciśnij przycisk Wybierz folder .
  • Po zakończeniu tworzenia aparatu Unity zostanie wyświetlone okno Eksplorator plików.
  • Kliknij dwukrotnie folder Aplikacja, aby go otworzyć.
  • Kliknij dwukrotnie plik Planetarium.sln , aby załadować projekt w programie Visual Studio.
  • W programie Visual Studio użyj górnego paska narzędzi, aby zmienić konfigurację na Wydanie.
  • Zmień platformę na x86.
  • Kliknij strzałkę listy rozwijanej po prawej stronie pozycji "Maszyna lokalna", a następnie wybierz pozycję Maszyna zdalna.
  • Wprowadź adres IP urządzenia w polu Adres i zmień tryb uwierzytelniania na uniwersalny (protokół niezaszyfrowany).
  • Kliknij pozycję Debuguj —> rozpocznij bez debugowania lub naciśnij klawisze Ctrl + F5.
  • Obejrzyj panel Dane wyjściowe w programie Visual Studio, aby uzyskać informacje o stanie kompilacji i wdrożenia.
  • Po wdrożeniu aplikacji przejdź po pokoju. Zobaczysz otaczające powierzchnie pokryte czarnymi i białymi siatkami szkieletowymi.
  • Skanuj otoczenie. Pamiętaj, aby spojrzeć na ściany, sufity i podłogi.

Kompilowanie i wdrażanie (część 2)

Teraz przyjrzyjmy się, jak mapowanie przestrzenne może wpływać na wydajność.

  • W aproszcie Unity wybierz pozycję Profiler okna>.
  • Kliknij pozycję Dodaj procesor GPU profilera>.
  • Kliknij pozycję Aktywny profiler ><Wprowadź adres IP>.
  • Wprowadź adres IP urządzenia HoloLens.
  • Kliknij przycisk Połącz.
  • Obserwuj liczbę milisekund potrzebnych do renderowania ramki przez procesor GPU.
  • Zatrzymaj uruchamianie aplikacji na urządzeniu.
  • Wróć do programu Visual Studio i otwórz plik SpatialMappingObserver.cs. Znajdziesz go w folderze HoloToolkit\SpatialMapping projektu Assembly-CSharp (uniwersalny system Windows).
  • Znajdź funkcję Awake() i dodaj następujący wiersz kodu: TrianglesPerCubicMeter = 1200;
  • Ponownie wdróż projekt na urządzeniu, a następnie ponownie połącz profilera. Obserwuj zmianę liczby milisekund w celu renderowania ramki.
  • Zatrzymaj uruchamianie aplikacji na urządzeniu.

Zapisywanie i ładowanie w a unity

Na koniec zapiszmy naszą siatkę pomieszczeń i załadujmy ją do aparatu Unity.

  • Wróć do programu Visual Studio i usuń wiersz TrianglesPerCubicMeter dodany w funkcji Awake() w poprzedniej sekcji.
  • Ponownie wdróż projekt na urządzeniu. Powinniśmy teraz działać z 500 trójkątami na metr sześcienny.
  • Otwórz przeglądarkę i wprowadź ciąg w adresie IPAddress urządzenia HoloLens, aby przejść do portalu urządzeń z systemem Windows.
  • Wybierz opcję Widok 3D w panelu po lewej stronie.
  • W obszarze Rekonstrukcja powierzchni wybierz przycisk Aktualizuj .
  • Obserwuj, jak obszary, które zostały zeskanowane na urządzeniu HoloLens, są wyświetlane w oknie wyświetlania.
  • Aby zapisać skanowanie pomieszczeń, naciśnij przycisk Zapisz .
  • Otwórz folder Pobrane , aby znaleźć zapisany model pokoju SRMesh.obj.
  • Skopiuj plik SRMesh.obj do folderu Assets projektu aparatu Unity.
  • W akrocie Unity wybierz obiekt SpatialMapping w panelu Hierarchy (Hierarchia).
  • Znajdź składnik Obserwator powierzchni obiektu (skrypt).
  • Kliknij okrąg po prawej stronie właściwości Model pokoju .
  • Znajdź i wybierz obiekt SRMesh , a następnie zamknij okno.
  • Sprawdź, czy właściwość Room Model w panelu Inspector (Inspektor ) jest teraz ustawiona na SRMesh.
  • Naciśnij przycisk Odtwórz , aby wprowadzić tryb podglądu aparatu Unity.
  • Składnik SpatialMapping załaduje siatki z zapisanego modelu pomieszczenia, aby można było ich używać w apieszcie Unity.
  • Przełącz się do widoku Scena , aby wyświetlić cały model pokoju z cieniatorem szkieletu.
  • Naciśnij ponownie przycisk Odtwarzania , aby zamknąć tryb podglądu.

UWAGA: Następnym razem, gdy przejdziesz do trybu podglądu w a aparatu Unity, domyślnie załaduje on zapisaną siatkę pomieszczenia.

Rozdział 2 — Wizualizacja

Cele

  • Poznaj podstawy cieniowania.
  • Wizualizowanie otoczenia.

Instrukcje

  • Na panelu Hierarchia aparatu Unity wybierz obiekt SpatialMapping .
  • Na panelu Inspector (Inspektor) znajdź składnik Spatial Mapping Manager (Script).
  • Kliknij okrąg po prawej stronie właściwości Surface Material .
  • Znajdź i wybierz materiał BlueLinesOnWalls i zamknij okno.
  • W folderze Cieniowania paneli projektu kliknij dwukrotnie pozycję BlueLinesOnWalls, aby otworzyć cieniowania w programie Visual Studio.
  • Jest to prosty piksel (wierzchołek do fragmentowania), który wykonuje następujące zadania:
    1. Konwertuje lokalizację wierzchołka na przestrzeń światową.
    2. Sprawdza normalny wierzchołek, aby określić, czy piksel jest pionowy.
    3. Ustawia kolor piksela do renderowania.

Wdróż i konfiguruj

  • Wróć do aparatu Unity i naciśnij klawisz Play , aby przejść do trybu podglądu.
  • Niebieskie linie będą renderowane na wszystkich pionowych powierzchniach siatki pomieszczenia (które są automatycznie ładowane z zapisanych danych skanowania).
  • Przejdź do karty Scena , aby dostosować widok pomieszczenia i zobaczyć, jak w aję aparatu Unity pojawia się cała siatka pomieszczenia.
  • Na panelu Projekt znajdź folder Materials i wybierz materiał BlueLinesOnWalls .
  • Zmodyfikuj niektóre właściwości i zobacz, jak zmiany są wyświetlane w edytorze aparatu Unity.
    • Na panelu Inspektor dostosuj wartość LineScale , aby linie wydawały się grubsze lub cieńsze.
    • Na panelu Inspector (Inspektor ) dostosuj wartość LinesPerMeter , aby zmienić liczbę linii wyświetlanych na każdej ścianie.
  • Kliknij przycisk Odtwórz ponownie, aby zamknąć tryb podglądu.
  • Skompiluj i wdróż na urządzeniu HoloLens i obserwuj, jak renderowanie cieniowania pojawia się na rzeczywistych powierzchniach.

Aparat Unity doskonale sprawdza się podczas wyświetlania podglądu materiałów, ale zawsze dobrym pomysłem jest wyewidencjonowanie renderowania na urządzeniu.

Rozdział 3 — Przetwarzanie

Cele

  • Poznaj techniki przetwarzania danych mapowania przestrzennego do użycia w aplikacji.
  • Analizowanie danych mapowania przestrzennego w celu znalezienia płaszczyzn i usunięcia trójkątów.
  • Używanie płaszczyzn do umieszczania hologramu.

Instrukcje

  • Na panelu Projekt aparatu Unity folder Hologramy znajdź obiekt SpatialProcessing .
  • Przeciągnij & upuść obiekt SpatialProcessing do panelu Hierarchy (Hierarchia ).

Prefab spatialProcessing zawiera składniki do przetwarzania danych mapowania przestrzennego. Plik SurfaceMeshesToPlanes.cs znajdzie i wygeneruje płaszczyzny na podstawie danych mapowania przestrzennego. Będziemy używać płaszczyzn w naszej aplikacji do reprezentowania ścian, podłóg i sufitów. Ten prefab zawiera również plik RemoveSurfaceVertices.cs , który może usuwać wierzchołki z siatki mapowania przestrzennego. Może to służyć do tworzenia otworów w siatce lub usuwania nadmiarowych trójkątów, które nie są już potrzebne (ponieważ zamiast tego można używać płaszczyzn).

  • Na panelu Projekt aparatu Unity folder Hologramy znajdź obiekt SpaceCollection .
  • Przeciągnij i upuść obiekt SpaceCollection na panelu Hierarchy .
  • W panelu Hierarchy (Hierarchia ) wybierz obiekt SpatialProcessing .
  • Na panelu Inspector (Inspektor) znajdź składnik Play Space Manager (Script).
  • Kliknij dwukrotnie plik PlaySpaceManager.cs , aby otworzyć go w programie Visual Studio.

Plik PlaySpaceManager.cs zawiera kod specyficzny dla aplikacji. Dodamy do tego skryptu funkcje umożliwiające następujące zachowanie:

  1. Zatrzymaj zbieranie danych mapowania przestrzennego po przekroczeniu limitu czasu skanowania (10 sekund).
  2. Przetwarzanie danych mapowania przestrzennego:
    1. Użyj powierzchniMeshesToPlanes, aby utworzyć prostszą reprezentację świata jako płaszczyzn (ściany, podłogi, sufity itp.).
    2. Użyj polecenia RemoveSurfaceVertices, aby usunąć trójkąty powierzchniowe, które mieszczą się w granicach płaszczyzny.
  3. Wygeneruj kolekcję hologramów na świecie i umieść je na płaszczyźnie ściany i podłogi w pobliżu użytkownika.

Wykonaj ćwiczenia programistyczne oznaczone w pliku PlaySpaceManager.cs lub zastąp skrypt gotowym rozwiązaniem poniżej:

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

Wdróż i konfiguruj

  • Przed wdrożeniem na urządzeniu HoloLens naciśnij przycisk Odtwórz w a środowisku Unity, aby przejść do trybu odtwarzania.
  • Po załadowaniu siatki pomieszczenia z pliku poczekaj 10 sekund przed rozpoczęciem przetwarzania w siatce mapowania przestrzennego.
  • Po zakończeniu przetwarzania płaszczyzny będą przedstawiać podłogę, ściany, sufit itp.
  • Po znalezieniu wszystkich samolotów, powinien zostać wyświetlony układ słoneczny na stole podłogi w pobliżu kamery.
  • Dwa plakaty powinny pojawić się na ścianach w pobliżu kamery też. Przejdź do karty Scena , jeśli nie widzisz ich w trybie gry .
  • Naciśnij ponownie przycisk Odtwarzania , aby zamknąć tryb odtwarzania.
  • Skompiluj i wdróż je na urządzeniu HoloLens, jak zwykle.
  • Poczekaj na ukończenie skanowania i przetwarzania danych mapowania przestrzennego.
  • Gdy zobaczysz samoloty, spróbuj znaleźć układ słoneczny i plakaty na świecie.

Rozdział 4 — Umieszczanie

Cele

  • Ustal, czy hologram zmieści się na powierzchni.
  • Prześlij opinię użytkownikowi, gdy hologram może/nie mieści się na powierzchni.

Instrukcje

  • Na panelu Hierarchia aparatu Unity wybierz obiekt SpatialProcessing .
  • Na panelu Inspector (Inspektor) znajdź składnik Surface Meshes To Planes (Script).
  • Zmień właściwość Rysuj płaszczyzny na Nic , aby wyczyścić zaznaczenie.
  • Zmień właściwość Rysuj płaszczyzny na Ściana, aby tylko płaszczyzny ściany zostały renderowane.
  • W panelu Project (Projekt ) folder Scripts (Skrypty ) kliknij dwukrotnie plik Placeable.cs , aby otworzyć go w programie Visual Studio.

Skrypt zastępczy jest już dołączony do plakatów i skrzynki projekcji, które są tworzone po zakończeniu znajdowania płaszczyzny. Wszystko, co musimy zrobić, to usunąć komentarz z kodu, a ten skrypt osiągnie następujące korzyści:

  1. Ustal, czy hologram zmieści się na powierzchni, prześwietlając od środka i cztery rogi sześcianu ograniczenia.
  2. Sprawdź, czy powierzchnia jest normalna, aby ustalić, czy jest wystarczająco gładka, aby hologram usiąść na.
  3. Renderowanie modułu ograniczenia wokół hologramu w celu pokazania rzeczywistego rozmiaru podczas umieszczania.
  4. Rzutowanie cienia pod hologramem/za hologramem, aby pokazać, gdzie zostanie umieszczony na podłodze/ścianie.
  5. Renderuj cień jako czerwony, jeśli hologram nie może być umieszczony na powierzchni lub zielony, jeśli może.
  6. Ponownie sorientuj hologram, aby dopasować go do typu powierzchni (pionowego lub poziomego), do którego ma koligację.
  7. Płynnie umieść hologram na wybranej powierzchni, aby uniknąć skoków lub przyciągania zachowania.

Usuń komentarz ze wszystkich kodu w poniższym ćwiczeniu kodowania lub użyj tego ukończonego rozwiązania w pliku 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;
    }
}

Wdróż i konfiguruj

  • Tak jak poprzednio, skompiluj projekt i wdróż go na urządzeniu HoloLens.
  • Poczekaj na ukończenie skanowania i przetwarzania danych mapowania przestrzennego.
  • Gdy widzisz układ słoneczny, spójrz na pole projekcji poniżej i wykonaj gest wyboru, aby go poruszać. Gdy pole projekcji jest zaznaczone, wokół pola projekcji będzie widoczny moduł ograniczenia.
  • Przenieś cię, aby spojrzeć na inną lokalizację w pokoju. Pole projekcji powinno podążać za spojrzeniem. Gdy cień poniżej pola projekcji zmieni kolor na czerwony, nie można umieścić hologramu na tej powierzchni. Gdy cień poniżej pola projekcji zmieni kolor na zielony, możesz umieścić hologram, wykonując inny gest wyboru.
  • Znajdź i wybierz jeden z holograficznych plakatów na ścianie, aby przenieść go do nowej lokalizacji. Zauważ, że nie można umieścić plakatu na podłodze lub suficie i że pozostaje prawidłowo zorientowany na każdą ścianę podczas poruszania się.

Rozdział 5 — Oklusja

Cele

  • Ustal, czy hologram jest okludniony przez siatkę mapowania przestrzennego.
  • Zastosuj różne techniki okluzji, aby osiągnąć efekt zabawy.

Instrukcje

Najpierw zezwolimy na mapowanie przestrzenne siatki innym hologramom bez okludium świata rzeczywistego:

  • W panelu Hierarchy (Hierarchia ) wybierz obiekt SpatialProcessing .
  • Na panelu Inspector (Inspektor) znajdź składnik Play Space Manager (Script).
  • Kliknij okrąg po prawej stronie właściwości Materiał pomocniczy .
  • Znajdź i wybierz materiał okluzji i zamknij okno.

Następnie dodamy specjalne zachowanie na Ziemi, aby mieć niebieskie wyróżnienie za każdym razem, gdy staje się okludniony przez inny hologram (na przykład słońce) lub przez siatkę mapowania przestrzennego:

  • W panelu Projekt w folderze Holograms rozwiń obiekt SolarSystem .
  • Kliknij pozycję Ziemia.
  • Na panelu Inspector (Inspektor ) znajdź materiał Ziemi (dolny składnik).
  • Z listy rozwijanej Cieniuj zmień cieniowania na Custom > OcclusionRim. Spowoduje to renderowanie niebieskiego wyróżnienia wokół Ziemi za każdym razem, gdy jest okludniony przez inny obiekt.

Na koniec włączymy efekt widzenia rentgenowskiego dla planet w naszym Układzie Słonecznym. Musimy edytować plik PlanetOcclusion.cs (znajdujący się w folderze Scripts\SolarSystem), aby osiągnąć następujące czynności:

  1. Ustal, czy planeta jest okludiona przez warstwę SpatialMapping (siatki i płaszczyzny pomieszczeń).
  2. Pokaż reprezentację szkieletowej planety za każdym razem, gdy jest on okludniony przez warstwę SpatialMapping.
  3. Ukryj reprezentację szkieletowej planety, gdy nie jest ona blokowana przez warstwę SpatialMapping.

Postępuj zgodnie z ćwiczeniem kodowania w pliku PlanetOcclusion.cs lub użyj następującego rozwiązania:

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

Wdróż i konfiguruj

  • Skompiluj i wdróż aplikację na urządzeniu HoloLens, jak zwykle.
  • Poczekaj na ukończenie skanowania i przetwarzania danych mapowania przestrzennego (powinny pojawić się niebieskie linie na ścianach).
  • Znajdź i wybierz pole projekcji Układu Słonecznego, a następnie ustaw pole obok ściany lub za licznikiem.
  • Podstawowe okluzji można wyświetlić, ukrywając się za powierzchniami, aby zaglądać na plakat lub pole projekcji.
  • Poszukaj Ziemi, powinien istnieć niebieski efekt wyróżnienia za każdym razem, gdy idzie za innym hologramem lub powierzchnią.
  • Obserwuj, jak planety poruszają się za ścianą lub innymi powierzchniami w pokoju. Masz teraz wizję rentgenowskią i widzisz ich szkielety szkieletów szkieletów szkieletowych!

Koniec

Gratulacje! Ukończono tworzenie mapowania przestrzennego MR Spatial 230: Mapowanie przestrzenne.

  • Wiesz, jak skanować środowisko i ładować dane mapowania przestrzennego do aparatu Unity.
  • Poznasz podstawy cieniowania i sposób, w jaki materiały mogą być używane do ponownego wizualizowania świata.
  • Poznaliśmy nowe techniki przetwarzania służące do znajdowania płaszczyzn i usuwania trójkątów z siatki.
  • Udało Ci się poruszać i umieszczać hologramy na powierzchniach, które miały sens.
  • Doświadczyłeś różnych technik okluzji i wykorzystać moc wzroku rentgenowskiego!