Version: 5.6
공간 매핑의 개념
공간 매핑 베스트 프랙티스

공간 매핑 기본 로우 레벨 API 사용(Spatial Mapping basic low level API usage)

Spatial Mapping 에 제공되는 로우 레벨 API는 복잡한 시스템에 대한 미세 조정을 할 수 있도록 합니다.

아래는 일반적으로 사용되는 유용한 패턴입니다.

SurfaceObserver

SurfaceObserverSpatial Mapping 로우 레벨 API의 가장 중요한 패턴입니다. 이는 디바이스가 실제 세상을 어떻게 인지하고 있는지 파악할 수 있게 합니다.

SurfaceObserver 는 월드의 볼륨을 묘사하며, 어떤 Surfaces 가 추가, 업데이트, 또는 삭제되었는지 보고합니다. 그 이후, 애플리케이션은 비동기적으로 물리 충돌 데이터를 포함/비포함한 메시 데이터를 요청할 수 있습니다. 이 요청이 완료된 이후에는, 다른 콜백이 애플리케이션에 해당 데이터가 준비되었음을 알리게 됩니다.

SurfaceObserver 는 다음과 같은 기본 기능을 제공합니다.

  1. Surface 의 추가, 삭제, 업데이트와 같은 변경 사항에 대한 콜백을 온디멘드 방식으로 생성합니다.
  2. 알려진 Surface 에 해당하는 메시 데이터를 요청하는 데 사용하는 인터페이스를 제공합니다.
  3. 요청한 메시 데이터를 사용할 수 있는 경우 콜백을 생성합니다.
  4. SurfaceObserver 의 위치와 영역을 정의하는 방법을 제공합니다.

SurfaceData

다른 중요한 부분으로는 SurfaceData 오브젝트가 있습니다. 이는 Surface 의 메시 데이터를 빌드하고 보고하는 데 필요한 모든 데이터를 포함하고 있습니다.

작성된 SurfaceData 는 시스템에서 RequestMeshAsync 호출로 패스됩니다. 메시 데이터가 준비된 경우, 그에 해당하는 SurfaceData 는 요청 시점에서 제공된 “데이터 준비됨” 콜백에 리턴됩니다. 이는 애플리케이션이 어떤 Surface 가 데이터에 해당하는지 정확하고, 명료하게 파악할 수 있도록 합니다.

애플리케이션에 필요한 정보에 기반하여 SurfaceData 를 작성해야 합니다. 이는 이들 정보를 포함합니다.

올바르지 않게 설정된 SurfaceDataRequestMeshAsync를 호출하면 인수 예외(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초에 한 번 호출하면 충분합니다.

공간 매핑의 개념
공간 매핑 베스트 프랙티스