本教程基于Unity2017.2及Visual Studio 2017
本教程编写时间:2017年12月16日

本文内容提要

空间映射让holograms了解周围环境,将真实世界和虚拟世界更好地结合在一起。本教程你将会学到:
- 扫描环境并将数据传输到你的开发设备
- 探索shader并用shader来展示你的空间
- 使用网格处理将房间网格划分为简单的平面
- 我们将研究比入门篇中更高级的放置技术,当一个hologram可以放置在环境中时提供反馈
- 学习遮挡效果,当你的hologram被真实世界物体挡住时,你仍然可以通过X光透视看到它!

准备工作

  • 安装好Hololens模拟器开发环境
  • 已完成Hololens入门篇的学习

资源下载

请先下载官方教程的资源
原地址
如果下载有困难,百度云地址

0 创建工程

  1. 将下载资源的解压出来
  2. 打开Unitiy,创建新工程,命名为 Planetarium, 选择 3D,点击Create Project
  3. Edit > Project Settings > Player,在Inspector面板中,点击Windows Store图标
  4. 展开XR Settings,勾选Virtual Reality Supported复选框,确认Windows Mixed Reality在下面的列表中
  5. 展开Publishing Settings,在Capabilities中选中
    • InternetClientServer
    • PrivateNetworkClientServer
    • Microphone
    • SpatialPerception
  6. 在Other Settings里面,找到Scripting Backend设置为.NET
  7. Assets > Import Package > Custom Package,导入刚解压出来的HolographicAcademy-Holograms-230-SpatialMapping\Starting\Planetarium.unitypackage
  8. 在Hierarchy中,删除Main Camera
  9. 将Project面板中,HoloToolkit-SpatialMapping-230\Utilities\Prefabs 文件夹下的 Main Camera 拖到Hierarchy空白处
  10. 删除Hierarchy中的Directional Light物体
  11. 将Project面板中,Holograms文件夹下的Cursor拖到Hierarchy中
  12. 在Hierarchy面板选中 Cursor 物体,在Inspector面板的上部Layer下拉菜单中选择 Edit Layers,将 User Layer 31 命名为 “SpatialMapping”
  13. 保存场景到 Scenes\Planetarium.unity

1 扫描

目标

  • 学习SurfaceObserver及其设置如何影响体验和性能。
  • 创建一个房间扫描来收集你的房间网格。

步骤

  • Project面板下 HoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs 文件夹中找到 SpatialMapping prefab
  • 将 SpatialMapping prefab拖到 Hierarchy 的空白处

编译部署(第一部分)

  • File > Build Settings, 点击 Add Open Scenes 将当前场景添加到build
  • 选择Universal Windows Platform,点击Switch Platform按钮
    1. Target device设置为Hololens,选中Unity C# Projects
    2. 在Build Settings面板,点击Build,新建App文件夹并选择该文件夹
    3. Build完成后,打开App文件夹下的解决方案,将Debug改为Release,ARM改为x86,并选中Hololens Emulator
    4. 点击调试 > 开始执行(不调试)或者Ctrl+F5(注意:模拟器启动慢可能会引起部署超时,这时候不要关闭模拟器,直接再次Ctrl+F5即可)

编译部署第二部分

现在我们来看下空间映射如何影响性能。
- 在Unity中,选择 Window> Profiler。
- 点击 Add Profiler > GPU。
- 点击 Active Profiler >
- 输入您的HoloLens 的IP地址,模拟器的地址看下图图示获得

- 点击 Connect
- 观察GPU渲染每帧所花费的毫秒数。
- 返回到Visual Studio并打开SpatialMappingObserver.cs。可以在 Assembly-CSharp (Universal Windows) 项目的 HoloToolkit-SpatialMapping-230\SpatialMapping\Scripts 文件夹中找到它。
- 找到 Awake() 函数,并添加以下代码:TrianglesPerCubicMeter = 1200;

- 将项目重新部署到您的设备,然后重新连接分析器。观察渲染每帧的毫秒数的变化。(每帧耗时应该有所升高)

保存房间模型并在Unity中加载

