본문 바로가기
UE4 & UE5/Rendering

[UE5] Nanite (1/5)

by scahp 2024. 3. 14.

[UE5] Nanite (1/5)

최초 작성 : 2024-03-14
마지막 수정 : 2024-03-14
최재호

 

목차

1. 환경
2. 목표
3. 내용
   3.1. Nanite 에서 주요 기술 둘러보기
     3.1.1. MeshShader 와 같은 Cluster 기반 렌더링
     3.1.2. TwoPassOcclusionCulling
     3.1.3. Visibility Buffer Rendering
  3.2. Nanite MeshDrawCommandCaching
  3.3. Nanite PrimitiveSceneProxy 의 생성
  3.4. 새로 생성한 Nanite PrimitiveSceneProxy 를 UpdateAllPrimitiveSceneInfos 으로 FScene 에 등록
  3.5. GPU Scene MaterialSlot 데이터 업로드
  3.6. Visibility Buffer 초기화

4. 레퍼런스

 

1. 환경

Unreal Engine 5.3.2 (release branch 072300df18a94f18077ca20a14224b5d99fee872)

개인적으로 분석한 내용이라 틀린 점이 있을 수 있습니다. 그런 부분은 알려주시면 감사하겠습니다.

이번 글은 한 번에 많은 길이의 코드를 분석하는 부분이 종종 등장합니다. 그래서 글을 2개 띄우고 한쪽은 설명 부분을 한쪽은 코드 이미지를 최대화해서 보는 것을 추천합니다.

 

2. 목표

Nanite 에 들어간 핵심 기술들을 파악하고 BasePass 렌더링 과정을 코드레벨로 이해해 봅시다.

 

Nanite 는 시네마틱 퀄리티 장면을 구현하기 위해서 아주 작은 크기의 삼각형으로 이뤄진 복잡한 지오메트리를 빠르게 렌더링 합니다.

 

Nanite 코드는 기존 렌더링 파이프라인에 비해서 상당히 읽기가 어렵습니다. 그 이유는 대부분의 렌더링 패스가 Compute Shader 를 기반으로 하기 때문입니다. 그래서 Nanite 를 이해하기 전에 Shader 가 코드를 실행하는 방식에 대해서 이해하는 것이 좋습니다. 특히 Shader 는 동일한 코드를 Wavefront 단위로 실행한다는 점을 잘 이해해야 합니다. 아래의 리스트는 Nanite 코드 이해를 위해 알면 좋은 내용들입니다.

- Shader code 의 동작 방식은 레퍼런스5, 6 를 참고하시면 좋습니다.

- Compute Shader 는 GPU 에 대한 이해는 레퍼런스7, 8, 9 를 참고해 주세요.

- Visibility Buffer Renderer 는 레퍼런스10 을 참고해 주세요.

- UE5 렌더링의 기본 단위인 MeshDrawCommand 에 대한 이해도 있으면 좋습니다. 해당 내용은 레퍼런스11, 12 를 참고해 주세요.

 

Nanite 사용을 위해서 에셋으로부터 Nanite 데이터를 생성하는 과정도 있지만 (메시로부터 클러스터를 생성하고 클러스터 계층구조를 만드는 작업), 이 시리즈에서는 Nanite 를 렌더링 하기 위해 필요한 데이터는 모두 준비된 상태라 가정하고 Nanite BasePass 렌더링 과정에만 집중할 예정입니다.

 

이 글은 총 5개로 구성될 예정입니다.

1. Nanite 1/5 : Nanite 에서 사용하는 주요 기술과 MeshDrawCommand 생성 및 VisibilityBuffer 초기화 과정 리뷰

2. Nanite 2/5 :  TwoPassOcclusionCulling 의 전체 레이아웃을 확인하고 MainPass 의 노드 및 클러스터 컬링 리뷰

3. Nanite 3/5 :  SW, HW 레스터라이저와 PostPass 리뷰

4. Nanite 4/5 :  Visibility Buffer 로 부터 Depth/Stencil 텍스쳐를 생성하는 부분 리뷰

5. Nanite 5/5 :  Visibility Buffer 로 부터 G-Buffer 생성하는 MaterialPass 리뷰

 

3. 내용

3.1. Nanite 에서 주요 기술 둘러보기

3.1.1. MeshShader 와 같은 Cluster 기반 렌더링

UE5 의 Nanite 지원 에셋의 경우 에디터에서 미리 메시의 삼각형을 클러스터화 시켜서 에셋에 저장해 둡니다. 그리고 실제 렌더링 시에는 클러스터화 한 데이터를 필요한 만큼 스트리밍 로드 하여 사용합니다(마치 버추어 텍스쳐처럼). 물론 컬링 또한 메시가 아닌 클러스터 단위로 결정됩니다.

 

3.1.2. TwoPassOcclusionCulling

  • “Patch-Based Occlusion Culling for Hardware Tessellation” 논문의 아이디어를 기반으로 한 내용입니다.
    • 이전 프레임과 현재 프레임의 변화에 큰 차이가 없다는 것을 기반으로 구현된 아이디어입니다.
  • 처리 순서는 아래와 같습니다.
    • MainPass
      • InstanceCulling : 이전 프레임의 HZB 를 기반으로 전체 인스턴스들을 빠르게 컬링 합니다.
      • Persistent Hierarchy Cluster Culling : 이전 단계에서 살아남은 인스턴스를 클러스터 단위로 컬링 테스트를 하고 살아남은 것들을 추려 레스터라이즈를 준비합니다.
      • 전 과정에서 살아남은 클러스터에 대해서 SW/HW Rasterizer 를 수행합니다. 결과는 Visibility Buffer 에 기록됩니다.
    • MainPass 의 결과로 부터 현재 프레임의 HZB 를 생성합니다.
    • PostPass (저번 프레임에 안보였다가 이번 프레임에 보이는 인스턴스를 위한 패스)
      • MainPass 의 InstanceCulling, Persistent Hierarchy Cluster Culling 단계에서 컬링 되었던 인스턴스와 클러스터에 대해서 현재 프레임을 기준으로 생성된 HZB 에 대해 컬링 테스트를 진행합니다.
        • InstanceCulling, Persistent Hierarchy Cluster Culling 수행
      • 전 과정에서 살아남은 클러스터에 대해서 SW/HW Rasterizer 를 수행합니다. 결과는 VisibilityBuffer 에 기록됩니다.
    • 이제 최종 HZB 를 빌드하고, 이 후과정에서 사용합니다.
    • Material Passes 에서는 VisibilityBuffer 의 데이터를 기반으로 GBuffer 를 생성합니다. 이 부분이 Visibility Buffer Rendering 에 해당합니다.

그림1. TwoPassOcclusionCulling (출처 : 레퍼런스2)

 

3.1.3. Visibility Buffer Rendering
- 더 사실적인 장면을 렌더링 하기 위해서는 더 작은 삼각형이 필요할 수 있습니다. 극단적으로는 삼각형이 1개 픽셀 크기 수준으로 작아질 수도 있을 겁니다. PixelShader 에서는 1 개의 픽셀을 쉐이딩 할 때는 MipMap 결정 및 DDX, DDY 같은 연산을 위해서 실제로 4개의 픽셀을 쉐이딩 합니다. 삼각형의 크기가 충분히 크다면 쉐이딩 한 4개의 픽셀을 인접 픽셀에서 공유하여 쓸 수 있을 것입니다. 만약 삼각형당 1개의 픽셀을 사용하게 된다면 1 Activie + 3 Helper lane 으로 구성되게 될 것입니다. 이 경우 Helper lane 이 공유되지 못하게 되므로 픽셀당 최대 4 배의 연산이 필요하게 됩니다. Visibility Buffer Renderer 는 이런 부분을 해결해 줍니다.

 

3.2. Nanite MeshDrawCommandCaching

Raster/ShadingBin 에 대한 코드는 레퍼런스13 에 좋은 설명이 있어서 참고했습니다. 해당 자료를 확인해 보는 것도 좋을 것 같습니다. Raster/ShadingBin 은 CPU와 GPU 에서 공유하는 MaterialID 로 볼 수 있습니다. RasterBin 은 VisibilityBuffer 생성 단계에서 Rasterize 단계, ShadingBin 은 VisibilityBuffer 에서 GBuffer 를 생성해 내는 MaterialPass 에서 사용하게 됩니다.

 

3.3. Nanite PrimitiveSceneProxy 의 생성

먼저 Nanite 용 메시가 UPrimitiveComponent 에서 Rendering Thread 에 있는 FScene 에 등록되는 과정을 확인해 봅시다.
1. BatchAddPrimitives 로 부터 UPrimitiveComponent 들을 넘겨받습니다.
2. 기존과 동일하게 UPrimitiveComponent 의 CreateSceneProxy 를 사용하여 FPrimitiveSceneProxy 를 생성합니다.

그림2. UPrimitiveComponent 로 부터 SceneProxy 를 생성하여 FScene 에 등록하려는 코드 (출처 : 레퍼런스1)

 

문제의 범위를 좁히기 위해서 UStaticMeshComponent 라고 가정하고 코드를 계속해서 보겠습니다.
1. FMaterialAudit 이라는 클래스는 ShouldCreateNaniteProxy 함수가 호출될 때 넘겨지는 데, 만약 이 UStaticMeshComponent 가 Nanite 를 사용하면 렌더링에 필요한 Material 정보를 FMaterialAudit 에 담아줍니다.
1.1. ShouldCreateNaniteProxy 내부로 들어왔습니다.
1.2. AuditMaterials 함수를 호출하여 Material 정보를 모읍니다.
2. AuditMaterials 내부로 들어갑니다.
3. UStaticMeshComponent 가 가지고 있는 모든 머터리얼을 차례로 순회합니다.
4. FMaterialAuditEntry 라는 객체에 해당 머터리얼의 정보를 담아줍니다.
4.1. AuditMaterials 를 호출하여 얻어온 Nanite에 사용할 Material 정보를 OutNaniteMaterials 에 넘겨줘서 함수 외부에서 사용할 수 있게 합니다.
5. 이제 얻어온 NaniteMaterials 정보를 사용하여 CreateStaticMaterialSceneProxy 를 호출하여 Nanite 용 SceneProxy 를 생성합니다.

그림3. CreateSceneProxy 중 FMaterialAudit 을 채우는 코드 (출처 : 레퍼런스1)

 

 

1. 계속해서 CreateStaticMeshSceneProxy 내부로 들어가면 Nanite::FSceneProxy 를 생성할 수 있는지 확인하고 생성합니다. 현재는 Nanite 에 대해서 알아보는 중이기 때문에 Nanite::FSceneProxy 생성자 내부로 들어가 봅시다.
2. UStaticMeshComponent 로 부터 FResources 라는 데이터를 얻어옵니다. 아마 에디터에 생성한 Nanite 에 필요한 데이터들 같습니다. 클러스터 스트리밍에 필요한 데이터나 클러스터의 Hierarchy 정보가 담긴 FPackedHierarchyNode 등이 있는 것을 볼 수 있습니다.
3. Mesh 의 각 섹션을 순회합니다. 그리고 반복문 아래에 있는 FMaterialSections 배열에 메시 섹션에 대한 머터리얼에 대한 정보를 모읍니다.
4. 머터리얼에서 Hidden 으로 체크되어있지 않다면, ShadingMaterial 을 얻기 위해서 준비합니다. Nanite 에는 Rasterize, ShadingMaterial 로 머터리얼 타입이 나뉩니다. Rasterize 는 삼각형을 Rasterize 하여 Visibility Buffer 를 구성하기 위해 사용됩니다. Shading 은 Visibility Buffer Rendering 단계에서 GBuffer 를 만들 때 사용하는 머터리얼 입니다. 이렇게 두 종류로 나뉘는 이유는 Rasterize 중에는 픽셀의 Shading 에 대한 연산을 할 필요가 없기 때문입니다. 그래서 특별한 경우가 아니라면 Default Material 을 Rasterize 머터리얼로 사용하게 됩니다.
5. 이전 단계에서 얻어온 MaterialAudit 에서 머터리얼 인덱스를 통해 현재 섹션에서 사용하려고 하는 머터리얼을 가져옵니다. 이 머터리얼을 ShadingMaterial 에 할당합니다.
6. 소유한 머터리얼이 없는 경우, 메시 섹션이 Hidden 이면 NaniteHiddenSectionMaterial 을 설정하고 그렇지 않은 경우 Default 머터리얼을 설정합니다.
7. 마지막으로 OnMaterialsUpdated 함수를 호출합니다.
8. 위의 3에 있는 반복을 통해 채워진 MaterialSections 배열을 순회하며 MaterialSection 의 필요한 것들을 갱신합니다. 어떤 것을 갱신하는지 계속해서 봅시다.
9. ProgrammableRaster 인 경우인지 확인합니다. 이 경우는 World Position Offset, PixelDepthOffset, Masked, Displacedment 기능을 사용하는 경우로 Default 머터리얼로 Rasterize 를 수행할 수 없는 상태입니다.
10. RasterMaterial 를 등록합니다. Hidden 머터리얼의 경우 ShadingMaterial 와 동일하게 NaniteHiddenSectionMaterial 을 등록하고, ProgrammableRaster 의 경우 ShadingMaterial 을 등록하고 그렇지 않으면 Default 머터리얼을 등록합니다.

그림4. CreateStaticSceneProxy 중 FMaterialAudit 을 사용하여 Shading, Raster Material 을 등록하는 코드 (출처 : 레퍼런스1)

 

 

3.4. 새로 생성한 Nanite PrimitiveSceneProxy 를 UpdateAllPrimitiveSceneInfos 으로 FScene 에 등록

FPrimitiveSceneProxy 가 생성된 다음에는 렌더스레드에서 UpdateAllPrimitiveSceneInfos 를 호출하여 등록 대기 중인 FPrimitiveSceneProxy 를 일괄 등록해 줍니다.
1. UpdateAllPrimitveSceneInfos 함수에 진입합니다.
2. 이번에 추가되거나 갱신된 PrimitiveSceneInfo 의 수를 계산합니다.
3. 2번 과정에서 얻는 수를 사용하여 SceneInfoWidthAddToScene 컨테이너의 메모리를 예약합니다.
4. StaticMesh 의 경우 CacheNaniteDrawCommand 함수를 호출하여 Nanite 의 MeshDrawCommand 를 캐싱합니다. 스태틱 메시인 것으로 가정하고 코드를 계속 확인해 봅시다.
5. CacheNaniteDrawCommands 함수로 진입합니다.
6. 각각의 FPrimitiveSceneInfo 에 대해서 BuildNaniteDrawCommands 를 호출합니다.
7. BuildNaniteDrawCommands 내부로 진입합니다.
8. MeshDrawCommand 를 만들기 위해서 NaniteMeshProcessor 를 생성합니다.
8.1. NaniteMeshProcessor 를 만들어주는 CreateNaniteMeshProcessor 함수 내로 진입합니다.
8.2. DepthStencilState 설정이 조금 특이한데, Depth Equal 상태로 설정합니다. 이 부분은 추후에 보겠지만 동일한 MaterialID를 가진 픽셀만 렌더링 하기 위해서 추가된 부분입니다.
8.3. 최종적으로 FNaniteMeshProcessor 를 생성합니다.
9. PassBody 라는 람다함수를 호출합니다.
10. PassBody 내부에서는 PrimitiveSceneInfo 가 소유한 StaticMeshes 배열을 순회합니다. StaticMeshes 배열은 FStaticMeshBatch 정보가 담겨 있습니다.
11. Nanite 를 지원하는 경우라면 NaniteMeshProcessor 에 FStaticMeshBatch 를 전달하여 MeshDrawCommand 를 생성하고 캐싱합니다.
11.1. AddMeshBatch 내부로 진입하고 계속해서 TryAddMeshBatch 로 진입합니다.
11.2. FNaniteIndirectMaterialVS 를 버택스 쉐이더로 사용합니다. 이 쉐이더는 FullscreenQuad 를 위한 VS 입니다.
11.3. 그리고 사용할 BasePassPixelShader 를 얻어옵니다.
11.4. 앞에서 얻은 버택스, 픽셀 쉐이더를 바인딩합니다. 여기서 버택스 쉐이더만 Nanite 용 FullscreenQuad 로 사용하는데 레스터라이즈 방식만 바뀌고 픽셀을 쉐이딩 하는 방식은 유사하지 않을까? 하고 추측할 수 있습니다.
11.5. 이제 최종적으로 BuildMeshDrawCommands 를 호출하여 MeshDrawCommand 를 생성합니다. 생성 및 캐싱 과정은 레퍼런스11 에도 나와있기 때문에 생략합니다.
12. MeshDrawCommand 생성을 마치고 나서 Nanite::SceneProxy 에 생성해 둔 MaterialSections 을 얻어옵니다.
13. 그리고 DrawListContext.DeferredPipelines[MeshPass] 에 FDeferredPipelines 를 생성하여 정보를 채워줍니다. FDeferredPipelines 에는 FNaniteRasterPipeline, FNaniteShadingPipeline 에 대한 정보들이 담깁니다.
14. Nanite::SceneProxy 가 가진 MaterialSections 정보를 순회합니다.
15. 여기서는 우선 FNaniteRasterPipeline 정보만 설정합니다. NaniteMaterialSections 로부터 RasterMaterial 과 기타 Rasterize 에 필요한 머터리얼 정보들을 설정합니다.
16. WPO 가 거리에 따라 꺼지는 경우 해당 메시는 WPO 의 활성 여부에 따라 서로 다른 머터리얼을 Rasterize 에 사용해야 할 것입니다. 그래서 WPODisableDistance 를 평가한 뒤 Secondary FNaniteRasterPipline 을 생성하는 데 사용합니다.
17. FNaniteDrawListContexts 에 Apply 함수를 호출하여 FScene 에 생성한 Nanite 머터리얼을 FNaniteRasterPipelines, FNaniteShadingPipelines 형태로 등록해 줍니다. 함수의 내부는 그림6 에서 추가로 더 알아봅시다.
18. bAllowComputeMaterials 가 true 면 BuildShadingCommands 함수를 호출하지만 기본 설정으로는 bAllowComputeMaterials 가 false 입니다. 그래서 bAllowComputeMaterials 관련 코드는 더 이상 추적하지 않을 것입니다.

그림5. UpdateAllPrimitiveSceneInfos 과정을 통해 NaniteMeshDrawCommand 가 캐싱되는 과정 (출처 : 레퍼런스1)

 

 

1. FNaniteDrawListContext 의 Apply 함수 내부입니다. 모든 ENaniteMeshPass 에 대해서 순회합니다. ENaniteMeshPass 는 BasePass 와 LumenCardCapture 두 가지입니다. 이제 MeshDrawCommand 를 만들면서 준비한 머터리얼 파이프라인 데이터들을 FScene 에 등록합니다. FNaniteMaterialCommands 는 머터리얼 슬롯 정보, FNaniteRasterPipelines 은 Rasterize 관련 머터리얼 정보, FNaniteShadingPipelines 는 Shading 관련 머터리얼 정보를 담고 있습니다.
2. DeferredCommands 멤버로부터 는 그림5 의 15 에서 우리가 생성해 둔 FNaniteRasterPipeline 정보를 얻어옵니다.
3. FNaniteMaterialCommands 타입인 ShadingCommands.Register 함수를 통해 FNaniteCommandInfo 를 생성합니다. Register 의 내부 코드는 그림7 에서 계속 추적합니다.
4. AddShadingCommand 함수를 호출하여 PrimitiveSceneInfo 에 방금 새로 만든 FNaniteCommandInfo 를 등록합니다.
5. 이번에는 DeferredPipelines 를 순회합니다.
6. RasterizePipelines 을 순회합니다. 
7. FScene 의 RasterPipelines.Register 함수를 호출하여 RasterPipeline 을 등록합니다. 리턴값으로 FNaniteRasterBin 을 돌려받는데 Register 함수 내부에서는 CPU 에서 RasterPipeline 을 얻을 수 있는 키인 BinId 와 GPU 에서 얻을 수 있는 키인 BinIndex 를 갖고 있습니다. Register 의 내부 코드는 그림8 에서 계속 추적합니다.
8. WPO 가 꺼져 있는 경우 RasterPipelines.Register 함수로 SecondaryRaster 를 추가 등록해 줍니다.
9. AddRasterBin 함수를 사용하여 PrimaryRasterBin 과 SecondaryRasterBin 을 PrimitiveSceneInfo 에 등록해 줍니다. AddRasterBin 함수 내부는 그림9에서 계속해서 추적합니다.
10. RasterBins 는 기본 설정에서는 nullptr 이기 때문에 여기서는 무시합니다.
11. RefreshNaniteRasterBins() 는 CustomDepth 렌더링에 대한 추가 작업을 수행합니다. 코드의 범위를 최소화하기 위해서 여기서는 CustomDepth 를 사용하지 않는다고 가정하고 넘어가겠습니다.

그림6. Nanite MeshDrawCommand 를 준비한 뒤, FScene 에 Nanite Material 정보를 등록하는 코드 (출처 : 레퍼런스1)

 

아래 내용은 ShadingCommands.Register 함수 내부입니다.

여기서는 MaterialSlot 의 인덱스를 할당 받습니다. 그리고 그림7 의 AddShadingCommand 함수에서 MaterialSlot 의 인덱스를 LegacyShadingId 에 등록합니다. MaterialSlot 에는 Raster, Shading Bin Index (GPU 에서의 인덱스) 또한 포함되어 있습니다. GPU 측에서는 MaterialSlot 을 통해 현재 처리 중인 프리미티브의 머터리얼 정보를 얻을 때 사용합니다.

3.1. 로빈후드해시맵에 FMeshDrawCommand 해시를 기반으로 FNaniteMaterialEntry 를 등록합니다. 등록 후 돌려받은 해시키를 CommandInfo 저장합니다. 이 키는 CPU 에서 FNaniteMaterialEntry 를 찾을 때 사용됩니다.
3.2. FNaniteMaterialEntry 가 처음 등록된 경우 Entry 에 필요한 정보를 설정합니다. MaterialSlotAllocator 로 부터 MaterialSlot 을 할당받는 부분이 가장 중요합니다. 이 부분은 GPU 에서 Material 을 찾을 때 사용되는 Index 입니다. 이후에는 ShadingBin 이라는 이름으로 불립니다.
3.3. CommandInfo 에 Entry 에 있는 MaterialSlot 을 설정해 줍니다.
3.4. MaterialEntry 의 레퍼런스 카운트를 증가시켜 줍니다.

추가그림1. NaniteMaterialCommands::Register 로 MaterialSlot 할당 (출처 : 레퍼런스1)

 

 

AddShadingCommand 함수는 PrimitiveSceneInfo 에 Nanite 관련 정보를 기록하는데 사용됩니다. 그림6의 3번 과정에서 생성한 FNaniteCommandInfo 를 PrimitiveSceneInfo 에 등록하고, PrimitiveSceneInfo.NaniteMaterialSlots 에 MaterialSlot 또한 기록합니다. MaterialSlot 은 그림7의 3.2 과정에서 생성한 GPU 에서 사용할 Material Index 입니다.

그림7. FNaniteMaterialCommands::Register 함수 내부 (출처 : 레퍼런스1)

 

아래 내용은 RasterPipelines.Register 함수의 내용입니다.

7.1. Register 함수에 진입합니다.
7.2. FNaniteRasterPipeline 을 사용하여 해시를 만들어서 로빈후드해시맵에 FNaniteRasterEntry 를 생성합니다. 그리고 CPU 에서 로빈후드해시맵으로부터 FNaniteRasterEntry 을 얻어올 수 있는 키를 BinId 에 설정해 줍니다.
7.3. FNaniteRasterEntry 가 처음 생성되었다면 AllocateBin 을 사용하여 GPU 에서 FNaniteRasterEntry 를 얻을 수 있는 BinIndex 를 할당받습니다. AllocateBin 에 연결된 화살표를 따라가보면, BinIndex 를 할당받을 때, bPerPixelEval 이 true 면 인덱스가 거꾸로 자라나고, false 면 0에서부터 자라나는 것을 볼 수 있습니다. 
7.4. FNaniteRasterEntry 의 레퍼런스 카운트를 증가시킵니다.

그림8. FNaniteRasterPipelines::Register 함수 내부 (출처 : 레퍼런스1)

 

 

아래는 그림8에서 생성한 FNaniteRasterBin 정보를 PrimitiveSceneInfo 에 등록하는 과정입니다.

9.1. Primary, Secondary RasterBin 을 PrimitiveSceneInfo 의 NaniteRasterBins[MeshPass] 컨테이너에 등록해 줍니다.
9.2. PrimitiveSceneInfo.NaniteMaterialSlots[MeshPass] 에 FNaniteMaterialSlot 을 추가해 줍니다. FNaniteMaterialSlot 의 RasterBin, SecondaryRasterBin 에는 GPU 에서 RasterMaterial 을 찾을 수 있는 키를 BinIndex 를 등록합니다.

그림9. FNaniteDrawListContext::AddRasterBin 함수 내부 (출처 : 레퍼런스1)

 

3.5. GPU Scene MaterialSlot 데이터 업로드

위에서 준비한 FNaniteMaterialSlot 은 GPUScene 의 Update 시점에 GPU 로 올려줍니다.
1. FGPUScene::UpdateInternal 함수로 UploadGeneral<FUploadDataSourceAdapterScenePrimitives> 가 호출됩니다.
2. UploadGeneral 은 FUploadDataSourceAdapterScenePrimitives 를 사용하고 있기 때문에 bUpdateNaniteMaterialTables 가 항상 true 입니다. 조건문 내부로 진입합니다.
3. 모든 Nanite 패스에 대해서 순회하면서 FScene 에 있는 FNaniteMaterialCommands NaniteMaterials[ENaniteMeshPass::Num]; 이 MaterialSlot 데이터를 업로드할 수 있게 Begin 함수를 호출합니다.
3.1. FNaniteMaterialCommands 의 Begin 함수 내부로 진입합니다.
3.2. MaterialSlotDataBuffer 에 NumPrimitiveUpdates * MaxMaterials * MaterialSlotSize 만큼의 데이터를 업로드하기 위해서 준비합니다. MaxMaterials 로 보아 Primitive 당 최대 머터리얼이 정해진 것을 알 수 있습니다.
4. NumPrimitiveDataUploads 수만큼 순회합니다.
5. MaterialSlot 을 업로드할 Dest 위치를 가진 NaniteMaterialUploader 와 MaterialSlot 의 원본 정보를 가진 PrmitiveSceneInfo->NaniteMaterialSlots 를 준비합니다.
6. MaterialSlot 을 복사합니다.
7. FNaniteMaterialCommands 의 Finish 함수를 호출하여 업로드를 마칩니다.
8. 마지막으로 업로드한 MaterialSlot 과 Shader 에서 MaterialSlot 로드하는 부분을 확인해보려고 합니다. 그전에 MaterialSlot 을 복사하기 위해서 호출한 NaniteMaterialUploader→GetMaterialSlotPtr  내부에 대해 봅시다. (초록색 선을 따라 이동) GetMaterialSlotPtr 함수를 보면 PrimitiveIndex * MaxMaterials 를 하여 해당 위치에 데이터를 저장할 수 있게 하는 것으로 보입니다. 여기서 MaxMaterials 는 64 입니다. 코드를 조금 더 따라가보면 NANITE_MAX_MATERIALS 로 정의되어있고, Nanite 메시는 최대 64 개의 머터리얼을 가질 수 있다는 것을 알 수 있습니다.

그림10. FScene 을 통해서 MaterialSlot 을 GPUScene 에 업로드하는 과정 (출처 : 레퍼런스1)

 

 

이번에는 MaterialSlot 을 로드하는 Shader 를 확인해 봅시다.
1. LoadMaterialSlot 이 호출되면 업로드 시와 마찬가지로 PrimitiveIndex * MaxMaterials 를 하여 GlobalMaterialIndex 를 얻어오는 것을 볼 수 있습니다. 그리고 이것을 기반으로 ByteAddressBuffer 로 부터 FNaniteMaterialSlot 을 로드합니다.

2. RemapMaterialIndexToOffset 함수는 PrimitiveIndex, MaterialIndex 를 사용하여 MaterialSlot 을 로드할 Offset 을 얻어냅니다. BytesPerMaterials 는 총 8 byte 인데, MaterialSlot 을 Packing 한 사이즈입니다.

3. MaxMaterials 는 그림10 의 8 에서 최대 머터리얼 수 64 개인 것을 확인할 수 있었습니다. 그리고 CPU 측에서 업로드 시에 사용한 것과 같이 PrimitiveIndex * MaxMaterial 위치를 얻어오고 MaterialIndex 를 더하여 최종 GlobalMaterialIndex 를 만들어냅니다.

그림11. Shader 에서 MaterialSlot 데이터를 로드하는 함수 (출처 : 레퍼런스1)

 

3.6. Visibility Buffer 초기화

이제 본격적으로 Nanite 를 보기 위한 준비를 마쳤습니다. 아래 코드는 FDeferredShadingSceneRenderer 의 Render 함수 내부입니다. 먼저 전체 Nanite 코드의 레이아웃을 확인합니다. 각각의 함수들은 코드 덩치가 아주 크기 때문에 이후 세부사항도 차근차근 볼 예정입니다.
1. Nanite 의 Rasterize 결과인 VisibilityBuffer 는 NaniteRasterResults 에 담깁니다. 그리고 PrimaryNaniteViews 는 Nanite 렌더링에 사용될 View 정보가 담깁니다. 현재는 BasePass 에 대해서 알아보는 중이기 때문에 View 의 개수는 1개로 고정이라고 생각하고 코드를 계속 보겠습니다.
2. InitRasterContext 함수에서는 Nanite 의 Rasterize 패스를 진행하기 위해서 필요한 초기화를 수행합니다. 함수 내부 내용은  그림13 에서 다시 봅시다.
3. CreateNaniteViews 람다 함수는 Nanite 에 사용할 View 를 만들어줍니다.
4. 모든 View 에 대해서 NaniteView 를 만들어줍니다. 우리는 1개의 View 라고 생각하고 계속 코드를 보겠습니다.
5. CreateNaniteViews 람다 함수를 사용하여 NaniteView 를 실제로 만듭니다.
6. Nanite::IRenderer::Create 는 FRenderer 를 생성해 줍니다. 이 클래스가 Culling 에서 Rasterize 까지 필요한 모든 자료를 갖고 있습니다.
7.  DrawGemoetry 함수에서 Two-Pass Occlusion Culling 과 SW, HW Rasterize 를 수행합니다. SceneInstanceCullQuery 는 기본적으로 nullptr 이기 때문에 무시합니다.
8. Rasterize 결과를 FRasterResults 구조체로 옮겨줍니다.
9. Visibility Buffer 로부터 Depth, Stencil 과 같은 정보들을 추출합니다.

그림12. Nanite 코드 중 Culling 부터 Visibility Buffer 생성 과정까지 전체 코드 레이아웃 (출처 : 레퍼런스12)

 

InitRasterContext 내부 구현을 확인해 봅시다.
1. 코드 읽는 복잡도를 줄이기 위해서 RasterScheduling 방식은 HardwareThenSoftware 로 가정하고 봅니다.
2. 64 bit uint 를 지원여부에 따라 VisibilityBuffer 의 포맷을 설정합니다.
3. DepthBuffer 와 VisibilityBuffer 를 생성합니다.
4. VisibilityBuffer 의 UAV 를 생성합니다.
5. AddClearVisBufferPass 함수를 호출해 VisibilityBuffer 의 내용을 초기화합니다.
6. AddClearVisBufferPass 함수의 내부에 진입합니다.
7. 기본 설정의 RasterMode 는 DepthOnly 가 아닙니다. #define RASTER_CLEAR_DEPTH 0
8. 기본 설정의 bTiled 는 false 입니다. #define RASTER_CLEAR_TILED 0
9. CPU 측에서 RasterClear Shader 를 실행합니다. 그리고 GPU 측에서 VisibilityBuffer 를 0으로 초기화합니다. (초록색 화살표 참고)

그림13. Visibility Buffer 초기화 함수 (출처 : 레퍼런스1)

 

 

다음글 [UE5] Nanite (2/5)

 

 

4. 레퍼런스

1. https://github.com/EpicGames/UnrealEngine/commit/072300df18a94f18077ca20a14224b5d99fee872

2. https://www.youtube.com/watch?v=eviSykqSUUw (Slide link)

3. https://docs.unrealengine.com/5.0/en-US/nanite-virtualized-geometry-in-unreal-engine/

4. https://advances.realtimerendering.com/s2015/aaltonenhaar_siggraph2015_combined_final_footer_220dpi.pdf

5. https://scahp.tistory.com/41

6. https://scahp.tistory.com/42

7. https://scahp.tistory.com/97

8. https://scahp.tistory.com/98

9. https://scahp.tistory.com/99

10. https://scahp.tistory.com/81

11. https://scahp.tistory.com/74

12. https://scahp.tistory.com/75

13. https://zhuanlan.zhihu.com/p/603861270

 

 

반응형

'UE4 & UE5 > Rendering' 카테고리의 다른 글

[UE5] Nanite (3/5)  (1) 2024.03.22
[UE5] Nanite (2/5)  (0) 2024.03.21
[UE5] D3D12 ResourceAllocation 리뷰(2/2)  (0) 2023.08.24
[UE5] D3D12 ResourceAllocation 리뷰 (1/2)  (0) 2023.08.23
[UE5] Auto Exposure (2/2) - Histogram  (0) 2022.12.15