今年9月,Glu mobile明斯克办事处Unstoppable的《泰坦世界》手机游戏即将发布。 该项目在全球发布之前就被取消了。 但是,在丹尼斯·兹多诺夫(Dennis Zdonov)和亚历克斯·帕利(Alex Paley)工作室负责人的允许下,我取得的成就仍然是最有趣的,我想与公众分享。在2018年3月,我和团队负责人举行了会议,讨论了下一步工作:渲染代码已完成,并且计划中没有新功能和特殊效果。 从头开始重写粒子系统似乎是合乎逻辑的选择-根据所有测试,它最大程度地降低了生产率,此外,它还因其界面(文本配置文件)和极其微弱的功能而使设计师疯狂。
应该注意的是,大多数时候,团队都是在“明天发布”模式下进行游戏开发的,所以我编写了所有子系统,首先,试图不破坏已经运行的子系统,其次,开发周期很短。 特别是,常规系统无法执行的大多数效果是在片段着色器中完成的,而不会影响主代码。
对粒子数量的限制(每个粒子的转换矩阵都在cpu上形成,结论是通过gl可扩展ios的安装程序完成的),例如,有必要编写一个着色器,基于对象形状的解析表示并与空间混合,“模拟”大量粒子将假数据移入深度缓冲区。
就像我们在绘制一个球体一样,该片段的z坐标是针对平面粒子计算的,并且考虑到时间,该球体的半径由Perlin噪声的正弦调制:
r=.5+.5*sin(perlin(specialUV)+time)
关于球体深度重建的完整描述可以在
ÍñigoQuílez上找到,但是我使用了一个简化的,更快的代码。 当然,他是一个大概的近似值,但是在复杂的几何形状(烟,爆炸)上,他给出了相当不错的图像。
游戏截图。 烟雾“裙”被制成一小部分,爆炸的主体上还剩下几根。 当然,当烟雾轻轻地笼罩建筑物和单位时,它看起来“最引人注目”,但是,爆炸期间更改摄像机位置的建议并未付诸实施。问题陈述
您想走什么路? 相反,我们摆脱了在以前的粒子系统上受到折磨的限制。 由于帧预算几乎用尽,并且在较弱的设备(例如ipad air)上,像素和顶点传送带都已满载,这一情况使情况更加恶化。 因此,即使我的功能有所限制,我也希望获得最高效的系统。
设计师根据自己的经验和实践,结合统一,虚幻和后效应,绘制了功能列表并绘制了UI草图。
可用技术
由于总部的遗留和限制,我们只能使用opengl es2。因此,现代粒子系统中使用的诸如转换反馈之类的技术不可用。
还剩下什么? 使用顶点纹理获取并在纹理中存储位置/加速度? 一个可行的选择,但是内存也快要用完了,这种解决方案的性能并不是最佳的,其结果在建筑美感上也没有什么不同。
到这个时候,我已经阅读了许多关于在gpu上实现粒子系统的文章。 绝大多数包含一个明亮的标题(“移动gpu上有数以百万计的粒子,带有偏好和诗人”),但是,实现过程归结为简单的示例,尽管看上去很有趣,但看上去很有趣,而且在游戏中的实际使用几乎毫无用处。
本文带来了最大的好处:作者解决了实际的问题,并且没有做“真空中的球形粒子”。 本文中的基准数字和性能分析结果在设计阶段节省了大量时间。
寻找方法
我首先对粒子系统解决的问题进行分类,然后搜索特殊情况。 结果大致如下(与团队负责人的往来是该概念的真正码头):
“-具有循环运动的粒子/网格阵列。 整个运动方程式都没有加工位置。 应用-来自管道的烟雾,水蒸气,雪/雨,大雾,树木摇曳,也可以部分用于爆炸的非周期性影响。
-胶带。 通过事件形成vb,仅在GPU上进行处理(射线射击,沿着带轨迹的固定(?)轨迹飞行)。 也许是将开始完成坐标转移到制服的变体,并且通过vertexID构建磁带的工作将会开始。 与t.z. 与菲涅尔交叉,如在Directlights + uvscroll上渲染。
-粒子生成和速度处理。 最通用,最困难/最慢的选项,请参阅技术运动处理。”
简而言之:存在不同的粒子效果,其中某些效果比其他效果更容易实现。
我们决定将任务分为几个迭代-从简单到复杂。 由于这样的开发速度要高出几个数量级,因此我的引擎/编辑器在Windows / directx11下完成了原型制作。 该项目仅需几秒钟即可完成编译,并且可以实时对着色器进行完全编辑并在后台进行编译,从而实时显示结果,而无需按下按钮之类的任何其他手势。 我认为,使用一堆macbook / xcode构建大型项目的任何人都会理解做出此决定的原因。
所有代码示例均来自Windows原型。
Windows的开发环境。实作
第一阶段是粒子阵列的静态输出。 没什么复杂的:启动顶点缓冲,用四边形填充(为每个四边形写入正确的uv),然后将顶点id缝到“其他” uv中。 之后,在着色器中,通过基于发射器设置的顶点id,我们形成粒子的位置,通过uv,我们恢复屏幕坐标。
如果vertex_id本机可用,则完全可以在没有缓冲区且没有uv的情况下还原屏幕坐标(因此,它是在Windows版本中完成的)。
着色器:
struct VS_INPUT { … uint v_id:SV_VertexID; … } //float index = input.uv2.x/6.0;// vertex_id index = floor(input.v_id/6.0);// vertex_id float2 map[6]={0,0,1,0,1,1,0,0,1,1,0,1}; float2 quaduv=map[frac(input.v_id/6.0)*6];
之后,您可以用很少的代码来实现简单的方案,例如,偏差很小的循环运动适合下雪效果。 但是,我们的目标是将粒子行为的控制权交给美术师,而正如您所知,他们很少知道如何着色。 具有行为预设和通过滑块编辑参数的选项也没有吸引力-切换着色器或在内部分支,增加预设选项,缺乏完全控制。
下一个任务是为此类系统实现淡入/淡出。 粒子不应从无处出现而消失在无处。 在粒子系统的经典实现中,我们使用cpu以编程方式处理缓冲区,创建新粒子并删除旧粒子。 实际上,要获得良好的性能,您需要编写一个智能的内存管理器。 但是,如果您只是不绘制“死”粒子会怎样?
假设(对于初学者而言)粒子发射的时间间隔和粒子的寿命在单个发射器内是恒定的。

