먼저 제 소개를 해드릴게요. 저는 에드 카신스키(Ed Kashinsky)이며 러시아 상트페테르부르크 출신 사운드 디자이너 겸 음악가입니다. 현재 저는 아주 흥미롭고 독특한 사운드를 가진 프로젝트를 작업하고 있죠. 바로 프리스트 vs. 폴터가이스트 VR (Priest vs. Poltergeist VR)라는 로컬 멀티플레이어 VR 게임입니다. 이 프로젝트에 사운드를 작업하는 것이 굉장히 까다로웠습니다. 사운드가 아주 흥미롭고 독특하다고 느껴지는 이유가 바로 이 때문이지 않을까 하네요.
프리스트 vs. 폴터가이스트는 폴터가이스트 (시끄러운 소리를 내는 유령)의 역할을 하는 VR 플레이어와 성직자의 역할을 하며 마우스와 키보드로 캐릭터를 제어하는 PC 플레이어 간의 결투 게임입니다. 여기서 까다로운 점은 두 플레이어 모두 한 PC에서 플레이한다는 점입니다. 두 플레이어가 다른 출력 장치 (VR 헤드폰과 PC 헤드폰/스피커)를 통해 소리를 들어야 할 뿐만 아니라, 각 사운드가 별도의 위치 지정, 감쇠, 차단, 타이밍 등을 가져야 하죠.
레벨의 어딘가에서 의자가 땅바닥으로 떨어진다고 상상해보세요. 한 플레이어가 불과 몇 미터 떨어진 곳에 있었다면 소리가 아주 명확하게 들리겠죠. 하지만 다른 플레이어가 같은 거리만큼 떨어져 있지만 벽을 사이에 두고 있을 경우 충돌음이 더욱 건조하고 낮게 들릴 겁니다. 그렇기 때문에 두 플레이어 모두 각자의 출력 장치에서 충돌음을 들어야 하지만 각자 현재 위치와 설정에 따라 소리가 변경되어야 합니다.
Unreal의 네이티브 오디오 라이브러리를 포함한 몇 가지 해결책을 시험해본 후 저희는 Wwise를 사용하기로 했습니다. Wwise는 별도의 응용 프로그램 안에서 원하는 대로 사운드를 작업할 수 있게 해주는 게임을 위한 독립형 오디오 엔진입니다. 심지어 저희 게임의 궁극적인 목표인 사운드를 서로 다른 오디오 장치로 전송하는 기능도 가지고 있죠.
이 엔진의 기능을 Unreal에 통합하기 위해서 Wwise는 플러그인을 제공합니다. 이론적으로 이 플러그인은 Wwise 엔진의 모든 기능을 게임 엔진으로 전달하여 게임 엔진 안에서 직접 사용할 수 있게 해주어야 합니다. 여기서 바로 문제점이 생겼습니다. Wwise의 Unreal 플러그인이 저희가 보조 오디오 장치 출력으로 사운드를 재생하는 데에 필요한 기능을 포함하지 않더라고요.
그래서 직접 C++를 사용하여 이 기능을 추가해야 했죠.
목표
플러그인의 주요 컴포넌트인 AkComponent는 레벨 안에 존재하며 리스너와 에미터 두 가지 모두로서 작동합니다. 에미터는 레벨 안에 있는 어느 위치에서나 사운드를 재생합니다. 리스너는 한 쌍의 귀와 같이 모든 사운드를 듣고 캐릭터의 위치에 따라 사운드를 오디오 장치로 출력합니다 (보통 카메라 컴포넌트에 연결되어 있죠).
저희 게임에는 두 캐릭터가 있습니다. 각 캐릭터는 동시에 두 오디오 장치로 출력하는 리스너의 역할을 하는 AkComponent를 가지고 있습니다. 일반적으로 다음과 같죠:
- 에미터는 Post Ak Event 블루프린트 함수를 실행하며 이로 인해 Wwise에서 이벤트가 실행됩니다.
- 이 이벤트는 두 오디오 버스로 전송되는 사운드를 재생하죠.
- 오디오 버스는 각자의 Wwise 오디오 장치로 사운드를 전송합니다. 저희 게임의 경우 System 혹은 System_VR로 사운드를 전송하죠.
- 각 리스너는 각자의 출력으로 연결되어 있으며 (Output Device 1, Output Device 2) Wwise로부터 사운드를 듣습니다. 사운드가 오디오 버스에 도달하면 각자의 출력에서 사운드를 재생합니다.
이렇게 하기 위해서 저희는 AkComponent 설정에서 두 개의 텍스트 입력란을 얻어서 사운드를 라우팅할 오디오 장치의 이름을 입력할 수 있게 해야 했습니다.
1 단계. Wwise 구성하기
먼저 Wwise 오디오 장치와 오디오 버스를 만들어봅시다. 더 자세한 정보는 원본 설명서 (버스 라우팅 섹션)를 참고해 주세요.
- Wwise에서 VR 플레이어에 사용할 장치를 추가합니다. 타입을 'System'으로 설정하고 이름을 'System_VR'로 지정합니다. 바로 이 이름이 나중에 AkComponent 설정의 'Wwise Device Name' 입력란에 입력해야 할 Wwise 오디오 장치의 이름입니다.
- VR에 사용할 Audio Bus를 만들고 System_VR을 Audio Device로 설정합니다.
- Wwise에서는 사운드가 단 하나의 출력 버스로만 전송될 수 있습니다. 하지만 저희는 사운드가 두 캐릭터에게 재생되어야 하기 때문에 Auxilary Bus를 만들어서 사용해봅시다.
한 사운드를 두 장치에서 재생하기 위해서 VR 오디오 버스에 사용할 네스팅된 Auxiliary Bus를 만들어야 합니다. 바로 다음과 같이 말이죠:
이제 사운드를 각 캐릭터로 혹은 두 캐릭터로 전송할 수 있습니다.
***
이제 주요 작업이 끝났으니 모든 사운드에 버스를 설정하기만 하면 됩니다. 예를 들면 다음과 같습니다:
- 성직자 사운드 (PC): Output Bus를 Master Audio Bus로 설정하고 Auxiliary Bus는 비워둡니다
- 폴터가이스트 사운드 (VR): Output Bus를 Master Audio VR Bus로 설정하고 Auxiliary Bus는 비워둡니다
- 두 캐릭터에 사용할 사운드: Output Bus를 Master Audio Bus로 설정하고 Auxiliary Buses 목록에서 Master_VR_Aux_Bus를 선택합니다
이제 Wwise 구성이 끝났습니다.
2 단계. AkComponent 구성하기
이제 컴파일러를 다뤄야 합니다. 그렇지 않으면 플러그인의 변경 사항이 작동하지 않습니다. Blueprint 프로젝트를 C++ 프로젝트로 변환하고 컴파일하는 법을 모르신다면 다음 설명서를 참고해 주세요:
https://docs.unrealengine.com/en-US/Programming/Development/CompilingProjects/index.html
https://docs.unrealengine.com/en-US/Programming/Introduction/index.html
작업해야 할 파일은 아래에서 찾을 수 있습니다
{Project Folder}/Plugins/Wwise/Source/AkAudio
AkAudioDevice.h
/Public/AkAudioDevice.h를 열고 ‘public:’ 뒤에 다음 내용을 추가합니다
//wwise에서의 장치로 오디오 장치 연결
AkOutputDeviceID AddCustomOutput(FString AudioDevice, FString WwiseDevice, UAkComponent* in_pComponent);
//연결 제거
AKRESULT RemoveCustomOutput(AkOutputDeviceID deviceId);
// 하위 문자열로 오디오 장치 검색
TTuple <AkUInt32, FString> SearchAudioDeviceIdByName(FString deviceName);
AKComponent.h
이제 /Classes/AkComponent.h로 가서 ‘public:’ 뒤에 다음 내용을 추가합니다
TTuple <AkUInt32, FString> FAkAudioDevice::SearchAudioDeviceIdByName(FString deviceName)
{
TTuple <AkUInt32, FString> result;
AkUInt32 deviceId = AK_INVALID_DEVICE_ID;
if (deviceName.Len() == 0) {
// getting default device
AK::GetWindowsDevice(-1, deviceId, NULL, AkDeviceState_Active);
auto deviceNameWstr = AK::GetWindowsDeviceName(-1, deviceId, AkDeviceState_Active);
result.Key = deviceId;
result.Value = FString(deviceNameWstr);
} else {
AkUInt32 immDeviceCount = AK::GetWindowsDeviceCount(AkDeviceState_Active);
for (AkUInt32 i = 0; i < immDeviceCount; ++i) {
AK::GetWindowsDevice(i, deviceId, NULL, AkDeviceState_Active);
auto deviceNameWstr = AK::GetWindowsDeviceName(i, deviceId, AkDeviceState_Active);
if (FString(deviceNameWstr).Contains(deviceName)) {
result.Key = deviceId;
result.Value = FString(deviceNameWstr);
break;
}
}
}
return result;
}
AkOutputDeviceID FAkAudioDevice::AddCustomOutput(FString AudioDevice, FString WwiseDevice, UAkComponent* in_pComponent)
{
TTuple <AkUInt32, FString> Device;
AkOutputDeviceID deviceId = AK_INVALID_DEVICE_ID;
FString WwiseDeviceName = "System";
AKRESULT res = AK_Fail;
if (AudioDevice.Len() == 0 && WwiseDevice.Len() == 0) {
return deviceId;
}
if (WwiseDevice.Len() > 0) {
WwiseDeviceName = WwiseDevice;
}
Device = SearchAudioDeviceIdByName(*AudioDevice);
if (Device.Key) {
AkOutputSettings outputSettings(*WwiseDeviceName, Device.Key);
auto gameObjID = in_pComponent->GetAkGameObjectID();
res = AK::SoundEngine::AddOutput(outputSettings, &deviceId, &gameObjID, 1);
}
FString componentName = in_pComponent->GetName();
if (res != AK_Success) {
UE_LOG(LogAkAudio, Error, TEXT("Error attaching of AkComponent \"%s\" to \"%s\" <-> \"%s\". Error \"%d"), *componentName, *AudioDevice, *WwiseDeviceName, res);
} else {
UE_LOG(LogAkAudio, Warning, TEXT("AkComponent \"%s\" attached to \"%s\" <-> \"%s\" "), *componentName, *Device.Value, *WwiseDeviceName);
}
return deviceId;
}
AKRESULT FAkAudioDevice::RemoveCustomOutput(AkOutputDeviceID deviceId)
{
return AK::SoundEngine::RemoveOutput(deviceId);
}
AKComponent.h
이제 /Classes/AkComponent.h로 가서 ‘public:’ 뒤에 다음 내용을 추가합니다
/**
* Name of Audio Device in Wwise. If empty, "System" is using
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "AkComponent")
FString WwiseDeviceName;
/**
* Name of Audio Device in OS. If empty, default device is using
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "AkComponent")
FString AudioDeviceName;
AkOutputDeviceID OutputID;
AkComponent.cpp
마지막으로 /Private/AkComponent.cpp로 가서 PostRegisterGameObject와 PostUnregisterGameObject라는 두 개의 빈 함수를 찾아서 다음 내용으로 교체합니다:
void UAkComponent::PostRegisterGameObject()
{
FAkAudioDevice* AkAudioDevice = FAkAudioDevice::Get();
if (AudioDeviceName.Len() > 0 || WwiseDeviceName.Len() > 0) {
OutputID = AkAudioDevice->AddCustomOutput(AudioDeviceName, WwiseDeviceName, this);
}
}
void UAkComponent::PostUnregisterGameObject()
{
FAkAudioDevice* AkAudioDevice = FAkAudioDevice::Get();
if (AkAudioDevice && OutputID != AK_INVALID_DEVICE_ID) {
AkAudioDevice->RemoveCustomOutput(OutputID);
}
}
여기까지 잘 따라오셨나요? 축하드립니다! 이제 Visual Studio에서 프로젝트를 컴파일하고 Unreal 에디터를 실행하기만 하면 됩니다.
모든 작업을 올바르게 실행했다면 Unreal Engine 에디터의 AkComponent 설정에 두 개의 텍스트 입력란이 나타납니다. 바로 이 입력란이 오디오 출력 장치를 Wwise 오디오 장치와 연결해 줍니다.
- Wwise Device Name - 저희의 경우 'System' 혹은 'System_VR'
- Audio Device Name - 예를 들어 'Sony Headphones'과 같은 하드웨어 장치 이름. 'Headphones' 만으로도 충분히 실제 장치를 찾을 수 있을 겁니다.
장치를 찾았는지의 여부는 Unreal Editor의 Output Log에서 확인할 수 있습니다:
3 단계. 마무리 작업
마지막 단계는 캐릭터 리스너를 설정하는 것입니다. 각 폰/캐릭터에 사용할 AkComponent를 만듭니다. 그런 다음 AkComponent의 설정에서 각 컴포넌트에 맞게 오디오 장치를 입력합니다.
이때 '모든' 에미터에 리스너를 설정하는 것이 아주 중요합니다. 그렇지 않으면 이때까지 작업한 내용이 작동하지 않게 됩니다. 리스너를 설정하기 위해서는 'Set Listeners'라는 블루프린트 함수를 사용할 수 있습니다.
Begin Play에서 다음 예시와 같이 함수를 사용하세요.
이제 작업이 끝났습니다. 에디터에서 게임을 실행하고 두 오디오 장치를 확인해보세요. Output Log에 다음 내용이 표시되며 두 출력 장치에서 사운드를 들을 수 있을 겁니다.
궁금한 것이 있으시다면 제 Soundcloud를 둘러보시거나 저에게 연락해 주세요.
댓글