本文主要讲述在实现空间映射(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.Cube);
destPlane.AddComponent<SurfacePlane>();
destPlane.GetComponent<Renderer>().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
}
destPlane.transform.parent = planesParent.transform;
SurfacePlane surfacePlane = destPlane.GetComponent<SurfacePlane>();
//设置平面属性以调整变换位置/缩放/旋转并确定平面类型。
surfacePlane.Plane = boundedPlane;
SetPlaneVisibility(surfacePlane);
if ((destroyPlanesMask & surfacePlane.PlaneType) == surfacePlane.PlaneType)
{
DestroyImmediate(destPlane);
}
else
{
// Set the plane to use the same layer as the SpatialMapping mesh.
destPlane.layer = SpatialMappingManager.Instance.PhysicsLayer;
ActivePlanes.Add(destPlane);
}
// If too much time has passed, we need to return control to the main game loop.
if ((Time.realtimeSinceStartup - start) > FrameTime)
{
// Pause our work here, and continue making additional planes on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
}
}
Debug.Log("Finished making planes.");
//平面创建完成,触发事件
EventHandler handler = MakePlanesComplete;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
makingPlanes = false;
}
/// <summary>
/// Sets visibility of planes based on their type.
/// </summary>
/// <param name="surfacePlane"></param>
private void SetPlaneVisibility(SurfacePlane surfacePlane)
{
surfacePlane.IsVisible = ((drawPlanesMask & surfacePlane.PlaneType) == surfacePlane.PlaneType);
}
}
#if UNITY_EDITOR
/// <summary>
/// Editor extension class to enable multi-selection of the 'Draw Planes' and 'Destroy Planes' options in the Inspector.
/// </summary>
[CustomEditor(typeof(SurfaceMeshesToPlanes))]
public class PlaneTypesEnumEditor : Editor
{
public SerializedProperty drawPlanesMask;
public SerializedProperty destroyPlanesMask;
void OnEnable()
{
drawPlanesMask = serializedObject.FindProperty("drawPlanesMask");
destroyPlanesMask = serializedObject.FindProperty("destroyPlanesMask");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
drawPlanesMask.intValue = (int)((PlaneTypes)EditorGUILayout.EnumMaskField
("Draw Planes", (PlaneTypes)drawPlanesMask.intValue));
destroyPlanesMask.intValue = (int)((PlaneTypes)EditorGUILayout.EnumMaskField
("Destroy Planes", (PlaneTypes)destroyPlanesMask.intValue));
serializedObject.ApplyModifiedProperties();
}
}
#endif
}
RemoveSurfaceVertices.cs 主要用来删除网格三角型
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace HoloToolkit.Unity
{
/// <summary>
/// RemoveSurfaceVertices will remove any vertices from the Spatial Mapping Mesh that fall within the bounding volume.
/// This can be used to create holes in the environment, or to help reduce triangle count after finding planes.
/// </summary>
public class RemoveSurfaceVertices : Singleton<RemoveSurfaceVertices>
{
[Tooltip("The amount, if any, to expand each bounding volume by.")]
public float BoundsExpansion = 0.0f;
/// <summary>
/// Delegate which is called when the RemoveVerticesComplete 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 RemoveSurfaceVertices is finished.
/// </summary>
public event EventHandler RemoveVerticesComplete;
/// <summary>
/// Indicates if RemoveSurfaceVertices is currently removing vertices from the Spatial Mapping Mesh.
/// </summary>
private bool removingVerts = false;
/// <summary>
/// Queue of bounding objects to remove surface vertices from.
/// Bounding objects are queued so that RemoveSurfaceVerticesWithinBounds can be called even when the previous task has not finished.
/// </summary>
private Queue<Bounds> boundingObjectsQueue;
#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()
{
boundingObjectsQueue = new Queue<Bounds>();
removingVerts = false;
}
/// <summary>
/// Removes portions of the surface mesh that exist within the bounds of the boundingObjects.
/// </summary>
/// <param name="boundingObjects">Collection of GameObjects that define the bounds where spatial mesh vertices should be removed.</param>
public void RemoveSurfaceVerticesWithinBounds(IEnumerable<GameObject> boundingObjects)
{
if (boundingObjects == null)
{
return;
}
if (!removingVerts)
{
removingVerts = true;
AddBoundingObjectsToQueue(boundingObjects);
// We use Coroutine to split the work across multiple frames and avoid impacting the frame rate too much.
StartCoroutine(RemoveSurfaceVerticesWithinBoundsRoutine());
}
else
{
// Add new boundingObjects to end of queue.
AddBoundingObjectsToQueue(boundingObjects);
}
}
/// <summary>
/// Adds new bounding objects to the end of the Queue.
/// </summary>
/// <param name="boundingObjects">Collection of GameObjects which define the bounds where spatial mesh vertices should be removed.</param>
private void AddBoundingObjectsToQueue(IEnumerable<GameObject> boundingObjects)
{
foreach (GameObject item in boundingObjects)
{
Bounds bounds = new Bounds();
Collider boundingCollider = item.GetComponent<Collider>();
if (boundingCollider != null)
{
bounds = boundingCollider.bounds;
// Expand the bounds, if requested.
if (BoundsExpansion > 0.0f)
{
bounds.Expand(BoundsExpansion);
}
boundingObjectsQueue.Enqueue(bounds);
}
}
}
/// <summary>
/// Iterator block, analyzes surface meshes to find vertices existing within the bounds of any boundingObject and removes them.
/// </summary>
/// <returns>Yield result.</returns>
private IEnumerator RemoveSurfaceVerticesWithinBoundsRoutine()
{
List<MeshFilter> meshFilters = SpatialMappingManager.Instance.GetMeshFilters();
float start = Time.realtimeSinceStartup;
while (boundingObjectsQueue.Count > 0)
{
// Get the current boundingObject.
Bounds bounds = boundingObjectsQueue.Dequeue();
foreach (MeshFilter filter in meshFilters)
{
// Since this is amortized across frames, the filter can be destroyed by the time
// we get here.
if (filter == null)
{
continue;
}
Mesh mesh = filter.sharedMesh;
if (mesh != null || !mesh.bounds.Intersects(bounds))
{
// We don't need to do anything to this mesh, move to the next one.
continue;
}
// Remove vertices from any mesh that intersects with the bounds.
Vector3[] verts = mesh.vertices;
List<int> vertsToRemove = new List<int>();
// Find which mesh vertices are within the bounds.
for (int i = 0; i < verts.Length; ++i)
{
if (bounds.Contains(verts[i]))
{
// These vertices are within bounds, so mark them for removal.
vertsToRemove.Add(i);
}
// If too much time has passed, we need to return control to the main game loop.
if ((Time.realtimeSinceStartup - start) > FrameTime)
{
// Pause our work here, and continue finding vertices to remove on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
}
}
if (vertsToRemove.Count == 0)
{
// We did not find any vertices to remove, so move to the next mesh.
continue;
}
// We found vertices to remove, so now we need to remove any triangles that reference these vertices.
int[] indices = mesh.GetTriangles(0);
List<int> updatedIndices = new List<int>();
for (int index = 0; index < indices.Length; index += 3)
{
// Each triangle utilizes three slots in the index buffer, check to see if any of the
// triangle indices contain a vertex that should be removed.
if (vertsToRemove.Contains(indices[index]) ||
vertsToRemove.Contains(indices[index + 1]) ||
vertsToRemove.Contains(indices[index + 2]))
{
// Do nothing, we don't want to save this triangle...
}
else
{
// Every vertex in this triangle is good, so let's save it.
updatedIndices.Add(indices[index]);
updatedIndices.Add(indices[index + 1]);
updatedIndices.Add(indices[index + 2]);
}
// If too much time has passed, we need to return control to the main game loop.
if ((Time.realtimeSinceStartup - start) > FrameTime)
{
// Pause our work, and continue making additional planes on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
}
}
if (indices.Length == updatedIndices.Count)
{
// None of the verts to remove were being referenced in the triangle list.
continue;
}
// Update mesh to use the new triangles.
mesh.SetTriangles(updatedIndices.ToArray(), 0);
mesh.RecalculateBounds();
yield return null;
start = Time.realtimeSinceStartup;
// Reset the mesh collider to fit the new mesh.
MeshCollider collider = filter.gameObject.GetComponent<MeshCollider>();
if (collider != null)
{
collider.sharedMesh = null;
collider.sharedMesh = mesh;
}
}
}
Debug.Log("Finished removing vertices.");
// We are done removing vertices, trigger an event.
EventHandler handler = RemoveVerticesComplete;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
removingVerts = false;
}
}
}
新增PlaySpaceManager.cs脚本组件,该脚本主要是用来控制生成平面,以及生成平面后创建游戏对象
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.
/// SurfaceManager类允许应用程序在指定的时间内扫描环境然后在该时间到期后处理空间映射网格(找到平面,删除顶点)。
/// </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.")]
//当SpatialMappingObserver扫描正在运行时,在渲染空间映射网格时使用的材质
public Material defaultMaterial;
[Tooltip("Optional Material to use when rendering Spatial Mapping meshes after the observer has been stopped.")]
//在SpatialMappingObserver扫描停止后呈现空间映射网格时使用的材料
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>
/// 标记表面网格的处理是否完成
/// </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);
// 注册生成平面后的回调事件
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...
//当达到了扫描时间,停止对空间的扫描映射
if (SpatialMappingManager.Instance.IsObserverRunning())
{
SpatialMappingManager.Instance.StopObserver();
}
// 创建生成平面
CreatePlanes();
// 标记表面网格处理完成
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 */
// 水平平面列表(地面、桌面等)
List<GameObject> horizontal = new List<GameObject>();
// 垂直平面列表(墙面等垂直平面)
List<GameObject> vertical = new List<GameObject>();
// 获取所有的水平平面(桌面、地面)
horizontal = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Table | PlaneTypes.Floor);
// 获取所有的垂直墙面
vertical = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Wall);
// 检查垂直平面和水平平面的数量是否达到了最少要求的数量。如果未到达,重新进行空间扫描
if (horizontal.Count >= minimumFloors && vertical.Count >= minimumWalls)
{
// We have enough floors and walls to place our holograms on...
//删除SpatialMapping网格中的三角形以减少三角形数量
RemoveVertices(SurfaceMeshesToPlanes.Instance.ActivePlanes);
//设置不同空间映射网格的材料向用户指示扫描已结束。
SpatialMappingManager.Instance.SetSurfaceMaterial(secondaryMaterial);
//完成了网格的处理,初始化全息对象并使用水平或者垂直平面来设置其起始位置,传入参数为水平和垂直平面的列表
SpaceCollectionManager.Instance.GenerateItemsInWorld(horizontal, vertical);
}
else
{
//未有扫描足够可以放置全息对象的垂直和水平平面,重新进行空间扫描
SpatialMappingManager.Instance.StartObserver();
// 3.a: Re-process spatial data after scanning completes by
// re-setting meshesProcessed to false.
//重新标记表面网格未处理完成
meshesProcessed = false;
}
}
/// <summary>
/// 将扫描得到的空间映射信息处理转换成平面
/// </summary>
private void CreatePlanes()
{
// Generate planes based on the spatial map.
SurfaceMeshesToPlanes surfaceToPlanes = SurfaceMeshesToPlanes.Instance;
if (surfaceToPlanes != null && surfaceToPlanes.enabled)
{
surfaceToPlanes.MakePlanes();
}
}
/// <summary>
/// 从空间映射中删除生成的三角形
/// </summary>
/// <param name="boundingObjects"></param>
private void RemoveVertices(IEnumerable<GameObject> boundingObjects)
{
RemoveSurfaceVertices removeVerts = RemoveSurfaceVertices.Instance;
if (removeVerts != null && removeVerts.enabled)
{
removeVerts.RemoveSurfaceVerticesWithinBounds(boundingObjects);
}
}
/// <summary>
///释放资源
/// </summary>
private void OnDestroy()
{
if (SurfaceMeshesToPlanes.Instance != null)
{
SurfaceMeshesToPlanes.Instance.MakePlanesComplete -= SurfaceMeshesToPlanes_MakePlanesComplete;
}
}
}
4.创建测试对象预置体,并在测试对象上新建Placeable.cs脚本组件
Placeable.cs脚本用于判断虚拟物体在某区域内是否可以放置。
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;
// 当物体在被放置前处于悬停状态时,物体离目标表面的距离.
private float hoverDistance = 0.15f;
// 阈值(越接近0,标准越严格),用于确定表面是否平坦
private float distanceThreshold = 0.02f;
// 阈值(越接近1,标准越严格),用于确定表面是否垂直。
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;
// 获取或创建BoxCollider
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;
}
// 创建全息对象的边界对象
boundsAsset = GameObject.CreatePrimitive(PrimitiveType.Cube);
boundsAsset.transform.parent = gameObject.transform;
boundsAsset.SetActive(false);
// 创建全息对象的阴影对象
shadowAsset = GameObject.CreatePrimitive(PrimitiveType.Quad);
shadowAsset.transform.parent = gameObject.transform;
shadowAsset.SetActive(false);
}
//当全息对象被选中时调用
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;
}
}
测试对象cube1放置在墙面上,设置PlacementSurface为Vertical:
测试对象cube2放置在地面上,设置PlacementSurface为Horizontal:
其中涉及到的这些材质,可以自行进行新增几个材质球:
- PlaceableBounds用于在可放置时显示的边界
- NotPlaceableBounds 不可放置时显示的边界
- PlaceableShadow 可放置时显示的阴影
- NotPlaceableShadow 不可放置时显示的阴影
5.新增游戏对象集合SpaceCollection
创建空对象,并新增SpaceCollectionManager.cs脚本组件,将上面创建的两个cube Prefab拖拽进去。
ObjectCollectionManager.cs如下,该脚本主要是用来在空间中生成游戏对象,放置游戏对象
using System.Collections.Generic;
using UnityEngine;
using HoloToolkit.Unity;
/// <summary>
/// Called by PlaySpaceManager after planes have been generated from the Spatial Mapping Mesh.
/// This class will create a collection of prefab objects that have the 'Placeable' component and
/// will attempt to set their initial location on planes that are close to the user.
/// </summary>
public class SpaceCollectionManager : Singleton<SpaceCollectionManager>
{
[Tooltip("A collection of Placeable space object prefabs to generate in the world.")]
public List<GameObject> spaceObjectPrefabs;
/// <summary>
/// Generates a collection of Placeable objects in the world and sets them on planes that match their affinity.
/// </summary>
/// <param name="horizontalSurfaces">Horizontal surface planes (floors, tables).</param>
/// <param name="verticalSurfaces">Vertical surface planes (walls).</param>
public void GenerateItemsInWorld(List<GameObject> horizontalSurfaces, List<GameObject> verticalSurfaces)
{
List<GameObject> horizontalObjects = new List<GameObject>();
List<GameObject> verticalObjects = new List<GameObject>();
foreach (GameObject spacePrefab in spaceObjectPrefabs)
{
Placeable placeable = spacePrefab.GetComponent<Placeable>();
if (placeable.PlacementSurface == PlacementSurfaces.Horizontal)
{
horizontalObjects.Add(spacePrefab);
}
else
{
verticalObjects.Add(spacePrefab);
}
}
if (horizontalObjects.Count > 0)
{
CreateSpaceObjects(horizontalObjects, horizontalSurfaces, PlacementSurfaces.Horizontal);
}
if (verticalObjects.Count > 0)
{
CreateSpaceObjects(verticalObjects, verticalSurfaces, PlacementSurfaces.Vertical);
}
}
/// <summary>
/// Creates and positions a collection of Placeable space objects on SurfacePlanes in the environment.
/// </summary>
/// <param name="spaceObjects">Collection of prefab GameObjects that have the Placeable component.</param>
/// <param name="surfaces">Collection of SurfacePlane objects in the world.</param>
/// <param name="surfaceType">Type of objects and planes that we are trying to match-up.</param>
private void CreateSpaceObjects(List<GameObject> spaceObjects, List<GameObject> surfaces, PlacementSurfaces surfaceType)
{
List<int> UsedPlanes = new List<int>();
// Sort the planes by distance to user.
surfaces.Sort((lhs, rhs) =>
{
Vector3 headPosition = Camera.main.transform.position;
Collider rightCollider = rhs.GetComponent<Collider>();
Collider leftCollider = lhs.GetComponent<Collider>();
// This plane is big enough, now we will evaluate how far the plane is from the user's head.
// Since planes can be quite large, we should find the closest point on the plane's bounds to the
// user's head, rather than just taking the plane's center position.
Vector3 rightSpot = rightCollider.ClosestPointOnBounds(headPosition);
Vector3 leftSpot = leftCollider.ClosestPointOnBounds(headPosition);
return Vector3.Distance(leftSpot, headPosition).CompareTo(Vector3.Distance(rightSpot, headPosition));
});
foreach (GameObject item in spaceObjects)
{
int index = -1;
Collider collider = item.GetComponent<Collider>();
if (surfaceType == PlacementSurfaces.Vertical)
{
index = FindNearestPlane(surfaces, collider.bounds.size, UsedPlanes, true);
}
else
{
index = FindNearestPlane(surfaces, collider.bounds.size, UsedPlanes, false);
}
// If we can't find a good plane we will put the object floating in space.
Vector3 position = Camera.main.transform.position + Camera.main.transform.forward * 2.0f + Camera.main.transform.right * (Random.value - 1.0f) * 2.0f;
Quaternion rotation = Quaternion.identity;
// If we do find a good plane we can do something smarter.
if (index >= 0)
{
UsedPlanes.Add(index);
GameObject surface = surfaces[index];
SurfacePlane plane = surface.GetComponent<SurfacePlane>();
position = surface.transform.position + (plane.PlaneThickness * plane.SurfaceNormal);
position = AdjustPositionWithSpatialMap(position, plane.SurfaceNormal);
rotation = Camera.main.transform.localRotation;
if (surfaceType == PlacementSurfaces.Vertical)
{
// Vertical objects should face out from the wall.
rotation = Quaternion.LookRotation(surface.transform.forward, Vector3.up);
}
else
{
// Horizontal objects should face the user.
rotation = Quaternion.LookRotation(Camera.main.transform.position);
rotation.x = 0f;
rotation.z = 0f;
}
}
//Vector3 finalPosition = AdjustPositionWithSpatialMap(position, surfaceType);
GameObject spaceObject = Instantiate(item, position, rotation) as GameObject;
spaceObject.transform.parent = gameObject.transform;
}
}
/// <summary>
/// Attempts to find a the closest plane to the user which is large enough to fit the object.
/// </summary>
/// <param name="planes">List of planes to consider for object placement.</param>
/// <param name="minSize">Minimum size that the plane is required to be.</param>
/// <param name="startIndex">Index in the planes collection that we want to start at (to help avoid double-placement of objects).</param>
/// <param name="isVertical">True, if we are currently evaluating vertical surfaces.</param>
/// <returns></returns>
private int FindNearestPlane(List<GameObject> planes, Vector3 minSize, List<int> usedPlanes, bool isVertical)
{
int planeIndex = -1;
for(int i = 0; i < planes.Count; i++)
{
if (usedPlanes.Contains(i))
{
continue;
}
Collider collider = planes[i].GetComponent<Collider>();
if (isVertical && (collider.bounds.size.x < minSize.x || collider.bounds.size.y < minSize.y))
{
// This plane is too small to fit our vertical object.
continue;
}
else if(!isVertical && (collider.bounds.size.x < minSize.x || collider.bounds.size.y < minSize.y))
{
// This plane is too small to fit our horizontal object.
continue;
}
return i;
}
return planeIndex;
}
/// <summary>
/// Adjusts the initial position of the object if it is being occluded by the spatial map.
/// </summary>
/// <param name="position">Position of object to adjust.</param>
/// <param name="surfaceNormal">Normal of surface that the object is positioned against.</param>
/// <returns></returns>
private Vector3 AdjustPositionWithSpatialMap(Vector3 position, Vector3 surfaceNormal)
{
Vector3 newPosition = position;
RaycastHit hitInfo;
float distance = 0.5f;
// Check to see if there is a SpatialMapping mesh occluding the object at its current position.
if(Physics.Raycast(position, surfaceNormal, out hitInfo, distance, SpatialMappingManager.Instance.LayerMask))
{
// If the object is occluded, reset its position.
newPosition = hitInfo.point;
}
return newPosition;
}
}
6.设置 SpatialMapping 功能开启
为了使应用能够使用空间映射数据,SpatialPerception能力必须被启用。
使用以下步骤启用此能力:
在Unity编辑器中,进入Player Settings选项(Edit > Project Settings > Player)
点击Window Store选项卡
展开Publish Settings选项,并在Capabilities列表勾选SpatialPerception选项
7.运行测试
可以放置,阴影显示绿色
不能放置,阴影显示红色
可以放置,阴影显示绿色
不能放置,阴影显示红色