iPhoneでのピクセル処理をNEON(ベクタ演算)を使って4倍高速化する

目次


はじめに


IMG_0001
ARMのNEONというベクタ演算を使ってコードを書いたところ、C言語で書いたコードの4倍の速度で動作する事ができました。この記事では、C言語でのコードの紹介、アセンブラでのコードの紹介、そしてNEONを利用したコードの紹介を行い、高速化を実現させた手法を書きます。


どのような処理をするのか?


吾輩の小説
弊社からリリースしている自炊系読書アプリ「吾輩の小説 for iPhone」は、画像のピクセルを全てチェックして文章を整形して表示しています。この処理がとても重いので最適化の一貫として研究した情報を公開します。

このプログラムでは 1024×768 ピクセルでRGBAのフォーマットをもつデータから、RGBの各色の合計値が256未満であるピクセルの数を計算します。つまり、画像データから黒っぽいピクセルがいくつあるのかを計算します。また、計算速度の違いが分かりやすくなるように、これを100回繰り返し合計で約8000万ピクセルの計算を行ないます。また、その計算時間を表示します。


コードの入手先

githubにプロジェクトファイルも含めてiPhoneで実行できるファイル一式をアップしましたのでご利用ください。gitコマンドを使用しない場合はこちらからzip化したものを落としてください。


C言語でのコード


以下がコードになります。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR		(0xFF)
#define LOOP_COUNT		(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testC(){
	int pixelCount = width * height;
	int hitCount = 0;
	int startTime = getTime();
	for(int i = 0; i < LOOP_COUNT; i++){
		unsigned char* imageWork = image;
		for(int j = 0; j < pixelCount; j++){
			int color = imageWork[0]+imageWork[1]+imageWork[2];
			if(color < CHECK_COLOR){
				hitCount++;
			}
			imageWork += ELEMENT_OF_PIXEL;
		}
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", pixelCount * LOOP_COUNT, hitCount, endTime-startTime];
	return string;
}

RGBの合計値を算出し、CHECK_COLOR(0xFF) よりも小さいものをカウントしていくだけのシンプルなプログラムです。


アセンブラでのコードとgccの最適化の素晴らしさ


以下がコードになります。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR		(0xFF)
#define LOOP_COUNT		(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testAsm(){
	int pixelCount = width * height;
	int hitCount = 0;
	int checkColor = CHECK_COLOR;
	int startTime = getTime();
	for(int i = 0; i < LOOP_COUNT; i++){
		__asm__ volatile (
				"mov	r0, #0 \n\t"
 
				// ループ開始
				"1: \n\t"
				"add	r0, r0, #1 \n\t"
 
				"ldrb	r3, [%[image]] \n\t"
				"ldrb	r2, [%[image], #1] \n\t"
				"add	r2, r2, r3  \n\t"
 
				"ldrb	r3, [%[image], #2] \n\t"
				"add	r2, r2, r3  \n\t"
				"add	%[image], %[image], #4  \n\t"
 
				// 色判定とカウント
				"cmp	r2, %[checkColor] \n\t"
				"addlt %[hitCount], %[hitCount], #1 \n\t"
 
				// 「ループ開始」へ戻る
				"cmp	r0, %[pixelCount] \n\t"
				"bne	1b \n\t"
 
				: [hitCount] "+r" (hitCount)
				: [pixelCount] "r" (pixelCount), [image] "r" (image), [checkColor] "r" (checkColor)
				: "r0", "r1", "r2", "r3", "cc", "memory"
				);
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", pixelCount * LOOP_COUNT, hitCount, endTime-startTime];
	return string;
}

時間を計測したところ、驚くことに C言語 で書いたものよりも速度が遅くなりました。gccがCからアセンブラにコンパイルする時の最適化がとても優れているのだと思います。NEONなどの特殊な機能を使わないかぎりは、アセンブラ化せず C言語 で書くほうが良いと思います。


NEON : 資料


今回のコードでお見苦しい点があればご容赦願います。アセンブラでの開発経験が全く無く2日前に勉強しながら作成したものです。そこで、使用した資料を列挙しておきます。

  • ARM アセンブリ
    ARMの基本的なプログラミングの手法を知る事ができます。


NEON : 処理の流れと制限

