欢迎光临
我们一直在努力

中秋节特别篇:游戏里的团圆仪式与烟花粒子系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

中秋节特别篇:游戏里的团圆仪式与烟花粒子系统设计

封面信息图

节日庆典场景中,夜空大规模烟花表演与主城灯火是烘托团圆氛围的核心视觉元素。但在移动端与中低端 PC 设备上,数十组高密度烟花同屏绽放极易引发两大性能灾难:一是大量重叠的半透明粒子引发严重的像素填充率(Overdraw)瓶颈;二是数十万粒子的 CPU 物理轨迹模拟与渲染管线合批开销。设计一套基于 GPU Compute Shader 驱动、带有二次子爆破(Secondary Burst)与局部光照注入的烟花粒子系统,是兼顾视觉冲击力与 60fps 流畅度的关键。

烟花生命周期物理模型与爆破数学分布

一个真实的烟花爆破包含三个阶段:上升推进阶段(Rocket Ascent)、主爆破扩散阶段(Primary Burst)以及二次闪烁残渣与柳条拖尾阶段(Secondary Sparks & Willow Trails)。

为了让主爆破粒子在三维球体表面均匀散开,避免传统球面随机采样造成的极点聚集(Pole Clustering),采用**斐波那契球面采样算法(Fibonacci Sphere Distribution)**结合随机速度扰动:

$$x_i = \\sqrt{1 – z_i^2} \\cos(\\theta_i), \\quad y_i = \\sqrt{1 – z_i^2} \\sin(\\theta_i), \\quad z_i = 1 – \\frac{2i + 1}{N}$$$$\\theta_i = \\pi (1 + \\sqrt{5}) i$$

其中 $N$ 为单发烟花粒子总数,$i \\in [0, N-1]$。

GPU Compute Shader 粒子物理模拟与发射流水线

粒子模拟全部交由 GPU Compute Shader 在异步计算队列中完成,利用 StructuredBuffer 存储粒子属性,避免 CPU 到 GPU 的每帧数据传输。

// FireworksSimulation.compute
#pragma kernel CS_InitLaunch
#pragma kernel CS_SimulateSparks

struct SparkParticle
{
float3 position;
float3 velocity;
float4 color;
float lifetime;
float maxLifetime;
float size;
uint stage; // 0: 上升, 1: 主爆破, 2: 柳条下落
};

RWStructuredBuffer<SparkParticle> _ParticlesBuffer;
AppendStructuredBuffer<uint> _ActiveList;

cbuffer SimParams
{
float _DeltaTime;
float3 _Gravity;
float _AirDrag;
float _Time;
};

[numthreads(64, 1, 1)]
void CS_SimulateSparks(uint3 id : SV_DispatchThreadID)
{
uint idx = id.x;
SparkParticle p = _ParticlesBuffer[idx];

if (p.lifetime <= 0.0) return;

p.lifetime -= _DeltaTime;

if (p.stage == 0) // 上升阶段
{
p.velocity += (_Gravity * 0.2) * _DeltaTime;
p.position += p.velocity * _DeltaTime;

// 达到最高点或生命周期耗尽触发主爆破
if (p.lifetime <= 0.0 || p.velocity.y <= 0.0)
{
p.stage = 1;
p.lifetime = p.maxLifetime; // 重置为主爆破生命周期

// 依据粒子索引生成斐波那契球面扩散速度
float phi = 3.14159265 * (3.0 – sqrt(5.0)); // 黄金角
float y = 1.0 – (float(idx % 256) / 256.0) * 2.0;
float radius = sqrt(1.0 – y * y);
float theta = phi * float(idx % 256);

float3 dir = float3(cos(theta) * radius, y, sin(theta) * radius);
float explosionSpeed = 18.0 + frac(sin(idx * 78.233) * 43758.5453) * 8.0;
p.velocity = dir * explosionSpeed;
}
}
else if (p.stage == 1) // 主爆破与柳条下落
{
// 空气阻力与重力作用
p.velocity += _Gravity * _DeltaTime;
p.velocity *= (1.0 – _AirDrag * _DeltaTime);
p.position += p.velocity * _DeltaTime;

// 颜色渐变与闪烁(Blinking Sparkle)
float progress = 1.0 – (p.lifetime / p.maxLifetime);
float sparkle = sin(_Time * 40.0 + idx) * 0.5 + 0.5;
p.color.a = (1.0 – progress * progress) * sparkle;
}

_ParticlesBuffer[idx] = p;
}

