微軟Hololens學院教程-Hologram 230-空間場景建模(Spatial mapping )【微軟教程已經更新,本文是老版本】


這是老版本的教程,為了不耽誤大家的時間,請直接看原文,本文僅供參考哦!原文鏈接:https://developer.microsoft.com/EN-US/WINDOWS/HOLOGRAPHIC/holograms_230

空間場景建模是將真實環境的環境信息掃描到設備中,使得全息對象可以識別真實場景環境,從而達到可以將虛擬對象與真實世界相結合的效果。這節教程主要學習內容如下:

  • 使用Hololens掃描空間環境並將空間數據導入到開發計算機中。
  • 學習利用shader給空間網格賦予材質以便其更容易被發現。
  • 使用網格處理方法將網格變成簡單的平面。
  • 對全息對象可以放置的位置進行放置提醒,使得用戶更容易的放置。
  • 開發遮擋效果,即當全息對象被真實場景中的物體或者其他全息對象遮擋時,你仍然可以看見它,只不過它是線框模式的。

項目文件:

Download the files required by the project.

Unity 設置

  • 打開 Unity.
  • 選擇新建 New創建一個新的項目.
  • 將項目命名為 Planetarium.
  • 保存地址到你下載的項目文件夾下 HolographicAcademy-Holograms-230-SpatialMapping .
  • 選擇 3D .
  • 點擊創建項目 Create Project.
  • 當Unity打開后,打開 Edit > Project Settings > Player.
  •  在Inspector 面板下,點擊選擇 Windows Store 圖標.
  • 展開  Other Settings 設置.
  •  在Rendering 部分, 勾選 Use 16-bit Depth Buffers 選項.
  •  在Rendering部分, 勾選Virtual Reality Supported 選項.
  • 確保Windows Holographic 出現在 Virtual Reality SDKs 列表中. 如果沒有,選擇 + 按鈕再選上 Windows Holographic.
  • 展開 Publishing Settings .
  • Capabilities 部分,勾選以下設置:
    • InternetClientServer
    • PrivateNetworkClientServer
    • Microphone
    • SpatialPerception
  • Edit > Project Settings > Quality
  • Inspector 面板下, 點擊 Windows Store 圖標最下方, 'Default' 的第二個下拉黑色箭頭,將默認設置改為 Fastest.
  • 到project面板下, Assets > Import Package > Custom Package.
  • 定位到你下載的項目文件...\HolographicAcademy-Holograms-230-SpatialMapping\Starting 文件夾.
  • 點擊 Planetarium.unitypackage.
  •  點擊Open.
  •  一個Import Unity Package窗口會出現,點擊Import 按鈕.
  • 等待Unity導入所有的項目資源.
  •  在Hierarchy 面板中,刪除 Main Camera.
  • Project 面板下, HoloToolkit\Utilities\Prefabs 文件夾,找到 Main Camera 對象.
  • 拖拽 Main Camera 預設到 Hierarchy 面板中.
  • Hierarchy 面板中,刪除 Directional Light .
  • Project 面板, Holograms 文件夾下,定位到 Cursor 對象.
  • 拖拽 Cursor 預設到 Hierarchy 面板中.
  • 選中 Hierarchy 面板下的 Cursor 對象.
  •  在Inspector 面板下,點擊 Layer 下拉按鈕選擇 Edit Layers....
  • User Layer 31 命名為 "SpatialMapping" 並添加此標簽.
  • 將當前場景保存: File > Save Scene As...
  • 點擊 New Folder 創建一個新的文件夾為 Scenes.
  • 命名新場景為 "Planetarium" 然后將它保存在 Scenes 文件夾下.

章節1 掃描

步驟:

  • Project 面板下打開HoloToolkit\SpatialMapping\Prefabs 文件夾, 找到 SpatialMapping 預設.
  • 拖拽 SpatialMapping 預設到 Hierarchy 面板中.

