이 페이지에 포함된 정보는 커스텀 메시 패스를 추가하고자 하거나 언리얼 엔진의 메시 그리기 퍼포먼스 특징을 이해하고자 하는 프로그래머를 대상으로 합니다.
메시 그리기 파이프라인(Mesh Drawing Pipeline) 은 모든 씬 드로가 프레임마다 빌드되는 것이 아니라 사전에 준비되는 유지 모드라는 개념을 바탕으로 합니다. 또한 변경이 드물고 여러 프레임에 걸쳐 재사용할 수 있는 스태틱 메시의 프로퍼티를 활용하기 위한 공격적인 캐싱과 드로 콜 병합도 특징입니다.

그리기 프로세스
메시 렌더링은 게임 스레드의 UPrimitiveComponent
에 대한 렌더 스레드 표현인 FPrimitiveSceneProxy
에서 시작합니다. FPrimitiveSceneProxy
는 GetDynamicMeshElements
및 DrawStaticElements
로의 콜백을 통해 FMeshBatch를 렌더러에 제출하는 역할을 합니다.
FMeshBatch
는 FPrimitiveSceneProxy
구현(사용자 코드)을 메시 패스(비공개 렌더러 모듈)에서 분리합니다. 여기에는 패스가 최종 셰이더 바인딩 및 렌더 상태를 파악하기 위해 필요한 모든 요소가 포함되어 있으므로, 프록시는 렌더링될 패스가 무엇인지 전혀 모릅니다.
다음 단계는 FMeshBatch
를 메시 패스별 FMeshDrawCommand
로 변환하는 것입니다. FMeshDrawCommand
는 FMeshBatch
와 RHI 사이의 인터페이스입니다. 이는 상태가 아예 없는 그리기 설명으로, RHI가 메시 그리기에 관해 파악해야 하는 다음의 모든 정보를 저장합니다.
-
사용할 셰이더
-
해당 셰이더의 리소스 바인딩
-
드로 콜 파라미터
이를 통해 RHI 수준 바로 위에서 드로 콜을 캐싱하고 병합할 수 있습니다. FMeshDrawCommand
는 FMeshBatch
에서 메시 패스별 FMeshPassProcessor
로 생성됩니다.
마지막으로 SubmitMeshDrawCommands
를 사용하여 FMeshDrawCommand
를 RHICommandList에 있는 일련의 RHI 명령 세트로 변환합니다.
캐시된 메시 및 다이내믹 메시 배치
FPrimitiveSceneProxy
에는 FMeshBatches
생성을 위한 캐시(Cached)와 다이내믹(Dynamic)이라는 두 가지 경로가 있습니다. FPrimitiveSceneProxy
구현은 GetViewRelevance()
함수를 통해 각 프레임에 사용되는 경로를 제어합니다.

FMeshBatch 코드 경로입니다. 주황색 화살표는 매 프레임마다 수행되어야 하는 연산이고, 파란색 화살표는 캐시되기 전에 한 번 수행되는 연산을 나타냅니다.
캐시 경로는 FMeshBatch
를 빌드 및 재사용하며, 스태틱 메시와 같이 매 프레임마다 변경되지 않는 드로를 빠르게 렌더링할 때 선호됩니다. 캐시 경로는 프록시가 씬에 추가될 때 호출되는 DrawStaticElements
에 의해 구현됩니다. 생성된 FMeshBatches
는 FPrimitiveSceneInfo::StaticMeshes
내부에 저장되며 프록시가 씬에서 제거될 때까지 매 프레임마다 재사용됩니다.
다이내믹 경로는 매 프레임마다 FMeshBatch
를 재생성합니다. 이는 가장 유연한 경로로, 파티클과 같이 프레임마다 종종 변경되는 드로에 사용됩니다. GetDynamicMeshElements
에 의해 구현되며, 이 함수는 InitViews에서 매 프레임마다 호출되며 모든 뷰에 대해 임시 FMeshBatch
를 생성합니다.
FMeshPassProcessor
특정 패스 메시 프로세서는 FMeshPassProcessor
베이스 클래스에서 파생되며, FMeshBatch
를 주어진 패스에 대한 메시 그리기 명령으로 변환하는 역할을 합니다. 여기에서 최종 그리기 필터링이 일어나고, 적절한 셰이더가 선택되며, 셰이더 바인딩이 수집됩니다.
커스텀 메시 패스 프로세서를 생성하려면 반드시 FMeshPassProcessor
에서 파생되어야 하고, AddMeshBatch
함수가 오버라이드되어야 합니다.
AddMeshBatch
구현:
-
그리기 필터링 - 예를 들어 머티리얼에 반투명 그리기 모드가 있는 경우
FDepthPassMeshProcessor
에서 처리하지 않습니다. -
셰이더 및 파이프라인 상태 선택(뎁스/스텐실/블렌드 상태)
-
패스/머티리얼/버텍스 팩토리/프리미티브에 대해 셰이더 바인딩을 수집하고 적절한 목록에 새 그리기 명령을 추가하는
BuildMeshDrawCommands()
를 최종적으로 호출
셰이더 바인딩
언리얼 엔진의 셰이더 바인딩은 유니폼 버퍼, 샘플러, 텍스처, ShaderResourceViews 또는 느슨한 파라미터( FShaderParameter
)일 수 있습니다.
FMeshPassProcessor
는 RHICmdList.SetShaderParameter
로 RHI에 직접 셰이더 바인딩을 전송하지 않으며, 단순히 FMeshDrawSingleShaderBindings
클래스에 기록할 뿐입니다. 모든 패스 간에 공유되는 코드인 BuildMeshDrawCommands()
함수가 패스 셰이더에서 GetShaderBindings()
를 호출합니다.
셰이더 바인딩은 다음과 같은 몇 가지 카테고리로 분류됩니다.
-
ViewUniformBuffer
또는DepthPassUniformBuffer
와 같은 일정 패스의 유니폼 버퍼 -
버텍스 팩토리 바인딩
-
머티리얼 바인딩
-
프리미티브 바인딩
-
드로 간에 변경되는 패스별 바인딩
드로별로 바인딩을 다르게 설정하면 드로 콜 병합이 방지됩니다. 또한 느슨한 파라미터(유니폼 버퍼에 없는 셰이더 파라미터)를 설정해도 드로 간에 느리고 꾸준한 버퍼 업데이트가 강제되어 드로 콜 병합이 방지됩니다.
각 FMeshPassProcessor
는 BuildMeshDrawCommands()
를 통해 패스 셰이더의 GetShaderBindings()
를 호출해야 하기 때문에, FMeshPassProcessor
에서 GetShaderBindings()
호출로 임의 데이터를 전달하는 메커니즘이 필요합니다. 이는 ShaderElementData
파라미터를 통해 BuildMeshDrawCommands()
로 구현됩니다.
FMeshDrawCommand 퍼포먼스 위험
FMeshDrawCommand
에는 추가 힙 할당 없이 가변 길이 배열을 저장하기 위해 다수의 인라인 할당자가 사용됩니다. 이 인라인 할당자에서 오버플로가 발생하면 퍼포먼스 위험이 유발될 수 있는데, 각 메시 그리기 명령이 명령 탐색 시 캐시 누락과 함께 힙 할당을 생성/소멸/복사해야 하기 때문입니다.
FMeshDrawShaderBindings
는 다음과 같이 셰이더 프리퀀시를 2 번으로 가정합니다(버텍스 + 픽셀).
TArray<FMeshDrawShaderBindingsLayout, TInlineAllocator<2>>ShaderLayouts
FMeshDrawCommand
는 모든 프리퀀시 중 셰이더 바인딩을 10 개로 가정합니다.
const int32 NumInlineShaderBindings = 10;
FMeshDrawCommand
는 버텍스 팩토리의 버텍스 스트림을 4 개로 가정합니다.
typedef TArray<FVertexInputStream, TInlineAllocator<4>>FVertexInputStreamArray;
패스 타입
그리기에 FMeshPassProcessor
를 사용하는 방법은 다음과 같이 세 가지가 있습니다.
패스 타입 | 설명 |
---|---|
EMeshPass::Type 열거형 |
여기에 항목을 추가하면 FScene 내부에 FParallelMeshDrawCommandPass 가 할당됩니다. 이를 통해 FScene 이 활성화되어 AddToScene 시간에 패스에 대한 메시 그리기 명령이 캐시됩니다. FMeshPassProcessor 는 반드시 FRegisterPassProcessorCreateFunction 으로 열거형에 등록되어야 합니다. 패스 구성 및 디스패치는 태스크에서 일어납니다. |
수동 패스(Manual Pass) | FParallelMeshDrawCommandPass 가 임의 클래스 내 변수로 저장된 수동 패스를 사용합니다. 이는 패스의 수가 매 프레임마다 변하는 경우 사용됩니다(예: 섀도 뎁스 패스). 이 타입의 패스는 FScene::AddToScene 시간에 명령을 캐시할 수 없지만, 태스크에서 일어나는 패스 구성 및 디스패치의 이점을 계속 누립니다. |
DrawDynamicMeshPass |
즉시 모드 그리기에 사용되며, 가장 느리지만 가장 편리한 접근법입니다. 패스 구성 및 디스패치가 호출자 스레드 내에서 즉시 일어납니다. |
이때 렌더러는 DrawDynamicMeshPass
를 제외하면 플러그인으로 확장 가능하도록 만들어지지 않았기 때문에, 새 패스를 추가하면 렌더러 모듈 코드를 변경해야 합니다.
FParallelMeshDrawCommandPass
커스텀 메시 패스를 추가하려면 먼저 EMeshPass
열거형에 새 항목을 추가해야 합니다. 다음으로는 FRelevancePacket::MarkRelevant()
에서 연관 플래그에 따라 보이는 메시 그리기 명령 목록에 스태틱 메시를 추가합니다. 예를 들어, 뎁스 패스에 관련된 경우 이 스니펫으로 뎁스 패스에 메시 그리기 명령을 추가합니다.
if (StaticMeshRelevance.bUseForDepthPass)
{
DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, Scene, bCanCache, EMeshPass::DepthPass);
}
ComputeDynamicMeshRelevance
에서 다이내믹 그리기에 대한 EMeshPass
의 연관성을 표시합니다.
if (ViewRelevance.bDrawRelevance && (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth))
{
PassMask.Set(EMeshPass::DepthPass);
View.NumVisibleDynamicMeshElements[EMeshPass::DepthPass] += NumElements;
}
FParallelMeshDrawCommandPass::DispatchDraw
를 사용하여 다음과 같은 특정 패스를 그립니다.
View.ParallelMeshDrawCommandPasses[EMeshPass::DepthPass].DispatchDraw(nullptr, RHICmdList);
병렬로 이 패스를 그리기 위해 병렬 명령 목록 세트를 구성하는 것도 가능합니다.
FPrePassParallelCommandListSet ParallelCommandListSet(View, this, ParentCmdList, true, DrawRenderState);
View.ParallelMeshDrawCommandPasses[EMeshPass::DepthPass].DispatchDraw(&ParallelCommandListSet, ParentCmdList);
DrawDynamicMeshPass
FParallelMeshDrawCommandPass
는 일반적인 메시 패스의 디폴트 경로입니다. 메시 그리기 명령 캐싱과 병렬 렌더링을 지원하는 유일한 경로이기 때문에 퍼포먼스가 중요한 메시 패스에 사용합니다. 한편, 퍼포먼스 요구 사항으로 인해 매우 엄격한 설계가 필요합니다. 예를 들어, InitViews 이후에 메시 그리기 명령이나 셰이더 바인딩을 수정하는 것은 불가능합니다.
에디터 내에서 소수의 메시를 그리는 등의 특정 사용 사례의 경우 DrawDynamicMeshPass
가 더 간단한 해결책이 될 수 있습니다. 이는 즉시 모드 메시 그리기를 제공하며, 가장 유연한 렌더링 경로입니다. 언리얼 엔진은 일부 에디터 전용 패스 및 캔버스 렌더링에 DrawDynamicMeshPass
를 사용합니다.
DrawDynamicMeshPass
로 그리기는 꽤 간단합니다. 다음과 같이 메시 그리기 명령의 임시 목록을 채울 람다만 전달하면 됩니다.
DrawDynamicMeshPass(View, RHICmdList, [&View, CurrentDecalStage, RenderTargetMode](FDynamicPassMeshDrawListContext* DynamicMeshPassContext)
{
FMeshDecalMeshProcessor PassMeshProcessor(
View.Family->Scene->GetRenderScene(),
&View,
CurrentDecalStage,
RenderTargetMode,
DynamicMeshPassContext);
for (int32 MeshBatchIndex = 0; MeshBatchIndex < View.MeshDecalBatches.Num(); ++MeshBatchIndex)
{
const FMeshBatch* Mesh = View.MeshDecalBatches[MeshBatchIndex].Mesh;
const FPrimitiveSceneProxy* PrimitiveSceneProxy = View.MeshDecalBatches[MeshBatchIndex].Proxy;
const uint64 DefaultBatchElementMask = ~0ull;
PassMeshProcessor.AddMeshBatch(*Mesh, DefaultBatchElementMask, PrimitiveSceneProxy);
}
});
캐시된 메시 그리기 명령
캐시된 메시 그리기 명령은 FPrimitiveSceneInfo::CacheMeshDrawCommands
내부에서 기본으로 제공되는 FPrimitiveSceneInfo::AddToScene
입니다. 이 명령을 통한 그리기는 매우 효율적인데, 단순히 적절한 사전 빌드 명령을 매 프레임마다 선택하기만 하면 되기 때문입니다( FDrawCommandRelevancePacket::AddCommandsForMesh
). 캐시된 그리기 명령은 그리기 상태가 매 프레임마다 바뀌지 않고 모든 셰이더 바인딩을 AddToScene 안에서 구성할 수 있을 때만 사용할 수 있습니다.

