開発のヒホ

iOSとかAndroidとかのアプリを開発するのに四苦八苦するブログ

OpenGL ES(GLKit)を使って動くエフェクトを作る(その2:花火の火花みたいなのを作る)

OpenGL ES(GLKit)を使って動くエフェクトを作る(その1:GLKViewを使ってみる)の続き

f:id:hihokaruta:20130831154904j:plain

 花火みたいなエフェクトを作ろう!ってのが目標です。実際の花火は上の画像みたいに「複数の火花が尾を引いて落下している」ように見えます。
 ということでまずは複数表示をしてみます。

複数表示

 同じ所に複数表示してもしゃーないので、描画箇所を変える必要があります。
 普通のOpenGLではglTransformなどで位置を変えますが、GLKitではGLKBaseEffect.transform.modelviewMatrixを変更します。

 今回は[self.baseEffect prepareToDraw]としているので、このbaseEffectに移動情報を突っ込みます。

    _baseEffect.transform.modelviewMatrix = GLKMatrix4MakeTranslation(x, y, z) ;
    [_baseEffect prepareToDraw] ;

    // 描画
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

 こないな感じですね。
 modelviewMatrixに値を入れて、prepareToDrawしてから描画という順番です。

 これさえ抑えておけば複数表示も簡単です。forで回しましょう。

    for( int i=0 ; i<10 ; i++ ) {
        _baseEffect.transform.modelviewMatrix = GLKMatrix4MakeTranslation(x[i], y[i], z[i]) ;
        [_baseEffect prepareToDraw] ;

        // 描画
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    }

 当たり前ですが、prepareToDrawは毎回呼ぶ必要がありますので、注意してください。

飛び散る動きをつける

 GLKitにも、もちろんOpenGLにも、CAAnimationのように半自動的にアニメーションを追加する方法はありません。
 実際にx,y,zの値を変えて代入し、再描画します。

 では、どうすれば花火が飛び散るような動きをつけれるのでしょうか?
 思いつく感じ、いろんな方向に初速を与えて自由落下をさせればよさそうです。とても簡単。

 初期位置p0、初速v0、重力gの火花のt秒後の位置は
   p = p0 + v0t + gt*t/2
 でしょうか。中学レベルの数学ですが、とりあえずこれでやってみます。

クラス化する

 将来的にAndroidとかの移植も考えてるので、C++で花火の動きとかをクラス化します。
 どうせなら描画する部分もクラス化したいですね!

 ・・・というのが全ての過ちでした。わかりにくくなるだけなので全くおすすめできません。

 やっちゃったものは仕方ない、クラスの構成はこんな感じにしました。

CPPClass.cpp / OpenGL特有の受け渡しを便利にする大元のクラス
↓ 継承
FireWorksClass.cpp / 花火の描画や移動を担当
↑ 利用
FireworksView.mm / GLKitを継承してる

 ここまではよかったのですが、C++iOS特有のGLKitを組み込ませると意味ないので、baseEffectを使うとこを全部コールバックしないといけないんですね。
 ここで引き返せばよかった。

 コールバックしないといけないのは『描画位置を変える時』『テクスチャを生成する時』『テクスチャを貼り付ける時』などです。
 後者2つはまだでてきていません。

 C++のコールバックを実現するために、何を血迷ったのかLambda式を使いました。

CPPClass.h

#ifndef __OpenGLEffects__CPPClass__
#define __OpenGLEffects__CPPClass__

#include <iostream>

typedef std::function<void( float x, float y, float z )> OpenGLTransformLambda ;

class CPPClass {
public:
    static OpenGLTransformLambda openGLTransformLambda ;
    static void setOpenGLTransformLambda( OpenGLTransformLambda lambda ) ;
};

#endif /* defined(__OpenGLEffects__CPPClass__) */

・CPPClass.cpp

#include "CPPClass.h"

OpenGLTransformLambda CPPClass::openGLTransformLambda ;

void CPPClass::setOpenGLTransformLambda( OpenGLTransformLambda lambda ) {
    openGLTransformLambda = lambda ;
}

 Lambda式については省略しますが、Objective-Cのblocksに似た動きをします。というかblocksってただのLambda式な気がする。

 このCPPClassを継承したFireWorksClassに向けて、コールバックを登録します。

・FireworksView.mm

OpenGLTransformLambda func = [=](float x, float y, float z){
        _baseEffect.transform.modelviewMatrix = GLKMatrix4MakeTranslation(x, y, z) ;
        [_baseEffect prepareToDraw];
    } ;
    FireWorksClass::setOpenGLTransformLambda( func ) ;

 他のOSに移植するときはこの部分を変えればおっけー☆
 ちなみに花火クラスはこんな感じになりました。

・FireWorksClass.h

#ifndef __OpenGLEffects__FireWorksClass__
#define __OpenGLEffects__FireWorksClass__

#include <iostream>

#include <CPPClass/CPPClass.h>
#include <OpenGLES/ES2/gl.h>

#include <vector>


class FireWorksClass : public CPPClass {
    
public:
    
