【USparkle专栏】假如你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的参加,让才智的火花磕碰交错,让常识的传递生生不息!

一、技能规划背景

Unity引擎自带的粒子体系一直是CPU端核算的,这儿是指粒子体系以下三大过程都是在CPU核算。

粒子体系的主要3个开销大的过程:

  1. 每个发射器每帧创立新粒子实例
  2. 每个粒子实例每帧更新粒子方位、色彩等状态
  3. 每个发射器的制作提交与发射器之间烘托排序

后来硬件的发展GPU提升的更快,而实践项目中常常也是CPU瓶颈居多。所以有了基于ComputeShader与GPUInstance技能的GPU粒子体系。比方Unreal Engine有CPU和GPU 2套,较新版Unity也有VFX。可是挑选自己写一套主要是这几个考量。

  • ComputeShader与GPUInstance结合的技能现已开发过很多次最高收益的功用了,比方海量植被烘托、大世界的GPUTerrain、RealtimeVirtualTexture等,所以算比较有把握。
  • 现有项目现已上线,期望现有美术资源不做人工修改,就能实现与引擎的粒子体系功用共同、算法共同、逻辑架构共同并实现一键批量转换,所以自己按Unity的算法写到ComputeShader更适宜些。
  • 通用的GPU粒子更符合少数发射器产生很多粒子的形式,而实践游戏很少用到这种形式。不论角色技能还是FPS的规划,子弹磕碰作用、弹孔作用等等,全都是发射器数量多,但每个反射器创立的粒子数很少,所以需求用自己定制优化的特殊排序进步功用。

定制高性能GPU粒子系统

基础功用的面板数据

二、单个杂乱粒子形式

这种形式尽管游戏内不太常用,可是功用提升最大,也是开发最简单直观的。而且GitHub现已有Demo,我就不重复写这种形式的代码了。假如觉得我这儿说的不够详细,没有基础代码部分有点晕的同学能够下载这份很短但完好的源码。
github.com/Robert-K/gp…

详细的做法分3个过程:

  1. 在C#脚本中,每帧对这个发射器核算这一帧需求创立的粒子数(依据粒子体系上每秒多少个和 Burst参数),然后需求创立多少个Dispatch、多少个线程数,由于这种形式发射器数量很少,粒子数很大,比方全地图烟雾、全图落叶等。所以CPU核算发射数的工作量十分少,没必要让GPU核算。
  2. 把这种粒子体系当作粒子数量是固定的,比方N,这N便是粒子体系里粒子上限参数。创立长度为N的StructuredBuffer,寄存Particle实例信息的Struct。由于每个实例生命完毕次序不固定,所以需求一个可用粒子池的AppendBuffer来记载Particle数组里哪些Index粒子可被拿来复用。
  3. 每帧对一切粒子实例更新,每个ComputeShader线程处理一个粒子实例。所以不论当前多少个粒子在烘托都是按N来做的。这种粒子一般都是循环N,根本便是要烘托的悉数,只需设置合理,其实并不会糟蹋不行见粒子的空循环,比再用Buffer办理有用粒子,烘托时再跳转反而功用更好。

部分关键代码:

定制高性能GPU粒子系统

Buff内粒子实例数据

定制高性能GPU粒子系统

粒子数据与可用粒子目标池索引变量

这儿需求留意:dead与alive其实对于C#那边同一份Buffer数据。只是在创立粒子的Kernel里消费,在Init与Update的Kernel里Append,由于逝世或初始化都要把粒子设置为可用,便是把Index还给Buffer。

定制高性能GPU粒子系统

创立粒子是消耗可用的粒子Index

定制高性能GPU粒子系统

更新时,假如生命到期就把粒子的Index还给可用Buffer

烘托的时分,数量逻辑相同按粒子体系的设置maxCount作为InstanceCount。其间不行见的粒子用col=pInst.alive*pInst.color,实现躲藏。这种形式绝大部分时分制作的粒子数量就挨近maxCount,所以根本都是alive=true的,很少空核算。

以下是测验成果烘托20w个粒子,这种功用提升是巨大的。Unity的CPU计划107帧 VS GPU实现计划1661帧

色彩不同是由于,Demo的作者在对色彩随生命改变的渐变图转图形时,没考虑用线性空间导致的,不影响功用比照。

定制高性能GPU粒子系统

单个杂乱粒子CPU/GPU计划帧数比照

左面是抓帧证明烘托的粒子数量相同

三、多发射器的简单粒子