메시 그리기 명령 캐싱 경로입니다. 주황색 화살표는 매 프레임마다 수행되어야 하는 연산이고, 파란색 화살표는 한 번 수행되어 캐시되는 연산을 나타냅니다.
캐시된 메시 그리기 명령을 지원하려면 다음 조건이 갖춰져야 합니다.
-
패스가
EMeshPass::Type
내 항목을 사용해야 합니다. -
EMeshPassFlags::CachedMeshCommands
플래그가 커스텀 메시 프로세서 등록 시 전달되어야 합니다. -
캐싱 중에 null이 되는
FSceneView
에 의존하지 않고 메시 패스 프로세서가 모든 셰이더 바인딩을 구성할 수 있어야 합니다.
셰이더가 캐시된 메시 그리기 명령으로 프레임별 데이터에 액세스하기 위해서는 씬 단위의 유니폼 버퍼를 바인딩한 다음( FScene::UniformBuffers
참조), 그리기 전 RHIUpdateUniformBuffer
를 사용하여 콘텐츠를 변경합니다.
현재는 FLocalVertexFactory (UStaticMeshComponent)
만 캐시 가능합니다. 다른 모든 버텍스 팩토리는 셰이더 바인딩을 위해 뷰를 구성해야 하기 때문입니다.
캐시 인밸리데이션
메시 패스 프로세서가 AddMeshBatch
에서 읽어 들이는 모든 데이터는 캐시된 메시 그리기 명령의 종속성입니다. 종속성이 변경되면 캐시된 명령을 무효화해야 합니다. 단일 프리미티브의 캐시된 명령은 FPrimitiveSceneInfo::BeginDeferredUpdateStaticMeshes
로 무효화할 수 있습니다. 전체 씬의 캐시된 명령은 Scene->bScenesPrimitivesNeedStaticMeshElementUpdate
를 true
로 설정하여 무효화할 수 있습니다. 이는 매우 무거운 연산으로, 대형 씬에서 멈춤 현상을 유발하므로 게임플레이 중에는 피해야 합니다.
예를 들어, FBasePassMeshProcessor::AddMeshBatch
는 Scene->SkyLight
를 사용하여 스카이라이트 셰이더 순열 선택 여부를 결정합니다. Scene-SkyLight
가 변경되면,, 캐시된 메시 그리기 명령을 무효화해야 합니다.
이 캐싱 체계에서 좋은 퍼포먼스를 달성하려면 지속되는 유니폼 버퍼에 데이터를 넣는 것이 중요합니다. 그리고 캐시된 명령을 자주 무효화하기보다는 해당 버퍼를 업데이트해야 합니다. 예를 들어, 스카이라이트 사례에서는 다른 셰이더 순열을 선택하지 않고 PassUniformBuffer
콘텐츠에 기반한 셰이더 내 동적 분기로 변경할 수 있습니다.
리소스 수명 관리
FMeshDrawCommand
에는 참조하는 리소스의 수명을 유지하는 역할이 없으므로, 캐시된 메시 그리기 명령을 각별히 관리하여 특정 리소스를 참조할 수도 있는 명령을 무효화하도록 해야 합니다. 예를 들어, 캐시된 메시 그리기 명령에 의해 참조되는 유니폼 버퍼를 재생성할 때 렌더링을 위해 캐시된 메시 그리기 명령을 오갈 때 크래시가 발생합니다. 유니폼 버퍼를 업데이트하거나 캐시된 메시 그리기 명령을 무효화해야 합니다.
VALIDATE_UNIFORM_BUFFER_LIFETIME
을 사용하여 캐시된 메시 그리기 명령이 계속 참조하는 유니폼 버퍼가 삭제된 사례를 추적할 수 있습니다.
드로 콜 병합
FMeshDrawCommands
는 RHI 수준 바로 위에서 그리기에 필요한 모든 상태를 캡처하기 때문에, 드로 콜 병합과의 호환성을 손쉽게 비교할 수 있습니다. 현재 구현되는 드로 콜 병합의 유일한 형태는 D3D11 기능 세트에 기반하며, 동일한 셰이더 바인딩이 있는 드로 콜을 인스턴스드 드로로 병합할 수 있습니다. D3D12와 같은 고급 RHI는 더 공격적인 드로 병합을 가능하게 하지만, 아직 구현되지는 않았습니다.
다이내믹 인스턴싱
2개의 드로를 하나의 인스턴스드 드로로 병합하기 위해서는 셰이더 바인딩이 서로 동일해야 합니다( FMeshDrawCommand::MatchesForDynamicInstancing
). 셰이더의 InstanceID 또는 인스턴스 프리퀀시에서 구성된 버텍스 스트림만 다릅니다.
셰이더 파라미터는 다이내믹 인스턴싱을 활성화하기 위해 신중하게 제작되어야 하며, 이는 파라미터 프리퀀시에 따라 다양한 수단을 통해 구현 가능합니다.
패스 타입 | 설명 |
---|---|
패스 파라미터(Pass Parameters) | 패스 내 모든 드로가 병합 가능한 패스 유니폼 버퍼에 배치됩니다. |
FLocalVertexFactory 파라미터(FLocalVertexFactory Parameters) | 같은 UStaticMesh 가 있는 모든 드로가 병합할 수 있는, UStaticMesh 가 소유한 유니폼 버퍼에 배치됩니다. |
머티리얼 인스턴스 파라미터(Material Instance Parameters) | 같은 머티리얼 인스턴스를 사용하는 모든 드로가 병합할 수 있는, 머티리얼 유니폼 버퍼에 배치됩니다. |
라이트 맵 리소스 파라미터(Lightmap Resource Parameters) | 같은 LightmapTexture 를 사용하는 모든 드로가 병합할 수 있는, LightmapResourceCluster 유니폼 버퍼에 배치됩니다. |
프리미티브 파라미터(Primitive Parameters) | PrimitiveID 를 사용하는 셰이더에서 인덱스되는, GPUScene 라는 씬 단위 프리미티브 데이터 버퍼에 배치됩니다. |
GPU 씬
프리미티브별 파라미터를 사용하여 동일한 인스턴스 드로에서 서로 다른 프리미티브를 보유하기 위해, 지원 플랫폼( UseGPUScene
)은 씬 단위 버퍼에 해당 프리미티브를 업로드하고( UpdateGPUScene
) PrimitiveId
로 인덱스합니다. FLocalVertexFactory
의 경우 PrimitiveId는 인스턴스 프리퀀시 버텍스 입력 스트림에서 가져옵니다. 이는 프리미티브 유니폼 버퍼에 직접 액세스하는 대신( Primitive.Member
) 프리미티브 셰이더 파라미터에 액세스하기 위해, 반드시 GetPrimitiveData(Parameters.PrimitiveId).Member
를 사용하는 픽셀 셰이더에 전달되어야 합니다.
인스턴싱 효율성
현재로서는 캐시된 메시 그리기 명령만이 다이내믹 인스턴싱과 병합 가능하며, 이는 다이내믹 인스턴싱을 FLocalVertexFactory
로 제한합니다.
다음과 같은 특정 엣지 케이스로 인해서도 병합이 방지됩니다.
-
작은 라이트맵 텍스처 —
DefaultEngine.ini
에서 MaxLightmapRadius 조정 -
컴포넌트별 버텍스 컬러
-
SpeedTree Wind 노드
레벨에서 다이내믹 인스턴싱 효율성을 조사하려면 r.MeshDrawCommands.LogDynamicInstancingStats 1 콘솔 명령을 사용하여 로그에서 출력을 검사합니다.
뎁스 프리패스(Depth Prepass) 및 섀도 뎁스(Shadow Depth) 패스는 가능한 경우 디폴트 머티리얼의 셰이더를 자주 오버라이드하므로 병합 효율성이 더 높습니다.
메시 그리기 병렬성
대부분의 메시 그리기 작업은 렌더링 스레드의 필수 경로에서 벗어난 태스크에서 이루어집니다. RT 프레임 시작 부분의 InitViews에서는 FParallelMeshDrawCommandPass
가 패스 구성(동적 명령 생성, 분류, 드로 콜 병합)에 대해 패스당 1개의 태스크를 발행합니다. 렌더링 스레드가 프레임을 통해 진행되어 메시 패스에 도착하면(예: RenderBasePass) 시스템의 코어 개수와 디스패치될 드로 수에 따라 드로 디스패치( RHICmdList
를 기록)에 대해 패스별로 다수의 FDrawVisibleMeshCommandsAnyThreadTasks
를 시작합니다.
-
r.MeshDrawCommands.ParallelPassSetup 을 0 으로 설정하면 패스 구성 태스크가 비활성화되고 작업이 렌더링 스레드에서 수행되며, 이는 디버깅에 유용합니다.
-
r.RHICmdBasePassDeferredContexts 를 0 으로 설정하면 베이스 패스 드로 디스패치에 대한 병렬 태스크가 비활성화되고 렌더링 스레드에서 수행됩니다.
이 태스크는 종속성 체인으로 가능한 한 초기에 시작되므로, 한 프레임의 렌더링 스레드와 병렬로 실행될 수 있습니다. 렌더링 스레드는 FSceneRenderer::WaitForTasksClearSnapshotsAndDeleteSceneRenderer
의 프레임 끝에서 이 태스크 완료 시에만 차단합니다.
콘솔 변수
메시 그리기 파이프라인 내에서 문제를 진단할 수 있는 유용한 콘솔 변수가 몇 가지 있습니다.
콘솔 변수 | 설명 |
---|---|
r.MeshDrawCommands.ParallelPassSetup |
메시 그리기 명령 처리 태스크를 토글합니다. 메시 패스 스레드 문제 진단에 유용합니다. |
r.MeshDrawCommands.UseCachedCommands |
비활성화 시 모든 메시 그리기 명령을 강제로 다이내믹으로 만듭니다. 캐시된 메시 그리기 명령 내 묵은 데이터 관련 문제를 진단하는 데 유용합니다. |
r.MeshDrawCommands.DynamicInstancing |
다이내믹 인스턴싱을 토글합니다. 다이내믹 인스턴싱 문제를 진단하는 데 유용합니다. |
r.MeshDrawCommands.LogDynamicInstancingStats |
다이내믹 인스턴싱 효율성을 검사하는 데 유용합니다. |
r.GPUScene.UploadEveryFrame |
GPU 씬이 매 프레임마다 완전히 업데이트되도록 강제합니다. 묵은 GPU 씬 데이터 관련 문제를 진단하는 데 유용합니다. |
r.GPUScene.ValidatePrimitiveBuffer |
GPU 씬을 CPU에 다운로드하고 프리미티브 유니폼 버퍼 대비 콘텐츠의 유효성을 검사합니다. |