[UE] World Partition Deep Dive 2 - Streaming Generation

2024. 8. 29. 22:46·Column
  • Last Updated: UE 5.4
  • World Partition, OFPA, Data Layer, Level Instance, HLOD의 해당 기능에 대한 기본적인 이해가 필요할 수 있습니다.
  • 월드 빌딩 전반에 대한 공식 문서는 World Building Guide 참고
    • https://dev.epicgames.com/community/learning/knowledge-base/r6wl/unreal-engine-world-building-guide

1편에서 월드 파티션에서의 액터 관리에 대해 알아봤습니다. 이제 본격적으로 스트리밍 생성과 업데이트 방법에 대해 살펴보겠습니다.
월드 파티션은 스트리밍 소스와 셀의 거리에 따라 로드, 언로드하는 자동 스트리밍 시스템입니다. 1편에서 액터를 월드에 배치하고 저장한 상황에서의 정보는 월드에 배치된 Actor의 Desc 까지입니다. 아직 해당 정보로는 어떤 셀에 어떤 액터가 들어있는지 알 수 없습니다. 따라서 액터 위치를 기반으로 셀에 액터 정보를 배치해줘야 스트리밍 할 때 셀을 로드하면서 액터 또한 불러올 수 있습니다.
 
정말 간단히 요약하자면 아래 내용은 여러 레벨 인스턴스에 담겨있는 액터들을 월드에 셀 단위로 하는 작업입니다. 


5.4부터 RuntimeHash가 UWorldPartitionRuntimeHashSet이 기본으로 변경되어 해당 글에서는 HashSet의 방식을 다루며, 이전 SptialHash의 경우 스트리밍 생성 방식이 아래 내용과 많이 다르고 별도로 다루지 않는 점 참고 부탁드립니다.

 

틀린 내용이 있을 수 있으며, 댓글로 알려주시면 수정하겠습니다 😊

 

1. Generating Streaming Data - 1


1.1. 무엇을?

액터가 셀에 담기기까지 많은 클래스를 거쳐갑니다. 따라서 헷갈리지 않도록, 마지막에 데이터가 어디에 담겨야하는지 알아보겠습니다. 액터 정보(Actor Desc)가 담겨야 할 곳인 셀의 클래스는 UWorldPartitionRuntimeCell입니다.

아래 구조에서 볼 수 있듯이, UWorldPartition이 가진 RuntimHash -> FRuntimePartitionStreamingData이 Cell을 관리한다는 것을 알 수 있습니다. 

그리고 UWorldPartitionRuntimeCell(추상 클래스)를 가보면 다음과 같은 AddActorToCell(...) 함수를 제공하며 해당 함수의 인자를 보면 FStreamingGenerationActorDescView라는 클래스를 받는 것을 알 수 있습니다.

virtual void UWorldPartitionRuntimeCell::AddActorToCell(const FStreamingGenerationActorDescView& ActorDescView)

1편에서 UWorldPartition의 컨테이너 타입이 FWorldPartitionActorDescInstance였다는 것을 기억한다면 액터를 셀에 넣기 위해 아래와 같은 변환 과정이 필요합니다.

요약하자면 다음과 같습니다.

  • 액터는 UWorldPartitionRuntimeCell에 담긴다.
  • 그러기 위해서는 FWorldPartitionActorDescInstance를 FStreamingGenerationActorDescView로 변경해야 한다.

 

1.2. FWorldPartitionStreamingGenerator 살펴보기

스트리밍 생성의 흐름을 쫓기 전에 생성의 중심으로 동작하는 FWorldPartitionStreamingGenerator 클래스와 각종 하위 클래스들의 관계의 전체적인 모습은 다음과 같습니다. 

그림만 봐서 한번에 봐서 이해하긴 어려우니 대강 이런 클래스들이 있구나 정도만 보시고 이어서 차근차근 하나씩 데이터를 채워가면서 PIE에서의 스트리밍 생성의 흐름을 따라가보겠습니다.
 

1.3. Streaming Generation ActorDescView

그림의 왼쪽 붉은 박스에 해당하는 부분입니다. FWorldPartitionActorDescInstance를 FStreamingGenerationActorDescView로 변환해서 FContainerCollectionInstanceDescriptor의 FStreamingGenerationActorDescViewMap을 채웁니다.

1.3.1. UWorldPartition::OnBeginPlay()

FGenerateStreamingParams와 FGenerateStreamingContext를 생성하고 UWorldPartition::GenerateStreaming(...)를 호출합니다.

 

