UE5渲染--GPUScene与合并绘制

UE5渲染--GPUScene与合并绘制

https://zhuanlan.zhihu.com/p/614758211

前言:上一篇分析了Render函数里数据配置部分之MeshDrawPipeline,在正式进入渲染管线(如RenderPrePass)之前,还需执行一些更新操作:GPUScene、InstanceCullingManager、UpdatePhysicsField、PrepareDistanceFieldScene等。本章将分析GPUScene,而这是UE实现合批渲染的关键。

1、GPUScene是什么

顾名思义,GPU场景,是CPU端渲染数据在GPU端的镜像。GPUScene主要创建并更新一系列全局的UniformBuffer,在后续执行各个RenderPass的时候可以直接按PrimitiveId去索引使用,主要包含数据:

  1. Primitive数据:每个Primitive的Shader参数列表,存储了每个Primitive渲染需要的Shader参数;
  2. Instance数据:对于能进行Auto Instance的物体,存储了每个Instance渲染需要的Shader参数;
  3. Payload数据:给Instance使用的,解析为SceneData.ush里的FInstanceSceneData字段;
  4. BVH数据:场景空间结构,用于Cull遮挡剔除;
  5. Lightmap数据:场景光照贴图;

内容挺多的,本章对合批绘制进行分析,涉及Primitive、Instance、Payload。

Primitive:C++的数据类型FPrimitiveUniformShaderParameters(PrimitiveUniformShaderParameters.h)对应Shader的数据FPrimitiveSceneData(SceneData.ush);

Instance:C++的数据类型FInstanceSceneShaderData(InstanceUniformShaderParameters.h)对应Shader的数据FInstanceSceneData(SceneData.ush);

Payload:C++数据类型FPackedBatch、FPackedItem(InstanceCullingLoadBalancer.h)对应Shader的数据类型FPackedInstanceBatch、FPackedInstanceBatchItem(InstanceCullingLoadBalancer.ush)

2、GPUScene数据更新

// 函数调用关系
void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder)
{
	void FGPUScene::Update(FRDGBuilder& GraphBuilder, FScene& Scene, FRDGExternalAccessQueue& ExternalAccessQueue)
	{		
		void FGPUScene::UpdateInternal(FRDGBuilder& GraphBuilder, FScene& Scene, FRDGExternalAccessQueue& ExternalAccessQueue)
	}
}

主要逻辑在FGPUScene::UpdateInternal,其实现如下:

2.1、更新PrimitiveId

调用的Shader为/Engine/Private/GPUScene/GPUSceneDataManagement.usf,通过ComputeShader去更新InstanceSceneData Buffer里对应的PrimitiveId。理一下这里的关系,InstanceSceneData Buffer对应C++的PrimitiveData列表,而Buffer里存储了场景里每个Primitive对应的数据PrimitiveData,而每个PrimitiveData结构里有个PrimitiveId字段。这部分逻辑便是在更新PrimitiveId字段,因为更新的只有一部分数据,所以专门使用一个单独的Pass处理,减少数据量。

2.2、更新PrimitiveData

接下来,对标记为dirty的Primitive进行更新,更新逻辑为找到该Primitive在PrimitiveData Buffer里对应offset位置,对其进行更新。使用模板FUploadDataSourceAdapterScenePrimitives调用UploadGeneral()。

初始化5类Buffer的上传任务TaskContext

后面代码都是在对这个TaskContext进行初始化,然后启动任务,进行数据的上传。

2.2.1、并行更新Primitive数据,将需要更新的PrimitiveCopy到Upload Buffer里,后续通过ComputeShader进行显存的上传然后更新到目标Buffer里;

2.2.2、同理InstanceSceneData以及InstancePayloadData数据的处理。

2.2.3、同理InstanceBVHUploader,LightmapUploader。

2.2.4、最后都会调用每个Uploader的End()方法进行GPU显存的更新。

总结:数据的上传都一样,将需要更新的数据Copy到对应的UploadBuffer对应的位置里,然后通过对应的ComputeShader进行更新到目标Buffer,在函数FRDGAsyncScatterUploadBuffer::End()里实现,截图:

3、GPUScene数据的解析应用

数据的上传更新,是为了使用,那么它们是怎么应用的,分析一下。

3.1、FPrimitiveSceneData

