本文基于UE4版本4.25.3,对Cascade粒子系统的移动端渲染管线进行简单的概括和描述。
Game Thread部分
粒子系统Actor被Spawn(或所在的Level被加载)的时候,UParticleSystemComponent注册和初始化,并通过CreateSceneProxy函数注册和创建FParticleSystemSceneProxy。
UParticleSystemComponent的主要数据成员是TArray<struct FParticleEmitterInstance*> EmitterInstances,包含粒子系统的所有粒子发射器。每一个粒子发射器对应一个FParticleEmitterInstance,管理当前发射器下所有的粒子。
FBaseParticle为粒子数据结构,保存粒子的Location、Rotation、Size和Velocity等数据。FParticleEmitterInstance用uint8* ParticleData指向保存FBaseParticle类型数据集合的连续内存空间。
UParticleSystemComponent关联的所有粒子数据内存空间只由GameThread访问。在帧结束的时候通过UpdateDynamicData函数创建一份包含粒子内存数据的拷贝——FParticleDynamicData,并异步传递给FParticleSystemSceneProxy,供RenderThread渲染使用。
大部分情况下,UE4通过给渲染线程生成一份渲染数据拷贝和函数线程专用(比如函数后缀加_RenderThread)的方式来防止出现多线程渲染的Race Condition问题。
Render Thread部分
在FParticleDynamicData被GameThread传递过来之后,粒子系统的渲染真正开始。
粒子系统的渲染始于FParticleSystemSceneProxy::GetDynamicMeshElements函数,输出FMeshBatchs。FMeshBatch是FParticleSystemSceneProxy到MeshDrawCommand之间的一个中间数据结构,主要包含顶点数据流(FVertexStream)、IndexBuffer和材质等。
粒子系统渲染用到的顶点数据流主要有两个:索引为0的几何数据流和索引为1的Instanced数据流,由FParticleSpriteVertexFactory创建和关联。几何数据流描述每个粒子的几何结构为4个顶点构成的单位大小的正方形。Instanced数据流为粒子数据集合,用于以Instanced的方式绘制这些粒子正方形。
Instanced数据流由FParticleDynamicData初始化而来。因为FParticleDynamicData的数据内容主要是GameThread使用的FBaseParticle类型数据集合的拷贝,所以RenderThread通过FDynamicSpriteEmitterData::GetVertexAndIndexData函数把原类型数据集合转换成渲染使用的FParticleSpriteVertex类型数据集合。FParticleSpriteVertex定义了需要传入GPU的顶点数据结构,与ParticleSpriteVertexFactory.ush 定义的顶点输入类型FVertexFactoryInput对应。
为什么GameThread和RenderThread要分别定义粒子数据结构?GameThread对粒子的抽象是一个Pivot点,更新的是这个Pivot点的位置、旋转和Size等信息。而RernderThread把粒子点几何化为一个4个顶点,2个面构成的单位正方形,渲染的时候基于粒子Pivot点的位置、旋转和Size等信息把粒子的4个顶点计算并绘制出来。
FMeshBatch生成后,进入到MeshDrawCommand的构建阶段。FMeshBatch与MeshDrawCommand可理解为一对n的关系,n的值取决于该Mesh会被多少个EMeshPass处理。比如,产生实时阴影的不透明Mesh会被DepthPass、CSMShadowDepth和BasePass 三个EMeshPass处理来分别构建MeshDrawCommand。
为什么需要定义不同的EMeshPass?不同的EMeshPass绘制关联的顶点数据流,Shader和渲染状态等不一样,比如DepthPass的绘制只需要设置顶点位置数据流(PositionOnlyStream),关联的Shader主要功能为Vertex的空间变换和Pixel的深度输出,不用做光照计算等。
对粒子系统来说,一般不写场景深度,不产生阴影,所以DepthPass和CSMShadowDepth的MeshDrawCommand构建过程会被过滤掉。BasePass的构建始于FMobileSceneRenderer::SetupMobileBasePassAfterShadowInit函数,在关联好Shader,设置好TaskContext渲染状态等之后,调用BuildMeshDrawCommands函数真正进行MeshDrawCommand的构建。
所有相关EMeshPass的MeshDrawCommand构建好之后,开始MeshDrawCommand的提交和绘制。BasePass的绘制始于FMobileSceneRenderer::RenderMobileBasePassBasePass函数,然后根据构建阶段生成的TaskContext渲染状态设置PipelineState,根据MeshDrawCommand关联的顶点数据流设置StreamSource等,最后调用RHICmdList::DrawIndexedPrimitive完成绘制。
构建MeshDrawCommand阶段引擎默认是渲染线程起Work线程来并发执行,可以设置Console命令r.MeshDrawCommands.ParallelPassSetup=0来关掉并发,最后的Mesh绘制可以设置r.RHIThread.Enable=0来关掉RHI线程平台相关的绘制API的调用,这样上述所有阶段都在RenderThread依次顺序执行,方便调试。
PS:UE4引擎的代码量巨大,流程庞杂,单扣一个小细节就可能要研究上好几天,想要用短短几页文字把渲染管线描述清楚难度很大,而且还需要一些写作技巧使得描述不失偏颇,有点理解官网文档的“言简意赅”了(doge~~~
参考文献:
- Mesh Drawing Pipeline
- Threaded Rendering