1.3.2. UWorldPartition::GenerateStreaming(...)

UWorldPartition이 가진 컨테이너 컬렉션을 모두 넘겨 Params을 세팅합니다.

UWorldPartition::FGenerateStreamingParams 구조체를 보면 FStreamingGenerationContainerInstanceCollection라는 클래스를 변수로 갖는데 UWorldPartition과 동일하게 TActorDescContainerInstanceCollection<TObjectPtr<const UActorDescContainerInstance>>를 상속 받은 클래스이고, 스트리밍 생성 과정에서 보다 경량화된 구조를 위해 사용되는 컨테이너 컬렉션 클래스입니다.

 

1.3.3. UWorldPartition::GenerateContainerStreaming(...)

Preparation Phase가 FStreamingGenerationActorDescView 생성하는 부분의 메인이라고 할 수 있는데요. 간단하게 요약 하자면 아까 위에서 봤던 ActorDesc의 변환을 하는 곳입니다.

FWorldPartitionStreamingGenerator를 생성하고, PreparationPhase(...)를 호출하는 부분입니다.

 
FWorldPartitionStreamingGenerator::PreparationPhase(...)를 열어보면 호출이 간단해 크게 복잡하지 않아보이는데 CreateActorContainers(...) -> CreateActorDescriptorViewsRecursive(...)를 거치다보면 방향을 잃기 쉽습니다. 그래서 정리 차원에서 그림으로 정리해보자면 아래처럼 PreparationPhase(...)의 내부 함수 호출을 나타낼 수 있습니다.

PreprationPhase 내부 호출 함수들

CreateActorDescriptorViewsRecursive(...) 함수는 다음과 같이 이루어져 있습니다. (소스 코드 주석 발췌) 

1. Inherited parent per-instance data logic
2. Hold on to ID
    2-1. Create container instance descriptor
    2-2. Gather actor descriptor views for this container 
    2-3. Resolve actor descriptor views before validation
    2-4. Validate container, fixing anything illegal, etc.
    2-5. Update container, computing cluster, bounds, etc.
    2-6. Calculate Bounds of non-container actor descriptor views
3. Parse actor containers (SubContainer)
4. Apply per-instance data

이름에서 알 수 있듯이 서브 컨테이너(일반적으로 Level Instance) 가 있으면 재귀로 서브 컨테이너에 대한 CreateActorDescriptorViewsRecursive(...)를 호출합니다. 현재는 메인 컨테이너 1개만 있는 상황을 가정하고 진행하겠습니다.
이 함수에서 눈여겨봐야 할 부분은 CreateActorDescViewMap(..) 입니다. 요약하면 InActorDescCollection에는 월드에 배치된 Actor Desc 목록이 들어있고, 반복자와 저장 안된 액터 처리나 이런저런 조건을 거쳐서 FWorldPartitionActorDescInstance를 FStreamingGenerationActorDescView 생성자에 넣어서 마지막으로 OutActorDescViewMap에 넣습니다.

그럼 이제 위에서 말했던 FWorldPartitionActorDescInstance -> FStreamingGenerationActorDescView로 변경이 되었으니 끝난게 아닌가? 할 수 있습니다. 하지만 실제 AddActorToCell(...) 호출하는 곳을 보면 다음과 같이 GetActorDescView()를 호출하고 해당 함수의 내부는 ActorSet(Instance)의 ActorSetContainer(Instance)의 ActorDescViewMap에서 ActorDesc를 찾습니다.

 

따라서 현재는 FWorldPartitionActorDescInstance가 레퍼런스, Spatially Loaded 여부, Grid, Data Layer, HLOD, Cluster 등 약간의 정보와 함께 FStreamingGenerationActorDescView로 변경되고 FContainerCollectionInstanceDescriptor가 채워졌을 뿐 Cell에 추가되기에는 부족한 정보가 많습니다.

현재까지는 컨테이너와 컨테이너에 속한 액터들의 정보가 보다 깔끔히 정돈된 상태 정도로만 생각하면 좋을 것 같습니다.
 

1.4. Streaming Generation Context

StreamingGenerator.PreparationPhase(...)이 끝나고 가다보면 Base Container의 스트리밍 생성에 대한 내용이 보입니다.

람다인 GenerateRuntimeHash를 보면 드디어 RuntimeHash::GenerateStreaming(...)이 보이지만 그 전에 StreamingGenerationContext가 생성되는 부분을 보겠습니다. (StreamingPolicy도 중요해보이지만 생성 과정에서는  RuntimeCellClass를 얻어서 Cell을 생성할 때만 사용됩니다.)
 

1.4.1. FStreamingGenerationContext 생성

지금까지 컨테이너와 액터에 대한 내용이었다면, 이제는 셀에 담길 데이터로 정리하는 곳이라고 할 수 있습니다.
생성자에서 이전에 구한 ContainerCollectionInstanceDescriptorsMap에서 필요한 데이터만 갖고 FStreamingGenerationContext::FActorSetContainerInstance, FActorSetInstance, WorldBounds를 세팅합니다.

그림으로 보자면 다음 부분에 해당합니다.

 
그럼 FActorSetInstance, FActorSetContainerInstance이 무엇인가 하는 궁금함이 생길텐데요.
단순하게는 액터끼리 Hard Reference가 없는 대부분의 경우 FActorSetInstance는 단일 액터라고 생각해도 무방하고,  FActorSetContainerInstance는 레벨이나 레벨 인스턴스라고 생각하면 됩니다.

가끔 레벨에서 액터끼리 Hard Reference인 경우 하나의 FActorSetInstance에 들어감

2. Generating Streaming Data - 2


2.1. Cell 생성/처리하기

FStreamingGenerationContext의 데이터를 조금 더 가공해서 최종적으로 UWorldPartitionRuntimeHashSet의 스트리밍 데이터(FRuntimePartitionDesc, FRuntimePartitionStreamingData)를 생성하는 내용입니다.

2.1.1. GenerateStreaming(...)

UWorldPartitionRuntimeHashSet::GenerateStreaming(...)을 보면 다음과 같은 과정을 거칩니다.

  • PersistentPartitionDesc 세팅
  • Runtime Partition Streaming Cell Descriptor 생성
  • Streaming Object 생성 및 채우기
  • Runtime Cell 생성
  • Finalize Streaming Object

하나씩 따라가보도록 하겠습니다.
 

2.1.2. PersistentPartitionDesc 세팅

월드에 항상 존재하며 Spatially Loaded가 아닌 액터들이 배치되는 Persistent Partition의 FRuntimePartitionDesc 대해 세팅해줍니다. FRuntimePartitionDesc는 Runtime Partition의 Instance에 대한 세팅을 갖고 있는 구조체입니다.
 

2.1.3. Runtime Partition Streaming Cell Descriptor 생성

GenerateRuntimePartitionsStreamingDescs(...) 함수는 각 ActorSet을 해당하는 Runtime Partition으로 분할하며 그 과정에서 ActorSet이 배치될 Cell의 좌표와 크기 등을 정합니다.
과정은 다음과 같습니다.
 
2.1.3.1. RuntimePartition 이름 수집

기본적으로는 None, Persistent, MainGrid 3개가 수집되며, None과 MainGrid는 동일한 RuntimePartition인데, None인 이름을 추가한 이유는 RuntimeGrid가 None으로 설정된 액터를 default runtime partition에 할당하기 위해서 입니다.
 
2.1.3.2. 고유한 Runtime Partition의 ActorSetInstance 세팅

StreamingGenerationContext의 ActorSetInstance를 순회하며 항상 로드되는 액터면 Main Runtime Partition에 아니라면  ActorSetInstance의 RuntimeGrid에서 RuntimePartition Name을 파싱해 적절한 RuntimePartitionsToActorSetMap에 추가합니다.
 
2.1.3.3.  Runtime Partition Streaming Data 생성하기
이제 RuntimePartition->GenerateStreaming(...)을 호출합니다.

Runtime Partition의 GenerateStreaming을 호출하고 있지만, 사실 URuntimePartition은 추상 클래스이고, 하위의 URuntimePartitionPersistent, URuntimePartitionLHGrid가 메인이라고 볼 수 있습니다. 또한 내부에는 UWorldPartitionRuntimeCell 생성하기까지 사용되는 FCellDesc, FCellDescInstance 구조체 등이 있습니다.

URuntimePartitionPersistent은 Persistent라는 이름의 셀을 추가하는 기능이 전부이며, URuntimePartitionLHGrid (LH는 Loose Hierarchical을 나타냄)에서 본격적인 Cell의 좌표와 크기, Bound 등 파티셔닝을 담당하고 있습니다. (URuntimePartitionLevelStreaming는 LHGrid 추가 전에 사용하던 것으로 추정) 
 
URuntimePartitionLHGrid의 GenerateStreaming(...) 다음과 같습니다. ActorSetInstance 단위로 CellDesc를 생성합니다.

 기존 RuntimeSpatialHash와 가장 큰 차이가 드러난다고 볼 수 있는 부분인데, SpatialHash는 월드를 X,Y로만 나누고 Cell Size의 2배씩 키워서 할당하는 로직으로 인해(FSquare2DGridHelper 참고), 공중에 떠있는 오브젝트가 있다거나 하면 셀의 크기가 의도치 않게 커진다거나, 동일 셀에 놓여서 공중에 있는 오브젝트만 불러올 수 없고 지상에 있는 오브젝트도 불러오게 된다는 등의 한계점이 존재했습니다. 

X,Y로만 분할했던 SpatialHash와 X, Y, Z로 분할하는 LHGrid 기반 HashSet 차이


 
그러나 5.4에서 기본으로 변경된 RuntimeHashSet에서는 X, Y, Z의 3차원 좌표를 지원합니다. 자세한 계산 방식은 FCellCoord 구조체의 GetLevelForBox(...), GetCellCoords(...), GetCellBounds(...) 등을 참고하면 됩니다. 

LHGrid Cell Level, Coord, Bound 계산하는 코드
Level과 Coord 계산 할 때 월드 파티션 세팅의 해당 RuntimePartition CellSize가 들어감

LHGrid에 대한 간략한 설명은 다음과 같습니다. (github commit message에서 가져옴)

  • 계층적 레벨로 기본 셀 크기를 고정하여 마지막 셀까지 크기가 두 배로 증가합니다.
  • 셀 배치 알고리즘
    • 그리드 레벨은 액터의 바운드 범위에 따라 결정됩니다.
    • 셀은 액터의 바운드 중심에 따라 결정됩니다.
  • 모든 액터가 배치되면 셀 크기가 포함된 액터를 나타내도록 조정되므로 조정된 셀은 더 작아질 수 있습니다:
    • 그리드 레벨 셀 크기의 최대 절반만큼 작아집니다.
    • 인접 셀을 그리드 레벨 셀 크기의 최대 절반까지 겹칩니다.

Cell의 이름은 RuntimePartitionName_X_Y_Z_LEVEL 으로 정해집니다.

 
 
 
결과적으로 URuntimePartitionLHGrid::GenerateStreaming(...)에서는 TArray<FCellDesc>의 데이터 (Name, Bounds, Level, ...)을 채우게 되는데 이제 빠져나와서 RuntimePartitionsStreamingDescs를 채워넣었습니다.

 
2.1.3.4. 적당한 Data Layer에 Cell Desc Instance 생성
FCellDesc의 ActorSetInstance를 순회하면서 ActorSetInstance의 Data Layer마다 FCellDescInstance를 생성합니다. 따라서 현재까지는 동일한 좌표, 크기지만 다른 Data Layer의 FCellDescInstance가 존재할 수 있습니다.

이러한 과정을 거쳐 RuntimePartitionsStreamingDescs의 데이터를 채우게 되었습니다.

 

2.1.4. Streaming Object 생성 및 채우기

가장 처음에 이야기 했던 UWorldPartitionRuntimeCell과 FRuntimePartitionStreamingData을 생성하는 부분입니다.

코드는 다음과 같습니다. PopulateCellActorInstances(...)에서 ActorSetInstances 돌면서 각 Actor마다 조건 체크해서
OutCellActorInstances에 넣거나 AlwaysLoadedActorsForPIE로 분류해 단일 액터를 나타내는 FActorInstance를 생성해 TArray<>를 채웁니다.

 
CreateRuntimeCellFromCellDesc 람다에서 Cell의 Unique Id 생성, UWorldPartitionRuntimeHash::CreateRuntimeCell(...)를 호출해 UWorldPartitionRuntimeCell를 생성하고, DataLayer 등 각종 필요한 정보를 채웁니다.

 
UWorldPartitionRuntimeHash::PopulateRuntimeCell(...)에서는 액터 컨테이너를 생성하여 중복된 액터가 Outer를 공유하여 액터 사이의 레퍼런스를 올바르게 리매핑할 수 있도록 하며, 액터를 셀에 추가하고(RuntimeCell->AddActorToCell(ActorDescView)) Content Bounds를 ActorInstances에 맞게 갱신, 항상 로드되지 않는 액터를 가진 셀을 OutPackagesToGenerate에 추가합니다.

액터를 셀에 추가하는 UWorldPartitionRuntimeLevelStreamingCell::AddActorToCell(...)를 보면 다음과 같이 Cell의 Packages에 액터의 정보가 저장됨을 확인할 수 있습니다. UWorldPartitionRuntimeLevelStreamingCell에 대한 부분은 다음 스트리밍 업데이트에서 추가적으로 다루겠습니다.

 
마지막으로 Streaming Data를 생성하고, 항상 로드되는 셀과 아닌 셀을 구분합니다.

 

2.1.5. Finalize Streaming Object

이제 Runtime Cell도 만들었고, 스트리밍에 필요한 모든 정보는 생성된 상태입니다. 그러나 모든 Cell이 하나씩 관리된다면 스트리밍 업데이트를 할 때 성능 상의 문제가 생길 수 있습니다. 따라서 월드 파티션에서는 3차원 셀을 효율적으로 관리하기 위해 R-Tree를 사용합니다.

대강 이런 느낌 출처: https://en.wikipedia.org/wiki/R-tree

 
월드 파티션의 R-Tree은 FNode::FNodeType과 FNode::FLeafType과으로 구성되는데 FStaticSpatialIndex::FRTreeImpl::Init(...)를 보면 각 셀은 Leaf로 Bound를 기준으로 정렬되어서 Node에 추가되게 되고 각 노드마다 MaxNumElementsPerLeaf(=64)을 초과하면 새로운 노드를 생성하는데 노드가 MaxNumElementsPerNode(=16)를 초과하면 새로운 상위 노드를 생성합니다. 대략적인 구조는 아래와 같습니다.

 

실제론 3D, Node(점선)은 NodeType을 나타내고, Cell은 LeafType을 나타냅니다.

현재까지의 내용을 통해 액터가 셀에 Package 형태로 추가되는 것과, 각 셀은 FRuntimePartitionStreamingData에 저장되고, R-Tree에서 크기와 Index가 관리됨을 알 수 있었습니다.

 

3. 그림으로 간단하게 정리하기


월드 파티션 관련 코드를 처음 접하시는 분이라면, 함수의 함수가 계속 되고 익숙치 않은 클래스로 인해 헷갈리는 내용이 많을 수 있고, 설명이 부족한 내용도 있습니다. 그래서 그림으로 최대한 간단하게 현재까지 다룬 내용 중 중요 클래스 위주로 요약하면 아래와 같이 나타낼 수 있습니다.

 

그럼 이어지는 3편에서 스트리밍 업데이트는 어떻게 이뤄지는지 알아보겠습니다.
 

Etc.


Log로 결과 확인하기

  • 경로: ...\Saved\Logs\WorldPartition\StreamingGeneration-###.log

1, 2편에 이어서 클래스와 생성 흐름을 기반으로 이야기했기에, 현재 작업 중인 맵에서 Container, ActorDesc, Cluster에 어떤게 담기고 Persistent Level에 배치되는 액터들은 무엇인지, 각 셀에 담기는 액터는 무엇인지 궁금하실 수 있고 매번 디버깅을 하면서 따라가거나 하긴 쉽지 않습니다. 따라서 언리얼에서 생성 결과에 대한 로그를 제공합니다. 중간에 생략한 내용인데 DumpStateLog(FHierarchicalLogArchive& Ar) 라는 함수를 컨테이너나 생성에 관여하는 클래스 이곳저곳에서 볼 수 있습니다.

크게 보면 1편에서 다뤘던 Actor와 컨테이너에 관한 부분과 2편에서 다뤘던 내용으로 구분해서 결과를 확인할 수 있습니다.
 

참고

  • UE 5.4 Source Code
  •  「World Partition 2024」 - https://www.youtube.com/live/nzKB5sCBCnk

'Column' 카테고리의 다른 글

[UE] World Partition Deep Dive 3 - Streaming Update  (0) 2025.02.09
[UE] World Partition Deep Dive 1 - Actor Management  (0) 2024.08.29
'Column' 카테고리의 다른 글
  • [UE] World Partition Deep Dive 3 - Streaming Update
  • [UE] World Partition Deep Dive 1 - Actor Management
winterseaotter31
winterseaotter31
winterseaotter31 님의 블로그 입니다.
  • winterseaotter31
    게임 개발 랭커가 될 거야!
    winterseaotter31
  • 전체
    오늘
    어제
    • 분류 전체보기 (11)
      • Column (3)
      • 정리 및 번역 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 게임 개발 랭커가 될 거야!
  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
winterseaotter31
[UE] World Partition Deep Dive 2 - Streaming Generation
상단으로

티스토리툴바