본문 바로가기

Oculus와 놀기

퀘스트에 Spatial Anchor를 사용하기

AR상에서 물체의 위치가 고정되지 않는다.

 퀘스트에서 유니티로 만든 게임을 실행하면 실행 시점의 헤드셋의 위치 + 바닥의 위치가 (0, 0, 0)이 된다. 이 말은 실행 시킬 때 마다 유저의 위치에 따라 물체의 위치가 변한다는 뜻이다. 하지만 현실과 상호작용하는 몇몇 앱의 경우 현실의 물체에 고정된 좌표를 필요로 한다. 이를 위해서는 자체적인 공간 매핑을 구현하거나 퀘스트가 제공하는 Spatial Anchor를 사용해야한다.

 

현실을 기준으로 하는 위치가 필요하다.

 Sptial Anchor는 퀘스트의 카메라 정보를 기반으로 특정 물체의 좌표를 현실의 좌표에 매핑 시켜준다. 공유 공간 앵커를 사용할 경우 서로 다른 퀘스트가 동시에 현실의 좌표에 동기화 할 수 있다. 그렇지만 지금은 혼자 사용하는 경우에 대해 알아보겠다.

 

 

 우선 OVRCameraRig의 Quest Features로 가서 Anchor Support를 Enabled로 바꾸고, Shared Spatial Anchor Support를 Supported 혹은 Required로 바꾼다.

 

 

 앵커가 될 오브젝트를 생성하고 OVRSpatialAnchor 컴포넌트를 추가하고 프리맵으로 만든다. 보기 쉽도록 시각적인 요소도 추가했다.

 

 

그리고 앵커를 저장하고 로드할 MyAnchorLoader.cs를 만들어 적절한 오브젝트에 넣어준다.

 

 

앵커의 생성과 저장

 

       // 오른쪽 A버튼으로 새로운 앵커를 생성
        if (OVRInput.GetDown(OVRInput.Button.One, OVRInput.Controller.RTouch))
        {
            if (anchor == null)
            {
                var obj = Instantiate(anchorPrefab, rHandTr.position, rHandTr.rotation);
                anchor = obj.GetComponent<OVRSpatialAnchor>();
            }
        }

 

 MyAnchorLoader의 Update에서 A버튼을 누르면 앵커를 만든다. 아까 만든 앵커 프리맵을 생성하고 위치와 각도는 적당히 오른손 컨트롤러를 기준으로 했다. OVRCameraRig 아래에 RightHandAnchor를 연결해주자. 편의상 앵커는 하나만 생성하기로 했지만, 여러 개를 생성해도 문제 없다.

 

        // 오른쪽 B버튼으로 앵커를 저장
        if (OVRInput.GetDown(OVRInput.Button.Two, OVRInput.Controller.RTouch))
        {
            if (anchor != null)
            {
                anchor.Save((anchor, success) =>
                {
                    if (success == false) return;

                    PlayerPrefs.SetString("Uuid", anchor.Uuid.ToString());
                });
            }
        }

 

 B버튼을 누르면 앵커를 저장한다. OVRSpatialAnchor에 Save를 호출하면 퀘스트는 해당 앵커와 주변 정보를 저장하고 거기에 접근 할 수 있는 고유한 UUID를 생성한다. 이 때, 앵커의 저장, 로드는 모두 비동기로 작동하기 때문에 저장이 끝나고 호출할 콜백 함수를 전달해야한다. 만약 Save를 호출하고 바로 anchor.Uuid 값을 저장하면 아직 앵커가 저장이 되지 않아 유효한 값이 나오지 않는다. 함수 인자 success는 저장의 성공 여부를 알려준다.

 

 

앵커의 로드

 

        // 왼쪽 X버튼으로 앵커를 불러오기
        if (OVRInput.GetDown(OVRInput.Button.One, OVRInput.Controller.LTouch))
        {
            var uuid = PlayerPrefs.GetString("Uuid");
            var uuids = new Guid[1] { new Guid(uuid) };
            var options = new OVRSpatialAnchor.LoadOptions { Timeout = 0,
            	StorageLocation = OVRSpace.StorageLocation.Local, Uuids = uuids };

            OVRSpatialAnchor.LoadUnboundAnchors(options, (anchors) =>
            {
                if (anchors == null) return;

                foreach (var anchor in anchors)
                {
                    if (anchor.Localized)
                        OnLocalized(anchor, true);
                    else if (!anchor.Localizing)
                        anchor.Localize(OnLocalized);
                }
            });
        }

 

 앵커를 로드하기 위해서는 앵커의 UUID의 배열이 필요하다. 여기서는 공유 공간 앵커를 사용하지 않으니 저장공간을 로컬로 설정하고 Uuids에는 하나의 UUID만 넣었다. 그리고 OVRSpatialAnchor.LoadUnboundAnchors에 옵션을 넣어 호출한다. 이 또한 비동기로 작동하니 완료 후 로드된 anchor의 배열 anchors를 받는 함수를 넣는다. 이 때 anchor.Localized가 true면 해당 앵커는 이미 로드가 끝났다는 뜻이다. 그렇지 않을 경우 anchor.Localize에 로드하는 함수를 전달한다.

 

    private void OnLocalized(OVRSpatialAnchor.UnboundAnchor unboundAnchor, bool success)
    {
        if (success == false) return;

        var pose = unboundAnchor.Pose;
        var obj = Instantiate(anchorPrefab, pose.position, pose.rotation);
        anchor = obj.GetComponent<OVRSpatialAnchor>();
        unboundAnchor.BindTo(anchor);
    }

 

 anchor.Localize에 넣는 콜백 함수 OnLocalized는 로드됐지만 연결되지 않은 unboundAnchor과 성공 여부를 알리는 success를 받는다. unboundAnchor.Pose에 있는 position과 rotation값으로 앵커 오브젝트를 생성한다. 그리고 바로 unboundAnchor.BindTo(anchor)를 해야한다. 그렇지 않으면 생성된 앵커 오브젝트는 별개의 앵커로 작동한다.

 

 

앵커의 삭제

 

       // 왼쪽 Y버튼으로 앵커를 삭제
        if (OVRInput.GetDown(OVRInput.Button.Two, OVRInput.Controller.LTouch))
        {
            if (anchor != null)
            {
                anchor.Erase((anchor, success) =>
                {
                    if (success == true)
                        Debug.Log("Erase Success");
                });
                PlayerPrefs.DeleteKey("Uuid");
                Destroy(anchor.gameObject);
                anchor = null;
            }
        }

 

 앵커를 저장소에서 삭제하고 싶다면 anchor.Erase를 호출하면 된다. Save와 동일하게 해당 작업이 끝났을 때 콜백 함수를 넘겨 줄 수 있다.

 

 

결과

 



 앵커를 저장하고 다음 세션에서 로드하면, 게임 월드에서 앵커의 좌표는 바뀌었지만 현실 세계의 기준으로 동일한 위치에 생성된 것을 볼 수 있다. 오큘러스 버튼을 길게 눌러 시점 재설정을 하더라도, 앵커의 위치를 인식할 수 있는 한 LocalPos가 변하면서 현실에 고정된다. 이 앵커를 기준으로 자식 오브젝트를 생성해 로컬 좌표과 각도를 저장하고, 로드할 때 다시 넣어주는 식으로 현실 세계에 오브젝트를 고정할 수 있다.

 

 

 

MyAnchorLoader.cs

 

using System;
using UnityEngine;

public class MyAnchorLoader : MonoBehaviour
{
    public GameObject anchorPrefab;
    public Transform rHandTr;

    private OVRSpatialAnchor anchor = null;

    void Update()
    {
        // 오른쪽 A버튼으로 새로운 앵커를 생성
        if (OVRInput.GetDown(OVRInput.Button.One, OVRInput.Controller.RTouch))
        {
            if (anchor == null)
            {
                var obj = Instantiate(anchorPrefab, rHandTr.position, rHandTr.rotation);
                anchor = obj.GetComponent<OVRSpatialAnchor>();
            }
        }

        // 오른쪽 B버튼으로 앵커를 저장
        if (OVRInput.GetDown(OVRInput.Button.Two, OVRInput.Controller.RTouch))
        {
            if (anchor != null)
            {
                anchor.Save((anchor, success) =>
                {
                    if (success == false) return;

                    PlayerPrefs.SetString("Uuid", anchor.Uuid.ToString());
                });
            }
        }

        // 왼쪽 X버튼으로 앵커를 불러오기
        if (OVRInput.GetDown(OVRInput.Button.One, OVRInput.Controller.LTouch))
        {
            var uuid = PlayerPrefs.GetString("Uuid");
            var uuids = new Guid[1] { new Guid(uuid) };
            var options = new OVRSpatialAnchor.LoadOptions { Timeout = 0, StorageLocation = OVRSpace.StorageLocation.Local, Uuids = uuids };

            OVRSpatialAnchor.LoadUnboundAnchors(options, (anchors) =>
            {
                if (anchors == null) return;

                foreach (var anchor in anchors)
                {
                    if (anchor.Localized)
                        OnLocalized(anchor, true);
                    else if (!anchor.Localizing)
                        anchor.Localize(OnLocalized);
                }
            });
        }

        // 왼쪽 Y버튼으로 앵커를 삭제
        if (OVRInput.GetDown(OVRInput.Button.Two, OVRInput.Controller.LTouch))
        {
            if (anchor != null)
            {
                anchor.Erase((anchor, success) =>
                {
                    if (success == true)
                        Debug.Log("Erase Success");
                });
                PlayerPrefs.DeleteKey("Uuid");
                Destroy(anchor.gameObject);
                anchor = null;
            }
        }
    }

    private void OnLocalized(OVRSpatialAnchor.UnboundAnchor unboundAnchor, bool success)
    {
        if (success == false) return;

        var pose = unboundAnchor.Pose;
        var obj = Instantiate(anchorPrefab, pose.position, pose.rotation);
        anchor = obj.GetComponent<OVRSpatialAnchor>();
        unboundAnchor.BindTo(anchor);
    }
}

 

'Oculus와 놀기' 카테고리의 다른 글

Passthrough를 이용해 AR 만들기  (0) 2023.03.20
유니티로 퀘스트2 앱 개발 시작하기  (0) 2023.03.19