最后,保存房间网格并将其加载到Unity中。
- 返回到Visual Studio 删除上一节中在Awake()函数中添加的TrianglesPerCubicMeter代码。
- 将项目重新部署。现在我们应该每立方米显示500个三角形。
- 打开浏览器并输入HoloLens IP地址进入 Windows Device Protal。模拟器可以直接点击右侧工具栏中的Device Protal按钮(从下往上第二个)进入。
- 在左侧面板中选择3D View。
- 在 Spatial mapping 下,点击 Update 按钮。
- 可以看到 HoloLens 扫描过的区域显示在窗口中。
- 要保存房间扫描数据,点击Save按钮,浏览器会下载模型文件。
- 打开你的下载文件夹找到保存的房间模型 SpatialMapping.obj。
- 将 SpatialMapping.obj 复制到Unity项目的Assets文件夹中。
- 在Unity中,在 Hierarchy 面板中选择 SpatialMapping 物体的 Object Surface Observer (Script) 组件
- 将 Room Model 属性设置为刚加入工程的 SpatialMapping.obj
- 在Unity中运行下, SpatialMapping组件应该会加载网格模型
- 切换到场景视图可以查看线框显示的整个房间模型。

注意:下一次在Unity中进入Play模式时,默认会加载这个房间网格。

2 可视化

目标

  • 学习shader基础
  • 将周围环境可视化

步骤

  • 在Hierarchy米娜版那种,选额 SpatialMapping物体上面的 Spatial Mapping Manager (Script) 组件
  • 将 Surface Material 属性搜索设置为 BlueLinesOnWalls 材质
  • BlueLinesOnWalls 是一个简单的像素着色器,实现以下功能:
    • 将顶点的位置转换为世界坐标
    • 通过顶点的发现判断像素是否垂直
    • 设置像素的颜色

编译部署

  • 在Unity中进入Play模式,蓝线将在房间网格的所有垂直表面上显示(从我们保存的扫描数据中自动加载)
  • 切到 Scene 面板,查看整个房间网格是如何显示的
  • 调整 BlueLinesOnWalls 的一些属性,看看显示效果如何变化:
    • 调整LineScale值以使线条显得更粗或更薄
    • 调整LinesPerMeter值以更改每个墙上显示的线条数
  • 部署到Hololens模拟器看看

Unity预览材质非常方便,但是开发时一定要到真机上看下效果

处理

目标

  • 学习处理空间映射数据以便在应用程序中使用
  • 分析空间映射数据以找到平面并去除三角形。
  • 使用平面进行hologram放置

步骤

  • 将 Holograms 文件夹下的 SpatialProcessing prefab拖到场景中
    SpatialProcessing prefab包含处理空间映射数据的组件。SurfaceMeshesToPlanes.cs根据空间映射数据找到并生成平面。我们将在我们的应用中使用平面来代表墙壁,地板和天花板。这个prefab还包括RemoveSurfaceVertices.cs,它可以从空间映射网格中删除顶点。这可以用来在网格中创建孔,或去除不再需要的多余三角形(因为可以使用平面代替)。
  • 将 Holograms 文件夹下的 SpaceCollection prefab拖到场景中
  • 打开 SpatialProcessing 上的 PlaySpaceManager 脚本
    PlaySpaceManager.cs包含针对本程序的代码。我们需要在脚本中添加以下功能:

    1. 超过扫描时间限制(10秒)后停止采集空间映射数据。
    2. 处理空间映射数据:
      • 使用SurfaceMeshesToPlanes将墙壁,地板,天花板等用平面表示。
      • 使用RemoveSurfaceVertices去除落在平面内的三角形。
    3. 在场景中生成hologram的集合,并将它们放置在靠近用户的墙壁和地板上。
  • 根据 PlaySpaceManager.cs 中注释标记补全代码,最终完整代码如下
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;}}
}

编译部署

  • 在部署到HoloLens之前,在 Unity 中运行预览一下
  • 加载房间网格数据后,等待10秒钟处理数据。
  • 处理完成后,地板,墙壁,天花板等将显示为平面。
  • 所有的平面处理完成后,你应该看到太阳能系出现在靠近相机的地板上。
  • 照相机附近的墙壁上也应该出现两张海报。如果在Game下看不到它们,请切换到 Scene 面板。
  • 再次按下Play按钮退出Play模式。
  • 部署到HoloLens。
  • 等待空间映射数据的扫描和处理完成。
  • 看到平面以后,试着找到世界中的太阳系和海报。

