언리얼 렌더링 시스템 분석(03) - 렌더링 메커니즘
🌐 원문 링크: 剖析虚幻渲染体系(03)- 渲染机制 (cnblogs.com/timlly)
📅 원문 발행일: 2021-03-28 | ✍️ 저자: Timlly (00 / )💡 시리즈 분류:
Unreal Engine Rendering| 국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료
이 문서에서는 주로 UE가 장면 객체를 하나씩 Draw Call로 구성하는 방법, 프로세스 중에 수행되는 최적화 및 처리, 장면 렌더러가 전체 장면을 렌더링하는 방법에 대해 설명합니다. 관련된 주요 내용은 다음과 같습니다.
모델 그리기 과정.
동적 및 정적 렌더링 경로.
장면 렌더러.
기본 개념 및 최적화 기술이 포함됩니다.
핵심 클래스 및 인터페이스의 코드 분석.
이후 장에서는 이러한 기술을 자세히 다룰 것입니다.
3.1.2 렌더링 메커니즘의 기본
평소와 마찬가지로 이 기사의 주제를 더 잘 이해하기 위해 먼저 이 기사에서 다룰 몇 가지 기본 개념과 유형을 설명하거나 검토합니다.
유형 분석하다
UPrimitiveComponent 기본 구성 요소는 렌더링할 수 있거나 물리적 시뮬레이션을 가질 수 있는 모든 객체의 상위 클래스입니다. CPU 레이어 클리핑을 위한 가장 작은 단위입니다.
FPrimitiveSceneProxy 기본 장면 프록시는 렌더러의 UPrimitiveComponent를 대표하며 렌더링 스레드의 UPrimitiveComponent 상태를 미러링합니다.
FPrimitiveSceneInfo 렌더러의 내부 상태(FRendererModule의 구현을 설명)는 UPrimitiveComponent와 FPrimitiveSceneProxy의 융합과 동일합니다. 렌더러 모듈만 존재하므로 엔진 모듈은 그 존재를 인식할 수 없습니다.
FS씬 렌더링 모듈에서는 UWorld의 대표자입니다. FScene에 추가된 객체만 렌더러에서 인식됩니다. 렌더링 스레드는 FScene의 모든 상태를 소유합니다(게임 스레드는 이를 직접 수정할 수 없습니다).
FScenView FScene 내의 단일 뷰를 설명합니다. 동일한 FScene은 여러 보기를 허용합니다. 즉, 여러 뷰로 장면을 그릴 수도 있고, 동시에 여러 뷰를 그릴 수도 있습니다. 프레임마다 새 뷰 인스턴스가 생성됩니다.
F보기정보 뷰는 렌더러 내부에 표시됩니다. 렌더러 모듈만 존재하며, 엔진 모듈은 보이지 않습니다.
FSceneRenderer 프레임마다 생성되어 프레임 사이에 임시 데이터를 캡슐화합니다. 각각 PC와 모바일의 기본 렌더러를 나타내는 FDeferredShadingSceneRenderer(디퍼드 렌더링 씬 렌더러)와 FMobileSceneRenderer(모바일 씬 렌더러)를 파생시킵니다.
FMeshBatchElement 단일 메쉬 모델의 데이터에는 꼭짓점, 인덱스, 균일 버퍼 및 다양한 식별자와 같은 메쉬 렌더링에 필요한 일부 데이터가 포함됩니다.
FMeshBatch 저장된 FMeshBatchElement 데이터 세트가 있습니다. 이 FMeshBatchElement 데이터 세트에는 동일한 재질과 버텍스 버퍼가 있습니다.
FMeshDraw명령 셰이더 바인딩, 버텍스 데이터, 인덱스 데이터, PSO 캐시 등 Pass Draw Call의 모든 상태와 데이터를 완벽하게 설명합니다.
FMeshPass프로세서 메시 렌더링 패스 프로세서는 장면에서 관심 있는 메시 개체를 처리하고 이를 FMeshBatch 개체에서 하나 이상의 FMeshDrawCommand로 변환하는 일을 담당합니다.
위 개념에서는 게임 스레드에 속하는 객체인 UPrimitiveComponent를 제외하고 나머지는 모두 렌더링 스레드에 속한다는 점에 유의해야 합니다.
3.2 모델 그리기 파이프라인
3.2.1 모델 드로잉 파이프라인 개요
OpenGL 또는 DirectX와 같은 그래픽 API를 학습할 때 비슷한 코드를 접했을 것입니다(예: 삼각형 그리기를 위한 OpenGL 사용).
void DrawTriangle()
{
// 포인트 및 데이터 .
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
unsigned int indices[] = {
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
// 생성(Create)GPU의 리소스 바인딩(Bind).
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
//
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 드로우/렌더(Draw)
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}위의 Hello Triangle은 대략적으로 CPU 리소스 구성, GPU 측 리소스 생성 및 바인딩, 드로잉 인터페이스 호출 등 여러 단계를 거쳤습니다. 간단한 애플리케이션의 경우 또는 그래픽 학습의 경우 그래픽 API를 직접 호출하면 프로세스가 단순화되고 주제로 바로 이동할 수 있습니다. 그러나 복잡한 장면(수백 또는 수천 개의 Draw Call, 수십만 또는 수백만 개의 삼각형)을 초당 수십 프레임으로 렌더링해야 하는 상용 게임 엔진의 경우 간단한 그래픽 API 호출을 직접 사용할 수 없습니다.
상용 게임 엔진은 실제로 그래픽 API를 호출하기 전에 오클루전 컬링, 동적 및 정적 병합, 동적 인스턴스, 상태 및 명령 캐싱, 중간 명령 생성 및 그래픽 API 명령으로 변환 등 많은 작업과 최적화를 수행해야 합니다.
UE4.21 이전에는 위의 목적을 달성하기 위해 메시 렌더링 프로세스(Mesh Draw Pipeline)를 사용했습니다. 개략도는 다음과 같습니다.

UE4.21 및 이전 버전의 메시 그리기 프로세스.
일반적인 프로세스는 렌더링 시 렌더러가 가시성 테스트를 통과한 장면의 모든 PrimitiveSceneProxy 개체를 탐색하고 인터페이스를 사용하여 다양한 FMeshBatch를 수집한 다음 다양한 렌더링 패스에서 이러한 FMeshBatch를 탐색하고 패스에 해당하는 DrawingPolicy를 사용하여 이를 RHI 명령 목록으로 변환한 다음 마지막으로 그래픽 API에 해당하는 명령을 생성하고 실행을 위해 GPU 하드웨어에 제출하는 것입니다.
이를 바탕으로 UE4.22는 렌더링을 더 잘 최적화하기 위해 그리드 렌더링 파이프라인을 비교적 대규모로 재구성했습니다. 비효율적인 DrawingPolicy를 버리고 PassMeshProcessor로 대체했습니다. FMeshBatch와 RHI 명령 사이에 FMeshDrawCommand 개념을 추가하여 보다 제어 가능한 방식으로 그리기 지침을 정렬, 캐시 및 병합했습니다.

UE4.22 재구성 이후 새로운 메시 그리기 프로세스. FMeshDrawCommand 및 FMeshPassProcessor와 같은 새로운 개념과 작업이 추가되었습니다.
이를 수행하는 데는 두 가지 주요 목적이 있습니다.
RTX에 대한 실시간 Ray Tracing을 지원합니다. 레이 트레이싱은 전체 장면의 개체를 탐색하고 전체 장면의 셰이더 리소스를 유지해야 합니다.
GPU 기반 렌더링 파이프라인. 여기에는 GPU 클리핑이 포함되어 있으므로 CPU는 각 프레임의 가시성을 알 수 없지만 매 프레임 전체 장면에 대한 그리기 지침을 생성할 수는 없습니다. 그렇지 않으면 실시간 렌더링을 달성할 수 없습니다.
위의 목적을 달성하기 위해 재구성된 파이프라인은 다음과 같이 반영되는 더 많은 집계 캐싱 조치를 채택합니다.
정적 프리미티브가 장면에 추가된 후 캐시될 때 그리기 지침이 생성됩니다.
RHI 레이어가 가능한 한 많은 전처리를 수행하도록 허용합니다.
셰이더 바인딩 테이블 항목.
그래픽 파이프라인 상태.
정적 메쉬가 매 프레임마다 그리기 지침을 다시 작성하는 것을 방지합니다.
모델 렌더링 파이프라인을 재구성한 후 대부분의 시나리오에서 DepthPass와 BasePass는 그리기 호출 수를 여러 번 줄이고 대규모 명령을 캐시할 수 있습니다.

새 그리드 렌더링 파이프라인과 기존 그리드 렌더링 파이프라인에서 Fortnite 테스트 장면의 렌더링 데이터를 비교합니다. 새로운 메쉬 렌더링 프로세스에서는 Draw Call이 크게 줄어들고 명령 캐시 수도 엄청나다는 것을 알 수 있습니다.
이 섹션의 다음 장에서는 재구성된 메쉬 그리기 프로세스를 분석 개체로 사용합니다.
3.2.2 FPrimitiveSceneProxy에서 FMeshBatch로
이전 기사에서는 FPrimitiveSceneProxy가 렌더링 스레드의 게임 스레드 UPrimitiveComponent의 미러 데이터인 것으로 분석되었습니다. FMeshBatch는 이 섹션에서만 소개되는 새로운 개념입니다. 여기에는 패스를 그리는 데 필요한 모든 정보가 포함되어 있으며 그리드 패스와 FPrimitiveSceneProxy를 분리하므로 FPrimitiveSceneProxy는 어떤 패스가 그려질지 알 수 없습니다.
FMeshBatch 및 FMeshBatchElement의 주요 선언은 다음과 같습니다.
// Engine\Source\Runtime\Engine\Public\MeshBatch.h
// 회 , 저장(Store)FMeshBatch개 의 데이터 .
struct FMeshBatchElement
{
// 의 UniformBuffer, 만약 을(를) 활용하여 GPU Scene, 이면 로 null.
FRHIUniformBuffer* PrimitiveUniformBuffer;
// 의 UniformBufferCPU의 데이터 .
const TUniformBuffer<FPrimitiveUniformShaderParameters>* PrimitiveUniformBufferResource;
// 버퍼 .
const FIndexBuffer* IndexBuffer;
union
{
uint32* InstanceRuns;
class FSplineMeshSceneProxy* SplineMeshSceneProxy;
};
// 을(를) 활용하여 데이터 .
const void* UserData;
void* VertexFactoryUserData;
FRHIVertexBuffer* IndirectArgsBuffer;
uint32 IndirectArgsOffset;
// ID, PrimID_FromPrimitiveSceneInfo(GPU Scene) 및 PrimID_DynamicPrimitiveShaderData(개 의 UniformBuffer)
// 오직 렌더링(Render)수정 .
EPrimitiveIdMode PrimitiveIdMode : PrimID_NumBits + 1;
uint32 DynamicPrimitiveShaderDataIndex : 24;
uint32 FirstIndex;
/** When 0, IndirectArgsBuffer will be used. */
uint32 NumPrimitives;
// Instance개수
uint32 NumInstances;
uint32 BaseVertexIndex;
uint32 MinVertexIndex;
uint32 MaxVertexIndex;
int32 UserIndex;
float MinScreenSize;
float MaxScreenSize;
uint32 InstancedLODIndex : 4;
uint32 InstancedLODRange : 4;
uint32 bUserDataIsColorVertexBuffer : 1;
uint32 bIsSplineProxy : 1;
uint32 bIsInstanceRuns : 1;
// 페치/가져오기(Fetch)개수 .
int32 GetNumPrimitives() const
{
if (bIsInstanceRuns && InstanceRuns)
{
int32 Count = 0;
for (uint32 Run = 0; Run < NumInstances; Run++)
{
Count += NumPrimitives * (InstanceRuns[Run * 2 + 1] - InstanceRuns[Run * 2] + 1);
}
return Count;
}
else
{
return NumPrimitives * NumInstances;
}
}
};
// 회 .
struct FMeshBatch
{
// FMeshBatchElement의 데이터 의 머티리얼(Material) 및 포인트 버퍼 。
// TInlineAllocator<1>테이블 Elements배열 1개 .
TArray<FMeshBatchElement,TInlineAllocator<1> > Elements;
const FVertexFactory* VertexFactory; // 포인트 .
const FMaterialRenderProxy* MaterialRenderProxy; // 렌더링(Render)을(를) 활용하여 의 머티리얼(Material).
uint16 MeshIdInPrimitive; // 의 id, 을(를) 활용하여 의 정렬 .
int8 LODIndex; // LOD, 을(를) 활용하여 LOD의 스무딩 .
uint8 SegmentIndex; // 모델 .
// 클리핑/클램핑(Clipping)플래그 .
uint32 ReverseCulling : 1;
uint32 bDisableBackfaceCulling : 1;
// 렌더링(Render)Pass의 플래그 .
uint32 CastShadow : 1; // 는 그림자(Shadow)Pass 내에서 렌더링(Render).
uint32 bUseForMaterial : 1; // 는 머티리얼(Material)의 Pass 내에서 렌더링(Render).
uint32 bUseForDepthPass : 1; // 는 뎁스 Pass 내에서 렌더링(Render).
uint32 bUseAsOccluder : 1; // 는 차폐/오클루전 .
uint32 bWireframe : 1; // 는 .
uint32 Type : PT_NumBits; // 타입/유형 , PT_TriangleList(기본값(Default)), PT_LineList, ...
uint32 DepthPriorityGroup : SDPG_NumBits; // 뎁스 단계 , SDPG_World (default), SDPG_Foreground
// 플래그 및 데이터
const FLightCacheInterface* LCI;
FHitProxyId BatchHitProxyId;
float TessellationDisablingShadowMapMeshSize;
uint32 bCanApplyViewModeOverrides : 1;
uint32 bUseWireframeSelectionColoring : 1;
uint32 bUseSelectionOutline : 1;
uint32 bSelectable : 1;
uint32 bRequiresPerElementVisibility : 1;
uint32 bDitheredLODTransition : 1;
uint32 bRenderToVirtualTexture : 1;
uint32 RuntimeVirtualTextureMaterialType : RuntimeVirtualTexture::MaterialType_NumBits;
(......)
// 인터페이스 .
bool IsTranslucent(ERHIFeatureLevel::Type InFeatureLevel) const;
bool IsDecal(ERHIFeatureLevel::Type InFeatureLevel) const;
bool IsDualBlend(ERHIFeatureLevel::Type InFeatureLevel) const;
bool UseForHairStrands(ERHIFeatureLevel::Type InFeatureLevel) const;
bool IsMasked(ERHIFeatureLevel::Type InFeatureLevel) const;
int32 GetNumPrimitives() const;
bool HasAnyDrawCalls() const;
};FMeshBatch는 동일한 재질 및 버텍스 팩토리(아래 참조)를 사용하여 FMeshBatchElement 데이터 세트를 기록하고, 후속 메쉬 렌더링 프로세스에서 사용 및 2차 처리를 위해 렌더링 패스별 태그 및 기타 필수 데이터도 저장하는 것을 볼 수 있습니다.

FMeshBatch에는 FMeshBatchElements 세트, 버텍스 팩토리 및 재질 인스턴스가 있습니다. 동일한 FMeshBatch의 모든 FMeshBatchElement는 동일한 재질과 버텍스 버퍼를 공유합니다(버텍스 팩토리로 간주할 수 있음). 그러나 일반적으로(대부분의 경우) FMeshBatch에는 FMeshBatchElement가 하나만 있습니다.
렌더링 시작 시 장면 렌더러 FSceneRenderer는 가시성 테스트 및 제거를 수행하여 가려지거나 숨겨진 개체를 제거합니다. 이 단계가 끝나면 GatherDynamicMeshElements가 호출되어 현재 장면의 모든 FPrimitiveSceneProxy를 수집합니다. 프로세스 코드는 다음과 같습니다.
void FSceneRender::Render(FRHICommandListImmediate& RHICmdList)
{
bool FDeferredShadingSceneRenderer::InitViews((FRHICommandListImmediate& RHICmdList, ...)
{
void FSceneRender::ComputeViewVisibility(FRHICommandListImmediate& RHICmdList, ...)
{
FSceneRender::GatherDynamicMeshElements(Views, Scene, ViewFamily, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer, HasDynamicMeshElementsMasks, HasDynamicEditorMeshElementsMasks, HasViewCustomDataMasks, MeshCollector);
}
}
}그런 다음 FSceneRender::GatherDynamicMeshElements를 입력하여 어떤 로직이 실행되는지 확인하세요.
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp
void FSceneRenderer::GatherDynamicMeshElements(
TArray<FViewInfo>& InViews,
const FScene* InScene,
const FSceneViewFamily& InViewFamily,
FGlobalDynamicIndexBuffer& DynamicIndexBuffer,
FGlobalDynamicVertexBuffer& DynamicVertexBuffer,
FGlobalDynamicReadBuffer& DynamicReadBuffer,
const FPrimitiveViewMasks& HasDynamicMeshElementsMasks,
const FPrimitiveViewMasks& HasDynamicEditorMeshElementsMasks,
const FPrimitiveViewMasks& HasViewCustomDataMasks,
FMeshElementCollector& Collector)
{
(......)
int32 NumPrimitives = InScene->Primitives.Num();
int32 ViewCount = InViews.Num();
{
// 처리(Process)FMeshElementCollector.
Collector.ClearViewMeshArrays();
for (int32 ViewIndex = 0; ViewIndex < ViewCount; ViewIndex++)
{
Collector.AddViewMeshArrays(
&InViews[ViewIndex],
&InViews[ViewIndex].DynamicMeshElements,
&InViews[ViewIndex].SimpleElementCollector,
&InViews[ViewIndex].DynamicPrimitiveShaderData,
InViewFamily.GetFeatureLevel(),
&DynamicIndexBuffer,
&DynamicVertexBuffer,
&DynamicReadBuffer);
}
const bool bIsInstancedStereo = (ViewCount > 0) ? (InViews[0].IsInstancedStereoPass() || InViews[0].bIsMobileMultiViewEnabled) : false;
const EShadingPath ShadingPath = Scene->GetShadingPath();
// 순회(Traverse/Iterate)의 .
for (int32 PrimitiveIndex = 0; PrimitiveIndex < NumPrimitives; ++PrimitiveIndex)
{
const uint8 ViewMask = HasDynamicMeshElementsMasks[PrimitiveIndex];
if (ViewMask != 0) // 오직 처리(Process)차폐/오클루전 또는 의
{
// Don't cull a single eye when drawing a stereo pair
const uint8 ViewMaskFinal = (bIsInstancedStereo) ? ViewMask | 0x3 : ViewMask;
FPrimitiveSceneInfo* PrimitiveSceneInfo = InScene->Primitives[PrimitiveIndex];
const FPrimitiveBounds& Bounds = InScene->PrimitiveBounds[PrimitiveIndex];
// FPrimitiveSceneProxy의 정보 설정(Set)까지 내에서 .
Collector.SetPrimitive(PrimitiveSceneInfo->Proxy, PrimitiveSceneInfo->DefaultDynamicHitProxyId);
// 설정(Set)동적 정의 데이터 .
SetDynamicMeshElementViewCustomData(InViews, HasViewCustomDataMasks, PrimitiveSceneInfo);
// 플래그 DynamicMeshEndIndices의 .
if (PrimitiveIndex > 0)
{
for (int32 ViewIndex = 0; ViewIndex < ViewCount; ViewIndex++)
{
InViews[ViewIndex].DynamicMeshEndIndices[PrimitiveIndex - 1] = Collector.GetMeshBatchCount(ViewIndex);
}
}
// 페치/가져오기(Fetch)동적 의 데이터 .
PrimitiveSceneInfo->Proxy->GetDynamicMeshElements(InViewFamily.Views, InViewFamily, ViewMaskFinal, Collector);
// 플래그 DynamicMeshEndIndices의 .
for (int32 ViewIndex = 0; ViewIndex < ViewCount; ViewIndex++)
{
InViews[ViewIndex].DynamicMeshEndIndices[PrimitiveIndex] = Collector.GetMeshBatchCount(ViewIndex);
}
// 처리(Process)MeshPass의 데이터 및 플래그 .
for (int32 ViewIndex = 0; ViewIndex < ViewCount; ViewIndex++)
{
if (ViewMaskFinal & (1 << ViewIndex))
{
FViewInfo& View = InViews[ViewIndex];
const bool bAddLightmapDensityCommands = View.Family->EngineShowFlags.LightMapDensity && AllowDebugViewmodes();
const FPrimitiveViewRelevance& ViewRelevance = View.PrimitiveViewRelevanceMap[PrimitiveIndex];
const int32 LastNumDynamicMeshElements = View.DynamicMeshElementsPassRelevance.Num();
View.DynamicMeshElementsPassRelevance.SetNum(View.DynamicMeshElements.Num());
for (int32 ElementIndex = LastNumDynamicMeshElements; ElementIndex < View.DynamicMeshElements.Num(); ++ElementIndex)
{
const FMeshBatchAndRelevance& MeshBatch = View.DynamicMeshElements[ElementIndex];
FMeshPassMask& PassRelevance = View.DynamicMeshElementsPassRelevance[ElementIndex];
// 계산/산출(Calculate)현재(Current)의 MeshBatchMeshPass을(를) 활용하여 , 로부터 까지 view의 MeshPass의 배열 내에서 .
ComputeDynamicMeshRelevance(ShadingPath, bAddLightmapDensityCommands, ViewRelevance, MeshBatch, View, PassRelevance, PrimitiveSceneInfo, Bounds);
}
}
}
}
}
}
(......)
// 실행(Execute).
MeshCollector.ProcessTasks();
}위 코드에서 볼 수 있듯이, 동적 프리미티브 데이터를 수집할 때 장면에 표시되는 모든 FPrimitiveSceneProxy의 그리드 데이터를 수집하기 위해 각 FSceneRenderer에 대해 FMeshElementCollector 객체가 생성됩니다. 중간에는 주요 코드인 PrimitiveSceneInfo->Proxy->GetDynamicMeshElements()가 있는데, 이는 각 기본 개체에 표시 기본 요소를 렌더러(수집기)에 추가할 수 있는 기회를 제공합니다. 이 함수를 확장하여 살펴보겠습니다(기본 클래스 FPrimitiveSceneProxy의 인터페이스는 빈 함수 본체이고 아무런 작업도 수행되지 않으므로 이 수집 작업은 특정 하위 클래스에 의해 구현됩니다. 여기서는 하위 클래스 FSkeletalMeshSceneProxy의 구현이 예로 사용됩니다):
// Engine\Source\Runtime\Engine\Private\SkeletalMesh.cpp
void FSkeletalMeshSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
GetMeshElementsConditionallySelectable(Views, ViewFamily, true, VisibilityMap, Collector);
}
void FSkeletalMeshSceneProxy::GetMeshElementsConditionallySelectable(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, bool bInSelectable, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
(......)
const int32 LODIndex = MeshObject->GetLOD();
const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];
if( LODSections.Num() > 0 && LODIndex >= SkeletalMeshRenderData->CurrentFirstLODIdx )
{
const FLODSectionElements& LODSection = LODSections[LODIndex]
// 에 따라 LOD순회(Traverse/Iterate)의 모델 , 추가(Add)까지 collector 내에서 .
for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
{
const FSkelMeshRenderSection& Section = Iter.GetSection();
const int32 SectionIndex = Iter.GetSectionElementIndex();
const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();
bool bSectionSelected = false;
if (MeshObject->IsMaterialHidden(LODIndex, SectionElementInfo.UseMaterialIndex) || Section.bDisabled)
{
continue;
}
// LODIndex 및 SectionIndex추가(Add)까지 Collector 내에서 .
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, bInSelectable, Collector);
}
}
(......)
}
void FSkeletalMeshSceneProxy::GetDynamicElementsSection(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, bool bSectionSelected, const FSectionElementInfo& SectionElementInfo, bool bInSelectable, FMeshElementCollector& Collector ) const
{
const FSkelMeshRenderSection& Section = LODData.RenderSections[SectionIndex];
const bool bIsSelected = false;
const bool bIsWireframe = ViewFamily.EngineShowFlags.Wireframe;
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
if (VisibilityMap & (1 << ViewIndex))
{
const FSceneView* View = Views[ViewIndex];
// 로부터 Colloctor할당(Allocate)개 FMeshBatch.
FMeshBatch& Mesh = Collector.AllocateMesh();
// 생성(Create)의 회 객체/오브젝트 (FMeshBatchElement인스턴스 ).
CreateBaseMeshBatch(View, LODData, LODIndex, SectionIndex, SectionElementInfo, Mesh);
if(!Mesh.VertexFactory)
{
// hide this part
continue;
}
Mesh.bWireframe |= bForceWireframe;
Mesh.Type = PT_TriangleList;
Mesh.bSelectable = bInSelectable;
// 설정(Set)개 FMeshBatchElement객체/오브젝트 .
FMeshBatchElement& BatchElement = Mesh.Elements[0];
const bool bRequiresAdjacencyInformation = RequiresAdjacencyInformation( SectionElementInfo.Material, Mesh.VertexFactory->GetType(), ViewFamily.GetFeatureLevel() );
if ( bRequiresAdjacencyInformation )
{
check(LODData.AdjacencyMultiSizeIndexContainer.IsIndexBufferValid() );
BatchElement.IndexBuffer = LODData.AdjacencyMultiSizeIndexContainer.GetIndexBuffer();
Mesh.Type = PT_12_ControlPointPatchList;
BatchElement.FirstIndex *= 4;
}
BatchElement.MinVertexIndex = Section.BaseVertexIndex;
Mesh.ReverseCulling = IsLocalToWorldDeterminantNegative();
Mesh.CastShadow = SectionElementInfo.bEnableShadowCasting;
Mesh.bCanApplyViewModeOverrides = true;
Mesh.bUseWireframeSelectionColoring = bIsSelected;
(......)
if ( ensureMsgf(Mesh.MaterialRenderProxy, TEXT("GetDynamicElementsSection with invalid MaterialRenderProxy. Owner:%s LODIndex:%d UseMaterialIndex:%d"), *GetOwnerName().ToString(), LODIndex, SectionElementInfo.UseMaterialIndex) &&
ensureMsgf(Mesh.MaterialRenderProxy->GetMaterial(FeatureLevel), TEXT("GetDynamicElementsSection with invalid FMaterial. Owner:%s LODIndex:%d UseMaterialIndex:%d"), *GetOwnerName().ToString(), LODIndex, SectionElementInfo.UseMaterialIndex) )
{
// FMeshBatch추가(Add)까지 내에서 .
Collector.AddMesh(ViewIndex, Mesh);
}
(......)
}
}
}FSkeletalMeshSceneProxy는 서로 다른 LOD 인덱스에 따라 각 섹션 메시에 FMeshBatch를 추가하고 각 FMeshBatch에는 FMeshBatchElement 인스턴스가 하나만 있는 것을 볼 수 있습니다. 게다가 FSceneRender::GatherDynamicMeshElements의 로직에는 ComputeDynamicMeshRelevance라는 핵심 문장이 있습니다. 그 기능은 현재 MeshBatch가 참조될 MeshPass를 계산한 다음 이를 뷰의 해당 MeshPass 개수에 추가하는 것입니다.
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp
void ComputeDynamicMeshRelevance(EShadingPath ShadingPath, bool bAddLightmapDensityCommands, const FPrimitiveViewRelevance& ViewRelevance, const FMeshBatchAndRelevance& MeshBatch, FViewInfo& View, FMeshPassMask& PassMask, FPrimitiveSceneInfo* PrimitiveSceneInfo, const FPrimitiveBounds& Bounds)
{
const int32 NumElements = MeshBatch.Mesh->Elements.Num();
// 뎁스 Pass/Pass.
if (ViewRelevance.bDrawRelevance && (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth || ViewRelevance.bRenderInDepthPass))
{
PassMask.Set(EMeshPass::DepthPass);
View.NumVisibleDynamicMeshElements[EMeshPass::DepthPass] += NumElements;
if (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth)
{
PassMask.Set(EMeshPass::BasePass);
View.NumVisibleDynamicMeshElements[EMeshPass::BasePass] += NumElements;
if (ShadingPath == EShadingPath::Mobile)
{
PassMask.Set(EMeshPass::MobileBasePassCSM);
View.NumVisibleDynamicMeshElements[EMeshPass::MobileBasePassCSM] += NumElements;
}
if (ViewRelevance.bRenderCustomDepth)
{
PassMask.Set(EMeshPass::CustomDepth);
View.NumVisibleDynamicMeshElements[EMeshPass::CustomDepth] += NumElements;
}
if (bAddLightmapDensityCommands)
{
PassMask.Set(EMeshPass::LightmapDensity);
View.NumVisibleDynamicMeshElements[EMeshPass::LightmapDensity] += NumElements;
}
if (ViewRelevance.bVelocityRelevance)
{
PassMask.Set(EMeshPass::Velocity);
View.NumVisibleDynamicMeshElements[EMeshPass::Velocity] += NumElements;
}
if (ViewRelevance.bOutputsTranslucentVelocity)
{
PassMask.Set(EMeshPass::TranslucentVelocity);
View.NumVisibleDynamicMeshElements[EMeshPass::TranslucentVelocity] += NumElements;
}
if (ViewRelevance.bUsesSingleLayerWaterMaterial)
{
PassMask.Set(EMeshPass::SingleLayerWaterPass);
View.NumVisibleDynamicMeshElements[EMeshPass::SingleLayerWaterPass] += NumElements;
}
}
}
// 반투명(Translucent) 및 Pass.
if (ViewRelevance.HasTranslucency()
&& !ViewRelevance.bEditorPrimitiveRelevance
&& ViewRelevance.bRenderInMainPass)
{
if (View.Family->AllowTranslucencyAfterDOF())
{
if (ViewRelevance.bNormalTranslucency)
{
PassMask.Set(EMeshPass::TranslucencyStandard);
View.NumVisibleDynamicMeshElements[EMeshPass::TranslucencyStandard] += NumElements;
}
if (ViewRelevance.bSeparateTranslucency)
{
PassMask.Set(EMeshPass::TranslucencyAfterDOF);
View.NumVisibleDynamicMeshElements[EMeshPass::TranslucencyAfterDOF] += NumElements;
}
if (ViewRelevance.bSeparateTranslucencyModulate)
{
PassMask.Set(EMeshPass::TranslucencyAfterDOFModulate);
View.NumVisibleDynamicMeshElements[EMeshPass::TranslucencyAfterDOFModulate] += NumElements;
}
}
else
{
PassMask.Set(EMeshPass::TranslucencyAll);
View.NumVisibleDynamicMeshElements[EMeshPass::TranslucencyAll] += NumElements;
}
if (ViewRelevance.bDistortion)
{
PassMask.Set(EMeshPass::Distortion);
View.NumVisibleDynamicMeshElements[EMeshPass::Distortion] += NumElements;
}
if (ShadingPath == EShadingPath::Mobile && View.bIsSceneCapture)
{
PassMask.Set(EMeshPass::MobileInverseOpacity);
View.NumVisibleDynamicMeshElements[EMeshPass::MobileInverseOpacity] += NumElements;
}
}
(......)
}위 코드에는 수집기 FMeshElementCollector도 포함됩니다. 그 기능은 지정된 뷰에서 표시되는 모든 MeshBatch 정보를 수집하는 것입니다. 다음과 같이 선언됩니다.
// Engine\Source\Runtime\Engine\Public\SceneManagement.h
class FMeshElementCollector
{
public:
// 드로우/렌더(Draw)포인트 , , , 의 인터페이스 .
FPrimitiveDrawInterface* GetPDI(int32 ViewIndex)
{
return SimpleElementCollectors[ViewIndex];
}
// 할당(Allocate)개 FMeshBatch객체/오브젝트 .
FMeshBatch& AllocateMesh()
{
const int32 Index = MeshBatchStorage.Add(1);
return MeshBatchStorage[Index];
}
// 추가(Add)MeshBatch까지 내에서 . 추가(Add)초기화(Initialize) 및 설정(Set)데이터 , 추가(Add)까지 MeshBatches리스트 내에서 .
void AddMesh(int32 ViewIndex, FMeshBatch& MeshBatch);
// 데이터 페치/가져오기(Fetch)인터페이스 .
FGlobalDynamicIndexBuffer& GetDynamicIndexBuffer();
FGlobalDynamicVertexBuffer& GetDynamicVertexBuffer();
FGlobalDynamicReadBuffer& GetDynamicReadBuffer();
uint32 GetMeshBatchCount(uint32 ViewIndex) const;
uint32 GetMeshElementCount(uint32 ViewIndex) const;
ERHIFeatureLevel::Type GetFeatureLevel() const;
void RegisterOneFrameMaterialProxy(FMaterialRenderProxy* Proxy);
template<typename T, typename... ARGS>
T& AllocateOneFrameResource(ARGS&&... Args);
bool ShouldUseTasks() const;
// 인터페이스 .
void AddTask(TFunction<void()>&& Task)
{
ParallelTasks.Add(new (FMemStack::Get()) TFunction<void()>(MoveTemp(Task)));
}
void AddTask(const TFunction<void()>& Task)
{
ParallelTasks.Add(new (FMemStack::Get()) TFunction<void()>(Task));
}
void ProcessTasks();
protected:
FMeshElementCollector(ERHIFeatureLevel::Type InFeatureLevel);
// 설정(Set)FPrimitiveSceneProxy의 데이터 .
void SetPrimitive(const FPrimitiveSceneProxy* InPrimitiveSceneProxy, FHitProxyId DefaultHitProxyId)
{
check(InPrimitiveSceneProxy);
PrimitiveSceneProxy = InPrimitiveSceneProxy;
for (int32 ViewIndex = 0; ViewIndex < SimpleElementCollectors.Num(); ViewIndex++)
{
SimpleElementCollectors[ViewIndex]->HitProxyId = DefaultHitProxyId;
SimpleElementCollectors[ViewIndex]->PrimitiveMeshId = 0;
}
for (int32 ViewIndex = 0; ViewIndex < MeshIdInPrimitivePerView.Num(); ++ViewIndex)
{
MeshIdInPrimitivePerView[ViewIndex] = 0;
}
}
void ClearViewMeshArrays();
// View추가(Add)Mesh.
void AddViewMeshArrays(
FSceneView* InView,
TArray<FMeshBatchAndRelevance,SceneRenderingAllocator>* ViewMeshes,
FSimpleElementCollector* ViewSimpleElementCollector,
TArray<FPrimitiveUniformShaderParameters>* InDynamicPrimitiveShaderData,
ERHIFeatureLevel::Type InFeatureLevel,
FGlobalDynamicIndexBuffer* InDynamicIndexBuffer,
FGlobalDynamicVertexBuffer* InDynamicVertexBuffer,
FGlobalDynamicReadBuffer* InDynamicReadBuffer);
TChunkedArray<FMeshBatch> MeshBatchStorage; // 저장(Save)할당(Allocate)의 FMeshBatch인스턴스 .
TArray<TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>*, TInlineAllocator<2> > MeshBatches; // 렌더링(Render)의 FMeshBatch인스턴스
TArray<int32, TInlineAllocator<2> > NumMeshBatchElementsPerView; // 개 view까지 의 MeshBatchElement개수 .
TArray<FSimpleElementCollector*, TInlineAllocator<2> > SimpleElementCollectors; // 포인트 의 .
TArray<FSceneView*, TInlineAllocator<2> > Views; // 의 FSceneView인스턴스 .
TArray<uint16, TInlineAllocator<2> > MeshIdInPrimitivePerView; // Current Mesh Id In Primitive per view
TArray<TArray<FPrimitiveUniformShaderParameters>*, TInlineAllocator<2> > DynamicPrimitiveShaderDataPerView; // view의 동적 데이터 , 을(를) 활용하여 갱신(Update)까지 GPU Scene 내에서 .
TArray<FMaterialRenderProxy*, SceneRenderingAllocator> TemporaryProxies;
TArray<FOneFrameResource*, SceneRenderingAllocator> OneFrameResources;
const FPrimitiveSceneProxy* PrimitiveSceneProxy; // 현재(Current)의 PrimitiveSceneProxy
// 글로벌 동적 버퍼 .
FGlobalDynamicIndexBuffer* DynamicIndexBuffer;
FGlobalDynamicVertexBuffer* DynamicVertexBuffer;
FGlobalDynamicReadBuffer* DynamicReadBuffer;
ERHIFeatureLevel::Type FeatureLevel;
const bool bUseAsyncTasks; // 는 을(를) 활용하여 비동기 .
TArray<TFunction<void()>*, SceneRenderingAllocator> ParallelTasks; // 동적 데이터 처리(Process)의 리스트 .
};FMeshElementCollector와 FSceneRenderer는 일대일 대응을 가지며 각 FSceneRenderer에는 수집기가 있습니다. 컬렉터는 뷰에 해당하는 가시적 기본 요소 목록을 수집한 후 일반적으로 후속 프로세스를 위한 충분한 준비를 수집하고 준비하기 위해 렌더링해야 하는 FMeshBatch 목록 세트와 해당 관리 데이터 및 상태를 갖습니다.
또한 FMeshElementCollector는 그리드 데이터를 수집한 후 다중 스레드 병렬 처리의 동기화를 달성하기 위해 처리해야 하는 작업 목록을 지정할 수도 있습니다.
3.2.3 FMeshBatch에서 FMeshDrawCommand로
이전 섹션에서는 동적 MeshElements 수집에 대해 설명했습니다. 실제로 FMeshPassProcessor를 생성하기 위해 SetupMeshPass가 호출됩니다.
void FSceneRender::Render(FRHICommandListImmediate& RHICmdList)
{
bool FDeferredShadingSceneRenderer::InitViews((FRHICommandListImmediate& RHICmdList, ...)
{
void FSceneRender::ComputeViewVisibility(FRHICommandListImmediate& RHICmdList, ...)
{
// 동적 MeshElement
FSceneRender::GatherDynamicMeshElements(Views, Scene, ViewFamily, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer, HasDynamicMeshElementsMasks, HasDynamicEditorMeshElementsMasks, HasViewCustomDataMasks, MeshCollector);
// 처리(Process)view의 FMeshPassProcessor.
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
FViewInfo& View = Views[ViewIndex];
if (!View.ShouldRenderView())
{
continue;
}
// 처리(Process)view의 FMeshPassProcessor.
FViewCommands& ViewCommands = ViewCommandsPerView[ViewIndex];
SetupMeshPass(View, BasePassDepthStencilAccess, ViewCommands);
}
}
}
}FSceneRenderer::SetupMeshPass의 논리와 설명은 다음과 같습니다:void FSceneRenderer::SetupMeshPass(FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewCommands& ViewCommands)
{
const EShadingPath ShadingPath = Scene->GetShadingPath();
// 순회(Traverse/Iterate)EMeshPass정의 의 Pass。
for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
{
const EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;
if ((FPassProcessorManager::GetPassFlags(ShadingPath, PassType) & EMeshPassFlags::MainView) != EMeshPassFlags::None)
{
(......)
// 생성(Create)FMeshPassProcessor
PassProcessorCreateFunction CreateFunction = FPassProcessorManager::GetCreateFunction(ShadingPath, PassType);
FMeshPassProcessor* MeshPassProcessor = CreateFunction(Scene, &View, nullptr);
// 페치/가져오기(Fetch)Pass의 FParallelMeshDrawCommandPass객체/오브젝트 。
FParallelMeshDrawCommandPass& Pass = View.ParallelMeshDrawCommandPasses[PassIndex];
if (ShouldDumpMeshDrawCommandInstancingStats())
{
Pass.SetDumpInstancingStats(GetMeshPassName(PassType));
}
// 병렬 처리(Process)Pass의 처리(Process),생성(Create)Pass의 드로우/렌더(Draw)。
Pass.DispatchPassSetup(
Scene,
View,
PassType,
BasePassDepthStencilAccess,
MeshPassProcessor,
View.DynamicMeshElements,
&View.DynamicMeshElementsPassRelevance,
View.NumVisibleDynamicMeshElements[PassType],
ViewCommands.DynamicMeshCommandBuildRequests[PassType],
ViewCommands.NumDynamicMeshCommandBuildRequestElements[PassType],
ViewCommands.MeshCommands[PassIndex]);
}
}
}위 코드에 포함된 EMeshPass 열거형은 다음과 같이 정의됩니다.
// Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.h
namespace EMeshPass
{
enum Type
{
DepthPass, // 뎁스
BasePass, // /
SkyPass, //
SingleLayerWaterPass, // 레벨
CSMShadowDepth, // 단계 그림자(Shadow)뎁스
Distortion, //
Velocity, //
// 반투명(Translucent)의 Pass
TranslucentVelocity,
TranslucencyStandard,
TranslucencyAfterDOF,
TranslucencyAfterDOFModulate,
TranslucencyAll,
LightmapDensity, // 라이팅
DebugViewMode, // 뷰(View)
CustomDepth, // 정의 뎁스
MobileBasePassCSM,
MobileInverseOpacity,
VirtualTexture, // 텍스처(Texture)
// 의 Pass
#if WITH_EDITOR
HitProxy,
HitProxyOpaqueOnly,
EditorSelection,
#endif
Num,
NumBits = 5,
};
}UE는 미리 그려야 할 모든 패스를 나열하고 SetupMeshPass 단계에서 사용해야 하는 패스에 대해 DrawCommand를 병렬로 생성하는 것을 볼 수 있습니다. 그 중 FParallelMeshDrawCommandPass::DispatchPassSetup의 주요 로직과 분석은 다음과 같습니다.
// Engine\Source\Runtime\Renderer\Private\MeshDrawCommands.cpp
void FParallelMeshDrawCommandPass::DispatchPassSetup(
FScene* Scene,
const FViewInfo& View,
EMeshPass::Type PassType,
FExclusiveDepthStencil::Type BasePassDepthStencilAccess,
FMeshPassProcessor* MeshPassProcessor,
const TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>& DynamicMeshElements,
const TArray<FMeshPassMask, SceneRenderingAllocator>* DynamicMeshElementsPassRelevance,
int32 NumDynamicMeshElements,
TArray<const FStaticMeshBatch*, SceneRenderingAllocator>& InOutDynamicMeshCommandBuildRequests,
int32 NumDynamicMeshCommandBuildRequestElements,
FMeshCommandOneFrameArray& InOutMeshDrawCommands,
FMeshPassProcessor* MobileBasePassCSMMeshPassProcessor,
FMeshCommandOneFrameArray* InOutMobileBasePassCSMMeshDrawCommands
)
{
MaxNumDraws = InOutMeshDrawCommands.Num() + NumDynamicMeshElements + NumDynamicMeshCommandBuildRequestElements;
// 설정(Set)TaskContext의 데이터 ,MeshCommand의 데이터 。
TaskContext.MeshPassProcessor = MeshPassProcessor;
TaskContext.MobileBasePassCSMMeshPassProcessor = MobileBasePassCSMMeshPassProcessor;
TaskContext.DynamicMeshElements = &DynamicMeshElements;
TaskContext.DynamicMeshElementsPassRelevance = DynamicMeshElementsPassRelevance;
TaskContext.View = &View;
TaskContext.ShadingPath = Scene->GetShadingPath();
TaskContext.ShaderPlatform = Scene->GetShaderPlatform();
TaskContext.PassType = PassType;
TaskContext.bUseGPUScene = UseGPUScene(GMaxRHIShaderPlatform, View.GetFeatureLevel());
TaskContext.bDynamicInstancing = IsDynamicInstancingEnabled(View.GetFeatureLevel());
TaskContext.bReverseCulling = View.bReverseCulling;
TaskContext.bRenderSceneTwoSided = View.bRenderSceneTwoSided;
TaskContext.BasePassDepthStencilAccess = BasePassDepthStencilAccess;
TaskContext.DefaultBasePassDepthStencilAccess = Scene->DefaultBasePassDepthStencilAccess;
TaskContext.NumDynamicMeshElements = NumDynamicMeshElements;
TaskContext.NumDynamicMeshCommandBuildRequestElements = NumDynamicMeshCommandBuildRequestElements;
// Only apply instancing for ISR to main view passes
const bool bIsMainViewPass = PassType != EMeshPass::Num && (FPassProcessorManager::GetPassFlags(TaskContext.ShadingPath, TaskContext.PassType) & EMeshPassFlags::MainView) != EMeshPassFlags::None;
TaskContext.InstanceFactor = (bIsMainViewPass && View.IsInstancedStereoPass()) ? 2 : 1;
// 설정(Set)view의 반투명(Translucent)정렬
TaskContext.TranslucencyPass = ETranslucencyPass::TPT_MAX;
TaskContext.TranslucentSortPolicy = View.TranslucentSortPolicy;
TaskContext.TranslucentSortAxis = View.TranslucentSortAxis;
TaskContext.ViewOrigin = View.ViewMatrices.GetViewOrigin();
TaskContext.ViewMatrix = View.ViewMatrices.GetViewMatrix();
TaskContext.PrimitiveBounds = &Scene->PrimitiveBounds;
switch (PassType)
{
case EMeshPass::TranslucencyStandard: TaskContext.TranslucencyPass = ETranslucencyPass::TPT_StandardTranslucency; break;
case EMeshPass::TranslucencyAfterDOF: TaskContext.TranslucencyPass = ETranslucencyPass::TPT_TranslucencyAfterDOF; break;
case EMeshPass::TranslucencyAfterDOFModulate: TaskContext.TranslucencyPass = ETranslucencyPass::TPT_TranslucencyAfterDOFModulate; break;
case EMeshPass::TranslucencyAll: TaskContext.TranslucencyPass = ETranslucencyPass::TPT_AllTranslucency; break;
case EMeshPass::MobileInverseOpacity: TaskContext.TranslucencyPass = ETranslucencyPass::TPT_StandardTranslucency; break;
}
// 커맨드 리스트
FMemory::Memswap(&TaskContext.MeshDrawCommands, &InOutMeshDrawCommands, sizeof(InOutMeshDrawCommands));
FMemory::Memswap(&TaskContext.DynamicMeshCommandBuildRequests, &InOutDynamicMeshCommandBuildRequests, sizeof(InOutDynamicMeshCommandBuildRequests));
if (TaskContext.ShadingPath == EShadingPath::Mobile && TaskContext.PassType == EMeshPass::BasePass)
{
FMemory::Memswap(&TaskContext.MobileBasePassCSMMeshDrawCommands, InOutMobileBasePassCSMMeshDrawCommands, sizeof(*InOutMobileBasePassCSMMeshDrawCommands));
}
else
{
check(MobileBasePassCSMMeshPassProcessor == nullptr && InOutMobileBasePassCSMMeshDrawCommands == nullptr);
}
if (MaxNumDraws > 0)
{
// 에 따라 드로우/렌더(Draw)개수 (MaxNumDraws)렌더링(Render)스레드(Thread)할당(Allocate)리소스 .
bPrimitiveIdBufferDataOwnedByRHIThread = false;
TaskContext.PrimitiveIdBufferDataSize = TaskContext.InstanceFactor * MaxNumDraws * sizeof(int32);
TaskContext.PrimitiveIdBufferData = FMemory::Malloc(TaskContext.PrimitiveIdBufferDataSize);
PrimitiveIdVertexBufferPoolEntry = GPrimitiveIdVertexBufferPool.Allocate(TaskContext.PrimitiveIdBufferDataSize);
TaskContext.MeshDrawCommands.Reserve(MaxNumDraws);
TaskContext.TempVisibleMeshDrawCommands.Reserve(MaxNumDraws);
const bool bExecuteInParallel = FApp::ShouldUseThreadingForPerformance()
&& CVarMeshDrawCommandsParallelPassSetup.GetValueOnRenderThread() > 0
&& GRenderingThread; // Rendering thread is required to safely use rendering resources in parallel.
// 만약 는 병렬 , 생성(Create)병렬 인스턴스 추가(Add)TaskGraph실행(Execute).
if (bExecuteInParallel)
{
FGraphEventArray DependentGraphEvents;
DependentGraphEvents.Add(TGraphTask<FMeshDrawCommandPassSetupTask>::CreateTask(nullptr, ENamedThreads::GetRenderThread()).ConstructAndDispatchWhenReady(TaskContext));
TaskEventRef = TGraphTask<FMeshDrawCommandInitResourcesTask>::CreateTask(&DependentGraphEvents, ENamedThreads::GetRenderThread()).ConstructAndDispatchWhenReady(TaskContext);
}
else
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_MeshPassSetupImmediate);
FMeshDrawCommandPassSetupTask Task(TaskContext);
Task.AnyThreadTask();
FMeshDrawCommandInitResourcesTask DependentTask(TaskContext);
DependentTask.AnyThreadTask();
}
}
}위 코드에는 FMeshPassProcessor, FMeshDrawCommandPassSetupTaskContext, FMeshDrawCommandPassSetupTask, FMeshDrawCommandInitResourcesTask와 같은 몇 가지 주요 개념이 포함됩니다. 후자의 세 가지 개념에 대한 정의와 분석은 다음과 같다.
// Engine\Source\Runtime\Renderer\Private\MeshDrawCommands.h
// 병렬 드로우/렌더(Draw)채널/패스(Pass)설정(Set)(FMeshDrawCommandPassSetupTask)의 .
class FMeshDrawCommandPassSetupTaskContext
{
public:
// view의 데이터 .
const FViewInfo* View;
EShadingPath ShadingPath;
EShaderPlatform ShaderPlatform;
EMeshPass::Type PassType;
bool bUseGPUScene;
bool bDynamicInstancing;
bool bReverseCulling;
bool bRenderSceneTwoSided;
FExclusiveDepthStencil::Type BasePassDepthStencilAccess;
FExclusiveDepthStencil::Type DefaultBasePassDepthStencilAccess;
// 채널/패스(Pass)처리(Process)(Mesh pass processor).
FMeshPassProcessor* MeshPassProcessor;
FMeshPassProcessor* MobileBasePassCSMMeshPassProcessor;
const TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>* DynamicMeshElements;
const TArray<FMeshPassMask, SceneRenderingAllocator>* DynamicMeshElementsPassRelevance;
// 의 데이터 .
int32 InstanceFactor;
int32 NumDynamicMeshElements;
int32 NumDynamicMeshCommandBuildRequestElements;
FMeshCommandOneFrameArray MeshDrawCommands;
FMeshCommandOneFrameArray MobileBasePassCSMMeshDrawCommands;
TArray<const FStaticMeshBatch*, SceneRenderingAllocator> DynamicMeshCommandBuildRequests;
TArray<const FStaticMeshBatch*, SceneRenderingAllocator> MobileBasePassCSMDynamicMeshCommandBuildRequests;
FDynamicMeshDrawCommandStorage MeshDrawCommandStorage;
FGraphicsMinimalPipelineStateSet MinimalPipelineStatePassSet;
bool NeedsShaderInitialisation;
// 렌더링(Render)스레드(Thread)할당(Allocate)의 리소스 .
void* PrimitiveIdBufferData;
int32 PrimitiveIdBufferDataSize;
FMeshCommandOneFrameArray TempVisibleMeshDrawCommands;
// 반투명(Translucent)정렬 .
ETranslucencyPass::Type TranslucencyPass;
ETranslucentSortPolicy::Type TranslucentSortPolicy;
FVector TranslucentSortAxis;
FVector ViewOrigin;
FMatrix ViewMatrix;
const TArray<struct FPrimitiveBounds>* PrimitiveBounds;
// For logging instancing stats.
int32 VisibleMeshDrawCommandsNum;
int32 NewPassVisibleMeshDrawCommandsNum;
int32 MaxInstances;
};
// Engine\Source\Runtime\Renderer\Private\MeshDrawCommands.cpp
// 변환(Transform/Convert)EMeshPass 내에서 의 개 FMeshBatch까지 FMeshDrawCommand. FMeshDrawCommandPassSetupTask을(를) 활용하여 까지 .
void GenerateDynamicMeshDrawCommands(
const FViewInfo& View,
EShadingPath ShadingPath,
EMeshPass::Type PassType,
FMeshPassProcessor* PassMeshProcessor,
const TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>& DynamicMeshElements,
const TArray<FMeshPassMask, SceneRenderingAllocator>* DynamicMeshElementsPassRelevance,
int32 MaxNumDynamicMeshElements,
const TArray<const FStaticMeshBatch*, SceneRenderingAllocator>& DynamicMeshCommandBuildRequests,
int32 MaxNumBuildRequestElements,
FMeshCommandOneFrameArray& VisibleCommands,
FDynamicMeshDrawCommandStorage& MeshDrawCommandStorage,
FGraphicsMinimalPipelineStateSet& MinimalPipelineStatePassSet,
bool& NeedsShaderInitialisation
)
{
(......)
// FDynamicPassMeshDrawListContext인스턴스 , 을(를) 활용하여 PassMeshProcessor의 드로우/렌더(Draw).
FDynamicPassMeshDrawListContext DynamicPassMeshDrawListContext(
MeshDrawCommandStorage,
VisibleCommands,
MinimalPipelineStatePassSet,
NeedsShaderInitialisation
);
PassMeshProcessor->SetDrawListContext(&DynamicPassMeshDrawListContext);
// 처리(Process)동적 회 .
{
const int32 NumCommandsBefore = VisibleCommands.Num();
const int32 NumDynamicMeshBatches = DynamicMeshElements.Num();
// 순회(Traverse/Iterate)의 동적 회 .
for (int32 MeshIndex = 0; MeshIndex < NumDynamicMeshBatches; MeshIndex++)
{
if (!DynamicMeshElementsPassRelevance || (*DynamicMeshElementsPassRelevance)[MeshIndex].Get(PassType))
{
const FMeshBatchAndRelevance& MeshAndRelevance = DynamicMeshElements[MeshIndex];
check(!MeshAndRelevance.Mesh->bRequiresPerElementVisibility);
const uint64 BatchElementMask = ~0ull;
// FMeshBatch추가(Add)까지 PassMeshProcessor처리(Process).
PassMeshProcessor->AddMeshBatch(*MeshAndRelevance.Mesh, BatchElementMask, MeshAndRelevance.PrimitiveSceneProxy);
}
}
(......)
}
// 처리(Process)정적 회 .
{
const int32 NumCommandsBefore = VisibleCommands.Num();
const int32 NumStaticMeshBatches = DynamicMeshCommandBuildRequests.Num();
for (int32 MeshIndex = 0; MeshIndex < NumStaticMeshBatches; MeshIndex++)
{
const FStaticMeshBatch* StaticMeshBatch = DynamicMeshCommandBuildRequests[MeshIndex];
const uint64 BatchElementMask = StaticMeshBatch->bRequiresPerElementVisibility ? View.StaticMeshBatchVisibility[StaticMeshBatch->BatchVisibilityId] : ~0ull;
// FMeshBatch추가(Add)까지 PassMeshProcessor처리(Process).
PassMeshProcessor->AddMeshBatch(*StaticMeshBatch, BatchElementMask, StaticMeshBatch->PrimitiveSceneInfo->Proxy, StaticMeshBatch->Id);
}
(......)
}
}
// 병렬 설정(Set)드로우/렌더(Draw)의 . 동적 드로우/렌더(Draw)의 , 정렬 , .
class FMeshDrawCommandPassSetupTask
{
public:
FMeshDrawCommandPassSetupTask(FMeshDrawCommandPassSetupTaskContext& InContext)
: Context(InContext)
{
}
(......)
void AnyThreadTask()
{
const bool bMobileShadingBasePass = Context.ShadingPath == EShadingPath::Mobile && Context.PassType == EMeshPass::BasePass;
const bool bMobileVulkanSM5BasePass = IsVulkanMobileSM5Platform(Context.ShaderPlatform) && Context.PassType == EMeshPass::BasePass;
if (bMobileShadingBasePass)
{
(......)
}
else
{
// 동적 및 정적 드로우/렌더(Draw)(MeshPassProcessorFMeshBatch변환(Transform/Convert)MeshDrawCommand).
GenerateDynamicMeshDrawCommands(
*Context.View,
Context.ShadingPath,
Context.PassType,
Context.MeshPassProcessor,
*Context.DynamicMeshElements,
Context.DynamicMeshElementsPassRelevance,
Context.NumDynamicMeshElements,
Context.DynamicMeshCommandBuildRequests,
Context.NumDynamicMeshCommandBuildRequestElements,
Context.MeshDrawCommands,
Context.MeshDrawCommandStorage,
Context.MinimalPipelineStatePassSet,
Context.NeedsShaderInitialisation
);
}
if (Context.MeshDrawCommands.Num() > 0)
{
if (Context.PassType != EMeshPass::Num)
{
// 적용 view 내에서 의 MeshDrawCommand, 예: :렌더링(Render)반사(Reflection)의 역방향(Inverse)클리핑/클램핑(Clipping)(reverse culling mode)
ApplyViewOverridesToMeshDrawCommands(
Context.ShadingPath,
Context.PassType,
Context.bReverseCulling,
Context.bRenderSceneTwoSided,
Context.BasePassDepthStencilAccess,
Context.DefaultBasePassDepthStencilAccess,
Context.MeshDrawCommands,
Context.MeshDrawCommandStorage,
Context.MinimalPipelineStatePassSet,
Context.NeedsShaderInitialisation,
Context.TempVisibleMeshDrawCommands
);
}
// 갱신(Update)정렬 .
if (bMobileShadingBasePass || bMobileVulkanSM5BasePass)
{
(......)
}
else if (Context.TranslucencyPass != ETranslucencyPass::TPT_MAX)
{
// 을(를) 활용하여 view의 데이터 갱신(Update)정렬 . 정렬 의 타입/유형 는 FMeshDrawCommandSortKey, BasePass 및 반투명(Translucent)의 , 내에서 반투명(Translucent)의 정렬 로써 까지 의 거리로 .
UpdateTranslucentMeshSortKeys(
Context.TranslucentSortPolicy,
Context.TranslucentSortAxis,
Context.ViewOrigin,
Context.ViewMatrix,
*Context.PrimitiveBounds,
Context.TranslucencyPass,
Context.MeshDrawCommands
);
}
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_SortVisibleMeshDrawCommands);
// 실행(Execute)MeshDrawCommand의 정렬 , FCompareFMeshDrawCommands로써 FMeshDrawCommandSortKey로써 정렬 , 회 을(를) 활용하여 StateBucketId.
Context.MeshDrawCommands.Sort(FCompareFMeshDrawCommands());
}
if (Context.bUseGPUScene)
{
// GPU의 데이터 (는 렌더링(Render) 내에서 의 Primitive데이터 ).
BuildMeshDrawCommandPrimitiveIdBuffer(
Context.bDynamicInstancing,
Context.MeshDrawCommands,
Context.MeshDrawCommandStorage,
Context.PrimitiveIdBufferData,
Context.PrimitiveIdBufferDataSize,
Context.TempVisibleMeshDrawCommands,
Context.MaxInstances,
Context.VisibleMeshDrawCommandsNum,
Context.NewPassVisibleMeshDrawCommandsNum,
Context.ShaderPlatform,
Context.InstanceFactor
);
}
}
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
AnyThreadTask();
}
private:
FMeshDrawCommandPassSetupTaskContext& Context; // 디바이스.
};
// MeshDrawCommand의 할당(Allocate)리소스 。
class FMeshDrawCommandInitResourcesTask
{
public:
(......)
void AnyThreadTask()
{
TRACE_CPUPROFILER_EVENT_SCOPE(MeshDrawCommandInitResourcesTask);
if (Context.NeedsShaderInitialisation)
{
// 초기화(Initialize)바인딩(Bind)의 shader리소스 。
for (const FGraphicsMinimalPipelineStateInitializer& Initializer : Context.MinimalPipelineStatePassSet)
{
Initializer.BoundShaderState.LazilyInitShaders();
}
}
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
AnyThreadTask();
}
private:
FMeshDrawCommandPassSetupTaskContext& Context;
};FMeshDrawCommandPassSetupTask는 동적 그리드 그리기 및 정적 그리기 명령의 생성, 정렬 및 병합을 포함하여 그리드 렌더링 파이프라인에서 매우 중요한 역할을 한다는 것을 알 수 있습니다. 정렬 단계의 키 값은 FMeshDrawCommandSortKey에 의해 결정되며 다음과 같이 정의됩니다.
// Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.h
// FVisibleMeshDrawCommand의 정렬 .
class RENDERER_API FMeshDrawCommandSortKey
{
public:
union
{
uint64 PackedData; // 의 64데이터
// 채널/패스(Pass)정렬
struct
{
uint64 VertexShaderHash : 16; // :VS의 。
uint64 PixelShaderHash : 32; // 내에서 :PS의 。
uint64 Masked : 16; // :는 Masked의 머티리얼(Material)
} BasePass;
// 반투명(Translucent)채널/패스(Pass)정렬
struct
{
uint64 MeshIdInPrimitive : 16; // :개 Primitive의 의 id
uint64 Distance : 32; // 내에서 :까지 의 거리
uint64 Priority : 16; // :단계 (에 의해 머티리얼(Material))
} Translucent;
// 정렬
struct
{
uint64 VertexShaderHash : 32; // :VS의 。
uint64 PixelShaderHash : 32; // :PS의 。
} Generic;
};
//
FORCEINLINE bool operator!=(FMeshDrawCommandSortKey B) const
{
return PackedData != B.PackedData;
}
// ,을(를) 활용하여 정렬 。
FORCEINLINE bool operator<(FMeshDrawCommandSortKey B) const
{
return PackedData < B.PackedData;
}
static const FMeshDrawCommandSortKey Default;
};위의 FMeshDrawCommandSortKey에는 몇 가지 추가 설명이 필요합니다.
FMeshDrawCommandSortKey는 BasePass, Transparent Pass 및 Normal Pass의 세 가지 키 값을 저장할 수 있지만 동시에 한 가지 유형의 데이터만 유효합니다.
키 값의 계산 논리는 다양한 파일과 단계에 분산되어 있습니다. 예를 들어 BasePass 키 값은 BasePassRendering, DepthRendering 및 MeshPassProcessor 단계에서 발생할 수 있습니다. 이들의 핵심가치 계산 로직 및 분석은 다음과 같습니다.
키 이름 계산 코드 분석하다
VertexShaderHash 포인터해시(버텍스 셰이더) 자료에서 사용하는 VS의 포인터 해시입니다.
PixelShaderHash 포인터해시(PixelShader) 머티리얼에서 사용하는 PS의 포인터 해시입니다.
마스크됨 BlendMode == EBlendMode::BLEND_Masked ? 0 : 1 재질의 혼합 모드가 마스크인지 여부입니다.
MeshIdInPrimitive MeshIdInPrimitivePerView[ViewIndex] 동일한 프리미티브를 공유하는 뷰 기반 안정적인 그리드 ID입니다.
거리 (uint32)~BitInvertIfNegativeFloat(((uint32)&Distance)) ETranslucentSortPolicy를 기반으로 거리를 계산한 다음 음수 거리를 반대로 바꿉니다.
우선순위
재질에 할당된 투명도 정렬 우선순위에서 직접 가져옵니다.
Operator<는 PackedData와 직접 비교하여 데이터 우선순위가 높을수록 우선순위가 높다는 것을 나타냅니다. 구체적으로 BasePass의 정렬 기준은 먼저 자료가 마스크되었는지 여부를 결정한 다음 PS 및 VS의 주소 해시 값을 결정합니다. 마찬가지로 투명 채널의 정렬 우선순위는 재질에 의해 지정된 우선순위, 그리드에서 카메라까지의 거리, 그리드 ID입니다.
일반적으로 그리드를 정렬할 때 성능에 가장 큰 영향을 미치는 요소가 최우선적으로 고려됩니다.
예를 들어, BasePass 단계에서 Masked 재질은 일부 GPU 장치의 병렬 효율성과 처리량을 심각하게 방해하여 가장 높은 순위를 차지할 수 있습니다. PS는 명령 수와 계산 복잡성이 VS보다 높은 경우가 많으므로 VS보다 먼저 순위를 매기는 것이 합리적입니다.
그러나 투명 채널 정렬에는 한 가지 특별한 점이 있는데, 바로 물체와 카메라 사이의 거리 관계입니다. 반투명 물체의 앞뒤 관계를 정확하게 그리려면 먼 거리에서 가까운 거리로 그려야 하기 때문입니다. 그렇지 않으면 앞뒤 관계가 혼란스러울 것입니다. 따라서 투명 채널은 거리를 가장 높은 위치(가장 높은 우선순위)에 두어야 합니다.
PackedData는 여러 데이터 세트를 단일 uint64로 묶어서 한 번만 비교하면 되므로 정렬 효율성을 향상시킬 수 있습니다. 그렇지 않은 경우 전통적인 작성 방법에 따르면 여러 개의 if-else 문을 사용하면 필연적으로 CPU 명령어 수가 증가하고 정렬 효율성이 감소합니다.
키 값 및 관련 정렬 논리를 수정하고 정렬 우선순위 및 알고리즘을 사용자 지정합니다. 예를 들어 텍스처, 버텍스 데이터, 렌더링 상태 등 여러 정렬 차원을 추가합니다.
다음으로 위 코드에서 여러 번 나타나는 몇 가지 중요한 개념인 FMeshPassProcessor 및 FMeshDrawCommands에 대해 설명합니다. FMeshPassProcessor는 FMeshBatch를 FMeshDrawCommands로 변환하는 역할을 합니다. 이에 대한 정의와 분석 및 관련 개념은 다음과 같습니다.
// Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.h
// 렌더링(Render)텍스처(Texture)(Render Target)의 렌더링(Render)파이프라인 스테이트 。RT의 드로우/렌더(Draw) 내에서 을(를) 활용하여 。의 해상도/크기 드로우/렌더(Draw)의 순회(Traverse/Iterate)성능 。
class FGraphicsMinimalPipelineStateInitializer
{
public:
// RT의 데이터 :픽셀(Pixel),플래그 。
using TRenderTargetFormats = TStaticArray<uint8/*EPixelFormat*/, MaxSimultaneousRenderTargets>;
using TRenderTargetFlags = TStaticArray<uint32, MaxSimultaneousRenderTargets>;
(......)
// 의 。
FGraphicsPipelineStateInitializer AsGraphicsPipelineStateInitializer() const
{
return FGraphicsPipelineStateInitializer
( BoundShaderState.AsBoundShaderState()
, BlendState
, RasterizerState
, DepthStencilState
, ImmutableSamplerState
, PrimitiveType
, 0
, FGraphicsPipelineStateInitializer::TRenderTargetFormats(PF_Unknown)
, FGraphicsPipelineStateInitializer::TRenderTargetFlags(0)
, PF_Unknown
, 0
, ERenderTargetLoadAction::ENoAction
, ERenderTargetStoreAction::ENoAction
, ERenderTargetLoadAction::ENoAction
, ERenderTargetStoreAction::ENoAction
, FExclusiveDepthStencil::DepthNop
, 0
, ESubpassHint::None
, 0
, 0
, bDepthBounds
, bMultiView
, bHasFragmentDensityAttachment
);
}
(......)
// 계산/산출(Calculate)FGraphicsMinimalPipelineStateInitializer의 。
inline friend uint32 GetTypeHash(const FGraphicsMinimalPipelineStateInitializer& Initializer)
{
//add and initialize any leftover padding within the struct to avoid unstable key
struct FHashKey
{
uint32 VertexDeclaration;
uint32 VertexShader;
uint32 PixelShader;
uint32 RasterizerState;
} HashKey;
HashKey.VertexDeclaration = PointerHash(Initializer.BoundShaderState.VertexDeclarationRHI);
HashKey.VertexShader = GetTypeHash(Initializer.BoundShaderState.VertexShaderIndex);
HashKey.PixelShader = GetTypeHash(Initializer.BoundShaderState.PixelShaderIndex);
HashKey.RasterizerState = PointerHash(Initializer.RasterizerState);
return uint32(CityHash64((const char*)&HashKey, sizeof(FHashKey)));
}
// 비교 인터페이스 。
bool operator==(const FGraphicsMinimalPipelineStateInitializer& rhs) const;
bool operator!=(const FGraphicsMinimalPipelineStateInitializer& rhs) const
bool operator<(const FGraphicsMinimalPipelineStateInitializer& rhs) const;
bool operator>(const FGraphicsMinimalPipelineStateInitializer& rhs) const;
// 렌더링(Render)파이프라인 스테이트
FMinimalBoundShaderStateInput BoundShaderState; // 바인딩(Bind)의 shader상태 。
FRHIBlendState* BlendState; // 블렌딩(Blending)상태 。
FRHIRasterizerState* RasterizerState; // 래스터라이제이션 상태 。
FRHIDepthStencilState* DepthStencilState; // 뎁스 상태 。
FImmutableSamplerState ImmutableSamplerState; // 의 샘플러(Sampler)상태 。
// 상태 。
bool bDepthBounds = false;
bool bMultiView = false;
bool bHasFragmentDensityAttachment = false;
uint8 Padding[1] = {}; // 메모리 정렬 의 데이터 。
EPrimitiveType PrimitiveType;
};
// 테이블 FGraphicsMinimalPipelineStateInitializer개 인스턴스 의 id,을(를) 활용하여 정렬 。
class FGraphicsMinimalPipelineStateId
{
public:
uint32 GetId() const
{
return PackedId;
}
// 판정/확인 및 비교 인터페이스 。
inline bool IsValid() const
inline bool operator==(const FGraphicsMinimalPipelineStateId& rhs) const;
inline bool operator!=(const FGraphicsMinimalPipelineStateId& rhs) const;
// 페치/가져오기(Fetch)의 FGraphicsMinimalPipelineStateInitializer。
inline const FGraphicsMinimalPipelineStateInitializer& GetPipelineState(const FGraphicsMinimalPipelineStateSet& InPipelineSet) const
{
if (bComesFromLocalPipelineStateSet)
{
return InPipelineSet.GetByElementId(SetElementIndex);
}
{
FScopeLock Lock(&PersistentIdTableLock);
return PersistentIdTable.GetByElementId(SetElementIndex).Key;
}
}
static void InitializePersistentIds();
// 페치/가져오기(Fetch)FGraphicsMinimalPipelineStateInitializer의 의 pipeline state id。
static FGraphicsMinimalPipelineStateId GetPersistentId(const FGraphicsMinimalPipelineStateInitializer& InPipelineState);
static void RemovePersistentId(FGraphicsMinimalPipelineStateId Id);
// 에 따라 페치/가져오기(Fetch)pipeline state id:글로벌 의 id테이블 및 PassSet파라미터 ,만약 까지 ,생성(Create)개 의 인스턴스 ,추가(Add)까지 PassSet파라미터 。
RENDERER_API static FGraphicsMinimalPipelineStateId GetPipelineStateId(const FGraphicsMinimalPipelineStateInitializer& InPipelineState, FGraphicsMinimalPipelineStateSet& InOutPassSet, bool& NeedsShaderInitialisation);
private:
// 의 。
union
{
uint32 PackedId = 0;
struct
{
uint32 SetElementIndex : 30;
uint32 bComesFromLocalPipelineStateSet : 1;
uint32 bValid : 1;
};
};
struct FRefCountedGraphicsMinimalPipelineState
{
FRefCountedGraphicsMinimalPipelineState() : RefNum(0)
{
}
uint32 RefNum;
};
static FCriticalSection PersistentIdTableLock;
using PersistentTableType = Experimental::TRobinHoodHashMap<FGraphicsMinimalPipelineStateInitializer, FRefCountedGraphicsMinimalPipelineState>;
// id테이블 。
static PersistentTableType PersistentIdTable;
static int32 LocalPipelineIdTableSize;
static int32 CurrentLocalPipelineIdTableSize;
static bool NeedsShaderInitialisation;
};
// 드로우/렌더(Draw),드로우/렌더(Draw)개 Mesh의 리소스 및 데이터 ,의 데이터 ,만약 InitView데이터 ,을(를) 활용하여 FVisibleMeshDrawCommand。
// FMeshDrawCommand을(를) 활용하여 의 리소스 필수: ,이므로 FMeshDrawCommand리소스 의 。
class FMeshDrawCommand
{
public:
// 리소스 바인딩(Bind)
FMeshDrawShaderBindings ShaderBindings;
FVertexInputStreamArray VertexStreams;
FRHIIndexBuffer* IndexBuffer;
// 캐싱(Cache)의 렌더링(Render)파이프라인 스테이트 (PSO)
FGraphicsMinimalPipelineStateId CachedPipelineId;
// 드로우/렌더(Draw)파라미터 。
uint32 FirstIndex;
uint32 NumPrimitives;
uint32 NumInstances;
// 포인트 데이터 , 및 。
union
{
struct
{
uint32 BaseVertexIndex;
uint32 NumVertices;
} VertexParams;
struct
{
FRHIVertexBuffer* Buffer;
uint32 Offset;
} IndirectArgs;
};
int8 PrimitiveIdStreamIndex;
// 렌더링(Render)상태 파라미터 。
uint8 StencilRef;
// 판정/확인 는 및 의 FMeshDrawCommand매칭 ,만약 매칭 ,로써 개 instance드로우/렌더(Draw)。
bool MatchesForDynamicInstancing(const FMeshDrawCommand& Rhs) const
{
return CachedPipelineId == Rhs.CachedPipelineId
&& StencilRef == Rhs.StencilRef
&& ShaderBindings.MatchesForDynamicInstancing(Rhs.ShaderBindings)
&& VertexStreams == Rhs.VertexStreams
&& PrimitiveIdStreamIndex == Rhs.PrimitiveIdStreamIndex
&& IndexBuffer == Rhs.IndexBuffer
&& FirstIndex == Rhs.FirstIndex
&& NumPrimitives == Rhs.NumPrimitives
&& NumInstances == Rhs.NumInstances
&& ((NumPrimitives > 0 && VertexParams.BaseVertexIndex == Rhs.VertexParams.BaseVertexIndex && VertexParams.NumVertices == Rhs.VertexParams.NumVertices)
|| (NumPrimitives == 0 && IndirectArgs.Buffer == Rhs.IndirectArgs.Buffer && IndirectArgs.Offset == Rhs.IndirectArgs.Offset));
}
// 페치/가져오기(Fetch)동적 인스턴스 의 。
uint32 GetDynamicInstancingHash() const
{
//add and initialize any leftover padding within the struct to avoid unstable keys
struct FHashKey
{
uint32 IndexBuffer;
uint32 VertexBuffers = 0;
uint32 VertexStreams = 0;
uint32 PipelineId;
uint32 DynamicInstancingHash;
uint32 FirstIndex;
uint32 NumPrimitives;
uint32 NumInstances;
uint32 IndirectArgsBufferOrBaseVertexIndex;
uint32 NumVertices;
uint32 StencilRefAndPrimitiveIdStreamIndex;
//
static inline uint32 PointerHash(const void* Key)
{
#if PLATFORM_64BITS
// Ignoring the lower 4 bits since they are likely zero anyway.
// Higher bits are more significant in 64 bit builds.
return reinterpret_cast<UPTRINT>(Key) >> 4;
#else
return reinterpret_cast<UPTRINT>(Key);
#endif
};
//
static inline uint32 HashCombine(uint32 A, uint32 B)
{
return A ^ (B + 0x9e3779b9 + (A << 6) + (A >> 2));
}
} HashKey;
// FMeshDrawCommand의 변수패딩/채우기 까지 FHashKey
HashKey.PipelineId = CachedPipelineId.GetId();
HashKey.StencilRefAndPrimitiveIdStreamIndex = StencilRef | (PrimitiveIdStreamIndex << 8);
HashKey.DynamicInstancingHash = ShaderBindings.GetDynamicInstancingHash();
for (int index = 0; index < VertexStreams.Num(); index++)
{
const FVertexInputStream& VertexInputStream = VertexStreams[index];
const uint32 StreamIndex = VertexInputStream.StreamIndex;
const uint32 Offset = VertexInputStream.Offset;
uint32 Packed = (StreamIndex << 28) | Offset;
HashKey.VertexStreams = FHashKey::HashCombine(HashKey.VertexStreams, Packed);
HashKey.VertexBuffers = FHashKey::HashCombine(HashKey.VertexBuffers, FHashKey::PointerHash(VertexInputStream.VertexBuffer));
}
HashKey.IndexBuffer = FHashKey::PointerHash(IndexBuffer);
HashKey.FirstIndex = FirstIndex;
HashKey.NumPrimitives = NumPrimitives;
HashKey.NumInstances = NumInstances;
if (NumPrimitives > 0)
{
HashKey.IndirectArgsBufferOrBaseVertexIndex = VertexParams.BaseVertexIndex;
HashKey.NumVertices = VertexParams.NumVertices;
}
else
{
HashKey.IndirectArgsBufferOrBaseVertexIndex = FHashKey::PointerHash(IndirectArgs.Buffer);
HashKey.NumVertices = IndirectArgs.Offset;
}
// 패딩/채우기 의 HashKey,데이터 의 HashKey는 의 ,로써 판정/확인 는 로써 렌더링(Render)。
return uint32(CityHash64((char*)&HashKey, sizeof(FHashKey)));
}
(......)
// FMeshBatch의 데이터 처리(Process)까지 FMeshDrawCommand 내에서 。
void SetDrawParametersAndFinalize(
const FMeshBatch& MeshBatch,
int32 BatchElementIndex,
FGraphicsMinimalPipelineStateId PipelineId,
const FMeshProcessorShaders* ShadersForDebugging)
{
const FMeshBatchElement& BatchElement = MeshBatch.Elements[BatchElementIndex];
IndexBuffer = BatchElement.IndexBuffer ? BatchElement.IndexBuffer->IndexBufferRHI.GetReference() : nullptr;
FirstIndex = BatchElement.FirstIndex;
NumPrimitives = BatchElement.NumPrimitives;
NumInstances = BatchElement.NumInstances;
if (NumPrimitives > 0)
{
VertexParams.BaseVertexIndex = BatchElement.BaseVertexIndex;
VertexParams.NumVertices = BatchElement.MaxVertexIndex - BatchElement.MinVertexIndex + 1;
}
else
{
IndirectArgs.Buffer = BatchElement.IndirectArgsBuffer;
IndirectArgs.Offset = BatchElement.IndirectArgsOffset;
}
Finalize(PipelineId, ShadersForDebugging);
}
// 저장(Save)PipelineId 및 shader정보 。
void Finalize(FGraphicsMinimalPipelineStateId PipelineId, const FMeshProcessorShaders* ShadersForDebugging)
{
CachedPipelineId = PipelineId;
ShaderBindings.Finalize(ShadersForDebugging);
}
/** Submits commands to the RHI Commandlist to draw the MeshDrawCommand. */
static void SubmitDraw(
const FMeshDrawCommand& RESTRICT MeshDrawCommand,
const FGraphicsMinimalPipelineStateSet& GraphicsMinimalPipelineStateSet,
FRHIVertexBuffer* ScenePrimitiveIdsBuffer,
int32 PrimitiveIdOffset,
uint32 InstanceFactor,
FRHICommandList& CommandList,
class FMeshDrawCommandStateCache& RESTRICT StateCache);
(......)
};
// 의 드로우/렌더(Draw)。저장(Store)로 의 드로우/렌더(Draw)의 정보 ,로써 처리(Process)。
// 및 FMeshDrawCommand의 는 ,FVisibleMeshDrawCommand오직 저장(Store)InitViews(/정렬 )의 ,드로우/렌더(Draw)의 데이터 。
class FVisibleMeshDrawCommand
{
public:
(......)
// 의 FMeshDrawCommand인스턴스 。
const FMeshDrawCommand* MeshDrawCommand;
// 상태 정렬 의 。(뎁스 정렬 의 반투명(Translucent)드로우/렌더(Draw))
FMeshDrawCommandSortKey SortKey;
// 드로우/렌더(Draw)id,을(를) 활용하여 로부터 PrimitiveSceneData의 SRV페치/가져오기(Fetch)데이터 。유효(Valid)의 DrawPrimitiveId로써 역방향(Inverse)FPrimitiveSceneInfo인스턴스 。
int32 DrawPrimitiveId;
// FVisibleMeshDrawCommand의 id,만약 는 -1이면 테이블 FPrimitiveSceneInfo,로써 역방향(Inverse)FPrimitiveSceneInfo인스턴스 。
int32 ScenePrimitiveId;
// Offset into the buffer of PrimitiveIds built for this pass, in int32's.
int32 PrimitiveIdBufferOffset;
// 동적 instancing상태 id(Dynamic instancing state bucket ID)。
// StateBucketId의 드로우/렌더(Draw)까지 개 instancing 내에서 。
// -1테이블 에 의해 StateBucketId정렬 。
int32 StateBucketId;
// Needed for view overrides
ERasterizerFillMode MeshFillMode : ERasterizerFillMode_NumBits + 1;
ERasterizerCullMode MeshCullMode : ERasterizerCullMode_NumBits + 1;
};
// 채널/패스(Pass)처리(Process)
class FMeshPassProcessor
{
public:
// 로써 의 、view、context데이터 에 의해 함수 .
const FScene* RESTRICT Scene;
ERHIFeatureLevel::Type FeatureLevel;
const FSceneView* ViewIfDynamicMeshCommand;
FMeshPassDrawListContext* DrawListContext;
(......)
// 추가(Add)FMeshBatch인스턴스 , 에 의해 의 Pass구현 .
virtual void AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId = -1) = 0;
// 드로우/렌더(Draw)설정(Set).
struct FMeshDrawingPolicyOverrideSettings
{
EDrawingPolicyOverrideFlags MeshOverrideFlags = EDrawingPolicyOverrideFlags::None;
EPrimitiveType MeshPrimitiveType = PT_TriangleList;
};
(......)
// 1개 FMeshBatch변환(Transform/Convert)1 또는 개 MeshDrawCommands.
template<typename PassShadersType, typename ShaderElementDataType>
void BuildMeshDrawCommands(
const FMeshBatch& RESTRICT MeshBatch,
uint64 BatchElementMask,
const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
const FMaterial& RESTRICT MaterialResource,
const FMeshPassProcessorRenderState& RESTRICT DrawRenderState,
PassShadersType PassShaders,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode,
FMeshDrawCommandSortKey SortKey,
EMeshPassFeatures MeshPassFeatures,
const ShaderElementDataType& ShaderElementData)
{
const FVertexFactory* RESTRICT VertexFactory = MeshBatch.VertexFactory;
const FPrimitiveSceneInfo* RESTRICT PrimitiveSceneInfo = PrimitiveSceneProxy ? PrimitiveSceneProxy->GetPrimitiveSceneInfo() : nullptr;
// FMeshDrawCommand인스턴스 , 을(를) 활용하여 렌더링(Render)리소스 및 데이터 .
FMeshDrawCommand SharedMeshDrawCommand;
// 처리(Process)FMeshDrawCommand의 스텐실 데이터 .
SharedMeshDrawCommand.SetStencilRef(DrawRenderState.GetStencilRef());
// 렌더링(Render)상태 인스턴스 .
FGraphicsMinimalPipelineStateInitializer PipelineState;
PipelineState.PrimitiveType = (EPrimitiveType)MeshBatch.Type;
PipelineState.ImmutableSamplerState = MaterialRenderProxy.ImmutableSamplerState;
// 처리(Process)FMeshDrawCommand의 포인트 데이터 , shader 및 렌더링(Render)상태 .
EVertexInputStreamType InputStreamType = EVertexInputStreamType::Default;
if ((MeshPassFeatures & EMeshPassFeatures::PositionOnly) != EMeshPassFeatures::Default) InputStreamType = EVertexInputStreamType::PositionOnly;
if ((MeshPassFeatures & EMeshPassFeatures::PositionAndNormalOnly) != EMeshPassFeatures::Default) InputStreamType = EVertexInputStreamType::PositionAndNormalOnly;
FRHIVertexDeclaration* VertexDeclaration = VertexFactory->GetDeclaration(InputStreamType);
SharedMeshDrawCommand.SetShaders(VertexDeclaration, PassShaders.GetUntypedShaders(), PipelineState);
PipelineState.RasterizerState = GetStaticRasterizerState<true>(MeshFillMode, MeshCullMode);
PipelineState.BlendState = DrawRenderState.GetBlendState();
PipelineState.DepthStencilState = DrawRenderState.GetDepthStencilState();
VertexFactory->GetStreams(FeatureLevel, InputStreamType, SharedMeshDrawCommand.VertexStreams);
SharedMeshDrawCommand.PrimitiveIdStreamIndex = VertexFactory->GetPrimitiveIdStreamIndex(InputStreamType);
// 처리(Process)VS/PS/GSshader의 바인딩(Bind)데이터 .
int32 DataOffset = 0;
if (PassShaders.VertexShader.IsValid())
{
FMeshDrawSingleShaderBindings ShaderBindings = SharedMeshDrawCommand.ShaderBindings.GetSingleShaderBindings(SF_Vertex, DataOffset);
PassShaders.VertexShader->GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, MaterialResource, DrawRenderState, ShaderElementData, ShaderBindings);
}
if (PassShaders.PixelShader.IsValid())
{
FMeshDrawSingleShaderBindings ShaderBindings = SharedMeshDrawCommand.ShaderBindings.GetSingleShaderBindings(SF_Pixel, DataOffset);
PassShaders.PixelShader->GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, MaterialResource, DrawRenderState, ShaderElementData, ShaderBindings);
}
(......)
const int32 NumElements = MeshBatch.Elements.Num();
// 순회(Traverse/Iterate)FMeshBatch의 MeshBatchElement, 로부터 머티리얼(Material) 내에서 페치/가져오기(Fetch)FMeshBatchElement의 shader타입/유형 의 바인딩(Bind)데이터 .
for (int32 BatchElementIndex = 0; BatchElementIndex < NumElements; BatchElementIndex++)
{
if ((1ull << BatchElementIndex) & BatchElementMask)
{
const FMeshBatchElement& BatchElement = MeshBatch.Elements[BatchElementIndex];
FMeshDrawCommand& MeshDrawCommand = DrawListContext->AddCommand(SharedMeshDrawCommand, NumElements);
DataOffset = 0;
if (PassShaders.VertexShader.IsValid())
{
FMeshDrawSingleShaderBindings VertexShaderBindings = MeshDrawCommand.ShaderBindings.GetSingleShaderBindings(SF_Vertex, DataOffset);
FMeshMaterialShader::GetElementShaderBindings(PassShaders.VertexShader, Scene, ViewIfDynamicMeshCommand, VertexFactory, InputStreamType, FeatureLevel, PrimitiveSceneProxy, MeshBatch, BatchElement, ShaderElementData, VertexShaderBindings, MeshDrawCommand.VertexStreams);
}
if (PassShaders.PixelShader.IsValid())
{
FMeshDrawSingleShaderBindings PixelShaderBindings = MeshDrawCommand.ShaderBindings.GetSingleShaderBindings(SF_Pixel, DataOffset);
FMeshMaterialShader::GetElementShaderBindings(PassShaders.PixelShader, Scene, ViewIfDynamicMeshCommand, VertexFactory, EVertexInputStreamType::Default, FeatureLevel, PrimitiveSceneProxy, MeshBatch, BatchElement, ShaderElementData, PixelShaderBindings, MeshDrawCommand.VertexStreams);
}
(......)
// 처리(Process)PrimitiveId.
int32 DrawPrimitiveId;
int32 ScenePrimitiveId;
GetDrawCommandPrimitiveId(PrimitiveSceneInfo, BatchElement, DrawPrimitiveId, ScenePrimitiveId);
// 포스트 프로세스MeshDrawCommand
FMeshProcessorShaders ShadersForDebugging = PassShaders.GetUntypedShaders();
DrawListContext->FinalizeCommand(MeshBatch, BatchElementIndex, DrawPrimitiveId, ScenePrimitiveId, MeshFillMode, MeshCullMode, SortKey, PipelineState, &ShadersForDebugging, MeshDrawCommand);
}
}
}
protected:
RENDERER_API void GetDrawCommandPrimitiveId(
const FPrimitiveSceneInfo* RESTRICT PrimitiveSceneInfo,
const FMeshBatchElement& BatchElement,
int32& DrawPrimitiveId,
int32& ScenePrimitiveId) const;
};위의 키 값을 계산할 때 CityHash64가 여러 번 사용되었습니다. CityHash64는 임의 개수의 문자열에 대한 해시 값을 계산하는 알고리즘입니다. 빠른 비암호화 해시 함수와 빠른 비암호화 해시 함수입니다. 구현 코드는 Engine\Source\Runtime\Core\Private\Hash\CityHash.cpp에 있습니다. 관심있는 아이들이 스스로 공부할 수 있습니다.
유사한 해시 알고리즘에는 HalfMD5, MD5, SipHash64, SipHash128, IntHash32, IntHash64, SHA1, SHA224, SHA256 등이 포함됩니다.
FMeshDrawCommand는 RHI가 메시를 그리는 데 필요한 모든 정보를 저장합니다. 이 정보는 플랫폼 독립적이고 그래픽 API 독립적(상태 비저장)이며 데이터 중심 설계를 기반으로 하므로 장치 컨텍스트를 공유할 수 있습니다.
FMeshPassProcessor::AddMeshBatch는 하위 클래스에 의해 구현되며 각 하위 클래스는 일반적으로 EMeshPass 열거형의 채널에 해당합니다. 일반적인 하위 범주는 다음과 같습니다.
FDepthPassMeshProcessor: EMeshPass::DepthPass에 해당하는 깊이 채널 메시 프로세서입니다.
FBasePassMeshProcessor: EMeshPass::BasePass에 해당하는 지오메트리 채널 메시 프로세서입니다.
FCustomDepthPassMeshProcessor: EMeshPass::CustomDepth에 해당하는 사용자 정의 깊이 채널 메시 프로세서입니다.
FShadowDepthPassMeshProcessor: EMeshPass::CSMShadowDepth에 해당하는 섀도우 채널 메시 프로세서입니다.
FTranslucencyDepthPassMeshProcessor: 투명한 깊이 채널 메시 프로세서, 해당 EMeshPass가 없습니다.
FLLightmapDensityMeshProcessor: EMeshPass::LightmapDensity에 해당하는 라이트맵 메시 프로세서입니다.
...
Pass마다 FMeshBatch를 다르게 처리합니다. 가장 일반적인 FBasePassMeshProcessor를 예로 들어 보겠습니다.
// Engine\Source\Runtime\Renderer\Private\BasePassRendering.cpp
void FBasePassMeshProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
if (MeshBatch.bUseForMaterial)
{
(......)
if (bShouldDraw
&& (!PrimitiveSceneProxy || PrimitiveSceneProxy->ShouldRenderInMainPass())
&& ShouldIncludeDomainInMeshPass(Material.GetMaterialDomain())
&& ShouldIncludeMaterialInDefaultOpaquePass(Material))
{
(......)
// 처리(Process)의 렌더링(Render)
if (IsSimpleForwardShadingEnabled(GetFeatureLevelShaderPlatform(FeatureLevel)))
{
AddMeshBatchForSimpleForwardShading(
MeshBatch,
BatchElementMask,
StaticMeshId,
PrimitiveSceneProxy,
MaterialRenderProxy,
Material,
LightMapInteraction,
bIsLitMaterial,
bAllowStaticLighting,
bUseVolumetricLightmap,
bAllowIndirectLightingCache,
MeshFillMode,
MeshCullMode);
}
// 렌더링(Render)반투명(Translucent)그림자(Shadow)의
else if (bIsLitMaterial
&& bIsTranslucent
&& PrimitiveSceneProxy
&& PrimitiveSceneProxy->CastsVolumetricTranslucentShadow())
{
(......)
if (bIsLitMaterial
&& bAllowStaticLighting
&& bUseVolumetricLightmap
&& PrimitiveSceneProxy)
{
Process< FSelfShadowedVolumetricLightmapPolicy >(
MeshBatch,
BatchElementMask,
StaticMeshId,
PrimitiveSceneProxy,
MaterialRenderProxy,
Material,
BlendMode,
ShadingModels,
FSelfShadowedVolumetricLightmapPolicy(),
ElementData,
MeshFillMode,
MeshCullMode);
}
(......)
}
// 에 따라 의 라이팅 의 및 품질 단계 ,호출(Call)Process처리(Process)。
else
{
static const auto CVarSupportLowQualityLightmap = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.SupportLowQualityLightmaps"));
const bool bAllowLowQualityLightMaps = (!CVarSupportLowQualityLightmap) || (CVarSupportLowQualityLightmap->GetValueOnAnyThread() != 0);
switch (LightMapInteraction.GetType())
{
case LMIT_Texture:
if (bAllowHighQualityLightMaps)
{
const FShadowMapInteraction ShadowMapInteraction = (bAllowStaticLighting && MeshBatch.LCI && bIsLitMaterial)
? MeshBatch.LCI->GetShadowMapInteraction(FeatureLevel)
: FShadowMapInteraction();
if (ShadowMapInteraction.GetType() == SMIT_Texture)
{
Process< FUniformLightMapPolicy >(
MeshBatch,
BatchElementMask,
StaticMeshId,
PrimitiveSceneProxy,
MaterialRenderProxy,
Material,
BlendMode,
ShadingModels,
FUniformLightMapPolicy(LMP_DISTANCE_FIELD_SHADOWS_AND_HQ_LIGHTMAP),
MeshBatch.LCI,
MeshFillMode,
MeshCullMode);
}
(......)
}
(......)
break;
default:
if (bIsLitMaterial
&& bAllowStaticLighting
&& Scene
&& Scene->VolumetricLightmapSceneData.HasData()
&& PrimitiveSceneProxy
&& (PrimitiveSceneProxy->IsMovable()
|| PrimitiveSceneProxy->NeedsUnbuiltPreviewLighting()
|| PrimitiveSceneProxy->GetLightmapType() == ELightmapType::ForceVolumetric))
{
Process< FUniformLightMapPolicy >(
MeshBatch,
BatchElementMask,
StaticMeshId,
PrimitiveSceneProxy,
MaterialRenderProxy,
Material,
BlendMode,
ShadingModels,
FUniformLightMapPolicy(LMP_PRECOMPUTED_IRRADIANCE_VOLUME_INDIRECT_LIGHTING),
MeshBatch.LCI,
MeshFillMode,
MeshCullMode);
}
(......)
break;
};
}
}
}
}
// FBasePassMeshProcessor의 라이팅 타입/유형 처리(Process)(shader바인딩(Bind),렌더링(Render)상태 ,정렬 ,포인트 데이터 ),호출(Call)BuildMeshDrawCommandsFMeshBatch변환(Transform/Convert)FMeshDrawCommands。
template<typename LightMapPolicyType>
void FBasePassMeshProcessor::Process(
const FMeshBatch& RESTRICT MeshBatch,
uint64 BatchElementMask,
int32 StaticMeshId,
const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
const FMaterial& RESTRICT MaterialResource,
EBlendMode BlendMode,
FMaterialShadingModelField ShadingModels,
const LightMapPolicyType& RESTRICT LightMapPolicy,
const typename LightMapPolicyType::ElementDataType& RESTRICT LightMapElementData,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode)
{
const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;
const bool bRenderSkylight = Scene && Scene->ShouldRenderSkylightInBasePass(BlendMode) && ShadingModels.IsLit();
const bool bRenderAtmosphericFog = IsTranslucentBlendMode(BlendMode) && (Scene && Scene->HasAtmosphericFog() && Scene->ReadOnlyCVARCache.bEnableAtmosphericFog);
TMeshProcessorShaders<
TBasePassVertexShaderPolicyParamType<LightMapPolicyType>,
FBaseHS,
FBaseDS,
TBasePassPixelShaderPolicyParamType<LightMapPolicyType>> BasePassShaders;
// 페치/가져오기(Fetch)라이팅 타입/유형 의 shader。
GetBasePassShaders<LightMapPolicyType>(
MaterialResource,
VertexFactory->GetType(),
LightMapPolicy,
FeatureLevel,
bRenderAtmosphericFog,
bRenderSkylight,
Get128BitRequirement(),
BasePassShaders.HullShader,
BasePassShaders.DomainShader,
BasePassShaders.VertexShader,
BasePassShaders.PixelShader
);
// 렌더링(Render)상태 처리(Process)。
FMeshPassProcessorRenderState DrawRenderState(PassDrawRenderState);
SetDepthStencilStateForBasePass(
ViewIfDynamicMeshCommand,
DrawRenderState,
FeatureLevel,
MeshBatch,
StaticMeshId,
PrimitiveSceneProxy,
bEnableReceiveDecalOutput);
if (bTranslucentBasePass)
{
SetTranslucentRenderState(DrawRenderState, MaterialResource, GShaderPlatformForFeatureLevel[FeatureLevel], TranslucencyPassType);
}
// 초기화(Initialize)Shader의 머티리얼(Material)。
TBasePassShaderElementData<LightMapPolicyType> ShaderElementData(LightMapElementData);
ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshId, true);
// 처리(Process)정렬 。
FMeshDrawCommandSortKey SortKey = FMeshDrawCommandSortKey::Default;
if (bTranslucentBasePass)
{
SortKey = CalculateTranslucentMeshStaticSortKey(PrimitiveSceneProxy, MeshBatch.MeshIdInPrimitive);
}
else
{
SortKey = CalculateBasePassMeshStaticSortKey(EarlyZPassMode, BlendMode, BasePassShaders.VertexShader.GetShader(), BasePassShaders.PixelShader.GetShader());
}
// FMeshBatch의 변환(Transform/Convert)FMeshDrawCommands。
BuildMeshDrawCommands(
MeshBatch,
BatchElementMask,
PrimitiveSceneProxy,
MaterialRenderProxy,
MaterialResource,
DrawRenderState,
BasePassShaders,
MeshFillMode,
MeshCullMode,
SortKey,
EMeshPassFeatures::Default,
ShaderElementData);
}FMeshPassProcessor의 주요 기능은 다음과 같습니다.
통과 필터. 투명한 물체를 필터링하는 깊이 패스(Depth Pass)와 같이 패스와 관련이 없는 MeshBatch를 필터링합니다.
드로잉 명령에 필요한 셰이더 및 렌더링 상태(깊이, 템플릿, 블렌딩 상태, 래스터라이제이션 상태 등)를 선택합니다.
그리기 명령과 관련된 셰이더 리소스 바인딩을 수집합니다.
ViewUniformBuffer, DepthPassUniformBuffer와 같은 Pass의 균일 버퍼.
버텍스 팩토리 바인딩(버텍스 데이터 및 인덱스).
재료 바인딩.
그리기 지침과 관련된 바인딩을 전달합니다.
드로우콜 관련 파라미터를 수집합니다.
FMeshPassProcessor::BuildMeshDrawCommands는 마지막 단계에서 FMeshPassDrawListContext::FinalizeCommand를 호출합니다. FMeshPassDrawListContext는 두 가지 기본 인터페이스를 제공하며 추상 클래스입니다. 파생 클래스에는 각각 동적 그리드 그리기 지침과 캐시된 그리드 그리기 지침의 컨텍스트를 나타내는 FDynamicPassMeshDrawListContext 및 FCachedPassMeshDrawListContext가 포함됩니다. 인터페이스와 분석은 다음과 같습니다.
// Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.h
// 채널/패스(Pass)드로우/렌더(Draw)리스트 。
class FMeshPassDrawListContext
{
public:
virtual FMeshDrawCommand& AddCommand(FMeshDrawCommand& Initializer, uint32 NumElements) = 0;
virtual void FinalizeCommand(
const FMeshBatch& MeshBatch,
int32 BatchElementIndex,
int32 DrawPrimitiveId,
int32 ScenePrimitiveId,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode,
FMeshDrawCommandSortKey SortKey,
const FGraphicsMinimalPipelineStateInitializer& PipelineState,
const FMeshProcessorShaders* ShadersForDebugging,
FMeshDrawCommand& MeshDrawCommand) = 0;
};
// 【동적 】채널/패스(Pass)드로우/렌더(Draw)리스트 。
class FDynamicPassMeshDrawListContext : public FMeshPassDrawListContext
{
public:
(......)
virtual FMeshDrawCommand& AddCommand(FMeshDrawCommand& Initializer, uint32 NumElements) override final
{
// FMeshDrawCommand리스트 ,반환(Return)배열 의 。
const int32 Index = DrawListStorage.MeshDrawCommands.AddElement(Initializer);
FMeshDrawCommand& NewCommand = DrawListStorage.MeshDrawCommands[Index];
return NewCommand;
}
virtual void FinalizeCommand(
const FMeshBatch& MeshBatch,
int32 BatchElementIndex,
int32 DrawPrimitiveId,
int32 ScenePrimitiveId,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode,
FMeshDrawCommandSortKey SortKey,
const FGraphicsMinimalPipelineStateInitializer& PipelineState,
const FMeshProcessorShaders* ShadersForDebugging,
FMeshDrawCommand& MeshDrawCommand) override final
{
// 페치/가져오기(Fetch)렌더링(Render)파이프라인 Id
FGraphicsMinimalPipelineStateId PipelineId = FGraphicsMinimalPipelineStateId::GetPipelineStateId(PipelineState, GraphicsMinimalPipelineStateSet, NeedsShaderInitialisation);
// FMeshBatch데이터 처리(Process), 저장(Save)까지 MeshDrawCommand 내에서 .
MeshDrawCommand.SetDrawParametersAndFinalize(MeshBatch, BatchElementIndex, PipelineId, ShadersForDebugging);
// 생성(Create)FVisibleMeshDrawCommand, FMeshDrawCommand데이터 패딩/채우기 .
FVisibleMeshDrawCommand NewVisibleMeshDrawCommand;
NewVisibleMeshDrawCommand.Setup(&MeshDrawCommand, DrawPrimitiveId, ScenePrimitiveId, -1, MeshFillMode, MeshCullMode, SortKey);
// 추가(Add)까지 TArray 내에서 ,동적 및 인스턴스 MeshDrawCommand。
DrawList.Add(NewVisibleMeshDrawCommand);
}
private:
// 저장(Save)FMeshDrawCommand의 리스트 ,을(를) 활용하여 의 데이터 구조체 는 TChunkedArray。
FDynamicMeshDrawCommandStorage& DrawListStorage;
// FVisibleMeshDrawCommand리스트 ,을(를) 활용하여 의 데이터 구조체 는 TArray,을(를) 활용하여 FMeshDrawCommand,의 데이터 저장(Store)DrawListStorage。
FMeshCommandOneFrameArray& DrawList;
// PSO。
FGraphicsMinimalPipelineStateSet& GraphicsMinimalPipelineStateSet;
bool& NeedsShaderInitialisation;
};
// 【캐싱(Cache)】채널/패스(Pass)드로우/렌더(Draw)리스트 。
class FCachedPassMeshDrawListContext : public FMeshPassDrawListContext
{
public:
FCachedPassMeshDrawListContext(FCachedMeshDrawCommandInfo& InCommandInfo, FCriticalSection& InCachedMeshDrawCommandLock, FCachedPassMeshDrawList& InCachedDrawLists, FStateBucketMap& InCachedMeshDrawCommandStateBuckets, const FScene& InScene);
virtual FMeshDrawCommand& AddCommand(FMeshDrawCommand& Initializer, uint32 NumElements) override final
{
if (NumElements == 1)
{
return Initializer;
}
else
{
MeshDrawCommandForStateBucketing = Initializer;
return MeshDrawCommandForStateBucketing;
}
}
virtual void FinalizeCommand(
const FMeshBatch& MeshBatch,
int32 BatchElementIndex,
int32 DrawPrimitiveId,
int32 ScenePrimitiveId,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode,
FMeshDrawCommandSortKey SortKey,
const FGraphicsMinimalPipelineStateInitializer& PipelineState,
const FMeshProcessorShaders* ShadersForDebugging,
FMeshDrawCommand& MeshDrawCommand) override final
{
FGraphicsMinimalPipelineStateId PipelineId = FGraphicsMinimalPipelineStateId::GetPersistentId(PipelineState);
MeshDrawCommand.SetDrawParametersAndFinalize(MeshBatch, BatchElementIndex, PipelineId, ShadersForDebugging);
if (UseGPUScene(GMaxRHIShaderPlatform, GMaxRHIFeatureLevel))
{
Experimental::FHashElementId SetId;
auto hash = CachedMeshDrawCommandStateBuckets.ComputeHash(MeshDrawCommand);
{
FScopeLock Lock(&CachedMeshDrawCommandLock);
(......)
// 로부터 캐싱(Cache)테이블 내에서 탐색/조회(Find/Lookup)hash의 id,만약 이면 추가(Add)의 . 로부터 까지 FMeshDrawCommand의 의 。
SetId = CachedMeshDrawCommandStateBuckets.FindOrAddIdByHash(hash, MeshDrawCommand, FMeshDrawCommandCount());
// 1
CachedMeshDrawCommandStateBuckets.GetByElementId(SetId).Value.Num++;
(......)
}
CommandInfo.StateBucketId = SetId.GetIndex();
}
else
{
FScopeLock Lock(&CachedMeshDrawCommandLock);
// Only one FMeshDrawCommand supported per FStaticMesh in a pass
// Allocate at lowest free index so that 'r.DoLazyStaticMeshUpdate' can shrink the TSparseArray more effectively
CommandInfo.CommandIndex = CachedDrawLists.MeshDrawCommands.EmplaceAtLowestFreeIndex(CachedDrawLists.LowestFreeIndexSearchStart, MeshDrawCommand);
}
// 저장(Store)데이터 .
CommandInfo.SortKey = SortKey;
CommandInfo.MeshFillMode = MeshFillMode;
CommandInfo.MeshCullMode = MeshCullMode;
}
private:
FMeshDrawCommand MeshDrawCommandForStateBucketing;
FCachedMeshDrawCommandInfo& CommandInfo;
FCriticalSection& CachedMeshDrawCommandLock;
FCachedPassMeshDrawList& CachedDrawLists;
FStateBucketMap& CachedMeshDrawCommandStateBuckets; // 테이블 , 및 의 FMeshDrawCommand。
const FScene& Scene;
};FMeshBatch부터 FMeshDrawCommand 단계까지 렌더러는 FMeshBatch를 FMeshDrawCommand로 변환하고 FMeshPassProcessor의 FMeshPassDrawListContext 멤버 변수에 저장하기 위해 많은 처리를 하는 것을 볼 수 있습니다. 이 기간 동안 그리드 그리기 지침에 필요한 모든 데이터는 후속 렌더링 프로세스에 들어가기 위해 각 개체에서 수집되거나 처리됩니다. 아래 다이어그램은 이러한 주요 프로세스를 보여줍니다.

FMeshDrawCommand 병합과 관련하여 동적 드로잉 경로 모드에서 FDynamicPassMeshDrawListContext의 FMeshDrawCommand가 TArray 구조에 저장된다는 점을 추가해야 합니다. FMeshDrawCommand는 병합되지 않으며 그리드가 동적으로 인스턴스화되지도 않지만 상태 기반 정렬의 견고성은 향상될 수 있습니다.
캐시된(정적) 드로잉 경로 모드의 FCachedPassMeshDrawListContext는 FStateBucketMap을 사용하여 드로잉 제출 단계에서 드로잉을 인스턴스화하기 위해 병합 및 계산 기능을 구현합니다.
게다가 UE에는 Unity와 같은 동적 배칭 기능이 없으며, 에디터 단계에서 수동으로 메싱만 수행합니다(아래 그림 참조).


UE 에디터에 내장된 액터 병합 도구의 열기 방법과 인터페이스 미리보기입니다.
3.2.4 FMeshDrawCommand에서 RHICommandList로
이전 섹션에서는 FMeshBatch를 FMeshDrawCommand로 변환하는 방법을 자세히 설명했습니다. 이 섹션에서는 후속 단계, 즉 FMeshDrawCommand를 RHICommandList로 변환하는 방법과 프로세스 중에 수행된 처리 및 최적화에 대해 설명합니다.
FMeshBatch가 FMeshDrawCommand로 변환된 후 각 패스는 FMeshPassProcessor에 해당하고 각 FMeshPassProcessor는 렌더러가 적절한 시간에 트리거하고 렌더링할 수 있도록 패스에서 그려야 하는 모든 FMeshDrawCommand를 저장합니다. 가장 간단한 PrePass(Deep Pass)를 예로 들어 보겠습니다.
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
(......)
// FMeshBatch변환(Transform/Convert)FMeshDrawCommand의 로직 InitViews
InitViews(RHICmdList, BasePassDepthStencilAccess, ILCTaskData, UpdateViewCustomDataEvents);
(......)
// 렌더링(Render)PrePass(뎁스 Pass)
RenderPrePass(FRHICommandListImmediate& RHICmdList, TFunctionRef<void()> AfterTasksAreStarted)
{
bool bParallel = GRHICommandList.UseParallelAlgorithms() && CVarParallelPrePass.GetValueOnRenderThread();
(......)
if(EarlyZPassMode != DDM_None)
{
const bool bWaitForTasks = bParallel && (CVarRHICmdFlushRenderThreadTasksPrePass.GetValueOnRenderThread() > 0 || CVarRHICmdFlushRenderThreadTasks.GetValueOnRenderThread() > 0);
// 순회(Traverse/Iterate)view,개 view렌더링(Render)회 뎁스 Pass。
for(int32 ViewIndex = 0;ViewIndex < Views.Num();ViewIndex++)
{
const FViewInfo& View = Views[ViewIndex];
// 처리(Process)뎁스 Pass의 렌더링(Render)리소스 및 상태 。
TUniformBufferRef<FSceneTexturesUniformParameters> PassUniformBuffer;
CreateDepthPassUniformBuffer(RHICmdList, View, PassUniformBuffer);
FMeshPassProcessorRenderState DrawRenderState(View, PassUniformBuffer);
SetupDepthPassState(DrawRenderState);
if (View.ShouldRenderView())
{
Scene->UniformBuffers.UpdateViewUniformBuffer(View);
if (bParallel)
{
// 병렬 렌더링(Render)뎁스 Pass。
bDepthWasCleared = RenderPrePassViewParallel(View, RHICmdList, DrawRenderState, AfterTasksAreStarted, !bDidPrePre) || bDepthWasCleared;
bDidPrePre = true;
}
(......)
}
(......)
}
}
(......)
}
}
// Engine\Source\Runtime\Renderer\Private\DepthRendering.cpp
// 병렬 렌더링(Render)뎁스 Pass인터페이스
bool FDeferredShadingSceneRenderer::RenderPrePassViewParallel(const FViewInfo& View, FRHICommandListImmediate& ParentCmdList, const FMeshPassProcessorRenderState& DrawRenderState, TFunctionRef<void()> AfterTasksAreStarted, bool bDoPrePre)
{
bool bDepthWasCleared = false;
{
// 드로우/렌더(Draw)저장(Store)。
FPrePassParallelCommandListSet ParallelCommandListSet(View, this, ParentCmdList,
CVarRHICmdPrePassDeferredContexts.GetValueOnRenderThread() > 0,
CVarRHICmdFlushRenderThreadTasksPrePass.GetValueOnRenderThread() == 0 && CVarRHICmdFlushRenderThreadTasks.GetValueOnRenderThread() == 0,
DrawRenderState);
// 트리거 병렬 드로우/렌더(Draw)。
View.ParallelMeshDrawCommandPasses[EMeshPass::DepthPass].DispatchDraw(&ParallelCommandListSet, ParentCmdList);
(......)
}
(......)
return bDepthWasCleared;
}
// Engine\Source\Runtime\Renderer\Private\MeshDrawCommands.cpp
void FParallelMeshDrawCommandPass::DispatchDraw(FParallelCommandListSet* ParallelCommandListSet, FRHICommandList& RHICmdList) const
{
(......)
FRHIVertexBuffer* PrimitiveIdsBuffer = PrimitiveIdVertexBufferPoolEntry.BufferRHI;
const int32 BasePrimitiveIdsOffset = 0;
if (ParallelCommandListSet)
{
(......)
const ENamedThreads::Type RenderThread = ENamedThreads::GetRenderThread();
// 처리(Process)。
FGraphEventArray Prereqs;
if (ParallelCommandListSet->GetPrereqs())
{
Prereqs.Append(*ParallelCommandListSet->GetPrereqs());
}
if (TaskEventRef.IsValid())
{
Prereqs.Add(TaskEventRef);
}
// 및 스레드(Thread)개수 의 병렬 드로우/렌더(Draw)。
const int32 NumThreads = FMath::Min<int32>(FTaskGraphInterface::Get().GetNumWorkerThreads(), ParallelCommandListSet->Width);
const int32 NumTasks = FMath::Min<int32>(NumThreads, FMath::DivideAndRoundUp(MaxNumDraws, ParallelCommandListSet->MinDrawsPerCommandList));
const int32 NumDrawsPerTask = FMath::DivideAndRoundUp(MaxNumDraws, NumTasks);
// 순회(Traverse/Iterate)NumTasks회 ,NumTasks개 드로우/렌더(Draw)(FDrawVisibleMeshCommandsAnyThreadTask)인스턴스 。
for (int32 TaskIndex = 0; TaskIndex < NumTasks; TaskIndex++)
{
const int32 StartIndex = TaskIndex * NumDrawsPerTask;
const int32 NumDraws = FMath::Min(NumDrawsPerTask, MaxNumDraws - StartIndex);
checkSlow(NumDraws > 0);
FRHICommandList* CmdList = ParallelCommandListSet->NewParallelCommandList();
// FDrawVisibleMeshCommandsAnyThreadTask인스턴스 추가(Add)TaskGraph 내에서 , 내에서 TaskContext.MeshDrawCommands는 의 에 의해 FMeshPassProcessor의 。
FGraphEventRef AnyThreadCompletionEvent = TGraphTask<FDrawVisibleMeshCommandsAnyThreadTask>::CreateTask(&Prereqs, RenderThread).ConstructAndDispatchWhenReady(*CmdList, TaskContext.MeshDrawCommands, TaskContext.MinimalPipelineStatePassSet, PrimitiveIdsBuffer, BasePrimitiveIdsOffset, TaskContext.bDynamicInstancing, TaskContext.InstanceFactor, TaskIndex, NumTasks);
// 이벤트 추가(Add)ParallelCommandListSet,로써 뎁스 Pass의 병렬 드로우/렌더(Draw)는 。
ParallelCommandListSet->AddParallelCommandList(CmdList, AnyThreadCompletionEvent, NumDraws);
}
}
(......)
}
// Engine\Source\Runtime\Renderer\Private\MeshDrawCommands.cpp
void FDrawVisibleMeshCommandsAnyThreadTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
// 계산/산출(Calculate)드로우/렌더(Draw)의 범위
const int32 DrawNum = VisibleMeshDrawCommands.Num();
const int32 NumDrawsPerTask = TaskIndex < DrawNum ? FMath::DivideAndRoundUp(DrawNum, TaskNum) : 0;
const int32 StartIndex = TaskIndex * NumDrawsPerTask;
const int32 NumDraws = FMath::Min(NumDrawsPerTask, DrawNum - StartIndex);
// 드로우/렌더(Draw)의 데이터 까지 드로우/렌더(Draw)인터페이스
SubmitMeshDrawCommandsRange(VisibleMeshDrawCommands, GraphicsMinimalPipelineStateSet, PrimitiveIdsBuffer, BasePrimitiveIdsOffset, bDynamicInstancing, StartIndex, NumDraws, InstanceFactor, RHICmdList);
RHICmdList.EndRenderPass();
RHICmdList.HandleRTThreadTaskCompletion(MyCompletionGraphEvent);
}
// 범위 의 드로우/렌더(Draw)。
void SubmitMeshDrawCommandsRange(
const FMeshCommandOneFrameArray& VisibleMeshDrawCommands,
const FGraphicsMinimalPipelineStateSet& GraphicsMinimalPipelineStateSet,
FRHIVertexBuffer* PrimitiveIdsBuffer,
int32 BasePrimitiveIdsOffset,
bool bDynamicInstancing,
int32 StartIndex,
int32 NumMeshDrawCommands,
uint32 InstanceFactor,
FRHICommandList& RHICmdList)
{
FMeshDrawCommandStateCache StateCache;
// 순회(Traverse/Iterate)범위 의 드로우/렌더(Draw),개 개 。
for (int32 DrawCommandIndex = StartIndex; DrawCommandIndex < StartIndex + NumMeshDrawCommands; DrawCommandIndex++)
{
const FVisibleMeshDrawCommand& VisibleMeshDrawCommand = VisibleMeshDrawCommands[DrawCommandIndex];
const int32 PrimitiveIdBufferOffset = BasePrimitiveIdsOffset + (bDynamicInstancing ? VisibleMeshDrawCommand.PrimitiveIdBufferOffset : DrawCommandIndex) * sizeof(int32);
// 개 MeshDrawCommand.
FMeshDrawCommand::SubmitDraw(*VisibleMeshDrawCommand.MeshDrawCommand, GraphicsMinimalPipelineStateSet, PrimitiveIdsBuffer, PrimitiveIdBufferOffset, InstanceFactor, RHICmdList, StateCache);
}
}
// 개 MeshDrawCommand까지 RHICommandList.
void FMeshDrawCommand::SubmitDraw(
const FMeshDrawCommand& RESTRICT MeshDrawCommand,
const FGraphicsMinimalPipelineStateSet& GraphicsMinimalPipelineStateSet,
FRHIVertexBuffer* ScenePrimitiveIdsBuffer,
int32 PrimitiveIdOffset,
uint32 InstanceFactor,
FRHICommandList& RHICmdList,
FMeshDrawCommandStateCache& RESTRICT StateCache)
{
(......)
const FGraphicsMinimalPipelineStateInitializer& MeshPipelineState = MeshDrawCommand.CachedPipelineId.GetPipelineState(GraphicsMinimalPipelineStateSet);
// 설정(Set) 및 캐싱(Cache)PSO.
if (MeshDrawCommand.CachedPipelineId.GetId() != StateCache.PipelineId)
{
FGraphicsPipelineStateInitializer GraphicsPSOInit = MeshPipelineState.AsGraphicsPipelineStateInitializer();
RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
StateCache.SetPipelineState(MeshDrawCommand.CachedPipelineId.GetId());
}
// 설정(Set) 및 캐싱(Cache)스텐실 .
if (MeshDrawCommand.StencilRef != StateCache.StencilRef)
{
RHICmdList.SetStencilRef(MeshDrawCommand.StencilRef);
StateCache.StencilRef = MeshDrawCommand.StencilRef;
}
// 설정(Set)포인트 데이터 .
for (int32 VertexBindingIndex = 0; VertexBindingIndex < MeshDrawCommand.VertexStreams.Num(); VertexBindingIndex++)
{
const FVertexInputStream& Stream = MeshDrawCommand.VertexStreams[VertexBindingIndex];
if (MeshDrawCommand.PrimitiveIdStreamIndex != -1 && Stream.StreamIndex == MeshDrawCommand.PrimitiveIdStreamIndex)
{
RHICmdList.SetStreamSource(Stream.StreamIndex, ScenePrimitiveIdsBuffer, PrimitiveIdOffset);
StateCache.VertexStreams[Stream.StreamIndex] = Stream;
}
else if (StateCache.VertexStreams[Stream.StreamIndex] != Stream)
{
RHICmdList.SetStreamSource(Stream.StreamIndex, Stream.VertexBuffer, Stream.Offset);
StateCache.VertexStreams[Stream.StreamIndex] = Stream;
}
}
// 설정(Set)shader바인딩(Bind)의 리소스 .
MeshDrawCommand.ShaderBindings.SetOnCommandList(RHICmdList, MeshPipelineState.BoundShaderState.AsBoundShaderState(), StateCache.ShaderBindings);
// 에 따라 의 데이터 호출(Call)타입/유형 의 드로우/렌더(Draw)까지 RHICommandList.
if (MeshDrawCommand.IndexBuffer)
{
if (MeshDrawCommand.NumPrimitives > 0)
{
RHICmdList.DrawIndexedPrimitive(
MeshDrawCommand.IndexBuffer,
MeshDrawCommand.VertexParams.BaseVertexIndex,
0,
MeshDrawCommand.VertexParams.NumVertices,
MeshDrawCommand.FirstIndex,
MeshDrawCommand.NumPrimitives,
MeshDrawCommand.NumInstances * InstanceFactor
);
}
else
{
RHICmdList.DrawIndexedPrimitiveIndirect(
MeshDrawCommand.IndexBuffer,
MeshDrawCommand.IndirectArgs.Buffer,
MeshDrawCommand.IndirectArgs.Offset
);
}
}
else
{
if (MeshDrawCommand.NumPrimitives > 0)
{
RHICmdList.DrawPrimitive(
MeshDrawCommand.VertexParams.BaseVertexIndex + MeshDrawCommand.FirstIndex,
MeshDrawCommand.NumPrimitives,
MeshDrawCommand.NumInstances * InstanceFactor);
}
else
{
RHICmdList.DrawPrimitiveIndirect(
MeshDrawCommand.IndirectArgs.Buffer,
MeshDrawCommand.IndirectArgs.Offset);
}
}
}위 코드는 PrePass(깊이 채널)의 그리기 과정을 자세히 설명한 것입니다. FMeshDrawCommand에서 RHICommandList로 이동하는 방법에 대해 다음 지침을 추가해야 합니다.
각 패스는 위와 유사한 프로세스를 수행합니다. 동일한 프레임이 여러 번 실행되지만 모든 패스가 활성화되는 것은 아닙니다. 뷰의 PassMask를 통해 동적으로 켜고 끌 수 있습니다.
DispatchDraw 및 SubmitMeshDrawCommandsRange는 의도적으로 평면 배열을 사용하고 다음 요소를 고려합니다.
FVisibleMeshDrawCommand의 배열은 가시성 컬렉션을 통해서만 편리하고 빠르게 분할할 수 있으므로 FMeshDrawCommand 그리기 명령을 플랫한 방식으로 멀티 스레드 시스템 TaskGraph에 제출할 수 있습니다.
FMeshDrawCommand 목록을 정렬하고 StateCache를 추가하여 RHICommandList에 제출되는 명령 수를 줄여 RHICommandList 변환 및 실행에 대한 부하를 줄입니다. 이 단계를 추가하면 Fortnite는 RHI 실행 시간을 20%까지 줄일 수 있습니다.
캐시 일관성 순회. FMeshDrawCommand를 긴밀하게 패키징하고 가볍고 플랫하며 SubmitDraw에 필요한 데이터를 메모리에 지속적으로 저장하여 캐시 및 프리페치 적중률을 향상시킬 수 있습니다.
TChunkedArray<FMeshDrawCommand> MeshDrawCommands;
typedef TArray<FVisibleMeshDrawCommand, SceneRenderingAllocator> FMeshCommandOneFrameArray;
TArray<FMeshDrawShaderBindingsLayout, TInlineAllocator<2>>ShaderLayouts;
typedef TArray<FVertexInputStream, TInlineAllocator<4>>FVertexInputStreamArray;
const int32 NumInlineShaderBindings = 10;
MeshDrawCommandPasses를 RHICommandList 명령으로 변환할 때 병렬 모드가 지원됩니다. 병렬 할당 전략은 단순히 배열을 작업자 스레드 수와 동일한 부분으로 나눈 다음 각 작업자 스레드가 지정된 범위의 그리기 명령을 실행합니다. 이것의 장점은 간단하고 빠르며 이해하기 쉽고 CPU의 캐시 적중률을 향상시킨다는 것입니다. 단점은 각 그룹 내에서 작업 실행 시간에 큰 차이가 있을 수 있다는 것입니다. 이런 방식으로 전체 실행 시간은 가장 긴 그룹에 의해 결정되며, 이로 인해 필연적으로 시간이 길어지고 병렬 효율성이 저하됩니다. 이 문제에 대해 저자는 몇 가지 전략을 제시했습니다.
영감을 주는 전략. 이전 프레임에서 각 MeshDrawCommand의 실행 시간을 기록하고 다음 프레임의 실행 시간에 따라 인접한 MeshDrawCommand를 추가합니다. 그 합이 각 그룹의 평균에 가까워지면 실행 주체의 그룹으로 사용됩니다.
- MeshDrawCommand의 하나 이상의 속성을 검사합니다. 예를 들어, 메시의 면 수나 재질을 그룹화 기준으로 사용하며, MeshDrawCommand의 각 그룹별 검사 속성의 합은 대략 동일합니다.
물론 위 전략은 논리 복잡성을 증가시키고 CPU의 캐시 적중률을 감소시킬 수도 있습니다. 실제 효과는 작동 환경에 따라 다릅니다.
- FMeshDrawCommand::SubmitDraw 프로세스는 PSO 및 템플릿 값을 캐시하여 반복되는 데이터 및 명령이 RHICommandList에 제출되는 것을 방지하고 CPU와 GPU 간의 IO 상호 작용을 줄입니다.
CPU와 GPU 간의 IO 및 렌더링 상태 전환은 항상 실시간 렌더링 분야를 괴롭히는 문제였으며, 특히 CPU와 GPU가 있는 이기종 시스템에서 더욱 그렇습니다. 따라서 CPU와 GPU 간의 데이터 상호 작용을 줄이는 것이 렌더링 성능 최적화를 위한 주요 방법입니다. 캐시 PSO 및 기타 상태를 채택한 후 극단적인 경우 몇 배의 성능 향상을 가져올 수 있습니다.
- FMeshDrawCommand::SubmitDraw는 4개의 드로잉 모델을 지원합니다. 한 차원은 버텍스 인덱스가 있는지 여부이고 다른 차원은 간접 드로잉인지 여부입니다.
간접 추첨 소개
간접 그리기 이전에는 응용 프로그램이 동일한 그리기 호출을 구현하여 여러 개체를 그리려는 경우 GPU 인스턴스만 사용할 수 있었습니다. 그러나 GPU 인스턴스에는 많은 제한 사항이 있습니다. 예를 들어 정확히 동일한 버텍스, 인덱스, 렌더링 상태 및 재질 데이터가 필요하며 다른 변환만 허용됩니다. Atlas에 텍스처를 패킹할 수 있고, Material 속성과 모델 메시를 StructuredBuffer에 패킹할 수 있다고 하더라도, 그릴 때마다 정점의 개수가 동일해야 한다는 치명적인 제한을 피할 수 없습니다. GPU 기반 렌더링 파이프라인을 구현하려면 동일한 수의 정점을 가진 클러스터로 나누어야 합니다.
Indirect Draw 기술의 출현으로 GPU 기반 렌더링 파이프라인은 더욱 단순해지고 효율적이 될 것입니다. 핵심 아이디어는 동일한 그리드에 필요한 리소스 참조를 인수 버퍼에 넣을 수 있도록 하는 것입니다.

