Entries

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

ポイントスプライト(というかパーティクル)で遊ぶ

タグ: iPhone OpenGL Objective-C
particle2.png

指を置くとパーティクルが噴き出す。
複数(5本まで)指を置くと、別の指から噴き出したパーティクル同士が引かれ合う。
そんなおもちゃ。

  • 例によって「OpenGL ES Application」の新規プロジェクトを作成する(Xcode 3.1.2)。
  • iPhone OS 2.2 Libraryのサンプル「GLPaint」に含まれている「Particle.png」をプロジェクトに追加する。
  • 「Frameworks」に「CoreGraphics.framework」を追加する。
  • 新規ファイル「ParticleSystem.h」をプロジェクトに追加し、以下のコードを記述する。
    #import <CoreGraphics/CGGeometry.h>
    
    typedef CGPoint Coord;
    typedef Coord Vector;
    
    @interface Particle : NSObject
    {
    	NSUInteger life;
    	Coord coord;
    	Vector vector;
    }
    - (BOOL)step;
    @property (nonatomic) NSUInteger life; // 残生存ステップ数
    @property (nonatomic) Coord coord; // 現在座標
    @property (nonatomic) Vector vector; // ステップ辺りの移動量
    @end
    
    @interface ParticleSystem : NSObject
    {
    	NSUInteger life;
    	Coord coord;
    	NSUInteger interval;
    	NSMutableArray* particles;
    	
    	NSInteger counter;
    }
    - (void)step;
    @property (nonatomic) NSUInteger life; // パーティクルの初期生存ステップ数
    @property (nonatomic) Coord coord; // パーティクルの初期座標
    @property (nonatomic) NSUInteger interval; // パーティクルの生成間隔(0なら生成せず)
    @property (nonatomic, readonly) NSArray* particles; // 管理しているパーティクル
    @end
    
    @interface ParticleSystem(Super)
    + (void)stepSystems:(NSArray*)particleSystems;
    @end
  • 新規ファイル「ParticleSystem.m」をプロジェクトに追加し、以下のコードを記述する。
    #include <math.h>
    #include <limits.h>
    #import "ParticleSystem.h"
    
    @implementation Particle
    
    @synthesize life, coord, vector;
    
    - (BOOL)step
    {
    	if(life == 0)
    		return NO;
    	coord.x += vector.x;
    	coord.y += vector.y;
    	life--;
    	return YES;
    }
    
    @end
    
    @implementation ParticleSystem
    
    @synthesize life, coord, interval, particles;
    
    - (id)init
    {
    	if(self = [super init]) {
    		life = 60;
    		interval = 1;
    		particles = [[NSMutableArray alloc] initWithCapacity:life / interval + 1];
    	}
    	return self;
    }
    
    - (void)dealloc
    {
    	[particles release];
    	[super dealloc];
    }
    
    - (void)step
    {
    	if(interval != 0) {
    		if(counter == 0) {
    			Particle* particle;
    			particle = [[Particle alloc] init];
    			particle.life = life;
    			particle.coord = coord;
    			Vector vector = {
    				(rand() % 1001 - 500) / 500.f,
    				(rand() % 1001 - 500) / 500.f
    			};
    			particle.vector = vector;
    			[particles addObject:particle];
    		}
    		counter = (counter + 1) % interval;
    	}
    
    	NSMutableIndexSet* deadParticleIndices = [NSMutableIndexSet indexSet];
    	NSUInteger index = 0;
    	for(Particle* particle in particles) {
    		if(! [particle step]) {
    			[deadParticleIndices addIndex:index];
    		}
    		index++;
    	}
    	[particles removeObjectsAtIndexes:deadParticleIndices];
    }
    
    @end
    
    @implementation ParticleSystem(Super)
    + (void)stepSystems:(NSArray*)particleSystems
    {
    	for(ParticleSystem* system in particleSystems) {
    		[system step];
    	}
    
    	NSUInteger n = [particleSystems count];
    	if(n <= 1)
    		return;
    
    	for(NSUInteger i = 0; i < n - 1; i++) {
    		ParticleSystem* system1 = [particleSystems objectAtIndex:i];
    		for(NSUInteger j = i + 1; j < n; j++) {
    			ParticleSystem* system2 = [particleSystems objectAtIndex:j];
    			for(Particle* p1 in system1.particles) {
    				Vector v1 = p1.vector;
    				for(Particle* p2 in system2.particles) {
    					Vector v2 = p2.vector;
    					float dx = p2.coord.x - p1.coord.x;
    					float dy = p2.coord.y - p1.coord.y;
    					float d = sqrtf(dx * dx + dy * dy);
    					if(d < 10.f)
    						continue;
    					float s = 0.002f;
    					float vx = s * dx / d;
    					float vy = s * dy / d;
    					v1.x += vx;
    					v1.y += vy;
    					v2.x -= vx;
    					v2.y -= vy;
    					p1.vector = v1;
    					p2.vector = v2;
    				}
    			}
    		}
    	}
    }
    @end
  • EAGLViewクラスに以下のインスタンス変数を追加する。
    NSMutableArray* particleSystems; // 表示中のパーティクルシステム
    NSMutableDictionary* touchingSystems; // 操作中のパーティクルシステム
  • 追加したインスタンス変数は EAGLView::dealloc で解放する。
    [touchingSystems release];
    [particleSystems release];
  • EAGLViewクラスに以下のメソッドを追加する。
    - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
    {
    	for(UITouch* touch in touches) {
    		ParticleSystem* system = [[ParticleSystem alloc] init];
    		system.coord = [touch locationInView:self];
    		[touchingSystems setObject:system forKey:[NSValue valueWithPointer:touch]];
    		[particleSystems addObject:system];
    		[system release];
    	}
    }
    
    - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
    {
    	for(UITouch* touch in touches) {
    		ParticleSystem* system = [touchingSystems objectForKey:[NSValue valueWithPointer:touch]];
    		system.coord = [touch locationInView:self];
    	}
    }
    
    - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
    {
    	for(UITouch* touch in touches) {
    		NSValue* value = [NSValue valueWithPointer:touch];
    		ParticleSystem* system = [touchingSystems objectForKey:value];
    		system.interval = 0;
    		[touchingSystems removeObjectForKey:value];
    	}
    }
    
    - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
    {
    	for(UITouch* touch in touches) {
    		NSValue* value = [NSValue valueWithPointer:touch];
    		ParticleSystem* system = [touchingSystems objectForKey:value];
    		[touchingSystems removeObjectForKey:value];
    		[particleSystems removeObject:system];
    	}
    }
  • EAGLView::initWithCoder:animationInterval = 1.0 / 60.0; の下辺りに以下のコードを追加する。
    // テクスチャを読み込む
    CGImageRef image = [UIImage imageNamed:@"Particle.png"].CGImage;
    size_t width = CGImageGetWidth(image);
    size_t height = CGImageGetHeight(image);
    
    GLubyte* data = (GLubyte*)malloc(width * height * 4);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef particleContext =
    	CGBitmapContextCreate(data, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);
    CGContextClearRect(particleContext, CGRectMake(0, 0, width, height));
    CGContextDrawImage(particleContext, CGRectMake(0, 0, width, height), image);
    CGContextRelease(particleContext);
    
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    free(data);
    
    // テクスチャを有効化
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glEnable(GL_TEXTURE_2D);
    
    // ポイントスプライトを有効化
    glTexEnvi(GL_POINT_SPRITE_OES, GL_COORD_REPLACE_OES, GL_TRUE);
    glEnable(GL_POINT_SPRITE_OES);
    
    // ブレンド方針を設定
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glEnable(GL_BLEND);
    
    // ポイントスプライトのサイズは64で固定
    glPointSize(64);
    
    // 頂点配列と色配列を有効化
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    
    // その他初期化
    particleSystems = [[NSMutableArray alloc] init];
    touchingSystems = [[NSMutableDictionary alloc] init];
    self.multipleTouchEnabled = YES;
  • EAGLView::drawView を以下の様に書き換える。
    [EAGLContext setCurrentContext:context];
    
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
    glViewport(0, 0, backingWidth, backingHeight);
    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrthof(0.0f, backingWidth, backingHeight, 0.0f, -1.0f, 1.0f);
    
    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    [ParticleSystem stepSystems:particleSystems];
    
    NSMutableIndexSet* deadParticleSystemIndices = [NSMutableIndexSet indexSet];
    NSUInteger index = 0;
    for(ParticleSystem* system in particleSystems) {
    	NSArray* particles = system.particles;
    	NSUInteger particlesCount = [particles count];
    	if(particlesCount == 0) {
    		// パーティクルを管理しておらず
    		if(system.interval == 0) {
    			// パーティクル生成間隔が0(=生成しない)のシステムは削除対象
    			[deadParticleSystemIndices addIndex:index];
    		}
    	}
    	else {
    		GLfloat data[particlesCount * 6];
    		GLfloat* p = data;
    		GLfloat ratioUnit = 1.f / system.life;
    		for(Particle* particle in particles) {
    			GLfloat ratio =  particle.life * ratioUnit;
    			*(p++) = particle.coord.x;
    			*(p++) = particle.coord.y;
    			*(p++) = 0.8f * ratio;
    			*(p++) = 0.8f * ratio;
    			*(p++) = 2.0f * ratio;
    			*(p++) = 1.f;
    		}
    		glVertexPointer(2, GL_FLOAT, 6 * sizeof(GLfloat), data);
    		glColorPointer(4, GL_FLOAT, 6 * sizeof(GLfloat), data + 2);
    		glDrawArrays(GL_POINTS, 0, particlesCount);
    	}
    	index++;
    }
    [particleSystems removeObjectsAtIndexes:deadParticleSystemIndices];
    
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
    [context presentRenderbuffer:GL_RENDERBUFFER_OES];