發布測試 (part 1)

  • 在 Unity中選擇 File > Build Settings.
  • 點擊 Add Open Scenes 將當前場景 Planetarium 添加到列表中.
  • 選擇Platform 列表下的 Windows Store 然后點擊下方的 Switch Platform.
  • 設置 SDKUniversal 10 ,設置 UWP Build TypeD3D.
  • 勾選 Unity C# Projects.
  • 點擊 Build.
  • 創建一個新文件 New Folder 命名 "App".
  •  單機App 文件夾.
  • 點擊 Select Folder 按鈕.
  • 當Unity發布完成,一個文件資源管理器窗口會彈出.
  • 雙擊 App 文件夾並打開它.
  • 雙擊 Planetarium.sln 用VS打開.
  • 在VS中,將頂部工具欄內設置改為 Release,x86.
  • 選擇遠程設備Remote Machine.
  • 輸入你設備的IP地址 your device's IP address ,選擇通用未加密模式Universal (Unencrypted Protocol).記得設備要連網。
  • 點擊Debug -> Start Without debugging 或者 Ctrl + F5.
  • 當你的APP成功發布到你的Hololens上時. 戴上它在你的房間行走,你會看到你房間的物體表面會被黑色和白色線框網格覆蓋。
  • 掃描你的房間. 確保你看到了牆面,地面,以及天花板。

發布測試 (part 2)

現在讓我們探索空間映射是如何影響性能的。
  •  在unity 中,選擇分析器Window > Profiler.
  • 點擊添加GPU分析器 Add Profiler > GPU.
  • 點擊激活分析器 Active Profiler > <Enter IP>.
  • 輸入你設備的IP地址 IP address .
  • 點擊連接 Connect.
  • 觀察GPU渲染幀所需的毫秒數.
  • 在設備上關閉應用.
  • 返回到VS中,打開 SpatialMappingObserver.cs 腳本. 你會在Assembly-CSharp (Universal Windows) 項目的HoloToolkit\SpatialMapping 文件夾里找到它 .
  • 定位到 Awake() 函數,添加如下代碼: TrianglesPerCubicMeter = 1200;
  • 重新部署項目到你的設備上,然后重新連接分析器  profiler. 觀察GPU渲染幀所需的毫秒數的變化.
  • 停止運行應用.

將掃描數據保存在電腦中

接下來需要將掃描的空間數據下載下來並導入Unity中以供編輯。

  • 返回到VS中,將上一步在Awake() 函數中添加的 TrianglesPerCubicMeter 代碼刪除.
  • 從新將項目發布到你的設備上. 我們現在應該運行渲染的是每立方米500個三角形。
  • 在電腦上打開你的瀏覽器,輸入https://<你的IP地址> ,可以打開設備控制台,Windows Device Portal.
  • 選擇 3D View .帶上你的設備掃描室內一周
  •  選擇Update 按鈕.
  • 你會看到你掃描的空間場景會顯示在窗口中.
  • 點擊 Save 按鈕.
  • 打開你的下載文件夾查看剛下載的空間文件 SRMesh.obj(或者你命名的其他文件).
  • 復制SRMesh.obj 到你Unity項目的 Assets 文件夾下.
  •  回到Unity, 選擇Hierarchy 面板下的  SpatialMapping 對象.
  • 在Inspector面板中,找到 Object Surface Observer (Script) 組件.
  • 點擊 Room Model 屬性右邊的小圓圈.
  • 在彈出窗口中搜索SRMesh 對象並選中,然后關閉窗口.
  • 現在 Room Model 屬性值為 SRMesh.
  • 點擊 Play 按鈕進入Unity預覽模式.
  •  SpatialMapping 組件會默認加載保存的房間模型的網格數據。
  • 選擇 Scene 界面來查看你掃描的房間模型.
  • 再一次點擊 Play 按鈕可以退出預覽.

NOTE: 下次您在Unity中進入預覽模式時,它將默認加載保存的房間網格。

 章節2  可視化

給房間網格模型添加材質使其可見。

步驟:

  • 在 Unity的 Hierarchy 面板中,選擇 SpatialMapping 對象.
  • Inspector 面板中找到 Spatial Mapping Manager (Script) 組件.
  • 點擊Surface Material 屬性右邊的圓圈.
  • 在新彈出的窗口中搜索 BlueLinesOnWalls 材質並選中然后關閉窗口.
  • Project 面板下的 Shaders 文件夾,雙擊 BlueLinesOnWalls用VS打開.
  • 這是一個簡單的頂點片段着色器程序, 實現了以下幾點:
    1. 將頂點位置轉換為世界空間。
    2. 檢查頂點法線以確定像素是否是垂直的.
    3. 設置要渲染的像素的顏色.

部署發布:

  • 返回到Unity,進入預覽模式。
  • 藍線將在房間網格的所有垂直表面上呈現。
  • 切換到“Scene”選項卡以調整你觀看房間的視角,並查看整個房間網格在Unity中的顯示方式。
  • 在“Project”面板中,找到“Materials”文件夾,然后選擇BlueLinesOnWalls材質球。
  • 修改材質球的一些屬性,並查看更改在Unity編輯器中顯示的效果。
             -在“Inspector”面板中,調整LineScale值,使線條看起來更厚或更薄。
             -在“Inspector”面板中,調整LinesPerMeter值以更改每個牆上顯示的行數。
  • 退出預覽模式。
  • 重新部署到HoloLens,並觀察着色渲染如何在真實環境表面上顯示。
  • Unity在渲染材質方面做得很好,但是在設備中不使用材質渲染總是一個好主意。

章節3 處理

這章主要學習處理空間場景模型數據的技術以在應用程序中使用。分析空間場景建模數據以查找平面和刪除三角形。使全息圖正確放置在真實環境中的平面上。

步驟:

  • 在 Unity的 Project 面板中, Holograms 文件夾下找到 SpatialProcessing 對象.
  • 拖拽 SpatialProcessing 對象到 Hierarchy 面板中.

SpatialProcessing 預設包括了用於處理空間場景建模數據的組件。 SurfaceMeshesToPlanes.cs腳本將基於空間場景建模數據查找和生成平面。 在我們的應用程序中我們將使用平面來代表牆壁,地板和天花板。 此預制還包括RemoveSurfaceVertices.cs腳本,它可以從空間場景建模網格中刪除頂點。 這可以用於在網格中創建孔,或者刪除不再需要的多余三角形(因為可以使用平面來代替)。

  •  在Unity的Project 面板, Holograms 文件夾下找到SpaceCollection 對象.
  • 拖拽 SpaceCollection 對象到 Hierarchy 面板中.
  • Hierarchy 面板中選擇 SpatialProcessing 對象.
  • Inspector 面板找到 Play Space Manager (Script) 組件.
  • 雙擊 PlaySpaceManager.cs 腳本用VS打開.

PlaySpaceManager.cs腳本包含特定於應用程序的代碼。 我們將向此腳本添加功能以實現以下行為:

    1 在超過掃描時間限制(10秒)后停止收集空間場景建模數據。

    2 處理空間場景建模數據:

            - 使用SurfaceMeshesToPlanes創建一個更簡單的平面世界(牆壁,地板,天花板等)。

            - 使用RemoveSurfaceVertices刪除落在平面邊界內的表面三角形。

    3 生成一個全息圖集合,並將它們放置在用戶附近的牆壁和地板上。

完成代碼:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Windows.Speech;
using 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;
        }
    }
}
PlaySpaceManager

部署發布:

  • 在部署到HoloLens之前,按Unity中的播放按鈕進入播放模式。
  • 在文件加載房間網格后,等待10秒鍾,這時在對空間場景模型網格進行處理。
  • 當處理完成時,地板,牆壁,天花板等將以平面版的形式呈現。
  • 在找到所有的平面之后,你應該會看到一個太陽系,在靠近相機的地板上。
  • 兩個海報應該出現在相機附近的牆上。 如果在游戲模式下看不到它們,請切換到場景選項卡。
  • 再次按下播放按鈕退出播放模式。
  • 像往常一樣構建和部署該項目到HoloLens。
  • 等待掃描和處理空間場景模型數據完成。
  • 一旦你看到場景中平面生成,試着找到你的世界中的太陽系和海報。

章節4 放置

這章主要學習實現如何將你的全息對象通過手勢操作將它放置在你想放置的平面上,如果你想放置的位置不具備放置的條件,提供視覺反饋提醒用戶不能放置。

步驟:

  • 在Unity的 Hierarchy 面板選擇 SpatialProcessing 對象.
  • Inspector 面板中找到 Surface Meshes To Planes (Script) 組件.
  • 改變 Draw Planes 屬性為 Nothing .
  • 改變 Draw Planes 屬性 Wall, 這時只有檣面會被渲染成平面狀態.
  • Project 面板下, Scripts 文件夾, 雙擊 Placeable.cs 腳本用VS打開.