    FireWorksClass( int numSparks ) ;
    
    void start() ;
    void restart() ;
    void appendTime( float time ) ;
    void setMaxTimeInterval( float maxTimeInterval ) ; // ラグったに補正する最大の経過時間
    
    void setPosition( float px, float py ) ;
    void setGravity( float gx, float gy ) ;
    void setRestartTime( float restartTime ) ;
    
    void draw() ;
    
    
private:
    
    int m_numSparks ;
    float m_timeNow ;
    float m_maxTimeInterval ;
    
    float m_px, m_py ; // 爆発位置
    float m_gx, m_gy ; // 重力
    float m_restartTime ; // 再発射する時間間隔
    
    class FireWorksSparksClass {
    public:
        float m_vx0, m_vy0 ; // 初速
    } ;
    
    std::vector< FireWorksSparksClass* > *m_sparks ;
    
};


#endif /* defined(__OpenGLEffects__FireWorksClass__) */

・FireworksClass.cpp

#include "FireWorksClass.h"

#include <math.h>


GLuint vertexBufferID;
GLuint colorBufferID;


// 三角の座標
static const float vertices[] =
{
    -0.05f, -0.05f,  0.0,
    0.05f, -0.05f,  0.0,
    0.05f,  0.05f,  0.0,
    -0.05f,  0.05f,  0.0,
};

static const GLfloat colors[] =
{
    // R, G, B, A の順番
    1.0f, 0.0f, 0.0f, 1.0f, // 左上
    0.0f, 1.0f, 0.0f, 1.0f, // 右上
    0.0f, 0.0f, 1.0f, 1.0f, // 左下
    1.0f, 0.0f, 1.0f, 1.0f, // 右下
};



static bool isInitialized = false ;
static void initialize() {
    
    if( isInitialized ) return ;
    isInitialized = true ;
    
    // GPUに点の情報を
    glGenBuffers(1, &vertexBufferID);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    glGenBuffers(1, &colorBufferID);
    glBindBuffer(GL_ARRAY_BUFFER, colorBufferID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
    
}

static void dealloc() {
    
}


FireWorksClass::FireWorksClass( int numSparks ) {
    initialize() ;
    m_numSparks = numSparks ;
    
    setMaxTimeInterval(0.3f) ;
    setPosition((rand()%10000-5000)/5000.f, (rand()%10000-5000)/5000.f) ;
    setGravity(0, -0.3) ;
    setRestartTime( 3 ) ;
    
    // 火花
    m_sparks = new std::vector< FireWorksSparksClass* > ;
    for( int i_sparks=0 ; i_sparks<m_numSparks ; i_sparks++ ) {
        FireWorksSparksClass* sparks = new FireWorksSparksClass ;
        float deg = M_PI*2 / m_numSparks * i_sparks ;
        sparks->m_vx0 = cos(deg) ; sparks->m_vy0 = sin(deg) ;
        
        m_sparks->push_back( sparks ) ;
    }
}


void FireWorksClass::start() {
    m_timeNow = 0 ;
}

void FireWorksClass::restart() {
    setPosition((rand()%10000-5000)/5000.f, (rand()%10000-5000)/5000.f) ;
    start() ;
}

void FireWorksClass::appendTime( float time ) {
    if( time<m_maxTimeInterval ) {
        m_timeNow += time ;
    }
    else {
        m_timeNow += m_maxTimeInterval ;
    }
    
    if( m_timeNow > m_restartTime ) {
        restart() ;
    }
}

void FireWorksClass::setMaxTimeInterval(float maxTimeInterval) {
    m_maxTimeInterval = maxTimeInterval ;
}

void FireWorksClass::setPosition(float px, float py) {
    m_px = px ; m_py = py ;
}

void FireWorksClass::setGravity( float gx, float gy ) {
    m_gx = gx ; m_gy = gy ;
}

void FireWorksClass::setRestartTime( float restartTime ) {
    m_restartTime = restartTime ;
}


void FireWorksClass::draw() {
    
    for( int i_sparks=0 ; i_sparks<m_numSparks ; i_sparks++ ) {
        
        float x = ( m_sparks->at(i_sparks)->m_vx0 + m_gx/2*m_timeNow ) * m_timeNow + m_px ;
        float y = ( m_sparks->at(i_sparks)->m_vy0 + m_gy/2*m_timeNow ) * m_timeNow + m_py ;
        openGLTransformLambda( x, y, 0 ) ;
        
        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID) ;
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, NULL);
        
        glEnableVertexAttribArray(2);
        glBindBuffer(GL_ARRAY_BUFFER, colorBufferID) ;
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(float)*4, NULL) ;
        
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    }
    
}

 最初にinitializeを呼んでいます。頂点情報の作成は全火花で同じなので、1回だけ情報を作ります。
 (deallocを作って破棄する予定ですが、未実装です)

 Viewの方から火花の数を指定して初期化。
 appendTimeを呼んで経過時間を入力し、drawで再描画する仕組みです。
 Viewからの更新部分は悪くない実装なんじゃないかと思います。たぶん。

 drawの部分で前回と今回のコードがちょっと違う件ですが、

    // 色の情報をbind
    // 前回
    glEnableVertexAttribArray(GLKVertexAttribColor);

    // 今回
    glEnableVertexAttribArray(2);

 GLKitではGLKVertexAttribColor=2なので、GLKitの使えないC++側ではマジックナンバーを入れてます。
 ・・・ええ、他OSで使えるようにするなら、ここもどうにかしないといけません。もうしんどい。なんで描画部分もC++に含めちゃったんだろ。

