学习OpenGL ES之粒子效果

本系列所有文章目录

获取示例代码


占位,占位,占位

前言

本文将为大家介绍如何使用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的billboardCenterPositionspeed是粒子的x,y,z三个方向的速度,在update中使用它更新粒子的位置。size表示粒子的大小,直接赋值给billboardSizecolor表示粒子的颜色,粒子渲染时一般会使用一张白色的半透明图,在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];
}

发射回收粒子

粒子系统通过activeParticlesinactiveParticles两个数组复用粒子对象,初始化时,将粒子全部放入非激活态粒子数组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中指定了粒子属性的随机范围,从startXXXendXXXemissionBoxTransformemissionBoxExtends表示了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的粒子系统。不过只要理解了粒子系统的基本概念,再去看复杂的粒子系统就会容易理解的多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容