占位,占位,占位
前言
本文将为大家介绍如何使用Billboards构建一个简单的粒子系统。粒子系统可在做到一些单纯的几何体无法做到的特效,它有很多变种和配置项,譬如制作下雪场景,技能特效,灰尘飞扬的效果等等。本文的例子中只是实现了一个简单的受重力影响的粒子效果,下面是效果图。
粒子的基本属性
本文中每个粒子就是一个billboard,我创建了新的类Particle来表示粒子,它主要负责粒子的渲染和行为更新。一个完善的粒子系统有很多配置项来控制粒子的属性,这里我列举了粒子的几个基本属性。这里粒子是直接继承Billboard,这样我就可以用最少的三角形来表示一个粒子了,无论你从哪个角度看这个粒子,它始终都面朝摄像机。
@interface Particle: Billboard @property (assign, nonatomic) float life; @property (assign, nonatomic) GLKVector3 position; @property (assign, nonatomic) GLKVector3 speed; @property (assign, nonatomic) float size; @property (assign, nonatomic) GLKVector3 color; @end
life表示粒子的生命,粒子将发射时,被赋予生命,单位是秒。每次update,生命减少,生命小于等于0,则粒子死亡(无效)。position表示粒子的位置,这里的位置属性将被直接赋值给billboard的billboardCenterPosition。speed是粒子的x,y,z三个方向的速度,在update中使用它更新粒子的位置。size表示粒子的大小,直接赋值给billboardSize。color表示粒子的颜色,粒子渲染时一般会使用一张白色的半透明图,在Shader中将像素和粒子的颜色相乘。这些属性的使用会在后面详细介绍。
生成粒子
粒子系统的最基本的功能是发射粒子,回收粒子。ParticleSystem是表示粒子系统的类,初始化时生成指定数目的粒子。具体要生成多少粒子通过ParticleSystemConfig中的maxParticles指定。
- (instancetype)initWithGLContext:(GLContext *)context config:(ParticleSystemConfig)config particleTexture:(GLKTextureInfo *)particleTexture { self = [super initWithGLContext:context]; if (self) { self.config = config; self.particleTexture = particleTexture; self.activeParticles = [NSMutableArray new]; self.inactiveParticles = [NSMutableArray new]; [self fillParticles]; } return self; } - (void)fillParticles { for (int i = 0; i < self.config.maxParticles; ++i) { [self newParticle]; } } - (void)newParticle { Particle *particle = [[Particle alloc] initWithGLContext:self.context texture:self.particleTexture]; [self resetParticle:particle]; [self.inactiveParticles addObject:particle]; }
发射回收粒子
粒子系统通过activeParticles和inactiveParticles两个数组复用粒子对象,初始化时,将粒子全部放入非激活态粒子数组inactiveParticles中,粒子系统请求新的粒子时,将从inactiveParticles中选取。pickParticle是选取粒子的方法。
- (Particle *)pickParticle { if (self.inactiveParticles.count > 0) { Particle *particle = self.inactiveParticles[0]; [self.inactiveParticles removeObjectAtIndex:0]; [self resetParticle:particle]; return particle; } return nil; }
在每次update中,先检测是否有粒子的生命已经结束,如果结束,从activeParticles移除放到inactiveParticles中。recycleInactiveParticle是检测并回收已死亡粒子的方法。
- (void)recycleInactiveParticle { for (int index = 0; index < self.activeParticles.count; ++index) { Particle *particle = self.activeParticles[index]; if (particle.life <= 0) { [self.inactiveParticles addObject:particle]; [self.activeParticles removeObjectAtIndex:index]; index--; } } } - (void)update:(NSTimeInterval)timeSinceLastUpdate { [self recycleInactiveParticle]; int birthParicleCount = self.config.birthRate * timeSinceLastUpdate * self.config.maxParticles; for (int i = 0; i < birthParicleCount; ++i) { Particle *particle = [self pickParticle]; if (particle) { [self.activeParticles addObject:particle]; } } for (Particle *particle in self.activeParticles) { [particle update:timeSinceLastUpdate]; } }
接着,通过config中的出生率birthRate控制每次update发射的粒子数,从非激活态的粒子数组中选取这些粒子并重新初始化粒子的属性。最后更新所有被激活的粒子。
粒子属性赋值
在粒子被发射前,都要重新对粒子的属性赋值,粒子属性的具体赋值由ParticleSystemConfig中的配置项来决定。config中指定了粒子属性的随机范围,从startXXX到endXXX。emissionBoxTransform和emissionBoxExtends表示了Box发射区域的变换和尺寸,粒子会在指定的Box区域随机生成。如果你想在球形区域或者其他区域发射,也可以替换成自己的算法。
- (void)resetParticle:(Particle *)particle { particle.life = [self randFloat:config.startLife end:config.endLife]; GLKVector4 newPos = GLKMatrix4MultiplyVector4(config.emissionBoxTransform, GLKVector4Make(0, 0, 0, 1)); particle.position = [self randInBox:config.emissionBoxExtends center: GLKVector3Make(newPos.x, newPos.y, newPos.z)]; particle.speed = [self randVector3:config.startSpeed end:config.endSpeed]; particle.size = [self randFloat:config.startSize end:config.endSize]; particle.color = [self randVector3:config.startColor end:config.endColor]; }
物理模型
物理模型决定的粒子的运动方式,下面是本文粒子的update方法。
- (void)update:(NSTimeInterval)timeSinceLastUpdate { self.life -= timeSinceLastUpdate; float lifePercent = self.life / self.originLife; self.billboardSize = GLKVector2Make(self.size * lifePercent, self.size * lifePercent); self.billboardCenterPosition = self.position; self.speed = GLKVector3Make(self.speed.x, self.speed.y + timeSinceLastUpdate * -9.8, self.speed.z); self.position = GLKVector3Add(GLKVector3MultiplyScalar(self.speed, timeSinceLastUpdate), self.position); }
这里主要使用了重力模型进行运动控制,speed在每次update中根据重力改变自身的值,然后通过speed计算新的位置。你也可以使用其他模型来控制粒子行为,比如引力模型,假设中心点是太阳,粒子从一个球面上发射,受引力影响运动。
代码中的lifePercent主要用来控制粒子的大小随生命周期改变
Shader和Blend
粒子的Shader很简单,把贴图的颜色和粒子颜色相乘即可。
void main(void) { vec4 diffuseColor = texture2D(diffuseMap, fragUV); gl_FragColor = diffuseColor * vec4(particleColor, 1.0); }
因为粒子是透明的,所以还要开启Blend模式。同时关闭深度写入,避免有些像素被discard掉,而无法进行混合,这个我在透明和混合中有提到。
- (void)draw:(GLContext *)glContext { glDepthMask(GL_FALSE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA); for (Particle *particle in self.activeParticles) { [particle draw:glContext]; } glDepthMask(GL_TRUE); }
创建粒子
最后在ViewController中创建粒子。
- (void)createParticles { NSString *vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"vtx_billboard" ofType:@".glsl"]; NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"frag_particle" ofType:@".glsl"]; GLContext *particleContext = [GLContext contextWithVertexShaderPath:vertexShaderPath fragmentShaderPath:fragmentShaderPath]; ParticleSystemConfig config; config.birthRate = 0.3; config.emissionBoxExtends = GLKVector3Make(0.6,0.6,0.6); config.emissionBoxTransform = GLKMatrix4MakeTranslation(0, -4, 0); config.startLife = 1; config.endLife = 2; config.startSpeed = GLKVector3Make(-1.6, 12.5, -1.6); config.endSpeed = GLKVector3Make(1.6, 12.5, 1.6); config.startSize = 1.9; config.endSize = 2.6; config.startColor = GLKVector3Make(0, 0, 0); config.endColor = GLKVector3Make(0.6, 0.5, 0.6); config.maxParticles = 600; GLKTextureInfo *qrcode = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"particle.png"].CGImage options:nil error:nil]; ParticleSystem *particleSystem = [[ParticleSystem alloc] initWithGLContext:particleContext config:config particleTexture:qrcode]; [self.objects addObject:particleSystem]; }
总结
本文主要介绍了一个基本的粒子系统是怎样构建起来的。当然投入产品使用的粒子系统会更加复杂,具体可以参考unity3d的粒子系统。不过只要理解了粒子系统的基本概念,再去看复杂的粒子系统就会容易理解的多。