ue4 改变枢轴位置_UE4 粒子系统底层详解

前言

本文讨论的粒子系统为UE4的CPU粒子,GPU粒子不在本文的讨论范畴之内,文中粒子的渲染流程和逻辑流程以Sprite粒子为例,并且为PC平台(DeferredShadingRenderer)。


UE4 粒子系统的整体框架

11b675e500d7d80626dbaa8a1e161928.png
粒子系统架构

粒子系统可以分为两个大的模块,一个是CPU模块一个是GPU模块,两个模块分别负责不同的职责,简单来说,CPU端负责生成粒子的逻辑数据,比如粒子的位置、大小、颜色、速度等等。GPU端则负责以逻辑数据为基础,将粒子展开成Mesh并且画出来。


GameThread上粒子数据的组织方式

UParticleSystemComponent

CPU端的粒子数据和更新逻辑由UParticleSystemComponent组件来承载,它是一个SceneComponent,带有位置信息,这意味着这个Component可以被挂到任意Actor身上,整个粒子系统的更新入口就是这个UParticleSystemComponent的TickComponent。

UParticleSystem

UParticleSystem就是美术制作的粒子特效静态资源,它包括了粒子系统的基本参数,比如EmitterName、EmitterRenderMode、LockAxisFlag、SignificanceLevel等等。除此之外还有发射器Module,比如SpawnModule、VelocityModule等等,其中SpawnModule和RequiredModule是每个发射器必须的,其他的比如VelocityModule、Const Acceleration等等都是可选的,这些Module相当于是对粒子行为的封装。然后发射器当然也提供了Level of detail的功能,也就是这个UParticleLODLevel,美术同学可以配置不同LOD级数对应的发射器Module,以及他们的参数。

FParticleEmitterInstance

说完了粒子静态资源UParticleSystem,那么FParticleEmitterInstance就是一个发射器的运行时实例,它存储了一个粒子发射器所有的运行时数据,同一个粒子系统可以有多个发射器,UParticleSystemComponent中维护了一个FParticleEmitterInstance数组。这些运行时数据包括了当前LODLevel、ActiveParticles、EmitterTime、PivotOffset等等。

FBaseParticle

发射器再下一层自然就是单个粒子了,FBaseParticle承载了单个粒子的数据组织,一个粒子的CPU数据包括:

7482436ea4423f3ccffe5468c9b6f257.png

FPartcielEmitterInstance作为一个发射器实例,它维护了一个FBaseParticle的缓冲区来存储当前帧的所有粒子信息,FBaseParticle可以看做是单个粒子的运行时快照。FParticleEmitterInstance每一帧都以上一帧所有粒子的快照为起点,计算出当前帧所有粒子的新状态。


RenderThread上粒子数据的组织方式

UParticleSystemComponent是UPrimitiveComponent的子类,在UE渲染和逻辑数据分离的框架下,RenderThread负责管理组件的渲染层数据,GameThread负责管理组件的逻辑层数据。粒子系统渲染层数据的载体是FParticleSystemSceneProxy,它由每一个World对应的FScene来管理,它负责粒子系统整个渲染状态和上下文的创建。

FParticleDynamicData

每一个粒子系统的渲染层运行时数据由FParticleDynamicData来承载,它维护了一个FDynamicEmitterDataBase数组,代表每一个发射器的渲染数据和逻辑。

FDynamicEmitterDataBase

包含了发射器的渲染层数据和逻辑,包括当前渲染帧整个发射器的粒子快照(FDynamicEmitterReplayDataBase),还有其他的一些运行时信息,比如是否使用DynamicParameter、MaterialRenderProxy、发射器当前是否被选中、发射器数据是否已经被初始化等等。逻辑部分,FDynamicEmitterDataBase还决定了每一个粒子由几个三角形组成、并且为粒子生成了VertexBuffer、InstanceBuffer、DynamicParameterBuffer和UniformBuffer,并且生成了对应的MeshBatch。

FDynamicEmitterReplayDataBase

这个表示当前Emitter所拥有的所有粒子的快照,包括所有粒子对应的FBaseParticle、粒子个数、Scale、SortMode、MacroUVOverride参数等等。UE对粒子数据的存取使用了Prefetch指令先将发射器当前访问和下一个相邻的粒子数据载入cache。

FMeshBatch

对于每一个发射器来说,所有粒子使用的材质是一样的,所以FDynamicEmitterDataBase会为该发射器拥有的所有粒子生成FMeshBatch,FMeshBatch里面还存有当前Mesh所对应的材质信息FMaterialRenderProxy,这个在引擎后续的MeshDrawPipeline流程中会用到。


UE4 ParticleSystem逻辑流程

32a18ea8e728825168ef80ff9beeda9c.png
粒子系统逻辑流程

粒子系统的逻辑层主要负责每一帧根据各种Module为每一个发射器的粒子计算新的状态以供渲染层使用。粒子系统的整个逻辑层更新入口是UParticleSystemComponent的TickComponent,它一共做三件事情:

  1. 根据LODDistances里配置的数据和当前PlayerController中玩家的ViewLocation来计算当前应该用哪一个LOD级数。
  2. 更新每一个EmitterInstance。
  3. PostUpdate,处理每一个发射器产生的Event、抛出SignificanceLevelChanged事件(如果SignificanceLevel改变的话),更新BoundingBox。