NEONはベクタ演算です。ベクタ演算とは復数の演算を1命令で実行します。今回の方法では、16回の足し算、16回の比較などを1命令で実行します。それにともなって、今までの単純なループや比較ではなくて、それなりに複雑なプログラムになってきます。処理の大まかな流れは以下の順番になります。

  1. 16ピクセル(64バイト)分のデータを詠込む
  2. 16ピクセルのRGBを一気に足す
  3. 16ピクセルの0xFF未満の値を一気にカウンタに追加
  4. 4096ピクセルの処理が終わるまで1-3を繰返す
  5. カウンタの合計を取る
  6. 全てのピクセルの処理が終わるまで1-5を繰返す

ポイントは「16ピクセルを一気に処理」している所と、「4096ピクセルの処理」を一つの境目としている所です。以下で詳しく説明していきます。


NEON : コード


以下がコードになります。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR			(0xFF)
#define LOOP_COUNT			(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testNeon(){
	int pixelCount = 4096;
	int innerLoop = ((width*height)/pixelCount);
	int totalHitCount = 0;
	unsigned int checkColor =
		CHECK_COLOR << 24 |
		CHECK_COLOR << 16 |
		CHECK_COLOR << 8 |
		CHECK_COLOR << 0;
	int startTime = getTime();
	unsigned int addMask = 0x01010101;
	for(int i = 0; i < LOOP_COUNT; i++){
		unsigned char* _image = image;
		for(int j = 0; j < innerLoop; j++){
			int hitCount = 0;
			__asm__ volatile (
					// 初期化
					"mov	r0, #0 \n\t" // 0クリア用
					"mov	r1, #0 \n\t" // 処理済みピクセルのカウンタ
					"vmov.u32 d8, r0, r0 \n\t"
					"vmov.u32 d9, r0, r0 \n\t"
					"vmov.u32 d10, %[checkColor], %[checkColor] \n\t"
					"vmov.u32 d11, %[checkColor], %[checkColor] \n\t"
					"vmov.u32 d12, %[addMask], %[addMask] \n\t"
					"vmov.u32 d13, %[addMask], %[addMask] \n\t"
 
					// ループ開始
					"1: \n\t"
					"add	r1, r1, #16 \n\t"
 
					// データの読込と色の加算
					"add		r2, %[image], #32 \n\t"
					"vld4.8	{d0, d2, d4, d6}, [%[image]] \n\t"
					"vld4.8	{d1, d3, d5, d7}, [r2] \n\t"
					"vqadd.u8	q0, q1 \n\t"
					"vqadd.u8	q0, q2 \n\t"
 
					// 色の判定とカウント
					"vclt.u8 q1, q0, q5 \n\t"
					"vand q1, q6 \n\t"
					"vadd.u8 q4, q4, q1 \n\t"
 
					// データのアドレスを進める
					"add	%[image], #64 \n\t"
 
					// 「ループ開始」へ
					"cmp	r1, %[pixelCount] \n\t"
					"bcc	1b \n\t"
 
					// 色数の合計
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d8[0] \n\t"
					"2: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		2b \n\t"
 
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d8[1] \n\t"
					"3: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		3b \n\t"
 
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d9[0] \n\t"
					"4: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		4b \n\t"
 
					"mov	r0, #0 \n\t"
					"vmov.u32	r1, d9[1] \n\t"
					"5: \n\t"
					"and		r2, r1, #0xFF \n\t"
					"add		%[hitCount], r2 \n\t"
					"lsr		r1, #8 \n\t"
					"add		r0, #1 \n\t"
					"cmp		r0, #4 \n\t"
					"bne		5b \n\t"
 
					: [hitCount] "+r" (hitCount)
					: [pixelCount] "r" (pixelCount), [image] "r" (_image), [checkColor] "r" (checkColor), [addMask] "r" (addMask)
					: "r0", "r1", "r2", "q0", "q1", "q2", "q3", "q4", "cc", "memory"
					);
			totalHitCount += hitCount;
		}
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", width * height * LOOP_COUNT, totalHitCount, endTime-startTime];
	return string;
}


NEON : コード解説-処理限界数でのループ


上記しましたが、この処理でのポイントに「4096ピクセルの処理」というものがあります。これはピクセル数のカウントを1バイトで行い、それを16個保持しているからです。「256(1バイト)x16=4096」となります。なぜ1バイトで計算するのか?と思われるとおもいますが、これがベクタ演算を使った高速化のカギになります。以下で説明します。


NEON : コード解説-レジスタの役割


