第四周咖啡馆:烟花粒子与掉帧猫的帧率保卫战总结
周五深夜的咖啡馆里研磨机的轰鸣混合着浓缩咖啡的香气。工位桌面上橘猫「掉帧/Drop」正前爪悬空极其专注地试图去抓屏幕上四散绽放的虚拟烟花粒子——由于它整只猫身挡在主监视器正前方屏幕里的粒子流刚好穿过它的毛茸茸耳朵形成了极具荒诞感的“物理穿模”。然而半天前这套为了节日版本特供的全城烟花粒子系统却在性能测试机上引发了一场“灾难”当 12 组绚丽的烟花同时在主城夜空炸开手机帧率在 3 秒之内从 60fps 狂跌至 18fps中端机机身瞬间发烫。烟花盛宴背后的性能杀手Overdraw 与 CPU 实例化拉开 Xcode GPU Frame Capture 与 Snapdragon Profiler导致帧率雪崩的元凶清晰地暴露在管线瀑布图中灾难级的 Overdraw 像素重绘每个烟花爆炸瞬间生成 2,000 个带软边缘光晕Soft Glow的半透明面片。数十层半透明面片在屏幕中心叠在一起中心区域的像素重绘倍数高达16x ~ 24x。在移动端 TBDR 架构下Alpha Blending 疯狂争抢 Tile 内存带宽直接将 GPU 显存吞吐打到死锁。CPU 主线程粒子发射与销毁开销传统 CPU 粒子系统在每帧更新 20,000 个粒子的速度、重力阻尼与生命周期同时频繁调用ParticleSystem.Emit产生堆内存分配CPUParticleSystem.Update耗时高达 11.8ms。父亲的涡轮喷嘴与 GPU 粒子的动力学积分小时候父亲曾指着航空发动机模型上的环形燃油喷嘴告诉我“把航空煤油雾化成微米级油雾需要严格计算压力腔的流场速度与阻力每一颗油滴的运动都是流体力学方程的精确体现。”在虚拟引擎中烟花粒子的尾迹、散落与空气阻力本质上也是牛顿运动定律在微观离散时间步上的数值积分。为了彻底解放 CPU我们将整个烟花粒子系统完全重构为基于GPU Compute Shader 间接绘制Indirect Draw的物理驱动管线// GPU 烟花粒子物理动力学更新 Compute Shader #pragma kernel UpdateParticles struct ParticleData { float3 position; float3 velocity; float4 color; float life; float maxLife; float size; }; RWStructuredBufferParticleData _ParticleBuffer; AppendStructuredBufferParticleData _AliveParticleBuffer; float _DeltaTime; float3 _Gravity; float _DragCoefficient; [numthreads(128, 1, 1)] void UpdateParticles(uint3 id : SV_DispatchThreadID) { uint index id.x; ParticleData p _ParticleBuffer[index]; if (p.life 0.0) return; p.life - _DeltaTime; if (p.life 0.0) { // 物理动力学计算重力加速度与空气粘滞阻力 float3 dragForce -_DragCoefficient * p.velocity * length(p.velocity); float3 acceleration _Gravity dragForce; p.velocity acceleration * _DeltaTime; p.position p.velocity * _DeltaTime; // 颜色渐变与淡出衰减 float normalizedAge p.life / p.maxLife; p.color.a saturate(normalizedAge * 2.0); // 尾期快速透明消隐 p.size * (1.0 0.2 * _DeltaTime); // 扩散膨胀 _ParticleBuffer[index] p; _AliveParticleBuffer.Append(p); } }降维打击半分辨率粒子渲染Half-Resolution Rendering为了彻底根治 Overdraw 引起的带宽瓶颈我们引入了半分辨率离屏渲染 Pass降采样离屏绘制将烟花粒子渲染到宽高仅为原屏幕 1/2 的低分辨率 Render Target1/4 像素总量Overdraw 的像素填充开销直接被砍掉 75%。双边深度感知上采样Bilateral Depth Upsampling在将半分辨率粒子纹理混合回全分辨率主帧缓冲时采样高精度深度图进行边缘法线比对在保证烟花中心光晕柔和的同时彻底消除了物体边缘与粒子交界处的低分辨率马赛克锯齿。[全分辨率 G-Buffer / Depth Buffer] │ ├──► [创建 1/2 分辨率深度贴图] │ │ │ ▼ │ [在 1/2 目标缓冲中渲染密集烟花粒子] │ │ ▼ ▼ [执行双边深度感知上采样 (Bilateral Blend)] ──► [最终全分辨率画面合成]帧率保卫战的战果经过重构20,000 个全城烟花粒子同时绽放的极限压力场景下CPUParticleSystem耗时由11.8ms 归零完全在 GPU 流转GPU 渲染耗时从26.4ms 下降至 5.2ms中端测试机帧率稳定在59.6 fps机身温度全程控制在 38.8°C。掉帧猫终于在键盘旁边趴了下来发出满足的呼噜声。在代码与着色器的世界里保卫每一个 16.6ms 的流畅就是技术人最坚实的日常。