这个形式才是我真正为项目开发的形式,也是更能写出功用大收益的形式,老老实实的写很容易负优化。这是由于GPU中的半通明与CPU中的半通明目标很难一同高功用排序,通用引擎为了通用与肯定正确,据我粗略了解,这个问题是无解的(高功用的解),后边会讲怎样定制优化,先看功用比照。

独自200个子弹磕碰特效,每个有6个发射器,所以一共1200粒子反射器,但来回切换激活 同时只显示50%左右(后边按每帧600个粒子更新来算)。Unity CPU版是373FPS,本计划是2461FPS。假如用上个计划的那个GitHub Demo之间做这种,会发现只有100多帧,负优化。所以我没有拿那个源码用,而是自己重新规划了一套符合详细项目的计划。

定制高性能GPU粒子系统

很多发射器实例的形式下

功用比照:Unity CPU粒子(上)

vs 本计划GPU粒子(下)

这是由于单个杂乱粒子形式是每个粒子发射器都创立一个含有粒子数据的Buff,每帧经过Dispatch ComputeShader更新这些粒子,也便是说,这样需求600次Dispatch,功用自然就差了。

所以第一步改善便是申请一个公用的大Buff来寄存当前激活的一切发射器的粒子数据。对于这种数据组织一般有2种形式:一种是间接寻址,一种是每个粒子发射器定长数组占用,然后经过Offset获取自己在Buffer内的数据。

这儿采用第二种,每种发射器最多同时存在32个粒子实例,这样能够满意大部分战斗中重复呈现的很多及时性特效。可是我们上面说Particles是依据粒子创立逝世保护的目标池,数据是无序的。其时是同一个粒子发射器,一次DrawIndirect,所以不需求介意次序。但现在这个数据里有不同的发射器创立的粒子,烘托时也需求访问不同的Index来获取对应数据。所以需求一个RWStructuredBufferparticlesIndexer;来记载每个发射器,包含的粒子在Particles数组中的Index。每个发射器占32位元素,同样烘托的时分,需求用另一个RWStructuredBufferemitterCounter;,这个变量便是用在 DrawMeshInstancedIndirect(Mesh mesh, int submeshIndex, Material material, Bounds bounds, ComputeBuffer bufferWithArgs, int argsOffset); 这个API里的bufferWithArgs,配合后边argsOffset就能实现每个发射器不同的偏移了。

更新函数中,是这样把当前帧需求烘托的活着的粒子写入这2个Buffer的。

定制高性能GPU粒子系统

这样尽管每帧对粒子的Update在一次Dispatch后就履行完了,但烘托的时分,每个发射器独自履行DrawCall还是会功用很差。从Nsight东西能够看到十分恐怖的切换Shader次数,时刻很快是由于我是3080显卡,在一般显卡中这个功用是不具备实际可用性的。

定制高性能GPU粒子系统

每个粒子发射器一次DrawCall的GPU切换情况

四、半通明排序与合批烘托

这是整个技能的关键所在也是最大的对立点,现在的DrawIndirect API每次调用都只能传一个AABB,引擎会依据这个AABB中心参加场景里其他目标进行排序,所以一次DrawIndirect制作的一切粒子具有同一个次序,要么悉数在某目标前,要么悉数在某目标后烘托。现在每个粒子发射器独自一个DrawCall的情况下排序正常了(和Unity自带CPU粒子相同,逐发射器排序正常,不考虑多个发射器之间逐粒子排序),但功用不行。

假如一切同原料发射器合并成一个DrawCall,那么排序又会不正常,由于它们中心呈现场景的半通明目标无法穿插到这个DrawCall里。这也是为什么Unity的GPUInstance文章都是不拿半通明做比方,由于Opaque的排序不正确不影响画面作用,有Depth保证终究次序。通明原料是没有写Depth的,除非用了深度剥离技能。但这说远了,一般不会这样做的,所以怎样合批是要点。

先看下Unity本身是怎样合批粒子的,经过简单测验就能发现,假如ab是相同的粒子发射器的不同实例,c是不同的粒子反射器,ab距离靠近,而c在ab前或在ab后,那么只有2个DrawCall;假如c在ab中心就会有3个DrawCall。所以引擎是排序后才把相邻的又相同的反射器合批烘托。但我们烘托数据是在GPU,假如让CPU排序后要合批,则需求转移Buffer内数据后合并到一同,很杂乱且要改引擎。假如在GPU内排序更不或许,GPU内只能粒子自己排序,无法与场景上目标排序,这些目标都在CPU。所以通用引擎很难解决这个问题。

