Hololens入门之空间映射

Hololens入门之空间映射

本文主要讲述使用HoloToolkit项目中提供的空间映射组件,便捷快速的开始使用空间映射特性,本文示例在 Hololens入门之凝视射线 的基础上进行修改。

空间映射提供了Hololens周边真实世界物体表面的详细表示,允许开发人员创建一个具有说服力的混合现实。通过将真实世界和虚拟世界的合并,一个应用程序可以使得全息图像显得更加真实。

空间映射(Spatial mapping)有以下常见的应用场景


1、Occlusion
2、Visualization
3、Placement
4、Physics
5、Navigation

以下通过一个例子来说明空间映射的使用

1、在HoloToolkit->SpatialMapping->Prefabs 中找到并添加SpatialMapping Prefabs


在SpatialMapping中能够看到已经存在了三个脚本组件

SpatialMappingObserver.cs 主要是用于定期进行对周围环境进行扫描并更新Surface数据,具体可参考代码

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.VR.WSA;

namespace HoloToolkit.Unity
{
    /// <summary>
    /// Spatial Mapping Observer states.
    /// </summary>
    public enum ObserverStates
    {
        /// <summary>
        /// The SurfaceObserver is currently running.
        /// </summary>
        Running = 0,

        /// <summary>
        /// The SurfaceObserver is currently idle.
        /// </summary>
        Stopped = 1
    }

    /// <summary>
    /// The SpatialMappingObserver class encapsulates the SurfaceObserver into an easy to use
    /// object that handles managing the observed surfaces and the rendering of surface geometry.
    /// </summary>
    public class SpatialMappingObserver : SpatialMappingSource
    {
        [Tooltip("The number of triangles to calculate per cubic meter.")]
        public float TrianglesPerCubicMeter = 500f;

        [Tooltip("The extents of the observation volume.")]
        public Vector3 Extents = Vector3.one * 10.0f;

        [Tooltip("How long to wait (in sec) between Spatial Mapping updates.")]
        public float TimeBetweenUpdates = 3.5f;

        /// <summary>
        /// Our Surface Observer object for generating/updating Spatial Mapping data.
        /// </summary>
        private SurfaceObserver observer;

        /// <summary>
        /// A dictionary of surfaces that our Surface Observer knows about.
        /// Key: surface id
        /// Value: GameObject containing a Mesh, a MeshRenderer and a Material
        /// </summary>
        private Dictionary<int, GameObject> surfaces = new Dictionary<int, GameObject>();

        /// <summary>
        /// A queue of SurfaceData objects. SurfaceData objects are sent to the
        /// SurfaceObserver to generate meshes of the environment.
        /// </summary>
        private Queue<SurfaceData> surfaceWorkQueue = new Queue<SurfaceData>();

        /// <summary>
        /// 防止不同surface的网格同时生成,置标记位,只允许一次性生成一个surface的网格。
        /// </summary>
        private bool surfaceWorkOutstanding = false;

        /// <summary>
        /// Used to track when the Observer was last updated.
        /// </summary>
        private float updateTime;

        /// <summary>
        /// Indicates the current state of the Surface Observer.
        /// </summary>
        public ObserverStates ObserverState { get; private set; }

        private void Awake()
        {
            //为需要空间映射数据的空间区域在应用中初始化一个SurfaceObserver对象。
            observer = new SurfaceObserver();
            ObserverState = ObserverStates.Stopped;
        }

        private void Start()
        {
            //通过调用SetVolumeAsSphere、SetVolumeAsAxisAlignedBox、
            //SetVolumeAsOrientedBox、 或 SetVolumeAsFrustum方法可以为每个SurfaceObserver对象
            //指定它们需要获取数据的空间范围。以后你还可以通过再次调用它们来重新设定检测的空间范围。
            observer.SetVolumeAsAxisAlignedBox(Vector3.zero, Extents);
        }

        /// <summary>
        /// Called once per frame.
        /// </summary>
        private void Update()
        {
            // 只有在SurfaceObserver处于运行状态时进行处理
            if (ObserverState == ObserverStates.Running)
            {
                if (surfaceWorkOutstanding == false && surfaceWorkQueue.Count > 0)
                {
                    //将SurfaceData从队列中取出
                    SurfaceData surfaceData = surfaceWorkQueue.Dequeue();
                    //当RequestMeshAsync调用成功,置surfaceWorkOutstanding为true,等待回调函数SurfaceObserver_OnDataReady
                    //处理完成,再将surfaceWorkOutstanding置为false
                    surfaceWorkOutstanding = observer.RequestMeshAsync(surfaceData, SurfaceObserver_OnDataReady);
                }
                //每隔一段时间进行刷新,查看是否发生变化
                else if (surfaceWorkOutstanding == false && (Time.time - updateTime) >= TimeBetweenUpdates)
                {
                    observer.Update(SurfaceObserver_OnSurfaceChanged);
                    updateTime = Time.time;
                }
            }
        }

        /// <summary>
        /// Starts the Surface Observer.
        /// </summary>
        public void StartObserving()
        {
            if (ObserverState != ObserverStates.Running)
            {
                Debug.Log("Starting the observer.");
                ObserverState = ObserverStates.Running;

                // We want the first update immediately.
                updateTime = 0;
            }
        }

        /// <summary>
        /// Stops the Surface Observer.
        /// </summary>
        /// <remarks>Sets the Surface Observer state to ObserverStates.Stopped.</remarks>
        public void StopObserving()
        {
            if (ObserverState == ObserverStates.Running)
            {
                Debug.Log("Stopping the observer.");
                ObserverState = ObserverStates.Stopped;
            }
        }

        /// <summary>
        /// Handles the SurfaceObserver's OnDataReady event.
        /// </summary>
        /// <param name="cookedData">Struct containing output data.</param>
        /// <param name="outputWritten">Set to true if output has been written.</param>
        /// <param name="elapsedCookTimeSeconds">Seconds between mesh cook request and propagation of this event.</param>
        private void SurfaceObserver_OnDataReady(SurfaceData cookedData, bool outputWritten, float elapsedCookTimeSeconds)
        {
            GameObject surface;
            if (surfaces.TryGetValue(cookedData.id.handle, out surface))
            {
                // 设置材质
                MeshRenderer renderer = surface.GetComponent<MeshRenderer>();
                renderer.sharedMaterial = SpatialMappingManager.Instance.SurfaceMaterial;
                renderer.enabled = SpatialMappingManager.Instance.DrawVisualMeshes;

                if (SpatialMappingManager.Instance.CastShadows == false)
                {
                    renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
                }
            }

            surfaceWorkOutstanding = false;
        }

        /// <summary>
        /// Handles the SurfaceObserver's OnSurfaceChanged event.
        /// </summary>
        /// <param name="id">The identifier assigned to the surface which has changed.</param>
        /// <param name="changeType">The type of change that occurred on the surface.</param>
        /// <param name="bounds">The bounds of the surface.</param>
        /// <param name="updateTime">The date and time at which the change occurred.</param>
        //处理空间表面变化
        private void SurfaceObserver_OnSurfaceChanged(SurfaceId id, SurfaceChange changeType, Bounds bounds, System.DateTime updateTime)
        {
            if (ObserverState != ObserverStates.Running)
            {
                return;
            }

            GameObject surface;
            //关于空间表面变化,有几个典型情形需要处理。Added状态和Updated状态可以使用相同的代
            //码处理,Removed状态则使用另一种代码来处理。
            switch (changeType)
            {
                //在Added和Updated情形下,我们从字典中添加或者获取代码当前网格的对象,使用必要
                //的组件来创建一个SurfaceData结构体,然后调用RequestMeshDataAsync方法在场景中
               //使用网格数据和位置来填充对象。
                case SurfaceChange.Added:
                case SurfaceChange.Updated:
                    if (!surfaces.TryGetValue(id.handle, out surface))
                    {
                        surface = AddSurfaceObject(null, string.Format("Surface-{0}", id.handle), transform);
                        surface.AddComponent<WorldAnchor>();
                        surfaces.Add(id.handle, surface);
                    }

                    //将surface添加到队列中,等待处理
                    QueueSurfaceDataRequest(id, surface);
                    break;
                //在Removed情形下,我们从字典中移除当前网格代表的对象并销毁它。
                case SurfaceChange.Removed:
                    if (surfaces.TryGetValue(id.handle, out surface))
                    {
                        RemoveSurfaceObject(surface);
                        surfaces.Remove(id.handle);
                    }
                    break;
            }
        }

        /// <summary>
        /// Calls GetMeshAsync to update the SurfaceData and re-activate the surface object when ready.
        /// </summary>
        /// <param name="id">Identifier of the SurfaceData object to update.</param>
        /// <param name="surface">The SurfaceData object to update.</param>
        private void QueueSurfaceDataRequest(SurfaceId id, GameObject surface)
        {
            SurfaceData surfaceData = new SurfaceData(id,
                                                        //当前对象的MeshFilter组件
                                                        surface.GetComponent<MeshFilter>(),
                                                        //用于在空间中定位对象的空间锚
                                                        surface.GetComponent<WorldAnchor>(),
                                                        //当前网格对象的MeshCollider组件
                                                        surface.GetComponent<MeshCollider>(),
                                                        //每立方米网格三角形的数量
                                                        TrianglesPerCubicMeter,
                                                        true);

            surfaceWorkQueue.Enqueue(surfaceData);
        }

        /// <summary>
        /// Called when the GameObject is unloaded.
        /// </summary>
        private void OnDestroy()
        {
            // Stop the observer.
            StopObserving();

            observer.Dispose();
            observer = null;

            // Clear our surface mesh collection.
            surfaces.Clear();
        }
    }
}
SpatialMappingManager.cs 主要是对Surface的材质,是否显示网格等一些参数进行配置获取管理

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections.Generic;
using UnityEngine;

namespace HoloToolkit.Unity
{
    /// <summary>
    /// The SpatialMappingManager class allows applications to use a SurfaceObserver or a stored 
    /// Spatial Mapping mesh (loaded from a file).
    /// When an application loads a mesh file, the SurfaceObserver is stopped.
    /// Calling StartObserver() clears the stored mesh and enables real-time SpatialMapping updates.
    /// </summary>
    [RequireComponent(typeof(SpatialMappingObserver))]
    public partial class SpatialMappingManager : Singleton<SpatialMappingManager>
    {
        [Tooltip("The physics layer for spatial mapping objects to be set to.")]
        public int PhysicsLayer = 31;

        [Tooltip("The material to use for rendering spatial mapping data.")]
        public Material surfaceMaterial;

        [Tooltip("Determines if the surface observer should be automatically started.")]
        public bool autoStartObserver = true;

        [Tooltip("Determines if spatial mapping data will be rendered.")]
        public bool drawVisualMeshes = false;

        [Tooltip("Determines if spatial mapping data will cast shadows.")]
        public bool castShadows = false;

        /// <summary>
        /// Used for gathering real-time Spatial Mapping data on the HoloLens.
        /// </summary>
        private SpatialMappingObserver surfaceObserver;

        /// <summary>
        /// Time when StartObserver() was called.
        /// </summary>
        [HideInInspector]
        public float StartTime { get; private set; }

        /// <summary>
        /// The current source of spatial mapping data.
        /// </summary>
        public SpatialMappingSource Source { get; private set; }

        // Called when the GameObject is first created.
        private void Awake()
        {
            surfaceObserver = gameObject.GetComponent<SpatialMappingObserver>();
            Source = surfaceObserver;
        }

        // Use for initialization.
        private void Start()
        {
            if (autoStartObserver)
            {
                StartObserver();
            }
        }

        /// <summary>
        /// Returns the layer as a bit mask.
        /// </summary>
        public int LayerMask
        {
            get { return (1 << PhysicsLayer); }
        }

        /// <summary>
        /// The material to use when rendering surfaces.
        /// </summary>
        public Material SurfaceMaterial
        {
            get
            {
                return surfaceMaterial;
            }
            set
            {
                if (value != surfaceMaterial)
                {
                    surfaceMaterial = value;
                    SetSurfaceMaterial(surfaceMaterial);
                }
            }
        }

        /// <summary>
        /// Specifies whether or not the SpatialMapping meshes are to be rendered.
        /// </summary>
        public bool DrawVisualMeshes
        {
            get
            {
                return drawVisualMeshes;
            }
            set
            {
                if (value != drawVisualMeshes)
                {
                    drawVisualMeshes = value;
                    UpdateRendering(drawVisualMeshes);
                }
            }
        }

        /// <summary>
        /// Specifies whether or not the SpatialMapping meshes can cast shadows.
        /// </summary>
        public bool CastShadows
        {
            get
            {
                return castShadows;
            }
            set
            {
                if (value != castShadows)
                {
                    castShadows = value;
                    SetShadowCasting(castShadows);
                }
            }
        }

