iOS粒子特效

最早接触这个概念是在去年的圣诞节的时候,那个时候手机淘宝首页出了一个页面下雪的动画,当时觉得蛮炫酷的,而且以为实现起来蛮难的,后来自己查了资料才知道“粒子特效”这个概念,今天就来简单聊聊它吧。

粒子特效

粒子系统表示三维计算机图形学中模拟一些特定的模糊现象的技术,而这些现象用其它传统的渲染技术难以实现的真实感的游戏图形。经常使用粒子系统模拟的现象有火、爆炸、烟、水流、火花、落叶、云、雾、雪、尘、流星尾迹或者象发光轨迹这样的抽象视觉效果等等。

以上定义来自维基百科,详细的解释点击这里.

在 iOS 系统当中的实现

粒子特效由两个部分组成,一个是发射器,主要负责粒子的生成和发射,另一个是粒子单元,用于描述粒子的状态。在 iOS 系统当中这两部分分别由 CAEmitterLayer(发射器)和 CAEmitterCell(组成)。

具体使用可以查看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//雪花动画
- (void)snow {

//粒子发射器
CAEmitterLayer *snowEmitter = [CAEmitterLayer layer];
//粒子发射的位置
snowEmitter.emitterPosition = CGPointMake(100, 30);
//发射源的大小
snowEmitter.emitterSize = CGSizeMake(self.view.bounds.size.width, 0.0);
//发射模式
snowEmitter.emitterMode = kCAEmitterLayerOutline;
//发射源的形状
snowEmitter.emitterShape = kCAEmitterLayerLine;

//创建雪花粒子
CAEmitterCell *snowflake = [CAEmitterCell emitterCell];
//粒子的名称
snowflake.name = @"snow";
//粒子参数的速度乘数因子。越大出现的越快
snowflake.birthRate = 1.0;
//存活时间
snowflake.lifetime = 120.0;
//粒子速度
snowflake.velocity = -10; // falling down slowly
//粒子速度范围
snowflake.velocityRange = 10;
//粒子y方向的加速度分量
snowflake.yAcceleration = 2;
//周围发射角度
snowflake.emissionRange = 0.5 * M_PI; // some variation in angle
//子旋转角度范围
snowflake.spinRange = 0.25 * M_PI; // slow spin
//粒子图片
snowflake.contents = (id)[[UIImage imageNamed:@"DazFlake"] CGImage];
//粒子颜色
snowflake.color = [[UIColor redColor] CGColor];

//设置阴影
snowEmitter.shadowOpacity = 1.0;
snowEmitter.shadowRadius = 0.0;
snowEmitter.shadowOffset = CGSizeMake(0.0, 1.0);
snowEmitter.shadowColor = [[UIColor whiteColor] CGColor];

// 将粒子添加到粒子发射器上
snowEmitter.emitterCells = [NSArray arrayWithObject:snowflake];
[self.view.layer insertSublayer:snowEmitter atIndex:0];
}

//烟花动画
- (void)fireworks {
// Cells spawn in the bottom, moving up

//分为3种粒子,子弹粒子,爆炸粒子,散开粒子
CAEmitterLayer *fireworksEmitter = [CAEmitterLayer layer];
CGRect viewBounds = self.view.layer.bounds;
fireworksEmitter.emitterPosition = CGPointMake(viewBounds.size.width/2.0, viewBounds.size.height);
fireworksEmitter.emitterSize = CGSizeMake(viewBounds.size.width/2.0, 0.0);
fireworksEmitter.emitterMode = kCAEmitterLayerOutline;
fireworksEmitter.emitterShape = kCAEmitterLayerLine;
fireworksEmitter.renderMode = kCAEmitterLayerAdditive;
fireworksEmitter.seed = (arc4random()%100)+1;

// Create the rocket
CAEmitterCell *rocket = [CAEmitterCell emitterCell];

rocket.birthRate = 1.0;
rocket.emissionRange = 0.25 * M_PI; // some variation in angle
rocket.velocity = 380;
rocket.velocityRange = 100;
rocket.yAcceleration = 75;
rocket.lifetime = 1.02; // we cannot set the birthrate < 1.0 for the burst

//小圆球图片
rocket.contents = (id)[[UIImage imageNamed:@"DazRing"] CGImage];
rocket.scale = 0.2;
rocket.color = [[UIColor redColor] CGColor];
rocket.greenRange = 1.0; // different colors
rocket.redRange = 1.0;
rocket.blueRange = 1.0;
rocket.spinRange = M_PI; // slow spin



// the burst object cannot be seen, but will spawn the sparks
// we change the color here, since the sparks inherit its value
CAEmitterCell *burst = [CAEmitterCell emitterCell];

burst.birthRate = 1.0; // at the end of travel
burst.velocity = 0; //速度为0
burst.scale = 2.5; //大小
burst.redSpeed =-1.5; // shifting
burst.blueSpeed =+1.5; // shifting
burst.greenSpeed =+1.0; // shifting
burst.lifetime = 0.35; //存在时间

// and finally, the sparks
CAEmitterCell *spark = [CAEmitterCell emitterCell];

spark.birthRate = 400;
spark.velocity = 125;
spark.emissionRange = 2* M_PI; // 360 度
spark.yAcceleration = 75; // gravity
spark.lifetime = 3;
//星星图片
spark.contents = (id)[[UIImage imageNamed:@"DazStarOutline"] CGImage];
spark.scaleSpeed = -0.2;
spark.greenSpeed = -0.1;
spark.redSpeed = 0.4;
spark.blueSpeed =-0.1;
spark.alphaSpeed =-0.25;
spark.spin = 2* M_PI;
spark.spinRange = 2* M_PI;

// 3种粒子组合,可以根据顺序,依次烟花弹-烟花弹粒子爆炸-爆炸散开粒子
fireworksEmitter.emitterCells = [NSArray arrayWithObject:rocket];
rocket.emitterCells = [NSArray arrayWithObject:burst];
burst.emitterCells = [NSArray arrayWithObject:spark];
[self.view.layer addSublayer:fireworksEmitter];
}

