HoloLens (1. generace) Spatial 230: Prostorové mapování

Důležité

Kurzy Mixed Reality Academy byly navrženy pro HoloLens (1. generace), Unity 2017 a Mixed Reality imerzivní náhlavní soupravy. Proto si myslíme, že je důležité ponechat tyto kurzy na místě pro vývojáře, kteří stále hledají pokyny při vývoji těchto zařízení. Tyto kurzy nebudou aktualizovány nejnovějšími sady nástrojů nebo interakcemi používanými pro HoloLens 2 a nemusí být kompatibilní s novějšími verzemi Unity. Budou zachovány, aby pokračovaly v práci na podporovaných zařízeních. V roce 2 byla publikována nová série kurzů HoloLens 2.

Prostorové mapování kombinuje reálný a virtuální svět tím, že vyučuje hologramy o prostředí. V MR Spatial 230 (Project Planetarium) se naučíme:

  • Prohledat prostředí a přenést data z HoloLens do vývojového počítače.
  • Prozkoumejte shadery a zjistěte, jak je používat k vizualizaci prostoru.
  • Pomocí zpracování sítě můžete rozdělit síť místností na jednoduché roviny.
  • Přejděte nad rámec technik umístění, které jsme se naučili v mr. Basics 101,a poskytněte nám zpětnou vazbu ohledně umístění hologramu v prostředí.
  • Prozkoumejte efekt okluze, takže když je hologram za skutečným objektem, můžete ho stále vidět pomocí x-ray vision!

Podpora zařízení

Kurz HoloLens Imerzivní náhlavní soupravy
MR Spatial 230: Prostorové mapování ✔️

Než začnete

Požadavky

Project souborů

  • Stáhněte si soubory vyžadované projektem. Vyžaduje Unity 2017.2 nebo novější.
  • Soubory můžete archivovat na plochu nebo na jiné snadno dosažitelné místo.

Poznámka

Pokud si chcete před stažením projít zdrojový kód, je k dispozici na GitHub.

Poznámky

  • Možnost Povolit Pouze můj kód v Visual Studiomusíbýt zakázaná (nezaškrtnutá) v části Nástroje Možnosti Ladění, aby se ve vašem kódu dostaly na > zarážky.

Nastavení Unity

  • Spusťte Unity.
  • Vyberte Nový a vytvořte nový projekt.
  • Projekt pojmechnte Planetarium.
  • Ověřte, že je vybrané nastavení 3D.
  • Klikněte na Create Project (Vytvořit projekt).
  • Po spuštění Unity přejděte na Edit Project Nastavení Player ( Upravit Project Nastavení > Playeru).
  • Na panelu inspektoru vyhledejte a vyberte zelenou ikonu Windows Store (Obchod).
  • Rozbalte další Nastavení.
  • V části Rendering (Vykreslování) zaškrtněte možnost Virtual Reality Supported (Podpora virtuální reality).
  • Ověřte, Windows se v seznamu sdk virtuální reality zobrazí holographic. Pokud ne, vyberte tlačítko v dolní části seznamu a + zvolte +.
  • Rozbalte položku Publishing Nastavení.
  • V části Schopnosti zkontrolujte následující nastavení:
    • InternetClientServer
    • PrivateNetworkClientServer
    • Mikrofon
    • SpatialPerception
  • Přejděte na Upravit Project Nastavení > kvality.
  • Na panelu inspektoru pod ikonou Windows Store (Obchod) vyberte černou šipku rozevíracího seznamu pod řádkem Default (Výchozí) a změňte výchozí nastavení na Very Low (Velmi nízká).
  • Přejděte do části Assets Import Package Custom > Package (Vlastní balíček importu prostředků).
  • Přejděte do složky ...\HolographicAcademy-Hologramy-230-SpatialMapping\Starting.
  • Klikněte na Planetarium.unitypackage.
  • Klikněte na Otevřít.
  • Mělo by se zobrazit okno Import Unity Package (Importovat balíček Unity) a klikněte na tlačítko Import (Importovat).
  • Počkejte, až Unity naimportuje všechny prostředky, které budeme potřebovat k dokončení tohoto projektu.
  • Na panelu Hierarchie odstraňte hlavní kameru.
  • Na panelu ProjectHoloToolkit-SpatialMapping-230\Utilities\Prefabs vyhledejte objekt Hlavní kamera.
  • Přetáhněte předfab Hlavní kamera na panel Hierarchie.
  • Na panelu Hierarchy (Hierarchie) odstraňte objekt Directional Light (Směrové světlo).
  • Na Projectpanelu Hologramy vyhledejte objekt Kurzor.
  • &Přetáhněte & Kurzor do hierarchie.
  • Na panelu Hierarchie vyberte objekt Kurzor.
  • Na panelu inspektoru klikněte na rozevírací seznam Vrstva a vyberte Upravit vrstvy....
  • Vrstvu uživatele 31 pojmnteSpatialMapping.
  • Save the new scene: File Save Scene As...
  • Klikněte na New Folder (Nová složka) a pojmnte složku Scenes (Scény).
  • Pojmete souborPlanetariuma uložte ho do složky Scenes.