4 放置

目标

  • 确定 hologram 是否放到了表面
  • hologram能否放到表面,给用户提供反馈

步骤

  • 选中 SpatialProcessing 物体,将 Surface Meshes To Planes (Script) 组件的 Draw Planes 属性设置为 Wall(可以先选Nothing清空所有)
  • 打开Scripts文件夹下的Placeable脚本
    Placeable脚本已经添加到找到平面后自动生成的海报和投影仪上。现在我们需要去掉一些代码的注释,这个脚本就能实现以下功能:

    1. 通过射线检测判断一个hologram是否适合一个表面
    2. 通过表面的发现来判断表面是否平滑,能让hologram放平
    3. hologram放置的时候渲染一个边界立方体来显示hologram的真实大小
    4. 在hologram将被放到的地面或墙上投射阴影
    5. 如果hologram可以放置,阴影显示为绿色,否则显示为红色
    6. 根据表面的类型(水平或垂直)重新旋转hologram
    7. 将hologram平滑地放置在选定的表面上,避免跳跃或捕捉情况。
  • 根据提示删掉代码的注释,最终代码如下:

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 pointif (!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;}
}

编译部署

  • 部署到HoloLens。
  • 等待空间映射数据的扫描和处理完成。
  • 当你看到太阳系时,凝视下面的投影仪,用select手势来移动它。在选中投影仪的同时,投影仪周围将出现一个边界立方体。
  • 移动你的头凝视在房间的另一个位置。投影仪会跟随你的目光。当投影仪下方的阴影变红时,不能将hologram放置在该表面上。当投影仪下方的阴影变为绿色时,可以用select手势放置。
  • 找到并选择一个墙上的全息海报,将其移动到一个新的位置。请注意,您不能将海报放在地板或天花板上,并且在您移动时它会保持正确朝向。

5 遮挡

目标

  • 判断hologram是否被空间映射网格遮挡
  • 使用不同的遮挡技术实现有趣的效果

步骤

首先,我们将允许空间映射网格遮挡hologram,但不遮挡真实世界
- 选中场景中SpatialProcessing物体,将Play Space Manager (Script)组件的Secondary Material属性设置为 Occlusion

然后我们给场景中的地球添加一个特殊的效果,当它被其他hologram(如太阳)或者空间映射网格遮挡时显示为蓝色高亮效果
- 在Holograms文件夹,展开SolarSystem,点击Earch,将他的shader改为 Custom > OcclusionRim。这个shader可以使他被遮挡时显示蓝色高亮描边

最后,给太阳系加上一个X光透视效果。我们需要编辑PlanetOcclusion.cs (Scripts\SolarSystem folder) 来实现以下功能:
1. 判断一个星球是否被 SpatialMapping 层(房间网格和平面)遮挡
2. 当星球被遮挡时,使用线框显示
3. 星球没被遮挡时,隐藏线框显示

根据PlanetOcclusion.cs中的注释提示补全代码,最终代码如下:

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

编译部署

  • 部署到Hololens
  • 等待空间映射数据的扫描和处理完成(你应该看到墙上出现蓝线)。
  • 找到并选择太阳系的投影仪,然后投影仪放在墙上或柜台后面。
  • 您可以将海报或投影仪放在物体后面来查看基本遮挡。
  • 找到地球,每当它在另一个全息图或面后面,应该有一个蓝色的高光效果。
  • 观看行星移动到房间的墙壁或其他表面之后。你现在有X光透视,可以看到他们的线框骨架!

总结

恭喜!你现在已经完成空间映射高级篇的学习。
- 知道如何扫描您的环境并将空间映射数据加载到Unity。
- 了解着色器的基础知识以及如何使用材质让世界有不同效果。
- 了解了寻找平面和从网格中去除三角形的新处理技术。
- 可以将全息图移动并放置在有意义的表面上。
- 了解不同的遮挡技术,并掌握X光透视超能力!


洪流学堂,最科学的Unity3d学习路线,让你快人一步掌握Unity3d开发核心技术!