粒子特效的叠加

通过上面的代码和注释、以及苹果的注释,我相信大部分人都已掌握了粒子效果的基本使用,下面我们来谈谈粒子特效叠加的问题。

在 CAEmitterCell.h 当中我们可以看到这样一段描述

1
2
3
4
5
6
7
/* An array containing the sub-cells of this cell, or nil (the default
* value). When non-nil each particle emitted by the cell will act as
* an emitter for each of the cell's sub-cells. The emission point is
* the current particle position and the emission angle is relative to
* the current direction of the particle. Animatable. */

@property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells;

这段描述说明了当设置粒子单元(CAEmitterCell *)的 emitterCells 时,每个粒子会充当一个发射器的角色,发射对应粒子,形成粒子特效的叠加。

将 rocket.emitterCells = [NSArray arrayWithObject:burst];修改为 rocket.emitterCells = [NSArray arrayWithObject: spark];就可以看到如下图对应的叠加效果了。

粒子特效叠加

取消叠加

看了粒子特效的叠加,你可能还会有这样一个想法,我能不能取消某个时间段的叠加,只让发射的粒子在最后的时间作为发射器产生粒子效果呢,类似于发射一颗礼炮,在礼炮到达空中时爆炸,而不是像现在这样一路炸上天呢。

取消叠加

实例代码的 fireworks 部分就实现了这样的效果,下面我们来分析一下他的思路

fireworks 部分的基本思路是这样的,给发射的粒子 A 附加一个新的粒子效果 B,B 的效果是在 A 生命周期的结尾发射一个生命周期极短的粒子,在 B 粒子极短的生命周期内,发射爆炸效果的粒子 C,这样看起来的效果就是 A 被发射出去,在其生命周期的结尾发射了一个爆炸的粒子 C,用户并没有意识到 B 粒子的存在,从而达到取消叠加的效果。

如何在代码中实现这种取消叠加的效果

birthRate 粒子每秒钟产生粒子的个数,决定了粒子产生的间隔,而且并不是一开始就发射粒子,而是要从开始过相应的时间间隔才发送粒子。所以我们只需要将 B 粒子的生成间隔设定为 A 粒子的生存长度即可,这样就实现了在 A 粒子即将结束的时候发射 B 粒子。

fireworks 部分的代码也可以这样表示

1
burst.birthRate = 1.0/rocket.lifetime;

取消叠加的坑

你做了取消叠加部分的操作,发现 A 粒子生命周期结束时并没有出现爆炸效果的粒子 C,这个时候问题出现在哪里呢?

原因就是刚好在 A 生命周期的结尾要发射单个粒子 B 的时候,A died,然后 B 就没发射出来……

这个时候你需要将 A 的生命周期比原来加长一点,要比 B 的生成间隔时间长那么一小点,代码实现上为:

1
2
3
4
CGFloat rocketLifeTime = 1.0f;
rocket.lifetime = rocketLifeTime + 0.01f;

burst.birthRate = 1.0f/rocketLifeTime;

或者

1
burst.birthRate = 1.0f/(rocket.lifetime-0.01f);

补充:粒子属性的继承

如果发射器是粒子的话,它的相应属性,会继承给它发射出的子粒子。比如说粒子 A 设置了 redSpeed 属性,粒子 B redSpeed 的默认值会和 A 相同,除非 B 自己设置一个 redSpeed 的数值。

参考博文:

iOS 粒子效果

CAEmitterLayer(粒子系统)学习笔记