空间映射提供了HoloLens周围环境中真实世界表面的详细表示,允许开发人员创建令人信服的混合现实体验。通过将真实世界与虚拟世界合并,应用可以使全息图看起来是真实的。通过提供熟悉的现实世界行为和交互,应用程序也可以更自然地与用户期望一致。
Unity内置了对空间映射功能的支持,通过以下两种方式提供给开发者:
- HoloToolkit项目中你可以找到空间映射组件,这可以让你便捷快速地开始使用空间映射特性。
- Unity还提供更多底层的空间映射API,以便开发者能够完全控制空间映射特性,满足定制复杂的应用需求
本文主要介绍HoloToolKit提供的空间映射(SpatialMapping)组件,利用此组件可以快速的应用中集成使用HoloLens的空间映射特性。
空间映射(SpatialMapping)主要有下面的使用用途:
- Occlusion
- Visualization
- Placement
- Physics
- Navigation
设置 SpatialMapping 功能开启
为了使应用能够使用空间映射数据,SpatialPerception能力必须被启用。
使用以下步骤启用此能力:
- 在Unity编辑器中,进入Player Settings选项(Edit > Project Settings > Player)
- 点击Window Store选项卡
- 展开Publish Settings选项,并在Capabilities列表勾选SpatialPerception选项
use the API 使用底层API步骤
SurfaceObserver是主要使用到的API对象,下面是应用使用空间映射特性推荐的大致流程:
- 设定SurfaceObserver对象
要为每一个需要空间映射数据的空间区域在应用中声明并初始化一个SurfaceObserver对象:
SurfaceObserver surfaceObserver;
void Start () {
surfaceObserver = new SurfaceObserver();
}
- 通过调用SetVolumeAsSphere、SetVolumeAsAxisAlignedBox、 SetVolumeAsOrientedBox、 或 SetVolumeAsFrustum方法可以为每个SurfaceObserver对象指定它们需要获取数据的空间范围。以后你还可以通过再次调用它们来重新设定检测的空间范围。
void Start () {
...
surfaceObserver.SetVolumeAsAxisAlignedBox(Vector3.zero, new Vector3(3, 3, 3));
}
处理OnDataReady事件
OnDataReady事件方法会接收到一个SurfaceData对象,它包含了WorldAnchor、MeshFilter和MeshCollider对象数据,表示了当前关联的空间表面最新状态。通过访问Mesh Filter对象的Mesh数据可以进行性能分析或者处理网格。使用最新的Mesh数据来渲染空间表面并将它用于物理碰撞或者射线击中对象。确认SurfaceData内容不为空很重要。-
处理空间表面变化,即处理OnSurfaceChanged事件
关于空间表面变化,有几个典型情形需要处理。Added状态和Updated状态可以使用相同的代码处理,Removed状态则使用另一种代码来处理。
- 在Added和Updated情形下,我们从字典中添加或者获取代码当前网格的对象,使用必要的组件来创建一个SurfaceData结构体,然后调用RequestMeshDataAsync方法在场景中使用网格数据和位置来填充对象。
- 在Removed情形下,我们从字典中移除当前网格代表的对象并销毁它。
System.Collections.Generic.Dictionary spatialMeshObjects = new System.Collections.Generic.Dictionary();
private void OnSurfaceChanged(SurfaceId surfaceId, SurfaceChange changeType, Bounds bounds, System.DateTime updateTime)
{
switch (changeType)
{
case SurfaceChange.Added:
case SurfaceChange.Updated:
if (!spatialMeshObjects.ContainsKey(surfaceId))
{
spatialMeshObjects[surfaceId] = new GameObject("spatial-mapping-" + surfaceId);
spatialMeshObjects[surfaceId].transform.parent = this.transform;
spatialMeshObjects[surfaceId].AddComponent();
}
GameObject target = spatialMeshObjects[surfaceId];
SurfaceData sd = new SurfaceData(
//系统返回的surface id,
//当前对象的MeshFilter组件
target.GetComponent() ?? target.AddComponent(),
//用于在空间中定位对象的空间锚
target.GetComponent() ?? target.AddComponent(),
//当前网格对象的MeshCollider组件
target.GetComponent() ?? target.AddComponent(),
//每立方米网格三角形的数量
1000,
//bakeMeshes -如果是true,MeshCollider会被数据填充,反之MeshCollider为空
true
);
SurfaceObserver.RequestMeshAsync(sd, OnDataReady);
break;
case SurfaceChange.Removed:
var obj = spatialMeshObjects[surfaceId];
spatialMeshObjects.Remove(surfaceId);
if (obj != null)
{
GameObject.Destroy(obj);
}
break;
default:
break;
}
}
SpatialMapping Components 空间映射组件
下面通过一个例子来说明空间映射的使用
1.在HoloToolkit->SpatialMapping->Prefabs 中找到并添加SpatialMapping Prefabs
在SpatialMapping中能够看到已经存在了三个脚本组件:
- SpatialMappingObserver.cs
- SpatialMappingManager.cs
- ObjectSurfaceObserver.cs
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
{
///
/// Spatial Mapping Observer states.
///
public enum ObserverStates
{
///
/// The SurfaceObserver is currently running.
///
Running = 0,
///
/// The SurfaceObserver is currently idle.
///
Stopped = 1
}
///
/// The SpatialMappingObserver class encapsulates the SurfaceObserver into an easy to use
/// object that handles managing the observed surfaces and the rendering of surface geometry.
///
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;
[Tooltip("Recalculates normals whenever a mesh is updated.")]
public bool RecalculateNormals = false;
///
/// Our Surface Observer object for generating/updating Spatial Mapping data.
///
/// SurfaceObserver对象用于生成或更新空间映射数据
private SurfaceObserver observer;
///
/// A dictionary of surfaces that our Surface Observer knows about.
/// Key: surface id
/// Value: GameObject containing a Mesh, a MeshRenderer and a Material
///
private Dictionary surfaces = new Dictionary();
///
/// A queue of SurfaceData objects. SurfaceData objects are sent to the
/// SurfaceObserver to generate meshes of the environment.
///
private Queue surfaceWorkQueue = new Queue();
///
/// To prevent too many meshes from being generated at the same time, we will
/// only request one mesh to be created at a time. This variable will track
/// if a mesh creation request is in flight.
/// 防止不同surface的网格同时生成,置标记位,只允许一次性生成一个surface的网格。
///
private bool surfaceWorkOutstanding = false;
///
/// Used to track when the Observer was last updated.
/// 用于跟踪观察者的上次更新时间
///
private float updateTime;
///
/// Indicates the current state of the Surface Observer.
///
public ObserverStates ObserverState { get; private set; }
private void Awake()
{
//为每一个需要空间映射数据的空间区域在应用中初始化一个SurfaceObserver对象
observer = new SurfaceObserver();
ObserverState = ObserverStates.Stopped;
}
///
/// Called when the GaemObject is initialized.
///
private void Start()
{
//通过调用SetVolumeAsSphere、SetVolumeAsAxisAlignedBox、
//SetVolumeAsOrientedBox、 或 SetVolumeAsFrustum方法可以为每个SurfaceObserver对象
//指定它们需要获取数据的空间范围。可再次调用来重新设定检测的空间范围。
observer.SetVolumeAsAxisAlignedBox(Vector3.zero, Extents);
}
///
/// Called once per frame.
///
private void Update()
{
//只有在SurfaceObserver处于运行状态时进行处理
if (ObserverState == ObserverStates.Running)
{
// If we don't have mesh creation in flight, but we could schedule mesh creation, do so.
if (surfaceWorkOutstanding == false && surfaceWorkQueue.Count > 0)
{
// Pop the SurfaceData off the queue. A more sophisticated algorithm could prioritize
// the queue based on distance to the user or some other metric.
//将SurfaceData从队列中取出
SurfaceData surfaceData = surfaceWorkQueue.Dequeue();
// If RequestMeshAsync succeeds, then we have successfully scheduled mesh creation.
//当RequestMeshAsync调用成功,则就成功调度了网格创建,置surfaceWorkOutstanding为true
//等待回调函数SurfaceObserver_OnDataReady处理完成,再将surfaceWorkOutstanding置为false
surfaceWorkOutstanding = observer.RequestMeshAsync(surfaceData, SurfaceObserver_OnDataReady);
}
// If we don't have any other work to do, and enough time has passed since the previous
// update request, request updates for the spatial mapping data.
//每隔一段时间进行刷新,查看是否发生变化
else if (surfaceWorkOutstanding == false && (Time.time - updateTime) >= TimeBetweenUpdates)
{
observer.Update(SurfaceObserver_OnSurfaceChanged);
updateTime = Time.time;
}
}
}
///
/// Starts the Surface Observer.
///
public void StartObserving()
{
if (ObserverState != ObserverStates.Running)
{
Debug.Log("Starting the observer.");
ObserverState = ObserverStates.Running;
// We want the first update immediately.
updateTime = 0;
}
}
///
/// Stops the Surface Observer.
///
/// Sets the Surface Observer state to ObserverStates.Stopped.
public void StopObserving()
{
if (ObserverState == ObserverStates.Running)
{
Debug.Log("Stopping the observer.");
ObserverState = ObserverStates.Stopped;
}
}
///
/// Handles the SurfaceObserver's OnDataReady event.
///
/// Struct containing output data.
/// Set to true if output has been written.
/// Seconds between mesh cook request and propagation of this event.
private void SurfaceObserver_OnDataReady(SurfaceData cookedData, bool outputWritten, float elapsedCookTimeSeconds)
{
GameObject surface;
if (surfaces.TryGetValue(cookedData.id.handle, out surface))
{
// Set the draw material for the renderer.
//设置材质
MeshRenderer renderer = surface.GetComponent();
renderer.sharedMaterial = SpatialMappingManager.Instance.SurfaceMaterial;
renderer.enabled = SpatialMappingManager.Instance.DrawVisualMeshes;
if (RecalculateNormals)
{
MeshFilter filter = surface.GetComponent();
if (filter != null && filter.sharedMesh != null)
{
filter.sharedMesh.RecalculateNormals();
}
}
if (SpatialMappingManager.Instance.CastShadows == false)
{
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
}
}
surfaceWorkOutstanding = false;
}
///
/// Handles the SurfaceObserver's OnSurfaceChanged event.
///处理空间表面变化事件
///
/// The identifier assigned to the surface which has changed.
/// The type of change that occurred on the surface.
/// The bounds of the surface.
/// The date and time at which the change occurred.
private void SurfaceObserver_OnSurfaceChanged(SurfaceId id, SurfaceChange changeType, Bounds bounds, System.DateTime updateTime)
{
// Verify that the client of the Surface Observer is expecting updates.
if (ObserverState != ObserverStates.Running)
{
return;
}
GameObject surface;
//关于空间表面变化,有几个典型情形需要处理。Added状态和Updated状态可以使用相同的代码处理,Removed状态则使用另一种代码来处理。
switch (changeType)
{
// Adding and updating are nearly identical. The only difference is if a new GameObject to contain
// the surface needs to be created.
//在Added和Updated情形下,我们从字典中添加或者获取代码当前网格的对象,使用必要
//的组件来创建一个SurfaceData结构体,然后调用RequestMeshDataAsync方法在场景中
//使用网格数据和位置来填充对象。
case SurfaceChange.Added:
case SurfaceChange.Updated:
// Check to see if the surface is known to the observer.
if (!surfaces.TryGetValue(id.handle, out surface))
{
// If we are adding a new surface, construct a GameObject
// to represent its state and attach some Mesh-related
// components to it.
surface = AddSurfaceObject(null, string.Format("Surface-{0}", id.handle), transform);
surface.AddComponent();
// Add the surface to our dictionary of known surfaces so
// we can interact with it later.
surfaces.Add(id.handle, surface);
}
// Add the request to create the mesh for this surface to our work queue.
//将surface添加到队列中,等待处理
QueueSurfaceDataRequest(id, surface);
break;
//在Removed情形下,我们从字典中移除当前网格代表的对象并销毁它。
case SurfaceChange.Removed:
// Always process surface removal events.
if (surfaces.TryGetValue(id.handle, out surface))
{
RemoveSurfaceObject(surface);
surfaces.Remove(id.handle);
}
break;
}
}
///
/// Calls GetMeshAsync to update the SurfaceData and re-activate the surface object when ready.
///
/// Identifier of the SurfaceData object to update.
/// The SurfaceData object to update.
private void QueueSurfaceDataRequest(SurfaceId id, GameObject surface)
{
SurfaceData surfaceData = new SurfaceData(id,
surface.GetComponent(), //当前对象的MeshFilter组件
surface.GetComponent(), //用于在空间中定位对象的空间锚
surface.GetComponent(), //当前网格对象的MeshCollider组件
TrianglesPerCubicMeter, //每立方米网格三角形的数量
true);
surfaceWorkQueue.Enqueue(surfaceData);
}
///
/// Called when the GameObject is unloaded.
///
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
{
///
/// 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.
///
[RequireComponent(typeof(SpatialMappingObserver))]
public partial class SpatialMappingManager : Singleton
{
[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 spatial mapping data will be rendered.")]
public bool drawVisualMeshes = false;
[Tooltip("Determines if spatial mapping data will cast shadows.")]
public bool castShadows = false;
///
/// Used for gathering real-time Spatial Mapping data on the HoloLens.
///
private SpatialMappingObserver surfaceObserver;
///
/// Used for loading spatial mapping data from a room model.
///
private ObjectSurfaceObserver objectSurfaceObserver;
///
/// Time when StartObserver() was called.
///
[HideInInspector]
public float StartTime { get; private set; }
///
/// The current source of spatial mapping data.
///
public SpatialMappingSource Source { get; private set; }
// Called when the GameObject is first created.
private void Awake()
{
surfaceObserver = gameObject.GetComponent();
Source = surfaceObserver;
}
// Use for initialization.
private void Start()
{
#if !UNITY_EDITOR
StartObserver();
#endif
#if UNITY_EDITOR
objectSurfaceObserver = GetComponent();
if (objectSurfaceObserver != null)
{
// In the Unity editor, try loading saved meshes from a model.
objectSurfaceObserver.Load(objectSurfaceObserver.roomModel);
if (objectSurfaceObserver.GetMeshFilters().Count > 0)
{
SetSpatialMappingSource(objectSurfaceObserver);
}
}
#endif
}
///
/// Returns the layer as a bit mask.
///
public int LayerMask
{
get { return (1 << PhysicsLayer); }
}
///
/// The material to use when rendering surfaces.
///
public Material SurfaceMaterial
{
get
{
return surfaceMaterial;
}
set
{
if (value != surfaceMaterial)
{
surfaceMaterial = value;
SetSurfaceMaterial(surfaceMaterial);
}
}
}
///
/// Specifies whether or not the SpatialMapping meshes are to be rendered.
///
public bool DrawVisualMeshes
{
get
{
return drawVisualMeshes;
}
set
{
if (value != drawVisualMeshes)
{
drawVisualMeshes = value;
UpdateRendering(drawVisualMeshes);
}
}
}
///
/// Specifies whether or not the SpatialMapping meshes can cast shadows.
///
public bool CastShadows
{
get
{
return castShadows;
}
set
{
if (value != castShadows)
{
castShadows = value;
SetShadowCasting(castShadows);
}
}
}
///
/// Sets the source of surface information.
///
/// The source to switch to. Null means return to the live stream if possible.
public void SetSpatialMappingSource(SpatialMappingSource mappingSource)
{
UpdateRendering(false);
if (mappingSource == null)
{
Source = surfaceObserver;
}
else
{
Source = mappingSource;
}
UpdateRendering(DrawVisualMeshes);
}
///
/// Sets the material used by all Spatial Mapping meshes.
///
/// New material to apply.
public void SetSurfaceMaterial(Material surfaceMaterial)
{
SurfaceMaterial = surfaceMaterial;
if (DrawVisualMeshes)
{
foreach (Renderer renderer in Source.GetMeshRenderers())
{
if (renderer != null)
{
renderer.sharedMaterial = surfaceMaterial;
}
}
}
}
///
/// Checks to see if the SurfaceObserver is currently running.
///
/// True, if the observer state is running.
public bool IsObserverRunning()
{
return surfaceObserver.ObserverState == ObserverStates.Running;
}
///
/// Instructs the SurfaceObserver to start updating the SpatialMapping mesh.
///
public void StartObserver()
{
if (!IsObserverRunning())
{
surfaceObserver.StartObserving();
StartTime = Time.time;
}
}
///
/// Instructs the SurfacesurfaceObserver to stop updating the SpatialMapping mesh.
///
public void StopObserver()
{
if (IsObserverRunning())
{
surfaceObserver.StopObserving();
}
}
///
/// Gets all meshes that are associated with the SpatialMapping mesh.
///
///
/// Collection of Mesh objects representing the SpatialMapping mesh.
///
public List GetMeshes()
{
List meshes = new List();
List 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;
}
///
/// Gets all Mesh Filter objects associated with the Spatial Mapping mesh.
///
/// Collection of Mesh Filter objects.
public List GetMeshFilters()
{
return Source.GetMeshFilters();
}
///
/// Sets the Cast Shadows property for each Spatial Mapping mesh renderer.
///
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;
}
}
}
}
///
/// Updates the rendering state on the currently enabled surfaces.
///
/// True, if meshes should be rendered.
private void UpdateRendering(bool Enable)
{
List 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编辑环境下时,加载房间模型数据,来进行测试:
-
使用device Protal可以在浏览器查看所扫描的房间。点击update可以在上面视窗中显示查看,点击save进行将你所在的房间模型进行保存:
-
将保存的房间模型直接加载进unity项目Assets文件目录下,再将其拖拽到ObjectSurfaceObserver.cs脚本组件的Room Model中即可在项目文件中保留房间模型。当在unity环境中运行调试时,可加载此房间模型数据进行测试。
ObjectSurfaceObserver.cs详细代码如下:
// 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
}
///
/// Loads the SpatialMapping mesh from the specified room object.
///
/// The room model to load meshes from.
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();
foreach (MeshFilter filter in roomFilters)
{
GameObject surface = AddSurfaceObject(filter.sharedMesh, "roomMesh-" + surfaceObjects.Count, transform);
Renderer renderer = surface.GetComponent();
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();
collider.sharedMesh = null;
collider.sharedMesh = surface.GetComponent().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. 运行测试
能够看到在现实世界的物体表面附着一层网格线,落下的Cube可以在桌子表面,或者落到了地面上。当在环境中进行点击,可以进行切换Surface的材质。当点击后可以看到网格消失了。
(MD编辑模式下不能插入视频,移步视频链接:https://v.qq.com/x/page/e0350sn17nw.html)