學(xué)習(xí)OpenGL ES之粒子效果

本系列所有文章目錄

獲取示例代碼


占位,占位,占位

前言

本文將為大家介紹如何使用Billboards構(gòu)建一個(gè)簡單的粒子系統(tǒng)。粒子系統(tǒng)可在做到一些單純的幾何體無法做到的特效,它有很多變種和配置項(xiàng),譬如制作下雪場景,技能特效,灰塵飛揚(yáng)的效果等等。本文的例子中只是實(shí)現(xiàn)了一個(gè)簡單的受重力影響的粒子效果,下面是效果圖。


粒子的基本屬性

本文中每個(gè)粒子就是一個(gè)billboard,我創(chuàng)建了新的類Particle來表示粒子,它主要負(fù)責(zé)粒子的渲染和行為更新。一個(gè)完善的粒子系統(tǒng)有很多配置項(xiàng)來控制粒子的屬性,這里我列舉了粒子的幾個(gè)基本屬性。這里粒子是直接繼承Billboard,這樣我就可以用最少的三角形來表示一個(gè)粒子了,無論你從哪個(gè)角度看這個(gè)粒子,它始終都面朝攝像機(jī)。

@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表示粒子的生命,粒子將發(fā)射時(shí),被賦予生命,單位是秒。每次update,生命減少,生命小于等于0,則粒子死亡(無效)。position表示粒子的位置,這里的位置屬性將被直接賦值給billboard的billboardCenterPosition。speed是粒子的x,y,z三個(gè)方向的速度,在update中使用它更新粒子的位置。size表示粒子的大小,直接賦值給billboardSize。color表示粒子的顏色,粒子渲染時(shí)一般會使用一張白色的半透明圖,在Shader中將像素和粒子的顏色相乘。這些屬性的使用會在后面詳細(xì)介紹。

生成粒子

粒子系統(tǒng)的最基本的功能是發(fā)射粒子,回收粒子。ParticleSystem是表示粒子系統(tǒng)的類,初始化時(shí)生成指定數(shù)目的粒子。具體要生成多少粒子通過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];
}

發(fā)射回收粒子

粒子系統(tǒng)通過activeParticlesinactiveParticles兩個(gè)數(shù)組復(fù)用粒子對象,初始化時(shí),將粒子全部放入非激活態(tài)粒子數(shù)組inactiveParticles中,粒子系統(tǒng)請求新的粒子時(shí),將從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中,先檢測是否有粒子的生命已經(jīng)結(jié)束,如果結(jié)束,從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發(fā)射的粒子數(shù),從非激活態(tài)的粒子數(shù)組中選取這些粒子并重新初始化粒子的屬性。最后更新所有被激活的粒子。

粒子屬性賦值

在粒子被發(fā)射前,都要重新對粒子的屬性賦值,粒子屬性的具體賦值由ParticleSystemConfig中的配置項(xiàng)來決定。config中指定了粒子屬性的隨機(jī)范圍,從startXXXendXXX。emissionBoxTransformemissionBoxExtends表示了Box發(fā)射區(qū)域的變換和尺寸,粒子會在指定的Box區(qū)域隨機(jī)生成。如果你想在球形區(qū)域或者其他區(qū)域發(fā)射,也可以替換成自己的算法。

- (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];
}

物理模型

物理模型決定的粒子的運(yùn)動(dòng)方式,下面是本文粒子的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);
}

這里主要使用了重力模型進(jìn)行運(yùn)動(dòng)控制,speed在每次update中根據(jù)重力改變自身的值,然后通過speed計(jì)算新的位置。你也可以使用其他模型來控制粒子行為,比如引力模型,假設(shè)中心點(diǎn)是太陽,粒子從一個(gè)球面上發(fā)射,受引力影響運(yùn)動(dòng)。

代碼中的lifePercent主要用來控制粒子的大小隨生命周期改變

Shader和Blend

粒子的Shader很簡單,把貼圖的顏色和粒子顏色相乘即可。

void main(void) {
    vec4 diffuseColor = texture2D(diffuseMap, fragUV);
    gl_FragColor = diffuseColor * vec4(particleColor, 1.0);
}

因?yàn)榱W邮峭该鞯模赃€要開啟Blend模式。同時(shí)關(guān)閉深度寫入,避免有些像素被discard掉,而無法進(jìn)行混合,這個(gè)我在透明和混合中有提到。

- (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);
}

創(chuàng)建粒子

最后在ViewController中創(chuàng)建粒子。

- (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];
}

總結(jié)

本文主要介紹了一個(gè)基本的粒子系統(tǒng)是怎樣構(gòu)建起來的。當(dāng)然投入產(chǎn)品使用的粒子系統(tǒng)會更加復(fù)雜,具體可以參考unity3d的粒子系統(tǒng)。不過只要理解了粒子系統(tǒng)的基本概念,再去看復(fù)雜的粒子系統(tǒng)就會容易理解的多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容