Hololens入门之空间映射(放置物体)

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;
        }
    }
}


新增PlanesManager.cs脚本组件,该脚本主要是用来控制生成平面,以及生成平面后创建游戏对象
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;
    }
}


7、为了使用空间映射数据,SpatialPerception能力必须被启用


8、运行测试

可以放置,阴影显示绿色


不能放置,阴影显示红色


放置后效果


能够被放置


不能被放置


放置后效果


  • 8
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值