从源码深入理解Unreal渲染管线

Unreal渲染管线

前言

最近在搞超分相关的工作,发现自己对Unreal渲染管线的理解还是太浅,于是边工作边整理了此文,整篇文章比较长,建议边打开Unreal源码边阅读,跟随文中的顺序能够将Unreal渲染管线错综复杂的源码模块化串联起来,以后有新的发现也会持续更新此文,如果对文章内容有疑问欢迎指正。

游戏与渲染线程

Unreal与渲染管线有关的线程实际上有三条,分别是GameThread、RenderThread和RHIThread,它们都在引擎初始化FEngineLoop::PreInit的时候通过TaskGraphImplementation::Startup由TaskGraph创建,三者的关系是游戏逻辑线程在Tick结束后向渲染线程发送命令进行渲染,渲染线程根据场景内的信息以及渲染配置执行一条完整的渲染管线(比如DeferredRender),在这个过程中的每个Pass都打包成RHICommand发送给RHI线程,RHI线程作为封装层根据实际使用的图形接口等配置将命令转换为对应图形API的Call。

其中我们最关心的“渲染管线“实际上对应的就是RenderThread中运行的逻辑。首先,渲染所需要的所有场景数据都是通过与GameThread中对应类型的映射获取得到的(两个线程各自持有数据,避免thread race),一些基本的类型映射关系如下表,例如负责Mesh网格体的UPrimitiveComponent,它在渲染线程中的映射就是FPrimitiveSceneProxy,其中包括了在MeshCollect阶段中需要用到的顶点等信息,在每帧渲染前GameThread会调用函数将场景中的所有与渲染有关的信息同步到RenderThread上的映射上。场景渲染的基础是FScene,只有注册到FScene内的组件才会被渲染,其中包含了所有的场景渲染信息,并且它将根据渲染设置在每一帧中创建对应的SceneRenderer(每帧都重新创建的临时对象),其中Render为主要的渲染执行函数,Render中会调用渲染各个阶段的Pass具体执行函数。

GameRender
UWorldFScene
UPrimitiveComponentFPrimitiveSceneProxy/FPrimitiveSceneInfo
ULocalPlayerFSceneViewState
ULightComponentFLightSceneProxy/FLightSceneInfo

在整个过程中线程间具体调用方式如下
Game->RenderThread
通过ENQUEUE_RENDER_COMMAN宏,将函数放入渲染队列

ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
	[LocalSceneRenderers = CopyTemp(SceneRenderers), DrawSceneEnqueue](FRHICommandListImmediate& RHICmdList)
	{
		uint64 SceneRenderStart = FPlatformTime::Cycles64();
		const float StartDelayMillisec = FPlatformTime::ToMilliseconds64(SceneRenderStart - DrawSceneEnqueue);
		CSV_CUSTOM_STAT_GLOBAL(DrawSceneCommand_StartDelay, StartDelayMillisec, ECsvCustomStatOp::Set);
		TArray<FSceneRenderer*> SingleSceneRenderer;
		SingleSceneRenderer.AddZeroed(1);

		for (FSceneRenderer* SceneRenderer : LocalSceneRenderers)
		{
			SingleSceneRenderer[0] = SceneRenderer;
			RenderViewFamilies_RenderThread(RHICmdList, SingleSceneRenderer);
			FlushPendingDeleteRHIResources_RenderThread();
		}
	});

RenderThread->RHIThread
通过RDG的AddPass将Pass执行函数加入Graph中并指定依赖,在函数中将命令放入RHICmdList

GraphBuilder.AddPass(
	RDG_EVENT_NAME("DepthPassParallel"),
	PassParameters,
	ERDGPassFlags::Raster | ERDGPassFlags::SkipRenderPass,
	[this, &View, PassParameters](const FRDGPass* InPass, FRHICommandListImmediate& RHICmdList)
    {
	FRDGParallelCommandListSet ParallelCommandListSet(InPass, RHICmdList, GET_STATID(STAT_CLP_Prepass), *this, View, FParallelCommandListBindings(PassParameters));
	ParallelCommandListSet.SetHighPriority();

	View.ParallelMeshDrawCommandPasses[EMeshPass::DepthPass].DispatchDraw(&ParallelCommandListSet, RHICmdList, &PassParameters->InstanceCullingDrawParams);
});

渲染全流程

在这里插入图片描述

GameplayViewport::Draw

我们每帧画面绘制流程的分析从GameThread上的GameplayViewport::Draw开始,在函数中首先会创建这帧绘制的ViewFamily,然后会初始化其中的各值,比如ViewExtension就是在这个时候被填充的,紧接着就会调用各Extension中的SetupViewFamily。然后会处理很多有关动态分辨率、屏幕比例、LocalPlayer相关的内容,并且会进行更新LevelStreaming等,最后会调用RenderModule的BeginRenderViewFamily进入到真正的渲染部分(但还是在GameThread上)

FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues(
	InViewport,
	MyWorld->Scene,
	EngineShowFlags)
	.SetRealtimeUpdate(true));
ViewFamily.ViewExtensions = GEngine->ViewExtensions->GatherActiveExtensions(ViewExtensionContext);
for (auto ViewExt : ViewFamily.ViewExtensions)
{
	ViewExt->SetupViewFamily(ViewFamily);
}

FRenderModule::BeginRenderViewFamily

BeginRenderViewFamily中在真正开始任何渲染工作前首先调用UWorld::SendAllEndOfFrameUpdates将最新的渲染数据同步到渲染线程的代理上

World->SendAllEndOfFrameUpdates();

for (FSceneViewFamily* ViewFamily : ViewFamilies)
{
	for (int ViewExt = 0; ViewExt < ViewFamily->ViewExtensions.Num(); ViewExt++)
	{
		ViewFamily->ViewExtensions[ViewExt]->BeginRenderViewFamily(*ViewFamily);
	}
}

FSceneRenderer::CreateSceneRenderers(ViewFamiliesConst, Canvas->GetHitProxyConsumer(), SceneRenderers);

然后会调用所有ViewExtension的BeginRenderViewFamily,然后是创建SceneRenderer

FScene::UpdatePlanarReflectionContents

接下来在进入RenderThread前还进行了PlanarReflection的渲染,PlanarReflection相当于从反射物体朝反射方向重新渲染一次场景,优点是能够使反射物体反射屏幕视野外的物体,但缺点也很明显,从流程图上可以看出UpdatePlanarReflectionContents内几乎重新走了一遍从Draw开始的整个渲染流程。

if (!bShowHitProxies)
{
	USceneCaptureComponent::UpdateDeferredCaptures(Scene);

	for (int32 ReflectionIndex = 0; ReflectionIndex < Scene->PlanarReflections_GameThread.Num(); ReflectionIndex++)
	{
		UPlanarReflectionComponent* ReflectionComponent = Scene->PlanarReflections_GameThread[ReflectionIndex];
		for (FSceneRenderer* SceneRenderer : SceneRenderers)
		{
         	Scene->UpdatePlanarReflectionContents(ReflectionComponent, *SceneRenderer);
		}
	}
}

ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
	[LocalSceneRenderers = CopyTemp(SceneRenderers), DrawSceneEnqueue](FRHICommandListImmediate& RHICmdList)
	{
		{...}
		for (FSceneRenderer* SceneRenderer : LocalSceneRenderers)
		{
			SingleSceneRenderer[0] = SceneRenderer;
			RenderViewFamilies_RenderThread(RHICmdList, SingleSceneRenderer);
			FlushPendingDeleteRHIResources_RenderThread();
		}
    });
FRenderModule::RenderViewFamily_RenderThread

在真正的渲染线程中,首先会创建RDG,调用每个ViewExtension的PreRenderViewFamily_RenderThread和PreRenderView_RenderThread,然后调用RenderHitProxies或者Render进行多个Pass的实际渲染工作(将Pass指定依赖顺序加入RDG,然后调用RDG.Execute()),最后调用RenderThreadEnd释放渲染资源结束渲染。

//Init AllFamilyView
FSceneRenderer::RenderThreadBegin(RHICmdList, SceneRenderers);
//更新Defererd Resources
FDeferredUpdateResource::UpdateResources(RHICmdList);
for (FSceneRenderer* SceneRenderer : SceneRenderers)
{
	FRDGBuilder GraphBuilder(
		RHICmdList,
		RDG_EVENT_NAME("SceneRenderer_%s(ViewFamily=%s)",
		    ViewFamily.EngineShowFlags.HitProxies ? TEXT("RenderHitProxies") : TEXT("Render"),
		    ViewFamily.bResolveScene ? TEXT("Primary") : TEXT("Auxiliary")
	    ),
		FSceneRenderer::GetRDGParalelExecuteFlags(FeatureLevel)
	);

    FSceneRenderer::ViewExtensionPreRender_RenderThread(GraphBuilder, SceneRenderer);
    	if (ViewFamily.EngineShowFlags.HitProxies)
	{
		SceneRenderer->RenderHitProxies(GraphBuilder);
		bAnyShowHitProxies = true;
	}
	else
	{
		SceneRenderer->Render(GraphBuilder);
	}
	SceneRenderer->FlushCrossGPUFences(GraphBuilder);
	GraphBuilder.Execute();
}
FSceneRenderer::RenderThreadEnd(RHICmdList, SceneRenderers);
FSceneRenderer::Render

在调用AddPostingProcessPass前会先调用各ViewExtension的PrePostProcessPass_RenderThread

ViewFamily.ViewExtensions[ViewExt]->PrePostProcessPass_RenderThread(GraphBuilder, View, PostProcessingInputs);

无论哪个Renderer的Render都会在结束时调用RenderFinish(),其中包括AddDrawCanvasPass绘制UI以及ViewExtension的PostRenderViewFamily_RenderThread和PostRenderView_RenderThread,然后以一个EndScenePass结束整个RDG的构建。不同管线通过继承SceneRenderer重写Render方法来实现不同的渲染流程,这个函数也是渲染管线中最核心的函数,不同管线的Render详细内容在下面的章节中具体分析。

    AddDrawCanvasPass(...);
	for(int32 ViewExt = 0; ViewExt < ViewFamily.ViewExtensions.Num(); ++ViewExt)
	{
		RDG_EVENT_SCOPE(GraphBuilder, "ViewFamilyExtension(%d)", ViewExt);
		ISceneViewExtension& ViewExtension = *ViewFamily.ViewExtensions[ViewExt];
		ViewExtension.PostRenderViewFamily_RenderThread(GraphBuilder, ViewFamily);

		for(int32 ViewIndex = 0; ViewIndex < ViewFamily.Views.Num(); ++ViewIndex)
		{
			RDG_EVENT_SCOPE(GraphBuilder, "ViewExtension(%d)", ViewIndex);
			ViewExtension.PostRenderView_RenderThread(GraphBuilder, Views[ViewIndex]);
		}
	}

	AddPass(GraphBuilder, RDG_EVENT_NAME("EndScene"), [this](FRHICommandListImmediate& InRHICmdList)
	{
		// Notify the RHI we are done rendering a scene.
		InRHICmdList.EndScene();

		if (GDumpMeshDrawCommandMemoryStats)
		{
			GDumpMeshDrawCommandMemoryStats = 0;
			Scene->DumpMeshDrawCommandMemoryStats();
		}
	});

延迟渲染管线

延迟渲染管线是PC端默认的渲染管线,但Unreal的延迟渲染管线并不是标准的延迟渲染(Geometry Pass + Lighting Pass),而是将若干个Pass排列后的定制化管线,可以灵活的添加新功能,但底层源码非常复杂。

UpdateAllPrimitiveInfo

更新图元数据,为InitView中对Mesh的处理做准备

InitView

PreVisibilityFrameSetup

更新静态网格数据,设置运动模糊、TAA的偏移抖动等,做计算可见性图元的预处理工作

ComputeViewVisibility

通过ComputeLightVisibility计算在视锥体内的光源,然后调用UpdateReflectionSceneData更新如SSR以及反射捕获等配置数据,UpdateStaticMeshes更新静态网格数据,查看是否有预计算的可见性数据,如果有就直接存起来。然后开始进行Mesh Culling,包括HLOD层面的Culling、PrimitiveCull和OcculusionCull,最后通过GatherDynamicMeshElements将可见网格根据材质等创建不同的MeshBatch(DrawCall),在SetupMeshPass中创建不同的MeshProcessor将DrawCall发送至RHI线程,并行绘制顶点

UE::Tasks::FTask ComputeLightVisibilityTask = LaunchSceneRenderTask(UE_SOURCE_LOCATION, [this]
{
	ComputeLightVisibility();
});

UpdateReflectionSceneData(Scene);
FPrimitiveSceneInfo::UpdateStaticMeshes(RHICmdList, Scene, UpdatedSceneInfos, EUpdateStaticMeshFlags::AllCommands);
View.PrecomputedVisibilityData = ViewState->GetPrecomputedVisibilityData(View, Scene);

if(bNeedsFrustumCulling)
{
	// Update HLOD transition/visibility states to allow use during distance culling
	FLODSceneTree& HLODTree = Scene->SceneLODHierarchy;
	if (HLODTree.IsActive())
	{
		QUICK_SCOPE_CYCLE_COUNTER(STAT_ViewVisibilityTime_HLODUpdate);
		HLODTree.UpdateVisibilityStates(View);
	}
	else
	{
		HLODTree.ClearVisibilityState(View);
	}
}
{
	TRACE_CPUPROFILER_EVENT_SCOPE(FSceneRenderer_Cull);
	int32 NumCulledPrimitivesForView = PrimitiveCull(Scene, View, bNeedsFrustumCulling);
	STAT(NumCulledPrimitives += NumCulledPrimitivesForView);
}
// Occlusion cull for all primitives in the view frustum, but not in wireframe.
if (!View.Family->EngineShowFlags.Wireframe)
{
	int32 NumOccludedPrimitivesInView = OcclusionCull(RHICmdList, Scene, View, DynamicVertexBuffer);
	STAT(NumOccludedPrimitives += NumOccludedPrimitivesInView);
}
{
	SCOPED_NAMED_EVENT(FSceneRenderer_GatherDynamicMeshElements, FColor::Yellow);
	// Gather FMeshBatches from scene proxies
	GatherDynamicMeshElements(Views, Scene, ViewFamily, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer,
	    HasDynamicMeshElementsMasks, HasDynamicEditorMeshElementsMasks, MeshCollector);
}
SetupMeshPass(View, BasePassDepthStencilAccess, ViewCommands, InstanceCullingManager);
PostVisibilityFrameSetup

调用GatherReflectionCaptureLightMeshElements利用视锥体裁剪筛选开启ReflectionCapture的光源Mesh,然后还会对贴花Mesh进行排序
GatherReflectionCaptureLightMeshElements();

Z-PrePass/Motion Vector Pass

Z-PrePass即Early-Z,一个Depth Only的pass,用于之后的遮挡剔除。如果符合条件的话(没有半透明物体),会在此计算MotionVector

AddClearDepthStencilPass(GraphBuilder, SceneTextures.Depth.Target, DepthLoadAction, StencilLoadAction);
// Draw the scene pre-pass / early z pass, populating the scene depth buffer and HiZ
if (bNeedsPrePass)
{
	RenderPrePass(GraphBuilder, SceneTextures.Depth.Target, InstanceCullingManager);
}
else
{
	// We didn't do the prepass, but we still want the HMD mask if there is one
	RenderPrePassHMD(GraphBuilder, SceneTextures.Depth.Target);
}
GraphBuilder.SetCommandListStat(GET_STATID(STAT_CLM_AfterPrePass));
// special pass for DDM_AllOpaqueNoVelocity, which uses the velocity pass to finish the early depth pass write
if (bShouldRenderVelocities && Scene->EarlyZPassMode == DDM_AllOpaqueNoVelocity)
{
	// Render the velocities of movable objects
	GraphBuilder.SetCommandListStat(GET_STATID(STAT_CLM_Velocity));
	RenderVelocities(GraphBuilder, SceneTextures, EVelocityPass::Opaque, bHairStrandsEnable);
	GraphBuilder.SetCommandListStat(GET_STATID(STAT_CLM_AfterVelocity));
}

PreOcculusion/Pre Atmosphere/Pre Shadow Pass

预计算遮挡、阴影和大气(包括体积云)阶段所需要用到的信息,可以在这里预执行也可以直接放在对应的Pass计算
预计算Occulusion的前提时EarlyZPassMode为DDMAllOcculuders

const bool bOcclusionBeforeBasePass = ((DepthPass.EarlyZPassMode == EDepthDrawingMode::DDM_AllOccluders) || bIsEarlyDepthComplete);

预计算大气主要是计算LUT以及SkyEnvMap,还有体积云

if (SkyAtmospherePassLocation == ESkyAtmospherePassLocation::BeforeBasePass && bShouldRenderSkyAtmosphere)
{
	// Generate the Sky/Atmosphere look up tables
	RenderSkyAtmosphereLookUpTables(GraphBuilder);
}
// Capture the SkyLight using the SkyAtmosphere and VolumetricCloud component if available.
const bool bRealTimeSkyCaptureEnabled = Scene->SkyLight && Scene->SkyLight->bRealTimeCaptureEnabled && Views.Num() > 0 && ViewFamily.EngineShowFlags.SkyLighting;
if (bRealTimeSkyCaptureEnabled)
{
	FViewInfo& MainView = Views[0];
	Scene->AllocateAndCaptureFrameSkyEnvMap(GraphBuilder, *this, MainView, bShouldRenderSkyAtmosphere, bShouldRenderVolumetricCloud, InstanceCullingManager);
}
if (bShouldRenderVolumetricCloud && bAsyncComputeVolumetricCloud && DepthPass.EarlyZPassMode == DDM_AllOpaque && !bHasRayTracedOverlay)
{
    bAsyncComputeVolumetricCloud = RenderVolumetricCloud(GraphBuilder, SceneTextures, bSkipVolumetricRenderTarget, bSkipPerPixelTracing, HalfResolutionDepthCheckerboardMinMaxTexture, true, InstanceCullingManager);
}

预计算阴影的前提是开启前向渲染

if (IsForwardShadingEnabled(ShaderPlatform))
{
	// With forward shading we need to render shadow maps early
	RenderShadowDepthMaps(GraphBuilder, InstanceCullingManager);

	RenderForwardShadowProjections(GraphBuilder, SceneTextures, ForwardScreenSpaceShadowMaskTexture, ForwardScreenSpaceShadowMaskHairTexture);
	// With forward shading we need to render volumetric fog before the base pass
	ComputeVolumetricFog(GraphBuilder, SceneTextures);
}
if (IsForwardShadingEnabled(ShaderPlatform) && bAllowStaticLighting)
{
	RenderIndirectCapsuleShadows(GraphBuilder, SceneTextures);
}

BasePass

VS+RS+PS(包括Culling、坐标变换、插值计算等),申请G-Buffer并将深度、法线、材质等所有需要的信息写入其中,约等于标准延迟渲染中的Geometry pass,对应的光栅化过程可以参考后面Nanite BasePass的分析。

Occulusion Pass

如果没有执行PrePass的话在这里计算AO、体积云

if (!bOcclusionBeforeBasePass)
{
	RenderOcclusionLambda();
}
// If not all depth is written during the prepass, kick off async compute cloud after basepass
if (bShouldRenderVolumetricCloud && bAsyncComputeVolumetricCloud && DepthPass.EarlyZPassMode != DDM_AllOpaque && !bHasRayTracedOverlay)
{
	bAsyncComputeVolumetricCloud = RenderVolumetricCloud(GraphBuilder, SceneTextures, bSkipVolumetricRenderTarget, bSkipPerPixelTracing, HalfResolutionDepthCheckerboardMinMaxTexture, true, InstanceCullingManager);
}

Lighting/Shadow Pass

根据GBuffer中信息计算光照,包括
1.漫反射光照(Diffuse)和环境光(AO)
2.非直接光的胶囊阴影
3.距离场AO
4.lpv间接光照
5.高光反射和天空光照

RenderDiffuseIndirectAndAmbientOcculusion是计算间接Diffuse光照和环境光遮蔽
RenderIndirectCapsuleShadow/RenderDFAOsIndirectShadowing计算胶囊体阴影和DFAO
RenderLights计算的是直接光照
RenderDeferedReflectionAndSkyLighting计算Indirect Specular部分的各种反射和天光
AddSubsurfacePass计算次表面散射(SSR)

if (bRenderDeferredLighting)
{
	RenderDiffuseIndirectAndAmbientOcclusion(
		GraphBuilder,
		SceneTextures,
		LumenFrameTemporaries,
		LightingChannelsTexture,
		bHasLumenLights,
		/* bCompositeRegularLumenOnly = */ false,
		/* bIsVisualizePass = */ false,
		AsyncLumenIndirectLightingOutputs);

	// These modulate the scenecolor output from the basepass, which is assumed to be indirect lighting
	if (bAllowStaticLighting)
	{
		RenderIndirectCapsuleShadows(GraphBuilder, SceneTextures);
	}
	// These modulate the scene color output from the base pass, which is assumed to be indirect lighting
	RenderDFAOAsIndirectShadowing(GraphBuilder, SceneTextures, DynamicBentNormalAOTexture);
	// Clear the translucent lighting volumes before we accumulate
	if ((GbEnableAsyncComputeTranslucencyLightingVolumeClear && GSupportsEfficientAsyncCompute) == false)
	{
		TranslucencyLightingVolumeTextures.Init(GraphBuilder, Views, ERDGPassFlags::Compute);
	}

	RenderLights(GraphBuilder, SceneTextures, TranslucencyLightingVolumeTextures, LightingChannelsTexture, SortedLightSet);

	InjectTranslucencyLightingVolumeAmbientCubemap(GraphBuilder, Views, TranslucencyLightingVolumeTextures);
	FilterTranslucencyLightingVolume(GraphBuilder, Views, TranslucencyLightingVolumeTextures);
	// Do DiffuseIndirectComposite after Lights so that async Lumen work can overlap
	RenderDiffuseIndirectAndAmbientOcclusion(
		GraphBuilder,
		SceneTextures,
		LumenFrameTemporaries,
		LightingChannelsTexture,
		bHasLumenLights,
		/* bCompositeRegularLumenOnly = */ true,
		/* bIsVisualizePass = */ false,
		AsyncLumenIndirectLightingOutputs);

	// Render diffuse sky lighting and reflections that only operate on opaque pixels
	RenderDeferredReflectionsAndSkyLighting(GraphBuilder, SceneTextures, DynamicBentNormalAOTexture);

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
	// Renders debug visualizations for global illumination plugins
	RenderGlobalIlluminationPluginVisualizations(GraphBuilder, LightingChannelsTexture);
#endif

	AddSubsurfacePass(GraphBuilder, SceneTextures, Views);
}

Atmosphere Pass

绘制天空环境,如果之前没有计算体积云则会在这里计算

if (bShouldRenderVolumetricCloud && !bHasRayTracedOverlay)
{
	if (!bAsyncComputeVolumetricCloud)
	{
		RenderVolumetricCloud(GraphBuilder, SceneTextures, bSkipVolumetricRenderTarget, bSkipPerPixelTracing, HalfResolutionDepthCheckerboardMinMaxTexture, false, InstanceCullingManager);
	}
	// Reconstruct the volumetric cloud render target to be ready to compose it over the scene
	ReconstructVolumetricRenderTarget(GraphBuilder, Views, SceneTextures.Depth.Resolve, HalfResolutionDepthCheckerboardMinMaxTexture, bAsyncComputeVolumetricCloud);
}
// Draw the sky atmosphere
if (!bHasRayTracedOverlay && bShouldRenderSkyAtmosphere && !IsForwardShadingEnabled(ShaderPlatform))
{
	RenderSkyAtmosphere(GraphBuilder, SceneTextures);
}
// Draw fog.
if (!bHasRayTracedOverlay && ShouldRenderFog(ViewFamily))
{
	RenderFog(GraphBuilder, SceneTextures, LightShaftOcclusionTexture);
}

Render Traslucency

将所有半透明物体根据相机深度由远及近的被绘制到一张SeparateTranslucencyTexture上(在这里叫TranslucencyResourceMap)
同时因为有半透明物体,所以Velocity被延迟到这里进行计算

FTranslucencyPassResourcesMap TranslucencyResourceMap(Views.Num());
RenderTranslucency(GraphBuilder, SceneTextures, TranslucencyLightingVolumeTextures, &TranslucencyResourceMap, TranslucencyViewsToRender, InstanceCullingManager);
if (bShouldRenderVelocities)
{
	RenderVelocities(GraphBuilder, SceneTextures, EVelocityPass::Translucent, false);
}

Post Processing

所有后处理效果相关的Pass都在这里,Unreal自身支持通过SceneViewExtension模块化的将自己的后处理效果加入渲染管线中而不需要修改引擎源码,FSR和DLSS等插件都是通过这种方式将他们Upscalling Pass加入到后处理的流程中

for (int32 ViewExt = 0; ViewExt < ViewFamily.ViewExtensions.Num(); ++ViewExt)
{
	for (int32 ViewIndex = 0; ViewIndex < ViewFamily.Views.Num(); ++ViewIndex)
	{
	    ViewFamily.ViewExtensions[ViewExt]->PrePostProcessPass_RenderThread(GraphBuilder, View, PostProcessingInputs);
	}
}

for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
	if (IsPostProcessVisualizeCalibrationMaterialEnabled(View))
	{
		AddVisualizeCalibrationMaterialPostProcessingPasses(GraphBuilder, View, PostProcessingInputs, DebugMaterialInterface);
	}
	else
	{
	    AddPostProcessingPasses(
		GraphBuilder,
		View, ViewIndex,
		bAnyLumenActive,
		ViewPipelineState.ReflectionsMethod,
		PostProcessingInputs,
		NaniteResults,
		InstanceCullingManager,
		&VirtualShadowMapArray,
		LumenFrameTemporaries,
		SceneWithoutWaterTextures,
		TSRMoireInput);
	}
}

Nanite渲染管线

Nanite渲染管线的本质是GPU-Driven的渲染管线,Nanite提供了一套从资产导入到数据压缩加载再到 它在Unreal中与现有的延迟渲染管线耦合在一起,

Nanite Mesh Build

有些Mesh适用于Nanite,有些则不适用,对于开启Nanite的Mesh引擎会进行一套Nanite Build流程来生成Nanite所需数据,主要流程如下

Cluster切分

Nanite渲染管线的整个流程中都是以Cluster为最小单位,光栅化时也是对每个Cluster进行,所以将模型进行Cluster划分是Nanite管线的基础。每个Cluster包含128个三角形(符合光栅化阶段Cache的大小);然后组合相邻的若干个Cluster为Cluster Group,Cluster Group是生成各级LOD时Lock Edge的单位(注意Cluster和Cluster Group在每级LOD都是重新划分生成的),同时也是分页数据存储的单位,在存储Cluster时每个页大小为128kb,只有同Group并且相邻的Cluster才能放入同一页中。Cluster的切分起始使用的是LOD0级别的模型数据(切分前也只有LOD0)

Mesh简化和构建BVH

将LOD0的模型切分为若干个Cluster和Cluster Group后,在每个Cluster Group锁定边界的情况下采取QEM算法进行减面并重新划分Cluster,然后由新Cluster组成新一级LOD的Cluster Group,递归的进行此过程来生成多级LOD。
下图在边界不变的情况下将一个Group从4个Cluster减少至2个Cluster,并在这个过程中构建了之后用于剔除的BVH树结构。

BVH树的一层表示一级LOD,每个节点表示一个Cluster,所以一个模型简化的最终结果是只有一个Cluster作为Root节点,面减少到一定情况下后就只有一个Cluster Group。不难看出Cluster Group的划分算法直接决定了减面的效果(因为Cluster Group决定了减面时锁住的边界)。同时BVH树存储了每一层LOD的误差值,这个error值将作为层级选取的重要指标,根据与相机的距离找到某一层节点满足小于最大误差而父节点不满足要求(或者一直到叶子节点即LOD0都无法满足,则直接使用LOD0),按照这个逻辑在BVH树上做出当前需要加载Cluster的一个cut
在这里插入图片描述

数据压缩和Page编码

因为Nanite所面临的模型顶点数量往往特别巨大,所以需要一个兼顾压缩比和解压效率的压缩算法来处理大量顶点数据。一方面从数据特征上可以对不同面密度的Cluster指定不同的存储精度来实现压缩,同时每个顶点的坐标可以用更少的位数存储在LocalSpace的偏移,而且考虑到索引的连续性可以存储标志位来指示索引是否连续节省空间;另一方面因为Cluster中的index序列必然存在大量重复,每个Page可以使用LZ4等通用压缩算法进行压缩。

Nanite Mesh Render

Streaming

既然在存储Nanite Mesh时对数据进行了定制化的压缩,那么在Mesh加载进场景时也就需要进行定制化的Streaming。前面已经提到每个Page上存放的是同一个Group内的顶点数据,或者也可能是一个Group被分到两个Page中。Nanite采用自定义的方式以Page为单位将数据加载进内存,而GPU以Cluster Group为单位加载数据进入显存。除了Root级的数据外,不会将一个物体所有的Cluster数据全部加载进内存,而是在确定采用的LOD层级后以Cluster Group为单位进行加载。
当一个物体出现在视野中时,依据距离CPU查找BVH根据error确定需要请求的是哪一级的Cluster Group,将对应的Page(或者两个Page)加载进入内存,再按照Cluster Group的形式给到GPU。
这个部分可以作为定制引擎模型数据加载的案例深入研究。
UE5-nanite数据流处理_ue5 nanite_VT LI的博客-CSDN博客

Shadow

Virtual Shadow Map,原理与Virtual Texture以及上面提到的Streaming方案类似,生成多级的Shadow Map后,根据当前需要的精度(一个阴影像素对应一个屏幕像素)加载采样对应级别的Shadow Map,这样既能减少显存开销又能生成足够高精度的阴影。

BasePass

注意:BasePass只是输出GBuffer前的计算合集名称,并不是只有一个Shader Pass(由若干个Pass组成)

Nanite的管线是基于延时渲染管线上的,最大的变化就是BasePass部分,Conventional Mesh即传统Mesh在延时渲染管线中的步骤是标准的CPU-Driven渲染管线流程。开启Nanite的Mesh则会采用新的流程,但最终会输出兼容的GBuffer,并执行延时渲染管线中接下来的Lighting等步骤。

Culling

前面的Cluster、Cluster Group、BVH树以及存储和Streaming方案都是在为了高效的Culling铺垫

Instance Culling和传统管线的Culling完全一样,对整个Mesh做视锥体裁剪以及深度测试
Hierarchical Culling实际上是以Cluster Group为单位进行的Culling,会按照BVH从根节点向下进行视锥体裁剪以及深度测试,同时对于通过Culling的Cluster Group还会计算到相机的距离,根据SelfError和ParentError完成之前提到过的LOD层级选择,这个阶段后会输出StreamingPagesBuffer用于指引后续需要加载哪些Group(Page)的数据到显存。
Cluster Culling就是按照BVH树继续对通过Culling的Group进行Cluster Culling,方式依然是视锥体裁剪和深度测试,但这个阶段同时也会评估每个通过测试Cluster的大小并标记,用于确定后续采用硬件光栅还是软件光栅。这个阶段会输出VisibilityClusterBuffer包括需要光栅化的Cluster数据。

值得一提的是Nanite采取了双Pass的Depth Culling策略,在第一个Pass中使用上一帧的Z-Buffer,对通过初步筛选的Cluster建立本帧的Z-Buffer,再用本帧的Z-Buffer对未通过测试的Mesh做第二次Depth Culling。这里的思路就是连续两帧间画面变化不大,并且不该通过本帧Depth Culling的Cluster(第一次筛选出的部分Cluster)被绘制不会影响结果(最终会被覆盖),用第二个Pass来补上本帧新增需要被绘制的Cluster。
同时对于Cluster检测多线程策略采用了一个FIFO队列,不断将通过测试的BVH节点的子节点放入待检测队列,多线程持续从队列中拿去节点进行检测直至队列为空,保证了多个线程均匀分摊检测工作。

Rasterization

在实际光栅化前还会进行小三角形(小于一个像素)剔除和Back Culling,然后对大三角形和非Nanite Mesh使用硬件光栅化,小三角形使用软件光栅化。

硬件光栅化走的是标准的光栅化流程(VS+RS+PS),而软件光栅化除了用Compute Shader模拟计算光栅化外,还需要几个额外的全屏Pass进行Emit Depth Target,因为软件光栅化的结果全部写入了Visbility Buffer,而Depth、Stencil、Velocity和MaterialID需要写入Depth/Stencil/Velocity Buffer来和硬光栅结果对齐。

在输出MaterialID Buffer上Nanite采取了比较特殊的处理方式,对于画面中材质数量较少的情况,Buffer中每个像素存储一个MaterialID值,之后通过对每个材质进行一个全屏后处理Pass去Shading那些MaterialID等于当前材质的像素;但当画面中的材质数量非常大时,就无法在每个材质的全屏后处理Pass遍历对比所有像素。这时就会在Emit Depth阶段后添加一个Compute Shader将屏幕分成64*64的Block,统计每个Block内材质的范围(Min~Max),存储为一张Material Range贴图,之后Shading时就能快速筛选掉不含当前材质的像素块,加快每一个材质Pass的着色速度。

Visibility Buffer的思路其实很简单,就是通过存索引来替代完整的GBuffer以达到减少显存和带宽消耗的目的,Nanite为了和原有延迟管线兼容将通过Emit GBuffer步骤将Visibility Buffer转换为Gbuffer完成之后的着色等步骤。

Emit GBuffer

1.通过Visibility Buffer中存储的Cluster ID和Triangle ID查找VS信息(UV,Normal,Vertex Color等)
2.对顶点信息变化插值得到逐像素的信息
3.根据Material Range贴图,为每个材质进行一次全屏Pass对像素进行Shading计算出GBuffer所需数据(Albedo、Roughness、Normal等)

在管线中的位置与代码实现

UE5中Nanite与传统的Defered管线是整合在一起的,Nanite Mesh Render的工作实际上对应了传统管线中的Vertex Shader+Rasterization,所以它在延迟管线中的位置实际上是介于InitView和Z-PrePass之间的,虽然前面也有一些数据准备等工作,但Culling以及Raster都是在这里完成的

TArray<Nanite::FRasterResults, TInlineAllocator<2>> NaniteRasterResults;
{
	RDG_GPU_STAT_SCOPE(GraphBuilder, NaniteVisbuffer);
	RDG_EVENT_SCOPE(GraphBuilder, "Nanite VisBuffer");

	if (bNaniteEnabled && Views.Num() > 0)
	{
		LLM_SCOPE_BYTAG(Nanite);
		TRACE_CPUPROFILER_EVENT_SCOPE(InitNaniteRaster);

		NaniteRasterResults.AddDefaulted(Views.Num());
		if (NaniteVisibilityQuery != nullptr)
		{
			NaniteVisibility.Get().FinishVisibilityQuery(NaniteVisibilityQuery, NaniteRasterResults[0].VisibilityResults);

			// For now we'll share the same visibility results across all views
			for (int32 ViewIndex = 1; ViewIndex < NaniteRasterResults.Num(); ++ViewIndex)
			{
				NaniteRasterResults[ViewIndex].VisibilityResults = NaniteRasterResults[0].VisibilityResults;
			}

			uint32 TotalRasterBins = 0;
			uint32 VisibleRasterBins = 0;
			NaniteRasterResults[0].VisibilityResults.GetRasterBinStats(VisibleRasterBins, TotalRasterBins);

			uint32 TotalShadingDraws = 0;
			uint32 VisibleShadingDraws = 0;
			NaniteRasterResults[0].VisibilityResults.GetShadingDrawStats(VisibleShadingDraws, TotalShadingDraws);
		}

		const FIntPoint RasterTextureSize = SceneTextures.Depth.Target->Desc.Extent;

		// Primary raster view
        {...}
	}
}

核心部分实际上都集中在Nanite::CullRasterize中

CullRasterize

在CullRasterize中执行的实际上就是MainPass+PostPass的过程
第一部分就是先为MainPass的Culling准备输入参数,这里也可以看到HZBTexture使用的是上一帧的HZB(PrevHZB)

FCullingParameters CullingParameters;
{
	CullingParameters.InViews						= GraphBuilder.CreateSRV(CullingContext.ViewsBuffer);
	CullingParameters.NumViews						= Views.Num();
	CullingParameters.NumPrimaryViews				= NumPrimaryViews;
	CullingParameters.DisocclusionLodScaleFactor	= bDisocclusionHack ? 0.01f : 1.0f;	// TODO: Get rid of this hack
	CullingParameters.HZBTexture					= RegisterExternalTextureWithFallback(GraphBuilder, CullingContext.PrevHZB, GSystemTextures.BlackDummy);
	CullingParameters.HZBSize						= CullingContext.PrevHZB ? CullingContext.PrevHZB->GetDesc().Extent : FVector2f(0.0f);
	CullingParameters.HZBSampler					= TStaticSamplerState< SF_Point, AM_Clamp, AM_Clamp, AM_Clamp >::GetRHI();
	CullingParameters.PageConstants					= CullingContext.PageConstants;
	CullingParameters.MaxCandidateClusters			= Nanite::FGlobalResources::GetMaxCandidateClusters();
	CullingParameters.MaxVisibleClusters			= Nanite::FGlobalResources::GetMaxVisibleClusters();
	CullingParameters.RenderFlags					= CullingContext.RenderFlags;
	CullingParameters.DebugFlags					= CullingContext.DebugFlags;
	CullingParameters.CompactedViewInfo				= nullptr;
	CullingParameters.CompactedViewsAllocation		= nullptr;
}

然后起了一个CS做GPU端的InitArgs工作

auto ComputeShader = SharedContext.ShaderMap->GetShader< FInitArgs_CS >( PermutationVector );

FComputeShaderUtils::AddPass(
	GraphBuilder,
	RDG_EVENT_NAME( "InitArgs" ),
	ComputeShader,
	PassParameters,
	FIntVector( 1, 1, 1 )
);

为准备做裁剪的Node以及Cluster分配空间

AllocateNodesAndBatchesBuffers(GraphBuilder, SharedContext.ShaderMap, &MainAndPostNodesAndClusterBatchesBuffer);

然后就进行MainPass的Cull+Rasterize

{
	RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, !CullingContext.Configuration.bTwoPassOcclusion, "NoOcclusionPass");
	RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, CullingContext.Configuration.bTwoPassOcclusion, "MainPass");

	    AddPass_InstanceHierarchyAndClusterCull(
			GraphBuilder,
			Scene,
			CullingParameters,
			Views,
			NumPrimaryViews,
			SharedContext,
			CullingContext,
			RasterContext,
			RasterState,
			GPUSceneParameters,
			MainAndPostNodesAndClusterBatchesBuffer,
			MainAndPostCandididateClustersBuffer,
			CullingContext.Configuration.bTwoPassOcclusion ? CULLING_PASS_OCCLUSION_MAIN : CULLING_PASS_NO_OCCLUSION,
			VirtualShadowMapArray,
			VirtualTargetParameters
		);

		MainPassBinning = AddPass_Rasterize(
			GraphBuilder,
			RasterPipelines,
			VisibilityResults,
			Views,
			Scene,
			SceneView,
			SharedContext,
			RasterContext,
			RasterState,
			CullingContext.PageConstants,
			CullingContext.RenderFlags,
			CullingContext.ViewsBuffer,
			CullingContext.VisibleClustersSWHW,
			nullptr,
			CullingContext.ClusterCountSWHW,
			CullingContext.ClusterClassifyArgs,
			CullingContext.SafeMainRasterizeArgsSWHW,
			CullingContext.TotalPrevDrawClustersBuffer,
			GPUSceneParameters,
			true,
			VirtualShadowMapArray,
			VirtualTargetParameters
		);
	}

接下来判断是否要做PostPass,如果做的话就先RebuidHZB,再重复上面的Cull+Rasterize(这里有个疑问,为什么它要叫PreviousOcculuderHZB,这不应该是当前这一帧的HZB吗

	if (CullingContext.Configuration.bTwoPassOcclusion)
	{
			BuildHZBFurthest(
				GraphBuilder,
				SceneDepth,
				RasterizedDepth,
				CullingContext.HZBBuildViewRect,
				Scene.GetFeatureLevel(),
				Scene.GetShaderPlatform(),
				TEXT("Nanite.PreviousOccluderHZB"),
				/* OutFurthestHZBTexture = */ &OutFurthestHZBTexture);

			CullingParameters.HZBTexture = OutFurthestHZBTexture;
			CullingParameters.HZBSize = CullingParameters.HZBTexture->Desc.Extent;

            AddPass_InstanceHierarchyAndClusterCull()
            AddPass_Rasterize()
    }
AddPass_InstanceHierarchyAndClusterCull

整个函数体就两个步骤,InstanceCull和NodeAndClusterCull
InstanceCull的条件是有待Cull的Instance或者处于PostPass阶段,InstanceCull和传统管线一样不做深入

if (CullingContext.NumInstancesPreCull > 0 || CullingPass == CULLING_PASS_OCCLUSION_POST)
{
		if( InstanceCullingPass == CULLING_PASS_OCCLUSION_POST )
		{
			PassParameters->IndirectArgs = CullingContext.OccludedInstancesArgs;
			FComputeShaderUtils::AddPass(
				GraphBuilder,
				RDG_EVENT_NAME( "InstanceCull" ),
				ComputeShader,
				PassParameters,
				PassParameters->IndirectArgs,
				0
			);
		}
		else
		{
			FComputeShaderUtils::AddPass(
				GraphBuilder,
				InstanceCullingPass == CULLING_PASS_EXPLICIT_LIST ?	RDG_EVENT_NAME( "InstanceCull - Explicit List" ) : RDG_EVENT_NAME( "InstanceCull" ),
				ComputeShader,
				PassParameters,
				FComputeShaderUtils::GetGroupCountWrapped(CullingContext.NumInstancesPreCull, 64)
			);
		}
}
    AddPass_NodeAndClusterCull(
		GraphBuilder,
		CullingParameters,
		SharedContext,
		CullingContext,
		GPUSceneParameters,
		MainAndPostNodesAndClusterBatchesBuffer,
		MainAndPostCandididateClustersBuffer,
		CullingPass,
		VirtualShadowMapArray,
		VirtualTargetParameters,
		bMultiView);

NodeAndClusterCull中根据线程是否支持我们之前说的按照队列取Node做Cull分成PersistentCull和手动循环每一层做NodeCull然后做ClusterCull两种

	if (GNanitePersistentThreadsCulling)
	{
		AddPass_NodeAndClusterCull( GraphBuilder,
									RDG_EVENT_NAME("PersistentCull"),
									CullingParameters, SharedContext, CullingContext, GPUSceneParameters,
									MainAndPostNodesAndClusterBatchesBuffer, MainAndPostCandididateClustersBuffer,
									VirtualShadowMapArray, VirtualTargetParameters,
									nullptr,
									CullingPass,
									NANITE_CULLING_TYPE_PERSISTENT_NODES_AND_CLUSTERS,
									bMultiView);
    }
    else
    {		const uint32 MaxNodeLevels = 12;	// TODO: Calculate max based on installed pages?
		for (uint32 NodeLevel = 0; NodeLevel < MaxNodeLevels; NodeLevel++)
		{
		    FRDGBufferRef NodeCullArgs = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateIndirectDesc(3), TEXT("Nanite.NodeCullArgs"));
			AddPass_InitCullArgs(GraphBuilder, RDG_EVENT_NAME("InitNodeCullArgs"), SharedContext, CullingContext, NodeCullArgs, CullingPass, NANITE_CULLING_TYPE_NODES);
			
			AddPass_NodeAndClusterCull(
				GraphBuilder,
				RDG_EVENT_NAME("NodeCull_%d", NodeLevel),
				CullingParameters, SharedContext, CullingContext, GPUSceneParameters,
				MainAndPostNodesAndClusterBatchesBuffer, MainAndPostCandididateClustersBuffer,
				VirtualShadowMapArray, VirtualTargetParameters,
				NodeCullArgs,
				CullingPass,
				NANITE_CULLING_TYPE_NODES,
				bMultiView);
		}
		FRDGBufferRef ClusterCullArgs = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateIndirectDesc(3), TEXT("Nanite.ClusterCullArgs"));

		AddPass_InitCullArgs(GraphBuilder, RDG_EVENT_NAME("InitClusterCullArgs"), SharedContext, CullingContext, ClusterCullArgs, CullingPass, NANITE_CULLING_TYPE_CLUSTERS);

		AddPass_NodeAndClusterCull(
			GraphBuilder,
			RDG_EVENT_NAME("ClusterCull"),
			CullingParameters, SharedContext, CullingContext, GPUSceneParameters,
			MainAndPostNodesAndClusterBatchesBuffer, MainAndPostCandididateClustersBuffer,
			VirtualShadowMapArray, VirtualTargetParameters,
			ClusterCullArgs,
			CullingPass,
			NANITE_CULLING_TYPE_CLUSTERS,
			bMultiView);
    }
AddPass_Rasterize

首先与普通光栅流程一样先进行Rasterize Binning,实际上Binning可以不算在真正的Rasterize中,它属于Rasterize的前置工作,以非常粗糙的粒度(比如8*8个Block,比Coarse Raster要更粗糙一级)确定哪些Tile被哪些Triangle覆盖了,最终将所有的Tile分成三类

其中只有Overlap的Tile需要以更低的粒度(Block)进入Rasterize Queue中做进一步的Rasterize,Trivial-Accept的Tile可以直接整个进入后面的Fragment Shading阶段

// Rasterizer Binning
FBinningData BinningData = {};
AddPass_Binning(
	GraphBuilder,
	Scene,
	SharedContext,
	PageConstants,
	RenderFlags,
	VisibleClustersSWHW,
	ClusterOffsetSWHW,
	ClusterCountSWHW,
	ClusterClassifyArgs,
	TotalPrevDrawClustersBuffer,
	GPUSceneParameters,
	bMainPass,
	VirtualShadowMapArray != nullptr,
	bUsePrimitiveShader,
	BinningData
);

根据管线是否可编程以及硬件支持VS或MS,将Shader和参数绑定好,对应的软光栅CS就是FMicropolyRasterizeCS

	struct FRasterizerPass
	{		
        TShaderRef<FHWRasterizePS> RasterPixelShader;
		TShaderRef<FHWRasterizeVS> RasterVertexShader;
		TShaderRef<FHWRasterizeMS> RasterMeshShader;

		TShaderRef<FMicropolyRasterizeCS> RasterComputeShader;
    }
    	// Programmable vertex features
	if (bVertexProgrammable)
	{
		if (bUseMeshShader)
		{
			PermutationVectorMS.Set<FHWRasterizeMS::FVertexProgrammableDim>(bVertexProgrammable);
			PermutationVectorMS.Set<FHWRasterizeMS::FPixelProgrammableDim>(bPixelProgrammable);
			ProgrammableShaderTypes.AddShaderType<FHWRasterizeMS>(PermutationVectorMS.ToDimensionValueId());
		}
		else
		{
			PermutationVectorVS.Set<FHWRasterizeVS::FVertexProgrammableDim>(bVertexProgrammable);
			PermutationVectorVS.Set<FHWRasterizeVS::FPixelProgrammableDim>(bPixelProgrammable);
			ProgrammableShaderTypes.AddShaderType<FHWRasterizeVS>(PermutationVectorVS.ToDimensionValueId());
		}
    }

	// Programmable pixel features
	if (RasterizerPass.bPixelProgrammable)
	{
	    PermutationVectorPS.Set<FHWRasterizePS::FVertexProgrammableDim>(bVertexProgrammable);
		PermutationVectorPS.Set<FHWRasterizePS::FPixelProgrammableDim>(bPixelProgrammable);
		ProgrammableShaderTypes.AddShaderType<FHWRasterizePS>(PermutationVectorPS.ToDimensionValueId());
	}

	// Programmable micropoly features
	if (RasterizerPass.bVertexProgrammable || RasterizerPass.bPixelProgrammable)
	{
	    PermutationVectorCS.Set<FMicropolyRasterizeCS::FTwoSidedDim>(RasterizerPass.RasterPipeline.bIsTwoSided);
		PermutationVectorCS.Set<FMicropolyRasterizeCS::FVertexProgrammableDim>(bVertexProgrammable);
		PermutationVectorCS.Set<FMicropolyRasterizeCS::FPixelProgrammableDim>(bPixelProgrammable);
		ProgrammableShaderTypes.AddShaderType<FMicropolyRasterizeCS>(PermutationVectorCS.ToDimensionValueId());
	}

	auto* RasterPassParameters = GraphBuilder.AllocParameters<FRasterizePassParameters>();
	RasterPassParameters->RenderFlags = RenderFlags;
	if (RasterState.bReverseCulling)
	{
		RasterPassParameters->RenderFlags |= NANITE_RENDER_FLAG_REVERSE_CULLING;
	}

	RasterPassParameters->View = SceneView.ViewUniformBuffer;
	RasterPassParameters->ClusterPageData = GStreamingManager.GetClusterPageDataSRV(GraphBuilder);
	RasterPassParameters->GPUSceneParameters = GPUSceneParameters;
	RasterPassParameters->RasterParameters = RasterParameters;
	RasterPassParameters->VisualizeModeBitMask = RasterContext.VisualizeModeBitMask;
	RasterPassParameters->PageConstants = PageConstants;
	RasterPassParameters->HardwareViewportSize = FVector2f(ViewRect.Width(), ViewRect.Height());
	RasterPassParameters->MaxVisibleClusters = Nanite::FGlobalResources::GetMaxVisibleClusters();
	RasterPassParameters->VisibleClustersSWHW = GraphBuilder.CreateSRV(VisibleClustersSWHW);
	RasterPassParameters->IndirectArgs = BinIndirectArgs;
	RasterPassParameters->InViews = ViewsBuffer != nullptr ? GraphBuilder.CreateSRV(ViewsBuffer) : nullptr;
	RasterPassParameters->InClusterOffsetSWHW = GraphBuilder.CreateSRV(ClusterOffsetSWHW, PF_R32_UINT);
	RasterPassParameters->InTotalPrevDrawClusters = GraphBuilder.CreateSRV(TotalPrevDrawClustersBuffer);
	RasterPassParameters->MaterialSlotTable = Scene.NaniteMaterials[ENaniteMeshPass::BasePass].GetMaterialSlotSRV();
	RasterPassParameters->RasterizerBinData = GraphBuilder.CreateSRV(BinningData.DataBuffer);
	RasterPassParameters->RasterizerBinHeaders = GraphBuilder.CreateSRV(BinningData.HeaderBuffer);

然后分别进行硬件光栅(VS/MS+PS)和软件光栅(CS)

	GraphBuilder.AddPass(
		RDG_EVENT_NAME("HW Rasterize"),
		RasterPassParameters,
		ERDGPassFlags::Raster | ERDGPassFlags::SkipRenderPass,
		[RasterPassParameters, &RasterizerPasses, ViewRect, &SceneView, FixedMaterialProxy, RPInfo, bMainPass, bUsePrimitiveShader, bUseMeshShader](FRHICommandList& RHICmdList)
	{
	    ...
	});

	if (Scheduling != ERasterScheduling::HardwareOnly)
	{
		GraphBuilder.AddPass(
			RDG_EVENT_NAME("SW Rasterize"),
			RasterPassParameters,
			ComputePassFlags,
			[RasterPassParameters, &RasterizerPasses, &SceneView, FixedMaterialProxy](FRHIComputeCommandList& RHICmdList)
		{
            ...
		});
	}

这篇文章以非常简单易懂的方式阐述了普通硬件光栅化的过程
Chasing Triangles in a Tile-based Rasterizer | Tayfun Kayhan

EmitDepthTarget

结束光栅化后,如果需要Z-Prepass的话,则直接输出Depth Buffer

	if (bNeedsPrePass)
	{
		// Emit velocity with depth if not writing it in base pass.
		FRDGTexture* VelocityBuffer = !IsUsingBasePassVelocity(ShaderPlatform) ? SceneTextures.Velocity : nullptr;

		const bool bEmitStencilMask = NANITE_MATERIAL_STENCIL != 0;

		Nanite::EmitDepthTargets(
			GraphBuilder,
			*Scene,
			Views[ViewIndex],
			CullingContext.PageConstants,
			CullingContext.VisibleClustersSWHW,
			CullingContext.ViewsBuffer,
			SceneTextures.Depth.Target,
			RasterContext.VisBuffer64,
			VelocityBuffer,
			RasterResults.MaterialDepth,
			RasterResults.MaterialResolve,
			bNeedsPrePass,
			bEmitStencilMask
		);
	}
BuildFinalHZB

最后再计算一次PostPass后的完整HZB,供之后部分以及下一帧使用

	if (!bIsEarlyDepthComplete && CullingConfig.bTwoPassOcclusion && View.ViewState)
	{
						BuildHZBFurthest(
							GraphBuilder,
							SceneDepth,
							RasterContext.VisBuffer64,
							PrimaryViewRect,
							FeatureLevel,
							ShaderPlatform,
							TEXT("Nanite.HZB"),
							/* OutFurthestHZBTexture = */ &GraphHZB );
	}
  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: Unreal渲染引擎是由Epic Games开发的一款强大的游戏和虚拟现实应用程序的3D渲染引擎。其架构是基于一系列核心模块和子系统,用于处理图形渲染、物理仿真、动画、声音等方面。 在Unreal渲染引擎的架构中,最核心的模块是Render Core,负责管理图形渲染的整体流程。它将渲染任务划分为一个个渲染线程,通过使用多线程并行处理,提高整体渲染性能。在这个模块中,还包含了对图形API的封装,用于与不同的图形硬件进行交互。 另一个重要模块是Scene Management,用于构建和管理场景中的对象。通过使用场景图的数据结构,这个模块可以高效地管理复杂的场景,并支持可视化编辑和实时预览。它还包括了空间分区提高场景对象查询的效率,并提供了灯光、阴影、镜面反射等效果的支持。 为了实现真实的光照效果,Unreal渲染引擎还有一个独立的Lighting模块。这个模块使用了先进的光线追踪和辐射传输算法,并支持动态光源和全局光照。通过提供高质量的光照和阴影效果,使得场景更加逼真和细腻。 此外,Unreal渲染引擎还有许多其他模块,如Animation、Physics、Audio等,用于处理游戏中的动画、物理仿真和声音效果。这些模块都与核心模块紧密合作,共同构建出一个功能强大、性能优越的渲染引擎。 总结来说,Unreal渲染引擎的架构是基于一系列核心模块和子系统构建的。通过高效的多线程渲染、复杂的场景管理、先进的光照算法等功能,它能够提供逼真、流畅的游戏和虚拟现实体验。 ### 回答2: Unreal渲染引擎是一种先进的3D渲染引擎,用于创建高质量、逼真的虚拟世界和游戏环境。它采用了先进的图形技术和渲染算法,具有高度的可定制性和灵活性。 Unreal渲染引擎的架构是基于现代的渲染管线和算法构建的。它包括以下几个关键模块: 1. 场景管理:这个模块负责管理和渲染场景中的各种元素,包括物体、光照、材质等。它维护了一个场景图,用于高效地组织和管理场景中的对象。 2. 光照和阴影:Unreal引擎支持多种光照和阴影算法,包括实时光照、全局光照、阴影映射等。这些算法能够提供逼真的光照效果,并生成高质量的阴影。 3. 材质和纹理:Unreal引擎提供了一个强大的材质系统,用于创建和调整物体的外观。它支持复杂的纹理映射、反射、折射等特性,使物体看起来更加真实。 4. 特效和后期处理:这个模块负责添加各种特效和后期处理效果,如雾化、景深、色彩校正等。它可以增强场景的真实感,并提升图像的质量。 5. 渲染优化:Unreal引擎提供了多种渲染优化技术,用于提高渲染性能并减少资源消耗。这些技术包括级别LOD(细节层次)管理、延迟渲染、遮挡剔除等。 总的来说,Unreal渲染引擎的架构设计旨在提供灵活、高效、逼真的渲染技术,以满足不同类型游戏和虚拟世界的需求。它具有丰富的功能和工具,使开发者能够创造出惊人的视觉效果,并为用户提供极致的游戏体验。 ### 回答3: Unreal渲染引擎是由Epic Games开发的一款高效的游戏和虚拟现实渲染引擎。 Unreal渲染引擎的架构是基于场景图的,它采用了现代的图形技术和算法来实现逼真的实时渲染。其核心架构包括三个组件:场景图、渲染管道和渲染资源。 首先,场景图是一个层次化的结构,用于组织和管理游戏场景中的对象和模型。它包括了场景中的物体、光源、相机等元素。在实时渲染中,场景图的变动会直接影响到最终的渲染结果。 其次,渲染管道是Unreal渲染引擎的核心模块,负责处理图形数据的传递和处理。它由多个阶段组成,包括几何处理、光栅化、像素处理等。在每个阶段,渲染管道会对场景图中的数据进行处理和转换,然后将结果传递给下一个阶段,最终生成最终的渲染图像。 最后,渲染资源是用于存储渲染相关的数据和纹理的组件集合。它包括了模型、材质、纹理等各种资源,这些资源会被渲染管道使用和处理。渲染资源的高效管理和使用是保证渲染性能和效果的重要因素之一。 总之,Unreal渲染引擎的架构是基于场景图的,它使用先进的技术和算法来实现逼真的实时渲染。通过合理的组织和管理场景图中的元素,经过渲染管道的处理,结合高效的渲染资源,Unreal渲染引擎能够提供出色的渲染效果和性能,为游戏和虚拟现实应用提供强大的支持。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值