        /// <summary>
        /// Sets the source of surface information.
        /// </summary>
        /// <param name="mappingSource">The source to switch to. Null means return to the live stream if possible.</param>
        public void SetSpatialMappingSource(SpatialMappingSource mappingSource)
        {
            UpdateRendering(false);

            if (mappingSource == null)
            {
                Source = surfaceObserver;
            }
            else
            {
                Source = mappingSource;
            }

            UpdateRendering(DrawVisualMeshes);
        }

        /// <summary>
        /// Sets the material used by all Spatial Mapping meshes.
        /// </summary>
        /// <param name="surfaceMaterial">New material to apply.</param>
        public void SetSurfaceMaterial(Material surfaceMaterial)
        {
            SurfaceMaterial = surfaceMaterial;
            if (DrawVisualMeshes)
            {
                foreach (Renderer renderer in Source.GetMeshRenderers())
                {
                    if (renderer != null)
                    {
                        renderer.sharedMaterial = surfaceMaterial;
                    }
                }
            }
        }

        /// <summary>
        /// Checks to see if the SurfaceObserver is currently running.
        /// </summary>
        /// <returns>True, if the observer state is running.</returns>
        public bool IsObserverRunning()
        {
            return surfaceObserver.ObserverState == ObserverStates.Running;
        }

        /// <summary>
        /// Instructs the SurfaceObserver to start updating the SpatialMapping mesh.
        /// </summary>
        public void StartObserver()
        {
#if !UNITY_EDITOR
            if (!IsObserverRunning())
            {
                surfaceObserver.StartObserving();
                StartTime = Time.time;
            }
#endif
        }

        /// <summary>
        /// Instructs the SurfaceObserver to stop updating the SpatialMapping mesh.
        /// </summary>
        public void StopObserver()
        {
#if !UNITY_EDITOR
            if (IsObserverRunning())
            {
                surfaceObserver.StopObserving();
            }
#endif
        }

        /// <summary>
        /// Gets all meshes that are associated with the SpatialMapping mesh.
        /// </summary>
        /// <returns>
        /// Collection of Mesh objects representing the SpatialMapping mesh.
        /// </returns>
        public List<Mesh> GetMeshes()
        {
            List<Mesh> meshes = new List<Mesh>();
            List<MeshFilter> meshFilters = GetMeshFilters();

            // Get all valid mesh filters for observed surfaces.
            foreach (MeshFilter filter in meshFilters)
            {
                // GetMeshFilters ensures that both filter and filter.sharedMesh are not null.
                meshes.Add(filter.sharedMesh);
            }

            return meshes;
        }

        /// <summary>
        /// Gets all Mesh Filter objects associated with the Spatial Mapping mesh.
        /// </summary>
        /// <returns>Collection of Mesh Filter objects.</returns>
        public List<MeshFilter> GetMeshFilters()
        {
            return Source.GetMeshFilters();
        }

        /// <summary>
        /// Sets the Cast Shadows property for each Spatial Mapping mesh renderer.
        /// </summary>
        private void SetShadowCasting(bool castShadows)
        {
            CastShadows = castShadows;
            foreach (Renderer renderer in Source.GetMeshRenderers())
            {
                if (renderer != null)
                {
                    if (castShadows)
                    {
                        renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
                    }
                    else
                    {
                        renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
                    }
                }
            }
        }

        /// <summary>
        /// Updates the rendering state on the currently enabled surfaces.
        /// Updates the material and shadow casting mode for each renderer.
        /// </summary>
        /// <param name="Enable">True, if meshes should be rendered.</param>
        private void UpdateRendering(bool Enable)
        {
            List<MeshRenderer> renderers = Source.GetMeshRenderers();
            for (int index = 0; index < renderers.Count; index++)
            {
                if (renderers[index] != null)
                {
                    renderers[index].enabled = Enable;
                    if (Enable)
                    {
                        renderers[index].sharedMaterial = SurfaceMaterial;
                    }
                }
            }
        }
    }
}
ObjectSurfaceObserver.cs主要用于当处于Unity编辑环境下时,加载房间模型数据,来进行测试,真机环境下该脚本没啥用处

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using UnityEngine;

namespace HoloToolkit.Unity
{
    public class ObjectSurfaceObserver : SpatialMappingSource
    {
        [Tooltip("The room model to use when loading meshes in Unity.")]
        public GameObject roomModel;

        // Use this for initialization.
        private void Start()
        {
#if UNITY_EDITOR
            // When in the Unity editor, try loading saved meshes from a model.
            Load(roomModel);

            if (GetMeshFilters().Count > 0)
            {
                SpatialMappingManager.Instance.SetSpatialMappingSource(this);
            }
#endif
        }