花火Viewの方の実装

 描画部分が減ったので見やすくなってます。(その分C++の方が腐ってますが)
 アニメーションさせるためにNSTimerで定期的に更新します。

・FireworksView.h

#import <UIKit/UIKit.h>

#import <GLKit/GLKit.h>

@interface FireworksView : GLKView

@property (assign, nonatomic) float fps ;

@property (assign, nonatomic) int numFires ; // 花火の数
@property (assign, nonatomic) int numSparks ; // 火の粉

- (void)startAnimation ;
- (void)stopAnimation ;

@end

・FireworksView.mm

#import "FireworksView.h"
#import "FireWorksClass.h"

#import <vector>

#import <QuartzCore/QuartzCore.h>

@interface FireworksView() {
    
    std::vector< FireWorksClass* >* _fireWorkses ;
    
    double _timePassed ;
    double _timeBefore ;
    
    float _interval ;
}

@property (strong, nonatomic) GLKBaseEffect *baseEffect;
@property (retain, nonatomic) NSTimer* timerRefresh ;

@end



@implementation FireworksView

- (id)init {
    // 略
}

- (void)initView {
    
    // 略
    
    // 初期化
    _fireWorkses = new std::vector<FireWorksClass*> ;
    OpenGLTransformLambda func = [=](float x, float y, float z){
        _baseEffect.transform.modelviewMatrix = GLKMatrix4MakeTranslation(x, y, z) ;
        [_baseEffect prepareToDraw];
    } ;
    FireWorksClass::setOpenGLTransformLambda( func ) ;
    
    // デフォルト値
    self.numFires = 3 ;
    self.numSparks = 10 ;
    
    _timePassed=0 ; _timeBefore=0 ;
}

- (void)setFps:(float)fps {
    _fps = fps ;
    _interval = 1.f/fps ;
    
    for( auto fw : *_fireWorkses ) {
        fw->setMaxTimeInterval( _interval*2 ) ;
    }
}

- (void)setNumFires:(int)numFires {
    _numFires = numFires ;
    if( _numSparks>0 && _numFires>0 ) { [self _resetFireWorks] ; }
}

- (void)setNumSparks:(int)numSparks {
    _numSparks = numSparks ;
    if( _numSparks>0 && _numFires>0 ) { [self _resetFireWorks] ; }
}

- (void) _resetFireWorks {
    _fireWorkses->clear() ;
    for( int i=0 ; i<_numFires ; i++ ) {
        FireWorksClass* fw = new FireWorksClass( _numSparks ) ;
        _fireWorkses->push_back( fw ) ;
    }
}



- (void)drawRect:(CGRect)rect {
    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    [_baseEffect prepareToDraw];
    
    for( int i_sparks=0 ; i_sparks<_numFires ; i_sparks++ ) {
        _fireWorkses->at(i_sparks)->draw() ;
    }
}

- (void)startAnimation {
    
    for( int i_sparks=0 ; i_sparks<_numFires ; i_sparks++ ) {
        _fireWorkses->at(i_sparks)->start() ;
    }
    
    self.timerRefresh = [NSTimer scheduledTimerWithTimeInterval:_interval target:self selector:@selector(_actTimer:) userInfo:nil repeats:YES] ;
}

- (void)stopAnimation {
    
}


- (void) _actTimer:(NSTimer*)timer {
    
    double pased = 0 ;
    if( _timeBefore ) {
        pased = [NSDate timeIntervalSinceReferenceDate] - _timeBefore ;
        _timePassed += pased ;
    }
    _timeBefore = [NSDate timeIntervalSinceReferenceDate] ;
    
    for( int i_sparks=0 ; i_sparks<_numFires ; i_sparks++ ) {
        _fireWorkses->at(i_sparks)->appendTime( pased ) ;
    }
    
    [self display] ;
}


- (void)dealloc
{
    delete( _fireWorkses ) ;
    
    [super dealloc];
}

@end

 こっちは特に説明することは無いですね。

実行結果

f:id:hihokaruta:20130831163857g:plain

最後に

 最初の方のprepareToDrawのタイミングが重要です。GLKBaseEffectには描画位置の他にも、テクスチャを指定したり、ライトを設定したりできます。
 普通にOpenGLを使うと、そのへんの設定が冗長になりがちなのですが、うまいことまとめられてます。

 変な虹色の正方形じゃ花火な感じがしないので、次はテクスチャを貼ってみたいと思います。