Entries

スポンサーサイト

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

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

OpenGL ESとUIViewの比較

タグ: Objective-C iPhone OpenGL

同じ画像を50枚、画面内をくるくる跳ね回らせる処理を、OpenGL ESとUIViewでそれぞれ書いて実行してみた(SDK for iPhone OS 2.2)。

くるくるごちゃごちゃ

まずはOpenGL ES。
なお、以下の記述は全てSDK for iPhone OS 2.2のプロジェクトテンプレートを使ったものであり、それ以外のバージョンでも同じように書けるかどうかは不明である。

  1. プロジェクトテンプレート「OpenGL ES Application」を選んで新規プロジェクトを作成
  2. iPhone OS 2.1 Library(2.0でも良い/2.2には無い)のサンプルコード「CrashLanding」から「Texture2D.*」と「Ship.png」をコピーしてプロジェクトに加える
  3. EAGLViewに以下のインスタンス変数を追加
    Texture2D* texture;
    NSArray* array;
  4. 各画像(スプライト)の速度や位置を保持・計算するクラス・Fooを追加
    @interface Foo : NSObject {
    	CGPoint velocity;
    	NSInteger angularVelocity;
    	CGPoint coord;
    	NSInteger angle;
    }
    - (void)step;
    - (void)stepIn:(CGSize)size;
    @property CGPoint velocity;
    @property NSInteger angularVelocity;
    @property (readonly) CGPoint coord;
    @property (readonly) NSInteger angle;
    @end
    
    @implementation Foo
    @synthesize velocity, angularVelocity, coord, angle;
    - (id)init {
    	if(self = [super init]) {
    		velocity = CGPointMake(rand() % 11 - 5, rand() % 11 - 5);
    		angularVelocity = rand() % 21 - 10;
    	}
    	return self;
    }
    - (void)step {
    	NSInteger newAngle = angle + angularVelocity;
    	while(newAngle >= 360) newAngle -= 360;
    	while(newAngle < 0) newAngle += 360;
    	angle = newAngle;
    	coord = CGPointMake(coord.x + velocity.x, coord.y + velocity.y);
    }
    - (void)stepIn:(CGSize)size {
    	[self step];
    	if(fabs(coord.x) > size.width) velocity.x = -velocity.x;
    	if(fabs(coord.y) > size.height) velocity.y = -velocity.y;
    }
    @end
  5. EAGLView::initWithCorder: の後ろの方にテクスチャ関係の初期化コードを追加
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    
    texture = [[Texture2D alloc] initWithImage:[UIImage imageNamed:@"Ship.png"]];
    
    const NSUInteger objectCount = 50;
    NSMutableArray* mutableArray = [[NSMutableArray alloc] initWithCapacity:objectCount];
    for(int i = 0; i < objectCount; i++) {
    	Foo* foo = [[Foo alloc] init];
    	[mutableArray addObject:foo];
    	[foo release];
    }
    array = mutableArray;
  6. EAGLView::createFramebuffer の後ろの方に視点関係の初期化コードを追加
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrthof(-backingWidth / 2, backingWidth / 2, -backingHeight / 2, backingHeight / 2, -1, 1);
    glMatrixMode(GL_MODELVIEW);
    glViewport(0, 0, backingWidth, backingHeight);
    glScissor(0, 0, backingWidth, backingHeight);
  7. EAGLView::drawView を書き換える
    [EAGLContext setCurrentContext:context];
    
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    CGSize size = {
    	(backingWidth - texture.contentSize.width) / 2.0f,
    	(backingHeight - texture.contentSize.height) / 2.0f
    };
    
    for(Foo* foo in array) {
    	[foo stepIn:size];
    
    	glPushMatrix();
    	glTranslatef(foo.coord.x, foo.coord.y, 0.0f);
    	glRotatef(foo.angle, 0.0f, 0.0f, 1.0f);
    	[texture drawAtPoint:CGPointZero];
    	glPopMatrix();
    }
    
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
    [context presentRenderbuffer:GL_RENDERBUFFER_OES];
  8. EAGLView::dealloc に後始末コードを追加し、ビルドが通るように#importやらフレームワークやらを追加する

OpenGL ESを使ったのはこれが初めてなので、本当にこの書き方が効率いいのか分からなかったりするが、ともかく、これを第1世代iPod touchで実行した結果は24fps(Instrumentsで測定)だった。

続いてUIView。

  1. プロジェクトテンプレート「Window-Based Application」を選んで新規プロジェクトを作成
  2. サンプルコード「CrashLanding」から「Ship.png」をコピーしてプロジェクトに加える
  3. OpenGL ESの方で作成したクラスFooと、そのインスタンスとUIViewを関連づけるクラス・Barを追加
    @interface Bar : NSObject {
    	Foo* foo;
    	UIView* view;
    }
    @property (retain) Foo* foo;
    @property (retain) UIView* view;
    @end
    
    @implementation Bar
    @synthesize foo, view;
    - (void)dealloc {
    	[view release];
    	[foo release];
    	[super dealloc];
    }
    @end
  4. 新規ファイルテンプレート「Objective-C NSView subclas」でクラス・MyViewを追加し、以下のように書き換える
    @interface MyView : UIView {
        NSTimer *animationTimer;
        CGSize imageSize;
        NSArray* array;
    }
    @end
    
    @implementation MyView
    
    - (id)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {
    		self.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
    		self.layer.sublayerTransform = CATransform3DMakeTranslation(CGRectGetMidX(frame), CGRectGetMidY(frame), 0.0f);
    		
    		UIImage* image = [UIImage imageNamed:@"Ship.png"];
    		imageSize = image.size;
    		
    		const NSUInteger objectCount = 50;
    		NSMutableArray* mutableArray = [[NSMutableArray alloc] initWithCapacity:objectCount];
    		for(int i = 0; i < objectCount; i++) {
    			NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    			
    			Bar* bar = [[[Bar alloc] init] autorelease];
    			bar.foo = [[[Foo alloc] init] autorelease];
    			
    			UIView* view = [[[UIImageView alloc] initWithImage:image] autorelease];
    			view.layer.anchorPoint = CGPointMake(0.5f, 0.5f);
    			view.center = CGPointZero;
    			[self addSubview:view];
    			bar.view = view;
    			
    			[mutableArray addObject:bar];
    			
    			[pool release];
    		}
    		array = mutableArray;
        }
        return self;
    }
    
    - (void)drawRect:(CGRect)rect {
        [animationTimer release];
    	animationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(drawView) userInfo:nil repeats:YES];
    }
    
    - (void)drawView {
    	CGSize size = {
    		(CGRectGetWidth(self.bounds) - imageSize.width) / 2.0f,
    		(CGRectGetHeight(self.bounds) - imageSize.height) / 2.0f
    	};
    	for(Bar* bar in array) {
    		[bar.foo stepIn:size];
    		bar.view.center = bar.foo.coord;
    		bar.view.transform = CGAffineTransformMakeRotation(bar.foo.angle * M_PI / 180);
    	}
    }
    
    - (void)dealloc {
        [array release];
        [image release];
        [animationTimer release];
        [super dealloc];
    }
  5. applicationDidFinishLaunching: でMyViewのインスタンスをウィンドウに追加
    - (void)applicationDidFinishLaunching:(UIApplication *)application {    
    	UIView* view = [[MyView alloc] initWithFrame:[window bounds]];
    	[window addSubview:view];
        [window makeKeyAndVisible];
    }
  6. ビルドが通るように#importやらフレームワークやらを追加する

このコードの実行結果は7fps
結構、遅い。

ところで、実行してみると分かるのだが、この2つのコードは挙動に決定的な違いがある。
それは、OpenGL ESを使ったコードが60fpsに満たない分は「処理落ち」しているのに対し、UIViewの方は「フレームスキップ」している、ということである(まあ処理落ちもしているが)。
実際、1秒間にdrawViewが呼ばれる回数を数えてみたところ、OpenGL ESの方がfpsと同じ24であったのに対し、UIViewの方は40回も呼ばれていた。
setAnimationDidStopSelector: を使って描画の完了を待てば、OpenGL ESと同じ挙動を示すようにもできるのだが、フレームレートはむしろ下がる。

産経新聞

タグ: iPhone

産経朝刊、まるごとiPhoneで 無料アプリ登場 - ITmedia News

新聞元のレイアウトそのまま無料配信!?
その発想はなかったわ……

やってることがシンプルなら、アプリの作りも実にシンプル。
でもって、使いやすい。
大筋の操作はUIScrollViewに任せ、ダブルタップでの倍率変更は、多分ビューを作り直すことで対応しているのだろう。画面が一瞬暗転してしまうのは嬉しくないが、まあ致し方ない。

実際に新聞を表示しているレイヤは、挙動を見る限りではCATiledLayerのそれに近い。
CATiledLayerは、倍率が2倍4倍8倍となる毎に、その倍率にあう画像を描画してくれる(というか、「この倍率でこの部分の絵を描いてくれ」と要求してくる)。で、その間の倍率――1倍?2倍、2倍?4倍――は、低倍率の画像を拡大して表示する。そのため、3.9倍等、次の描画倍率に移る直前はどうしても見た目がぼやけ気味になる。その特徴がこのアプリにもあって、「もうちょっと縮小すれば全文読める」と思って縮小すると、途端に文字がぼやけて読み辛くなったりする。これはちょっと……いや結構……残念。

でも、全体的な作りは非常に良い。
なんというか、このシンプル・イズ・ベスト感が、私の中で大ヒット。
もっとも、それで実際これを読むかというと、それはまた別の話になるわけだが。

edgeAntialiasingMask

タグ: Quartz Mac Objective-C

CALayerはデフォルトではレイヤ境界にアンチエイリアスを掛けるが、MacOS X 10.5で追加された edgeAntialiasingMask を使うとそのアンチエイリアスを解除できる。

@interface MyView : NSView
@end

@implementation MyView
- (void)awakeFromNib
{
    self.wantsLayer = YES;
    CALayer* superlayer = [CALayer layer];
    superlayer.anchorPoint = CGPointZero;
    [self.layer addSublayer:superlayer];
	for(int i = 0; i < 4; i++)
	{
		CGFloat x = 9 + i * 80.5;
		CGFloat y = (i % 2) ? 9.5f : 9.0f;
		for(int j = 0; j < 2; j++)
		{
			for(int k = 0; k < 4; k++)
			{
				CALayer* layer = [CALayer layer];
				layer.bounds = CGRectMake(0, 0, 30, 30);
				layer.anchorPoint = CGPointZero;
				layer.position = CGPointMake(x + (k % 2) * 30, y + j * 60 + (k / 2) * 30);
				if(j == 0)
				{
					layer.edgeAntialiasingMask = edgeAntialiasingMask;
				}
				
				CGColorRef color =
					i < 2
					? CGColorCreateGenericRGB((k == 0 ? 1 : 0), (k == 1 ? 1 : 0), (k == 2 ? 1 : 0), 1)
					: CGColorCreateGenericRGB(0, 0, 0, 1);
				layer.backgroundColor = color;
				CGColorRelease(color);
				
				[superlayer addSublayer:layer];
			}
		}
	}
}
@end
edgeAntialiasingMask.png

一番左には「四辺の座標が整数になっているレイヤ」を並べている。
下4レイヤはレイヤ境界のアンチエリアス無しで、上4レイヤはアンチエイリアス有り。
どちらも、見た目には違いがない。

左から2番目は「四辺の座標が整数になっていないレイヤ」を並べたもので、アンチエリアスの有無に関しては左と同じ。
こちらは上4レイヤの境界がぼやけ、アンチエリアスが掛かっているのが分かる。

右2つは、左2つの集合と同じ条件で「全てのレイヤの色を黒に」したもの。
こうすると、レイヤ境界のアンチエイリアスが重なりあっている部分に隙間が空いているように見える。
右から2番目の、四辺の座標を整数にしている集合には隙間が見えないので、「常に四辺を整数にしておけば隙間は空かない」ように思えるが、superlayerのpositionが浮動小数点数なら隙間は空くし、そうじゃなくてもアニメーション中は境界がちらつく。
edgeAntialiasingMask でアンチエイリアスを解除すると、このような「隙間なく並んでいるCALayerの間に隙間が描かれる現象」を防ぐことができる。

12月09日のココロ日記(BlogPet)

ココロの小さいころの夢は領域になることでした。今はむいさんのマスクになりたい……なんて冗談です

*このエントリは、ブログペットのココロが書いてます♪

CGContextを同じ絵で埋め尽くす

タグ: Quartz Mac Objective-C

CGContextの任意の領域を特定の画像で埋め尽くす方法は二つある。

一つは CGContextDrawTiledImage()

Mac OS X 10.5で追加されたこの関数は、指定した画像でクリッピング領域を埋め尽くしてくれる。

const CGPoint starPoints[] = {
	{ 30, 0 }, { 130, 200 }, { 230, 0 }, { 0, 120 }, { 260, 120 }, { 30, 0 }
};

NSString* resourcePath = [[NSBundle mainBundle] resourcePath];
NSString* tileImagePath = [resourcePath stringByAppendingPathComponent:@"64x48.jpg"];
NSBitmapImageRep* tileImage = [NSBitmapImageRep imageRepWithContentsOfFile:tileImagePath];

CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];

CGContextSaveGState(context);
CGContextAddLines(context, starPoints, sizeof(starPoints) / sizeof(*starPoints));
CGContextClip(context);
CGContextDrawTiledImage(context, CGRectMake(0, 0, 64, 48), tileImage.CGImage);
CGContextRestoreGState(context);
DrawTiled1.png

対象はクリッピング領域なので、当然マスクも使える。
この関数の第2引数はタイルの初期位置(多分……)とサイズで、画像はこの矩形にフィットするよう拡大縮小される。

NSString* maskImagePath = [resourcePath stringByAppendingPathComponent:@"mask.png"];
NSBitmapImageRep* maskImage = [NSBitmapImageRep imageRepWithContentsOfFile:maskImagePath];

CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];

CGContextSaveGState(context);
CGContextClipToMask(context, NSRectToCGRect(rect), maskImage.CGImage);
CGContextRotateCTM(context, -0.1);
CGContextDrawTiledImage(context, CGRectMake(20, 20, 48, 36), tileImage.CGImage);
CGContextRestoreGState(context);
DrawTiled2.png

もう一つの方法は CGContextSetFillPattern()
これはADCにも解説がある
この関数は、 CGContextFillPath() 等によって普段は単色で塗りつぶされる領域を、任意のコールバック関数が描いたタイルで埋め尽くすようにする。
なのでこの関数を使うには、タイルを描くコールバック関数(と、必要があれば描画用情報オブジェクトを解放するコールバック関数)を定義しなければならない。

// 描画コールバック
void DrawPattern(void* info, CGContextRef context)
{
	NSBitmapImageRep* image = info;
	CGContextDrawImage(
		context,
		CGRectMake(0, 0, image.pixelsWide, image.pixelsHigh),
		image.CGImage
	);
}

// 解放コールバック
void ReleaseImage(void* info)
{
	[(NSBitmapImageRep*)info release];
}

とりあえず解放用のコールバックも用意したが、今回の例では(tileImageをretainしなければ)必要なかったりする。

static const CGPatternCallbacks callbacks = { 0, &DrawPattern, &ReleaseImage };

CGPatternRef pattern =
	CGPatternCreate(
		[tileImage retain],
		CGRectMake(0, 0, 64, 48),
		CGAffineTransformIdentity,
		64,
		48,
		kCGPatternTilingConstantSpacing,
		true,
		&callbacks
	);

CGContextSaveGState (context);

CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace(context, patternSpace);
CGColorSpaceRelease (patternSpace);

CGFloat alpha = 1;
CGContextSetFillPattern(context, pattern, &alpha);
CGContextAddLines(context, starPoints, sizeof(starPoints) / sizeof(*starPoints));
CGContextFillPath(context);

CGContextRestoreGState(context);

CGPatternRelease(pattern);
FillPattern1.png

CGPatternCreate() の第2引数がよく分からない。とりあえずはタイルのサイズと同じにしておけば良いようだが……

タイルを変形させたり、各タイル間の間隔を変える(これは CGContextDrawTiledImage() には不可能)には第3?5の引数をいじる。

CGPatternRef pattern =
	CGPatternCreate(
		[tileImage retain],
		CGRectMake(0, 0, 64, 48),
		CGAffineTransformMakeRotation(0.1),
		50,
		48,
		kCGPatternTilingConstantSpacing,
		true,
		&callbacks
	);

CGContextSaveGState (context);

CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace (context, patternSpace);
CGColorSpaceRelease (patternSpace);	

CGFloat alpha = 1;
CGContextSetFillPattern(context, pattern, &alpha);
CGContextClipToMask(context, CGContextGetClipBoundingBox(context), maskImage.CGImage);
CGContextAddRect(context, CGContextGetClipBoundingBox(context));
CGContextRotateCTM(context, 3.0);
CGContextFillPath(context);

CGContextRestoreGState(context);

CGPatternRelease(pattern);
FillPattern2.png

CGContextDrawTiledImageが「すべてのタイルを敷き詰めた後でCTMを適用する」のに対し、CGPatternは「各タイルにCGPatternCreateの第3引数を適用し、CTMは適用しない」。
タイル毎に変形させるため、うまく処理しないとタイル間に隙間が発生してしまう。
その辺がどう処理されるかは、CGPatternCreateの第6引数で決まる。

使用画像:

Appendix

タグ

Blog内検索

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