渲染管线深度优化:Overdraw 控制与 Procedural Draw

传统粒子系统使用 Mesh Renderer 或 Particle System 面片,当屏幕被数十万粒子覆盖时,Alpha Blending 导致片元着色器执行次数爆炸。为了控制 Overdraw 并实现零 CPU Draw Call 提交:

  • DrawProceduralIndirect 渲染:利用 Graphics.DrawProceduralIndirect 直接从 ComputeBuffer 读取粒子数据生成 Quad,绕过托管层 GameObject 与 Mesh 对象的内存创建。
  • 片元丢弃(Alpha Cutoff 预筛选)与软边缘:
  • // HLSL 烟花粒子渲染 Pass 局部
    struct Attributes
    {
    uint vertexID : SV_VertexID;
    uint instanceID : SV_InstanceID;
    };

    StructuredBuffer<SparkParticle> _ParticlesBuffer;

    struct Varyings
    {
    float4 positionCS : SV_POSITION;
    float4 color : COLOR;
    float2 uv : TEXCOORD0;
    };

    Varyings vert(Attributes input)
    {
    Varyings output;
    SparkParticle p = _ParticlesBuffer[input.instanceID];

    // 生成 Billboard 四边形顶点 (-0.5 到 0.5)
    uint quadIndex = input.vertexID % 4;
    float2 quadUV = float2((quadIndex << 1) & 2, quadIndex & 2);
    float3 cornerOffset = float3(quadUV – 0.5, 0.0) * p.size;

    // 面向摄像机旋转
    float3 cameraRight = UNITY_MATRIX_V[0].xyz;
    float3 cameraUp = UNITY_MATRIX_V[1].xyz;
    float3 worldPos = p.position + cameraRight * cornerOffset.x + cameraUp * cornerOffset.y;

    output.positionCS = TransformWorldToHClip(worldPos);
    output.color = p.color;
    output.uv = quadUV;
    return output;
    }

    half4 frag(Varyings input) : SV_Target
    {
    float dist = length(input.uv – 0.5);
    // 高斯光晕衰减
    half alpha = exp(-dist * dist * 12.0) * input.color.a;

    // 剔除近乎透明的片元,减轻 ROP 混合压力
    clip(alpha – 0.02);

    half3 finalColor = input.color.rgb * alpha * 2.5; // 发光 HDR Boost
    return half4(finalColor, alpha);
    }

    局部动态光照注入与中秋仪式感呈现

    烟花在空中绽放的瞬间,应当对地表建筑、水面以及聚集的玩家角色产生瞬态彩色照亮效果。如果为每朵烟花创建真实的 Light 组件,移动端 Forward 渲染器会在短时间内因重叠光源(Exceeded Max Pixel Lights)导致管线退化或 Shader 变体分支爆炸。

    工程解决方案是采用 Clustered Forward 光源缓冲注入:

    • 当检测到烟花进入爆破帧时,提取该烟花的中心坐标、主色调与衰减半径,写入全局 ConstantBuffer 中维护的最多 16 个瞬态虚拟光源环形队列(Ring Buffer)。
    • 场景地面与建筑 Shader 在一个通用的循环中计算这 16 个点光源的简易衰减,仅需数条 FMA 指令即可实现主城水面波光与亭台楼阁被烟花照亮的动态光影氛围。
    • 实测在 50,000 粒子同屏连环绽放下,GPU 耗时稳定在 1.8ms 以内,无任何 GC 产生,确保节日活动在万人同服时的丝滑运行。
    赞(0)
    未经允许不得转载:171主机测评 » 中秋节特别篇:游戏里的团圆仪式与烟花粒子系统设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址