本文主要讲述在实现空间映射(SpatialMapping)后,将场景扫描的网格转换为平面,然后在此基础上创建游戏对象,并将游戏对象放置到转换后的垂直平面或者水平平面上。
本文示例是在空间映射示例的基础上进行进一步修改。
空间映射 - 放置物体
1.在场景中添加Cursor组件(直接使用HoloToolKit中的Cursor的prefab),添加后既可以看见Cursor组件集成了GazeManager.cs 、GestureManager.cs等一些必要的脚本。
2.向场景中添加SpatialMapping Prefab,具体在HoloToolKit -> SpatialMapping -> Prefabs中可以找到该预制体。
SpatialMapping 包含的脚本,在空间映射已有详细阐述,在此不加重复。
3.创建空的游戏对象 SpatialProcessing
在SpatialProcessing上添加 SurfaceMeshesToPlanes.cs 和 RemoveSurfaceVertices.cs脚本组件 (可以直接在HoloToolkit->SpatialMapping->Scripts中找到)
SurfaceMeshesToPlanes.cs脚本主要是用来将扫描空间后生成的网格转换成平面
添加完该脚本后,在HoloToolkit->SpatialMapping->Prefabs中找到SurfacePlane,将其拖拽到SurfaceMeshesToPlanes.cs的SurfacePlanePrefab上
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if !UNITY_EDITOR
using System.Threading;
using System.Threading.Tasks;
#else
using UnityEditor;
#endif
namespace HoloToolkit.Unity
{
/// <summary>
/// SurfaceMeshesToPlanes will find and create planes based on the meshes returned by the SpatialMappingManager's Observer.
/// 根据 SpatialMappingManager's Observer 返回的网格信息,找到网格并基于网格信息创建plane进行替换
/// </summary>
public class SurfaceMeshesToPlanes : Singleton<SurfaceMeshesToPlanes>
{
[Tooltip("Currently active planes found within the Spatial Mapping Mesh.")]
public List<GameObject> ActivePlanes;
[Tooltip("Object used for creating and rendering Surface Planes.")]
public GameObject SurfacePlanePrefab;
[Tooltip("Minimum area required for a plane to be created.")]
public float MinArea = 0.025f;
/// <summary>
/// Determines which plane types should be rendered.
/// 确定应渲染哪些平面类型
/// </summary>
[HideInInspector]
public PlaneTypes drawPlanesMask =
(PlaneTypes.Wall | PlaneTypes.Floor | PlaneTypes.Ceiling | PlaneTypes.Table);
/// <summary>
/// Determines which plane types should be discarded.
/// Use this when the spatial mapping mesh is a better fit for the surface (ex: round tables).
/// 确定哪种类型的平面应该被丢弃
/// </summary>
[HideInInspector]
public PlaneTypes destroyPlanesMask = PlaneTypes.Unknown;
/// <summary>
/// Floor y value, which corresponds to the maximum horizontal area found below the user's head position.
/// 地板的Y坐标值,其对应于在用户头部位置下方找到的最大水平区域
/// This value is reset by SurfaceMeshesToPlanes when the max floor plane has been found.
/// </summary>
public float FloorYPosition { get; private set; }
/// <summary>
/// Ceiling y value, which corresponds to the maximum horizontal area found above the user's head position.
/// 天花板的Y坐标值,其对应于在用户头部位置上方找到的最大水平区域
/// This value is reset by SurfaceMeshesToPlanes when the max ceiling plane has been found.
/// </summary>
public float CeilingYPosition { get; private set; }
/// <summary>
/// Delegate which is called when the MakePlanesCompleted event is triggered.
/// 当平面创建完成后进行触发
/// </summary>
/// <param name="source"></param>
/// <param name="args"></param>
public delegate void EventHandler(object source, EventArgs args);
/// <summary>
/// EventHandler which is triggered when the MakePlanesRoutine is finished.
/// 当MakePlanesRoutine完成,进行触发
/// </summary>
public event EventHandler MakePlanesComplete;
/// <summary>
/// Empty game object used to contain all planes created by the SurfaceToPlanes class.
/// </summary>
private GameObject planesParent;
/// <summary>
/// Used to align planes with gravity so that they appear more level.
/// 用于对齐具有重力的平面,以使它们看起来更平坦
/// </summary>
private float snapToGravityThreshold = 5.0f;
/// <summary>
/// Indicates if SurfaceToPlanes is currently creating planes based on the Spatial Mapping Mesh.
/// 基于空间映射网格来标记当前是否正在创建平面
/// </summary>
private bool makingPlanes = false;
#if UNITY_EDITOR
/// <summary>
/// How much time (in sec), while running in the Unity Editor, to allow RemoveSurfaceVertices to consume before returning control to the main program.
/// </summary>
private static readonly float FrameTime = .016f;
#else
/// <summary>
/// How much time (in sec) to allow RemoveSurfaceVertices to consume before returning control to the main program.
/// </summary>
private static readonly float FrameTime = .008f;
#endif
// GameObject initialization.
private void Start()
{
makingPlanes = false;
ActivePlanes = new List<GameObject>();
planesParent = new GameObject("SurfacePlanes");
planesParent.transform.position = Vector3.zero;
planesParent.transform.rotation = Quaternion.identity;
}
/// <summary>
/// 根据SpatialMappingManager's SurfaceObserver生成的网格信息创建平面
/// </summary>
public void MakePlanes()
{
if (!makingPlanes)
{
makingPlanes = true;
// Processing the mesh can be expensive...
// We use Coroutine to split the work across multiple frames and avoid impacting the frame rate too much.
//使用协程将工作分割在多个帧中完成,避免影响帧率太多。
StartCoroutine(MakePlanesRoutine());
}
}
/// <summary>
///返回所有指定的平面类型的平面列表
/// </summary>
/// <param name="planeTypes">A flag which includes all plane type(s) that should be returned.</param>
/// <returns>预期的平面类型的平面列表</returns>
public List<GameObject> GetActivePlanes(PlaneTypes planeTypes)
{
List<GameObject> typePlanes = new List<GameObject>();
foreach (GameObject plane in ActivePlanes)
{
SurfacePlane surfacePlane = plane.GetComponent<SurfacePlane>();
if (surfacePlane != null)
{
if((planeTypes & surfacePlane.PlaneType) == surfacePlane.PlaneType)
{
typePlanes.Add(plane);
}
}
}
return typePlanes;
}
/// <summary>
/// Iterator block, analyzes surface meshes to find planes and create new 3D cubes to represent each plane.
/// 分析表面网格以找到平面并创建新的3D立方体来表示每个平面。
/// </summary>
/// <returns>Yield result.</returns>
private IEnumerator MakePlanesRoutine()
{
//删除之前生成的平面信息
for (int index = 0; index < ActivePlanes.Count; index++)
{
Destroy(ActivePlanes[index]);
}
// 暂停任务,等待下一帧处理下面的代码
yield return null;
float start = Time.realtimeSinceStartup;
ActivePlanes.Clear();
//从SpatialMappingManager中获取最新的网格信息
List<PlaneFinding.MeshData> meshData = new List<PlaneFinding.MeshData>();
List<MeshFilter> filters = SpatialMappingManager.Instance.GetMeshFilters();
for (int index = 0; index < filters.Count; index++)
{
MeshFilter filter = filters[index];
if (filter != null && filter.sharedMesh != null)
{
// 修复表面网格法线,得到正确的平面方向
filter.mesh.RecalculateNormals();
meshData.Add(new PlaneFinding.MeshData(filter));
}
if ((Time.realtimeSinceStartup - start) > FrameTime)
{
// 暂停任务,等待下一帧处理下面的代码
yield return null;
start = Time.realtimeSinceStartup;
}
}
// 暂停任务,等待下一帧处理下面的代码
yield return null;
#if !UNITY_EDITOR
// When not in the unity editor we can use a cool background task to help manage FindPlanes().
Task<BoundedPlane[]> planeTask = Task.Run(() => PlaneFinding.FindPlanes(meshData, snapToGravityThreshold, MinArea));
while (planeTask.IsCompleted == false)
{
yield return null;
}
BoundedPlane[] planes = planeTask.Result;
#else
// In the unity editor, the task class isn't available, but perf is usually good, so we'll just wait for FindPlanes to complete.
BoundedPlane[] planes = PlaneFinding.FindPlanes(meshData, snapToGravityThreshold, MinArea);
#endif
// Pause our work here, and continue on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
float maxFloorArea = 0.0f;
float maxCeilingArea = 0.0f;
FloorYPosition = 0.0f;
CeilingYPosition = 0.0f;
float upNormalThreshold = 0.9f;
if(SurfacePlanePrefab != null && SurfacePlanePrefab.GetComponent<SurfacePlane>() != null)
{
upNormalThreshold = SurfacePlanePrefab.GetComponent<SurfacePlane>().UpNormalThreshold;
}
// 找到地面及天花板
// 定义用户头部位置以下的面积最大的水平区域为地面
// 定义用户头部位置以上的面积最大的水平区域为天花板
for (int i = 0; i < planes.Length; i++)
{
BoundedPlane boundedPlane = planes[i];
if (boundedPlane.Bounds.Center.y < 0 && boundedPlane.Plane.normal.y >= upNormalThreshold)
{
maxFloorArea = Mathf.Max(maxFloorArea, boundedPlane.Area);
if (maxFloorArea == boundedPlane.Area)
{
FloorYPosition = boundedPlane.Bounds.Center.y;
}
}
else if (boundedPlane.Bounds.Center.y > 0 && boundedPlane.Plane.normal.y <= -(upNormalThreshold))
{
maxCeilingArea = Mathf.Max(maxCeilingArea, boundedPlane.Area);
if (maxCeilingArea == boundedPlane.Area)
{
CeilingYPosition = boundedPlane.Bounds.Center.y;
}
}
}
// 创建SurfacePlane对象以表示在空间映射网格中找到的每个平面。
for (int index = 0; index < planes.Length; index++)
{
GameObject destPlane;
BoundedPlane boundedPlane = planes[index];
// 实例化一个平面对象,它将具有和BoundedPlane对象相同的边界
if (SurfacePlanePrefab != null && SurfacePlanePrefab.GetComponent<SurfacePlane>() != null)
{
destPlane = Instantiate(SurfacePlanePrefab);
}
else
{
destPlane = GameObject.CreatePrimitive(PrimitiveType.C