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去索引使用,主要包含数据:
- Primitive数据:每个Primitive的Shader参数列表,存储了每个Primitive渲染需要的Shader参数;
- Instance数据:对于能进行Auto Instance的物体,存储了每个Instance渲染需要的Shader参数;
- Payload数据:给Instance使用的,解析为SceneData.ush里的FInstanceSceneData字段;
- BVH数据:场景空间结构,用于Cull遮挡剔除;
- 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。
优点:
- 从C++层面,这些数据都是面向数据,缓存命中友好;
- 像大部分StaticMesh,上传到Primitive Buffer后,后续相关物体DrawCall时,只需绑定使用即可;
- 提供了Instance支持,减少DrawCall;
- 后续光追特性也需要场景数据(就算不在可见范围的物体也有GI影响的),提供了基础;