가상현실 기술의 최신 혁신은 VR 헤드셋을 통해 몰입형 공간에서 사람들과 만나고 대화하는 더 현실적인 경험을 제공합니다. 실시간 상호작용 기술을 통해 VR 세계에서 비디오 채팅과 음성 채팅이 가능해졌습니다. 많은 VR 헤드셋 제조사들은 환경 소리를 위한 공간 음향 지원을 고려하고 있습니다. 그러나 3D 소스(예: 비디오 채팅 속 상대방)에서 나오는 음향은 항상 동일한 처리를 받지 않습니다.
이 튜토리얼에서는 스피커에서 공간 음향을 지원하는 VR 채팅 애플리케이션을 구축하는 과정을 안내합니다. 채팅 그룹의 멤버는 VR 헤드셋 외에도 웹이나 Unity 데스크톱 앱 등 다른 플랫폼에서 참여할 수 있다는 점을 이해하는 것이 중요합니다. 이 프로젝트에서는 Oculus Quest 헤드셋을 사용하여 구현을 시연할 것입니다. 동일한 기본 기술은 다른 호환 헤드셋에도 적용될 수 있습니다.
프로젝트 개요
이 프로젝트는 네 부분으로 구성됩니다. 첫 번째 부분은 프로젝트 설정 방법을 설명하며, Oculus 패키지, Agora SDK, 샘플 API 스크립트를 통합하는 과정을 포함합니다.
두 번째 부분은 장면 생성 방법을 설명하며, XR Rig와 컨트롤러 설정, UI 생성, Agora API 훅 구현을 포함합니다.
세 번째 부분은 다른 샘플 Unity 모바일 앱을 사용하여 VR과 비VR 사용자 간의 비디오 스트리밍을 테스트하는 방법을 보여줍니다.
마지막 부분은 오디오 프레임 및 오디오 소스 처리 기술에 대한 논의입니다. 이 부분은 구현을 더 잘 이해하려는 프로그래머를 대상으로 합니다. 이 섹션을 읽지 않아도 이 튜토리얼을 실행하는 데는 문제가 없습니다.
완전한 프로젝트 또는 패키지의 링크는 이 블로그의 마지막 섹션에 있습니다.
필수 조건
- Unity Editor (이 문서에서는 2019 LTS 버전 사용)
- Oculus 헤드셋 및 Get Started 방법 이해
- Unity Editor 및 게임 오브젝트에 대한 이해
- Unity의 XR 프레임워크에 대한 기본 이해
- Agora 개발자 계정 (How to Get Started with Agora 참조)
- Agora Video SDK for Unity
Part 1: 프로젝트 설정
Oculus 통합 패키지 가져오기
Oculus 웹사이트의 시작 페이지에는 VR용 Unity 프로젝트를 설정하는 자세한 단계가 설명되어 있습니다. 이 튜토리얼에서는 Unity 2019를 사용해 현재 프로젝트가 어떻게 설정되었는지 설명합니다. 2020 및 2021 버전에서는 패키지 임포트 단계가 약간 다를 수 있습니다.
먼저 Android 플랫폼을 대상으로 새로운 프로젝트를 열습니다.
다음으로 Asset Store에서 Oculus 통합 패키지를 임포트합니다. 이 과정은 조금 시간이 소요됩니다:

수입 후 Unity는 플러그인의 최신 버전으로 업데이트할지 묻을 수 있습니다. 최신 버전이 이미 다운로드되었기 때문에 이 메시지가 조금 이상하게 느껴질 수 있습니다. ‘예’ 또는 '아니오'를 선택하고 계속 진행할 수 있습니다. Unity가 재시작되어 수입 및 컴파일 과정을 완료합니다:

이제 빌드 설정을 다음과 같이 수정합니다:
- 텍스처 압축을 ASTC로 변경합니다.
- 플레이어 설정에서 색상 공간을 Linear로 변경하고, Auto Graphics API를 선택 해제하며, 그래픽 API로는 OpenGLES 3만 사용합니다.
- 플레이어 설정에서 회사 이름과 제품 이름 정보를 입력합니다.
- 추가 권장 Android 설정: 스크립팅 백엔드를 IL2CPP로, 대상 아키텍처를 ARM64로, 최소 API 레벨을 Android 7.0으로 설정합니다.
- 프로젝트 설정 > XR 플러그인 관리에서 Oculus를 활성화합니다. 또 다른 시간이 많이 소요되는 패키지 임포트 및 컴파일 과정이 발생합니다. (커피 한 잔 마시며 쉬어도 됩니다.)
Unity용 아고라 Video SDK 임포트
Asset Store에서 Agora SDK를 다운로드하고 모든 파일을 임포트합니다:

아고라를 처음 사용하시는 경우, Unity 에디터의 Assets 폴더에 있는 데모 앱을 먼저 시도해 보신 후 VR 프로젝트로 진행하시기 바랍니다. SDK에 포함된 README 파일을 확인하여 데모 및 SDK에 대한 유용한 정보를 확인하세요. SDK 데모는 곧바로 실행할 수 있을 것입니다.
아고라 애플리케이션을 실행하려면 App ID가 필요합니다. Agora 개발자 콘솔로 이동하여 테스트 프로젝트를 생성하고 App ID를 획득하세요:

이 샘플 프로젝트의 단순화를 위해 토큰 생성 단계를 생략합니다. 그러나 보안 강화를 위해 실제 생산 환경에서는 토큰이 활성화된 App ID를 사용해야 합니다.
아고라 API 샘플 코드 복사
Agora는 일반적인 용도에 맞는 API 예제를 보여주는 작은 샘플 프로젝트 모음을 제공합니다. 리포지토리에서 PlaybackAudioFrame 폴더와 tools 폴더 내의 의존성 스크립트를 복사합니다:
- Logger.cs
- PermissionHelper.cs
- RingBuffer.cs
편의를 위해 파일들은 이 패키지 아카이브에 포함되어 있으므로 API-Example에서 개별적으로 선택할 필요가 없습니다. 프로젝트 폴더 구조는 이제 다음 스크린샷과 유사해야 합니다:

파트 2: 장면 생성
샘플 장면 복제
Oculus Integration 패키지 중 하나에서 샘플 장면을 재사용하여 새로운 장면을 생성합니다. 프로젝트 탐색기에서 RedBallGreenBall 장면을 찾아 이 파일을 복제합니다. 파일을 AgoraSpatialTest로 이름 변경한 후 Assets/Scenes 폴더로 이동합니다.

AgoraSpatialTest 장면
이 장면에서는 위와 같이 FirstPersonController 프리팹이 사용되었습니다. 이 오브젝트를 제거한 후 프로젝트에서 OVRPlayerContoller 프리팹을 검색하고 찾아서 AgoraSpatialTest 장면으로 드래그 앤 드롭합니다. FirstPersonController는 장면의 오브젝트가 뷰 위치에 고정되어 표시되도록 하며, 즉 머리를 돌리면 공이 움직입니다. 반면 OVRPlayerController는 실제 VR 공간을 볼 수 있으며, 머리를 돌리더라도 오브젝트는 초기 위치에 고정됩니다.
체크포인트: 이제 프로젝트를 빌드하고 실행하여 빨간 공과 녹색 공에서 나오는 오디오 소스로 생성된 공간 오디오를 경험할 수 있습니다. Oculus 헤드셋으로 머리를 돌리면 각 공의 방향에서 소리가 들릴 것입니다.
UI 생성
- 공 위치: 빨간 공의 Z 위치를 -2.1로, 녹색 공의 Z 위치를 3으로 변경합니다.
- 로그 텍스트: 텍스트 UI를 생성하고 이름을 LogText로 지정합니다. 위치 = (0,0,0), 너비 = 600, 높이 = 400이며, 텍스트를 수평 및 수직으로 중앙에 배치합니다.
- 캔버스: 캔버스 위치를 (0,20,27.7)로 변경하고, 너비 = 600, 높이 = 400, 스케일 = (0.1, 0.1, 0.1)로 설정합니다.
- 이벤트 카메라: 캔버스에서 Canvas Render Mode를 World Space로 변경합니다. OVRPlayerController의 자식 요소에서 CenterEyeAnchor를 Event Camera 필드로 드래그합니다:

원격 사용자 프리팹: 3D 기본 객체 Capsule을 생성한 후, Z축 회전을 180도로 설정하여 객체를 뒤집습니다. 이후 AudioSource 컴포넌트를 해당 객체에 연결합니다. SpatializedSound1 객체(녹색 공)를 확인하고, 해당 객체의 AudioSource 값을 Capsule에 복사합니다. Capsule의 AudioSource 컴포넌트에서 Audio Clip 값을 지웁니다. Capsule에 ONSPAudioSource 컴포넌트를 추가합니다. (이 스크립트의 전체 경로는 Assets/Oculus/Spatializer/scripts/ONSPAudioSource.cs입니다.) 이 오브젝트를 Assets/Resources 폴더에 드래그하여 프리팹으로 만듭니다. 장면에서 Capsule 오브젝트를 삭제합니다.

- 제어 포인트: GameObject를 생성하고 이름을 AgoraRoot로 지정합니다. 해당 객체를 (0.58, -1.17, -4.06) 위치에 배치합니다. 이 위치는 빨간 공 근처에 있습니다.
- 샘플 스크립트 연결: UserAudioFrame2SourceSample 스크립트를 AgoraRoot 객체에 드래그합니다. Agora 앱 ID와 채널 이름을 입력합니다. 여기에서 unity3d는 테스트를 위해 사용할 채널 이름입니다.
- 사용자 프리팹: 리소스 폴더에서 Capsule 프리팹을 AgoraRoot의 UserAudioFrame2SourceSample 컴포넌트 내 User Prefab 필드로 드래그합니다.

이 스크린샷은 수행된 작업을 요약합니다:

멋진데요! 장면을 저장하는 것을 잊지 마세요 – 이건 항상 중요합니다! 이제 테스트를 진행할 준비가 되었습니다.
파트 3: VR 공간 음향 테스트
Oculus 헤드셋은 이제 통합된 Agora Video SDK를 통해 다른 사용자와 연결할 수 있습니다. 예제에서 헤드셋이 비디오 스트림을 생성하지 않기 때문에, 비-VR 헤드셋 소스에서 비디오 스트림을 가져오고 싶습니다. 이를 제공할 수 있는 방법은 여러 가지가 있습니다:
선택 1: SDK 데모 앱을 사용합니다. 샘플 프로젝트에서 AgoraEngine 폴더에 있는 데모 앱을 열고 에디터에서 실행하거나 기기에 빌드하여 실행할 수 있습니다. 기기를 TV나 라디오 옆에 두고 임의의 오디오 입력을 받을 수 있도록 합니다.
선택 2: Agora 웹 데모 앱을 사용합니다. 이 방법은 원격지에서 테스트를 도와줄 동료와 함께 사용하는 것이 좋습니다.
선택 사항 3: 이 데모 앱을 루핑 사운드 샘플로 사용합니다. 코드는 이전에 스크립트를 다운로드한 동일한 리포지토리에서 가져왔습니다. 이 선택 사항은 단독 테스터에게 적합하며, 테스트에서 예상대로 검증할 수 있는 일정한 사운드 샘플을 제공하기 때문입니다.
이 튜토리얼에서는 VR 헤드셋에 동일한 공간 오디오 경험을 보여줄 수 없습니다. 그러나 Oculus Quest의 뷰에 표시되는 내용을 공유할 수 있습니다. 테스트 시 헬퍼 앱은 VR 테스터 앞에 놓인 모바일폰에서 실행됩니다. 결과적으로 빨간 공 근처에 캡슐이 생성됩니다. 테스트 시작 시 샘플 음악의 소리는 뒤쪽 오른쪽 방향에서 나옵니다. 테스터가 캡슐을 보려고 머리를 돌리면 소리의 방향이 변경됩니다.
고려 사항: Oculus 샘플 리소스에서 녹색 공의 원본 소리(vocal1)는 테스트 음악 샘플에 비해 약간 너무 큽니다. AudioSource 구성 요소에서 볼륨을 0.5 이하로 줄여보세요.
자원이 충분하다면 테스트에 더 많은 비VR 원격 사용자를 추가할 수 있습니다. 각 사용자는 VR 장면 내에서 캡슐 인스턴스로 표시됩니다. VR 테스터는 가상 공간 내에서 이동하며 다른 사용자에게 가까이 다가가고, 해당 원격 오디오 스트림의 증폭된 소리를 들을 수 있습니다!
VR 환경에서 공간 음향을 지원하는 비디오 채팅 경험이 이렇게 간단할 수 있죠? 그리고 샘플 프로젝트를 재사용하기만 하면 단 한 줄의 코드도 작성하지 않았습니다.
이것으로 Oculus에서 공간 음향을 활용한 비디오 채팅을 구현하는 튜토리얼이 완료되었습니다. 하지만 이 기술을 가능하게 하는 기술에 대해 더 알고 싶다면 계속 읽어보세요.
파트 4: 프로그래머를 위한 요약
이 프로젝트가 어떻게 작동하는지 이해하기 위해, 핵심 API, 데이터 구조, 알고리즘을 살펴보며 프로젝트의 구성 요소를 분석해 보겠습니다. Oculus의 빠른 시작 튜토리얼이 이미 제공되므로, 이 프로젝트에서 Agora SDK를 어떻게 사용하는지에 초점을 맞출 것입니다.
user-audioframe-audiochannel 폴더에서 다음 스크립트를 찾을 수 있습니다:
- UserAudioFrame2SourceSample
- IUserAudioFrameDelegate
- UserAudioFrameHandler
여기서 UserAudioFrame2SourceSample은 Agora RTC 엔진을 설정하고 이벤트 콜백을 등록하며 사용자를 객체로 관리하는 컨트롤러입니다. 엔진 설정은 비교적 간단합니다. Agora를 처음 사용하시는 경우 이 가이드의 빠른 시작 튜토리얼을 참고하세요.
혼합 또는 비혼합
이 API는 개별 오디오 스트림을 분리하는 가장 중요한 API입니다:
_audioRawDataManager.SetOnPlaybackAudioFrameBeforeMixingCallback(OnPlaybackAudioFrameBeforeMixingHandler);
설명적 메서드 이름은 다음과 같은 의미를 암시합니다:
- 응용 프로그램의 오디오 출력에서 일반적으로 듣는 오디오 스트림에 믹싱 과정이 포함되어 있습니다.
- 이 API 콜백은 믹싱 전에 원격 사용자로부터 오디오 스트림을 전송합니다. 이는 공간 오디오 설정에서 오디오 소스를 설정하기 위해 정확히 필요한 기능입니다.
- 혼합 전에 별도의 오디오 스트림을 재생할 수 있지만, 오디오는 정상적인 프로세스에서 혼합되어 재생됩니다.
세 번째 함의를 위해, 엔진에 정상적인 혼합 오디오 출력을 비활성화하고 재생되는 내용을 제어할 수 있도록 알려야 합니다. 여기 두 번째 중요한 API 호출이 있습니다:
mRtcEngine.SetParameter("che.audio.external_render", true);
메인 스레드 디스패처
Unity에서 동적 컴포넌트 연결 또는 UI 업데이트는 메인 스레드에서 수행되어야 합니다. AudioSource 객체에서 오디오 클립을 재생하는 것은 UI 업데이트로 간주됩니다. 그러나 OnPlaybackAudioFrameBeforeMixingHandler 콜백은 배경 스레드에서 실행됩니다. 이 문제를 어떻게 해결할 수 있을까요? 답은 MainThread Dispatcher입니다. BlockingCollection 데이터 구조를 사용하면 스레드 안전성을 보장하는 데이터 구조를 생성할 수 있으며, 이 구조는 작업을 큐에 쌓아두고 Unity의 메인 스레드에서 실행되도록 합니다. 이 데이터 구조는 Update() 함수에서 액세스할 수 있습니다. 큐에 작업을 전송하려면 다음과 같이 dispatch 메서드를 호출합니다:
dispatch(()=> { <code statement 1> ; <code statement 2>; <etc.> });
디스패치 메서드는 C# 시스템 액션을 매개변수로 받아들이며, 이 액션은 또한 객체입니다. 이 객체를 BlockingCollection에 추가합니다. 매우 간단합니다.
UserAudioFrameHandler
이 핸들러 클래스는 원격 사용자가 연결될 때 생성되는 Capsule 객체에서 개별적으로 실행됩니다. 이 핸들러는 Agora 엔진에서 전송된 오디오 프레임을 오디오 클립 데이터로 변환하고 AudioSource 구성 요소에 재생합니다. 당연히 Oculus SDK의 스크립트 ONSPAudioSource를 사용하여 공간 음향 기능을 확장해야 합니다.
AudioSource 컴포넌트의 속성은 첫 번째 오디오 프레임 패킷에서 전달된 정보로 채워집니다. 여기서 기술적 도전 과제는 AudioSource에서 간격 기반 오디오 프레임 컬렉션의 부드러운 재생입니다. 이는 고전적인 Consumer-Producer 동시성 모델입니다. 이 문제의 해결책은 GitHub 기여자 Joe Osborn이 구현한 Ring Buffer 데이터 구조를 사용하는 것입니다. 샘플 코드에서는 오디오 프레임 속도와 채널에 따라 약 10초 분량의 오디오 데이터를 할당합니다. 기본적으로 사용자의 오디오 프레임 콜백 함수(이전 섹션에서 논의됨)가 프로듀서 역할을 하고, AudioSource의 OnAudioRead 함수가 컨슈머 역할을 합니다. 데이터가 충분하면 AudioSource는 이 데이터 구조체에서 버퍼된 오디오를 소비합니다.
다음 다이어그램은 이 아키텍처를 설명합니다:


고려 사항
- 성능: 프로그래머는 링 버퍼의 메모리 사용량과 오디오 프레임 변환 시 CPU 시간을 최적화하기 위해 구현을 애플리케이션에 맞게 최적화해야 합니다.
- 에디터 테스트: 헤드셋에 배포하기 전에 Unity 에디터에서 코드를 실행해야 합니다. 그러나 현재 Oculus SDK(작성 시점 기준 버전 29.0)에서 애플리케이션을 중단시킬 수 있는 오류가 있습니다. 다음과 같은 현상입니다:
NullReferenceException: Object reference not set to an instance of an object
OculusSampleFrameworkUtil.HandlePlayModeState (UnityEditor.PlayModeStateChange state) (at Assets/Oculus/SampleFramework/Editor/OculusSampleFrameworkUtil.cs:45)
UnityEditor.EditorApplication.Internal_PlayModeStateChanged (UnityEditor.PlayModeStateChange state) (at /Users/bokken/buildslave/unity/build/Editor/Mono/EditorApplication.cs:415)
이 경우, OculusSampleFrameworkUtil.cs 스크립트를 다음 코드 조각으로 업데이트할 수 있습니다:
private static void HandlePlayModeState(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode)
{
System.Version v0 = new System.Version(0, 0, 0);
UnityEngine.Debug.Log("V0=" + v0.ToString());
#if UNITY_EDITOR
OVRPlugin.SendEvent("load", v0.ToString(), "sample_framework");
#else
OVRPlugin.SendEvent("load", OVRPlugin.wrapperVersion.ToString(), "sample_framework");
#endif
}
}
완료되었습니다!
이 튜토리얼은 Oculus Quest에서 가장 잘 작동합니다. 이 프로젝트에는 단 한 줄의 코드도 작성하지 않았기 때문에 GitHub 리포지토리에 별도의 코드 샘플을 유지 관리할 필요가 없습니다. 그러나 의존성 파일에 포함된 장면과 스크립트는 맞춤형 패키지로 아카이브되어 있으므로, 이 패키지를 Oculus 프로젝트에 직접 가져와서 테스트해 볼 수 있습니다. 패키지 링크는 이 리포지토리 링크를 참조하세요.
다른 VR 헤드셋을 대상으로 개발 중이라면, 부분 4에서 설명한 기술은 보편적이며 다른 헤드셋의 API를 활용할 수 있습니다. 몰입형 개발에 성공을 기원합니다!
기타 자료
Agora Video SDK에 대한 자세한 정보는 Agora Unity SDK API Reference를 참조하세요. Oculus Integration Unity SDK에 대한 자세한 정보는 Oculus App Development page를 참조하세요.
Agora 기술 지원이 필요하시면 Agora Developer Slack 커뮤니티에 가입해 주세요.