Spatial Mapping 에 제공되는 로우 레벨 API는 복잡한 시스템에 대한 미세 조정을 할 수 있도록 합니다.
아래는 일반적으로 사용되는 유용한 패턴입니다.
SurfaceObserver는 Spatial Mapping 로우 레벨 API의 가장 중요한 패턴입니다. 이는 디바이스가 실제 세상을 어떻게 인지하고 있는지 파악할 수 있게 합니다.
SurfaceObserver 는 월드의 볼륨을 묘사하며, 어떤 Surfaces 가 추가, 업데이트, 또는 삭제되었는지 보고합니다. 그 이후, 애플리케이션은 비동기적으로 물리 충돌 데이터를 포함/비포함한 메시 데이터를 요청할 수 있습니다. 이 요청이 완료된 이후에는, 다른 콜백이 애플리케이션에 해당 데이터가 준비되었음을 알리게 됩니다.
SurfaceObserver 는 다음의 기본적 기능을 제공합니다.
다른 중요한 부분으로는 SurfaceData 오브젝트가 있습니다. 이는 Surface 의 메시 데이터를 빌드하고 보고하는 데 필요한 모든 데이터를 포함하고 있습니다.
작성된 SurfaceData 는 시스템에서 RequestMeshAsync 호출로 패스됩니다. 메시 데이터가 준비된 경우, 그에 해당하는 SurfaceData 는 요청 시점에서 제공된 “데이터 준비됨” 콜백에 리턴됩니다. 이는 애플리케이션이 어떤 Surface 가 데이터에 해당하는지 정확하고, 명료하게 파악할 수 있도록 합니다.
애플리케이션에 필요한 정보에 기반하여 SurfaceData 를 작성해야 합니다. 이는 이들 정보를 포함합니다.
올바르지 않게 설정된 SurfaceData 로 RequestMeshAsync
를 호출하는 경우, 인수 예외(argument exceptions)가 시스템에서 발생합니다. RequestMeshAsync
가 인수 예외를 발생시키지 않는 경우에도, 메시 데이터가 생성되거나 리턴되지 않을 수도 있음을 기억하십시오.
아래 예제는 이 API를 간단하게 사용하는 방법을 보여줍니다.
using UnityEngine;
using UnityEngine.VR;
using UnityEngine.VR.WSA;
using UnityEngine.Rendering;
using UnityEngine.Assertions;
using System;
using System.Collections;
using System.Collections.Generic;
public enum BakedState {
NeverBaked = 0,
Baked = 1,
UpdatePostBake = 2
}
// Data that is kept to prioritize surface baking.
class SurfaceEntry {
public GameObject m_Surface; // the GameObject corresponding to this surface
public int m_Id; // ID for this surface
public DateTime m_UpdateTime; // update time as reported by the system
public BakedState m_BakedState;
public const float c_Extents = 5.0f;
}
public class SMSample : MonoBehaviour {
// This observer is the window into the spatial mapping world.
SurfaceObserver m_Observer;
// This dictionary contains the set of known spatial mapping surfaces.
// Surfaces are updated, added, and removed on a regular basis.
Dictionary<int, SurfaceEntry> m_Surfaces;
// This is the material with which the baked surfaces are drawn.
public Material m_drawMat;
// This flag is used to postpone requests if a bake is in progress. Baking mesh
// data can take multiple frames. This sample prioritizes baking request
// order based on surface data surfaces and will only issue a new request
// if there are no requests being processed.
bool m_WaitingForBake;
// This is the last time the SurfaceObserver was updated. It is updated no
// more than every two seconds because doing so is potentially time-consuming.
float m_lastUpdateTime;
void Start () {
m_Observer = new SurfaceObserver ();
m_Observer.SetVolumeAsAxisAlignedBox (new Vector3(0.0f, 0.0f, 0.0f),
new Vector3 (SurfaceEntry.c_Extents, SurfaceEntry.c_Extents, SurfaceEntry.c_Extents));
m_Surfaces = new Dictionary<int, SurfaceEntry> ();
m_WaitingForBake = false;
m_lastUpdateTime = 0.0f;
}
void Update () {
// Avoid calling Update on a SurfaceObserver too frequently.
if (m_lastUpdateTime + 2.0f < Time.realtimeSinceStartup) {
// This block makes the observation volume follow the camera.
Vector3 extents;
extents.x = SurfaceEntry.c_Extents;
extents.y = SurfaceEntry.c_Extents;
extents.z = SurfaceEntry.c_Extents;
m_Observer.SetVolumeAsAxisAlignedBox (Camera.main.transform.position, extents);
try {
m_Observer.Update (SurfaceChangedHandler);
} catch {
// Update can throw an exception if the specified callback was bad.
Debug.Log ("Observer update failed unexpectedly!");
}
m_lastUpdateTime = Time.realtimeSinceStartup;
}
if (!m_WaitingForBake) {
// Prioritize older adds over other adds over updates.
SurfaceEntry bestSurface = null;
foreach (KeyValuePair<int, SurfaceEntry> surface in m_Surfaces) {
if (surface.Value.m_BakedState != BakedState.Baked) {
if (bestSurface == null) {
bestSurface = surface.Value;
} else {
if (surface.Value.m_BakedState < bestSurface.m_BakedState) {
bestSurface = surface.Value;
} else if (surface.Value.m_UpdateTime < bestSurface.m_UpdateTime) {
bestSurface = surface.Value;
}
}
}
}
if (bestSurface != null) {
// Fill out and dispatch the request.
SurfaceData sd;
sd.id.handle = bestSurface.m_Id;
sd.outputMesh = bestSurface.m_Surface.GetComponent<MeshFilter> ();
sd.outputAnchor = bestSurface.m_Surface.GetComponent<WorldAnchor> ();
sd.outputCollider = bestSurface.m_Surface.GetComponent<MeshCollider> ();
sd.trianglesPerCubicMeter = 300.0f;
sd.bakeCollider = true;
try {
if (m_Observer.RequestMeshAsync(sd, SurfaceDataReadyHandler)) {
m_WaitingForBake = true;
} else {
// A return value of false when requesting meshes
// typically indicates that the specified surface
// ID specified was invalid.
Debug.Log(System.String.Format ("Bake request for {0} failed. Is {0} a valid Surface ID?", bestSurface.m_Id));
}
}
catch {
// Requests can fail if the data struct is not filled out
// properly.
Debug.Log (System.String.Format("Bake for id {0} failed unexpectedly!", bestSurface.m_Id));
}
}
}
}
// This handler receives surface changed events and is propagated by the
// Update method on SurfaceObserver.
void SurfaceChangedHandler (SurfaceId id, SurfaceChange changeType, Bounds bounds, DateTime updateTime) {
SurfaceEntry entry;
switch (changeType) {
case SurfaceChange.Added:
case SurfaceChange.Updated:
if (m_Surfaces.TryGetValue(id.handle, out entry)) {
// If this surface has already been baked, mark it as needing bake
// in addition to the update time so the "next surface to bake"
// logic will order it correctly.
if (entry.m_BakedState == BakedState.Baked) {
entry.m_BakedState = BakedState.UpdatePostBake;
entry.m_UpdateTime = updateTime;
}
} else {
// This is a brand new surface so create an entry for it.
entry = new SurfaceEntry ();
entry.m_BakedState = BakedState.NeverBaked;
entry.m_UpdateTime = updateTime;
entry.m_Id = id.handle;
entry.m_Surface = new GameObject (System.String.Format("Surface-{0}", id.handle));
entry.m_Surface.AddComponent<MeshFilter> ();
entry.m_Surface.AddComponent<MeshCollider> ();
MeshRenderer mr = entry.m_Surface.AddComponent<MeshRenderer> ();
mr.shadowCastingMode = ShadowCastingMode.Off;
mr.receiveShadows = false;
entry.m_Surface.AddComponent<WorldAnchor> ();
entry.m_Surface.GetComponent<MeshRenderer> ().sharedMaterial = m_drawMat;
m_Surfaces[id.handle] = entry;
}
break;
case SurfaceChange.Removed:
if (m_Surfaces.TryGetValue(id.handle, out entry)) {
m_Surfaces.Remove (id.handle);
Mesh mesh = entry.m_Surface.GetComponent<MeshFilter> ().mesh;
if (mesh) {
Destroy (mesh);
}
Destroy (entry.m_Surface);
}
break;
}
}
void SurfaceDataReadyHandler(SurfaceData sd, bool outputWritten, float elapsedBakeTimeSeconds) {
m_WaitingForBake = false;
SurfaceEntry entry;
if (m_Surfaces.TryGetValue(sd.id.handle, out entry)) {
// These two asserts are checking that the returned filter and WorldAnchor
// are the same ones that the data was requested with. That should always
// be true here unless code has been changed to replace or destroy them.
Assert.IsTrue (sd.outputMesh == entry.m_Surface.GetComponent<MeshFilter>());
Assert.IsTrue (sd.outputAnchor == entry.m_Surface.GetComponent<WorldAnchor>());
entry.m_BakedState = BakedState.Baked;
} else {
Debug.Log (System.String.Format("Paranoia: Couldn't find surface {0} after a bake!", sd.id.handle));
Assert.IsTrue (false);
}
}
}
업데이트를 호출하려면 리소스가 많이 필요하기 때문에, 애플리케이션이 요구하는 이상으로 호출되어서는 안됨을 기억하십시오. 대부분의 경우, Update 는 3초에 한 번 호출하면 충분합니다.