1. kapitola – Prohledávání

Cíle

  • Přečtěte si o serveru SurfaceOb a o tom, jak jeho nastavení ovlivňuje prostředí a výkon.
  • Vytvořte prostředí pro kontrolu místnosti, které bude shromažďovat sítě místnosti.

Pokyny

  • Na panelu ProjectHoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs vyhledejte předfab SpatialMapping.
  • &Přetáhněte & na panel Hierarchie.

Sestavení a nasazení (část 1)

  • V Unity vyberte File Build Nastavení.
  • Kliknutím na Add Open Scenes (Přidat otevřené scény) přidejte scénu Planetarium do sestavení.
  • V seznamu Platforma vyberte Windows platformy a klikněte na Přepnout platformu.
  • Nastavte sadu SDKna Univerzální 10 a Typ sestavení UPW na D3D.
  • Zkontrolujte projekty Unity v jazyce C#.
  • Klikněte na Build (Sestavit).
  • Vytvořte novou složku s názvem "App".
  • Klikněte na složku Aplikace.
  • Stiskněte tlačítko Vybrat složku.
  • Po sestavení Unity se Průzkumník souborů okno.
  • Poklikejte na složku Aplikace a otevřete ji.
  • Poklikejte na Soubor Planetarium.sln a načtěte projekt v Visual Studio.
  • V Visual Studio horním panelu nástrojů změňte konfiguraci na Release (Verze).
  • Změňte platformu na x86.
  • Klikněte na šipku rozevíracího seznamu napravo od ' místní počítač ' a vyberte možnost vzdálený počítač.
  • Do pole Adresa zadejte IP adresu vašeho zařízení a změňte režim ověřování na univerzální (nešifrovaný protokol).
  • Klikněte na ladit – Spustit bez ladění nebo stiskněte CTRL + F5.
  • podívejte se na panel výstup v Visual Studio pro stav sestavení a nasazení.
  • Jakmile se vaše aplikace nasadí, Projděte si místnost. Zobrazí se okolní povrchy, které jsou pokryté černými a bílými drátěnými oky.
  • Naskenujte své okolí. Nezapomeňte se podívat na zdi, stropy a podlahy.

Sestavení a nasazení (část 2)

Nyní se podívejme, jak prostorové mapování může ovlivnit výkon.

  • V Unity vyberte Window Profiler.
  • Klikněte na Přidat GPU profileru.
  • Klikněte na aktivní Profiler .
  • Zadejte IP adresu vašeho HoloLens.
  • Klikněte na Připojit.
  • Sledujte, kolik milisekund trvá, než GPU vykreslí snímek.
  • Zastavte spuštění aplikace v zařízení.
  • vraťte se do Visual Studio a otevřete SpatialMappingObserver. cs. najdete ho ve složce HoloToolkit\SpatialMapping projektu Assembly-CSharp (Universal Windows).
  • Najděte funkci spánku () a přidejte následující řádek kódu: TrianglesPerCubicMeter = 1200;
  • Znovu nasaďte projekt do zařízení a pak znovu Připojte profiler. Sledujte změnu v počtu milisekund pro vykreslení snímku.
  • Zastavte spuštění aplikace v zařízení.

Uložení a načtení v Unity