但做定制开发就轻松多了。首先调查下这些项目中的特效,同一种特效总是呈现在世界空间方位相机的地方,比方一个人开枪的特效总是在他枪口邻近,而子弹的磕碰特效又总是在前方某个方位,不同的玩家是不同的,所以只需用玩家ID+粒子发射器Prefab种类做Key 来分组,Key相同的一次性烘托就能够了。但这个功用很高,需求献身精确度,比方同一个人在玻璃后开几枪,再跑玻璃前面开几枪,那么先创立出的玻璃后的粒子也会一同烘托到玻璃上面。可是这问题不大,由于这些特效都是0.5秒之内就消失的,不会长期逗留在跑动和下次开枪时,但墙上的弹孔是个特例他们会逗留30秒,所以这个计划欠好。

另一个更好的方法是依据世界空间把1立方米内的相同粒子发射器Prefab的一切粒子做一次Draw,由于方位很靠近所以它们按同一个方位参加排序根本是正确的,比较简单的是用long类型把这些信息核算到一同且不重复。假设这儿场景规模是正负5000米,悉数合批发射器用这个办理 Dictionary<long,ParticleEmitterBatch> activeEmitterTypes;。

定制高性能GPU粒子系统

依据方位与发射器类型核算合批烘托的编号

定制高性能GPU粒子系统

分组发射器数据结构

终究介绍该计划的主要数据。由于改用这种合批,这儿有和上面修改的地方。

定制高性能GPU粒子系统

按类型与空间合批烘托的更新方法

  • CreatingEmitter:发射器创立粒子时要传一份发射器数据让粒子初始化时能够知道怎样初始化,比方这个粒子life要从发射器的lifeMin与lifeMax之间随机取一个。
  • Emitters:一切发射器类型数据,由于更新每个粒子时,怎样更新是来自这个数据比方 色彩随生命改变,是把开端色彩和终究色彩记载到发射器的,假如重复的记载到每个粒子那么很糟蹋空间。
  • Particles:一切粒子,里面有激活的有不激活的,烘托哪些是ParticlesIndexer的值来这儿取。
  • ParticlesIndexer:每种发射器记载占MAX_COUNT_PER_EMITTERKIND(我用2048)个元素,记载自己创立的粒子在Particles数组中的实在方位。
  • EmitterCounter:用在DrawIndirect的粒子数量设置, Graphics.DrawMeshInstancedIndirect(quadMesh, 0, item.material, item.aabb, emitterCounter, (5 * item.emitterBatchID) * 4,item.mpb);
  • freePool_a与freePool_c是同一份粒子索引可用池,在不同阶段散布做消费与增加,保护粒子实例的复用。

定制高性能GPU粒子系统

该计划的主要数据

终究看下终究落地作用,从原来开枪掉18帧变成只掉5帧,至此优化几轮的开枪降帧问题终于有点稳住了,之前是根本不能与CSGO比较,他们优化的太好了。

定制高性能GPU粒子系统

终究落地项目

连发35(常见弹夹)后降帧比照

五、GPU的优化

这个GPU粒子主要功用是优化CPU瓶颈,关于GPU的功用优化顺便提下,开战会有很多重叠的多层的大屏幕面积的火焰、烟雾,导致Overdraw问题十分大,调查CSGO与COD有几个简单优化技巧:

  • 特效屏幕空间占比尽量小
  • 用8边形代替Quad作为粒子Mesh,能够大幅度减少PS
  • 假如是多层Billboard叠加,能够离线叠加成序列帧特效,多做几组随机播放
  • 近相机的特效要呈现十分快、消失也十分快,并存时刻要短,不要渐渐消失
  • 对于这种短生命周期的粒子不要用引擎默许保守规则,每帧去判别发射器是否可见。而是在发射时判别当前是否可见(视锥、hiz等),假如不行见直接不创立出粒子,由于创立出的也很快消失,这帧不行见根本能够当作他从出生到逝世都不行见。

这是侑虎科技第1413篇文章,感谢作者jackie 偶然不帅供稿。欢迎转发分享,未经作者授权请勿转载。假如您有任何独特的见地或许发现也欢迎联络我们,一同讨论。(QQ群:465082844)

作者主页:www.zhihu.com/people/jack…

再次感谢jackie 偶然不帅的分享,假如您有任何独特的见地或许发现也欢迎联络我们,一同讨论。(QQ群:465082844)