這個Placeable 腳本已附加海報和投影框上。 我們需要做的是取消注釋一些代碼,這個腳本將實現以下:   

     1.通過從立方體的中心和邊界四個角進行光線投射,確定全息圖是否匹配到了一個表面上。
     2.檢查表面法線以確定它是否足夠平滑以使全息圖齊平。
     3.渲染出一個圍繞全息圖的立方體邊框,以顯示全息圖放置時的實際尺寸。
     4.在全息圖下面/后面投一個陰影,以顯示它將放在地板/牆上的位置。
     5.如果全息圖不能放置在表面上,渲染陰影為紅色,如果可以放置,則將陰影渲染為綠色。
     6.重新定向全息圖以與具有親和力的表面類型(垂直或水平)對齊。
     7.平滑穩定地將全息圖放置在所選表面上,以避免跳躍或斷裂行為。

using System.Collections.Generic;
using UnityEngine;
using 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 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;
        }
        Quaternion rotation = Quaternion.Euler(rotationX, gameObject.transform.rotation.eulerAngles.y, 0);

        shadowAsset.transform.localScale = boxCollider.size;
        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;
    }
}
Placeable

發布測試:

  • 和前面一樣,構建項目並部署到HoloLens。
  • 等待掃描和空間場景模型數據預處理完成。
  • 當你看到太陽系時,注視下面的投影框,並執行選擇手勢移動它。 當投影框被選中時,在投影框周圍將出現一個邊界立方體。
  • 移動你的頭,凝視房間里其他的位置。 投影盒應該會遵循你的目光。 當投影框下方的陰影變為紅色時,表示不能將全息圖放置在該表面上。 當投影框下方的陰影變為綠色時,您可以通過執行另一個選擇手勢將它放置在這個位置
  • 找到一個你房間牆上的全息海報並選擇它,將其移動到一個新的位置。 請注意,您不能將海報放置在地板或天花板上,並且在您四處移動時,海報會保持正確朝向(即始終保持與牆壁垂直,並面向室內)。

章節5  遮擋

這章主要實現判斷全息對象是否被空間場景模型的網格所遮擋,如果被遮擋,賦予該全息對象X-射線的視覺效果以表明它被遮擋。

步驟:

首先,我們將使得空間場景模型網格遮擋其他全息圖,而不遮擋真實世界:

  • Hierarchy 面板下選擇SpatialProcessing 對象.
  • Inspector 面板中找到 Play Space Manager (Script) 組件.
  • 點擊Secondary Material 屬性右側的小圓圈.
  • 在彈出的窗口中搜索 Occlusion 材質球並選中,然后關閉此窗口.

接下來,我們將給地球添加一個特殊的效果,當它在被另一個全息圖(如太陽)遮擋或者被空間場景模型網格所遮擋時具有藍色高光:

  • Project 面板,  Holograms 文件夾, 展開 SolarSystem 對象.
  • 點擊 Earth.
  • Inspector 面板中, 找到地球的材質球 .
  •  將其shader 改選為 Custom > OcclusionRim.當地球被另一個物體遮擋時,這將在地球上呈現一個藍色的高光。

最后,我們將為我們的太陽系中的行星實現X射線的視覺效果。 我們將需要編輯PlanetOcclusion.cs(在Scripts \ SolarSystem文件夾中可以找到),以便實現以下操作:
     1.確定行星是否被SpatialMapping層(房間網格和平面)遮擋。
     2.只要行星被SpatialMapping圖層遮擋,顯示它的線框效果
     3.當行星沒有被SpatialMapping圖層阻擋時,隱藏它的線框效果。

using UnityEngine;
using 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);
            }
        }
    }
}
PlanetOcclusion

部署發布:

  • 像往常一樣,將應用程序構建並部署到HoloLens。
  • 等待空間場景建模數據的掃描和處理完成(您應該看到牆上出現藍線)。
  • 找到並選擇太陽系的投影框,然后將它放置在牆壁旁邊或在櫃台后面
  • 您可以通過將海報或投影框隱藏在其他對象的后面,以查看基本的遮擋效果。
  • 觀察地球,每當它在另一個全息圖或表面后面時,應該有一個藍色的高光效果。
  • 觀察行星移動到牆壁或房間里的其他表面后面時。 你會看到X光射線的效果,可以看到他們的線框骨架!

 原文鏈接:https://developer.microsoft.com/en-us/windows/holographic/holograms_230

本人按照自己的理解對原文稍作修改,如有不恰當的地方,請指正哦!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM