목표 정의

언리얼 엔진 5는 루멘을 통해 하드웨어 레이 트레이싱을 사용할 수 있다.
그리고 이를 위해 레이 트레이싱을 위한 가속 구조를 만들고, 환경 변화에 대응하여 업데이트하는 과정을 모두 처리해 준다.
따라서, 루멘을 위한 가속 구조(TLAS)를 그대로 활용한다면, 가속 구조에 대한 걱정 없이 기능 구현에 집중할 수 있다.
또한, 현재 화면으로 보이는 환경과 Ray Traced Audio를 위한 환경 정보의 정합성이 보장된다.
그렇다는 건 오디오의 공간 음향이 실시간 환경 변화에도 반영된다는 뜻이다.
물론, 엔진 코드 수정 없이 TLAS를 별도로 빌드하는 것이 가능한지는 모른다.
이런 최악의 상황을 막기 위해서라도 루멘 TLAS를 활용하는 방법을 찾을 수밖에 없는 상황이었다.
두 가지 방법
하드웨어 레이 트레이싱을 사용하는 건 두 가지 방법이 있다.
1. RTPSO (Ray Tracing Pipeline State Object)
일반적인 레이 트레이싱 방식으로, 레이 트레이싱을 위한 별도의 파이프라인으로 작동한다.
RayGen, ClosestHit, Miss 등 다양한 셰이더로 구성되고 이것들은 SBT(Shader Binding Table)를 통해 관리된다.
레이가 교차한 표면의 재질에 따라 서로 다른 셰이더를 실행하며, 전체 씬을 렌더링 하는데 적합하다.
2. Inline
DXR 1.1에서 도입된 방식으로, 픽셀 셰이더 및 컴퓨트 셰이더를 포함한 모든 셰이더 단계에서 사용 가능하다.
함수를 통해 레이 쿼리를 보내는 방식이므로 RTPSO보다 간편하지만, 복잡한 재질 표현이나 재귀를 통한 복합적인 레이 트레이싱을 구현하는 데는 한계가 있다.
따라서 래스터라이저 파이프라인과 같이 활용되어 그림자 같은 부분적인 효과 구현에 사용된다.
언리얼 엔진의 루멘 하드웨어 레이트레이싱도 두 방식을 같이 사용하고 있다. (UE 5.6 기준)
참고로 RHI Feature Level이 SM6으로 결정된다면, DXR 1.1도 지원되니 Inline 레이 트레이싱이 가능하다.
// Runtime/RHI/Public/RHIFeatureLevel.h
namespace ERHIFeatureLevel
{
enum Type : int
{
// ...
SM5,
/**
* Feature level defined by the capabilities of DirectX 12 hardware feature level 12_2 with Shader Model 6.5
* Raytracing Tier 1.1
* Mesh and Amplification shaders
* Variable rate shading
* Sampler feedback
* Resource binding tier 3
*/
SM6,
// ...
};
}
이제 이 프로젝트의 목적인 Ray Traced Audio에 더 어울리는 방식을 선택해야 한다.
Ray Traced Audio는 화면을 렌더 할 필요도 없고, 시각적 용도인 다양한 재질에 따라 서로 다른 셰이더를 실행할 필요도 없다.
또한, 레이 트레이싱 결과를 CPU로 전달하여 음향 효과를 적용해야 하니, GPGPU에 더 가까운 작업을 하게 되고 이때에는 컴퓨트 셰이더가 나중을 위해서라도 더 좋은 선택이다.
따라서 Inline 방식이 더 적합하다.
...
문제는 언리얼 엔진의 플러그인을 개발하는 입장에서는 선택권이 존재하지 않는다.
엔진 코드 수정 없이 RTPSO와 SBT를 다루어 커스텀 셰이더를 추가할 수 있을지의 여부는 불가능에 더 가깝다.
따라서 Inline 방식이 사실상 강제된다.
최종 목표
Inline 방식으로 정해졌으니, 컴퓨트 셰이더에서 레이 트레이싱을 실제로 진행하고 레이가 교차한 표면의 월드 노멀을 통해 레이를 반사시키는 것을 목표로 한다.
진행 과정
전체 과정은 크게 두 과정으로 진행된다.
1. 컴퓨트 셰이더 추가
2. 레이 트레이싱을 위한 준비 작업
1. 컴퓨트 셰이더 추가
컴퓨트 셰이더를 기반으로 진행하므로, 컴퓨트 셰이더 패스를 추가해야한다.
이건 언리얼 엔진에 다른 셰이더 패스를 추가하는 것과 동일하게 진행하면 된다.
이 과정은 아래의 영상을 참고하여 진행했다.
2. 레이 트레이싱을 위한 준비 작업
TLAS
레이 트레이싱을 위해 가장 중요한 것은 TLAS(Top Level Acceleration Structure)다.
TLAS가 있어야 레이의 교차 검사를 진행할 수 있다.
TLAS에 대한 기본적인 정보는 아래 글을 통해 확인할 수 있다.
[DirectX12][DXR] DXR프로그래밍2(Acceleration Structure)
DXR로 Ray tracing 렌더링을 하기 위해서는 렌더링할 Object들의 데이터(Vertex,Index)를 특수한 가속 구조(Acceleration Structure)에 넣어줘야 한다.왜 이러한 가속구조에 렌더링 데이터를 넣어줘야하는지 이
velog.io
따라서 셰이더의 파라미터 구성에 다음 파라미터가 추가된다.
class FAudioHardwareRaytracingCS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FAudioHardwareRaytracingCS)
SHADER_USE_PARAMETER_STRUCT(FAudioHardwareRaytracingCS, FGlobalShader)
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
// ...
SHADER_PARAMETER_RDG_BUFFER_SRV(RaytracingAccelerationStructure, TLAS)
// ...
END_SHADER_PARAMETER_STRUCT()
}
SceneViewExtension에서는 RayTracingScene을 통해 TLAS를 가져온다.
void FATSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
// ...
const FScene* Scene = View.Family->Scene->GetRenderScene();
const FRayTracingScene& RayTracingScene = Scene->RayTracingScene;
FAudioHardwareRaytracingCS::FParameters* Parameters = GraphBuilder.AllocParameters<FAudioHardwareRaytracingCS::FParameters>();
Parameters->TLAS = RayTracingScene.GetLayerView(ERayTracingSceneLayer::Base);
// ...
}
일단 TLAS가 있으면, 아래 예시 코드처럼 어떻게든 레이 트레이싱은 가능하다.
#include "/Engine/Private/RayTracing/RayTracingCommon.ush"
#include "/Engine/Private/RayTracing/TraceRayInlineCommon.ush"
#include "/Engine/Private/Lumen/LumenHardwareRayTracingCommon.ush"
RaytracingAccelerationStructure TLAS;
[numthreads(THREADS_X, 1, 1)]
void AudioHardwareRaytracingCS(uint3 DispatchThreadID : SV_DispatchThreadID, uint3 GroupThreadID : SV_GroupThreadID)
{
float3 Position = float3(0.f, 0.f, 0.f);
float3 Direction = float3(1.f, 0.f, 0.f);
float Distance = 10.f;
FRayDesc Ray;
Ray.Origin = Position;
Ray.Direction = Direction;
Ray.TMin = 0.001f;
Ray.TMax = Distance;
const uint RayFlags = RAY_FLAG_FORCE_OPAQUE;
FTraceRayInlineContext TraceRayInlineContext = CreateTraceRayInlineContext();
FTraceRayInlineResult Result = TraceRayInline(
TLAS,
RayFlags,
RAY_TRACING_MASK_OPAQUE,
Ray.GetNativeDesc(),
TraceRayInlineContext
);
if (Result.IsHit())
{
// Hit
}
}
Camera-Relative Rendering
언리얼 엔진 셰이더에서는 좌표 계산에 주의해야 한다.
언리얼 엔진은 카메라에 상대적인 방식으로 렌더링 한다. (Camera-Relative Rendering)
GPU에 값을 전달할 때부터, 월드의 좌표를 카메라 위치 기준(회전은 제외)으로 옮긴 값을 전달하여 계산한다.
이건 3D 그래픽스의 View Space와는 다른 개념이다.
이렇게 하는 이유는 카메라가 월드의 중심에서 매우 멀리 떨어지더라도, 부동 소수점의 정밀도 한계로 인한 오류를 막기 위함이다.
이 변환된 좌표를 구분하기 위해 변수명에 "Translated"가 붙어있다.
레이의 시작점은 일단 월드 기준으로 정해질 것이 분명하므로, 이걸 Translated Position으로 바꾼 뒤에 FRayDesc를 작성해야 한다.
Translated Position은 World Position - Camera Position을 통해 구할 수 있다.
이 Translated Position을 미리 계산해서 GPU로 전달해도 되고, 계산에 필요한 정보를 같이 전달하여 GPU에서 계산해도 된다.
이 프로젝트에서는 GPU에서 얻은 좌표 정보를 버퍼에 담아 CPU로 다시 전달하여 디버깅에 활용할 필요가 있다.
따라서, 계산에 필요한 정보를 같이 전달하여 GPU에서 계산하는 방식을 사용했다.
물론 최대한 CPU에서 계산한 뒤에 GPU로 넘겨주는 게, 각 스레드에 동일한 작업을 분산시키는 낭비를 줄일 수 있다.
이때의 Camera Position을 알기 위해서는 추가적인 정보가 필요하다.
이 정보들은 View Uniform Buffer에 이미 담겨서 GPU에 전달되므로, 이 버퍼를 셰이더에 바인딩하여 사용할 수 있게 했다.
class FAudioHardwareRaytracingCS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FAudioHardwareRaytracingCS)
SHADER_USE_PARAMETER_STRUCT(FAudioHardwareRaytracingCS, FGlobalShader)
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
// ...
SHADER_PARAMETER_RDG_BUFFER_SRV(RaytracingAccelerationStructure, TLAS)
SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, ViewUniformBuffer)
// ...
END_SHADER_PARAMETER_STRUCT()
}
void FATSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
// ...
const FViewInfo& ViewInfo = static_cast<const FViewInfo&>(View);
const FScene* Scene = View.Family->Scene->GetRenderScene();
const FRayTracingScene& RayTracingScene = Scene->RayTracingScene;
FAudioHardwareRaytracingCS::FParameters* Parameters = GraphBuilder.AllocParameters<FAudioHardwareRaytracingCS::FParameters>();
Parameters->TLAS = RayTracingScene.GetLayerView(ERayTracingSceneLayer::Base);
Parameters->ViewUniformBuffer = ViewInfo.ViewUniformBuffer;
// ...
}
셰이더에는 다음과 같은 함수를 만들어서 사용했다.
float3 TranslateWorldPositionToAbsoluteWorldPosition(float3 TranslatedWorldPosition)
{
return TranslatedWorldPosition - (View.PreViewTranslationHigh + View.PreViewTranslationLow);
}
float3 AbsoluteWorldPositionToTranslatedWorldPosition(float3 AbsoluteWorldPosition)
{
return AbsoluteWorldPosition + (View.PreViewTranslationHigh + View.PreViewTranslationLow);
}
View 유니폼 버퍼의 PreViewTranslationHigh와 PreViewTranslationLow는 -(시점의 절대 월드 위치)를 가지고 있다.
아니면 시점의 절대 월드 위치인 WorldViewOriginHigh와 WorldViewOriginLow를 활용할 수 있다.
float3 TranslateWorldPositionToAbsoluteWorldPosition(float3 TranslatedWorldPosition)
{
return TranslatedWorldPosition + (View.WorldViewOriginHigh + View.WorldViewOriginLow);
}
float3 AbsoluteWorldPositionToTranslatedWorldPosition(float3 AbsoluteWorldPosition)
{
return AbsoluteWorldPosition - (View.WorldViewOriginHigh + View.WorldViewOriginLow);
}
둘 다 같은 결과를 리턴한다.
여기에서 High와 Low로 나눠진 이유는 부동 소수점의 정밀도를 더 높이기 위한 방법이라고 한다.
실수의 정수 부분과 소수 부분을 나눠서 전달하는 방식으로 정밀도를 더 높인다.
예를 들면 123.456이라는 실수를 123.0과 0.456으로 나누어 전달한다.
View 유니폼 버퍼에는 이것 외에도 다양한 정보가 포함되어 있다.
Source/Runtime/Engine/Public/SceneView.h 파일을 보면 #define VIEW_UNIFORM_BUFFER_MEMBER_TABLE을 찾을 수 있고, 여기에 보이는 것만 해도 약 200개 이상의 정보가 존재하는 걸 확인할 수 있다.
일단 여기까지만 해도 레이 트레이싱을 하는 것에는 문제가 없다.
레이의 Origin, Direction, Distance를 임의로 지정할 수 있고, 이 직선 경로에 오브젝트가 있다면 레이 트레이싱 결과의 Hit 판정을 검사해서 확인할 수 있다.
하지만, 중요한 것은 레이가 교차한 표면의 월드 노멀 벡터를 계산하고 레이를 반사시켜 레이 트레이싱을 이어가는 것이다.
소리의 전파 경로를 계산하기 위해선 레이를 반사시켜야 하고, 레이를 반사시키기 위해선 표면의 월드 노멀을 알아야 한다.
이것과 관련한 내용은 다음 글에 이어서 작성하겠다.
'게임테크랩 1기 > Audio Tracing' 카테고리의 다른 글
| [UE5 | Audio Tracing] 음향 재질을 위한 에셋 구현 과정 (0) | 2025.11.29 |
|---|---|
| [UE5 | Audio Tracing] 실시간 음향 정보 갱신을 위한 Readback 버퍼 풀과 데이터 캡처 (0) | 2025.11.23 |
| [UE5 | Audio Tracing] 음향 재질을 위한 Custom Primitive Data 획득 (2) | 2025.10.27 |
| [UE5 | Audio Tracing] TraceRayInline을 통한 월드 노멀 획득 (1) | 2025.09.29 |
| [UE5 | Audio Tracing] 개요 (0) | 2025.09.28 |