[洪流学堂]Hololens开发高级篇5:空间映射(Spatial mapping)相关推荐

  1. [洪流学堂]Hololens开发高级篇4:立体音效(Spatial sound)

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年12月11日 本文内容提要 立体音效给Holograms注入了生命,并将它们融入现实世界.Hologr ...

  2. [洪流学堂]Hololens开发高级篇3:语音(Voice)

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年12月8日 本文内容提要 设计语音命令并针对Hololens语音引擎优化 让用户知道可以用什么语音命令 ...

  3. [洪流学堂]Hololens开发高级篇2:手势(Gesture)

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年12月7日 本文内容提要 当跟踪到用户的手时提供反馈 使用导航手势旋转hologram 当用户的手要离 ...

  4. [洪流学堂]Hololens开发高级篇1:凝视(Gaze)

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年12月5日 本文内容提要 用户凝视hologram时,光标和hologram都会发生变化 加入一些瞄准 ...

  5. [洪流学堂]Hololens开发入门篇3:使用基本功能开发一个小应用

    本文首发于"洪流学堂"公众号. 洪流学堂,让你快人几步 本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年12月4日 本文内容提要 ...

  6. [洪流学堂]Hololens开发入门篇2之Hello World

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年11月29日 如果你还没有配置好开发环境,请看[洪流学堂]MR开发之Hololens开发:入门篇1之模 ...

  7. [洪流学堂]Hololens开发入门篇1之模拟器开发环境配置

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年11月27日 视频教程 本教程入门篇的视频课程也已经上线 看视频教程,细节无遗漏哦~ https:// ...

  8. [洪流学堂]Hololens开发:Unity3d与Visual Studio最佳实践

    本教程基于Unity2017.2及Visual Studio 2017 本教程编写时间:2017年12月7日 Hololens开发:Unity3d与VS最佳实践(best practices) 使用U ...

  9. Hololens开发入门篇-郑洪智-专题视频课程

    Hololens开发入门篇-572人已学习 课程介绍         本课程使用Hololens模拟器,基于Unity2017.2及Visual Studio 2017开发 课程收益     学会Ho ...

最新文章

  1. windows下nodejs express安装及入门网站,视频资料,开源项目介绍
  2. 如何在代码中获取Java应用当前的版本号?
  3. 破windows xp登陆密码
  4. Hive JDBC:Permission denied: user=anonymous, access=EXECUTE, inode=”/tmp”
  5. 深入php-fpm的两种进程管理模式详解
  6. python wheel使用_【转载】Python,使用Wheel打包
  7. 【CVE-2020-1957】shiro搭配spring时身份验证绕过漏洞分析
  8. android.app.instrumentation解析
  9. CentOS mysql常用命令
  10. 用qt建立自定义对话框的方法,含vc2015 vc版本中使用
  11. 深度 | 面目全非自动化
  12. 销售管理系统c语言 总结报告,C语言课程设计报告-药品销售管理系统.doc
  13. python属于哪种类型_下列哪种类型是Python的列表类型?
  14. php开发之文件指针,文件锁定
  15. 数据分析试题集+答案
  16. 基于知识图谱和图卷积神经网络的应用——学习笔记
  17. 冲刺误区 | 一场失败的谷歌设计冲刺实践给我的启示
  18. Guitar Pro8.1吉他谱神器下载及简谱功能
  19. mysql数据库的连接
  20. 网站制作笔记-bootstrap可视化布局,表单生成工具,快速制作网页原型

热门文章

  1. css圆角box(宽度自适应)(百度知道挖出)
  2. 【译】组织好你的Asp.Net MVC解决方案
  3. 如何解除FSO上传程序小于200k限制?
  4. 两个网站:借物网,rss交流网站opml.cn
  5. python将字典写入csv_Python如何把字典写入到CSV文件的方法示例
  6. nuxt服务端php,nuxt服务端部署上线
  7. oracle运行产生的日志在哪里,运行临时表,是否有日志产生
  8. 网络摄像头转usb接口_Arduino + USB Host Sheild 实现USB鼠标转PS/2接口
  9. ubuntu 9.04 更新源_ubuntu更换中国源
  10. 年货节页面PSD分层模板,拿稳了!应急!