Nakonec ušetříme místnost místnosti a nahrajeme ji do Unity.

  • vraťte se do Visual Studio a odeberte řádek TrianglesPerCubicMeter , který jste přidali do funkce functions () během předchozí části.
  • Znovu nasaďte projekt do svého zařízení. Teď by měl být spuštěný s 500 trojúhelníky na měřič krychle.
  • otevřete prohlížeč a zadáním HoloLens IPAddress do Windows portálu zařízenípřejděte na portál.
  • Vyberte možnost 3D zobrazení na levém panelu.
  • V části rekonstrukce povrchu vyberte tlačítko aktualizovat .
  • sledujte, jak se v okně zobrazení zobrazí oblasti, které jste kontrolovali HoloLens.
  • Pokud chcete kontrolu místnosti uložit, stiskněte tlačítko Uložit .
  • Otevřete složku ke stažení , kde najdete uložený model místnosti SRMesh. obj.
  • Zkopírujte SRMesh. obj do složky assets vašeho projektu Unity.
  • V části Unity vyberte objekt SpatialMapping na panelu hierarchie .
  • Najděte komponentu objektu pozorovatele Surface (skript) .
  • Klikněte na kroužek napravo od vlastnosti model místnosti .
  • Vyhledejte a vyberte objekt SRMesh a zavřete okno.
  • Ověřte, zda je vlastnost model místnosti v panelu kontrolora nastavena na hodnotu SRMesh.
  • Stisknutím tlačítka Přehrát zadáte režim náhledu v Unity.
  • Komponenta SpatialMapping načte sítě z uloženého modelu místnosti, abyste je mohli použít v Unity.
  • Přepněte do zobrazení scéna a zobrazte si všechny modely místností zobrazené pomocí shaderu drátěného modelu.
  • Znovu stiskněte tlačítko Přehrát a ukončete režim náhledu.

Poznámka: Až příště v Unity zadáte režim náhledu, načte se ve výchozím nastavení uložená místnost místnosti.

Kapitola 2 – vizualizace

Cíle

  • Seznamte se se základy shaderů.
  • Vizualizujte své okolí.

Pokyny

  • V panelu hierarchie Unity vyberte objekt SpatialMapping .
  • V panelu Kontrola najděte komponentu správce prostorového mapování (skript) .
  • Klikněte na kroužek napravo od vlastnosti Materiál povrchu .
  • Vyhledejte a vyberte materiál BlueLinesOnWalls a zavřete okno.
  • ve složce Project panelů shaderů poklikejte na BlueLinesOnWalls a otevře se shader v Visual Studio.
  • Toto je jednoduchý pixel (vrchol ke fragmentu), který provádí následující úlohy:
    1. Převede umístění vrcholu na světový prostor.
    2. Zkontroluje normální vrchol a určí, jestli je pixel svisle.
    3. Nastaví barvu pixelu pro vykreslování.

Sestavení a nasazení

  • Vraťte se do Unity a stisknutím tlačítka Přehrát přejděte do režimu náhledu.
  • Modré čáry budou vykresleny na všech svislých površích sítě místnosti (které se automaticky načítají z našich uložených dat kontroly).
  • Přepněte na kartu scéna a upravte zobrazení místnosti a podívejte se, jak se v Unity zobrazuje celá místnost místnosti.
  • na panelu Project najděte složku materiály a vyberte materiál BlueLinesOnWalls .
  • Upravte některé vlastnosti a podívejte se, jak se změny objeví v editoru Unity.
    • V panelu inspektora upravte hodnotu LineScale , aby se čáry zobrazovaly tlustší nebo užší.
    • V panelu inspektora upravte hodnotu LinesPerMeter , abyste změnili, kolik řádků se má na jednotlivých stěnách.
  • Znovu klikněte na tlačítko Přehrát a ukončete režim náhledu.
  • sestavte a nasaďte do HoloLens a sledujte, jak se vykreslování shaderu zobrazuje na skutečných površích.

Unity nabízí skvělou úlohu prohlížení materiálů, ale je vždycky dobré vykreslovat vykreslování v zařízení.

Kapitola 3 – zpracování

Cíle

  • Naučte se techniky zpracování dat prostorového mapování pro použití ve vaší aplikaci.
  • Analyzujte data prostorového mapování, abyste našli roviny a odebrali trojúhelníky.
  • Používejte roviny pro umísťování hologramů.

Pokyny

  • v Project panelu Unity Hologramy složce najděte objekt SpatialProcessing .
  • Přetáhněte & objekt & na panel hierarchie .