この最適化コードでは7本のNEON128ビットレジスタを使用しています。それぞれの主な役割は以下のようになります。

  • q0(d0,d1): R値を保持
  • q1(d2,d3): G値を保持
  • q2(d4,d5): B値を保持
  • q3(d6,d7): A値を保持。読込むだけで計算には使わない。
  • q4(d8,d9): 色判定の結果をカウント
  • q5(d10,d11): 色判定で使用する値(0xFFが詰まっている)
  • q6(d12,d13): 結果をカウントするときに使用するビットマスク(0×01が詰まっている)


NEON : コード解説-データの読込


まず、計算の前にメモリからNEONレジスタにデータを読み込みます。

// データの読込と色の加算
"add	r2, %[image], #32 \n\t"
"vld4.8	{d0, d2, d4, d6}, [%[image]] \n\t"
"vld4.8	{d1, d3, d5, d7}, [r2] \n\t"

%[image]レジスタがデータの先頭のアドレスを指し、r2レジスタがそこから32バイトずらした8個目以降のピクセルのアドレスを指しています。そして vld4.8 命令を呼び以下の画像のような配置でデータが読み込まれます。

neon_1_registers


NEON : コード解説-加算


そして、RGBの値を加算します。

"vqadd.u8	q0, q1 \n\t"
"vqadd.u8	q0, q2 \n\t"

Rの値が入っている q0 レジスタに、G値の q1 と、B値の q2 を加算します。以下の図ような処理になります。

neon_2_adding_colors

この時に vadd.u8 ではなく vqadd.u8 を使います。前者は加算時にビットが溢れても気にせずに計算がされます。後者はビットが溢れた場合に最大値が設定されます。計算式でたとえると以下のような違いになります。

vadd.u8 : 0xFF + 0×01 = 0×0
vqadd.u8 : 0xFF + 0×01 = 0xFF

これで次に行う比較演算が問題なくおこなえます。

しかし、正確にいうと場合によっては問題が大ありです。上記のとおり、この最適化コードでは256以上の値での色判定は行えません。どんな値でも判定できるコードも書いてみたのですが、これがあまり高速化できなかったため今回はバッサリと切り捨てました。


NEON : コード解説-ピクセルの色チェックとカウント


ピクセルの色チェックとカウントは多少やっかいなので、図をまじえつつ説明していきます。

// 色の判定とカウント
"vclt.u8 q1, q0, q5 \n\t"
"vand q1, q6 \n\t"
"vadd.u8 q4, q4, q1 \n\t"

vclt.u8 は比較命令です。 RGBの合計値が入っている q0 と、比較用の値を詰め込んである q5 を比較し、 q0 の値の方が小さかった場合に対応するビットに 1 が設定された値を q1 に入れます。意味が分かりにくいと思うので図では以下のようになります。例として q0 にはRGB値が加算されたとして適当な値を入れています。

neon_3_vclt

次は、比較判定にヒットしたピクセルをカウントします。vclt.u8 では比較にヒットしたピクセルに対応する箇所のビットが全て立つ、つまり 0xFF が入っています。これを 1 になるように 0×1 で AND を取ります。

neon_4_vand

最後に vadd.u8 で q4 に q1 を足してあげれば、16ピクセル分のカウントが完了します。

neon_5_vadd

上記の図ではカウント用の q4 レジスタが空でしたが、ループを重ねるごとに値が加算されていくことになります。


NEON : コード解説-カウントの合計


上記したように、このコードでは4096ピクセルまで計算すると、q4 でのカウンタが最大値である 0xFF になるものが出てきます。ですので、ここで一度別のレジスタに待避させます。

// 色数の合計
"mov		r0, #0 \n\t"
"vmov.u32	r1, d8[0] \n\t"
"2: \n\t"
"and		r2, r1, #0xFF \n\t"
"add		%[hitCount], r2 \n\t"
"lsr		r1, #8 \n\t"
"add		r0, #1 \n\t"
"cmp	r0, #4 \n\t"
"bne		2b \n\t"
 
以下の同様の処理は省略

まず vmov.u32 で d8[0](d8の上位4バイト) を r1 レジスタにコピーします。and で 下位1バイトに格納されているカウンタを取り出し %[hitCount] に足します。lsr r1, #8 で1バイト分右にシフトし次のカウンタの値を取得する準備をします。and と lsr の処理は以下の図のようになります。

neon_6_shift_count

これらの処理をループで4回まわして、合計4個のカウント値を合計します。

その後 d8[1] d9[0] d9[1] にも同様の処理を行ない、16個を合計します。