然后,我们可以推测性地将我们的缓冲区(仅包含顶点ID)呈现为圆形,并确定其最大大小,如下所示:
pCount = round (prtPerSec * LifeTime / 60.0); pCountT = floor (prtPerSec * EmissionEndTime / 60.0); pCount=min (pCount, pCountT);
然后在着色器中,根据索引和时间(自效果开始以来经过的时间)计算时间
pTime=time-index/prtPerSec;
如果发射器处于循环状态(所有粒子都发射出去,现在死亡并同步出生),我们从粒子的时间开始压裂,从而形成回路。
我们不需要绘制pTime小于零的粒子-它们尚未诞生。 寿命和当前时间之和超过发射终止时间的粒子也是如此。 在这两种情况下,我们都不会通过取消粒子大小和/或将其放置在屏幕后面来绘制任何内容。 这种方法将在淡入/淡出阶段提供少量开销,同时在维持阶段保持最佳性能。
通过仅发送包含用于渲染的活动粒子的那部分顶点缓冲区,可以对算法进行一些改进。 由于发射是顺序发生的事实,活性颗粒将最多被分割一次,即 需要两次抽奖。
现在,知道了每个粒子的当前时间,您可以设置速度,加速度(通常还有其他任何参数)来编写运动方程,从而得出世界空间中的坐标。
使用从vertex_id uv中还原的图像,我们将已经获得四个点(更精确地说,我们将按照需要的方向移动每个四边形点),完成投影的顶点着色器将在其上完成其工作。
p.xy+=(quaduv-.5);
有了免费的奖金,我们不仅有机会暂停发射器,而且还可以精确地来回倒转时间。 事实证明,此功能在设计复杂效果时非常有用。
我们增加了功能
开发过程中的下一个迭代是移动发射器问题的解决方案。 我们的特定系统对其位置一无所知,并且当发射器移动时,整个效果会同步地移到其后方。 对于排气管中产生的烟雾和类似的影响,它看起来并不奇怪。
这个想法是当新粒子诞生时,将发射器的位置记录在顶点缓冲区中。 由于此类颗粒的数量很少,因此开销应已降至最低。
一位同事建议,在开发自己的UI时,他仅使用map / unmap顶点缓冲区的一部分,并且对该解决方案的性能感到非常满意。 我进行了测试,结果证明这种方法在台式机和移动平台上都非常有效。
困难随着cpu和gpu时间同步而出现。 必须确保在“新的”环状粒子处于其起始位置时准确地进行缓冲区更新。 即,关于环形缓冲区,必须使更新区域的边界与发射器的工作时间同步。
我将hlsl代码转移到C ++,为了进行测试,我编写了在Lissajous周围移动的发射器,所有这些突然起作用。 但是,系统有时会“拍打”一个或多个粒子,朝任意方向发射它们,而不是及时清除它们,或者在任意位置创建新粒子。
通过审核引擎中计算时间的准确性并在记录新的发射器位置时同时检查时间增量来解决该问题,从而更新了不受先前迭代影响的整个缓冲区。 系统还必须在强制不同步的情况下工作-突然降低fps不会破坏效果,尤其是因为对于不同的设备,我们的游戏根据性能-60/30/20记录了不同的fps。
方法代码已经增长了很多(环形缓冲区难以优雅地处理),但是,在考虑了所有条件之后,系统正常且稳定地工作。
大约在这个时候,合作伙伴已经制作了足以用来测试系统的编辑器“鱼”,并写出了/ api模板以将系统集成到我们的引擎中。
我将所有代码移植到ios / opengl中,进行了集成,最后在真实设备上进行了真实效果测试。 显然,该系统不仅有效,而且还适用于生产。 仍然需要完成UI编辑器并将代码完善为“明天发布它并不可怕”的状态。
我们甚至已经准备好编写一个内存管理器,以免为带有动态发射器的每个新效果分配/破坏一个缓冲区(最终存储了vertex_id,uv,位置和初始粒子向量),这是我想到的另一个想法。
这个系统中存在顶点缓冲区的事实困扰着我。 他清楚地看了她的古风,“固定传送带黑暗时代的遗产”。 在Windows原型上进行测试效果时,我认为发射器的运动总是平滑的,并且总是比粒子的运动慢得多。 此外,在有大量粒子的情况下,更新位置会导致以下事实:数百个粒子记录相同的数据。 结果证明很简单:我们引入一个固定的数组,该发射器位置的“历史”(通过粒子的寿命进行归一化)将落入其中。 在gpu上,我们将对数据进行插值。 在那之后,对动态缓冲区的需求在ios / gles2版本中消失了(仅保留了用于实现vertex_id的常规静态变量),而在Windows / dx11版本中,由于原生的vertex_id和d3d api接受null而不是链接到顶点缓冲区的能力,缓冲区完全消失了。
因此,按照现代标准,无论我们要显示多少个粒子,系统的双赢版版根本不会消耗内存。 只有一个小的带参数的常量缓冲区,一个位置/基数的缓冲区(对于任何情况,只有60对向量,在任何情况下都有余量),并在必要时提供纹理。 性能测量表明速度接近综合测试。
此外,火花等效果的“尾部”开始看起来更加自然,因为插值可以消除帧间的离散,从而使发射器平稳地改变其位置,就像以数百赫兹的频率进行绘制调用一样。
特色功能
除了粒子飞行的基本功能(速度,加速度,重力,介质阻力)外,我们还需要一定量的功能“脂肪”。
结果是运动模糊(沿运动矢量拉伸粒子),粒子在运动矢量上的方向(例如允许制作一个球形的粒子),根据粒子的当前生存时间调整其大小,并实现了许多其他小功能。
向量场产生了复杂性:由于系统不会存储每个粒子的状态(位置,加速度等),而是每次通过运动方程式对其进行计算,因此原则上不可能实现多种效果(例如搅拌咖啡时泡沫的运动)。 但是,由Perlin的噪音对速度和加速度进行简单的调制就可以得出非常现代的结果。 如此多粒子的实时噪声计算结果过于昂贵(即使限制为五个八度音阶),因此生成了纹理,然后可以从该纹理中采样顶点着色器。 为了增强伪矢量场的效果,根据发射器的当前时间添加了样本坐标的一小部分偏移。
香烟烟雾测试通过将初始速度和加速度分配到Perlin噪声上而起作用。像素输送机
最初,我们仅计划根据时间更改粒子的颜色/透明度。 我向像素着色器添加了几种算法。
纹理颜色旋转-简化,正弦(颜色+时间)。 可以在某种程度上模仿AfterEffects的置换效果。
假照明-通过粒子在世界坐标中的渐变来调制粒子的颜色,而与粒子的旋转角度无关。
边界演化-当粒子在空间中移动时,其边界(alpha通道)会受到聚光灯和佩林噪声的组合调制,从而使其流动动态非常类似于云,烟雾和其他流体效果。
着色器伪代码:
b=perlin(uv)
在稍微复杂的版本中,此着色器可以绘制具有任意柔和度和轮廓高光的边框,从而为真实感添加了“爆炸性”效果。
第一次实验随着边界的发展。接下来是什么?
尽管编辑人员已经准备好工作并可以集成到引擎中,但设计师没有时间对其进行任何处理-项目已关闭。 尽管如此,在其他地方使用这些做法也没有任何障碍-例如,进行演示版本修订。
从技术角度来看,还有移动的空间-例如,现在正在破坏线框对象的几种效果正在起作用:

到目前为止,对用于alpha混合的粒子进行排序的问题仍然存在:由于在着色器中分析性地考虑了所有内容,因此实际上没有用于排序的输入数据。 但是有很大的实验领域!
在《泰坦世界》的开发过程中,游戏的图形部分采用了许多技巧,但下次会更多。
PS:您可以
在这里深入研究源alpha引擎。 示例在release / samples文件夹中,主要控制键是空格,alt | control +鼠标。 着色器直接位于fxp文件中,其代码可通过编辑器窗口获取。