TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] 효율적인 ZSTD Shader 딕셔너리 학습 방안

jplee 2025. 9. 24. 21:12

An Efficient ZSTD Shader Dictionary Training Scheme
ZSTD 딕셔너리 기반 Shader 압축 방안 글에서, ZSTD 딕셔너리를 활용하여 UE의 ShaderCode를 압축하는 방법을 소개했습니다. 이 방법은 ShaderLibrary의 압축률을 크게 향상시킬 수 있습니다. 하지만 딕셔너리 학습과 압축 과정이 복잡하여 효율적인 엔지니어링 구현이 아닙니다:

  1. 엔진 기본 Shader 압축을 비활성화해야 함
  2. 각 Unique Shader의 ShaderCode를 Dump하기 위해 프로젝트를 한 번 Cook해야 함
  3. Dump된 ShaderCode 파일을 바탕으로 ZSTD 프로그램을 사용하여 딕셔너리 학습
  4. 완전한 Cook 과정을 다시 실행하여 학습된 딕셔너리 압축하고 최종 shaderbytecode 생성

위 과정에 따르면, ShaderCode를 Dump하기 위해 엔진을 수정해야 하고, 과정이 분리되어 있어 Dump 후 zstd 프로그램을 실행해야 딕셔너리를 얻을 수 있으며, 다시 한 번 프로젝트를 Cook해야 합니다. 또한 Shader 압축을 비활성화하면 LZ4 압축 DDC Cache Miss가 발생하여 반복 Cook 시간이 크게 증가하며, 대량의 Shader가 있는 프로젝트에서는 시간이 너무 많이 소요됩니다.
이러한 문제점을 바탕으로, 효율적인 딕셔너리 학습방법을 연구하고 구현했습니다. 엔진 수정 없이 빠르게 딕셔너리를 학습하고 압축할 수 있습니다. ushaderbytecode에서 직접 딕셔너리를 학습하고 ZSTD + 딕셔너리 압축으로 ushaderbytecode를 생성하여 처리 효율을 크게 향상시켰습니다. 완전한 Plugin-Only 구현으로 도입 비용이 거의 없으며, 향후 HotPatcher의 확장 모듈로 배포될 예정입니다.
앞서 언급한 딕셔너리 학습 과정의 핵심 문제는 세 가지입니다:

  1. 압축되지 않은 ShaderCode를 효율적으로 얻는 방법
  2. ZSTD 프로그램 의존성 없이 UE 내에서 딕셔너리 학습을 완전히 구현하여 추가 IO 과정을 피하는 방법
  3. 재Cook 없이 딕셔너리로 ShaderCode를 압축하는 방법

이 글에서는 위 세 가지 문제를 분석하고 해결하는 방법을 차례대로 살펴보겠습니다.

DumpShaderCode

어떤 Dump 방식이 더 합리적인가?

ShaderCode를 효율적으로 Dump하려면 어디서 Dump하는 것이 가장 합리적인지 이해해야 합니다:
Shader 컴파일 완료 시

FShaderCodeLibrary::AddShaderCode 시 (ushaderbytecode 직렬화)

ZSTD 딕셔너리 기반 Shader 압축 방안 글에서의 접근 방식은 FShaderCodeLibrary::AddShaderCode에서 Dump하는 것이었습니다. DDC 캐시된 Shader는 컴파일 과정을 거치지 않기 때문에, Shader 컴파일 완료 시점에서 Dump하면 DDC 캐시된 것들이 누락됩니다.
하지만 이 두 방식 모두 최선의 방법은 아닙니다. 사실 자세히 생각해보면, 최종 패키지에는 이미 현재 플랫폼의 모든 ShaderCode가 포함되어 있습니다:
기본적으로 프로젝트의 Global과 ProjectName ushaderbytecode 파일은 프로젝트의 완전한 ShaderLibrary이며, 기본적으로 LZ4로 압축되어 있습니다.
기본 패키징으로 생성된 ushaderbytecode에서 직접 원본 압축되지 않은 ShaderCode를 추출하여 학습할 수 있다면, 반복 Cook 과정을 피할 수 있습니다.
이 방식을 구현하려면 ushaderbytecode 파일 형식을 분석해야 합니다.

ushaderbytecode 형식 분석

ushaderbytecode 파일은 UE의 ShaderCode 관리 컨테이너로, shader hash와 shadercode 간의 대응 관계를 저장하여 런타임에 조회하고 로드합니다.
ushaderbytecode 파일의 직렬화는 ShaderCodeLibrary의 FEditorShaderCodeArchive::Finalize에서 구현됩니다.
최종 ushaderbytecode 파일 형식:

  • unsigned int GShaderArchiveVersion=2; 4바이트, ShaderArchive 버전 번호 기록
  • FSerializedShaderArchive SerializedShaders; shader의 Hash, 오프셋 등 정보 기록, ushaderbytecode의 인덱스로, 현재 shaderbytecode에 포함된 모든 shader 정보와 파일 내 특정 ShaderCode의 오프셋을 기록합니다.
classRENDERCORE_API FSerializedShaderArchive
{
public:
	TArray<FSHAHash> ShaderMapHashes;// 본 Archive의 ShaderMap HASH 배열, 예를 들어 특정 머티리얼은 독립된 ShaderMap입니다
	TArray<FSHAHash> ShaderHashes;// Archive의 모든 Shader HASH 배열
	TArray<FShaderMapEntry> ShaderMapEntries;// ShaderMap의 Archive 내 정보, Shader 수량 및 ShaderIndicesOffset의 오프셋 값
	TArray<FShaderCodeEntry> ShaderEntries;// Archive에 저장된 모든 ShaderCode 오프셋 배열(Unique Shader 수량), ShaderCode 실제 읽기 시 offset, Size 및 압축 해제 후 크기 접근용
	TArray<FFileCachePreloadEntry> PreloadEntries;// 미리 로드할 Shader 배열, 각 미리 로드할 Shader의 Offset과 크기 기록
	TArray<uint32> ShaderIndices;// 각 Shader의 Index 값 기록, 각 ShaderMap의 ShaderIndicesOffset은 현재 ShaderMap의 ShaderIndices 내 인덱스 오프셋 저장, ShaderIndicesOffset과 NumShader로 ShaderIndices에서 특정 ShaderMap의 모든 Index 접근 가능. 서로 다른 shadermap이 동일한 Shader 참조 가능, 이 항목으로 Shader 재사용 구현
	FHashTable ShaderMapHashTable;
	FHashTable ShaderHashTable;
// ...
};

FSerializedShaderArchive
ShaderMapEntries
ShaderEntries
ShaderIndices

  • ShaderCode 배열, 실제 컴파일된 ShaderCode, 순서대로 하나씩 직렬화됩니다.

FEditorShaderCodeArchive::Finalize
런타임에서 FShaderCodeLibrary의 OpenLibrary로 ushaderbytecode를 로드할 때, 파일 전체를 메모리에 로드하지 않고 GShaderArchiveVersion과 FSerializedShaderArchive 구조만 직렬화합니다:

ushaderbytecode의 인덱스 구조, 버전 번호, 파일 핸들(FileCacheHandle)을 얻어 실제 ShaderCode 로드에 사용합니다.
몇 가지 개념 설명:

  • ShaderMap은 여러 ShaderCode를 포함합니다. 예를 들어 하나의 Material은 여러 Shader 변형을 생성하며, 이들은 모두 동일한 ShaderMap에 위치합니다.
  • 여러 ShaderMap의 ShaderCode는 재사용되며 교차 참조 관계를 가질 수 있지만, 실제로 두 번 복사되지는 않습니다. ShaderMap의 Shaders는 ShaderIndices를 통해 기록되며, 이는 단지 인덱스일 뿐, 실제 ShaderCode의 오프셋은 ShaderEntries에 저장되며 Unique Shader라고 합니다.
  • Material은 직렬화 시(bShareCode 활성화), ShaderMap의 HASH를 uasset에 직렬화하여 런타임에서 해당 ShaderMap을 쉽게 조회할 수 있게 합니다.

읽기 시에는 이 FSHAHash 값을 통해 FShaderCodeLibrary::LoadResource를 사용하여 로드합니다:

ShaderCode 읽기

앞서 분석한 내용을 통해 ShaderCode가 ushaderbytecode에 저장되는 구조를 알 수 있습니다.
FShaderCodeArchive에는 ReadShaderCode 함수가 있어 특정 ShaderCode를 로드할 수 있습니다:

ShaderIndex는 ShaderHash를 통해 다음 인터페이스로 얻을 수 있습니다:

int32 FSerializedShaderArchive::FindShader(const FSHAHash& Hash) const;


ShaderIndex는 사실 해당 ShaderCode가 ShaderEntries에서의 인덱스를 의미합니다.
ushaderbytecode 내의 모든 Shader를 순회하는 방법:

ShaderHash나 ShaderIndex를 얻은 후에는 ReadShaderCode를 직접 사용하여 관련 ShaderCode 데이터를 읽을 수 있습니다:

IMemoryReadStreamRef Code = Library->ReadShaderCode(ReadShaderIndex);


하지만 기본적으로 이 데이터는 LZ4로 압축된 상태이므로 LZ4를 통해 압축을 해제해야 합니다:

FMemStackBase& MemStack = FMemStack::Get();
check(ShaderEntry.Size == Code->GetSize());
const uint8* ShaderCode = nullptr;

FMemMarkMark(MemStack);
if (ShaderEntry.UncompressedSize != ShaderEntry.Size)
{
    void* UncompressedCode = MemStack.Alloc(ShaderEntry.UncompressedSize, 16);
    const bool bDecompressResult = FCompression::UncompressMemoryStream(NAME_LZ4, UncompressedCode, ShaderEntry.UncompressedSize, Code, 0, ShaderEntry.Size);
    check(bDecompressResult);
    ShaderCode = (uint8*)UncompressedCode;
}
TArrayView<uint8> OrinalShaderCodeView = MakeArrayView((uint8*)ShaderCode, ShaderEntry.UncompressedSize);

이렇게 하면 ushaderbytecode에서 압축되지 않은 원본 ShaderCode를 직접 읽을 수 있습니다.
이를 개별적으로 디스크에 저장하거나 MemoryBuffer를 기반으로 딕셔너리를 학습하는 데 사용할 수 있습니다. 물론, 권장되는 방법은 압축 해제된 ShaderCode를 별도의 MemoryBuffer에 복사하여 나중에 데이터셋 학습에 사용하는 것입니다. 이렇게 하면 IO 없이 메모리에서 직접 딕셔너리를 학습할 수 있습니다.

엔진 수정 없는 구현

이전 섹션에서는 ushaderbytecode에서 ShaderCode를 읽는 방법을 소개했지만, 한 가지 문제가 있습니다: FShaderCodeArchive 클래스가 내보내지지 않았습니다.
RenderCore\Public\ShaderCodeArchive.h

class FShaderCodeArchive : public FRHIShaderLibrary { // ... }


외부 모듈에서는 그 멤버 함수에 접근할 수 없어 링크 오류가 발생합니다.
또한 ReadShaderCode 함수는 protected 멤버이므로 외부 심볼이 접근할 수 없습니다.
엔진을 수정해야 한다면, 가장 간단한 방법은 RENDERCODE_API를 추가하여 심볼을 내보내는 것입니다. 그렇지 않으면 일부 기술적 트릭을 사용해야 합니다. 이에 대한 내용은 제가 이전에 작성한 글을 참조하세요:

  • C++ 클래스의 액세스 제어 메커니즘 돌파하기
  • 액세스 제어 메커니즘의 가시성과 접근성

이 글의 내용을 기반으로 엔진을 수정하지 않고도 ShaderCode를 읽을 수 있어, 공식 엔진에서도 사용할 수 있습니다. 구체적인 방법은 본 글에서 다시 설명하지 않겠습니다.

UE에서 닥셔너리 학습

이전 글에서는 zstd 명령줄 프로그램을 사용하여 학습했습니다:

# 딕셔너리 생성
$ zstd --train ./DumpShaders/PCD3D_SM5/* -r -o PCD3D_SM5.dict

이전 섹션에서 얻은 ShaderCode의 MemoryBuffer를 기반으로 메모리에서 직접 딕셔너리를 학습할 수 있습니다.
UE에서 ZSTD 코드를 직접 사용:

ZDICTLIB_API size_t ZDICT_trainFromBuffer(void* dictBuffer, size_t dictBufferCapacity,
    const void* samplesBuffer,
    const size_t* samplesSizes, unsigned nbSamples);

딕셔너리를 생성하기 위한 보조 함수를 캡슐화할 수 있습니다:

static buffer_t FUZ_createDictionary(const void* src, size_t srcSize, size_t blockSize, size_t requestedDictSize)
{
    buffer_t dict = kBuffNull;
    size_t const nbBlocks = (srcSize + (blockSize-1)) / blockSize;
    size_t* const blockSizes = (size_t*)malloc(nbBlocks * sizeof(size_t));
    if (!blockSizes) return kBuffNull;
    dict.start = malloc(requestedDictSize);
    if (!dict.start) { free(blockSizes); return kBuffNull; }
    {
        size_t nb;
        for (nb=0; nb<nbBlocks-1; nb++) blockSizes[nb] = blockSize;
        blockSizes[nbBlocks-1] = srcSize - (blockSize * (nbBlocks-1));
    }
    {
        size_t const dictSize = ZDICT_trainFromBuffer(dict.start, requestedDictSize, src, blockSizes, (unsigned)nbBlocks);
        free(blockSizes);
        if (ZDICT_isError(dictSize)) { FUZ_freeDictionary(dict); return kBuffNull; }
        dict.size = requestedDictSize;
        dict.filled = dictSize;
        return dict;
    }
}

이렇게 하면 ShaderCode를 디스크에 저장한 다음 ZSTD 명령줄을 호출하여 학습하는 과정을 피할 수 있습니다. 메모리의 데이터를 기반으로 모든 학습 로직을 코드에서 실행할 수 있습니다.

딕셔너리로 shaderbytecode 압축

앞의 두 섹션의 을 기반으로 ushaderbytecode에서 직접 Shader의 ZSTD 딕셔너리를 학습할 수 있습니다.
그렇다면 ZSTD 딕셔너리 압축을 사용하여 최종 ushaderbytecode를 생성하는 가장 좋은 방법은 무엇일까요?
ushaderbytecode 형식 분석 섹션을 통해 ushaderbytecode의 저장 형식을 알 수 있습니다:

ushaderbytecode의 저장 형식
실제로 Shader를 로드할 때는 ShaderHash를 통해 ShaderIndices의 인덱스를 확인하고, ShaderHash에 해당하는 ShaderEntries[index]에 접근합니다. ShaderEntries[index]의 내용은 위 그림의 ShaderCodeData에서 ShaderCode의 offset, Size, UncompressedSize로, ShaderCode를 결정적으로 읽을 수 있게 합니다.
원본 ushaderbytecode를 기반으로 딕셔너리 압축을 통해 다른 최종 ushaderbytecode를 생성하려면 주로 다음 두 부분을 수정해야 합니다:

  1. 딕셔너리  압축 후의 ShaderCodeData 데이터
  2. 딕셔너리  압축 후 ShaderEntries의 각 Shader의 offset, Size(압축 후 크기) 수정

구체적인 구현 단계는 다음과 같습니다:

  1. 원본 ushaderbytecode를 읽고 SerializedShaders 저장
  2. 순서대로 각 ShaderCode를 읽고 LZ4로 압축 해제
  3. ZSTD + 딕셔너리 으로 각 ShaderCode를 압축하여 전역 MemoryBuffer에 추가하고, 이 MemoryBuffer에서 압축된 ShaderCode의 Offset, Size를 얻음
  4. 각 ShaderCode의 MemoryBuffer에서의 새 Offset, Size를 앞서 저장한 SerializedShaders에 업데이트
  5. 마지막으로 GShaderCodeArchiveVersion, SerializedShaders, MemoryBuffer를 순서대로 파일에 직렬화

이렇게 하면 기본 패키징된 ushaderbytecode에서 직접 딕셔너리 압축을 사용하는 ushaderbytecode를 생성하여 추가 Cook 프로세스를 피할 수 있습니다.

ZSTD 최적화 전략

최신 버전의 ZSTD(1.5.2)로 업그레이드하면 이전 통합(1.4.4)에 비해 압축률이 더 향상됩니다.
인터페이스 측면의 최적화, *usingDict 대신 *usingCDict 사용:

// 권장
size_t ZSTD_compress_usingCDict(ZSTD_CCtx* cctx,
    void* dst, size_t dstCapacity,
    const void* src, size_t srcSize,
    const ZSTD_CDict* cdict);
// 권장하지 않음
size_t ZSTD_compress_usingDict(ZSTD_CCtx* cctx,
    void* dst, size_t dstCapacity,
    const void* src, size_t srcSize,
    const void* dict, size_t dictSize,
    int compressionLevel);

*usingDict 시리즈 함수는 딕셔너리 을 로드하므로 단일 압축에만 권장됩니다. 동일한 딕셔너리 으로 빈번하게 압축하는 경우 매우 느려질 수 있으므로(한 단계 느림) *usingCDict로 대체해야 합니다. 압축 해제도 마찬가지입니다.

딕셔너리  학습 및 압축 효율성

테스트 데이터:

  • Shaderbytecode 256M
  • 원본 ShaderCode 총 크기(LZ4 압축 해제 후): 682M
  • 66579 Unique Shaders

딕셔너리  학습:

Shader 압축:

대부분의 ShaderCode의 압축 시간은 ShaderCode 크기에 따라 약 1ms 정도입니다.
딕셔너리  학습 + 최종 ushaderbytecode 생성의 총 시간은 4분도 걸리지 않아 시간이 전혀 병목이 되지 않습니다.
실제 프로젝트에서 LZ4 압축 ushaderbytecode와 ZSTD + 딕셔너리 압축의 크기 비교:

런타임 사례

패키징된 런타임 데모를 제공했습니다. 기본 패키지는 LZ4로 압축된 Shader이며, 딕셔너리와 딕셔너리 압축된 shaderbytecode(StarterContent 및 Global 포함)를 제공하여 기능 검증 및 런타임 효율성 테스트에 사용할 수 있습니다.
다운로드 링크: ZstdExample_WindowsNoEditor
ZstdShader_WindowsNoEditor_001_P.pak를 Content/Paks에 넣으면 기본적으로 ZSTD와 딕셔너리 모드를 사용하여 딕셔너리 압축된 ShaderLibrary를 읽습니다:

이 Pak을 넣지 않으면 엔진은 기본적으로 LZ4 압축된 ushaderbytecode를 사용합니다.
ZSTD 모드의 실행 효과, Shader 오류가 없음을 확인할 수 있습니다:

또한 Unreal Insight를 통해 런타임 성능을 분석할 수 있습니다:

ZstdExample -windowed -resx=1280 -resy=720 -log -trace=cpu -tracehost=127.0.0.1

HotPatcher 통합

HotPatcher 통합

ushaderbytecode에서 직접 딕셔너리를 학습하고 기존 ushaderbytecode를 딕셔너리로 압축하여 작업 비용을 최대한 줄일 수 있습니다.

결론

이 글에서는 효율적인 ZSTD Shader 딕셔너리 학습 방법을 공유했습니다. ushaderbytecode 파일 형식을 분석하고 ShaderCode를 덤프하는 방식을 비교하여 UE에서 직접 딕셔너리를 학습합니다. 또한 엔진을 수정하지 않고도 사용할 수 있는 방법을 공유하여 공식 엔진에서도 원활하게 사용할 수 있으며, 엔진 변경 관리 비용을 줄일 수 있습니다.
하나의 호출 과정에서 딕셔너리 학습과 딕셔너리를 이용한 압축을 모두 구현할 수 있어 압축 효율이 크게 향상됩니다. 이를 통해 딕셔너리 학습과 딕셔너리 압축을 완전한 Plugin-Only 구현으로 만들 수 있어, 추가 수정이나 프로세스 없이 HotPatcher 통합을 위한 장애물을 제거했습니다. 향후 HotPatcher의 확장 모듈로 출시될 예정입니다.
Export Recast Navigation Data from UE4

최신 버전은 UE5를 지원합니다. 자세한 내용은 github의 UE5.0 브랜치를 확인하세요: ue4-export-nav-data/tree/UE5.0.

Recast Navigation은 게임 내 AI에게 경로 탐색 기능을 제공하는 오픈 소스 게임 내비게이션/경로 찾기 엔진입니다. UE와 Unity 모두 RecastNavigation을 통합하여 게임에 내비게이션과 경로 찾기 계산을 제공합니다(물론 수정된 버전입니다). UE의 NavigationSystem 및 NavMesh 모듈에서 관련 코드 구현을 볼 수 있습니다.
최근에 클라이언트의 맵 정보를 UE가 아닌 네트워크 아키텍처의 서버로 내보내 서버에서 플레이어 위치를 검증하는 요구 사항이 있었습니다. 클라이언트에서 생성된 내비게이션 데이터를 클라이언트 월드의 으로 내보낼 수 있다고 생각했습니다. 그래서 UE 플러그인(Github에 오픈 소스로 공개: ue4-export-nav-data)을 개발하여직접 UE에서 생성된 내비게이션 데이터를 내보내는 기능을 구현했습니다. 관심 있으신 분들은 직접 코드를 확인해보세요.
내보낸 내비게이션 데이터를 사용하면 UE가 아닌 네트워크 아키텍처의 서버에서 Recast Navigation을 기반으로 경로 찾기 계산을 완벽하게 구현할 수 있으며, UE와 원활하게 연동됩니다.

2019.12.04 업데이트: 이 플러그인은 언리얼 마켓플레이스에 출시되었으며, 구매 링크는 ExportNavigation입니다. 개발자의 오픈 소스 정신을 지원하기 위해 Github의 오픈 소스 저장소는 계속 유지되지만 거의 업데이트되지 않을 예정입니다. 이 플러그인이 유용하다고 생각되시면 마켓플레이스에서 구매하여 작성자를 지원해 주세요.

Recast Navigation

먼저, Github 오픈 소스 버전의 Recast 컴파일에 대해 간단히 소개하겠습니다. UE 엔진에서 사용하는 recast는 이 오픈 소스 버전을 기반으로 수정된 것입니다. UE_4.22.3까지 UE에서 사용하는 recast 버전은 v1.4입니다(다양한 엔진 버전에서 사용하는 recast 버전 정보는 Source/Runtime/Navmesh/Recast-Readme.txt에서 확인할 수 있습니다).
RecastNavigation의 Github 소스 코드 주소: recastnavigation.
recastnavigation 코드에는 RecastDemo라는 내비게이션 메시 생성 및 경로 찾기 계산 도구가 포함되어 있어, 프로젝트에 RecastNavigation을 통합하는 예제로 활용할 수 있습니다.
README.md에는 각 플랫폼의 컴파일 과정이 설명되어 있습니다. 여기서는 Windows에서의 컴파일 과정을 자세히 설명하겠습니다.

  1. 먼저 premake5를 다운로드하고 시스템 PATH에 추가합니다.
  2. RecastNavigation 코드를 클론합니다.
  3. SDL2(Development Libraries 선택)를 다운로드하고 recastnavigation\\RecastDemo\\Contrib 디렉토리에 압축을 풉니다. 폴더 이름을 SDL로 변경하면 디렉토리 구조는 다음과 같습니다:
D:\\recastnavigation\\RecastDemo\\Contrib\\SDL>tree /a
+---docs
+---include
\\---lib
    +---x64
    \\---x86


recastnavigation\\RecastDemo 디렉토리에서 premake5 vs2017 명령을 실행합니다(vs201x는 현재 시스템에 설치된 버전에 따라 다릅니다). 이 명령은 RecastDemo 디렉토리에 build/vs2017 디렉토리를 생성하며, 여기에 VS 프로젝트 솔루션이 포함됩니다.

RecastDemo\Build\vs2017\recastnavigation.sln을 열고 컴파일합니다.

컴파일된 RecastDemo.exe는 RecastDemo\Bin 위치에 있습니다.

C:\Users\imzlp\source\repos\recastnavigation\RecastDemo\Bin&gt;tree /a /f
문서 경로 목록
볼륨 일련 번호는 000002D0 ECDB:6872
C:.
|   .gitignore
|   DroidSans.ttf
|   RecastDemo.exe
|   RecastDemo.pdb
|   SDL2.dll
|   Tests.exe
|   Tests.pdb
|
+---Meshes
|       dungeon.obj
|       nav_test.obj
|       undulating.obj
|
\---TestCases
        movement_test.txt
        nav_mesh_test.txt
        raycast_test.txt

주요 파일은 다음과 같습니다: DroidSans.ttf/RecastDemo.exe/SDL2.dll/Meshs/.

참고: RecastDemo가 인식할 수 있도록 obj 파일을 Meshs/ 디렉토리에 넣어야 합니다.

이제 RecastDemo.exe를 열어 기본 제공되는 세 개의 obj 모델에서 내비게이션 데이터 생성 테스트를 할 수 있습니다. Build로 생성한 후 Save를 하면 RecastDemo.exe가 있는 디렉토리에 .bin 파일이 생성됩니다. 이것이 recast로 생성된 내비게이션 데이터입니다.

참고: UE에서 Navmesh를 내보내는 의미는 UE 경로 찾기 범위 내의 모델을 내보낸 다음, RecastDemo를 통해 해당 모델을 기반으로 경로 찾기 데이터를 생성하는 것입니다. 이로 인해 UE에서 경로 찾기에 영향을 주는 프레임을 추가해도, 내보낸 Navmesh가 RecastDemo에서 생성될 때 적용되지 않는 문제가 발생합니다.

플러그인: ue-export-nav-data

이 플러그인은 UE에서 RecastNavigation을 내보내는 도구로, ExportNavRuntime과 ExportNavEditor 두 모듈로 구성됩니다. 에디터 모듈은 UE 에디터의 ToolBar 버튼을 제공하여 에디터에서 내비게이션 데이터를 내보낼 수 있게 합니다.

버튼을 클릭한 후 경로를 선택하면 선택한 경로에 .bin과 obj 두 파일이 생성됩니다. Windows에서는 내보낸 디렉토리가 자동으로 파일 탐색기에서 열립니다.

  • bin: UE에서 구축한 완성된 내비게이션 데이터로, 외부 서버에서 사용합니다.
  • obj: UE에서 경로 찾기를 위한 Mesh로, RecastDemo에서 bin 파일을 생성하는 데 사용할 수 있습니다.

또한, NavData는 런타임에 내보낼 수 있지만 NavMesh는 지원하지 않습니다. UE의 내비게이션은 딕셔너리 계산되기 때문입니다. 자세한 내용은 UFlibExportNavData에서 제공하는 메서드를 참조하세요.
C++와 블루프린트에서 내보낸 bin에서 위치가 유효한 경로 찾기 위치인지 확인하는 두 가지 방법을 제공합니다:

bool UFlibExportNavData::IsValidNavigationPointInNavbin(const FString& InNavBinPath, const FVector& Point, const FVector InExtern = FVector::ZeroVector);
bool UFlibExportNavData::IsValidNavigationPointInNavObj(class UdtNavMeshWrapper* InDtNavObject, const FVector& Point, const FVector InExtern = FVector::ZeroVector);

이는 UE에서 제공하는 UNavigationSystemV1::ProjectPointToNavigation과 비교하여 내보낸 데이터가 일치하는지 확인할 수 있습니다.
또한 시작점을 전달하여 내비게이션 경로 지점을 가져오는 메서드도 있습니다:

bool FindDetourPathByNavMesh(dtNavMesh* InNavMesh, const FVector3& InStart, const FVector3& InEnd, std::vector&lt;FVector3&gt;& OutPaths);


이 함수는 엔진의 UNavigationSystemV1::FindPathToLocationSynchrously 결과와 일치합니다. 게임 월드 좌표를 전달하면 함수 내부에서 변환이 이루어지고, 반환값도 월드 좌표입니다.
이 플러그인의 장점은 다음과 같습니다:

  • UE에서 NavMesh의 .obj를 내보낸 후 RecastDemo로 생성하는 대신, UE에서 직접 bin을 내보냅니다. 물론 NavMesh를 obj로 내보내는 기능도 유지합니다.
  • UE에서 생성된 내비게이션 데이터를 직접 내보내기 때문에 보이는 그대로 얻을 수 있습니다. 이는 NavMesh를 내보내고 RecastDemo에서 내비게이션 메시를 생성할 때 특정 영역이 경로 찾기를 생성하지 않는 문제와, UE와 RecastDemo의 다양한 경로 찾기 매개변수 불일치로 인한 경로 찾기 데이터 불일치 문제를 해결합니다.
  • UE의 ue-detour 버전을 추가로 추출하여 외부 서버에 원활하게 통합할 수 있으며, 클라이언트 좌표와 서버 좌표 간 변환이 필요하지 않습니다(물론 UE 좌표와 Recast 좌표는 변환이 필요하지만 내부적으로 처리되므로 사용 시 수동 변환이 필요하지 않습니다).

UE 좌표계와 Recast 좌표계 간 변환은 UE4RecastHelper의 두 함수를 사용할 수 있습니다:

namespace UE4RecastHelper
{
    FCustomVector Recast2UnrealPoint(const FCustomVector& Vector)
    {
        return FCustomVector(-Vector.X, -Vector.Z, Vector.Y);
    }

    FCustomVector Unreal2RecastPoint(const FCustomVector& Vector)
    {
        return FCustomVector(-Vector.X, Vector.Z, -Vector.Y);
    }
};

라이브러리: ue-recast-detour

이것은 UE 코드에서 추출한 recast detour 라이브러리로, UE 소스 코드 경로는 Runtime/Navmesh/Detour입니다. 주요 목적은 UE 클라이언트와 외부 서버의 검증 방법 일관성을 보장하는 것입니다.
Github의 RecastNavigation은 UE에서 사용하는 버전보다 높고, 앞서 언급했듯이 UE는 recast를 기반으로 많은 수정을 했습니다. 코드 차이로 인한 다른 결과를 방지하기 위해, UE에서 사용(및 수정)한 버전을 추출하여 외부에서 사용할 수 있게 했습니다. 이를 통해 클라이언트와 서버의 결과가 일치함을 보장할 수 있습니다.
코드는 Github에 있습니다: ue4-recast-detour. 이 저장소의 Detour/ 디렉토리에 Detour 라이브러리의 전체 코드가 있습니다.
나머지 코드는 UE와의 검증 및 UE와 Recast 좌표 변환을 위한 UE4RecastHelper 클래스와, UE의 월드 위치가 bin의 경로 찾기 데이터에서 유효한지 테스트하는 간단한 명령줄 프로그램입니다.
UE4RecastHelper는 현재(2019.11.01) dtNavMesh와 bin 파일 간의 serialize/deserialize 메서드와, UE의 UNavigationSystemV1::ProjectPointToNavigation 함수와 동일한 구현 방식의 UE4RecastHelper::dtIsValidNavigationPoint 함수를 제공합니다. 이는 점이 유효한 경로 찾기 위치인지 확인하여 UE 클라이언트와 외부 서버의 검증 방법 일관성을 보장합니다.
Detour가 지원하는 모든 작업은 서버에서 구현할 수 있습니다(UE에서 내비게이션 데이터를 이미 가져왔기 때문에). 필요에 따라 확장할 수 있습니다.
ue-detour.exe 도구 사용법을 간단히 설명하겠습니다. 명령줄에서 ue4-detour.exe를 실행하면 사용법이 표시됩니다.
먼저, UE에서 내비게이션 데이터를 내보내야 합니다. 여기서는 .bin 파일만 필요하며, 예를 들어 D:\\NavData에 생성됩니다.
그런 다음 ue4-detour.exe를 찾아 다음 명령을 사용할 수 있습니다:

D:\>ue4-detour.exe
Usage:
        ue4-detour.exe dtNavMesh.bin Loc.X Loc.Y,loc.Z Extren.X Extern.Y Extren.Z
PS:{Extern.X Extern.Y Extern.Z}는 생략 가능하며, 기본값은 {10.f 10.f 10.f}입니다.
예시:
        ue4-detour.exe dtNavMesh.bin -770.003 -593.709 130.267 10.0 10.0 10.0

블루프린트에서도 .bin을 직접 로드하여 테스트할 수 있습니다:

마무리

이 글에서 사용된 오픈 소스 저장소:

업데이트

2021.05.27 업데이트:

  • 플러그인이 UE5를 지원하며, UE5의 NavMesh 데이터 및 내비게이션 메시 내보내기를 지원합니다
  • 데이터와 ue-detour 검증이 성공적으로 완료되었습니다.