XR플밍 - 10. XR 애플리케이션 개발을 위한 VR/AR 프로그래밍 - 오클루전 컬링, 이미지 트래킹, 페이스 트래킹, 디바이스 센서(6/9)

이형원·2025년 6월 9일
0

XR플밍

목록 보기
99/215

1. 오클루전 컬링

오클루전 컬링은 위 사진과 같이 주변의 바닥과 벽 등을 인식하여, AR로 표시할 물체에서 실제로 가려져 표시되지 않는 부분을 제거하는 것을 말한다.
이걸 실제로 적용해보기 위해 세팅을 해 보자.


프로젝트는 AR Mobile로 열고 이전과 같이 Build Setting은 안드로이드로, 플레이어 세팅에서의 체크 사항들을 전부 잘 적용한다.

메인 카메라를 제거하고, AR Session과 XR Origin을 추가한다. 그리고 XR Interation Manager도 추가해준다.
그리고 XR Origin의 맨 아래 자식오브젝트로 큐브를 추가한다.(큐브는 시각적으로 보이는 걸 위해, 각도를 살짝 돌리는 것이 좋다.)


다음으로 하이어라키 창에서 AR Default Plane을 추가한다.
여기서 이 Plane의 Material을 변경해야 하는데, Shader를 VR/SpatialMapping/Occlusion으로 설정한다.

이와 같이 설정한 Material을 Plane에 반영하고, 해당 Plane을 프리팹화한 다음 XR Origin에 참조시킨다.
이때 Detective Mode가 Everything으로 되어 있는지 살펴보자.

다음으로 Main Camera에 AR Occlusion Manager를 추가한다.

환경 인식 및 사람 인식 등의 옵션이 있으나, 옵션을 강하게 설정할 수록 연산량이 증가해 최적화 등에 문제가 생길 수 있으므로 주의하도록 한다.
이와 같이 설정하면 바닥 및 벽 등에 물체가 가려지는 것을 확인할 수 있다.

2. 이미지 트래킹

이미지 트래킹은 특정 이미지를 카메라에 인식시킨 다음 오브젝트를 생성하는 등의 이벤트 연출에 사용된다.

씬을 새로 만들고 이전과 똑같이 세팅한 다음, XR Origin에 AR Tracked Image Manager를 추가한다.
여기에 인식시킬 이미지를 넣어주기 위해선, Reference Image Library를 만들어줘야 한다.
프로젝트 창에서 해당 컴포넌트를 만들어주자.

위와 같이 아이콘이 생성되는데 초기 화면은 다음과 같다.

Add Image로 이미지를 추가해주자. 이미지는 여러 개를 추가할 수도 있다.

이미지의 이름을 수정하고, 사이즈를 설정한다.

또한 해당 이미지에 어떤 오브젝트를 생성할지, 프리팹도 생성해 둔다.
이미지를 여러 개를 띄우기 위해서, 스크립트를 추가해보자.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class ImageTrackingSupport : MonoBehaviour
{
    private ARTrackedImageManager _imageManager;
    private Dictionary<string, GameObject> _prefabs = new();
    private Dictionary<TrackableId, GameObject> _bound = new();

    [SerializeField] private List<GameObject> _trackedPrefab;

    private void Awake() => Init();
    private void OnEnable() => SubscribeEvents();

    private void Init()
    {
        _imageManager = GetComponent<ARTrackedImageManager>();

        for(int i = 0; i < _trackedPrefab.Count; i++)
        {
            if (!_prefabs.ContainsKey(_trackedPrefab[i].name))
            {
                GameObject go = Instantiate(_trackedPrefab[i]);
                go.name = _trackedPrefab[i].name;
                _prefabs.Add(go.name, go);
                go.SetActive(false);
            }
        }
    }

    private void SubscribeEvents()
    {
        _imageManager.trackedImagesChanged += OnImageChanged;
    }

    private void OnImageChanged(ARTrackedImagesChangedEventArgs ars)
    {
        foreach (ARTrackedImage img in ars.added) // 새로 추가된 이미지들 List
        {
            Bind(img);
        } 

        foreach (ARTrackedImage img in ars.updated) // 트래킹중인 이미지들 List
        {
            SetPosition(img);
        }
    }

    private void Bind(ARTrackedImage img) // 이미지를 최초 추가
    {
        if (!_prefabs.TryGetValue(img.referenceImage.name, out GameObject go)) return;
        
        go.SetActive(true);
        _bound.Add(img.trackableId, go);
        SetPosition(img);
    }

    private void SetPosition(ARTrackedImage img) // 이미지의 위치 업데이트
    {
        if (!_bound.TryGetValue(img.trackableId, out GameObject go)) return;

        bool isTracked = img.trackingState == TrackingState.Tracking;
        go.SetActive(isTracked);

        if (isTracked)
        {
            go.transform.SetPositionAndRotation(
                img.transform.position,
                img.transform.rotation
                );
        }
    }
}

이와 같이 코드를 작성하고 XR Origin에 해당 컴포넌트를 넣는다.

Prefab을 적절하게 추가하여 빌드를 해 보면 아래와 같이 이미지를 바탕으로 오브젝트를 생성하는 모습을 확인할 수 있다.

3. 페이스 트래킹

페이스 트래킹은 카메라에서 얼굴을 인식하고, 얼굴에 효과를 주는 기술 등을 말한다.

페이스 트래킹을 시험하기 위해 다시 새롭게 씬을 생성해보자.

이번에는 메인 카메라를 삭제하지 않는다.
XR Origin에는 기본적으로 주어져 있는 내용 중 XR Origin을 제외한 모든 추가 컴포넌트를 지우고, AR Face Manager를 추가한다.
또한 스크립팅이 필요하므로 Face Tracking Support를 추가한다.

using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class FaceTrackingSurpport : MonoBehaviour
{
    [SerializeField] private GameObject _facePointPrefab;
    
    private ARCameraManager _arCameraManager;
    private ARFaceManager _arFaceManager;
    private Dictionary<TrackableId, List<Transform>> _pointsCache = new();

    private void Awake() => Init();
    private void Update() => SetFacePointPosition();
    
    private void Init()
    {
        _arCameraManager = GetComponentInChildren<ARCameraManager>();
        _arFaceManager = GetComponent<ARFaceManager>();
        _arCameraManager.requestedFacingDirection = CameraFacingDirection.User;
    }

    private void SetFacePointPosition()
    {
        foreach (ARFace face in _arFaceManager.trackables)
        {
            // 만약 새로 감지된 점이면 점 오브젝트를 생성
            if (!_pointsCache.TryGetValue(face.trackableId, out var points))
            {
                // 얼굴 버텍스 수 만큼 구체 생성
                points = CreatePointsList(face);
                _pointsCache.Add(face.trackableId, points);
            }
            
            // 포인트들의 위치 갱신
            UpdatePointsPosition(face, points);
        }
    }

    private void UpdatePointsPosition(ARFace face, List<Transform> points)
    {
        NativeArray<Vector3> vers = face.vertices;

        for (int i = 0; i < vers.Length; i++)
        {
            // 로컬 포지션 기준이기 때문에 의도된 위치가 대입되지 않는다.
            //points[i].position = vers[i]; 
            
            points[i].position = face.transform.TransformPoint(vers[i]);
        }
    }

    private List<Transform> CreatePointsList(ARFace face)
    {
        NativeArray<Vector3> vers = face.vertices;
        List<Transform> list = new List<Transform>(vers.Length);

        for (int i = 0; i < vers.Length; i++)
        {
            Transform p = Instantiate(_facePointPrefab).transform;
            list.Add(p);
        }

        return list;
    }
}

이와 같이 세팅하면 얼굴의 포인트를 Sphere로 표시할 수 있다.

4. 디바이스 센서

AR은 디바이스를 직접 들고 플레이 하는 컨텐츠이므로, 디바이스의 센서를 활용할 수 있다는 장점도 있다. 이를 사용해 Gps의 데이터를 취득해 컨텐츠에 이용할 수 있지만 디바이스 센서의 데이터를 취득하는 것은 위험한 행위이므로 권한 요청을 통해 데이터를 취득할 수 있는 권한을 얻게 된다.

이와 같은 권한 요청을 하는 방법에 대해 알아보자.

4.1 권한 요청

권한 요청을 하기 위해 우선 Custom Main Manifest를 활성화하자.

그러면 아래와 같이 플러그인이라는 파일이 생기고, AndriodManifest라는 파일이 생긴다.

이 파일을 열고 permission에 대한 내용을 추가하자.

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Android;
using TMPro;

public class PermissionManager : MonoBehaviour
{
    private string targetPermission = Permission.FineLocation;
    
    public TextMeshProUGUI _grantText;
    public TextMeshProUGUI _deniedText;
    
    private void Start() => RequestWithCallbacks();

    public void RequestsWithCallbacks()
    {
        string[] permissions = new string[]
        {
            Permission.FineLocation,
            Permission.Camera
        };
        
        PermissionCallbacks callbacks = new();
        Permission.RequestUserPermissions(permissions, callbacks);
        
        callbacks.PermissionGranted += OnGranted;
        callbacks.PermissionDenied += OnDenied;
    }
    
    private void OnGranted(string permission) => _grantText.text += $", {permission}";
    private void OnDenied(string permission) => _deniedText.text += $", {permission}";

    public void RequestWithCallbacks()
    {
        PermissionCallbacks callbacks = new();
        Permission.RequestUserPermission(targetPermission, callbacks);

        callbacks.PermissionGranted += t =>
        {
            //_grantText.text = "Granted";
            SceneManager.LoadScene(1);
        };
        callbacks.PermissionDenied += t =>
        {
            _deniedText.text = "Denied";
        };
    }
    
    public void Request()
    {
        // 1회성 요청
        Permission.RequestUserPermission(targetPermission);
    }
}

이와 같이 설정하고 테스트를 진행해보자.


여기에서 한 번 카메라는 권한 사용 거절을 하고,

위치 정보는 허용해보자.

그러면 이와 같이 카메라는 거부되고 위치 정보는 허용된 것을 확인할 수 있다.

4.2 GPS

현재의 위치와 경도를 받아올 수 있는 GPS 정보를 받아와 보자. 우선 EarthLocation 구조체를 만들어보자.

public struct EarthLocation
{
    public float Lat { get; set; } // 위도
    public float Lng { get; set; } // 경도

    public EarthLocation(float lat, float lng)
    {
        Lat = lat;
        Lng = lng;
    }
}

이제 GPS를 표시할 GPS 매니저를 만들어보자.
코루틴을 사용해 만들었다.

using System.Collections;
using UnityEngine;
using UnityEngine.Android;
using UnityEngine.Events;

public class GpsManager : MonoBehaviour
{
    [SerializeField] private float _refreshCycle = 1;
    [SerializeField] private int _maxRefreshWait = 20;

    public EarthLocation CurrentLocation { get; private set; }
    public UnityEvent<EarthLocation> OnLocationUpdated = new();
    
    private WaitForSeconds _cycle;
    private Coroutine _routine;

    public int RefreshCount { get; private set; }
    
    private void Awake() => Init();
    private void OnEnable() => StartGps();
    private void OnDisable() => StopGps();

    private void Init()
    {
        _cycle = new WaitForSeconds(_refreshCycle);
    }

    private void StartGps()
    {
        if(_routine != null) StopCoroutine(_routine);

        // Input.location.Start();
        _routine = StartCoroutine(GpsLoop());
    }

    private void StopGps()
    {
        if (_routine != null)
        {
            StopCoroutine(_routine);
            _routine = null;
        }

        if (Input.location.status != LocationServiceStatus.Stopped)
        {
            Input.location.Stop();
        }
    }

    private IEnumerator GpsLoop()
    {
        // 지금이 몇번 갱신됐는지 있으면 좋을 것 같음.
        RefreshCount = 0;
        
        // GPS가 활성화 되어있는가? 되어있지 않으면 멈춰야 함.
        bool isGpsActive =
            !Input.location.isEnabledByUser ||
            !Permission.HasUserAuthorizedPermission(Permission.FineLocation);

        if (isGpsActive)
        {
            yield break;
        }

        // 서비스 시작
        Input.location.Start();
        
        // GPS가 현재 구동될 조건이 안 갖춰졌고 + 대기 카운트가 0 초과라면
        // 사이클 주기로 카운트를 차감, 반복하며 대기
        int count = _maxRefreshWait;
        while (Input.location.status == LocationServiceStatus.Initializing && count > 0)
        {
            count--;
            yield return _cycle;
        }

        // 카운트가 0 이하거나(카운트 다 됨) GPS 상태가 실패일 경우.
        // 멈춘다.
        if (count <= 0 || Input.location.status == LocationServiceStatus.Failed)
        {
            yield break;
        }

        // 이젠 정상구동 되었으니 돌리자.
        while (true)
        {
            LocationInfo data = Input.location.lastData;
            CurrentLocation = new EarthLocation(data.latitude, data.longitude);
            
            RefreshCount++;
            OnLocationUpdated?.Invoke(CurrentLocation);

            yield return _cycle;
        }
    }
}

마지막으로 데이터를 찍는 UI 스크립트만 만들고 작동시켜보자.

using UnityEngine;
using TMPro;

public class LocationChecker : MonoBehaviour
{
    [SerializeField] private GpsManager _gpsManager;

    [SerializeField] private TextMeshProUGUI _latText;
    [SerializeField] private TextMeshProUGUI _lngText;
    [SerializeField] private TextMeshProUGUI _countText;

    private void OnEnable() => SubscribeEvents();

    private void SubscribeEvents()
    {
        _gpsManager.OnLocationUpdated.AddListener(SetUI);
    }

    private void SetUI(EarthLocation loaction)
    {
        _latText.text = loaction.Lat.ToString();
        _lngText.text = loaction.Lng.ToString();
        _countText.text = _gpsManager.RefreshCount.ToString();
    }
}

업로드중..

profile
게임 만들러 코딩 공부중

0개의 댓글