// SceneData.ush
struct FPrimitiveSceneData
{
	uint		Flags; // TODO: Use 16 bits?
	int		InstanceSceneDataOffset; // Link to the range of instances that belong to this primitive
	int		NumInstanceSceneDataEntries;
	int		PersistentPrimitiveIndex;
	uint		SingleCaptureIndex; // TODO: Use 16 bits? 8 bits?
	float3		TilePosition;
	uint		PrimitiveComponentId; // TODO: Refactor to use PersistentPrimitiveIndex, ENGINE USE ONLY - will be removed
	FLWCMatrix	LocalToWorld;
	FLWCInverseMatrix WorldToLocal;
	FLWCMatrix	PreviousLocalToWorld;
	FLWCInverseMatrix PreviousWorldToLocal;
	float3		InvNonUniformScale;
	float		ObjectBoundsX;
	FLWCVector3	ObjectWorldPosition;
	FLWCVector3	ActorWorldPosition;
	float		ObjectRadius;
	uint		LightmapUVIndex;   // TODO: Use 16 bits? // TODO: Move into associated array that disappears if static lighting is disabled
	float3		ObjectOrientation; // TODO: More efficient representation?
	uint		LightmapDataIndex; // TODO: Use 16 bits? // TODO: Move into associated array that disappears if static lighting is disabled
	float4		NonUniformScale;
	float3		PreSkinnedLocalBoundsMin;
	uint		NaniteResourceID;
	float3		PreSkinnedLocalBoundsMax;
	uint		NaniteHierarchyOffset;
	float3		LocalObjectBoundsMin;
	float		ObjectBoundsY;
	float3		LocalObjectBoundsMax;
	float		ObjectBoundsZ;
	uint		InstancePayloadDataOffset;
	uint		InstancePayloadDataStride; // TODO: Use 16 bits? 8 bits?
	float3		InstanceLocalBoundsCenter;
	float3		InstanceLocalBoundsExtent;
	float3		WireframeColor; // TODO: Should refactor out all editor data into a separate buffer
	float3		LevelColor; // TODO: Should refactor out all editor data into a separate buffer
	uint		PackedNaniteFlags;
	float2 		InstanceDrawDistanceMinMaxSquared;
	float		InstanceWPODisableDistanceSquared;
	uint		NaniteRayTracingDataOffset;
	float3		Unused;
	float		BoundsScale;
	float4		CustomPrimitiveData[NUM_CUSTOM_PRIMITIVE_DATA]; // TODO: Move to associated array to shrink primitive data and pack cachelines more effectively
};

以上是Primitive包含的字段,这里包含了渲染一个物体的全部数据。普通地渲染一个物体,就可以从这里取到需要的数据,然后进行Shader计算,如下BasePassPixelShader.usf的部分截图:

3.2、FInstanceSceneData

// SceneData.ush
struct FInstanceSceneData
{
	FLWCMatrix LocalToWorld;
	FLWCMatrix PrevLocalToWorld;
	FLWCInverseMatrix WorldToLocal;
	float4   NonUniformScale;
	float3   InvNonUniformScale;
	float    DeterminantSign;
	float3   LocalBoundsCenter;
	uint     PrimitiveId;
	uint     RelativeId;
	uint     PayloadDataOffset;
	float3   LocalBoundsExtent;
	uint     LastUpdateSceneFrameNumber;
	uint     NaniteRuntimeResourceID;
	uint     NaniteHierarchyOffset;
#if USES_PER_INSTANCE_RANDOM || USE_DITHERED_LOD_TRANSITION
	float    RandomID;
#endif
#if ENABLE_PER_INSTANCE_CUSTOM_DATA
	uint     CustomDataOffset;
	uint     CustomDataCount;
#endif
#if 1 //NEEDS_LIGHTMAP_COORDINATE // TODO: Fix Me
	float4   LightMapAndShadowMapUVBias;
#endif
	bool     ValidInstance;
	uint     Flags;

#if USE_EDITOR_SHADERS
	FInstanceSceneEditorData EditorData;
#endif
};

这个InstanceData是进行AutoInstance渲染的物体们,渲染每个Instance时使用的数据结构。比如有3个物体进行了instance合批,Shader在InstanceData Buffer里,通过InstanceId可以取到对应的InstanceData,然后使用该数据进行该Instance物体的渲染。而且InstanceData中还有个PrimitiveId字段,在渲染的时候还可以通过PrimitiveId进一步从PrimitiveData取数据,从而得到更丰富的渲染参数。

3.3、Payload数据,对应Shader文件是InstanceCullingLoadBalancer.ush,后续分析InstanceCulling再深入这里。

4、MeshDrawCommand合批的补充

经过可见性相关性,进行SetupMeshPass之后,对每个Pass调用DispatchPassSetup,然后创建FMeshDrawCommandPassSetupTask任务,这个Task任务通过MeshPassProcessor生成一个个MeshDrawCommand,这里的MeshDrawCommand还没有合批的;

生成完成后会进行排序,为的是让相同的Command能连续存放在一起:

InstaceCullingContext.cpp

之后进行Instance合批:

InstaceCullingContext.cpp

合批的计算方法是,通过对比MeshDrawCommand的 StateBucketId 是否相等:

合批渲染分析到这里了。

总结:GPUScene维护了这个场景的必要数据,以提供Primitive Buffer、Instance Buffer用于渲染着色,以及BVH用于GPU Culling。

优点:

  1. 从C++层面,这些数据都是面向数据,缓存命中友好;
  2. 像大部分StaticMesh,上传到Primitive Buffer后,后续相关物体DrawCall时,只需绑定使用即可;
  3. 提供了Instance支持,减少DrawCall;
  4. 后续光追特性也需要场景数据(就算不在可见范围的物体也有GI影响的),提供了基础;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值