一、效果介绍
话不多说,先上图:
烟花效果在shadertoy上比较常见,也是粒子系统里一个比较典型的实例。通过分析,该效果的粒子可分为三个阶段:粒子的发射,粒子的分散,粒子的消亡。
粒子并不难理解,只是通过粒子做出来的东西有很多,为了让粒子做出来的东西更有价值,更多心思可能要花在想象和创意上。
二、算法分析
1.粒子的发射
为了方便控制,这里采用的方法是定时发射,也就是隔一段时间发射一定数量的粒子,所以这里涉及到的参数有:粒子发射位置,每秒粒子发射数量或者间隔时间发射粒子数(两者描述的其实都是频率),粒子大小。发射比较简单,按照时间到点发射即可。
2.粒子的分散(运动)
粒子的分散又叫运动。运动轨迹与粒子单元决定了粒子形成的效果,在烟花效果中,粒子是以圆中心向外分散的。这里涉及到的参数有:粒子的运动速率,运动过程中的受力,比如重力加速度和阻力等等。通过发射时长来计算粒子的位移情况,让粒子做偏移,即可完成粒子的运动。
3.粒子的消亡
由于一直在发射粒子,粒子越来越多,性能跟不上,因此要考虑粒子的消亡,让着色器处理的粒子数维持一个稳定的最大数。这里将涉及的参数有:粒子的生命周期(存在时间)。通过粒子的生命周期和其发射时间过滤掉已经过期的粒子,然而粒子突然消失显得会有些突兀,一般会通过生命周期来做一个渐渐变暗的处理。
三、利弊分析
1.利
结构比较稳定,各个部件分得很清晰,参数明了,可以改变粒子类型,运动轨迹。不管是圆点还是条状,再或者是图片纹理,都可以很简单的替换,可以当作一个模板。
2.弊
参数可调,但需要达到比较好的效果,参数其实比较局限,只能是某几个值。粒子数量过大,性能还是不好,如果不设置生命周期,或者生命周期过长,性能问题还是没有解决。
四、代码实现
//计算点到线段的距离,返回x为距离,y为向量比例
vec2 line (vec2 a, vec2 b, vec2 p)
{
vec2 pa = p-a, ba = b-a;
float h = min(1., max(0., dot(pa, ba)/dot(ba, ba)));
return vec2(length(pa - ba * h), h);
}
void main()
{
vec2 textSize = textureSize(uTexture1,0);
vec4 nColor = texture(uTexture1, vTexPosition1);
nColor.rgb = vec3(0.0);
int scount = 100; //烟花粒子数
float s_peed = 200; //分散速度
float sdtime = 0.5; //发射间隔时间
float sdelay = 1.5; //生命周期
float particle_Size = 3.0; //粒子大小
float force = 100.0; //重力加速度
float decayRate = 0.7; //衰减比例-阻力加速度(这里模拟的是定向的,不太真实)
float Nt = floor(vtime/sdtime); //计算发射的批次
int nt = int(max(floor((vtime - sdelay)/sdtime),0.0)); //忽略过期的粒子
for(int i = nt;i < Nt+1;i++) //按发射批次循环
{
if(sdtime*(vtime/sdtime - i) > sdelay) //判断当前批次粒子是否超过生命周期(二次判断,实际可以不要)
continue;
float seed = rand((i+1.0)*3.3459); //每批粒子的随机值
vec2 randPos = 0.6*vec2(rand(seed*3.92351),rand(seed*1.18723)) + 0.2;
vec2 stpos = vTexPosition1 - randPos;
stpos *= textSize;
float nowtime = max(vtime - i*sdtime,0.0); //计算当前时间相对发射时的时间
vec3 colorR = vec3(rand(seed*2.13454),rand(seed*1.78345),rand(seed*3.26187))*vec3(2.0*nowtime/sdelay,1.5*nowtime/sdelay,1.0);
for(int j = 1;j<scount+1;j++) //按每个烟花中粒子数循环
{
float decay = (1.0 - nowtime/sdelay); //衰减
float rad = (s_peed - decayRate*s_peed*decay)*(rand(seed*j * 1.8372 + 3.3459)*0.7 + 0.3);
float a = 2.0*PI*rand(seed*j*2.1312 + 2.8923); //随机发射角度
//rad = 0.02*s_peed; //轨迹-心形
vec2 pdir = vec2(rad * cos(a), rad * sin(a)); //轨迹-圆形
//pdir.x = rad * 16* pow(sin(a),3.0); //心形
//pdir.y = rad * (13.0*cos(a) - 5.0*cos(2*a) - 2*cos(3*a) - cos(4*a));//心形
vec2 sparkPos = -max(nowtime,0.0)*pdir + Force(0.0,nowtime,force,vec2(0.0,1.0)); //加入重力影响
//采用到点的距离实现绘制圆状粒子 -- Start
float psize = length(sparkPos + stpos);
nColor.rgb += smoothstep(0.0,1.0,0.5*(1.0 - fract(nowtime/sdelay))*particle_Size/psize) * colorR;
//圆状粒子 -- End
//采用到线的距离绘制条状粒子 -- Start
float linelen = 0.5*s_peed;
float t1 = max(nowtime - 0.3,0.0);
//SDF计算到线的距离
vec2 sdline = line(-t1*smoothstep(-0.2,1.0,decay)*linelen*normalize(pdir),vec2(0.0,0.0),sparkPos + stpos);
sdline.x = smoothstep(1.5, 0.0, sdline.x);
nColor.rgb += smoothstep(-0.1,1.0,decay)*smoothstep(0.0, 1.0, sdline.y)*colorR*5.0*sdline.x;
//条状粒子 -- End
}
}
FragColor = nColor;
}