以上。
今回は「ポイントスプライトを使って何かする」のが目的なので、それ以外は割りと適当。
「UITouchインスタンスの同一性をキーにパーティクルシステムを管理している」部分も、これが正しいかどうかは分からない。

単一のテクスチャを

  • 回転等させず
  • GL_POINT_SIZE_MAX(iPhoneなら64)以下のサイズで

描きたい場合、ポイントスプライトを使う方がスマートで、かつ速い。
以前書いた記事の後半(まさにこの条件に合致している)なども、ポイントスプライトで実装するとそれだけで30fpsから38fpsに改善される。
ただし、OpenGLのポイントスプライトはテクスチャアトラスが使えない。
そのため「全種類のテクスチャを1回の描画呼び出しで描ききる」ことは、ポイントスプライトでは不可能である。

スポンサーサイト

コメント

[C17] マルチタッチの追跡について

はじめましてこんにちは。いつもすばらしい情報をありがとうございます。
ちょっと質問させてください。
このサンプルではtouches~メソッドの引数であるUITouchオブジェクトのポインタをNSValue valueWithPointer:で連想配列のキーにしていますが、このUITouchのポインタは、1系統のタッチが継続していれば、次回以降のtouches~イベントでも同じオブジェクトが継続している事が保障されるのでしょうか?どこかにドキュメント化されているのでしょうか?
以上、宜しくお願いします。
  • 2009-04-23 13:49
  • ken
  • URL
  • 編集

