Hololens入门之空间映射(放置物体)
本文讲述怎样使用HoloToolkit构建工程,实现在空间映射后,将网格转换为平面,然后构建游戏对象,将游戏对象放置到垂直平面或者水平平面的功能。
本文示例在 Hololens入门之凝视 示例的基础上进行修改
1、在Manager上添加GestureManager.cs脚本组件(直接使用Holotoolkit中的脚本,该脚本在本文中不在讲述,可前往手势识别章节进行查看)
2、在HoloToolkit->SpatialMapping->Prefabs 中找到并添加SpatialMapping Prefabs
SpatialMapping 中脚本在本文中不再描述,可以前往 Hololens入门之空间映射 进行查看
3、创建空的游戏对象 SpatialProcessing
在SpatialProcessing上添加SurfaceMeshesToPlanes.cs 和 RemoveSurfaceVertices.cs脚本组件 (可以直接在HoloToolkit->SpatialMapping->Scripts->SpatialProcessing中找到)
SurfaceMeshesToPlanes.cs脚本主要是用来将扫描空间后生成的网格转换成平面,添加完该脚本后,在HoloToolkit->SpatialMapping->Prefabs中找到SurfacePlane,将其拖拽到SurfaceMeshesToPlanes.cs的SurfacePlanePrefab上
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
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>
/// 根据SpatialMappingManager's Observer返回的网格信息,找到并创建平面.
/// </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;
//配置那种类型的平面被渲染
[HideInInspector]
public PlaneTypes drawPlanesMask =
(PlaneTypes.Wall | PlaneTypes.Floor | PlaneTypes.Ceiling | PlaneTypes.Table);
// 配置哪种类型的平面被丢弃
[HideInInspector]
public PlaneTypes destroyPlanesMask = PlaneTypes.Unknown;
// 地面的y坐标,处于用户头部位置以下的面积最大的水平区域
public float FloorYPosition { get; private set; }
//天花板y坐标,处于用户头部位置以上的面积最大的水平区域
public float CeilingYPosition { get; private set; }
/// <summary>
/// 当平面创建完成进行触发
/// </summary>
/// <param name="source"></param>
/// <param name="args"></param>
public delegate void EventHandler(object source, EventArgs args);
/// <summary>
/// 当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>
/// 标记当前是否正在创建平面
/// </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
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;
//为了不影响帧率,使用协程将工作分割到不同帧中完成
StartCoroutine(MakePlanesRoutine());
}
}
/// <summary>
/// 返回所有指定的平面类型的平面列表
/// </summary>
/// <param name="planeTypes">平面类型</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>
/// 分析网格信息,找到并用新的对象替换每个平面
/// </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
// 暂停任务,等待下一帧处理下面的代码
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;
}
}
}
// Create SurfacePlane objects to represent each plane found in the Spatial Mapping mesh.
for (int index = 0; index < planes.Length; index++)
{
GameObject destPlane;
BoundedPlane boundedPlane = planes[index];
// Instantiate a SurfacePlane object, which will have the same bounds as our BoundedPlane object.
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>();
// Set the Plane property to adjust transform position/scale/rotation and determine plane type.
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 主要用来删除网格三角型
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
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;
}
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Windows.Speech;
using HoloToolkit.Unity;
public class PlanesManager : Singleton<PlanesManager>
{
[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>
/// 标记表面网格是否处理完成
/// </summary>
private bool meshesProcessed = false;
private void Start()
{
//设置Surface表面材质
SpatialMappingManager.Instance.SetSurfaceMaterial(defaultMaterial);
//注册生成平面后的回调事件
SurfaceMeshesToPlanes.Instance.MakePlanesComplete += SurfaceMeshesToPlanes_MakePlanesComplete;
}
/// <summary>
/// 每帧调用
/// </summary>
private void Update()
{
//检查表面网格是否处理完成,且扫描空间的时间是否有限制
if (!meshesProcessed && limitScanningByTime)
{
//检查是否超出了扫描上限时间
if (limitScanningByTime && ((Time.time - SpatialMappingManager.Instance.StartTime) < scanTime))
{
}
else
{
//当达到扫描的时间,停止对空间的扫描映射
if(SpatialMappingManager.Instance.IsObserverRunning())
{
SpatialMappingManager.Instance.StopObserver();
}
//创建平面
CreatePlanes();
//标记表面网格处理完成
meshesProcessed = true;
}
}
}
/// <summary>
/// 当SurfaceMeshesToPlanes中 生成Planes处理完成,调用该事件
/// </summary>
/// <param name="source">事件源</param>
/// <param name="args">事件参数</param>
private void SurfaceMeshesToPlanes_MakePlanesComplete(object source, System.EventArgs args)
{
//水平平面列表(地面,桌面等)
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)
{
//删除顶点
RemoveVertices(SurfaceMeshesToPlanes.Instance.ActivePlanes);
//设置第二表面材质
SpatialMappingManager.Instance.SetSurfaceMaterial(secondaryMaterial);
//获取平面后,在世界中生成物体,传入参数为上面获取到的,水平平面,垂直平面的列表
ObjectCollectionManager.Instance.GenerateItemsInWorld(horizontal, vertical);
}
else
{
//未扫描到可以放置物体的垂直平面和水平平面,重新进行空间扫描
SpatialMappingManager.Instance.StartObserver();
//重新标记表面网格未处理完成
meshesProcessed = false;
}
}
/// <summary>
/// 将扫描得到的空间映射信息转换成平面
/// </summary>
private void CreatePlanes()
{
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脚本组件
该脚本用于判断虚拟物体在某区域内是否可以放置
using System.Collections.Generic;
using UnityEngine;
using HoloToolkit.Unity;
public enum PlacementSurfaces
{
Horizontal = 1,
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>();
//标记是否正在被放置
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;
// 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;
//物体被放置的目标位置
private Vector3 targetPosition;
private void Awake()
{
targetPosition = gameObject.transform.position;
//获取或创建对撞机
boxCollider = gameObject.GetComponent<BoxCollider>();
if (boxCollider == null)
{
managingBoxCollider = true;
boxCollider = gameObject.AddComponent<BoxCollider>();
boxCollider.enabled = false;
}
//创建对象表名gameObject的界限
boundsAsset = GameObject.CreatePrimitive(PrimitiveType.Cube);
boundsAsset.transform.parent = gameObject.transform;
boundsAsset.SetActive(false);
//创建gameObject的阴影对象
shadowAsset = GameObject.CreatePrimitive(PrimitiveType.Quad);
shadowAsset.transform.parent = gameObject.transform;
shadowAsset.SetActive(false);
}
//gameObject被选中时调用
public void OnSelect()
{
if (!IsPlacing)
{
//开始放置物体
OnPlacementStart();
}
else
{
//停止移动,放置物体
OnPlacementStop();
}
}
private void Update()
{
if (IsPlacing)
{
//处于移动被放置状态,移动物体
Move();
Vector3 targetPosition;
Vector3 surfaceNormal;
//检查当前位置是否可以放置该物体
bool canBePlaced = ValidatePlacement(out targetPosition, out surfaceNormal);
//显示边界
DisplayBounds(canBePlaced);
//显示阴影
DisplayShadow(targetPosition, surfaceNormal, canBePlaced);
}
else
{
//隐藏边界和阴影
boundsAsset.SetActive(false);
shadowAsset.SetActive(false);
// 将该对象物体放置在物体表面
float dist = (gameObject.transform.position - targetPosition).magnitude;
if (dist > 0)
{
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, targetPosition, placementVelocity / dist);
}
else
{
//显示物体的子物体
for (int i = 0; i < ChildrenToHide.Count; i++)
{
ChildrenToHide[i].SetActive(true);
}
}
}
}
/// <summary>
///检查物体是否可以被放置
/// </summary>
/// <param name="position">
/// 目标位置
/// </param>
/// <param name="surfaceNormal">
/// 物体被放置的平面的法向量
/// </param>
/// <returns>
/// 可以被放置返回true,不能被放置,返回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);
}
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()
{
Vector3 position;
Vector3 surfaceNormal;
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;
}
}
5、新增测试对象
新增两个cube,并添加上面创建的Placeable.cs脚本组件
Cube1用于测试放置在墙面上,设置PlacementSurface为Vertical
Cube2用于测试放置在地面上,设置PlacementSurface为Horizontal
其中涉及到的这些材质,可以自行进行新增几个材质球
PlaceableBounds用于在可放置时显示的边界
NotPlaceableBounds 不可放置时显示的边界
PlaceableShadow 可放置时显示的阴影
NotPlaceableShadow 不可放置时显示的阴影
6、新增游戏对象集合ObjectCollection
创建空对象,并且新增ObjectCollectionManager.cs脚本组件,将上面创建的两个cube拖拽进去
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 ObjectCollectionManager : Singleton<ObjectCollectionManager>
{
[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;
}
}
8、运行测试
可以放置,阴影显示绿色
不能放置,阴影显示红色
放置后效果
能够被放置
不能被放置
放置后效果