SpatialProcessing Prefab obsahuje komponenty pro zpracování dat prostorového mapování. SurfaceMeshesToPlanes. cs vyhledá a vygeneruje roviny na základě dat prostorového mapování. K reprezentaci zdí, podlah a stropů použijeme roviny v naší aplikaci. Tento Prefab také obsahuje RemoveSurfaceVertices. cs , který umožňuje odebrat vrcholy z mřížky mapování prostorových map. Dá se použít k vytvoření děr v síti nebo k odebrání přebytečných trojúhelníků, které už nepotřebujete (protože místo toho se dají použít roviny).

  • v Project panelu Unity Hologramy složce najděte objekt spacecollection .
  • Přetáhněte objekt prostorcollection do panelu hierarchie .
  • Na panelu hierarchie vyberte objekt SpatialProcessing .
  • V panelu Kontrola najděte komponentu správce prostoru přehrávání (skript) .
  • Dvojím kliknutím na PlaySpaceManager. cs ho otevřete v Visual Studio.

PlaySpaceManager. cs obsahuje kód specifický pro aplikaci. Do tohoto skriptu přidáme funkce, aby bylo možné povolit následující chování:

  1. Po překročení časového limitu kontroly (10 sekund) zastavte shromažďování dat prostorového mapování.
  2. Zpracování dat prostorového mapování:
    1. SurfaceMeshesToPlanes můžete použít k vytvoření jednodušší reprezentace světa jako roviny (zdi, podlaha, stropy atd.).
    2. Pomocí RemoveSurfaceVertices můžete odebrat Surface trojúhelníků, které spadají do hranic vrstev.
  3. Vygenerujte kolekci hologramů na světě a umístěte je na stěnu a podlahovou rovinu poblíž uživatele.

Dokončete programovací cvičení označené v PlaySpaceManager. cs nebo nahraďte skript pomocí dokončeného řešení níže:

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

Sestavení a nasazení

  • před nasazením do HoloLens stiskněte tlačítko přehrát v Unity a zadejte režim přehrávání.
  • Po načtení sítě místnosti ze souboru počkejte 10 sekund, než se zpracování začne na mřížce prostorového mapování.
  • Po dokončení zpracování se roviny zobrazí jako podlaha, stěny, strop atd.
  • Po nalezení všech ploch byste měli vidět, že se v tabulce patra v blízkosti kamery zobrazuje sluneční systém.
  • Na stěnách v blízkosti kamery by se měly zobrazit dvě plakáty. Pokud je nevidíte v herním režimu, přepněte na kartu scéna .
  • Stisknutím tlačítka Přehrát znovu Ukončete režim přehrávání.
  • sestavte a nasaďte do HoloLens, jako obvykle.
  • Počkejte na dokončení kontroly a zpracování dat prostorového mapování.
  • Až uvidíte plochy, zkuste najít sluneční systém a plakáty na světě.

Kapitola 4 – umístění

Cíle

  • Určí, jestli se na Surface vejde hologram.
  • Poskytněte uživateli zpětnou vazbu, když hologram může nebo nemůže být přizpůsoben na povrchu.

Pokyny

  • V panelu hierarchie Unity vyberte objekt SpatialProcessing .
  • Na panelu inspektora Najděte plochu plochy na součást rovin (skript) .
  • Chcete-li vymazat výběr, změňte vlastnost Draw roviny na hodnotu Nothing .
  • Změňte vlastnost Draw roviny na zeď, aby se vykreslily jenom plochy na stěnách.
  • na panelu Project , ve složce scripts , dvakrát klikněte na umístitelné. cs , aby se otevřel v Visual Studio.

Umístitelný skript je již připojen k poli plakáty a projekce, které jsou vytvořeny po dokončení zjištění roviny. Vše, co musíme udělat, je zrušit komentář k některému kódu a tento skript bude mít následující:

  1. Určete, jestli se na Surface vejde hologram raycasting ze středu a čtyř rohů ohraničující datové krychle.
  2. Zkontrolujte normální povrch, abyste zjistili, jestli je dostatečně plynulý, aby se pomohlo vyprázdnit na hologram.
  3. Vykreslit ohraničující datovou krychli kolem hologramu, aby se zobrazila jeho skutečná velikost, i když se umístí.
  4. Přetypování stínu pod hologramem, aby se zobrazilo umístění na podlaze/zeď.
  5. Vykreslí stín jako červený, pokud hologram nemůžete umístit na plochu nebo zelenou, pokud je to možné.
  6. Nastavte znovu orientaci hologramu tak, aby odpovídala typu povrchu (svisle nebo vodorovně), ke kterému má spřažení.
  7. Hladce umístěte hologram na vybrané ploše, abyste se vyhnuli chování skoku nebo přichycení.

Odkomentujte veškerý kód v následujícím cvičení kódování nebo použijte toto dokončené řešení . 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;
    }
}

Sestavení a nasazení

  • Stejně jako dřív Sestavte projekt a nasaďte ho do HoloLens.
  • Počkejte na dokončení kontroly a zpracování dat prostorového mapování.
  • Po zobrazení slunečního systému, pohledu v níže uvedeném poli projekce a provedení gesta pro výběr k přesunutí kolem. Když je vybráno pole projekce, zobrazí se kolem pole projekce ohraničující datová krychle.
  • Přesuňte hlavu na jiné místo v místnosti. Pole projekce by mělo následovat po vaší pohledu. Když se stín pod polem projekce změní na červenou, nemůžete na tuto plochu umístit hologram. Když se stín pod polem projekce změní na zelený, můžete hologram umístit pomocí jiného gesta pro výběr.
  • Najděte a vyberte jeden z holografických plakátů na zdi, abyste ho přesunuli na nové místo. Všimněte si, že plakáty nemůžete umístit na podlahovou hranici nebo strop a že při přesunu zůstane správně orientovaný na každou zeď.

Kapitola 5 – překrytí

Cíle

  • Určí, jestli je hologram zastíněna pomocí sítě prostorového mapování.
  • K dosažení zábavného efektu použijte různé techniky překrytí.

Pokyny

Nejdřív teď umožníme mřížce prostorového mapování, aby occlude další hologramy bez occluding reálného světa:

  • Na panelu hierarchie vyberte objekt SpatialProcessing .
  • V panelu Kontrola najděte komponentu správce prostoru přehrávání (skript) .
  • Klikněte na kroužek napravo od vlastnosti vedlejší materiál .
  • Vyhledejte a vyberte materiál překrytí a zavřete okno.

V dalším kroku přidáme speciální chování do země, aby byl modrý, kdykoli se zastíněna na jiný hologram (například slunce), nebo na mřížku prostorových mapování:

  • na panelu Project ve složce Hologramy rozbalte objekt SolarSystem .
  • Klikněte na země.
  • V panelu Kontrola Najděte materiál země (dolní součást).
  • V rozevíracím seznamu shaderzměňte shader na Custom OcclusionRim. Vykreslí se modrý zvýraznění kolem země, kdykoli je zastíněna jiným objektem.

Nakonec budeme pro Planet v našem slunečním systému povolit efekt pro zpracování obrazu x-ray. Aby bylo možné dosáhnout těchto kroků, bude nutné upravit PlanetOcclusion. cs (nalezeno ve složce Scripts\SolarSystem):

  1. Určí, zda je globálním zastíněna vrstvou SpatialMapping (místnostmi a rovinami).
  2. Umožňuje zobrazit znázornění globálnímového modelu pokaždé, když je zastíněna vrstvou SpatialMapping.
  3. Skryje drátěný reprezentace globálním, pokud není blokována vrstvou SpatialMapping.

Postupujte podle programovacího cvičení v PlanetOcclusion. cs nebo použijte následující řešení:

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

Sestavení a nasazení

  • sestavte a nasaďte aplikaci do HoloLens, a to obvyklým způsobem.
  • Počkejte na dokončení kontroly a zpracování dat prostorového mapování (měly by se zobrazit modré čáry na stěnách).
  • Vyhledejte a vyberte pole projekce slunečního systému a pak nastavte pole vedle zdi nebo za čítač.
  • Základní překrytí můžete zobrazit skrytím za povrchy do partnerského zařízení v poli plakát nebo projekce.
  • Vyhledejte zemi, měli byste mít modrý efekt zvýraznění vždy, když se dostanou za jiný hologram nebo povrch.
  • Sledujte, jak se Planet pohyb za stěnou nebo jinými povrchy v místnosti. Teď máte k dispozici zrakovou vizi a vidíte jejich kostry drátů.

Konec

Gratulujeme! Nyní jste dokončili pan prostor 230: prostorové mapování.

  • Víte, jak prohledávat prostředí a načíst data mapování prostorů do Unity.
  • Rozumíte základům shaderů a způsobu, jakým se dají materiály použít k opětovné vizualizaci světa.
  • Naučili jste se nové techniky zpracování pro hledání rovin a odstraňování trojúhelníků z sítě.
  • Mohli byste přesunout a umístit hologramy na povrchy, které byly smyslem.
  • Zjistili jste různé techniky překrytí a Využijte sílu x-ray visionu.