Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Viktigt
Självstudierna Mixed Reality Academy har utformats med HoloLens (första generationen), Unity 2017 och Mixed Reality integrerande headset i åtanke. Därför tycker vi att det är viktigt att låta de här självstudierna vara kvar för utvecklare som fortfarande behöver vägledning för att utveckla för dessa enheter. De här självstudierna uppdateras inte med de senaste verktygsuppsättningarna eller interaktionerna som används för HoloLens 2 och kanske inte är kompatibla med nyare versioner av Unity. De kommer att underhållas för att fortsätta arbeta på 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 tillsammans genom att lära hologram om miljön. I MR Spatial 230 (Project Planetarium) lär vi oss att:
- Skanna 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 rumsnätet i enkla plan med hjälp av nätbearbetning.
- Gå längre än de placeringstekniker som vi har lärt oss i MR Basics 101 och ge feedback om var ett hologram kan placeras i miljön.
- Utforska ocklusionseffekter, så när ditt hologram ligger bakom ett verkligt objekt kan du fortfarande se det med röntgenseende!
Stöd för enheter
| Kurs | HoloLens | Integrerande headset |
|---|---|---|
| MR Spatial 230: Rumslig mappning | ✔️ |
Innan du börjar
Förutsättningar
- En Windows 10 dator som konfigurerats med rätt verktyg installerade.
- Vissa grundläggande C#-programmeringskunskaper.
- Du bör ha slutfört MR Basics 101.
- En HoloLens-enhet som konfigurerats för utveckling.
Projektfiler
- 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 använder du den här versionen.
- Om du fortfarande behöver stöd för Unity 5.4 använder du den här versionen.
- Avarkivera filerna på skrivbordet eller på en annan plats som är lätt att nå.
Anteckning
Om du vill titta igenom källkoden innan du laddar ned den är den tillgänglig på GitHub.
Kommentarer
- "Aktivera Just My Code" i Visual Studio måste inaktiveras (avmarkerat) under Verktyg Alternativ >> Felsökning för att nå brytpunkter i koden.
Unity-konfiguration
- Starta Unity.
- Välj Ny för att skapa ett nytt projekt.
- Ge projektet namnet Planetarium.
- Kontrollera att 3D-inställningen är markerad.
- Klicka på Create Project (Skapa projekt).
- När Unity har lanserats går du till Redigera > projektinställningar > spelare.
- I panelen Inspector letar du upp och väljer den gröna Windows Store-ikonen .
- Expandera Andra inställningar.
- I avsnittet Rendering kontrollerar du alternativet Virtuell verklighet som stöds .
- Kontrollera att Windows Holographic visas i listan över SDK:er för virtuell verklighet. Annars väljer du + knappen längst ned i listan och väljer Windows Holographic.
- Expandera Publiceringsinställningar.
- I avsnittet Funktioner kontrollerar du följande inställningar:
- InternetClientServer
- PrivateNetworkClientServer
- Mikrofon
- SpatialPerception
- Gå till Redigera > projektinställningar > Kvalitet
- Välj den svarta nedrullningsbara pilen under raden Standard under ikonen Windows Store på panelen Kontroll och ändra standardinställningen till Mycket låg.
- Gå till Anpassat paket för tillgångsimportpaket >>.
- Gå till mappen ...\HolographicAcademy-Holograms-230-SpatialMapping\Starting .
- Klicka på Planetarium.unitypackage.
- Klicka på Öppna.
- Fönstret Importera Unity-paket bör visas. Klicka på knappen Importera .
- Vänta tills Unity importerar alla tillgångar som vi behöver för att slutföra projektet.
- Ta bort huvudkameran på panelen Hierarki.
- I projektpanelen, i mappen HoloToolkit-SpatialMapping-230\Utilities\Prefabs, letar du upp huvudkameraobjektet.
- Dra och släpp huvudkamerans prefab i hierarkipanelen .
- Ta bort objektet Directional Light på panelen Hierarki.
- Leta upp markörobjektet i mappen Holograms på projektpanelen.
- Dra & släpp prefab-markören i hierarkin.
- I panelen Hierarki väljer du markörens objekt.
- I panelen Kontroll klickar du på listrutan Lager och väljer Redigera lager....
- Namnge User Layer 31 som "SpatialMapping".
- Spara den nya scenen: Spara > scen som...
- Klicka på Ny mapp och namnge mappen Scener.
- Ge filen namnet "Planetarium" och spara den i mappen Scener .
Kapitel 1 – Genomsökning
Mål
- Lär dig mer om SurfaceObserver och hur dess inställningar påverkar upplevelse och prestanda.
- Skapa en rumsgenomsökningsupplevelse för att samla in ditt rums nät.
Instruktioner
- I projektpanelenHoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs letar du upp prefab för SpatialMapping .
- Dra & släppa prefab för SpatialMappingi hierarkipanelen .
Skapa och distribuera (del 1)
- I Unity väljer du Inställningar för filbygge>.
- Klicka på Lägg till öppna scener för att lägga till Planetarium-scenen i bygget.
- Välj Universell Windows-plattform i listan Plattform och klicka på 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 skapats visas ett Utforskaren fönster.
- Dubbelklicka på mappen App för att öppna den.
- Dubbelklicka på Planetarium.sln för att läsa in projektet i Visual Studio.
- I Visual Studio använder du det översta verktygsfältet för att ändra konfigurationen till Släpp.
- Ändra Plattformen till x86.
- Klicka på listrutepilen till höger om "Lokal dator" och välj Fjärrdator.
- Ange enhetens IP-adress i fältet Adress och ändra autentiseringsläget till Universellt (okrypterat protokoll).
- Klicka på Felsök –> Starta utan att felsöka eller tryck på Ctrl + F5.
- Titta på utdatapanelen i Visual Studio för att få statusen skapa och distribuera.
- När appen har distribuerats går du runt i rummet. Du ser de omgivande ytorna som täcks av svarta och vita trådramsnät.
- Skanna din omgivning. Se till att titta på väggar, tak och golv.
Skapa och distribuera (del 2)
Nu ska vi utforska hur spatial mappning kan påverka prestanda.
- I Unity väljer du Fönsterprofilerare>.
- Klicka på Lägg till Profiler > GPU.
- Klicka på Aktiv profilerare ><Ange IP>.
- Ange IP-adressen för din HoloLens.
- Klicka på Anslut.
- Observera antalet millisekunder som krävs för att GPU:n ska återge 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 projektet Assembly-CSharp (Universal Windows).
- Hitta funktionen Awake() och lägg till följande kodrad: TrianglesPerCubicMeter = 1200;
- Distribuera om projektet till enheten och återanslut sedan profileraren. Observera ändringen i antalet millisekunder för att återge en ram.
- Stoppa programmet från att köras på enheten.
Spara och läs in i Unity
Slutligen ska vi spara vårt rumsnät och läsa in det i Unity.
- Gå tillbaka till Visual Studio och ta bort linjen TrianglesPerCubicMeter som du lade till i funktionen Awake() under föregående avsnitt.
- Distribuera om projektet till enheten. Vi bör nu köra med 500 trianglar per kubikmeter.
- Öppna en webbläsare och ange i din HoloLens IPAddress för att navigera till Windows-enhetsportalen.
- Välj alternativet 3D-vy i den vänstra panelen.
- Under Surface-återuppbyggnad väljer du knappen Uppdatera .
- Se hur de områden som du har skannat på HoloLens visas i visningsfönstret.
- Om du vill spara rumsgenomsökningen trycker du på knappen Spara .
- Öppna mappen Nedladdningar för att hitta den sparade rumsmodellen SRMesh.obj.
- Kopiera SRMesh.obj till mappen Assets i unity-projektet.
- I Unity väljer du SpatialMapping-objektetpå hierarkipanelen .
- Leta upp komponenten Object Surface Observer (Script).
- Klicka på cirkeln till höger om egenskapen Rumsmodell .
- Leta upp och välj SRMesh-objektet och stäng sedan fönstret.
- Kontrollera att egenskapen Room Model i panelen Inspector nu är inställd på SRMesh.
- Tryck på knappen Spela upp för att ange Unitys förhandsgranskningsläge.
- SpatialMapping-komponenten 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 alla rumsmodeller som visas med wireframe-skuggningen.
- Tryck på knappen Spela upp igen för att avsluta förhandsgranskningsläget.
OBSERVERA: Nästa gång du anger förhandsgranskningsläge i Unity läses det sparade rumsnätet in som standard.
Kapitel 2 – Visualisering
Mål
- Lär dig grunderna i skuggor.
- Visualisera din omgivning.
Instruktioner
- I Unitys hierarkipanel väljer du spatialmappningsobjektet .
- Leta reda på komponenten Spatial Mapping Manager (Script) på panelen Inspector.
- Klicka på cirkeln till höger om egenskapen Surface Material .
- Leta upp och välj BlueLinesOnWalls-materialet och stäng fönstret.
- Dubbelklicka på BlueLinesOnWalls i mappen Project panel Shaders för att öppna skuggningen i Visual Studio.
- Detta är en enkel pixel (hörn till fragment) skuggning, som utför följande uppgifter:
- Konverterar ett hörns plats till världsrymden.
- Kontrollerar hörnets normala för att avgöra om en pixel är lodrät.
- Anger färg på pixeln för återgivning.
Skapa och distribuera
- Gå tillbaka till Unity och tryck på Spela upp för att ange förhandsgranskningsläge.
- Blå linjer återges på alla lodräta ytor i rummets nät (som automatiskt läses in från våra sparade genomsökningsdata).
- Växla till fliken Scen för att justera din vy över rummet och se hur hela rumsnätet visas i Unity.
- I projektpanelen letar du upp mappen Material och väljer materialet BlueLinesOnWalls .
- Ändra vissa egenskaper och se hur ändringarna visas i Unity-redigeraren.
- På panelen Kontroll justerar du värdet LineScale så att linjerna ser tjockare eller tunnare ut.
- I panelen Kontroll justerar du värdet LinesPerMeter 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 skuggningså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 rendering på enheten.
Kapitel 3 – Bearbetning
Mål
- Lär dig tekniker för att bearbeta spatial mappningsdata för användning i ditt program.
- Analysera spatial mappningsdata för att hitta plan och ta bort trianglar.
- Använd plan för hologramplacering.
Instruktioner
- I Unitys projektpanel, mappen Holograms, letar du upp spatialprocessing-objektet.
- Dra & släppa SpatialProcessing-objektet i hierarkipanelen .
Prefab för SpatialProcessing innehåller komponenter för bearbetning av rumsliga mappningsdata. SurfaceMeshesToPlanes.cs hittar och genererar plan baserat på spatial mappningsdata. Vi kommer att använda flygplan i vårt program för att representera väggar, golv och tak. Den här prefab innehåller även RemoveSurfaceVertices.cs som kan ta bort hörn från det rumsliga mappningsnä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 flygplan kan användas i stället).
- I Unitys projektpanel, hologrammappen, letar du upp Objektet SpaceCollection.
- Dra och släpp Objektet SpaceCollection i hierarkipanelen .
- På panelen Hierarki väljer du objektet SpatialProcessing .
- Leta reda på komponenten Play Space Manager (Script) i panelen Inspector.
- 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:
- Sluta samla in rumsliga mappningsdata när vi överskrider genomsökningstiden (10 sekunder).
- Bearbeta rumsliga mappningsdata:
- Använd SurfaceMeshesToPlanes för att skapa en enklare representation av världen som flygplan (väggar, golv, tak osv.).
- Använd RemoveSurfaceVertices för att ta bort yttrianglar som faller inom plangränser.
- 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å in i uppspelningsläge.
- När rumsnätet har lästs in från filen väntar du i 10 sekunder innan bearbetningen startar på det rumsliga mappningsnätet.
- När bearbetningen är klar ser planen ut att representera golvet, väggarna, taket osv.
- När alla plan har hittats bör du se ett solsystem visas på ett golvbord 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 data för rumslig mappning har slutförts.
- När du ser flygplan, försök att hitta solsystemet och affischer i din värld.
Kapitel 4 – Placering
Mål
- Kontrollera om ett hologram får plats på en yta.
- Ge feedback till användaren när ett hologram kan/inte får plats på en yta.
Instruktioner
- Välj SpatialProcessing-objektet i Unitys hierarkipanel.
- Leta reda på komponenten Surface Meshes To Planes (Skript) på panelen Inspector.
- Ändra egenskapen Rita plan till Ingenting för att rensa markeringen.
- Ändra egenskapen Draw Planes till Wall så att endast väggplan återges.
- Dubbelklicka på Placeable.cs i mappen Skript i projektpanelen för att öppna den i Visual Studio.
Placeable-skriptet är redan kopplat till affischerna och projektionsrutan som skapas när plansökningen har slutförts. Allt vi behöver göra är att avkommenteras med kod, och det här skriptet uppnår följande:
- Kontrollera om ett hologram får plats på en yta genom raycasting från mitten och fyra hörn av avgränsningskuben.
- Kontrollera ytnormalt för att avgöra om det är tillräckligt jämnt för att hologrammet ska sitta på.
- Rendera en avgränsningskub runt hologrammet för att visa dess faktiska storlek när den placeras.
- Kasta en skugga under/bakom hologrammet för att visa var den ska placeras på golvet/väggen.
- Rendera skuggan som röd, om hologrammet inte kan placeras på ytan, eller grönt, om det kan.
- Rikta om hologrammet så att det överensstämmer med den yttyp (lodrät eller vågrät) som den har tillhörighet till.
- Placera hologrammet på den valda ytan på ett smidigt sätt för att undvika att hoppa eller fästa.
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
- Som tidigare skapar du projektet och distribuerar till HoloLens.
- Vänta tills genomsökningen och bearbetningen av data för rumslig mappning har slutförts.
- När du ser solsystemet tittar du på projektionsrutan nedan och utför en markerad gest för att flytta runt den. Medan projektionsrutan är markerad visas en avgränsningskub runt projektionsrutan.
- Flytta dig huvudet för att titta på en annan plats i rummet. Projektion rutan bör följa din blick. När skuggan under projektionsrutan blir röd kan du inte placera hologrammet på den ytan. När skuggan under projektionsrutan blir grön kan du placera hologrammet genom att utföra en annan markeringsgest.
- Hitta 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å golvet eller taket, och att den förblir korrekt orienterad mot varje vägg när du rör dig.
Kapitel 5 – Ocklusion
Mål
- Kontrollera om ett hologram är occluded av det rumsliga mappningsnätet.
- Använd olika ocklusionstekniker för att uppnå en rolig effekt.
Instruktioner
Först ska vi tillåta att det rumsliga mappningsnätet occlude andra hologram utan att occludera den verkliga världen:
- På panelen Hierarki väljer du objektet SpatialProcessing .
- Leta reda på komponenten Play Space Manager (Script) i panelen Inspector.
- Klicka på cirkeln till höger om egenskapen Sekundärt material .
- Leta upp och välj ocklusionsmaterialet och stäng fönstret.
Därefter ska vi lägga till ett speciellt beteende på jorden, så att den har en blå markering när den blir occluded av ett annat hologram (som solen) eller av det rumsliga mappningsnätet:
- Expandera SolarSystem-objektet i mappen Holograms på projektpanelen.
- Klicka på Jorden.
- I panelen Inspector hittar du jordens material (bottenkomponenten).
- I listrutan Shader ändrar du skuggningen till Anpassad > OcclusionRim. Detta återger en blå markering runt jorden när den occluded av ett annat objekt.
Slutligen kommer vi att möjliggöra en röntgensyneffekt för planeter i vårt solsystem. Vi måste redigera PlanetOcclusion.cs (finns i mappen Scripts\SolarSystem) för att uppnå följande:
- Bestäm om en planet är occluded av SpatialMapping-lagret (rumsnät och plan).
- Visa wireframe-representationen av en planet när den är occluded av SpatialMapping-lagret.
- 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 har slutförts (du bör se att blå linjer visas på väggarna).
- Leta upp och välj solsystemets projektionslåda 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 bakomliggande ytor för att peer-koppla mot affischen eller projektionsrutan.
- Leta efter jorden, det bör finnas en blå markeringseffekt när den går bakom ett annat hologram eller en annan yta.
- Titta när planeterna rör sig bakom väggen eller andra ytor i rummet. Du har nu röntgensyn och kan se deras trådramskelett!
Slutet
Grattis! Nu har du slutfört MR Spatial 230: Spatial mappning.
- Du vet hur du genomsöker din miljö och läser in spatial mappningsdata till Unity.
- Du förstår grunderna i skuggor och hur material kan användas för att återvisualisera världen.
- Du har lärt dig om 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 upplevde olika ocklusionstekniker och utnyttjade kraften i röntgenseende!