[C18]

ごめんなさい、本文に「正しいか分からない」と、ただし書きがあったのを見落としてました。失礼しました。
  • 2009-04-23 15:02
  • ken
  • URL
  • 編集

[C19]

先程は失礼しました。
iPhone Application Programming Guide: Even-Handling

Event-Handling Techniques - Tracking the mutations of UITouch objects
に、
In the touchesBegan:withEvent: method, you can obtain the original location of each touch from the locationInView: method and store those in a CFDictionary object using the addresses of the UITouch objects as keys. Then, in the touchesEnded:withEvent: method you can use the address of each passed-in UITouch object to obtain the object’s original location and compare that with its current location. (You should use a CFDictionary object rather than an NSDictionary object; the latter copies its keys, but the UITouch class does not adopt the NSCopying protocol, which is required for object copying.)
と、ありました。UITouchのアドレスは保障されるようです。
  • 2009-04-23 17:06
  • ken
  • URL
  • 編集

[C20]

>と、ありました。UITouchのアドレスは保障されるようです。
おお。わざわざありがとうございます。

>You should use a CFDictionary object...
NSDictionaryはキー値をコピーするが、UITouchはコピーできない(というかコピーしたら意味ない)。
「ならばNSValueで『ポインタ』扱いすればいいじゃない」←私
「だからCFDictionaryを使うのがいいんじゃない」←Programming Guide
後者の発想はなかったなぁ。
  • 2009-04-24 00:03
  • むい
  • URL
  • 編集

