FC2ブログ

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回の描画呼び出しで描ききる」ことは、ポイントスプライトでは不可能である。

iPhone実機とシミュレータのOpenGL ESを見比べる

タグ: iPhone OpenGL

そのむかし、出立てのiPhone SimulatorはOpenGL ESをサポートしていなかった。
そのためか、SDKが2.2.1になった今でも「iPhone実機ではできるがシミュレータではできない」ことが割とよくある。
目立つところではOES_matrix_palette拡張関数とOES_draw_texture拡張関数。
これらの関数はシミュレータでは完全にスルーされるため、実機でなければ挙動を確認することはできない。

逆に「シミュレータではできるが実機ではできない」ことも存在する。
例えば、新規プロジェクト「OpenGL ES Application」の

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

glLineWidth(128);
glDrawArrays(GL_LINE_STRIP, 0, 4);

こう変えた場合、シミュレータでは以下のように描かれ、

GL_LINE_STRIP 幅64ピクセル シミュレータ

実機では以下のように描かれる。

GL_LINE_STRIP 幅64ピクセル 実機

この場合、両者に違いはない。
いや、まあ。
この画像だけ見ると、実機の方が崩れているように見えるが、これは「『幅』を作る軸をXとYのどちらにするかの切り替え角が実機とシミュレータで異なる」ためにこう見えているだけで、どちらもGL_ALIASED_LINE_WIDTH_RANGEが示す最大値64(指定値128は最大値を超えているので、最大値が適用される)で描けていることに変わりはない。
実際、少し回転させるとシミュレータも実機と同じように崩れてくる。

しかし、上記のコードを

glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glLineWidth(128);
glDrawArrays(GL_LINE_STRIP, 0, 4);

このように書き換えた場合、シミュレータではCore GraphicsでいうところのkCGLineCapButtでそれぞれの線分を描いたような形になるが、

GL_LINE_STRIP GL_LINE_SMOOTH 幅64ピクセル シミュレータ

実機では単に1本の線として描かれる。

GL_LINE_STRIP GL_LINE_SMOOTH 幅64ピクセル 実機

これは、実機のGL_SMOOTH_LINE_WIDTH_RANGEの最大値が1(シミュレータはGL_ALIASED_LINE_WIDTH_RANGEと同じ64)であるためで、それはすなわち実機ではGL_LINE_SMOOTHが意味を成さないことを示している。

これは結構痛い。
以前「星型にクリップした画像を貼り付ける」作業を行った時、あとでGL_LINE_SMOOTHで縁取ってアンチエイリアスを施そうと思っていたのだが、こんな状態なので諦めた。
今のiPhoneはFSAA/MSAAが効かないっぽいので、プリミティブ境界にアンチエイリアスを施すのは結構手間である。

ただ、GL_POINTSだけは、GL_SMOOTH_POINT_SIZE_RANGEが実機・シミュレータともに最大64であるため、アンチエイリアスを掛けられる。

glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glPointSize(128);
glDrawArrays(GL_POINTS, 0, 4);
GL_POINTS 幅64ピクセル

そのため「直径64ピクセル以下の中抜きの円をアンチエリアス有りで描く」のは、「アンチエイリアスが施された線分を描く」より簡単だったりする。

glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);

glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE);
glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);
glPointSize(64);
glDrawArrays(GL_POINTS, 0, 4);
glBlendFunc(GL_ONE, GL_ONE);
glPointSize(60);
glDrawArrays(GL_POINTS, 0, 4);

glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA);
glPointSize(64);
glDrawArrays(GL_POINTS, 0, 4);
直径62ピクセル・幅4ピクセルの円

描画命令呼び出し回数の最小化、とか

タグ: iPhone Objective-C OpenGL

TN2230『Optimizing OpenGL ES for iPhone OS』の最終項「Minimizing the Number of Draw Calls」に、次のような記述がある。

16種256匹の蝶を描こうというのであれば、まずは描画命令を呼出す回数を、「1匹1回計256回」から「1種1回計16回」に削減するよう努めよ。

というわけで、以前作成した「1種50機の宇宙船を描くサンプル」における「1機1回計50回」の glDrawArrays 呼び出しを、「1種1回計1回」の glDrawElements 呼び出しに変えてみる。
なお、座標計算(とそのコストの把握)が面倒だったので、今回 glRotatef によるモデルビューの回転は排除した。

// drawView のfor文をまるっと書き換え
const GLfloat textureCoordinates[] = {
	0, 1,
	1, 1,
	0, 0,
	1, 0,
};

const GLfloat halfPixelsWide = texture.pixelsWide / 2.f;
const GLfloat halfPixelsHigh = texture.pixelsHigh / 2.f;

const NSUInteger objectCount = [array count];
const NSUInteger vertexCount = objectCount * 4;
const NSUInteger indexCount = objectCount * 6 - 2;

GLfloat vertices[vertexCount * 3];
GLfloat coordinates[vertexCount * 2];
GLushort indices[indexCount];

GLfloat* pv = vertices;
GLfloat* pc = coordinates;
GLushort* pi = indices;
GLushort index = 0;
for(Foo* foo in array) {
	[foo stepIn:size];
	
	const GLfloat modelVertices[] = {
		-halfPixelsWide + foo.coord.x, -halfPixelsHigh + foo.coord.y, 0.f,
		 halfPixelsWide + foo.coord.x, -halfPixelsHigh + foo.coord.y, 0.f,
		-halfPixelsWide + foo.coord.x,  halfPixelsHigh + foo.coord.y, 0.f,
		 halfPixelsWide + foo.coord.x,  halfPixelsHigh + foo.coord.y, 0.f,
	};
	
	memcpy(pv, modelVertices, sizeof(modelVertices));
	pv += sizeof(modelVertices) / sizeof(*modelVertices);
	
	memcpy(pc, textureCoordinates, sizeof(textureCoordinates));
	pc += sizeof(textureCoordinates) / sizeof(*textureCoordinates);
	
	if(index > 0) {
		*(pi++) = index - 1;
		*(pi++) = index;
	}
	for(int i = 0; i < 4; i++) {
		*(pi++) = index++;
	}
}

glBindTexture(GL_TEXTURE_2D, texture.name);
glVertexPointer(3, GL_FLOAT, 0, vertices);
glTexCoordPointer(2, GL_FLOAT, 0, coordinates);
glDrawElements(GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_SHORT, indices);
glDrawArrays 50回26fps
glDrawElements 1回26fps

まるで変わっていない……!(^o^)

多分、アルファブレンディングが遅過ぎて、変更の影響が見えないのだろう。
TN2230にもアルファブレンディングやアルファテスティングの使用は必要最小限に留めよと記されている。
なのでとりあえず initWithCoder:glEnable(GL_BLEND); をコメントアウト。するとフレームレートがどちらも60fpsになってので、 objectCount を50から500に増やして比較してみた。

glDrawArrays 500回17fps
glDrawElements 1回8fps

1回にまとめた方がむしろ遅くなってる……!/(^o^)\

これは、なんだろう。画像が大きくて、重なりあう部分が多いからだろうか。
ならばあまり重ならないよう画像のサイズを小さく(32x32に)し、 velocity の計算を

velocity = CGPointMake(rand() % 1001 / 100.f - 5, rand() % 1001 / 100.f - 5);

このように変更したらどうか。

glDrawArrays 500回24fps
glDrawElements 1回16fps(開始1秒のばらけきっていない時分は33fps)

重なってると速いのは、むしろ1回にまとめた方だった!\(^o^)/

そんなわけで、残念ながら呼び出しオーバーヘッドの削減によるパフォーマンスの向上は実践できず。
素直に1機ずつ描いた方が、コード的にも速度的にも良いという結果になった。

ちなみに。
glDrawArrays を500回呼んでいる方は、同時に glBindTexture も500回呼んでいる。
これを、TN2230の冗長な状態変更を避けよという言葉に従い、ループ前に1度だけ呼ぶようにすると、フレームレートが24fpsから30fpsに向上する上、 glEnable(GL_BLEND); を有効にしても(少なくともこの32x32画像では)パフォーマンスにさほど影響が出ない。
この結果を見ると、同じくTN2230にある一つの大きなテクスチャに複数のテクスチャをまとめ、描画命令の呼び出し回数を削減せよという指示は、なるほど、たしかに効果が期待できそうに思える。

CAEAGLLayerを同じ絵で埋め尽くす

タグ: iPhone OpenGL Objective-C

以前CGContextでやった、任意の領域を特定の画像で埋め尽くす処理を、OpenGL ESでもやってみる。

まずは、任意の領域の指定に「深度バッファ」を使ってみよう。
このような用途には「ステンシルバッファ」の方が適しているのだろうが、iPhoneのOpenGL ESにはステンシルバッファはない。

  1. iGLUの試用に使ったプロジェクトに画像(ここでは「64x64.png」)を追加する。
  2. EAGLViewに以下のインスタンス変数を追加する。
    GLuint tileTexture;
    CGSize tileSize;
  3. #define USE_DEPTH_BUFFER 0#define USE_DEPTH_BUFFER 1 に書き換える。
  4. const int vertex_color_size = 4;const int vertex_color_size = 0; に書き換える。
  5. initWithCorder: の後ろの方でテクスチャーを作る。
    CGImageRef tileImage = [UIImage imageNamed:@"64x64.png"].CGImage;
    size_t width = CGImageGetWidth(tileImage);
    size_t height = CGImageGetHeight(tileImage);
    
    GLubyte* tileData = (GLubyte*)malloc(width * height * 4);
    CGContextRef tileContext = CGBitmapContextCreate(tileData, width, height, 8, width * 4, CGImageGetColorSpace(tileImage), kCGImageAlphaPremultipliedLast);
    CGContextDrawImage(tileContext, CGRectMake(0.0, 0.0, width, height), tileImage);
    CGContextRelease(tileContext);
    	
    glGenTextures(1, &tileTexture);
    glBindTexture(GL_TEXTURE_2D, tileTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, tileData);
    free(tileData);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    tileSize = CGSizeMake(width, height);
    
    glEnable(GL_TEXTURE_2D);
    
    // ついでに深度テストも有効に
    glEnable(GL_DEPTH_TEST);
  6. drawView:glClear から下を書き換える。
    // 深度バッファをクリア
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    
    // ☆を描く
    glDepthFunc(GL_ALWAYS);
    std::for_each(parts.begin(), parts.end(), &draw_part);
    
    const GLshort vertices[] = {
    	-1, -1,
    	1, -1,
    	-1, 1,
    	1, 1,
    };
    
    const GLfloat w = CGRectGetWidth(self.bounds) / tileSize.width;
    const GLfloat tileTexcoords[] = {
    	0.0f, w,
    	w, w,
    	0.0f, 0.0f,
    	w, 0.0f,
    };
    
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    
    // ☆が描かれたピクセル(=深度が同じピクセル)にのみ画像を貼り付ける
    glDepthFunc(GL_EQUAL);
    glVertexPointer(2, GL_SHORT, 0, vertices);
    glTexCoordPointer(2, GL_FLOAT, 0, tileTexcoords);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
    [context presentRenderbuffer:GL_RENDERBUFFER_OES];
  7. dealloc でテクスチャを削除する。

以上。

これで「☆形の領域にのみ画像を並べて貼り付ける」ことができる。
ただ、この方法は「描く」か「描かない」の二択でしかないため、境界のジャギを抑えられないのが難。

depth_star.png

次に、「アルファ値」を使う方法を試す。
アルファ値の計算は深度テストよりいささか手間だが、その分「半透明で描く」という指示も可能だ。

  1. 先のプロジェクトにマスク画像(ここでは「mask.png」)を追加する。
  2. EAGLViewに以下のインスタンス変数を追加する。
    GLuint maskTexture;
  3. #define USE_DEPTH_BUFFER 1#define USE_DEPTH_BUFFER 0 に戻す。
  4. initWithCorder: の後ろの方でさらにテクスチャーを作る。
    // glEnable(GL_DEPTH_TEST); 深度テストは有効にしない
    
    CGImageRef maskImage = [UIImage imageNamed:@"mask.png"].CGImage;
    width = CGImageGetWidth(maskImage);
    height = CGImageGetHeight(maskImage);
    
    GLubyte* maskData = (GLubyte*)malloc(width * height);
    CGContextRef maskContext = CGBitmapContextCreate(maskData, width, height, 8, width, NULL, kCGImageAlphaOnly);
    CGContextDrawImage(maskContext, CGRectMake(0.0, 0.0, width, height), maskImage);
    CGContextRelease(maskContext);
    
    glGenTextures(1, &maskTexture);
    glBindTexture(GL_TEXTURE_2D, maskTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, maskData);
    free(maskData);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  5. drawView:glClear から下を書き換える。
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    const GLshort maskTexcoords[] = {
    	0, 1,
    	1, 1,
    	0, 0,
    	1, 0,
    };
    
    glClear(GL_COLOR_BUFFER_BIT);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    
    glVertexPointer(2, GL_SHORT, 0, vertices);
    
    // 描画する領域の色を、アルファ値含めて落とす
    glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);
    glBindTexture(GL_TEXTURE_2D, maskTexture);
    glTexCoordPointer(2, GL_SHORT, 0, maskTexcoords);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    // アルファ値が1になるように色を混ぜ合わせる
    glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE);
    glBindTexture(GL_TEXTURE_2D, tileTexture);
    glTexCoordPointer(2, GL_FLOAT, 0, tileTexcoords);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
    [context presentRenderbuffer:GL_RENDERBUFFER_OES];
  6. dealloc でテクスチャを削除する。

以上。

境界を拡大してみると、マスクのアルファ値に合わせて、ちゃんとアンチエイリアスっぽくなっているのが分かる。

alpha_mask.png

まあ、これはこれで、元画像のアルファ値を完全に無視しているので、完璧ではないが。

iGLUでテセレーション

タグ: iPhone Objective-C C++ OpenGL

iGLU: OpenGL Utility Library for iPhone OS

Available features:
  • Matrix manipulation
  • Polygon tessellation