对于每一个EmitterInstance的更新,这个是逻辑最重的地方,它主要完成了:

  1. 对于已经超过生命周期的粒子,将其清理掉。
  2. 重置所有粒子状态为初始状态,后续会重新计算。
  3. Update所有Module。
  4. 处理新Spawn的粒子。
  5. 更新TypeDataModule。
  6. FinalUpdate所有Module

对于ParticleSpawn,粒子系统会根据SpawnModule计算出当前的SpawnRate,然后乘以DeltaTime计算出当前应该Spawn的粒子个数,取整数,余数累计到LeftOver参数中用作一下次计算。新Spawn出来的粒子会被初始化成InitialLocation、InitialVeloctiy,并且标记为JustSpawn。


UE4 ParticleSystem渲染流程

GameThread和RenderThread数据交互

ca195d6d894c2319e6385977faa315a1.png

当一个UParticleSystemComponent被创建出来的时候,RegisterComponent之后会调用CreateRenderState_Concurrent来创建这个组件的渲染上下文。UParticleSystemComponent创建完之后,在引擎主循环里,每一个逻辑帧执行完后,由UWorld::SendAllEndOfFrameUpdate发起逻辑数据同步到RenderThread,之后会调用DoDeferredRenderUpdates_Concurrent然后分别将渲染Transform提交给FScene,将DynamicData同步给FDynamicEmitterDataBase。

渲染主流程

9d800d1cc32f0fed98c4723daa81ad9e.png

在UE里,粒子系统的渲染流程和普通Mesh的渲染其实差不太多,无非是可见性计算(InitView->ComputeVisibility),然后收集DynamicMesh,最后生成FMeshBatch,然后送入MeshDrawPipeline,然后被发配到对应的RenderingPass中进行渲染。本文渲染流程的测试环境是PC,所使用的FeatureLevel是SM5,也就是FDeferredShadingSceneRenderer渲染器。

渲染层入口是UWorld::SendAllEndOfFrameUpdates,表明当前逻辑帧已经更新完毕,所有的逻辑层数据同步到RenderThread,然后开始渲染,也就是RenderViewFamily_RenderThread,这个函数是一个异步函数,在RenderThread上执行,它是RenderThread上渲染流程开始的入口。省略掉部分细节,首先进入到可见性计算阶段,通过InitViews->ComputeVisibillity,这个函数主要的工作就是计算场景中所有Primitive包括DynamicPrimitive和StaticMesh的可见性,最终生成一个VisibillityMap,标记着每一个Primitive的可见性。对于粒子系统来说,首先要做的是生成所有粒子对应的DynamicMesh并且剔除掉不可见的,也就是FParticleSystemSceneProxy::GatherDynamicMeshElementsEmitter所要做的事情,GatherDynamicMeshElementsEmitter又会调用每个发射器的GetDynamicMeshElementsEmitter。

DynamicMesh生成的细节

以Sprite粒子为例,它是由2个三角形,4个顶点组成,它的VertexFactory是FParticleSpriteVertexFactory,顶点数据流定义可以在FParticleSpriteVertexDeclaration中看到,在顶点的定义中可以看到Sprite粒子一共有6个Stream,分别是Position、OldPosition、Size/Rot/Subimage、Color、TexCoord、DynamicParameter。
不同类型的粒子有不同的VertexFactory和VertexDeclaration,以及生成不同个数的三角形。

a7d2c5e792c3d796c72ece4ff55d1e21.png

然后,我们可以在ParticleSpriteVertexFactory.ush中可以看到VertexShader中顶点的定义:

1750f7b6de86f78f60f9f0145dda2633.png

在CPU端,提交渲染DrawCall之前,实际上已经生成好了IndexBuffer和VertexBuffer,对于相同类型的粒子来说,它拥有相同个数的三角形,所以顶点索引和纹理坐标都是固定的,由引擎预先定义好全局的Buffer(比如, 两个三角形的Sprite粒子IndexBuffer为GParticleIndexBuffer)。但是由于粒子的位置是逻辑层计算出来的,所以需要每一帧在运行时生成VertexBuffer,这个过程可以在FDynamicSpriteEmitterData::GetVertexAndIndexData中看到。

粒子的展开

CPU端的VertexBuffer中只提交了粒子的逻辑位置,并未对粒子进行展开操作,这个操作被放到了VertexShader中,相信也是因为这样性能更好的缘故,我们可以在ParticleSpriteVertexFactory.ush中看到展开过程:

d64f114e6ac13b63a2fc335864d760cb.png

我们可以看到一个Sprite粒子顶点位置是由Size和UV坐标还有PivotOffset计算出来的,结果是一个单位大小的正方形。

Mesh的朝向

CPU只提供粒子的位置,因为粒子是一个点,朝向信息没有物理学含义,粒子展开面片的朝向由发射器参数(ScreenAlignment、AxisLock)和相机位置有关。我们可以在VertexShader代码中看到,粒子朝向是由其Tangent方向决定的,我们可以在GetTangents函数中看到切线的计算方式,根据不同的ScreenAlignment方式来计算切线。


总结

本文分析了UE4 粒子系统的整体代码架构、逻辑流程和渲染流程,不包括GPU粒子和Niagara粒子系统。其中包括了,逻辑层是如何组织数据、提供数据给渲染层,渲染层拿到逻辑数据之后又是如何将粒子渲染出来的。后续计划还会补充GPU粒子和Niagara粒子系统相关部分的知识。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值