다양한 그리드의 인수 버퍼는 더 긴 버퍼를 형성할 수 있습니다.

각 그리드의 데이터는 서로 다른 GPU 스레드에 저장될 수 있으므로 여러 그리드 간의 드로잉을 병렬로 수행할 수 있으며 이는 필연적으로 기존 직렬 드로잉에 비해 효율성이 크게 향상됩니다.

그러나 간접 그리기는 DirectX11, DirecXt12, Vulkan 및 Metal과 같은 최신 그래픽 API에서만 지원됩니다.
3.2.5 RHICommandList에서 GPU로
RHI는 Rendering Hardware Interface(Rendering Hardware Interface)의 약자로 다양한 그래픽 API에 대한 추상화 계층이며 RHICommandList는 그래픽 API와 관련이 없는 중간 계층 그리기 지침 및 데이터를 수집하는 역할을 담당합니다.
RHICommandList는 일련의 중간 그리기 명령을 수집한 후 RHI 스레드의 대상 그래픽 API에 해당하는 인터페이스로 하나씩 변환됩니다. 다음은 FRHICommandList::DrawIndexedPrimitive 인터페이스를 예로 사용합니다.
// Engine\Source\Runtime\RHI\Public\RHICommandList.h
void FRHICommandList::DrawIndexedPrimitive(FRHIIndexBuffer* IndexBuffer, int32 BaseVertexIndex, uint32 FirstInstance, uint32 NumVertices, uint32 StartIndex, uint32 NumPrimitives, uint32 NumInstances)
{
if (!IndexBuffer)
{
UE_LOG(LogRHI, Fatal, TEXT("Tried to call DrawIndexedPrimitive with null IndexBuffer!"));
}
// RHI스레드(Thread)실행(Execute).
if (Bypass())
{
GetContext().RHIDrawIndexedPrimitive(IndexBuffer, BaseVertexIndex, FirstInstance, NumVertices, StartIndex, NumPrimitives, NumInstances);
return;
}
// 생성(Create)드로우/렌더(Draw).
ALLOC_COMMAND(FRHICommandDrawIndexedPrimitive)(IndexBuffer, BaseVertexIndex, FirstInstance, NumVertices, StartIndex, NumPrimitives, NumInstances);
}
// FRHICommandDrawIndexedPrimitive의 선언
FRHICOMMAND_MACRO(FRHICommandDrawIndexedPrimitive)
{
// 의 데이터 .
FRHIIndexBuffer* IndexBuffer;
int32 BaseVertexIndex;
uint32 FirstInstance;
uint32 NumVertices;
uint32 StartIndex;
uint32 NumPrimitives;
uint32 NumInstances;
FRHICommandDrawIndexedPrimitive(FRHIIndexBuffer* InIndexBuffer, int32 InBaseVertexIndex, uint32 InFirstInstance, uint32 InNumVertices, uint32 InStartIndex, uint32 InNumPrimitives, uint32 InNumInstances)
: IndexBuffer(InIndexBuffer)
, BaseVertexIndex(InBaseVertexIndex)
, FirstInstance(InFirstInstance)
, NumVertices(InNumVertices)
, StartIndex(InStartIndex)
, NumPrimitives(InNumPrimitives)
, NumInstances(InNumInstances)
{
}
// 실행(Execute)의 인터페이스 .
RHI_API void Execute(FRHICommandListBase& CmdList);
};
// Engine\Source\Runtime\RHI\Public\RHICommandListCommandExecutes.inl
// FRHICommandDrawIndexedPrimitive의 실행(Execute)인터페이스 구현 .
void FRHICommandDrawIndexedPrimitive::Execute(FRHICommandListBase& CmdList)
{
RHISTAT(DrawIndexedPrimitive);
INTERNAL_DECORATOR(RHIDrawIndexedPrimitive)(IndexBuffer, BaseVertexIndex, FirstInstance, NumVertices, StartIndex, NumPrimitives, NumInstances);
}
// INTERNAL_DECORATOR의 는 호출(Call)RHICommandListIRHICommandContext의 인터페이스 .
#if !defined(INTERNAL_DECORATOR)
#define INTERNAL_DECORATOR(Method) CmdList.GetContext().Method
#endif
// Engine\Source\Runtime\RHI\Public\RHICommandList.h
// 할당(Allocate)RHI의 정의
#define ALLOC_COMMAND(...) new ( AllocCommand(sizeof(__VA_ARGS__), alignof(__VA_ARGS__)) ) __VA_ARGS__
// 할당(Allocate)RHI의 인터페이스 .
void* FRHICommandListBase::AllocCommand(int32 AllocSize, int32 Alignment)
{
checkSlow(!IsExecuting());
// 로부터 메모리 할당(Allocate)메모리 .
FRHICommandBase* Result = (FRHICommandBase*) MemManager.Alloc(AllocSize, Alignment);
++NumCommands;
// 할당(Allocate)의 까지 테이블 의 .
*CommandLink = Result;
CommandLink = &Result->Next;
return Result;
}위에서 볼 수 있듯이 RHICommandList 중간 계층 그리기 명령은 미리 정의된 매크로 FRHICOMMAND_MACRO, INTERNAL_DECORATOR 및 ALLOC_COMMAND를 통해 IRHICommandContext를 통해 해당 그래픽 API로 변환되므로 후속 그리기 명령이 GPU에 제출될 수 있습니다.
3.3 정적 및 동적 그리기 경로
3.3.1 그리기 경로 개요
3.2장에서는 실제로 정적 경로와 동적 경로의 그림자가 여러 개 있지만 그 중 더 많은 부분을 동적 경로 측면에서 설명합니다. 실제로 UE는 정적 메쉬의 렌더링을 최적화하기 위해 정적 렌더링 경로를 분리하여 맞춤형 성능 최적화를 수행할 수 있습니다. 정적 경로는 뷰 정보가 필요한 유형과 뷰 정보가 필요하지 않은 유형으로 구분되며 더 많은 캐시 최적화를 수행할 수 있습니다.

UE에는 3가지 유형의 그리드 그리기 경로가 있습니다(주황색은 각 프레임에 대해 동적으로 생성되고, 파란색은 한 번만 생성된 후 캐시됩니다). 첫 번째는 FPrimitiveSceneProxy에서 RHICommandList까지 모든 프레임을 동적으로 생성하는 동적 그리기 경로로, 효율성은 가장 낮지만 제어성은 가장 높습니다. 두 번째는 View가 필요하고 FMeshBatch 데이터를 캐시할 수 있는 정적 경로입니다. 효율적이고 제어 가능합니다. 세 번째 유형은 보기가 필요하지 않고 FMeshBatch 및 FMeshDrawCommand를 캐시할 수 있는 정적 드로잉 경로입니다. 가장 효율적이지만 제어성이 좋지 않고 충족해야 할 조건이 많습니다.
정적 그리기 경로의 캐시 데이터는 한 번만 생성하면 되므로 렌더링 스레드의 실행 시간이 줄어들고 작업 효율성이 향상될 수 있습니다. 예를 들어 스태틱 메시는 DrawStaticElements 인터페이스를 구현하여 FStaticMeshBatch에 주입되며, 일반적으로 SceneProxy가 씬에 추가될 때 DrawStaticElements가 호출됩니다.
3.3.2 동적 그리기 경로
동적으로 그리기 경로는 캐싱 없이 프레임마다 FMeshBatch 데이터를 다시 작성하므로 확장성은 가장 뛰어나지만 효율성은 가장 낮습니다. 파티클 효과, 골격 애니메이션, 절차적 동적 메시, 프레임마다 데이터를 업데이트해야 하는 메시에 일반적으로 사용됩니다. FMeshBatch는 GetDynamicMeshElements 인터페이스를 통해 수집됩니다. 자세한 내용은 [3.2 모델 드로잉 파이프라인](#3.2 모델 드로잉 파이프라인)을 참조하세요.
FParallelMeshDrawCommandPass는 범용 그리드 패스이며 병렬 및 캐시 렌더링만 지원하므로 성능이 중요한 그리드 패스에만 사용하는 것이 좋습니다. 병렬 또는 캐시된 경로를 사용하려면 InitViews 이후에는 그리드 그리기 명령 및 셰이더에 바인딩된 데이터를 수정할 수 없으므로 엄격한 설계를 거쳐야 합니다. FParallelMeshDrawCommandPass에 대한 코드는 이미 3.2장에 나와 있지만, 그 사용법을 더 자세히 설명하기 위해 여기에 비교적 간단한 그림자 렌더링 예가 있습니다.
// Engine\Source\Runtime\Renderer\Private\ShadowRendering.h
class FProjectedShadowInfo : public FRefCountedObject
{
(......)
// 선언 FParallelMeshDrawCommandPass인스턴스
FParallelMeshDrawCommandPass ShadowDepthPass;
(......)
};
// Engine\Source\Runtime\Renderer\Private\ShadowDepthRendering.cpp
void FProjectedShadowInfo::RenderDepthInner(FRHICommandListImmediate& RHICmdList, FSceneRenderer* SceneRenderer, FBeginShadowRenderPassFunction BeginShadowRenderPass, bool bDoParallelDispatch)
{
(......)
// 병렬
if (bDoParallelDispatch)
{
bool bFlush = CVarRHICmdFlushRenderThreadTasksShadowPass.GetValueOnRenderThread() > 0
|| CVarRHICmdFlushRenderThreadTasks.GetValueOnRenderThread() > 0;
FScopedCommandListWaitForTasks Flusher(bFlush);
{
// 병렬 처리(Process),을(를) 활용하여 의 RHICommandList리스트 。
FShadowParallelCommandListSet ParallelCommandListSet(*ShadowDepthView, SceneRenderer, RHICmdList, CVarRHICmdShadowDeferredContexts.GetValueOnRenderThread() > 0, !bFlush, DrawRenderState, *this, BeginShadowRenderPass);
// 드로우/렌더(Draw)
ShadowDepthPass.DispatchDraw(&ParallelCommandListSet, RHICmdList);
}
}
// 병렬
else
{
ShadowDepthPass.DispatchDraw(nullptr, RHICmdList);
}
}사용법이 아주 간단하고 편리하죠? 이것이 바로 UE가 뒤에서 우리를 위해 많은 캡슐화와 세부 처리를 수행한 이유입니다.
FParallelMeshDrawCommandPass 외에도 그리기 지침을 호출하는 더 간단한 방법인 DrawDynamicMeshPass가 있습니다. DrawDynamicMeshPass는 view/RHICommandList 및 람다 익명 함수만 전달하면 됩니다. 선언 및 사용 예는 다음과 같습니다.
// Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.inl
// DrawDynamicMeshPass의 선언
template<typename LambdaType>
void DrawDynamicMeshPass(const FSceneView& View, FRHICommandList& RHICmdList, const LambdaType& BuildPassProcessorLambda, bool bForceStereoInstancingOff = false);
// Engine\Source\Runtime\Renderer\Private\DepthRendering.cpp
void FDeferredShadingSceneRenderer::RenderPrePassEditorPrimitives(FRHICommandList& RHICmdList, const FViewInfo& View, const FMeshPassProcessorRenderState& DrawRenderState, EDepthDrawingMode DepthDrawingMode, bool bRespectUseAsOccluderFlag)
{
(......)
bool bDirty = false;
if (!View.Family->EngineShowFlags.CompositeEditorPrimitives)
{
const bool bNeedToSwitchVerticalAxis = RHINeedsToSwitchVerticalAxis(ShaderPlatform);
const FScene* LocalScene = Scene;
// 호출(Call)DrawDynamicMeshPass처리(Process)뎁스 Pass。
DrawDynamicMeshPass(View, RHICmdList,
[&View, &DrawRenderState, LocalScene, DepthDrawingMode, bRespectUseAsOccluderFlag](FDynamicPassMeshDrawListContext* DynamicMeshPassContext)
{
FDepthPassMeshProcessor PassMeshProcessor(
LocalScene,
&View,
DrawRenderState,
bRespectUseAsOccluderFlag,
DepthDrawingMode,
false,
DynamicMeshPassContext);
const uint64 DefaultBatchElementMask = ~0ull;
for (int32 MeshIndex = 0; MeshIndex < View.ViewMeshElements.Num(); MeshIndex++)
{
const FMeshBatch& MeshBatch = View.ViewMeshElements[MeshIndex];
PassMeshProcessor.AddMeshBatch(MeshBatch, DefaultBatchElementMask, nullptr);
}
});
(......)
}
}3.3.3 정적 그리기 경로
정적 도면 경로는 일반적으로 캐시될 수 있으므로 캐시된 도면 경로라고도 합니다. 적용 가능한 객체는 정적 모델일 수 있습니다(UE 편집기의 메시 속성 패널에서 지정할 수 있습니다. 아래 그림 참조).

정적 모델은 해당 FPrimitiveSceneInfo가 AddToScene을 호출할 때 캐시됩니다. 다음은 구체적인 처리 코드 및 분석입니다.
// Engine\Source\Runtime\Renderer\Private\PrimitiveSceneInfo.cpp
void FPrimitiveSceneInfo::AddToScene(FRHICommandListImmediate& RHICmdList, FScene* Scene, const TArrayView<FPrimitiveSceneInfo*>& SceneInfos, bool bUpdateStaticDrawLists, bool bAddToStaticDrawLists, bool bAsyncCreateLPIs)
{
(......)
{
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_AddToScene_AddStaticMeshes, FColor::Magenta);
// 처리(Process)정적 모델
if (bUpdateStaticDrawLists)
{
AddStaticMeshes(RHICmdList, Scene, SceneInfos, bAddToStaticDrawLists);
}
}
(......)
}
void FPrimitiveSceneInfo::AddStaticMeshes(FRHICommandListImmediate& RHICmdList, FScene* Scene, const TArrayView<FPrimitiveSceneInfo*>& SceneInfos, bool bAddToStaticDrawLists)
{
LLM_SCOPE(ELLMTag::StaticMesh);
{
// 병렬 처리(Process)정적 。
ParallelForTemplate(SceneInfos.Num(), [Scene, &SceneInfos](int32 Index)
{
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_AddStaticMeshes_DrawStaticElements, FColor::Magenta);
FPrimitiveSceneInfo* SceneInfo = SceneInfos[Index];
// 캐싱(Cache)의 정적 。
FBatchingSPDI BatchingSPDI(SceneInfo);
BatchingSPDI.SetHitProxy(SceneInfo->DefaultDynamicHitProxy);
// 호출(Call)Proxy의 DrawStaticElements인터페이스 ,까지 의 FStaticMeshBatch추가(Add)까지 SceneInfo->StaticMeshes 내에서 。
SceneInfo->Proxy->DrawStaticElements(&BatchingSPDI);
SceneInfo->StaticMeshes.Shrink();
SceneInfo->StaticMeshRelevances.Shrink();
check(SceneInfo->StaticMeshRelevances.Num() == SceneInfo->StaticMeshes.Num());
});
}
{
// PrimitiveSceneInfo의 staticMeshBatch추가(Add)까지 의 StaticMeshe리스트 。
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_AddStaticMeshes_UpdateSceneArrays, FColor::Blue);
for (FPrimitiveSceneInfo* SceneInfo : SceneInfos)
{
for (int32 MeshIndex = 0; MeshIndex < SceneInfo->StaticMeshes.Num(); MeshIndex++)
{
FStaticMeshBatchRelevance& MeshRelevance = SceneInfo->StaticMeshRelevances[MeshIndex];
FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshIndex];
// Add the static mesh to the scene's static mesh list.
// 추가(Add)정적 까지 의 정적 리스트 。
FSparseArrayAllocationInfo SceneArrayAllocation = Scene->StaticMeshes.AddUninitialized();
Scene->StaticMeshes[SceneArrayAllocation.Index] = &Mesh;
Mesh.Id = SceneArrayAllocation.Index;
MeshRelevance.Id = SceneArrayAllocation.Index;
// 처리(Process)의 (만약 의 )。
if (Mesh.bRequiresPerElementVisibility)
{
// Use a separate index into StaticMeshBatchVisibility, since most meshes don't use it
Mesh.BatchVisibilityId = Scene->StaticMeshBatchVisibility.AddUninitialized().Index;
Scene->StaticMeshBatchVisibility[Mesh.BatchVisibilityId] = true;
}
}
}
}
// 캐싱(Cache)정적 의 MeshDrawCommand
if (bAddToStaticDrawLists)
{
CacheMeshDrawCommands(RHICmdList, Scene, SceneInfos);
}
}
void FPrimitiveSceneInfo::CacheMeshDrawCommands(FRHICommandListImmediate& RHICmdList, FScene* Scene, const TArrayView<FPrimitiveSceneInfo*>& SceneInfos)
{
//@todo - only need material uniform buffers to be created since we are going to cache pointers to them
// Any updates (after initial creation) don't need to be forced here
FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions();
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_CacheMeshDrawCommands, FColor::Emerald);
QUICK_SCOPE_CYCLE_COUNTER(STAT_CacheMeshDrawCommands);
FMemMark Mark(FMemStack::Get());
// 병렬 의 스레드(Thread)개수 。
static constexpr int BATCH_SIZE = 64;
const int NumBatches = (SceneInfos.Num() + BATCH_SIZE - 1) / BATCH_SIZE;
// 스레드(Thread)。
auto DoWorkLambda = [Scene, SceneInfos](int32 Index)
{
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_CacheMeshDrawCommand, FColor::Green);
struct FMeshInfoAndIndex
{
int32 InfoIndex;
int32 MeshIndex;
};
TArray<FMeshInfoAndIndex, TMemStackAllocator<>> MeshBatches;
MeshBatches.Reserve(3 * BATCH_SIZE);
// 순회(Traverse/Iterate)현재(Current)스레드(Thread)의 범위 ,개 처리(Process)PrimitiveSceneInfo
int LocalNum = FMath::Min((Index * BATCH_SIZE) + BATCH_SIZE, SceneInfos.Num());
for (int LocalIndex = (Index * BATCH_SIZE); LocalIndex < LocalNum; LocalIndex++)
{
FPrimitiveSceneInfo* SceneInfo = SceneInfos[LocalIndex];
check(SceneInfo->StaticMeshCommandInfos.Num() == 0);
SceneInfo->StaticMeshCommandInfos.AddDefaulted(EMeshPass::Num * SceneInfo->StaticMeshes.Num());
FPrimitiveSceneProxy* SceneProxy = SceneInfo->Proxy;
// 반투명(Translucent)그림자(Shadow)갱신(Update),캐싱(Cache)。
if (!SceneProxy->CastsVolumetricTranslucentShadow())
{
// PrimitiveSceneInfo의 정적 추가(Add)까지 MeshBatch리스트 。
for (int32 MeshIndex = 0; MeshIndex < SceneInfo->StaticMeshes.Num(); MeshIndex++)
{
FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshIndex];
// 검사/감지(Detect)는 지원 캐싱(Cache)MeshDrawCommand
if (SupportsCachingMeshDrawCommands(Mesh))
{
MeshBatches.Add(FMeshInfoAndIndex{ LocalIndex, MeshIndex });
}
}
}
}
// 순회(Traverse/Iterate)정의 Pass,개 정적 의 MeshDrawCommand추가(Add)까지 Pass의 캐싱(Cache)리스트 내에서 。
for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
{
const EShadingPath ShadingPath = Scene->GetShadingPath();
EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;
if ((FPassProcessorManager::GetPassFlags(ShadingPath, PassType) & EMeshPassFlags::CachedMeshCommands) != EMeshPassFlags::None)
{
// 선언 캐싱(Cache)드로우/렌더(Draw)인스턴스
FCachedMeshDrawCommandInfo CommandInfo(PassType);
// 로부터 내에서 페치/가져오기(Fetch)Pass의 ,로써 FCachedPassMeshDrawListContext。
FCriticalSection& CachedMeshDrawCommandLock = Scene->CachedMeshDrawCommandLock[PassType];
FCachedPassMeshDrawList& SceneDrawList = Scene->CachedDrawLists[PassType];
FStateBucketMap& CachedMeshDrawCommandStateBuckets = Scene->CachedMeshDrawCommandStateBuckets[PassType];
FCachedPassMeshDrawListContext CachedPassMeshDrawListContext(CommandInfo, CachedMeshDrawCommandLock, SceneDrawList, CachedMeshDrawCommandStateBuckets, *Scene);
// 생성(Create)Pass의 FMeshPassProcessor
PassProcessorCreateFunction CreateFunction = FPassProcessorManager::GetCreateFunction(ShadingPath, PassType);
FMeshPassProcessor* PassMeshProcessor = CreateFunction(Scene, nullptr, &CachedPassMeshDrawListContext);
if (PassMeshProcessor != nullptr)
{
for (const FMeshInfoAndIndex& MeshAndInfo : MeshBatches)
{
FPrimitiveSceneInfo* SceneInfo = SceneInfos[MeshAndInfo.InfoIndex];
FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshAndInfo.MeshIndex];
CommandInfo = FCachedMeshDrawCommandInfo(PassType);
FStaticMeshBatchRelevance& MeshRelevance = SceneInfo->StaticMeshRelevances[MeshAndInfo.MeshIndex];
check(!MeshRelevance.CommandInfosMask.Get(PassType));
check(!Mesh.bRequiresPerElementVisibility);
uint64 BatchElementMask = ~0ull;
// 추가(Add)MeshBatch까지 PassMeshProcessor,FMeshBatch변환(Transform/Convert)까지 FMeshDrawCommand。
PassMeshProcessor->AddMeshBatch(Mesh, BatchElementMask, SceneInfo->Proxy);
if (CommandInfo.CommandIndex != -1 || CommandInfo.StateBucketId != -1)
{
static_assert(sizeof(MeshRelevance.CommandInfosMask) * 8 >= EMeshPass::Num, "CommandInfosMask is too small to contain all mesh passes.");
MeshRelevance.CommandInfosMask.Set(PassType);
MeshRelevance.CommandInfosBase++;
int CommandInfoIndex = MeshAndInfo.MeshIndex * EMeshPass::Num + PassType;
check(SceneInfo->StaticMeshCommandInfos[CommandInfoIndex].MeshPass == EMeshPass::Num);
// CommandInfo캐싱(Cache)까지 PrimitiveSceneInfo 내에서 。
SceneInfo->StaticMeshCommandInfos[CommandInfoIndex] = CommandInfo;
(......)
}
}
// 소멸 FMeshPassProcessor
PassMeshProcessor->~FMeshPassProcessor();
}
}
}
(......)
};
// 병렬
if (FApp::ShouldUseThreadingForPerformance())
{
ParallelForTemplate(NumBatches, DoWorkLambda, EParallelForFlags::PumpRenderingThread);
}
// 스레드(Thread)
else
{
for (int Idx = 0; Idx < NumBatches; Idx++)
{
DoWorkLambda(Idx);
}
}
FGraphicsMinimalPipelineStateId::InitializePersistentIds();
(.....)
}위 코드에서 볼 수 있듯이 정적 메쉬는 장면에 추가될 때 FMeshBatch를 캐시하고 해당 FMeshDrawCommand를 캐시할 수 있습니다. FMeshDrawCommand 캐싱이 지원되는지 여부를 결정하는 주요 인터페이스는 SupportsCachingMeshDrawCommands입니다. 구현은 다음과 같습니다.
// Engine\Source\Runtime\Engine\Private\PrimitiveSceneProxy.cpp
bool SupportsCachingMeshDrawCommands(const FMeshBatch& MeshBatch)
{
return
// FMeshBatch오직 개 。
(MeshBatch.Elements.Num() == 1) &&
// 포인트 지원 캐싱(Cache)FMeshDrawCommand
MeshBatch.VertexFactory->GetType()->SupportsCachingMeshDrawCommands();
}
// Engine\Source\Runtime\RenderCore\Public\VertexFactory.h
bool FVertexFactoryType::SupportsCachingMeshDrawCommands() const
{
return bSupportsCachingMeshDrawCommands;
}FMeshDrawCommand를 캐시할 수 있는지 여부를 결정하는 조건은 FMeshBatch에 요소가 하나만 있고 이것이 사용하는 버텍스 팩토리가 캐싱을 지원한다는 것을 알 수 있습니다.
현재는 FLocalVertexFactory(UStaticMeshComponent)만 지원하며, 다른 버텍스 팩토리는 뷰를 사용하여 셰이더 바인딩을 설정해야 합니다.
조건이 충족되지 않으면 FMeshDrawCommand를 캐시할 수 없습니다. 보다 자세하게는 다음과 같은 조건을 충족해야 합니다.
패스는 EMeshPass::Type의 열거형입니다.
사용자 정의 메시 패스 프로세서를 등록할 때 EMeshPassFlags::CachedMeshCommands 플래그가 올바르게 전달됩니다.
메쉬 패스 프로세서는 FSceneView에 의존하지 않고 모든 셰이더 바인딩 데이터를 처리할 수 있습니다. 캐싱 중에 FSceneView가 null이기 때문입니다.
캐시된 그리기 명령이 참조하는 모든 데이터는 무효화되고 재생성되어야 한다는 점에 유의해야 합니다.
FPrimitiveSceneInfo::BeginDeferredUpdateStaticMeshes를 호출하면 지정된 그리기 명령이 무효화됩니다.
Scene->bScenesPrimitivesNeedStaticMeshElementUpdate를 true로 설정하면 모든 캐시가 무효화되어 성능에 심각한 영향을 미칩니다. 사용하지 않거나 아껴서 사용하는 것이 좋습니다.
캐시를 무효화하면 렌더링 성능에 영향을 미칩니다. 대안은 패스의 균일 버퍼에 변수 데이터를 넣고 균일 버퍼를 사용하여 다양한 셰이더 로직을 실행하여 뷰 기반 셰이더 바인딩에 대한 종속성을 분리하는 것입니다.
동적 드로잉 경로와는 달리, 정적 메시 요소를 수집할 때 FPrimitiveSceneProxy::DrawStaticElements 인터페이스가 호출됩니다. 이 인터페이스는 특정 하위 클래스에 의해 구현됩니다. 하위 클래스 FStaticMeshSceneProxy의 구현 프로세스를 살펴보겠습니다.
// Engine\Source\Runtime\Engine\Private\StaticMeshRender.cpp
void FStaticMeshSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)
{
checkSlow(IsInParallelRenderingThread());
// 는 활성화(Enable)bUseViewOwnerDepthPriorityGroup
if (!HasViewDependentDPG())
{
// Determine the DPG the primitive should be drawn in.
uint8 PrimitiveDPG = GetStaticDepthPriorityGroup();
int32 NumLODs = RenderData->LODResources.Num();
//Never use the dynamic path in this path, because only unselected elements will use DrawStaticElements
bool bIsMeshElementSelected = false;
const auto FeatureLevel = GetScene().GetFeatureLevel();
const bool IsMobile = IsMobilePlatform(GetScene().GetShaderPlatform());
const int32 NumRuntimeVirtualTextureTypes = RuntimeVirtualTextureMaterialTypes.Num();
//check if a LOD is being forced
if (ForcedLodModel > 0)
{
// 페치/가져오기(Fetch)LOD단계 ()
int32 LODIndex = FMath::Clamp(ForcedLodModel, ClampedMinLOD + 1, NumLODs) - 1;
const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
// 드로우/렌더(Draw)모델 。
for(int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches * (1 + NumRuntimeVirtualTextureTypes));
// 회 의 추가(Add)PDI드로우/렌더(Draw)。
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch BaseMeshBatch;
if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, BaseMeshBatch))
{
(......)
{
// 추가(Add)까지 PDI실행(Execute)드로우/렌더(Draw)
PDI->DrawMesh(BaseMeshBatch, FLT_MAX);
}
}
}
}
}
(......)
}
}DrawStaticElements 인터페이스가 FStaticPrimitiveDrawInterface 인스턴스를 전달하여 PrimitiveSceneProxy의 모든 정적 요소를 수집하는 것을 볼 수 있습니다. FStaticPrimitiveDrawInterface와 그 하위 클래스 FBatchingSPDI의 선언과 구현을 입력하여 실제 내용을 살펴보겠습니다.
// Engine\Source\Runtime\Engine\Public\SceneManagement.h
class FStaticPrimitiveDrawInterface
{
public:
virtual void SetHitProxy(HHitProxy* HitProxy) = 0;
virtual void ReserveMemoryForMeshes(int32 MeshNum) = 0;
// PDI의 드로우/렌더(Draw)인터페이스
virtual void DrawMesh(const FMeshBatch& Mesh, float ScreenSize) = 0;
};
// Engine\Source\Runtime\Renderer\Private\PrimitiveSceneInfo.cpp
class FBatchingSPDI : public FStaticPrimitiveDrawInterface
{
public:
(......)
// 구현 PDI의 드로우/렌더(Draw)인터페이스
virtual void DrawMesh(const FMeshBatch& Mesh, float ScreenSize) final override
{
if (Mesh.HasAnyDrawCalls())
{
FPrimitiveSceneProxy* PrimitiveSceneProxy = PrimitiveSceneInfo->Proxy;
PrimitiveSceneProxy->VerifyUsedMaterial(Mesh.MaterialRenderProxy);
// 생성(Create)의 FStaticMeshBatch인스턴스 ,추가(Add)까지 PrimitiveSceneInfo의 StaticMeshe리스트 내에서 。
FStaticMeshBatch* StaticMesh = new(PrimitiveSceneInfo->StaticMeshes) FStaticMeshBatch(
PrimitiveSceneInfo,
Mesh,
CurrentHitProxy ? CurrentHitProxy->Id : FHitProxyId()
);
const ERHIFeatureLevel::Type FeatureLevel = PrimitiveSceneInfo->Scene->GetFeatureLevel();
StaticMesh->PreparePrimitiveUniformBuffer(PrimitiveSceneProxy, FeatureLevel);
// Volumetric self shadow mesh commands need to be generated every frame, as they depend on single frame uniform buffers with self shadow data.
const bool bSupportsCachingMeshDrawCommands = SupportsCachingMeshDrawCommands(*StaticMesh, FeatureLevel) && !PrimitiveSceneProxy->CastsVolumetricTranslucentShadow();
// 처리(Process)Relevance
bool bUseSkyMaterial = Mesh.MaterialRenderProxy->GetMaterial(FeatureLevel)->IsSky();
bool bUseSingleLayerWaterMaterial = Mesh.MaterialRenderProxy->GetMaterial(FeatureLevel)->GetShadingModels().HasShadingModel(MSM_SingleLayerWater);
FStaticMeshBatchRelevance* StaticMeshRelevance = new(PrimitiveSceneInfo->StaticMeshRelevances) FStaticMeshBatchRelevance(
*StaticMesh,
ScreenSize,
bSupportsCachingMeshDrawCommands,
bUseSkyMaterial,
bUseSingleLayerWaterMaterial,
FeatureLevel
);
}
}
private:
FPrimitiveSceneInfo* PrimitiveSceneInfo;
TRefCountPtr<HHitProxy> CurrentHitProxy;
};FBatchingSPDI::DrawMesh의 주요 기능은 PrimitiveSceneProxy를 FStaticMeshBatch로 변환한 다음 그리드의 관련성 데이터를 처리하는 것입니다.
3.4 렌더링 메커니즘 요약
3.4.1 도면 파이프라인 최적화 기술
이전 장에서는 UE가 그래픽 요소를 컴포넌트에서 최종 드로잉 명령으로 변환하는 방법을 단계별로 자세히 설명했습니다. 이렇게 하는 목적은 주로 렌더링 성능을 향상시키는 것입니다. 요약하자면, 관련된 최적화 기술에는 주로 다음 사항이 포함됩니다.
- 드로우 콜 병합
모든 FMeshDrawCommand는 GPU에 즉시 제출되지 않고 미리 캡처되므로 Draw Call 병합에 유리한 기본 보장을 제공합니다. 그러나 현재 버전의 병합은 D3D11의 특성에 따라 이루어지며 동일한 인스턴스 호출에 병합할지 여부는 셰이더 바인딩에 따라 결정됩니다. D3D12를 기반으로 한 집계 병합은 아직 구현되지 않았습니다.
병합 외에도 정렬을 사용하면 유사한 명령어를 인접한 시간에 그릴 수 있고, CPU 및 GPU 캐시 적중률을 개선하고, 호출되는 명령어 수를 줄일 수 있습니다.
- 동적 인스턴스화
두 개의 그리기 호출을 병합하려면 일관된 셰이더 바인딩이 있어야 합니다(FMeshDrawCommand::MatchesForDynamicInstancing이 true를 반환함).
현재는 캐시된 메시 그리기 명령만 동적으로 인스턴스화되며 FLocalVertexFactory가 캐싱을 지원하는지 여부에 대한 제한이 적용됩니다. 또한 병합을 방해하는 몇 가지 특별한 상황이 있습니다.
라이트맵은 매우 작은 텍스처를 생성합니다(DefaultEngine.ini의 MaxLightmapRadius 매개변수는 조정될 수 있음).
구성요소별 버텍스 색상.
바람 노드가 있는 SpeedTree.
콘솔 명령 r.MeshDrawCommands.LogDynamicInstancingStats 1을 사용하여 동적 인스턴스의 이점을 살펴보세요.
- 평행 드로잉
대부분의 그리드 그리기 작업은 렌더링 스레드에서 실행되지 않지만 TaskGraph 시스템에 의해 병렬로 트리거됩니다. 병렬 부분에는 패스 콘텐츠 설정, 동적 명령 생성/정렬/병합 등이 포함됩니다.
병렬 처리량은 장치를 실행하는 CPU 코어 수에 따라 결정됩니다. 병렬 처리가 켜진 후에는 모든 병렬 스레드가 실행을 완료할 때까지 기다리는 Join 단계가 있습니다(FSceneRenderer::WaitForTasksClearSnapshotsAndDeleteSceneRenderer는 병렬 그리기 대기를 켭니다).
- 캐시된 그림 지침
캐싱의 비율과 효율성을 향상시키기 위해 UE는 동적 개체와 정적 개체의 그리기를 분리하여 각각 동적 그리기 경로와 정적 그리기 경로를 형성합니다. 정적 드로잉 경로는 프리미티브가 장면에 추가될 때 FMeshBatch 및 FMeshDrawCommand를 캐시할 수 있으므로 한 번에 여러 드로잉을 생성하는 높은 효율성을 달성할 수 있습니다.
- 캐시 적중률 향상
CPU 또는 GPU의 캐시는 모두 시간적 지역성과 공간적 지역성의 원칙을 구현합니다. 시간적 지역성은 최근에 액세스한 데이터에 다시 액세스할 경우 캐시 적중 확률이 더 높다는 것을 의미합니다. 공간적 집약성(spatial locality)이란 현재 처리된 데이터 중 인접한 데이터가 캐싱될 확률이 높다는 것을 의미하며, 프리페치 적중률도 포함됩니다.
UE는 다음 방법을 통해 캐시 적중률을 향상시킵니다.
- 객체 지향 디자인이 아닌 데이터 중심 디자인을 기반으로 합니다.
FMeshDrawCommand의 구조 설계와 같은 것입니다.
- 데이터를 지속적으로 저장합니다.
FMeshDrawCommand를 저장하려면 TChunkedArray를 사용하세요.
- 메모리 정렬.
사용자 정의 메모리 정렬기와 메모리 할당자를 사용하세요.
경량 데이터 구조.
데이터에 대한 지속적인 액세스.
그리기 지침을 계속해서 탐색합니다.
- 그리기 지침 정렬.
캐시의 시간적 지역성을 최대한 활용하려면 유사한 명령을 함께 대기열에 넣으세요.
3.4.2 디버그 콘솔 변수
성능 및 동작을 동적으로 설정하거나 디버그하기 위한 병렬 그리기와 관련된 콘솔 명령은 다음과 같습니다.
통제변수분석
r.MeshDrawCommands.ParallelPassSetup 패스를 병렬로 처리하도록 메쉬 그리기 명령을 전환합니다.
r.MeshDrawCommands.UseCachedCommands 그리기 명령 캐싱을 전환합니다.
r.MeshDrawCommands.DynamicInstancing 스위치는 동적으로 인스턴스화됩니다.
r.MeshDrawCommands.LogDynamicInstancingStats 동적 인스턴스화의 이점을 탐색하는 데 자주 사용되는 동적 인스턴스화 데이터를 출력합니다.
r.RHICmdBasePassDeferredContexts 베이스 패스의 평행 그리기를 전환합니다.
3.4.3 제한사항
UE의 현재 모델 그리기 경로에는 많은 단계와 개념이 도입되어 있습니다. 이것의 목적은 렌더링 효율성을 최대한 향상시키는 것입니다. 하지만 이렇게 하면 장점만 있는 것이 아니고 단점도 없습니다. 속담처럼 세상에 공짜 점심은 없습니다. 일반적으로 이러한 렌더링 메커니즘에는 다음과 같은 단점이 있습니다.
시스템이 크고 복잡해 보여 초보자의 학습 비용이 증가합니다.
재건축 및 확장 비용이 증가했습니다. 예를 들어 다중 패스 드로잉을 빠르게 구현하거나 지정된 패스를 추가하는 것은 불가능합니다. 이를 달성하려면 엔진의 기본 소스 코드에 대한 심층적인 이해/익숙함/수정이 필요합니다.
UE의 대규모 렌더링 파이프라인 캡슐화에는 특정 기본 소비가 있습니다. 간단한 애플리케이션 시나리오의 경우 캡슐화되지 않은 렌더링 엔진만큼 성능이 좋지 않을 수 있습니다.
두 가지 권리는 상호 이익이 되며, 이는 UE의 장기적인 절충과 개선의 결과입니다.
하지만 이 렌더링 파이프라인은 가상화된 텍스처 및 형상, RGD, GPU 기반 렌더링 파이프라인, 실시간 레이 트레이싱과 같은 기술을 지원하여 미래 지향적입니다.
3.4.4 이 과제
처음 두 기사에서는 숙제를 할당하지 않았습니다. 이 기사에서는 독자가 UE 렌더링 시스템을 더 깊이 이해하고 마스터할 수 있도록 몇 가지 작은 과제를 할당하기 시작합니다. 이 기사의 작은 숙제는 다음과 같습니다.
모델 드로잉 파이프라인의 프로세스와 설계 개념, 그리고 그 역할을 간결하게 다시 설명합니다.
현재 모델 렌더링 파이프라인에서 어떤 로직을 최적화할 수 있는지 설명해주세요.
원하는 만큼의 재질을 그릴 수 있는 메시 구성요소를 추가합니다.
반투명 및 마스크된 개체의 깊이를 그리려면 전용 패스를 추가하세요.
위의 내용은 모두 공개 질문이며 표준 답변은 없습니다. 아이디어가 있는 학생들은 댓글 영역에 답변을 남겨주시면 최선을 다해 답변해 드리겠습니다.
특별 지침
모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.
이 시리즈의 기사는 저자가 직접 작성한 것이며 블로그에만 게시됩니다. 이 글의 링크를 공유하셔도 좋지만, 무단 전재를 금합니다!
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
📚 참고 자료 및 공식 링크 (References)
- Unreal Engine 4 Sources
- Unreal Engine 4 Documentation
- Rendering and Graphics
- Materials
- Graphics Programming
- Unreal Engine 4 Rendering
- Mesh Drawing Pipeline
- Mesh Drawing Pipeline Conversion Guide for Unreal Engine 4.22
- Refactoring the Mesh Drawing Pipeline for Unreal Engine 4.22 | GDC 2019 | Unreal Engine
- Take a look at Mesh Drawing Pipeline in UE 4.22
- Rendering - Schematic Overview
- UE4 Render System Sheet
- FVisibleMeshDrawCommandRHICommandList
- UE4렌더링
- 언리얼 엔진 4 (UE4)렌더링프로그래밍(Shader)【:MeshDrawPipline】
- UE4 StaticMesh렌더링
- UE4 렌더링 파이프라인
- Hash
- DirectX 12 Multi-Indirect Draw
- Indirect Drawing
- Indirect drawing and GPU culling
- GPU-Driven Rendering
- cache