iPhone向けGLUライブラリ。
ちょうどポリゴンをテセレーションしたかったのでありがたい。
早速使ってみる(SDK for iPhone OS 2.2)。

  1. プロジェクトテンプレート「OpenGL ES Application」を選んで新規プロジェクトを作成。
  2. iGLUへのパスを設定。
  3. 一部処理をC++のSTLで楽したいので、「EAGLView.m」を「EAGLView.mm」に改名。
  4. EAGLView.mmにインクルード文を追加。
    #include <vector>
    #include <list>
    #include <algorithm>
    #include <glu.h>
  5. コールバック関数等を定義。
    // 頂点配列(7要素で1頂点)
    
    typedef std::vector<GLfloat> es_vertices;
    typedef std::vector<GLdouble> glu_vertices;
    const int vertex_coord_size = 3;
    const int vertex_color_size = 4;
    const int vertex_size = vertex_coord_size + vertex_color_size;
    
    // ポリゴンの三角形プリミティブ
    
    struct polygon_part
    {
    	GLenum type;
    	es_vertices vertices;
    };
    
    void draw_part(const polygon_part& part)
    {
    	glVertexPointer(
    		vertex_coord_size,
    		GL_FLOAT,
    		sizeof(GLfloat) * vertex_size,
    		&part.vertices[0]
    	);
    	glColorPointer(
    		vertex_color_size,
    		GL_FLOAT,
    		sizeof(GLfloat) * vertex_size,
    		&part.vertices[0] + vertex_coord_size
    	);
    	glDrawArrays(part.type, 0, part.vertices.size() / vertex_size);
    }
    
    // テセレーション用データ
    
    typedef std::vector<polygon_part> polygon_parts;
    
    struct tessellation_data
    {
    	polygon_parts* parts;
    	std::list<glu_vertices> combine_buffer;
    };
    
    // テセレーション用コールバック
    
    void tess_begin_data(
    	GLenum type,
    	tessellation_data* polygon_data
    )
    {
    	polygon_part part;
    	part.type = type;
    	polygon_data->parts->push_back(part);
    }
    
    void tess_vertex_data(
    	const GLdouble* vertex_data,
    	tessellation_data* polygon_data
    )
    {
    	es_vertices& vertices = polygon_data->parts->back().vertices;
    	vertices.insert(vertices.end(), vertex_data, vertex_data + vertex_size);
    }
    
    void tess_combine_data(
    	const GLdouble coords[3],
    	const GLdouble* vertex_data[4],
    	const GLfloat weight[4],
    	const GLdouble** out_data,
    	tessellation_data* polygon_data
    )
    {
    	glu_vertices vertex(vertex_size);
    	std::copy(coords, coords + vertex_coord_size, vertex.begin());
    	for(int i = vertex_coord_size; i < vertex_size; i++)
    	{
    		vertex[i] =
    			weight[0] * vertex_data[0][i] +
    			weight[1] * vertex_data[1][i] +
    			weight[2] * vertex_data[2][i] +
    			weight[3] * vertex_data[3][i];
    	}
    	std::list<glu_vertices>& buffer = polygon_data->combine_buffer;
    	*out_data = &((*buffer.insert(buffer.end(), vertex))[0]);
    }
  6. drawViewの中身を書き換える。
    GLdouble starVertices[][vertex_size] = {
    	{ -0.5, -0.5, 0.0, 1.0, 0.0, 1.0, 1.0 },
    	{ 0.0, 0.5, 0.0, 1.0, 1.0, 0.0, 1.0 },
    	{ 0.5, -0.5, 0.0, 0.0, 1.0, 1.0, 1.0 },
    	{ -0.75, 0.1, 0.0, 1.0, 0.0, 0.0, 1.0 },
    	{ 0.75, 0.1, 0.0, 0.0, 1.0, 0.0, 1.0 },
    };
    
    polygon_parts parts;
    
    GLUtesselator* t = gluNewTess();
    gluTessCallback(t, GLU_TESS_BEGIN_DATA, (GLvoid (*) ())&tess_begin_data);
    gluTessCallback(t, GLU_TESS_VERTEX_DATA, (GLvoid (*) ())&tess_vertex_data);
    gluTessCallback(t, GLU_TESS_COMBINE_DATA, (GLvoid (*) ())&tess_combine_data);
    gluTessProperty(t, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_NONZERO);
    tessellation_data data;
    data.parts = &parts;
    gluTessBeginPolygon(t, &data);
    gluTessBeginContour(t);
    for(int i = 0; i < 5; i++)
    {
    	gluTessVertex(t, starVertices[i], starVertices[i]);
    }
    gluTessEndContour(t);
    gluTessEndPolygon(t);
    gluDeleteTess(t);
    
    [EAGLContext setCurrentContext:context];
    
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
    glViewport(0, 0, backingWidth, backingHeight);
    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
    glMatrixMode(GL_MODELVIEW);
    glRotatef(3.0f, 0.0f, 0.0f, 1.0f);
    
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    std::for_each(parts.begin(), parts.end(), &draw_part);
    
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
    [context presentRenderbuffer:GL_RENDERBUFFER_OES];
tessellation.png

Appendix

タグ

Blog内検索

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