ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] GPUScene 과 Auto-Instancing (1/2)
    UE4 & UE5/Rendering 2022. 2. 9. 21:21

    [UE5] GPUScene 과 Auto-Instancing

     

    최초 작성 : 2022-02-09
    마지막 수정 : 2022-02-09
    최재호

     

    목차

    1. 환경
    2. 목표
    3. 내용

     3.1. Primitive 전용 데이터인 PrimitiveSceneData의 구성과 쉐이더에 전달
      3.1.1. PrimitiveData 정의
      3.1.2. 실제 쉐이더에서 PrimitiveData 사용
      3.1.3. 실제 쉐이더에서 PrimitiveData 사용을 위해 캐싱하는 과정
      3.1.4. C++ PrimitiveData에서 UniformBuffer의 선언과 데이터 바인딩
     3.2. 한 개의 버퍼에 PrimitiveSceneData를 모두 모아 사용하는 GPUScene
      3.2.1. 실제 쉐이더에서 GPUScene에서 PrimitiveData 얻기
      3.2.2. C++ 에서 GPUScene 버퍼를 UniformBuffer View.PrimitiveSceneData 에 바인딩하는 과정
      3.2.3. C++ 에서 GPUScene 버퍼를 채우는 과정
       3.2.3.1. GPUScene 에 Primitive 추가/제거/업데이트
       3.2.3.2. 업데이트 예약된 Primitive를 GPUScene에 반영

    4. 레퍼런스

     

    1. 환경

    Unreal Engine 5 (ue5-main branch acc8c5f399ca01f6f549108be1fb75381fecbca8)

    UE4.26.2 코드와 거의 유사하므로 UE4에 익숙하시면 보시는데 문제가 없을 것 같습니다.
    일부 코드는 줄 바꿈 되거나 접은 상태로 중요한 코드를 위주로 설명하니 보실 때 참고해주세요.
    또한 코드를 직접 붙여 넣어서 진행했기 때문에 작아서 잘 보이지 않는 코드는 클릭하여 봐주세요.

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

    필요한 사전 지식
    Graphic API 기본 이해
    Shader 기본 이해

     

    아래 내용을 먼저 읽고 이해한 뒤 보면 더욱 좋습니다.

    [UE5] MeshDrawCommand (1/2)

    [UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정

     

    2. 목표

    렌더링을 하는데 필요한 기본 클래스는 UPrimitiveComponent 입니다. UPrimitiveComponent는 FPrimitiveSeneProxy와 FPrimitiveSceneInfo 형태로 Rednering thread에 전달되어 렌더링 됩니다. 실제 렌더링 시, 각 Primitive들을 렌더링 하는데 필요한 Primitive 마다의 고유 데이터가 있을 것입니다. 예를 들면, Matrix들인 WorldToLocal, LocalToWorld, ActorWorldLocation, ObjectOrientation, ObjectBound 등등입니다.

     

    1. 언리얼에서는 이런 Primitive 전용 데이터를 어떻게 관리하는지 알아봅시다.

    2. 렌더링 될 수 있는 모든 Primitive 의 전용 데이터의 한 개의 버퍼에 모두 넣어서 사용하는 방식인 GPUScene을 알아봅시다.

    3. 언리얼의 Auto-Instancing 기능을 알아봅시다.

     

    3. 내용

    3.1. Primitive 전용 데이터인 PrimitiveSceneData의 구성과 쉐이더에 전달

    3.1.1. PrimitiveData 정의

    먼저 Primitive 들이 어떤 데이터 타입을 가지고 있는지 보겠습니다. C++과 Shader 각각에 동일한 PrimitiveData 를 별도로 선언해서 사용합니다. 그림1이 C++, 그림2가 Shader에 선언된 PrimitiveData 입니다.

    그림1. C++에서 선언된 PrimitiveData

     

    그림2. Shader에서 선언된 PrimitiveData

    3.1.2. 실제 쉐이더에서 PrimitiveData 사용

    Vertex or Pixel 쉐이더에서 PrimitiveData를 사용할 때는 GetPrimitiveData 함수를 사용합니다. GetPrimitiveData 함수의 선언은 다양한 곳에 있습니다. 아래는 그림3은 그중 MaterialTemplate.ush와 LocalVertexFactory.ush에 선언된 것을 볼 수 있습니다.

    그림3. PrimitiveData를 얻어올 수 있는 GetPrimitiveData 함수의 선언

    이어서 실제로 GetPrimitiveData함수를 BasePassVertexShader.usf, BasePassPixelShader.usf, MaterialTemplate.usf에서 사용하는 것을 그림4에서 확인할 수 있습니다.

    그림4. 버택스/픽셀 쉐이더 그리고 MaterialTemplate에서 사용중인 GetPrimitiveData 예제

     

    또 하나 그림3을 보면 GetPrimitiveData에 PrimitiveId를 직접 전달하여서 PrimitiveData를 얻는 부분이 있습니다. PrimitiveId는 GPUScene을 사용하는 경우만 쓰게 되는 변수입니다.

    1. Mobile이고 VertexShader의 경우 GetPrimitiveData(uint PrimitiveId)의 사용하지 않음을 볼 수 있습니다.

    2. VF_USE_PRIMITIVE_SCENE_DATA 는 GPUScene을 사용하는 경우만 사용하므로 그렇지 않은 경우는 UniformBuffer에서 직접 데이터를 채워서 돌려줍니다. (GetPrimitiveDataFromUniformBuffer 의 구현은 그림6에 있습니다.)

    그림5. GPUScene을 사용하지 않는 경우 GetPrimitiveData(uint PrimitiveId)의 구현

     

    3.1.3. 실제 쉐이더에서 PrimitiveData 사용을 위해 캐싱하는 과정

    위의 그림3을 보면 PrimitiveData를 얻어올 때, Parameters.SceneData.Primitive에 캐싱된 데이터를 사용하는 것을 볼 수 있습니다. 이 데이터가 어떻게 캐싱되는지 알아봅시다. 그림6는 BasePassVertexShader.usf 를 기준으로 Primitive를 캐싱하는 과정을 따라가 봅니다.

    1. BasePassPixelShader.usf에서 GetvertexFactoryIntermediates 함수를 호출하여 FVertexFactoryIntermediates 클래스에 Primitive의 캐싱합니다.

    2. Intermediates.SceneData 에 VF_GPUSCENE_GET_INTERMEDIATES 매크로를 호출해서 FSceneDataIntermediates 클래스를 Intermediates.SceneData 에 캐싱합니다. 이 클래스 내부에 PrimtiveData가 같이 캐싱됩니다. 여기서 VF_USE_PRIMITIVE_SCENE_DATA를 0이라고 가정합니다. 현재는 GPUScene을 사용하지 않고 PrimitiveData 를 사용하기 때문입니다. (GPUScene을 사용하는 부분은 3.2에서 보겠습니다. )

    3. 계속해서 GetSceneDataIntermediates 함수를 호출합니다.

    4. GetSceneDataIntermediates에서 GetPrimitiveDataFromUniformBuffer 함수를 통해서 FPrimitivieSceneData를 채웁니다.

    5. "Primitive" 는 UniformBuffer이고, 여기서 부터 필요한 PrimitiveData를 채웁니다. 이 UniformBuffer의 선언은 그림8의 C++코드에서 "Primitive"로 설정됩니다.

    그림6. BasePassVertexShader.usf 에서 Primitive를 캐싱하는 과정

     

    이제 FVertexFactoryIntermediates 가 있으면 SceneData.Primitive 를 통해서 PrimitiveData를 얻어올 수 있습니다.

    추가로 FMaterialVertexParameters 에서도 SceneData.Primitive 를 통해서 PrimitiveData를 얻어올 수 있도록, 이전에 생성한 데이터를 복사하는 과정을 그림7에서 볼 수 있습니다.

    1. 그림6에서 생성한 FVertexFactoryIntermediates 데이터

    2. FMaterialVertexParameters를 GetMaterialVertexParameters로 생성

    3. FVertexFactoryIntermediates의 SceneData를 FMaterialVertexParameter의 SceneData에 그대로 복사

    그림7. Primitive 정보를 MaterialVertexParameters로 복사

     

    지금까지 쉐이더에서 어떻게 PrimitiveData를 캐싱하고, 사용하는지를 보았습니다. 지금부터는 어떻게 PrimitiveData에 관한 UniformBuffer를 C++에서 선언하고, 데이터를 채워서 쉐이더에 바인딩하는지에 대해서 알아볼 것입니다.

     

    3.1.4. C++ PrimitiveData에서 UniformBuffer의 선언과 데이터 바인딩

    그림1과 같이 FPrimitiveUniformShaderParameters 클래스로 PrimitiveData 관련 UniformBuffer를 선언합니다. 실제로 FPrimitiveUniformShaderParameters가 쉐이더에서는 "Primitive"로 불립니다.

    그림8. FPrimitiveUniformShaderParameters 를 "Primitive"로 약속함

     

    위에서 설정된 "Primitive"는 쉐이더 코드에서 사용됩니다. 그래서 컴파일된 쉐이더의 리플렉션 정보로부터 Primitive가 사용되었는지 여부를 얻을 수 있습니다. 만약 Primitive가 사용되고 있다면 쉐이더에서 PrimitiveData를 사용하고 있는 것이므로 UniformBuffer를 바인딩할 쉐이더 바인딩 포인트를 얻어오는 등의 작업을 합니다. (쉐이더 컴파일의 전체 과정은 [UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정 이 글에 소개합니다.)

    1. 컴파일이 완료된 쉐이더는 FShader로 만들어집니다. 함수의 생성자에 CompiledShaderInitializerType 클래스를 넘겨받아 필요한 정보를 설정합니다.

    2. 쉐이더에서 얻어온 리플렉션 정보들 중 UniformBuffer 정보들은 ParameterMap에 담겨있습니다. 이것을 사용하여 UniformBuffer가 바인딩될 포인트를 찾아서 추가해줍니다.

    3. ParameterMap에 보면 View와 Primitive 두 개의 UniformBuffer를 사용하는 것을 볼 수 있습니다.

    그림9. 컴파일된 쉐이더의 리플렉션 데이터를 사용하여 FPrimitiveUniformShaderParameters를 쉐이더에 바인딩 할 수 있도록 준비함

     

    계속해서 C++에서 FPrimitiveUniformShaderParameters의 데이터를 채워주는 클래스를 보겠습니다.

    1. FPrimitiveUniformShaderParametersBuilder 클래스의 Build함수를 사용하여 FPrimitiveUniformShaderParameters의 값을 채울 수 있습니다.

    2. PrimitiveData 중 Flag 정보에 다양한 정보를 담을 수 있습니다.

    3. 기타 나머지 Primitive 전용 데이터인 WorldToLocal, LocalToWorld 데이터를 담습니다.

    그림10. FPrimitiveUniformShaderParameters를 생성할 수있는 빌더 클래스

     

    계속해서 실제로 FPrimitiveUniformShaderParameters를 빌드하는 시점과 UniformBuffer를 어디에서 생성/업데이트하는지 알아봅시다.

    1. FStaticMeshSceneProxy 의 생성자에서 bVFRequiresPrimitiveUniformBuffer를 설정해줍니다.

    2. GPUScene을 사용하지 않거나 모바일 쉐이더를 사용하는 경우 bVFRequiresPrimitiveUniformBuffer를 true로 설정하여 Primitive UniformBuffer를 사용하게 됩니다.

    3. FPrimitiveUniformShaderParametersBuilder 에 필요한 데이터를 전달하고 FPrimitiveUniformShaderParameters를 생성합니다.

    4. 그리고 실제 UniformBuffer를 생성/갱신해줍니다.

    그림11. FPrimitiveSceneProxy에서 FPrimitiveUniformShaderParameters를 생성하는 과정

     

    이렇게 생성한 FPrimitiveSceneProxy는 MeshBatch를 생성할 때 전달됩니다. 그림12을 보세요. (MehBatch와 MeshDrawCommand 생성은 [UE5] MeshDrawCommand (1/2) 를 참고해주세요.)

    1. FMeshBatch를 PreparePrimitiveUniformBuffer를 호출합니다.

    2. FMeshBatch는 여러 개의 서브메시인 FMeshBatchElement를 가질 수 있습니다. 각 MeshElement의 PrimitiveUniformBuffer에 동일한 FPrimitiveUniformShaderParameters를 전달합니다. GPUScene을 사용하지 않는 경우만 Primitive UniformBuffer를 사용합니다.

    그림12. FPrimitiveUniformShaderParameters를 각 MeshElement에 전달

     

    계속해서 준비된 FPrimitiveUniformShaderParameters를 각 쉐이더 별 바인딩 정보를 생성하는 것을 보겠습니다. 여기서 준비된 바인딩 정보를 FMeshDrawCommand가 렌더링 할 때 그대로 사용합니다.

    1. FMeshDrawCommand에 실제로 렌더링할 모든 정보들이 캡슐화됩니다. 

    2. MeshBatchElement 별로 GetElementShaderBindings 함수를 사용해서 각 쉐이더에 대한 바인딩 정보를 모읍니다.

    3. GetElementShaderBindings 함수에서는 GPUScene을 사용하지 않는 경우 FPrimitiveUniformShaderParameters를 바인딩합니다. 현재는 GPUScene을 사용하지 않는 케이스를 보고 있기 때문에 FPrimitiveUniformShaderParameters의 바인딩 정보를 모읍니다.

    그림13. FMeshDrawCommand 바인딩 정보 객체에 FPrimitiveUniformShaderParameters 를 쉐이더에 바인딩 정보 저장

     

    이후 과정은 FMeshDrawCommand가 모아둔 쉐이더 바인딩 정보를 렌더링 전에 바인딩하고 렌더링 하게 됩니다.

    지금까지 GPUScene을 사용하지 않는 경우 PrimitiveData의 선언, 쉐이더에서의 사용, C++에서 쉐이더에 PrimitiveData 전달 과정을 봤습니다.

     

    3.2. 한 개의 버퍼에 PrimitiveSceneData를 모두 모아 사용하는 GPUScene

    GPUScene의 경우는 1개의 버퍼에 모든 PrimitiveData 들을 저장해둡니다. 그리고 실제 렌더링 될 때 PrimitiveId를 사용하여 버퍼에서 해당 Index 에 있는 PrimitiveData를 얻어와서 사용합니다.

     

    3.2.1. 실제 쉐이더에서 GPUScene에서 PrimitiveData 얻기

    위에서 봤던 GetPrimitiveData 함수입니다. 이제는 GPUScene을 사용하기 때문에 이 함수로부터 PrimitiveData를 얻습니다.

    1. 1개 버퍼로 이루어진 GPUScene 에서 PrimitiveData를 얻어오려면 해당 Primitive의 Index를 알고 있어야 합니다. PrimitiveId가 그것입니다.

    2. Primitive의 Stride를 고려하고 실제 Primitive의 시작 Index를 얻어냅니다.

    3. LoadPrimitivePrimitiveSceneDataElement 함수에 현재 Primitive의 Index와 Primitive가 가진 프로퍼티의 인덱스를 넘겨서 Primitive 가 가진 프로퍼티를 얻어옵니다. 이런 과정을 거쳐 모든 Primitive의 데이터를 채우면 그 결과를 리턴합니다.

    4. LoadPrimitivePrimitiveSceneDataElement 함수 호출 시 PrimitiveData의 시작 인덱스인 PrimitiveIndex와 PrimitiveData의 프로퍼티의 Offset 인 ItemIndex를 같이 넘겨줍니다.

    5. GPUScene의 데이터는 기본적으로 UniformBuffer View의 PrimitiveSceneData에 있는 데이터를 사용합니다. 그림15에서 C++ 에 선언되 View 를 볼 수 있습니다.

    그림14. GPUScene으로 부터 PrimitiveData를 얻어오는 GetPrimitiveData 함수

     

    그림15. UniformBuffer View 에 PrimitiveSceneData 에 GPUScene의 데이터가 들어감.

     

    이후 PrimitiveData를 Vertex or Pixel 쉐이더에서 가져다 쓰는 부분은 GPUScene을 사용하지 않았을 때와 동일합니다.

     

    3.2.2. C++ 에서 GPUScene 버퍼를 UniformBuffer View.PrimitiveSceneData 에 바인딩하는 과정

    GPUScene은 UniformBuffer View.PrimitiveSceneData 에다가 모든 PrimitiveData를 바인딩합니다. View는 FSceneRenderer 클래스에 TArray<FViewInfo> Views; 형태로 존재합니다. 에디터나 특정 게임의 경우 여러 개의 View를 가질 수 있기 때문에 배열로 되어있는 것을 볼 수 있습니다. 보통은 렌더링 패스가 시작되는 시점에서 View 를 갱신해줍니다.

    1. View 의 InitRHIResources() 함수를 따라가 보면 View 를 초기화하는 과정을 볼 수 있습니다.

    2. UniformBuffer의 데이터를 담을 FViewUniformShaderParameters 를 생성한 뒤 SetupUniformBufferParameters() 함수를 호출하여 내용을 채웁니다.

    3. SetupUniformBufferParameters() 함수를 호출합니다.

    4. GPUScene을 사용한다면, GPUScene의 PrimitiveBuffer.SRV를 ViewUniformShaderParameters.PrimitiveSceneData에 바인딩해줍니다.

    5. 마지막으로 FViewUniformShaderParameters 를 사용하여 UniformBuffer를 생성합니다.

    그림16. GPUScene이 UniformBufferParameter View.PrimitiveSceneData에 설정되고, Parameters를 사용하여 UniformBuffer를 생성하는 과정

     

    이제 이렇게 바인딩된 PrimitiveData를 쉐이더에서 사용하면 됩니다. 위에서 봐왔던 그림12, 그림13을 보면 GPUScene을 사용하는 경우 개별 Primitive가 자신의 UniformBuffer 인 "Primitive" 를 바인딩하지 않는 것을 볼 수 있습니다.

     

    이제 마지막 과정인 GPUScene의 버퍼를 어떻게 채우는지 확인해봅시다.

     

    3.2.3. C++ 에서 GPUScene 버퍼를 채우는 과정

    GPUScene은 FScene 클래스에 단 한 개 존재합니다. 클래스 이름은 FGPUScene 입니다. GPUScene에 Primitive의 데이터를 추가/갱신/삭제 하는 작업은 모두 FGPUScene에 있습니다.

     

    3.2.3.1. GPUScene 에 Primitive 추가/제거/업데이트

    Primitive가 렌더링 스레드에 등록되는 시점은 UpdateAllPrimitiveSceneInfos() 입니다. 여기서 실제 Primitive를 FScene에 등록하면서 GPUScene에 등록하게 됩니다. (UpdateAllPrimitiveSceneInfos 와 관련된 자세한 내용은 [UE5] MeshDrawCommand (1/2) 를 참고해주세요)

    1. UpdateAllPrimitiveSceneInfos 로 FScene에 등록/삭제/갱신 대기 중인 모든 Primtive를 여기서 처리합니다.

    2. Primitive 추가에 관한 부분입니다. 모든 추가될 Primitives 들을 여기서 FScene에 등록해줍니다.

    3. Primitive 추가 중에 GPUScene.AddPrimitiveUpdate 함수를 통해서 GPUScene에 PrimitiveData를 할당합니다. 여기서 SourceIndex를 전달하는데 이 Index는 FScene에 있는 Primitives 배열의 인덱스라는 것을 알 수 있습니다. 그리고 이 SourceIndex가 바로 PrimitiveId가 됩니다.

    4. GPUScene.AddPrimitiveUpdate 함수 내부에서는 업데이트할 PrimitiveId로 등록합니다. DirtyState 를 사용하여 업데이트할 PrimitiveId를 중복해서 등록하지 않습니다. 또한 DirtyState 에는 업데이트 해야할 상태 들을 누적해서 설정해두고 한번에 업데이트 할 수 있도록 사용합니다. 그림18 에서 DirtyState의 상태를 볼 수 있습니다.

    그림17. GPUScene에 새로 추가된 Primitive를 추가 예약

     

    그림18. PrimitiveDirtyState enum

     

    GPUScene에서 Primitive를 제거하는 과정 또한 UpdateAllPrimitiveSceneInfos() 함수에서 일어납니다. 제거될 Primitive는 RemovedLocalPrimitiveSceneInfos 배열에 담겨있습니다. 아래 그림19에서, GPUScene.AddPrimitiveToUpdate 함수를 통해 GPUScene에 있는 PrimitiveData의 제거 예약을 합니다.

    그림19. GPUScene에 제거된 Primitive를 제거 예약

     

    UpdateAllPrimitiveSceneInfos()에서는 렌더링 스레드에 존재하는 Primitive의 Transform만 업데이트할 수도 있습니다. 이것 또한 GPUScene.AddPrimitiveToUpdate 함수로 갱신합니다.

    그림20. GPUScene에서 Transform이 변경된 Primitive 예약

     

    3.2.3.2. 업데이트 예약된 Primitive를 GPUScene에 반영

    Primitive는 FScene에 등록/수정/제거 될 때마다 PrimitiveData를 갱신해달라고 GPUScene에 예약해둡니다. 이제 예약된 GPUScene 업데이트가 어떻게 이루어지는지 확인해봅시다. 그림21을 보면 GPUScene을 업데이트시키는 함수를 볼 수 있습니다. 이제 차근차근 따라가 봅시다.

    그림21. DeferredShading 을 사용하는 경우 Render 함수 중간에서 GPUScene을 업데이트 함 (모바일의 경우 InitViews 에서 호출됨)

     

    계속해서 Update 함수를 호출하는 경우 어떻게 처리되는지 보겠습니다.

    1. UpdateInternal 함수를 통해서 실제 업데이트를 처리합니다.

    2. GGPUSceneUploadEveryFrame 과 같은 플래그가 켜져 있는 경우 매프레임 GPUScene에 있는 모든 Primitive를 갱신할 수 있습니다. GPUScene이 업데이트되지 않는 것 같다거나 혹은 의심되는 경우 디버깅을 위해서 호출해줄 수 있습니다.

    3. FUploadDataSourceAdapterScenePrimitives 라는 클래스를 FScene, PrimitiveToUpdate 그리고 PrimitiveDirtyState 배열을 사용하여 생성합니다.

    4. FUploadDataSourceAdapterScenePrimitives는 전달받은 PrimitiveToUpdate, PrimitiveDirtyState를 정보 그리고 FScene을 가집니다. FScene에 실제 Primitive들의 정보들이 모두 있기 때문에 PrimitiveToUpdate 내에 있는 PrimitiveId 정보로 모든 정보를 얻어낼 수 있을 것입니다.

    5. FScene에 있는 PrimitiveSceneProxies를 사용하여 FPrimitiveUploadInfo 클래스를 채워줍니다. 여기에는 PrimitiveID와 FPrimitiveSceneShaderData를 채우는 과정이 핵심입니다. FPrimitiveUploadInfo 는 그림23에서 FPrimitiveSceneData는 그림24에서 볼 수 있습니다.

    그림22. GPUScene의 업데이트를 위해서 Adaptor를 준비하는 과정

     

    그림23. FPrimitiveUploadInfo FPrimitiveSceneShaderData(그림24)를 갖고 있음.

     

     

    그림24. FPrimitiveSceneShaderData 는 FPrimitiveSceneProxy로 부터 FPrimitiveUniformShaderParameters를 생성하고, 이것을 GPUScene에 담을 수 있게 TStaticArray&lt;FVector4, DataStridInFloat4&gt; 배열에 담음.

     

    계속해서 코드를 보겠습니다. 실제로 GPUScene의 데이터가 업데이트되는 것은 아래 그림25의 AddPass(GPUSceneupdate) 에서 진행됩니다. 실제 업데이트를 수행하는 함수는 UpdateGeneraal 입니다.

    (바로 전 과정에서 PrimitiveId를 갱신하거나 하는 작업도 있지만 여기서는 PrimitiveData가 업데이트되는 과정에 집중해보려고 합니다. 추가로 GPUScene에 InstanceData 데이터도 같이 업로드하는 부분이 있는데 이 부분은 InstanceData를 사용하는 코드를 볼 때 같이 보도록 하겠습니다.)

    그림25. GPUSceneUpdate 패스 추가

    1. UpdateGeneral 함수는 위에서 생성해놓은 Adaptor 함수와 GPUScene 버퍼를 전달받습니다.

    2. ProcessPrimitiveFn 람다 함수를 선언합니다. 이 함수는 Adaptor로부터 FPrimitiveUploadInfo(그림23)를 얻어 온 다음 PrimitiveData에 해당하는 부분을 FScatterUploadBuffer PrimitiveUploadBuffer;(그림27) 의 Upload 버퍼에 기록합니다. Upload 버퍼에 기록이 완료되면 FScatterUploadBuffer의 ResourceUploadTo 함수를 사용하여 실제 GPUScene 버퍼로 업로드하게 됩니다. 그리고 FScatterUploadBuffer의 내부에서는 Scatter 버퍼를 추가로 관리하는데 이 버퍼는 GPUScene 버퍼의 어느 인덱스에 Upload 버퍼의 데이터가 저장되어야 할지에 대한 정보가 담깁니다(즉, 인덱스 정보). 업로드는 ComputeShader를 통해 수행됩니다.

    3. GPUScene에 업로드를 위해서 버퍼에 PrimitiveId가 저장되어야 할 부분의 주소를 얻습니다. 이 과정에서 Upload 버퍼에 저장되어야 할 주소 위치가 리턴됩니다.

    4. 실제 FPrimitiveUploadInfo가 가진 PrimitiveSceneData(Primitive의 UniformBuffer 데이터)를 업로드 버퍼에 복사해줍니다.

    5. 업로드 방식은 총 2가지입니다. ComputeShader를 통해 업로드하기 때문에 ComputeShader가 사용할 수 있는 최대 Group 크기를 고려합니다. 그래서 만약에 한 번에 업데이트 가능한 경우 이 코드를 실행합니다. PrimitiveUploadBuffer.ResourceUploadTo가 한 번만 불리는 것을 확인할 수 있습니다.

    6. 만약 ComputeShader 가 지원하는 최대 Group 수보다 더 큰 개수를 업데이트하는 경우는 이 코드를 실행합니다. 여러 번의 ProcessPrimitiveFn과 PrimitiveUploadBuffer.ResourceUploadTo가 불리는 것을 볼 수 있습니다.

    그림26. GPUScene에 모든 PrimitiveData들을 업데이트 하는 코드
    그림27. GPUScene 버퍼에 PrimitiveData 들을 업로드하는 FScatterUploadBuffer, ResourceUploadTo 함수 호출로 GPUScene 버퍼에 업로드 함.

     

    이제 업로드 버퍼에 복사되어 실제 GPUScene 버퍼에 복사될 준비가 된 내용을 ResourceUploadTo 함수를 통해서 업로드합니다. 

    그림28. ResourceUploadTo 함수에서 Structured_Buffer 타입으로 DstBuffer(GPUScene Buffer)에 최종적으로 Primitive 데이터를 업로드 함.

     

    컴퓨트 쉐이더는 ScatterCopyCS 함수를 사용하며, Scatter 버퍼에서는 DstBuffer 저장되어야 할 인덱스, Upload 버퍼에는 실제 PrimitiveData 가 저장되어있습니다. 이 정보를 통해서 최종적으로 GPUScene 버퍼에 데이터를 복사합니다.

    그림29. 실제 ComputeShader에서 DstBuffer로 데이터가 복사되는 과정 (ByteBuffer.usf)

     

    여기까지가 Primitive 고유의 PrimtiveData를 GPUScene을 통해서 C++ 에서 쉐이더로 복사하고 사용하는 과정입니다. 이제 다음 글에서는 언리얼의 Auto-Instancing 기능을 알아보려 합니다. Auto-Instancing 기능은 GPUScene와 함께 작동하는 기능이기 때문에 이 글을 읽고 보시는 것을 추천합니다.

     

    4. 레퍼런스

    1. Unreal Engine 5 (ue5-main branch acc8c5f399ca01f6f549108be1fb75381fecbca8)

    2. [UE5] MeshDrawCommand (1/2)

    3. [UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정

     

    댓글

Designed by Tistory & scahp.