[C21]

Objective-Cを初めてまだ1ヶ月なので、valueForPointerの使い方を見てなるほどと、感心いたしました。
CFDictionaryはenumratorがないようでとても使いにくいのでNSDictionaryの方法を模倣させていただきたいと思います。

これからも応援させていただきます。ご教示ありがとうございました。m(_ _)m
  • 2009-04-24 03:05
  • ken
  • URL
  • 編集

[C22] CFDictionaryRefとNSDictionary *の関係

CFDictionaryRefはNSDictionary *に無償で型変換可能(toll-free bridge)です。
http://developer.apple.com/documentation/Cocoa/Conceptual/CarbonCocoaDoc/Articles/InterchangeableDataTypes.html

enumeratorが必要になったら、するっとNSDictionary *に型変換してしまいましょう。

……ん?
というか今回のような場合、逆に「追加するときにCFMutableDictionaryRefに型変換」すれば、NSValueを使わなくてもいけるのか……?
  • 2009-04-24 22:04
  • むい
  • URL
  • 編集

[C23]

お恥ずかしい、知りませんでした。
せっかくなのでテストしてみました。

NSMutableDictionary* touchDict;

--touchesBegan--
TouchInfo* value = [[TouchInfo alloc] initWithTouchInfo:touch];
CFDictionarySetValue((CFMutableDictionaryRef)touchDict, touch, value);

--touchesEnded--
TouchInfo* value = (TouchInfo*)CFDictionaryGetValue((CFMutableDictionaryRef)touchDict, touch);
CFDictionaryRemoveValue((CFMutableDictionaryRef)touchDict, touch);

のように使用してみたら全く問題なく使えました。
とてもお勉強になりました。どうもありがとう御座います。
touchDictはenumrateしてジェスチャーのセンター位置計算とかに使うのでNSMutableDictionary使ってましたが、素直にCFMutableDIctionary使った方が利口かな^^;
  • 2009-04-25 04:07
  • ken
  • URL
  • 編集

[C24]

あれ?まてよ?CFMutableDictionaryRefに型変換してCFDictionarySetValueでObjective-cのインスタンスポインタValueをセットしたらValueはretainされなくなるのかな?
とか思ったらちゃんとretainされてて、CFDictionaryRemoveValueできちんとdeallocされてました。
う~ん難しい。ちゃんと分かってないと怖いですね。
  • 2009-04-25 14:37
  • ken
  • URL
  • 編集

[C25]

その辺、私も気になったので確認していました。
http://idlysphere.blog66.fc2.com/blog-entry-194.html

> ちゃんと分かってないと怖いですね。
うむ。自分で納得&安心できる方法を選ぶのが一番です。
  • 2009-04-25 18:24
  • むい
  • URL
  • 編集

コメントの投稿

コメントの投稿
管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://idlysphere.blog66.fc2.com/tb.php/179-fcb715d8
この記事にトラックバックする(FC2ブログユーザー)

Appendix

タグ

Blog内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。