HoloLens (första generationen) Spatial 230: Rumslig mappning

Viktigt

Självstudierna Mixed Reality Academy har utformats med HoloLens (1:a gen), Unity 2017 och Mixed Reality Immersive Headsets i åtanke. Därför anser vi att det är viktigt att lämna de här självstudierna på plats för utvecklare som fortfarande letar efter vägledning i utvecklingen för dessa enheter. De här självstudierna uppdateras inte med de senaste verktygen eller interaktionerna som används för HoloLens 2 och kanske inte är kompatibla med nyare versioner av Unity. De kommer att finnas kvar för att fortsätta arbeta med de enheter som stöds. En ny serie självstudier har publicerats för HoloLens 2.

Rumslig mappning kombinerar den verkliga världen och den virtuella världen genom att lära hologram om miljön. I MR Spatial 230 (Project Planet graf) lär vi dig att:

  • Genomsök miljön och överför data från HoloLens till utvecklingsdatorn.
  • Utforska skuggare och lär dig hur du använder dem för att visualisera ditt utrymme.
  • Dela upp rummets nät i enkla plan med hjälp av nätbearbetning.
  • Gå längre än de placeringstekniker som vi har lärt oss i MR Basics 101och ge feedback om var ett hologram kan placeras i miljön.
  • Utforska ocklusionseffekter, så när hologrammet ligger bakom ett verkligt objekt kan du fortfarande se det med x-ray vision!

Stöd för enheter

Kurs HoloLens Integrerande headset
MR Spatial 230: Rumslig mappning ✔️

Innan du börjar

Förutsättningar

Project filer

  • Ladda ned de filer som krävs av projektet. Kräver Unity 2017.2 eller senare.
    • Om du fortfarande behöver stöd för Unity 5.6 använder du den här versionen.
    • Om du fortfarande behöver stöd för Unity 5.5 kan du använda den här versionen.
    • Om du fortfarande behöver stöd för Unity 5.4 använder du den här versionen.
  • Ta bort arkivering av filerna till skrivbordet eller någon annan plats som är lätt att nå.

Anteckning

Om du vill titta igenom källkoden innan du laddar ned den finns den på GitHub.

Kommentarer

  • "Aktivera Just My Code" i Visual Studio måste inaktiveras (avmarkerat) under Verktyg > Alternativ > Felsökning för att komma till brytpunkter i koden.

Unity-konfiguration

  • Starta Unity.
  • Välj Nytt för att skapa ett nytt projekt.
  • Ge projektet namnet Planettoxin.
  • Kontrollera att 3D-inställningen har valts.
  • Klicka på Create Project (Skapa projekt).
  • När Unity har startar går du till Redigera > Project Inställningar > Player.
  • I panelen Kontroll hittar och väljer du den gröna Windows Store-ikonen.
  • Expandera Övrigt Inställningar.
  • I avsnittet Rendering markerar du alternativet Virtual Reality Supported (Virtuell verklighet stöds).
  • Kontrollera att Windows Holographic visas i listan över VIRTUAL Reality-SDK:er. Annars väljer du + knappen längst ned i listan och väljer Windows Holographic.
  • Expandera Publishing Inställningar.
  • I avsnittet Funktioner kontrollerar du följande inställningar:
    • InternetClientServer
    • PrivateNetworkClientServer
    • Mikrofon
    • SpatialPerception
  • Gå till Redigera > Project Inställningar > Kvalitet
  • I panelen Kontroll, under Windows Store-ikonen, väljer du den svarta listrutan under raden Standard och ändrar standardinställningen till Mycket låg.
  • Gå till Assets > Import Package > Custom Package.
  • Gå till mappen ...\HolographicAcademy-Hologram-230-SpatialMapping\Starting.
  • Klicka på Planet unitypackage.
  • Klicka på Öppna.
  • Fönstret Importera Unity-paket bör visas och klicka på knappen Importera.
  • Vänta tills Unity har importerat alla tillgångar som vi behöver för att slutföra projektet.
  • Ta bort huvudkameranpanelen Hierarki.
  • I panelen Project HoloToolkit-SpatialMapping-230\Utilities\Prefabs, hittar du objektet Huvudkamera.
  • Dra och släpp huvudkamerans prefab i hierarkipanelen.
  • Ta bort objektet Directional Light i hierarkipanelen.
  • Leta upp Project i Hologrampanelen.
  • Dra & och släpp markörens prefab i hierarkin.
  • I panelen Hierarki väljer du markörobjektet.
  • I panelen Kontroll klickar du på listrutan Skikt och väljer Redigera lager....
  • Namnge User Layer 31 som "SpatialMapping".
  • Spara den nya scenen: Fil > Spara scen som...
  • Klicka på Ny mapp och ge mappen namnet Scenes.
  • Ge filen namnet "Planetmapp" och spara den i mappen Scenes.

