Hololens 作为一款混合现实设备,其与传统 VR/AR 设备最大的区别是,能够和现实世界进行交互。
以一个立方体为例,当我们没有使用 Spatial Mapping 时,我们只能在空间中移动它,而不能把它放置在现实世界的物体上,例如放置在一个椅子上。当我们使用了 Spatial Mapping 后,Hololens 会先扫描出所在房间的三维信息,扫描完毕后你就可以将物体放置在扫描后的空间物体上。
一 , 用 unity2018.4.9 vs2017 创建一个新的 Unity 项目 VoiceDemo,初始化项目:
1.导入 MRTK 包 (版本 HoloToolkit-Unity-2017.4.2.0)
2.应用项目设置为 MR 项目 (一键设置成为可以部署的环境)
3.使用 HoloLensCamera 替代默认相机
4.添加 CursorWithFeedback (识别并反馈手势的光标控件)
5.添加 InputManager (作为输入源管理器,管理 gaze,gesture,speech等)
6.设置 InputManager 的 SimpleSinglePointerSelector 脚本的 Cursor 属性为添加的 CursorWithFeedback (添加手势源到inputmanger)
7.创建一个空 GameObject,名为 Manager
,为其添加子 gameObject: InputManager
8.添加一个 Cube
最终 Hierarchy 结构如下:
二、实现空间映射Spatial Mapping
1)添加 MRTK 工具包下的 SpatialMapping 预制体到 Manager 对象下。 //用于扫描当前的空间环境
修改 Spatial Mapping Manager 的 Surface Material 属性值为 MRTK 包中的 SpatialUnderstandingSurface(空间理解时的表面网格材质),其他参数使用默认值即可,该属性为空间扫描时所使用的材质。
(2)在 Manager 下新建一个 GameObject(Create Empty ),名为 SpatialProcessing
。
(3)为 SpatialProcessing 添加以下两个 MRTK 包中的脚本:
SurfaceMeshesToPlanes.cs
RemoveSurfaceVertices.cs
(4)新建脚本 SpatialProcessing.cs
,并将其添加到 SpatialProcessing 上。
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System.Collections.Generic;
using UnityEngine;
namespace HoloToolkit.Unity.SpatialMapping.Tests
{
public class SpatialProcessing : Singleton<SpatialProcessing>
{
[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("结束处理所需要的最小floor数量")]
public uint minimumFloors = 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 yet.
if (!meshesProcessed)
{
// Check to see if enough scanning time has passed
// since starting the observer.
if ((Time.unscaledTime - 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...
if (SpatialMappingManager.Instance.IsObserverRunning())
{
// Stop the observer.
SpatialMappingManager.Instance.StopObserver();
}
// Call CreatePlanes() to generate planes.
CreatePlanes();
// 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)
{
// Collection of floor planes that we can use to set horizontal items on.
List<GameObject> floors = new List<GameObject>();
floors = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Floor);
// Check to see if we have enough floors (minimumFloors) to start processing.
if (floors.Count >= minimumFloors)
{
// Reduce our triangle count by removing any triangles
// from SpatialMapping meshes that intersect with active planes.
RemoveVertices(SurfaceMeshesToPlanes.Instance.ActivePlanes);
// After scanning is over, switch to the secondary (occlusion) material.
SpatialMappingManager.Instance.SetSurfaceMaterial(secondaryMaterial);
}
else
{
// Re-enter scanning mode so the user can find more surfaces before processing.
SpatialMappingManager.Instance.StartObserver();
// Re-process spatial data after scanning completes.
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>
protected override void OnDestroy()
{
if (SurfaceMeshesToPlanes.Instance != null)
{
SurfaceMeshesToPlanes.Instance.MakePlanesComplete -= SurfaceMeshesToPlanes_MakePlanesComplete;
}
base.OnDestroy();
}
}
}
(5)设置 SpatialProcessing 预设体属性如下:
Surface Meshes To Planes 脚本能够将扫描的网格转换为实体。
Draw Planes 为需要转换的类型。
Destory Planes 为需要丢弃的类型。
这里这两个参数都使用了默认值,即保留了 Wall、Floor、Ceiling、Table 类型的网格数据。
Remove Surface Vertices 脚本能够把与实体重合的网格删除。
SpatialProcessing 脚本用于处理网格数据。
Scan Time : 扫描过多少秒开始转换
Default Material: 扫描时使用的材质,这里使用 MRTK 包中的 WireframeBlue。
secondaryMaterial: 停止扫描时使用的材质,这里使用 MRTK 包中的 Occlusion,注意路径是 HoloToolKit/SpatialMapping/Materials/Occlusion.mat。
minimumFloors: 结束处理所需要的最小 floor 数量。
(6)为 Cube 添加 MRTK 包下的 TapToPlace.cs
脚本。
(7)使用真机运行程序,不要忘记添加 SpatialPerception
权限:
实验效果:
程序启动后,会先扫描空间信息。当扫描结束后,我们就可以把 Cube 放在实际的物体上,比如墙壁上。
三、Spatial UnderStanding 空间理解
不知道你在运行上面的程序时,有没有尝试过,在扫描结束后你走动到之前没有扫描到的地方,这时候就无法将Cube放置在实际的物体上了。
这也很好理解,程序在启动的一段时间内扫描空间数据,扫描结束后将其转换为(房屋)模型,你实际上放到的是在(房屋)模型上(不信你先扫描一个椅子,扫描结束后将椅子移走,Cube 只能放在椅子原来的位置上)。而我们之前没有扫描到的地方,自然没有(房屋)模型,因此无法放置。
解决办法:Hololens 为我们提供了 Spatial UnderStanding 的功能,能够让 Hololens 实时扫描空间数据,实时更新(房屋)模型。当然这样会占用较大的 CPU 资源。
MRTK 工具包为我们提供了 SpatialUnderstanding,直接将其拖入 Manager 下即可。
重新运行程序,我们发现是在实时扫描的,扫描到的部分被蓝色网格所覆盖。
查看下开启 SpatialUnderstanding 的 CPU 使用情况: