ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] MeshDrawCommand (2/2)
    UE4 & UE5/Rendering 2021. 6. 20. 12:34

    [UE5] MeshDrawCommand (2/2)

     

    최초 작성 : 2021-06-20
    마지막 수정 : 2023-05-17
    최재호

     

    Updated 2023-05-17 : DynamicMeshElement 가 어떤 MeshPass 에 추가될 것인지를 보여주는 추가그림1 추가

     

    1. 환경

    Unreal Engine 5 Early access

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

    필요한 사전지식
    1. NamedThread (TaskGraphSystem에 대한 이해가 있으면 더 좋음)
    2. Actor와 ActorComponent의 관계

    * 이 글에서 StaticMesh, DynamicMesh가 등장하는데, Relevance에 따라 어떤 객체든 StaticMesh, DynamicMesh가 될 수 있습니다. 하지만 설명을 간단하게 하기 위해서 StaticMesh == UStaticMeshComponent, DynamicMesh == USkeletalMeshComponent로 두고 설명합니다.
    * 코드에는 설명에 대응하는 1, 2, 3 등의 번호가 매겨져 있으니 참고해주세요.
    * 개인적인 공부 용도로 분석한 내용이라 틀린 내용이 있을 수 있습니다. 그런 부분은 지적 부탁드립니다.

    2. 목표

    1. UPrimitiveComponent 에서 렌더스레드에서 최종적으로 렌더링 커맨드를 호출하는 FMeshDrawCommand의 생성과정을 알아봅니다.
    2. FMeshDrawCommand는 StaticMesh냐 DynamicMesh냐 여부에 따라서 캐싱해놓고 반복해서 사용되는지 매 프레임 새로 만들어지는지 결정됩니다. 실제로 어떻게 그렇게 구현되는지 확인해봅시다.
    3. 렌더스레드는 UPrimitiveComponent와 대응하는 렌더링에 필요한 모든 자료를 관리합니다. 그래서 게임스레드에서 UPrimitiveComponent가 소멸되더라도 이와 대응되는 리소스들이 렌더스레드에서 즉시 소멸되지 않습니다. 이 부분이 어떻게 가능한지도 코드를 분석하며 알아봅시다.
    4. 오늘 주요 목표는 StaticMesh와 DynamicMesh가 어떻게 FMeshDrawCommand를 생성하고 렌더링 하는지 알아보는 것입니다. 그래서 그 외의 사항(쉐이더가 어떻게 컴파일되는지와 같은 기타 사항)은 다음에 다뤄볼 예정입니다.


    이 글은 분량이 많아서 2개로 쪼개 집니다.
    이전 글 MeshDrawCommand (1/2) 에서는 StaticMesh가 아래 과정대로 변화는 과정을 알아볼 것입니다.
    UPrimitivieComponent → FPrimitiveSceneProxy/FPrimitiveSceneInfo → FMeshBatch → MeshDrawCommand(캐싱)

    MeshDrawCommand (2/2)에서는 아래의 과정을 알아봅니다.
    1. DynamicMesh에서 FMeshBatch와 FMeshDrawCommand를 생성
    2. 렌더링 될 FMeshDrawCommand(Static, Dynamic 모두)를 모으기
    3. 전 과정에 모은 FMeshDrawCommand를 렌더링

    3. 내용

    지금부터는 DynamicMesh의 FMeshBatch와 FMeshDrawCommand를 생성하고 실제 렌더링 할 FMeshDrawCommand를 수집하고, 실행하는 과정을 볼 것입니다.
    모든 것의 InitViews 내에서 일어납니다. 먼저 ComputeViewVisibility를 호출합니다.

    1. FViewVisibleCommandsPerView에 렌더링 될 FMeshDrawCommand를 모읍니다. View별로 그리고 Render Pass 별로 별도로 모으게 됩니다. View 보통 1개니까 여기서는 1개로 가정하고 Render Pass 별로 렌더링될 FMeshDrawCommand가 FViewVisibleCommandsPerView에 담길 거라고 생각하고 계속 보도록 하겠습니다.

    그림1.

    1. FViewVisibleCommandsPerView 를 채우기 위해서 레퍼런스 형태로 넘겨받습니다.
    2. HasDynamicMeshElementsMasks는 FScene->Primitives와 같은 개수로 초기화되며, DynamicMesh 중 렌더링 될 Index에 1로 설정해줍니다. 즉, DynamicMesh 용 VisibilityMap입니다.
    3. FViewInfo는 현재 View에서 렌더링 될 FMeshDrawCommand들이 저장될 것입니다. ViewFrustum에 따라서 렌더링 될 FMeshDrawCommand가 달라질 것이기 때문입니다. 그리고 해당 View의 PrimitiveVisibilityMap 또한 초기화합니다.

    그림2.

    계속해서 ComputeViewVisibility내의 FSceneRenderer::ComputeAndMarkRelevanceForViewParallel을 호출합니다. 여기서는 2가지를 합니다.
    1. 렌더링 될 StaticMesh의 FMeshDrawCommand를 모읍니다.
    2. 렌더링될 DynamicMesh의 정보인 HasDynamicMeshElementsMasks를 채웁니다.

    그림3.

    1. PrimitiveVisibilityMap 요소들의 Visibility 체크를 병렬로 처리하기 위해서 PrimitiveVisibilityMap을 순회하면서 적절한 개수로 FRelevancePacket을 나눕니다.
    2. FRelevancePacket에 처리해야 할 Primitive의 Index를 담으며, FRelevancePacket이 가득 차면 FRelevancePacket을 추가해줍니다.
    3. AnyThreadTask를 호출하여 병렬로 Task를 실행합니다.

    그림4.

    1. AnyThreadTask에서는 먼저 ComputeRelevance가 호출됩니다. 내부에서는 전 과정에서 받은 Primitive에 ViewRelevance를 체크합니다. 여기서 저희는 StaticMesh로 처리될 것인가 DynamicMesh로 처리될 것인가 여부를 판단하는 부분에 집중할 것입니다.
    2. FPrimitiveSceneProxy->GetViewRelevance를 통해서 얻어집니다. 여기에는 Static or Dynamic 뿐만 아니라 렌더링 되어야 하는지나 Shadow Caster로 동작할 것인지 여부를 파악할 수 있습니다.
    3. DynamicMesh로 처리될 Primitive Index에 1을 설정해줍니다.
    4. StaticMesh로 처리될 Index를 저장합니다.

    그림5.

    다음으로 FRelevancePacket의 AnyThreadTask의 MarkRelevant를 호출하는 것을 이어서 봅시다. 여기서는 렌더링 될 StaticMesh에 대한 FMeshDrawCommand를 VisibleCachedDrawCommands에 넣습니다. (VisibleCachedDrawCommands는 FRelevancePacket->FDrawCommandRelevancePack에서 VisibleCachedDrawCommands를 모으는 컨테이너입니다. 이 컨테이너에 담긴 FMeshDrawCommand는 결국 FViewCommands로 전달되어 렌더링 될 것입니다.)

    1. ComputeRelevance 과정에서 렌더링 될 StaticMesh 모두에 대해서 처리를 합니다.
    2. StaticMesh들은 자신이 캐싱해둔 FMeshBatch 모두에 들에 대해서 처리를 합니다.
    3. 렌더링 될 것인지 확인하고
    4. FDrawCommandRelevancePacket::AddCommandsForMesh 함수를 호출하여 FMeshDrawCommand를 VisibleCachedDrawCommands에 넣습니다. 각 Render Pass별로 필요한 Pass에는 모두 추가해줍니다. 계속해서 AddCommandsForMesh의 내부로 들어가 캐싱된 FMeshDrawCommands를 가져오는 부분을 확인해봅시다.

    그림6.


    AddCommandsForMesh는 결국 VisibleCachedDrawCommands에 FMeshDrawCommand를 넣어주는 작업을 합니다.
    1. 이전 코드에서 StaticMesh에 대응한 FPrimitiveSceneInfo 정보를 넘겨줍니다. 이것만 있다면 우리는 FPrimitiveSceneProxy, 그리고 캐싱해둔 FMeshBatch들과 FMeshDrawCommand의 정보들을 얻을 수 있습니다. 그래서 이것으로 모든 것을 처리할 수 있습니다.
    2. 캐시 된 FMeshDrawCommand는 FPrimitiveSceneInfo에서 FCachedMeshDrawCommandInfo라는 곳에 담겨있다고 이전에 설명하였습니다. 그래서 그것과 FScene에 있는 FMeshDrawCommand가 캐시 되어있는 CachedDrawLists를 준비합니다.
    3. VisibleCachedDrawCommands[PassType] 에 새로운 FVisibleMeshDrawCommand를 추가합니다. 새로운 클래스가 나왔지만 이 구조는 FMeshDrawCommand를 갖고 있으며 Visibility 체크 시 캐시 효율을 위해서 필요한 데이터를 더 담고 있는 클래스라고만 알고 계속 진행해도 충분합니다.
    4. 여기서는 GPUScene을 사용하는 경우가 아니므로 SceneDrawList.MeshDrawCommands(== CahcedDrawLists[PassType].MeshDrawCommands)를 사용하게 됩니다. GPUScene 은 여기서 사용하지 않는다고 가정하고 계속 진행하겠습니다.
    5. 마지막으로 FVisibleMeshDrawCommand에 FMeshDrawCommand와 부가 정보를 담아서 캐싱된 FMeshDrawCommand를 하나 가져왔습니다. 이런 방식으로 모든 StaticMesh의 FMeshDrawCommand를 수집합니다. 현재까지 수집된 StaticMesh의 FMeshDrawCommand는 FDrawCommandRelevancePacket::VisibleCachedDrawCommands[EMeshPass::Num]에 담겨있습니다. 처리가 완료되면 이 데이터는 FViewCommands(FSceneRenderer::ComputeAndMarkRelevanceForViewParallel를 호출할 때 전달받았던)로 전달되어야 합니다

    • FViewCommands는 바로 우리가 가시성 판정을 시작한 InitViews에서 ComputeViewVisibility를 호출할 때 사용했던 넘겨받았던 FViewVisibleCommandsPerView ViewCommandsPerView; 의 배열 중 하나입니다.

    그림7.


    아래의 RenderThreadFinalize()를 호출하여 FViewVisibleCommandsPerView ViewCommandsPerView 에 VisibleCachedDrawCommands를 옮겨 담아 줍니다. 그림8과 그림9를 참고해주세요.

    그림8.
    그림9.


    이제 우리는 렌더링 할 StaticMesh의 FMeshDrawCommand를 모두 모았습니다.

    다음으로 DynamicMesh의 FMeshBatch와 FMeshDrawCommand를 생성하는 과정을 봅시다.
    앞에서 보던 ComputeAndMarkRelevanceForViewParallel 호출이 끝나고 이제 GatherDynamicMeshElements 함수를 호출합니다.
    1. 이전 과정에서 Primitive의 렌더링 될 StaticMesh, DynamicMesh 여부를 판단하고, StaticMesh인 경우 캐시 된 FMeshDrawCommand까지 모두 수집하였습니다.
    2. 이제 DynamicMesh에 대해서 FMeshBatch를 생성하고 동시에 FMeshDrawCommand를 생성할 함수입니다. 내부로 들어가 봅시다.

    그림10.


    1. 이전 과정에서 렌더링 될 DynamicMesh의 인덱스에 1을 표시해둔 것입니다. 이걸 기준으로 FMeshBatch와 FMeshDrawCommand를 만듭니다.
    2. 렌더링 되는지 여부를 판정합니다.
    3. 실제 FMeshBatch가 생성되는 부분입니다. FPrimitiveSceneProxy의 멤버 함수를 통해 호출됩니다. FPrimitiveSceneProxy는 다양한 형태로 상속받아 구현되어있어서, 각자만의 방식으로 FMeshBach를 추가해주고 있습니다. 우리는 그중 FSkeletalMeshSceneProxy를 봅시다.

    그림11.

     

    그림11 에서 GatherDynamicMeshElements 를 전체적으로 둘러봤습니다. 그런데 그림11의 3번 하단에 중요한 코드가 하나 더 있는데 빠드린 것 같아 내용을 추가합니다.

    1. 그림11 하단에 연결되는 코드입니다. 여기서 DynamicMesh 의 Relevance 를 체크하여 각 MeshPass 별 NumVisibleDynamicMeshElements 의 수를 증가시킵니다. 이 값을 기준으로 몇개의 DynamicMeshElement 를 각 MeshPass 별 MeshDrawCommand 로 생성할지 결정합니다.

    2. bDrawRelevance 로 Visibility 를 체크하고, 렌더패스에 따라서 어떤 MeshPass 에서 렌더링 될지 확인합니다.

    3. 예제로 DepthPass 를 확인해봅시다. 현재 처리중인 MeshBatch가 어느 MeshPass 에서 사용될지 PassMask 를 등록하여 나타냅니다. 그리고 NumVisibleDynamicMeshElements[EMeshPass::DepthPass] 의 Element 개수를 증가시킵니다.

    추가그림1.

     

    아래 코드는 FSkeletalMeshSceneProxy의 GetDynamicMeshElements입니다. 어떤 형태로 FMeshBatch가 생성되는지 따라가 봅시다.
    여러 함수를 타고 가는데 결국은 현재 LOD에 대한 정보들을 모아 GetDynamicElementSection을 호출합니다. 바로 GetDynamicElementSection로 이동해봅시다.

    그림12.


    1. 모든 View에 대해서 FMeshBatch를 생성합니다.
    2. MeshBatch를 Collector로부터 할당하고, 필요한 내용들을 채웁니다. FMeshBatch에 어떤 내용이 담기는지는 이전의 설명과 중복되므로 생략하겠습니다.
    3. 최종적으로 Collector에 생성된 Mesh를 담습니다. 계속해서 Collector의 AddMesh함수를 봅시다.

    그림13.

    AddMesh 함수에서는 추가한 MeshBatch와 그에 대응하는 FPrimitiveSceneProxy 등을 FMeshBatchAndRelevance에 담고, 이것을 Collector 내부에 있는 ViewMeshBatches에 보관합니다.
    ViewMeshBatches는 View.DynamicMeshElements의 레퍼런스입니다. 그러니 FViewInfo의 DynamicMeshElements에 MeshBatch들이 채워졌을 것입니다.
    곧 View.DynamicMeshElements를 어디선가 꺼내서 FMeshDrawCommand를 만드는 부분을 만날 것 같군요.

    그림14.

    여기까지 왔다면 DynamicMesh들은 View.DynamicMeshElement(FMeshBatch 배열)에 들어가 있을 것이고, StaticMesh는 바로 렌더링 가능한 FViewCommands에 MeshDrawCommand 형태로 존재할 것입니다.

    이제 GatherDynamicMeshElements에서 나와서 다음으로 진행합니다. 마지막 과정은 바로 SetupMeshPass입니다.

    그림15.

    1. 모든 Render Pass에 대해서 StaticMesh에 대한 캐싱된 FMeshDrawCommand 수집과 DynamicMesh에 대한 FMeshDrawCommand를 생성하여 FViewInfo의 ParallelMeshDrawCommandPasses[PassIndex] 각 패스에 전달합니다. 최종적으로 렌더링 커맨드를 실행하는 것은 바로 FViewInfo의 ParallelMeshDrawCommandPasses 입니다.
    2. ParallelMeshDrawCommandPasses 에 필요한 데이터를 설정합니다. 아래 3가지를 ParallelMeshDrawCommandPasses를 렌더링 준비 완료하는 데 사용합니다.

    • FMeshDrawCommand를 만드는데 필요한 FMeshPassProcessor
    • FViewCommands에 이미 추가된 StaticMesh에 대한 FMeshDrawCommand
    • DynamicMesh에 대한 FMeshBatch들인 View.DynamicMeshElements를 DistpatchPassSetup에 전달합니다.

    그림16.

    DispatchPassSetup에서는 아래와 같은 일을 합니다.
    1. FMeshDrawCommandPassSetupTaskContext에 필요한 정보를 채웁니다.
    2. FMeshDrawCommandPassSetupTask를 호출합니다. DispatchPassSetup는 각 Render Pass별로 호출되므로 각 Render Pass마다 병렬로 FMeshDrawCommandPassSetupTask를 호출하는 것을 알 수 있습니다.

    그림17.


    FMeshDrawCommandPassSetupTask 에서는 GenerateDynamicMeshDrawCommands가 호출됩니다.
    그리고 PassMeshProcessor에 필요한 데이터를 FDynamicPassMeshDrawListContext에 담아 바인딩해주고, StaticMesh의 FMeshDrawCommand의 생성 때와 동일하게 AddMeshBatch를 호출합니다.

    • FMeshDrawCommand를 생성 과정을 빠르게 요약
      • (AddMeshBatch -> TryAddMeshBatch -> Process -> BuildMeshDrawCommands 순을 진행)


    다른 점이 하나 있는데 BuidMeshDrawCommands에서 DrawListContext->FinalizeCommand 처리에서 생성한 FMeshDrawCommand를 FMeshCommandOneFrameArray(FVisibleMeshDrawCommand의 배열)에 바로 추가해주는 것입니다. 이런 것이 가능한 이유는 FDynamicPassMeshDrawListContext 에서 FinalizeCommand를 오버라이드 하여 기능을 추가했기 때문입니다.

    그림18.
    추가 그림1


    FParallelMeshDrawCommandPass에 추가된 StaticMesh와 DynamicMesh들은 이제 각 렌더 패스에서 DispatchDraw 함수를 통해 렌더링 됩니다. 이 함수에서 SubmitMeshDrawCommandsRange 를 호출하여 FMeshDrawCommand를 렌더링 합니다.
    1. InitViews 렌더링 할 FMeshDrawCommand를 최종적으로 FParallelMeshDrawCommandPass에 반영하였습니다. 이 작업은 TaskGraph를 통해 병렬로 수행되며, 렌더링 되기 직전까지 수행됩니다. 그 작업을 기다리고 있습니다.
    2. 보유하고 있는 FMeshDrawCommands를 원하는 범위만큼 렌더링 하는 함수입니다.
    3. For Loop를 돌면서 전달받은 범위에 대한 FMeshDrawCommand의 FmeshDrawCommand::SubmitDraw 함수를 호출합니다.

    그림19.


    SubmitDraw에서는 실제 렌더링을 수행합니다.
    1. SubmitDrawBegin (VertexStream, PipelineState, RenderTarget, 필요한 쉐이더 바인딩들 설정)
    2. SubmitDrawEnd (렌더 커맨드 호출 DrawIndexedPrimitive)

    그림20.


    이것으로 UPrimitiveComponent 부터 FMeshDrawCommand 의 생성하고, 렌더링 하는 것 까지 확인해봤습니다.
    이글에서 목표했던 대부분은 [UE5] MeshDrawCommand (1/2) 에서 확인했습니다.

    이 글에서는 아래 3가지를 확인했습니다.
    1. DynamicMesh의 경우 FMeshBatch, FMeshDrawCommand를 매프레임 만들어서 사용함
    2. StaticMesh는 캐싱해둔 FMeshDrawCommand를 가시성 확인만 확인하여 사용함
    3. 모든 그려질 FMeshDrawCommand는 ParallelMeshDrawCommandPasses 에 추가되어 관리되고 렌더링 됨.

    이전글 [UE5] MeshDrawCommand (1/2)

     

    4. 레퍼런스

    1. https://github.com/EpicGames/UnrealEngine/tree/ue5-early-access

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

    [UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정  (0) 2021.09.10
    [UE5] Shader [1/3] - 주요 클래스 파악  (8) 2021.09.03
    [UE5] MeshDrawCommand (1/2)  (0) 2021.06.20
    [UE4]Geometry Shader  (0) 2020.04.05
    [UE4]Compute Shader  (2) 2020.03.20

    댓글

Designed by Tistory & scahp.