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

공간 매핑 기본 저수준 API 사용

공간 매핑 을 위해 제공되는 저수준 API를 사용하여 복잡한 시스템을 매우 세부적으로 제어할 수 있습니다.

유용하게 사용되는 일반적인 사용 패턴은 다음과 같습니다.

SurfaceObserver

SurfaceObserverSpatial Mapping 저수준 API의 가장 중요한 구성 요소입니다. 이를 통해 기기에서 실제 세상을 어떻게 인지하는지 파악할 수 있습니다.

SurfaceObserver 는 월드의 영역을 묘사하고 어떤 표면 이 추가되거나, 업데이트되거나, 또는 삭제되었는지 보고합니다. 그런 다음, 애플리케이션은 물리 충돌 데이터와 함께 또는 물리 충돌 데이터 없이 메시 데이터를 비동기식으로 요청할 수 있습니다. 요청이 이행되면 다른 콜백을 통해 해당 데이터가 준비되었음을 애플리케이션에 알립니다.

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

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

SurfaceData

또 하나의 중요한 요소는 SurfaceData 오브젝트입니다. 이 오브젝트에는 Surface 의 메시 데이터를 빌드하고 보고하는 데 필요한 모든 데이터가 포함됩니다.

작성된 SurfaceDataRequestMeshAsync 호출에 포함되어 시스템에 전달됩니다. 메시 데이터가 준비되면 일치하는 SurfaceData 가 요청된 시점에 제공된 “data ready” 콜백에 포함되어 반환됩니다. 이를 통해 애플리케이션은 어떤 Surface 가 데이터에 해당하는지 정확하고 명료하게 파악할 수 있습니다.

애플리케이션에 필요한 정보에 따라 SurfaceData 를 작성해야 합니다. 여기에는 다음에서 패스하는 것이 포함됩니다.

올바르지 않게 설정된 SurfaceDataRequestMeshAsync를 호출하면 인수 예외(argument exceptions)가 시스템에서 발생합니다. RequestMeshAsync로 인해 인수 예외가 발생하지 않더라도 메시 데이터가 생성되거나 반환되지 않을 수도 있습니다.

간단한 사용 사례

아래 샘플은 이 API를 간단하게 사용하는 방법을 보여줍니다.

using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.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초마다 한 번씩 호출하면 충분합니다.

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