Version: 2019.1
공간 매핑 렌더러
공간 매핑의 일반적인 문제 해결

공간 매핑 저수준 API

공간 매핑 렌더러(Spatial Mapping Renderer) 및 공간 매핑 콜라이더(Spatial Mapping Collider) 컴포넌트를 이용하면 시스템의 세부 사항을 이해하지 않고도 공간 매핑 기능을 간편하게 사용할 수 있습니다. 애플리케이션에서 공간 매핑을 세부적으로 제어하고 싶다면 Unity가 공간 매핑에 제공하는 저수준 API를 사용하십시오.

API는 HoloLens가 수집하는 공간 매핑 정보에 액세스할 수 있도록 수많은 데이터 구조 및 오브젝트 타입을 제공합니다.

SurfaceObserver

SurfaceObserver를 사용하여 공간 매핑 데이터에 액세스할 수 있습니다. SurfaceObserver API 클래스는 애플리케이션이 공간 매핑 데이터에 요구하는 현실 공간 볼륨을 모니터링합니다. SurfaceObserver는 현실의 물리 영역을 묘사하고, 공간 매핑 시스템에서 추가, 변경 또는 제거한 교차하는 공간 표면 세트를 보고합니다.

Unity 스크립팅 API를 통해 공간 매핑과의 직접적인 인터랙션을 구현하려는 경우에만 SurfaceObservers를 사용하십시오.

Unity는 SurfaceObserver API를 기반으로 빌드된 자체 공간 매핑 렌더러 및 공간 매핑 콜라이더 컴포넌트를 제공하여 공간 매핑 기능에 쉽게 액세스할 수 있도록 지원합니다. 자세한 내용은 공간 매핑 컴포넌트에 대한 문서를 참조하십시오.

SurfaceObserver를 사용하여 애플리케이션은 물리 충돌 데이터의 존재 여부에 관계없이 메시 데이터를 비동기적으로 요청할 수 있습니다. 요청이 완료되면 다른 콜백이 애플리케이션에 데이터를 사용할 준비가 되었음을 알립니다.

SurfaceObserver는 다음과 같은 기능을 애플리케이션에 제공합니다.

  1. 추가, 제거, 업데이트 등 표면 변화에 대해 요청 시 콜백을 생성합니다.

  2. 알려진 표면에 해당하는 메시 데이터를 요청하기 위한 인터페이스를 제공합니다.

  3. 요청하는 메시 데이터가 사용할 준비가 되면 콜백을 생성합니다.

  4. SurfaceObserver의 위치 및 볼륨을 정의하는 방법을 제공합니다.

SurfaceData

SurfaceData 클래스에는 공간 매핑 시스템이 표면(Surface) 메시 데이터를 빌드하고 보고하는 데 필요한 모든 정보가 포함되어 있습니다.

RequestMeshAsync 메서드를 사용하여 채워진 SurfaceData 오브젝트를 공간 매핑 시스템에 보내야 합니다. RequestMeshAsync 메서드를 처음으로 호출하는 경우 SurfaceDataReadyDelegate를 전달해야 합니다. 메시 데이터가 준비되면 SurfaceDataReadyDelegate가 일치하는 SurfaceData 오브젝트를 보고합니다.

이를 통해 애플리케이션은 데이터에 해당하는 표면을 정확하게 확인할 수 있습니다.

애플리케이션이 요구하는 정보를 사용하여 SurfaceData 게임 오브젝트를 채워야 합니다. 여기에는 다음과 같은 컴포넌트와 데이터가 포함됩니다.

잘못 구성된 SurfaceData 오브젝트로 RequestMeshAsync 메서드를 호출하면 시스템에서 인수 예외가 발생합니다. RequestMeshAsync 메서드 호출이 인수 예외를 발생시키지 않더라도 다른 방법으로는 공간 매핑이 메시 데이터를 성공적으로 생성하고 반환하는지 확인할 수 없습니다. 스크립트를 통해 메시 데이터를 수동으로 추적할 것을 권장합니다.

API 사용 예제

아래 샘플 스크립트에서 주요 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
}

// This class holds data that is kept by the system 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 by the system on a regular basis.
    Dictionary<int, SurfaceEntry> m_Surfaces;

    // This is the material with which the system draws baked Surfaces.  
    public Material m_drawMat;

    // This flag is used by the Spatial Mapping system 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 only issues a new request
    // if there are currently no requests being processed by the system.
    bool m_WaitingForBake;

    // This is the last time the SurfaceObserver was updated by the system. It updates no 
    // more than every two seconds
    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 is 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 is invalid.
                        Debug.Log(System.String.Format ("Bake request for {0} failed.  Is {0} a valid Surface ID?", bestSurface.m_Id));
                    }
                }
                catch 
                {
                    // Requests can fail you do not fill out the data struct properly
                    Debug.Log (System.String.Format("Bake for id {0} failed unexpectedly!", bestSurface.m_Id));
                }
            }
        }
    }

    // This handler receives events when surfaces change, and propagates those events
    // using the SurfaceObserver’s Update method  
    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 the system as already baked this Surface, mark it as needing to be baked
                // in addition to the update time so the "next Surface to bake" 
                // logic orders 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 check that the returned filter and WorldAnchor
            // are the same as those used by the system to request the data. This should always
            // be true unless you have changed code 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);
        }
    }
}

참고: SurfaceObserver의 업데이트 메서드를 호출하는 작업은 리소스 소모가 큽니다. 따라서 애플리케이션에서 요구할 때를 제외하고는 이 작업을 수행하지 않는 것이 좋습니다. 대부분의 애플리케이션에서 3초당 1회 호출이 적절합니다.

  • 2018–05–01 편집 리뷰를 거쳐 페이지 게시됨

  • Unity 2017.3에서 HoloLens 공간 매핑에 대한 설명이 업데이트됨

공간 매핑 렌더러
공간 매핑의 일반적인 문제 해결