OpenGL ES(GLKit)を使って動くエフェクトを作る(その2:花火の火花みたいなのを作る)
OpenGL ES(GLKit)を使って動くエフェクトを作る(その1:GLKViewを使ってみる)の続き
花火みたいなエフェクトを作ろう!ってのが目標です。実際の花火は上の画像みたいに「複数の火花が尾を引いて落下している」ように見えます。
ということでまずは複数表示をしてみます。
複数表示
同じ所に複数表示してもしゃーないので、描画箇所を変える必要があります。
普通の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
こっちは特に説明することは無いですね。
実行結果
最後に
最初の方のprepareToDraw
のタイミングが重要です。GLKBaseEffect
には描画位置の他にも、テクスチャを指定したり、ライトを設定したりできます。
普通にOpenGLを使うと、そのへんの設定が冗長になりがちなのですが、うまいことまとめられてます。
変な虹色の正方形じゃ花火な感じがしないので、次はテクスチャを貼ってみたいと思います。