Kapitel 1 – Genomsökning

Mål

  • Lär dig mer om SurfaceObserver och hur dess inställningar påverkar upplevelsen och prestandan.
  • Skapa en rumsgenomsökningsupplevelse för att samla in näten i ditt rum.

Instruktioner

  • I Project HoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs hittar du prefab-verktyget SpatialMapping.
  • Dra & att släppa prefab-programmet SpatialMapping till panelen Hierarki.

Skapa och distribuera (del 1)

  • I Unity väljer du File > Build Inställningar.
  • Klicka på Lägg till öppna scener för att lägga till planetscenen i bygget.
  • Välj Universal Windows Platform (Universell plattform) i listan Platform (Plattform) och klicka på Switch Platform (Växla plattform).
  • Ange SDK till Universal 10 och UWP Build Type till D3D.
  • Kontrollera Unity C#-projekt.
  • Klicka på Skapa.
  • Skapa en ny mapp med namnet "App".
  • Klicka på mappen App.
  • Tryck på knappen Välj mapp.
  • När Unity har byggts klart visas Utforskaren fönster.
  • Dubbelklicka på mappen App för att öppna den.
  • Dubbelklicka på Planetlank.sln för att läsa in projektet i Visual Studio.
  • I Visual Studio du det översta verktygsfältet för att ändra Konfigurationen till Version.
  • Ändra Plattform till x86.
  • Klicka på listrutpilen till höger om Lokal dator och välj Fjärrdator.
  • Ange enhetens IP-adress i fältet Adress och ändra Autentiseringsläge till Universellt (okrypterat protokoll).
  • Klicka på Felsök - > starta utan felsökning eller tryck på Ctrl + F5.
  • Titta på panelen Utdata i Visual Studio för att se status för kom build och deploy.
  • När din app har distribuerats kan du gå runt i rummet. Du ser de omgivande ytorna som täcks av svarta och vita trådramsnät.
  • Sök igenom din miljö. Se till att titta på väggar, golv och golv.

Skapa och distribuera (del 2)

Nu ska vi utforska hur spatial mappning kan påverka prestanda.

  • I Unity väljer du Window > Profiler.
  • Klicka på Lägg till Profiler > GPU.
  • Klicka på Active Profiler >.
  • Ange IP-adressen för HoloLens.
  • Klicka på Anslut.
  • Observera hur många millisekunder det tar för GPU:n att rendera en ram.
  • Stoppa programmet från att köras på enheten.
  • Gå tillbaka till Visual Studio och öppna SpatialMappingObserver.cs. Du hittar den i mappen HoloToolkit\SpatialMapping i Assembly-CSharp -projektet (Universal Windows).
  • Leta upp funktionen Awake() och lägg till följande kodrad: TrianglesPerCuiqMeter = 1200;
  • Distribuera om projektet till enheten och återanslut profileraren. Observera ändringen i antalet millisekunder för att rendera en ram.
  • Stoppa programmet från att köras på enheten.

Spara och läsa in i Unity

Slutligen sparar vi vårt rumsnät och läser in det i Unity.

  • Gå tillbaka Visual Studio och ta bort raden TrianglesPerCuiqMeter som du lade till i funktionen Awake() i föregående avsnitt.
  • Distribuera om projektet till din enhet. Nu bör vi köra med 500 trianglar per meter.
  • Öppna en webbläsare och ange i HoloLens IP-adress för att gå till Windows Enhetsportalen.
  • Välj alternativet 3D-vy i den vänstra panelen.
  • Under Surface-rekonstruering väljer du knappen Uppdatera.
  • Se hur de områden som du har skannat på HoloLens visas i visningsfönstret.
  • Spara rumsgenomsökningen genom att trycka på knappen Spara.
  • Öppna mappen Hämtade filer för att hitta den sparade rumsmodellen SRMesh.obj.
  • Kopiera SRMesh.obj till mappen Assets i ditt Unity-projekt.
  • I Unity väljer du objektet SpatialMappingpanelen Hierarki.
  • Leta upp komponenten Object Surface Observer (Script).
  • Klicka på cirkeln till höger om egenskapen Rumsmodell.
  • Leta upp och välj objektet SRMesh och stäng sedan fönstret.
  • Kontrollera att egenskapen Room Model (Rumsmodell) i panelen Inspector nu är inställd på SRMesh.
  • Tryck på uppspelningsknappen för att gå till Unity-förhandsgranskningsläge.
  • Komponenten SpatialMapping läser in näten från den sparade rumsmodellen så att du kan använda dem i Unity.
  • Växla till scenvyn för att se hela din rumsmodell med wireframe-skuggaren.
  • Tryck på knappen Spela upp igen för att avsluta förhandsgranskningsläget.

OBS! Nästa gång du går in i förhandsgranskningsläget i Unity läses det sparade rumsnätet in som standard.

Kapitel 2 – Visualisering

Mål

  • Lär dig grunderna om skuggare.
  • Visualisera din miljö.

Instruktioner

  • På panelen Hierarki i Unity väljer du objektet SpatialMapping.
  • Leta reda på komponenten Spatial Mapping Manager (Skript) på panelen Kontroll.
  • Klicka på cirkeln till höger om egenskapen Surface Material.
  • Leta upp och välj materialet BlueLinesOnWalls och stäng fönstret.
  • I mappen Project shaders dubbelklickar du på BlueLinesOnWalls för att öppna skuggaren i Visual Studio.
  • Det här är en enkel pixelskärmning (hörn till fragment) som utför följande uppgifter:
    1. Konverterar en brytpunkts plats till world space.
    2. Kontrollerar brytpunktens normala för att avgöra om en pixel är lodrät.
    3. Anger färgen på pixeln för rendering.

Skapa och distribuera

  • Gå tillbaka till Unity och tryck på Spela upp för att gå över till förhandsgranskningsläget.
  • Blå linjer renderas på alla lodräta ytor i rumsnätet (som läses in automatiskt från våra sparade genomsökningsdata).
  • Växla till fliken Scen för att justera vyn av rummet och se hur hela rumsnätet visas i Unity.
  • I Project du mappen Material och väljer materialet BlueLinesOnWalls.
  • Ändra vissa egenskaper och se hur ändringarna visas i Unity-redigeraren.
    • På panelen Kontroll justerar du linescale-värdet så att linjerna ser tunnare eller tunnare ut.
    • På panelen Kontroll justerar du RadperMeter-värdet för att ändra hur många linjer som visas på varje vägg.
  • Klicka på Spela upp igen för att avsluta förhandsgranskningsläget.
  • Skapa och distribuera till HoloLens och observera hur skuggaråtergivningen visas på verkliga ytor.

Unity gör ett bra jobb med att förhandsgranska material, men det är alltid en bra idé att checka ut renderingen på enheten.

Kapitel 3 – Bearbetning

Mål

  • Lär dig tekniker för att bearbeta rumsliga mappningsdata för användning i ditt program.
  • Analysera rumsliga mappningsdata för att hitta plan och ta bort trianglar.
  • Använd plan för hologramplacering.

Instruktioner

  • Leta upp objektet SpatialProcessing Project i Hologram i Unity-panelen.
  • Dra & objektet SpatialProcessing till panelen Hierarki.

Prefab för SpatialProcessing innehåller komponenter för bearbetning av rumsliga mappningsdata. SurfaceMeshesToPlanes.cs hittar och genererar plan baserat på rumsliga mappningsdata. Vi kommer att använda plan i vårt program för att representera väggar, golv och golv. Den här prefaben innehåller även RemoveSurfaceVertices.cs som kan ta bort hörn från det rumsliga kartnätet. Detta kan användas för att skapa hål i nätet eller för att ta bort överflödiga trianglar som inte längre behövs (eftersom plan kan användas i stället).

  • Leta upp SpaceCollection-ProjectHologram i Unity-panelen.
  • Dra och släpp objektet SpaceCollection i hierarkipanelen.
  • På panelen Hierarki väljer du objektet SpatialProcessing.
  • Leta reda på komponenten Play Space Manager (Skript) i kontrollpanelen.
  • Dubbelklicka på PlaySpaceManager.cs för att öppna det i Visual Studio.

PlaySpaceManager.cs innehåller programspecifik kod. Vi kommer att lägga till funktioner i det här skriptet för att aktivera följande beteende:

  1. Sluta samla in data för rumslig mappning när vi har överskridit tidsgränsen för genomsökning (10 sekunder).
  2. Bearbeta data för rumslig mappning:
    1. Använd SurfaceMeshesToPlanes för att skapa en enklare representation av världen som plan (väggar, golv, golv osv.).
    2. Använd RemoveSurfaceVertices för att ta bort yttrianglar som faller inom plangränserna.
  3. Generera en samling hologram i världen och placera dem på vägg- och golvplan nära användaren.

Slutför kodningsövningarna som markerats i PlaySpaceManager.cs eller ersätt skriptet med den färdiga lösningen nedan:

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

Skapa och distribuera

  • Innan du distribuerar till HoloLens trycker du på knappen Spela upp i Unity för att gå till uppspelningsläget.
  • När rumsnätet har lästs in från filen väntar du i 10 sekunder innan bearbetningen startar i det rumsliga kartnätet.
  • När bearbetningen är klar visas plan för att representera golv, väggar, tak osv.
  • När alla plan har hittats bör du se ett solsystem på en golvtabell nära kameran.
  • Två affischer bör också visas på väggar nära kameran. Växla till fliken Scen om du inte kan se dem i spelläge.
  • Tryck på knappen Spela upp igen för att avsluta uppspelningsläget.
  • Skapa och distribuera till HoloLens som vanligt.
  • Vänta tills genomsökningen och bearbetningen av rumsliga mappningsdata har slutförts.
  • När du ser plan kan du försöka hitta solsystemet och affischerna i din värld.

Kapitel 4 – Placering

Mål

  • Ta reda på om ett hologram får plats på en yta.
  • Ge feedback till användaren när ett hologram får/inte får plats på en yta.

Instruktioner

  • I panelen Hierarki i Unity väljer du objektet SpatialProcessing.
  • På panelen Inspector (Kontroll) hittar du komponenten Surface Meshes To Planes (Script).
  • Ändra egenskapen Rita plan till Inget för att rensa valet.
  • Ändra egenskapen Rita plan till Vägg, så att endast väggplan återges.
  • I Project i mappen Skript dubbelklickar du på Placeable.cs för att öppna den i Visual Studio.

Skriptet Placeable (Platsbar) är redan kopplat till de affischer och projektionsrutor som skapas när planets finding har slutförts. Allt vi behöver göra är att avkommentera kod, och det här skriptet uppnår följande:

  1. Ta reda på om ett hologram får plats på en yta genom att raycasting från mitten och fyra hörn av gränskuben.
  2. Kontrollera att ytan är normal för att avgöra om det är tillräckligt jämnt för hologrammet att vara jämnt.
  3. Rendera en avgränsande kub runt hologrammet för att visa dess faktiska storlek när det placeras.
  4. Sätt en skugga under/bakom hologrammet för att visa var det ska placeras på golv/vägg.
  5. Rendera skugga som röd om hologrammet inte kan placeras på ytan, eller grönt, om det går.
  6. Omorientera hologrammet så att det överensstämmer med den yttyp (lodrät eller vågrät) som det har tillhörighet till.
  7. Placera hologrammet smidigt på den valda ytan för att undvika att hoppa eller fästa beteendet.

Avkommentera all kod i kodningsövningen nedan eller använd den här färdiga lösningen i 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;
    }
}

Skapa och distribuera

  • Precis som tidigare skapar du projektet och distribuerar det till HoloLens.
  • Vänta tills genomsökningen och bearbetningen av rumsliga mappningsdata har slutförts.
  • När du ser solsystemet kan du titta på projektionsrutan nedan och utföra en select-gest för att flytta runt den. När projektionsrutan är markerad visas en avgränsande kub runt projektionsrutan.
  • Flytta dig för att titta på en annan plats i rummet. Projektionsrutan bör följa din blick. När skugga under projektionsrutan blir röd kan du inte placera hologrammet på den ytan. När skugga under projektionsrutan blir grön kan du placera hologrammet genom att utföra en annan select-gest.
  • Leta upp och välj en av de holografiska affischerna på en vägg för att flytta den till en ny plats. Observera att du inte kan placera affischen på golv eller tak och att den är korrekt riktad mot varje vägg när du flyttar runt.

Kapitel 5 – Ocklusion

Mål

  • Kontrollera om ett hologram är ockluderat av det rumsliga kartnätet.
  • Använd olika ocklusionstekniker för att uppnå en rolig effekt.

Instruktioner

Först ska vi tillåta att nätet för rumslig mappning kan ockludera andra hologram utan att ockludera den verkliga världen:

  • I panelen Hierarki väljer du objektet SpatialProcessing.
  • Leta reda på komponenten Play Space Manager (skript) på panelen Kontroll.
  • Klicka på cirkeln till höger om egenskapen Sekundärt material.
  • Leta upp och välj Ocklusion-materialet och stäng fönstret.

Nu ska vi lägga till ett särskilt beteende till jorden, så att det får en blå markering när det blir ockluderat av ett annat hologram (som sol) eller av det rumsliga kartnätet:

  • I Project i mappen Hologram expanderar du objektet SolarSystem.
  • Klicka på Earth.
  • panelen Inspector (Kontroll) hittar du jordens material (den nedre komponenten).
  • I listrutan Shader ändrar du skuggaren till Custom > OcklusionRim. Detta renderar en blå markering runt jorden när den är ockluderad av ett annat objekt.

Slutligen ska vi aktivera en ray visionseffekt för planeter i vårt solsystem. Vi måste redigera PlanetOcclusion.cs (finns i mappen Scripts\SolarSystem) för att uppnå följande:

  1. Fastställ om en planet är ockluderad av SpatialMapping-skiktet (rumsnät och plan).
  2. Visa wireframe-representationen av en planet när den är ockluderad av SpatialMapping-skiktet.
  3. Dölj wireframe-representationen av en planet när den inte blockeras av SpatialMapping-lagret.

Följ kodningsövningen i PlanetOcclusion.cs eller använd följande lösning:

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

Skapa och distribuera

  • Skapa och distribuera programmet till HoloLens som vanligt.
  • Vänta tills genomsökningen och bearbetningen av data för rumslig mappning är klar (du bör se blå linjer på väggar).
  • Leta upp och välj projektionsrutan för solsystemet och ställ sedan in rutan bredvid en vägg eller bakom en räknare.
  • Du kan visa grundläggande ocklusion genom att dölja bakom ytor för peer vid affischen eller projektionsrutan.
  • Leta efter jorden. Det bör finnas en blå markeringseffekt när den hamnar bakom ett annat hologram eller en annan yta.
  • Se när planeterna rör sig bakom väggen eller andra ytor i rummet. Nu har du en x-ray-syn och kan se deras wireframe-stomme!

Slutet

Grattis! Nu har du slutfört MR Spatial 230: Spatial mapping.

  • Du vet hur du genomsöker din miljö och läser in rumsliga mappningsdata till Unity.
  • Du förstår grunderna för skuggare och hur material kan användas för att visualisera om världen.
  • Du har lärt dig nya bearbetningstekniker för att hitta plan och ta bort trianglar från ett nät.
  • Du kunde flytta och placera hologram på ytor som var meningsfulla.
  • Du har haft olika ocklusionstekniker och utnyttja kraften hos x-ray vision!