        /// <summary>
        /// Loads the SpatialMapping mesh from the specified room object.
        /// </summary>
        /// <param name="roomModel">The room model to load meshes from.</param>
        public void Load(GameObject roomModel)
        {
            if (roomModel == null)
            {
                Debug.Log("No room model specified.");
                return;
            }

            GameObject roomObject = GameObject.Instantiate(roomModel);
            Cleanup();

            try
            {
                MeshFilter[] roomFilters = roomObject.GetComponentsInChildren<MeshFilter>();

                foreach (MeshFilter filter in roomFilters)
                {
                    GameObject surface = AddSurfaceObject(filter.sharedMesh, "roomMesh-" + surfaceObjects.Count, transform);
                    Renderer renderer = surface.GetComponent<MeshRenderer>();

                    if (SpatialMappingManager.Instance.DrawVisualMeshes == false)
                    {
                        renderer.enabled = false;
                    }

                    if (SpatialMappingManager.Instance.CastShadows == false)
                    {
                        renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
                    }

                    // Reset the surface mesh collider to fit the updated mesh. 
                    // Unity tribal knowledge indicates that to change the mesh assigned to a
                    // mesh collider, the mesh must first be set to null.  Presumably there
                    // is a side effect in the setter when setting the shared mesh to null.
                    MeshCollider collider = surface.GetComponent<MeshCollider>();
                    collider.sharedMesh = null;
                    collider.sharedMesh = surface.GetComponent<MeshFilter>().sharedMesh;
                }
            }
            catch
            {
                Debug.Log("Failed to load object " + roomModel.name);
            }
            finally
            {
                if (roomModel != null && roomObject != null)
                {
                    GameObject.DestroyImmediate(roomObject);
                }
            }
        }
    }
}

2、新增一个Cube Prefab,并且添加脚本组件CubeScript.cs


using UnityEngine;
using System.Collections;

public class CubeScript : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
    //刚启动应用时,空间映射还没准备好,将创建的Cube释放掉
	void Update () {
        if (transform.position.y < -3)
        {
            Destroy(gameObject);
        }
    }
}

3、在MainCamera上新增脚本组件CubeCreator.cs,用于每隔一秒钟产生一个Cube,用于测试

using UnityEngine;
using System.Collections;

public class CubeCreator : MonoBehaviour {
    public GameObject cubePrefab;

    // Use this for initialization
    void Start () {
        StartCoroutine(CreateCube());
    }
	
	// Update is called once per frame
	void Update () {
	
	}

    private IEnumerator CreateCube()
    {
        while (true)
        {
            float r = 1.5f;
            var theta = transform.rotation.eulerAngles.y * Mathf.Deg2Rad;
            var x = r * Mathf.Sin(theta);
            var z = r * Mathf.Cos(theta);

            Instantiate(cubePrefab,
                new Vector3(x, 1, z),
                Quaternion.Euler(0, transform.rotation.eulerAngles.y, z));

            yield return new WaitForSeconds(1);
        }
    }
}

4、在SpatialMapping上添加DrawMeshChanger.cs脚本组件,用于改变Surface的材质,一个是带网格的,一个是没有网格线的


using UnityEngine;
using System.Collections;
using HoloToolkit.Unity;
using UnityEngine.VR.WSA.Input;
using System;

public class DrawMeshChanger : MonoBehaviour {

    GestureRecognizer recognizer;

    public bool isWireframe = true;
    public Material Wireframe;
    public Material Occlusion;

    // Use this for initialization
    void Start () {
        recognizer = new GestureRecognizer();
        recognizer.TappedEvent += Recognizer_TappedEvent;
        recognizer.StartCapturingGestures();
    }

    private void Recognizer_TappedEvent(InteractionSourceKind source, int tapCount, Ray headRay)
    {
        SpatialMappingManager.Instance.SetSurfaceMaterial(isWireframe ? Occlusion : Wireframe);
        isWireframe = !isWireframe;
    }

    // Update is called once per frame
    void Update () {
	
	}
}


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


6、运行测试

能够看到在现实世界的物体表面附着一层网格线,落下的Cube可以在桌子表面,或者落到了地面上。



当在环境中进行点击,可以进行切换Surface的材质。当点击后可以看到网格消失了。




评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值