これまでの処理を画像データの最後まで繰り返すと、RGB合計値が256未満の色をもつピクセル(黒っぽいピクセル)の個数が、C言語で書いたプログラムの約4倍の速度で取得できます。


終りに

今回最適化を行うにあたって上記のコード量の10倍程度を書き検証しました。そこで分かった注意点が、NEONで最適化するなら、メモリロードなどにARMレジスタや命令を使わずに、直接NEONレジスタと命令を使わないと意味が無いという事です。

決して汎用的な最適化コードではないですが、iPhoneで大量の処理を行う必要がある場合に、NEONでの高速化は有効だと思います。その時の参考になれば幸いです。

最後に、NEONを使いながらC言語の処理速度とほぼ同じだったコードを書いておきます。

#define IMAGE_SIZE_W		(1024)
#define IMAGE_SIZE_H		(768)
#define CHECK_COLOR		(0xFF)
#define LOOP_COUNT		(100)
#define ELEMENT_OF_PIXEL	(4)
 
NSString* Test::testNeon(){
	int pixelCount = width * height;
	int totalHitCount = 0;
	int checkColor = CHECK_COLOR;
	int startTime = getTime();
	unsigned int a = 0;
	for(int i = 0; i < LOOP_COUNT; i++){
		int hitCount = 0;
		__asm__ volatile (
				// 初期化
				"mov	r0, #0 \n\t"
				"vmov.u32 d8, r0, r0 \n\t"
				"vmov.u32 d9, r0, r0 \n\t"
				"vmov.u32 d6, %[checkColor], %[checkColor] \n\t"
				"vmov.u32 d7, %[checkColor], %[checkColor] \n\t"
 
				// ループ開始
				"1: \n\t"
				"add	r0, r0, #4 \n\t"
 
				"ldrb r1, [%[image]] \n\t"  
				"ldrb r2, [%[image], #1] \n\t"  
				"ldrb r3, [%[image], #2] \n\t"  
 
				"ldrb r4, [%[image], #4] \n\t"  
				"ldrb r5, [%[image], #5] \n\t"  
				"ldrb r6, [%[image], #6] \n\t"  
 
				"vmov d0, r1, r4 \n\t"
				"vmov d2, r2, r5 \n\t"
				"vmov d4, r3, r6 \n\t"
 
				"ldrb r1, [%[image], #8] \n\t"  
				"ldrb r2, [%[image], #9] \n\t"  
				"ldrb r3, [%[image], #10] \n\t"  
 
				"ldrb r4, [%[image], #12] \n\t"  
				"ldrb r5, [%[image], #13] \n\t"  
				"ldrb r6, [%[image], #14] \n\t"  
 
				"vmov d1, r1, r4 \n\t"
				"vmov d3, r2, r5 \n\t"
				"vmov d5, r3, r6 \n\t"
 
				"add %[image], %[image], #16 \n\t"  
 
				"vadd.s32	q0, q0, q1 \n\t"
				"vadd.s32	q0, q0, q2 \n\t"
 
				// カウント
				"vclt.s32 q1, q0, q3 \n\t"
				"vsub.s32 q4, q4, q1 \n\t"
 
				// 「ループ開始」へ
				"cmp	r0, %[pixelCount] \n\t"
				"bcc	1b \n\t"
 
				// 色判定とカウント
				"vmov.s32 %[hitCount], d8[0] \n\t"
				"vmov.s32 r0, d8[1] \n\t"
				"vmov.s32 r1, d9[0] \n\t"
				"vmov.s32 r2, d9[1] \n\t"
				"add %[hitCount], r0 \n\t"
				"add %[hitCount], r1 \n\t"
				"add %[hitCount], r2 \n\t"
 
				: [a] "+r" (a), [hitCount] "+r" (hitCount)
				: [pixelCount] "r" (pixelCount), [image] "r" (image), [checkColor] "r" (checkColor)
				: "r0", "r1", "r2", "r3", "r4", "r5", "r6", "q0", "q1", "q2", "q3", "q4", "cc", "memory"
				);
		totalHitCount += hitCount;
	}
	int endTime = getTime();
	NSString* string = [NSString stringWithFormat:@"Pixcel: %d\nHit Pixel: %d\nTime: %d msec\n", pixelCount * LOOP_COUNT, totalHitCount, endTime-startTime];
	return string;
}
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 開発 タグ:

分厚いハードカバーの本をカッターで綺麗に裁断する方法

2010 年 5 月 25 日 akiraak コメントはありません

1q84

「吾輩の小説 for iPhone」で村上春樹の1Q84-BOOK1を読んだら面白くてBOOK2を買ったのですが、この本は分厚くてカッターで綺麗に裁断するのが難しいんですよね。そこで綺麗に裁断する方法を教えます。

さて、1Q84-BOOK2はすでに裁断してしまったので、福田 和代のオーディンの鴉で説明します。

用意する道具は、カッター、定規、下敷きです。

厚いハードカバーをカッターで裁断

まず、ハードカバーの部分はページとの間にカッターの刃を入れて切り分離させます。

厚いハードカバーをカッターで裁断 厚いハードカバーをカッターで裁断

次に本体の部分を100ページ程度ごとに切り分けます。

厚いハードカバーをカッターで裁断 厚いハードカバーをカッターで裁断

そして、切り分けたものの糊付け部分を切っていきます。

厚いハードカバーをカッターで裁断

すると、こんなに綺麗にできあがります。

厚いハードカバーをカッターで裁断

お試しあれ。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ:

吾輩の小説: 最新の小説を快適に読めちゃう!小説が大好きな方へ。

2010 年 3 月 15 日 akiraak コメントはありません

今年は iPad, Kindle が発売され電子書籍の年になりそうですね。そこで、自炊系小説ビュワー「吾輩の小説」の紹介です。

「吾輩の小説」は青空文庫系アプリとは違い、自分で購入した最新の小説を読む事ができます。村山春樹だって涼宮ハルヒだって読めちゃいます。あなたの家の本棚に眠る懐かしい小説を読み返す事だってできます。

01

まずは、書籍の追加方法の説明です。書籍画面から追加ボタンを押して「書籍の追加」画面を表示させます。書籍はzipファイルにまとめてWEBにアップしておきそのURLを指定します。デフォルトで「人間失格」のURLが入っているので試しにこちらを取得してみてください。

02

小説を読むときは「書籍」画面から追加した小説名をタップします。「整形表示モード」を使えば、文字をこんなに大きく表示して快適に読めます(画像のページは開発者が「涼宮ハルヒの消失」の中で一番好きなシーンです)

03

そして、高速化されたアプリの起動時間です。信号待ちやエレベーターの待ち短い時間でも小説が読めるように、前回読んでいた部分を高速に表示します。開発者自身、毎日「吾輩の小説」を持ち出して、信号待ち時間で読んでテストしてるのでばっちりです。

読書中に気になった所はメモで残せます。ミステリー物など登場人物が多い小説はとっても便利です(画像は「かまいたちの夜」で有名な我孫子武丸による「殺戮にいたる病」この小説で登場人物をメモったところ死亡者リストになってしまいました)

04

そして、その日の読書が終わったらコメント付きの読書記録を twitter に残せます。

05

開発者自身が小学生の頃から「はてしない物語」「小説ドラゴンクエスト」「ロードス島戦記」など千冊程度の小説を読んできた小説大好きっ子です。小説が大好きな iPhone ユーザーは是非使ってみてください。

※「涼宮ハルヒの消失」は角川書店の出版物です
※「かまいたちの夜」はチュンソフトの発売物です
※「殺戮にいたる病」は講談社の出版物です
※「はてしない物語」は岩波書店の出版物です
※「小説ドラゴンクエスト」はエニックスの出版物です
※「ロードス島戦記」は角川書店の出版物です

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ:

CALayerの好きなプロパティをアニメーションさせる

2010 年 3 月 14 日 ento コメントはありません

iPhone 上の描画システムとして、弊社ではこれまで UIKit、OpenGL を使ってきました。 UIKit を使ったのは最初のアプリだけで、パフォーマンス上の理由からそれ以降のアプリはすべて OpenGL で描画しています。

しかし先週から開始した次のアプリの開発では、Quartz と Core Animation を使ってみています。簡単な時計アプリになる予定で、これらのフレームワークでどこまでできるかの試作段階といったところです。その中で 1 つ、詰まった点があったので紹介します。

それはというと、時計の針を描画するところまではよかったのですが、その角度を変えながらアニメーションさせるところで行き詰まったのです。CALayer の「アニメーション可能なプロパティ」 animatable properties (不透明度やサイズなどが含まれます) ならば、値を変えるだけで自動的にアニメーション implicit animation が実行されます。時計の針の角度のような、自分で定義したプロパティでもそれをするにはどうすれば?

Google 検索に 3 時間お付き合いいただいた結果、iPhone OS 3.0 以降なら、CALayer の needsDisplayForKey: クラスメソッドで「変更されたら再描画が必要になるプロパティ」を指定できること、actionForKey: メソッドで「変更されたら実行するアクション (アニメーション)」を指定できることが分かりました。ここまでは簡単に調べられたのですが、時計の針をアニメーションさせるにはもう 1 つ注意すべきことがありました。こちらは後で紹介します。

それより何よりソースコードを見てみましょう。

はじめに、時計の針用のレイヤーを作るコードです:

@implementation ClockView
 
- (void)awakeFromNib {
    [self setupLayers];
    [self start];
}
 
- (void)start {
    // tick メソッドを呼び出すタイマーを作成
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 
        target:self
        selector:@selector(tick)
        userInfo:nil
        repeats:YES];
    self.animationTimer = timer;
}
 
- (void)setupLayers {
    /*
     各針用のレイヤーを追加。
     angle を変化させてアニメーションする。
     */
    {
        HandLayer *hand = [[HandLayer alloc] init];
        hand.frame = self.frame;
        self.secondHand = hand;
        [hand release];
        [self.layer addSublayer:self.secondHand];
    }
    // ...
}
 
- (void) tick {
    // 各針に新しい角度を通知
}

ここまでは大したことはありませんね。次がこの記事の心臓、アニメーション部分です:

@interface HandLayer : CALayer {
}
 
@property CGFloat angle;
 
@end
 
 
@implementation HandLayer
 
@dynamic angle;
 
+ (BOOL)needsDisplayForKey:(NSString*)aKey {
    // angle が変わったら再描画がいるよ
    if ([aKey isEqualToString:@"angle"]) {
        return YES;
    } else {
        return [super needsDisplayForKey:aKey];
    }
}
 
- (id)actionForKey:(NSString *) aKey {
    if ([aKey isEqualToString:@"angle"]) {
        // angle 用の補間アニメーションを作る
        CABasicAnimation *theAnimation = [CABasicAnimation
            animationWithKeyPath:aKey];
        // 注意: fromValue を設定しないと正しくアニメーションしない
        theAnimation.fromValue = [[self presentationLayer] valueForKey:aKey];
        return theAnimation;
    } else {
        return [super actionForKey:aKey];
    }
}
 
- (void)drawInContext:(CGContextRef)context {
    // angle プロパティを参照しながら針を描画
}

theAnimation.fromValue を現在表示中の針の角度に設定していることに注意してください。これがドキュメントを探しても見つけられなかったポイントです。解決できたのは、Omni Group のブログ記事 “Animating CALayer content”のおかげでした。その当時は自分で needsDisplayForKey: 周辺の仕組みを実装しないといけなかったんですね。

最後に、動画をどうぞ:

※ 動画中の針のアニメーションは、アニメーションのローカル時間を決める関数 timingFunction にデフォルトの線形タイプではなく kCAMediaTimingFunctionEaseIn を使っています。

参考:

追記:
実際に実行されている補間アニメーションの開始値 fromValue → 終了値 toValue / 増加値 byValue をログに出してみました。

2010-03-13 11:12:41.179 app[14615:207] angle \
<CABasicAnimation: 0x3b02620>, 6.283185 -> (null) / (null)

fromValue が設定されていて、その他は設定されていないのが分かります。終了値が設定されていないのにどうやって補間しているのかは謎です…。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 開発 タグ:

「笑わない数学者 – 森博嗣 」に出てくる数学パズルをプログラム(Clojure)で解く [吾輩の小説 for iPhone]

2010 年 3 月 13 日 ento コメントはありません

自炊系小説ビューワ「吾輩の小説」で今日も電車で本を読んでいたら、話の中に数学パズルが出てきました。それもそのはず、読んでいたのは森博嗣 による「笑わない数学者」です。

五つのビリヤードの玉を、真珠のネックレスのように、リングにつなげてみるとしよう。玉には、それぞれナンバが書かれている。さて、この五つの玉のうち、幾つ取っても良いが、隣どうしが連続したものしか取れないとしよう。一つでも、二つでも、五つ全部でも良い。しかし、離れているものは取れない。この条件で取った玉のナンバを足し合わせて、1から21までのすべての数ができるようにしたい。さあ、どのナンバの玉を、どのように並べて、ネックレスを作れば良いかな?

うーん、まず1を作るには「1」の玉が必要。次に2を作るには、0+2、1+1の組み合わせがあって、1の玉を2つは入れられないから、「2」の玉も必要だ。と、ここまで考えたところで気付きました。プログラマにはコンピュータという計算の得意な友達がいる!

ということで計算機にパズルを解いてもらいました。言語は、最近はまっているClojureという関数型言語です。

; 名前空間の設定と使うライブラリのインポート
(ns ball-chain
  (:use [clojure.contrib.combinatorics :only (combinations permutations)])
  (:use clojure.contrib.test-is))
 
; 玉を定義
(defn balls [] (range 3 15))
 
; ネックレスの start 番目から length 個だけ取り出して数を合計する関数
(defn take-sum [chain start length]
  (reduce + (for [i (range start (+ start length))]
	       (nth chain
		    (mod i (count chain)) 1))))
 
; ネックレスから玉を取り出して
; 合計が number になるようにできるか? を返す関数
(defn yields? 
  ([chain number]
    (not (empty? (take 1 (filter #(yields? chain number %) (range 0 5))))))
  ([chain number start]
    (not (empty? (take 1 (filter #(= number (take-sum chain start %)) (range 1 5)))))))
 
; ネックレスが条件に合うか? を返す関数
(defn correct? [chain]
     (every? identity (map #(yields? chain %) (range 1 20))))
 
; 玉を組み合わせてネックレスを作る関数
(defn all-chains [] (map permutations (map #(concat % (list 1 2)) (combinations (balls) 3))))
 
; ネックレスの組み合わせの中から、条件に合うものを抽出する関数
(defn answers []
  (for [perms (all-chains) :when (not-empty (filter correct? perms))]
    (filter correct? perms)))
 
; yields? のテスト
(deftest test-yields
  (is (yields? (list 1 2 3 4 11) 1))
  (is (yields? (list 1 2 3 4 11) 2))
  (is (yields? (list 1 2 3 4 11) 3))
  (is (yields? (list 1 2 3 4 11) 4))
  (is (yields? (list 1 2 3 4 11) 11))
  )
 
; テストを実行
(run-tests)
 
; 答えを1つ出力
(println (take 1 (answers)))

「1と2の玉が必ずある」という条件を組み込んで、多少の高速化をはかっています。ネックレス状なので右回りも左回りも同等の組み合わせになる、といった条件は考慮できていません。

これを Mac OS X で動かすには、たとえば ball-chain.clj というファイルに保存した上で、以下のように実行します。

$ sudo port install clojure clojure-contrib
$ java -classpath .:/opt/local/share/java/clojure/lib/clojure.jar:/opt/local/share/java/clojure/lib/clojure-contrib.jar clojure.main ball-chain.clj --

※ 本来は

clj ball-chain.clj

だけで実行できるはずなんですが、MacPorts のバグのため、それには少し設定が必要です。詳しくはリンク先を参照してください。

答え? 答えはネタバレなのでここには書きません。どうしても見たい方は↓へどうぞ

。。。
スクリプトの出力結果はこちら:

$ clj ball-chain.clj
 
Testing ball-chain
 
Ran 1 tests containing 5 assertions.
0 failures, 0 errors.
(((3 10 2 5 1) (3 1 5 2 10) (5 1 3 10 2) (5 2 10 3 1) (10 3 1 5 2) (10 2 5 1 3) (1 3 10 2 5) (1 5 2 10 3) (2 5 1 3 10) (2 10 3 1 5)))

1 つ目の答え (3 10 2 5 1) が本当に合っているか、見てみましょう。

1 3 10 2 5 1
2 3 10 2 5 1
3 3 10 2 5 1
4 3 10 2 5 1
5 3 10 2 5 1
6 3 10 2 5 1
7 3 10 2 5 1
8 3 10 2 5 1
9 3 10 2 5 1
10 3 10 2 5 1
11 3 10 2 5 1
12 3 10 2 5 1
13 3 10 2 5 1
14 3 10 2 5 1
15 3 10 2 5 1
16 3 10 2 5 1
17 3 10 2 5 1
18 3 10 2 5 1
19 3 10 2 5 1
20 3 10 2 5 1
21 3 10 2 5 1

その他の答えも、右回り・左回りを逆にしたり、さらに開始点をずらしたりすることで 1 つ目の答えと同じネックレスになっていることがわかります。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: その他, 吾輩の小説 タグ:

「吾輩の小説 for iPhone」レビューへの返信

期待 by mskk – Version 2.0 – 2010/03/08

整形表示モード追加に大感謝です。
まさに求めていた機能です。有難うございました。

欲をいえばファイル名の変更が出来るようになると嬉しいです。
あと中途半端な部分でページ送りされるのでその辺を改善して欲しいです。
今後のアップデートも期待しています。

ver 1.0 をリリースしたのち、iPhoneなどの携帯電話の小さい画面で、さらに文字を見やすくする方法を半月ほど調査&研究していたのですが、やはり整形表示モードのような仕組みを入れるしかないと結論に達して実装しました。今後は整形の精度を上げていきます。

さて、2点ある修正点ですが、まず1つめの「ファイル名の変更」は、開発初期から必要性があると考えていましたので実装します。

2つめの「中途半端な部分でページ送り」は、私がページ送り後にページ送り前の1行を見ながら読まないと内容を把握できないという状態ですので実装は見送らせて頂きます。

ただし、人によって読み方はさまざまですので、まだ決定はしていませんが「吾輩の小説 Pro」という細かい設定が行えるバージョンを出す事を検討しています。

レビューありがとうございました。参考にしながら使いやすいアプリを作っていきます。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ:

猫を抱いて象と泳ぐ – 小川洋子 [吾輩の小説 for iPhone]

整形モードで見やすくなった「吾輩の小説 ver 2.0」「猫を抱いて象と泳ぐ」を読む。

主人公の祖母が孫の成長をチェスで知るこのシーンで涙した。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ:

電子出版へのアプローチの手法

iPad/Kindleが発売されて電子書籍市場は今後活性化していくと思います。そして、電子書籍市場への参加の仕方に2つの手法があると思っています。

1つはこちら。今までの印刷業者や書店に置き換わるところを担おうとする方法です。

電子出版はすでに始まっている -- 池田信夫 blog part2

3月1日付で「株式会社アゴラブックス」を設立し、私が代表取締役に就任した。役員兼社員5人の超零細企業だが、4月から電子書籍の刊行を始める予定だ――といっても、設備は何もない。インフラはGoogle Appsで1人年間6000円。システム管理もすべてアウトソースするので、固定費はゼロ。失敗した場合のリスクもほとんどない。

そして、もう1つの可能性を、富士通のScanSnapというスキャナが100万台売れたというところから感じました。

感謝を込めて「ScanSnapシリーズ 100万台突破記念キャンペーン」を開始!

一部の電子書籍ユーザーには自炊とも呼ばれている方法で、自分で書籍をスキャンしてPCやiPhoneなどで読む方法です。前者の電子書籍販売ルートが完璧に構築されれば不必要になると思うので、それまでのつなぎの手法になると思います。

弊社では後者の手法をとっていきます。一時的な需要しかありませんが、前者には無い優れた点として、電子書籍化されていない最新の出版物を楽しめるという非常に大きい利点があるからです。

まずは「吾輩の小説 for iPhone」で、小説をiPhoneで読めるアプリを作りました。

読書記録をつぶやける自炊系の小説ビュワー「我輩の小説」

nvv0_icon_reflection_96x100nvv1_icon_reflection_96x100

※整形モードを追加したバージョンは現在申請中です。今週中にリリースされると思います。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ:

殺戮にいたる病 – 我孫子武丸 [我輩の小説 for iPhone]

2010 年 2 月 14 日 akiraak コメントはありません

「オチがめちゃめちゃびっくりする日本の小説教えて」 を見て、中学生の頃にかなりハマった「かまいたちの夜」の作者「我孫子武丸」による「殺戮にいたる病」があったので、購入&裁断&スキャンして「我輩の小説」で読みました。

サスペンスものは話が複雑で登場人物も多いですよね。話を理解するために名前などをメモに追加していったら、なにやら死亡者リストが出来てしまいました。おそろしい。。。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ:

涼宮ハルヒの消失 – 谷川流 [我輩の小説 for iPhone]

2010 年 2 月 14 日 akiraak コメントはありません

「映画 : 涼宮ハルヒの消失」を観て、手持ちの文庫を読み直したくなったので、裁断&スキャンし「我輩の小説」で読んでみました。

アニメでもそうでしたが、ハルヒの原作再現度はかなり高いですよね。とくにセリフなんて全く一緒じゃないですかね。それは、ハルヒは過去と未来を行き来きし、パズルを合わせていくような話なので、原作を忠実に再現しないと話に矛盾がでてきてしまうからだと思いました。

読了です。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Hatena Bookmark
  • Twitter
カテゴリー: 吾輩の小説 タグ: