一般公開「小池邦人のCarbon視点でiPhone探求」
(MOSADeN Onlineでは、掲載日から180日を経過した記事を一般公開しています。)- 顔認識とCore Imageフィルタのコラボ(2013/10/29:掲載)
- CGColorRefではパタン描画も可能です(2013/08/23:掲載)
- Core Imageのトランジション用フィルタ(2013/06/26:掲載)
- ビューコントローラの入れ子状態(2013/04/22:掲載)
- iOS 6でEAGLViewをスクリーンキャプチャする(2013/02/17:掲載)
- Core Imageでビデオ映像にフィルタ処理(2012/12/17:掲載)
- ビデオ映像をOpenGLテクスチャとして使う(2012/10/27:掲載)
- キーフレームアニメーションの実行(2012/09/15:掲載)
- 明示的アニメーションで変化を拾う(2012/08/27:掲載)
- お手軽なUIViewアニメーションを使う(2012/05/23:掲載)
- 暗黙的アニメーションは忘れましょう!(2012/04/26:掲載)
- 暗黙的と明白的アニメーション(2012/04/13:掲載)
- UIViewに割り当てられたCALayer(2012/03/27:掲載)
- CALayerのサブクラスを調べる(2012/03/21:掲載)
- CIImage発、CGImageRef経由、UIImage行き(2012/02/27:掲載)
- こんな機能もあったのか!(2012/02/16:掲載)
- もっとビルトイン・フィルタを!(2012/01/30:掲載)
- くっきり、すっきり、名称変更!(2012/01/12:掲載)
- 果敢にローカライズをしましょう!(2011/12/22:掲載)
- すべてのイエローカードを解消する(2011/11/29:掲載)
- イエローカードを解消する前に(2011/11/17:掲載)
- まずは新規プロジェクト作成から(2011/10/26:掲載)
- Xcode 4の時代がやって来た(2011/09/15:掲載)
- Image Unitを開発するには(2011/09/03:掲載)
- Filter Browserをオープンする(2011/08/13:掲載)
- トランジション用フィルタを使う(2011/07/20:掲載)
- フィルタ処理で消費されるメモリ(2011/07/04:掲載)
- Core ImageがiOSにやってきた!(2011/06/16:掲載)
- Core Imageとは何ぞや?(2011/06/02:掲載)
- 残り物には福があるのか?(2011/05/21:掲載)
-
顔認識とCore Imageフィルタのコラボ
この記事は、2013年10月29日に掲載されました。以前に単独機能として紹介しましたが、「顔認識」は、OS X 10.7(Lion)とiOS 5からCore Imageに新しく追加された機能です。写真(画像)から人間の顔を抽出し、その顔の輪郭、目の位置、口の位置などを座標値として得る事ができます。この機能を使うのは簡単で、適切なオプション(NSDictonary)を設定してから、次のクラスメソッドでCIDetectorクラスのインスタンスを作成しておきます。
(CIDetector *)detectorOfType:(NSString*)type context:(CIContext *)context options:(NSDictionary *)options
顔認識は、CIDetectorAccuracyキーで「認識率を優先するのか?」「認識処理速度を優先するのか?」を選択できます。次の例は、CIDetectorAccuracyHigh値を代入して認識率を上げることを優先しています。現バージョンがサポートしている顔認識のタイプはCIDetectorTypeFace(人間の顔)だけです。残念ながら犬や猫はダメですね(笑)。
NSDictionary *options; // オプション値はCIDetectorAccuracyHighでキーはCIDetectorAccuracy options=[[NSDictionary alloc] initWithObjectsAndKeys: CIDetectorAccuracyHigh,CIDetectorAccuracy,nil]; faceDetector=[[CIDetector detectorOfType:CIDetectorTypeFace context:nil options:options]retain]; // CIDetectorインスタンス作成
顔認識処理の実行は次のメソッドで行います。結果はCIFeatureオブジェクトの配列で返されます。写真に複数の人が写っていても、その人数分だけ認識されます(人数に限界があるかどうかは未確認)。ここで渡すオプションは画像の回転情報です。
- (NSArray *)featuresInImage:(CIImage *)image options:(NSDictionary *)options
CIFeatureクラスには、顔の枠を示すboundsプロパティ(CGRect)や、両目の位置を示すleftEyePositionとrightEyePositionプロパティ(CGPoint)、そして口の位置を示すmouthPositionプロパティ(CGPoint)が保存されています。顔認識に成功した場合には、これらのプロパティを参照することで各エリアに対して特定の処理を実行できます。今回のサンプルでは、写真で認識した顔をモザイク処理します。このCore Imageによるフィルタ処理では、認識されたすべての顔をモザイク処理の対象とします。
まずは顔認識の準備と必要とされる画像やフィルタの準備です。準備した画像やフィルタなどを保存しておくインスタンス変数は次の通りです。
IBOutlet UIImageView *imageView; // 画像表示用 CIContext *ciContext; // 描画用コンテキスト CIFilter *blendFilter; // 合成用フィルタ CIImage *sourceImage; // ソース画像 CIImage *maskImage; // マスク画像 NSMutableArray *centerArray; // 顔認識の位置の配列 CGFloat inputValue; // ボザイクのスケール
setupFilterメソッドでは、対象となるソース画像(sourceImage)を呼び込み、自作のdoFaceDetectメソッドで顔認識を実行します。もし認識された顔があれば(複数でもOK)centerArrayに顔の中心座標が保存されて戻ります。そしてCIRadialGradientフィルタにより人数分だけ顔の位置に円のグラデーションを発生させてから、CISourceOverCompositingフィルタで順次合成していきます。そして、最終的にソース画像全体に対応するマスク画像(maskImage)を作成します。
- (void)setupFilter // フィルタの準備を行う { CIFilter *gradientFilter; NSURL *url; if( url=[[NSBundle mainBundle] URLForResource:@"Image" withExtension:@"jpg"] ) { if( sourceImage=[CIImage imageWithContentsOfURL:url] ) // 画像を読み込む { if( [self doFaceDetect]==YES ) // 顔認識を実行する { inputValue=10.0; // 初期モザイクスケール ciContext=[CIContext contextWithOptions:nil]; // マスクとの合成用フィルタで後からdoFilterで使う blendFilter=[CIFilter filterWithName:@"CIBlendWithMask" keysAndValues:@"inputBackgroundImage",sourceImage,nil]; // 顔の回りに円のグラデーションを発生させるため gradientFilter=[CIFilter filterWithName:@"CIRadialGradient" keysAndValues: @"inputRadius0",[NSNumber numberWithFloat:40.0], @"inputRadius1",[NSNumber numberWithFloat:80.0], @"inputColor0",[CIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0], @"inputColor1",[CIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.0],nil]; // 認識された顔の回りに円のグラデーションを発生さ合成していく for( CIVector *center in centerArray ) { [gradientFilter setValue:center forKey: @"inputCenter"]; if( ! maskImage ) maskImage=gradientFilter.outputImage; else { maskImage=[CIFilter filterWithName:@"CISourceOverCompositing" keysAndValues: @"inputImage",gradientFilter.outputImage, @"inputBackgroundImage",maskImage,nil].outputImage; } } } } } }
こちらが顔認識を行っているdoFaceDetectメソッドです。 読み込み済みのソース画像(sourceImage)を認識の対象にし、認識された人数分の顔の中心座標を配列(centerArray)に保存します。
- (BOOL)doFaceDetect // 顔認識を実行する { CIDetector *detector; NSArray *faceArray; CGFloat cx,cy; if( detector=[CIDetector detectorOfType:CIDetectorTypeFace context:nil options:nil] ) { faceArray=[detector featuresInImage:sourceImage options:nil]; // 顔認識を実行 if( [faceArray count] ) // 顔認識された { centerArray=[NSMutableArray array]; for( CIFeature *face in faceArray ) // 認識した分だけ中心座標を配列へ保存する { cx=face.bounds.origin.x+face.bounds.size.width/2.0; cy=face.bounds.origin.y+face.bounds.size.height/2.0; [centerArray addObject:[CIVector vectorWithX:cx Y:cy]]; } return YES; } } return NO; }
最後が完成した画像を表示させるdoFilterメソッドです。inputValueにモザイクスケール(タイルの大きさ)の値を入れてこのソッドを呼びだすと、CIBlendWithMaskフィルタを使いsetupFilterで作成しておいたマスク画像でソース画像を抜き、それとCIPixellateフィルタで作成したておいたモザイク画像を合成してからUIImageViewに表示します。
- (void)doFilter // フィルタ処理を実行する { CIFilter *pixelFilter; CGImageRef cgimage; CGRect drt; if( ciContext && blendFilter ) { // 画像全体をモザイクにするため(モザイクスケールはinputValueで可変) pixelFilter=[CIFilter filterWithName:@"CIPixellate" keysAndValues:@"inputImage",sourceImage, @"inputScale",[NSNumber numberWithFloat:inputValue],nil]; // オリジナル画像とモザイク画像をマスクで抜いた画像の合成 [blendFilter setValue:pixelFilter.outputImage forKey: @"inputImage"]; [blendFilter setValue:maskImage forKey: @"inputMaskImage"]; drt=[sourceImage extent]; cgimage=[ciContext createCGImage:blendFilter.outputImage fromRect:drt]; imageView.image=[UIImage imageWithCGImage:cgimage]; // 結果を表示する CGImageRelease( cgimage ); } }
「Mac OS XとiOS 行ったり来たり」というタイトルで長い間連載してきましたが、OS XとiOSの立ち位置も微妙に変化してきています。将来的には、何か大きな動きがあるかもしれません(統合?)。そんな訳で、本連載はここまでとし、可能であれば、また別テーマで新連載を始められたらと思います。長い間のご愛読ありがとうございました!
-
CGColorRefではパタン描画も可能です
この記事は、2013年08月23日に掲載されました。通常iOSアプリでのカラー操作にはUIColorオブジェクトを利用します。UIColorは、RGB、HSB、グレイスケールの値を使いカラー値を指定できる簡易メ ソッドを備えています。またデフォルトとして用意されているカラー値を得るには、以下のファクトリーメソッドを用いると便利です。
+ (UIColor *)blackColor; // 0.0 ブラック + (UIColor *)darkGrayColor; // 0.333 暗いグレー + (UIColor *)lightGrayColor; // 0.667 明るいグレー + (UIColor *)whiteColor; // 1.0 ホワイト + (UIColor *)grayColor; // 0.5 グレー + (UIColor *)redColor; // 1.0, 0.0, 0.0 レッド + (UIColor *)greenColor; // 0.0, 1.0, 0.0 グリーン + (UIColor *)blueColor; // 0.0, 0.0, 1.0 ブルー + (UIColor *)cyanColor; // 0.0, 1.0, 1.0 シアン + (UIColor *)yellowColor; // 1.0, 1.0, 0.0 イエロー + (UIColor *)magentaColor; // 1.0, 0.0, 1.0 マゼンタ + (UIColor *)orangeColor; // 1.0, 0.5, 0.0 オレンジ + (UIColor *)purpleColor; // 0.5, 0.0, 0.5 パープル + (UIColor *)brownColor; // 0.6, 0.4, 0.2 ブラウン + (UIColor *)clearColor; // 0.0 white, 0.0 クリア
例としてRGB値を代入して使うなら次のメソッドを利用します。
+ (UIColor *)colorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
実はこうしたメソッド以外にも、ヘッダファイルのUIInterface.hには有用なUIColorのファクトリーメソッドが定義されています。これにより、iOS自身がグラフィックユーザインターフェース(GUI)を描画する時に用いているデフォルトのカラー情報を得ることが可能です。これは自身のGUIのカラーをシステム側と合わせたい場合などに有効です。
+ (UIColor *)lightTextColor; // 背景が暗いグレーの時 + (UIColor *)darkTextColor; // 背景が明るいグレーの時 + (UIColor *)groupTableViewBackgroundColor; // テーブルビュー背景色 + (UIColor *)viewFlipsideBackgroundColor; // ビューフリップの背景色 + (UIColor *)scrollViewTexturedBackgroundColor; // スクロールビュー背景色
例えば自分のビューの背景としてスクロールビュー(UIScrollView)でのコンテンツ表示の背景パタンを使いたい時には、上記のscrollViewTexturedBackgroundColorクラスメソッドを用います。すると以下の様なパタン画像が描画されます(iPadの場合)。
myView.backgroundColor=[UIColor scrollViewTexturedBackgroundColor];
UIViewのパタン背景はXcode 4.6のInterface Builder機能からも設定可能です。
ここで不思議なのは「UIColorでパタン画像を取り扱うことが可能なのか?」という点です。直接にはそうしたメソッドは用意されていませんが、Core GraphicsのCGColorRefのCGColorCreateWithPattern()ルーチンを使うと、カラーをパタン画像として登録できます。こうして作成したCGColorRefをUIColorのクラスメソッドであるColorWithCGColor:でUIColorに変換してUIViewのbackgroundColorプロパティにセットすれば、ビューの背景に利用できるわけです。
- (CGPatternRef)getPattern // 余白付きパタン(CGPatternRef)の作成 { CGPatternCallbacks callbacks={ 0,&drawPatternImage,NULL }; // コールバックルーチン CGPatternRef pattern; CGAffineTransform matrix; CGRect drt; drt=CGRectMake( 0,0,18,18 ); // パターン表示矩形(余白がある) matrix=CGAffineTransformMakeScale( 2.0,2.0 ); // 表示スケールを2倍に // コールバックルーチンや変換行列をセットしパターン作成 pattern=CGPatternCreate( NULL,drt,matrix,18,18, kCGPatternTilingConstantSpacing,true,&callbacks ); return pattern; } void drawPatternImage( void *info,CGContextRef ctx ) // 多色矩形パタン描画コールバック { CGRect prt1,prt2,prt3,prt4; prt1=CGRectMake( 0.0,0.0,5.0,5.0 ); // 縦横5.0の矩形 prt2=CGRectMake( 5.0,5.0,5.0,5.0 ); prt3=CGRectMake( 0.0,5.0,5.0,5.0 ); prt4=CGRectMake( 5.0,0.0,5.0,5.0 ); CGContextSetRGBFillColor( ctx,0.0,0.0,1.0,1.0 ); // 青色矩形 CGContextFillRect( ctx,prt1 ); CGContextSetRGBFillColor( ctx,1.0,0.0,0.0,1.0 ); // 赤色矩形 CGContextFillRect( ctx,prt2 ); CGContextSetRGBFillColor( ctx,0.0,1.0,1.0,1.0 ); // シアン矩形 CGContextFillRect( ctx,prt3 ); CGContextSetRGBFillColor( ctx,1.0,1.0,0.0,1.0 ); // 黄色矩形 CGContextFillRect( ctx,prt4) ; } - (IBAction)colorPattern // UIViewの背景としてパタン描画 { CGFloat alpha=1.0; CGPatternRef pattern; CGColorSpaceRef spece; CGColorRef color; pattern=[self getPattern]; // パタン(CGPatternRef)作成 spece=CGColorSpaceCreatePattern( NULL ); color=CGColorCreateWithPattern( spece,pattern,&alpha ); // パタンのCGColorRef作成 CGColorSpaceRelease( spece ); CGPatternRelease( pattern ); myView.backgroundColor=[UIColor colorWithCGColor:color]; // UIViewの背景カラー設定 }
ところが、この方法でUIVIewの背景を設定すると、何故だかビューの矩形領域の背景が黒でクリアされてからパタン描画がなされることが分かります。
そこでUIVIewでなくCALayer(レイヤ)のbackgroundColorプロパティに直接CGColorRefをセットしてしまえば、矩形領域の黒によるクリアは行われません。- (IBAction)colorPattern // CALayerの背景としてパタン描画 { CGFloat alpha=1.0; CGPatternRef pattern; CGColorSpaceRef spece; CGColorRef color; pattern=[self getPattern]; // パタン(CGPatternRef)作成 spece=CGColorSpaceCreatePattern( NULL ); color=CGColorCreateWithPattern( spece,pattern,&alpha ); // パタンのCGColorRef作成 CGColorSpaceRelease( spece ); CGPatternRelease( pattern ); myView.layer.backgroundColor=color; // UIViewのCALayerの背景カラーを設定 }
UIViewでも各パタン画像を展開する場合の余白をゼロにすれば黒クリアの問題は影響ありません。つまり個々のパタン画像の矩形サイズとCGPatternCreate()に渡す縦横の展開幅を一致させればOKです。最初の表示がUIViewの背景カラーにセットした場合、後の表示がCALayerの背景カラーにセットした場合です。- (CGPatternRef)getPattern1 // 余白無しパタン(CGPatternRef)作成 { CGPatternCallbacks callbacks={ 0,&drawPatternImage,NULL }; // コールバックルーチン CGPatternRef pattern; CGAffineTransform matrix; CGRect drt; drt=CGRectMake( 0,0,10,10 ); // パターン表示矩形(サイズは一致) matrix=CGAffineTransformMakeScale( 2.0,2.0 ); // 表示スケールを2倍に // コールバックルーチンや変換行列をセットしパターン作成 pattern=CGPatternCreate( NULL,drt,matrix,10,10, kCGPatternTilingConstantSpacing,true,&callbacks ); return pattern; }
この両者を比べると分かりますが、UIViewの背景カラーにセットした場合とCALayerの背景カラーにセットした場合では、パタン図形が描画される座標系が異なりますので注意してください。UIViewでは左上が原点、 CALayerでは左下が原点でパタン図形が描画されます。これは、UIKitとCore Graphicsの描画システムの座標系の違いが反映されている結果です。 -
Core Imageのトランジション用フィルタ
この記事は、2013年06月26日に掲載されました。OS X 10.4から採用されたCore Imageフレームワークは、GPUのシェーダプログラムを活用することでリアルタイムイメージプロセッシング(画像や映像のフィルタ処理、合成処理、変形処理)を実現します。 このフレームワーク、iOSにはiOS 5から実装されており、iPhoneやiPadアプリでも画像フィルタの一部が使えるようになりました。またiOS 6からは利用できるフィルタ数も100近くに増えています(OS Xは130以上ある)。
iOS 5では存在せず、iOS 6で初めて実装されたフィルタにCICategoryTransition(トランジション)カテゴリがあります。CICategoryTransitionカテゴリには、2つの画像の表示切り換え(トランジション)時に色々なエフェクトを実行するためのフィルタが集められています。Core Imageでトランジションアニメーションを実行するためには、他のフィルタの場合とは少し異なる処理をソースコードとして記述する必用があります。以下が、iOS 6から利用できるようになったトランジション用フィルタです。
・CIBarsSwipeTransition(バースワイプ)
・CICopyMachineTransition(コピーマシン)
・CIDisintegrateWithMaskTransition(マスクからの分解)
・CIDissolveTransition(ディゾルブ)
・CIFlashTransition(フラッシュ)
・CIModTransition(モッド)
・CISwipeTransition(スワイプ)上記フィルタのうち、 CIFlashTransition(フラッシュ) トランジションを実行するソースを記述してみます。具体的には、アニメーションのためにCADisplayLinkを作成し、そこにトランジション用の実行メソッド(doTransition)を登録します。まず最初は、画像の呼び込みとCore ImageフィルタやCADisplayLinkの準備を行うsetupFilterメソッドです。またフィルタの結果を表示するために、先んじてCIContextも作成しておきます。このメソッドは、ビューコントローラのviewDidLoadメソッドなどから呼び出せばOKでしょう。切り換え用の2つの画像、Image1.jpgとImage2.jpgは、先んじてXcodeプロジェクトに登録しておきます。
- (void)setupFilter // フィルタの準備を行う { NSURL *url; CGRect drt; if( url=[[NSBundle mainBundle] URLForResource:@"Image1" withExtension:@"jpg"] ) sourceImage=[CIImage imageWithContentsOfURL:url]; // ソース画像 if( url=[[NSBundle mainBundle] URLForResource:@"Image2" withExtension:@"jpg"] ) targetImage=[CIImage imageWithContentsOfURL:url]; // テーゲット画像 if( sourceImage && targetImage ) { drt=[sourceImage extent]; // 画像の矩形枠 centerX=drt.size.width/2.0; // フィルタに渡す画像の中心X座標 centerY=drt.size.height/2.0; // フィルタに渡す画像の中心Y座標 ciContext=[CIContext contextWithOptions:nil]; // CIContext作成 // アニメーション用のディスプレイリンクを作成して呼び出すアクションを定義 displayLink=[CADisplayLink displayLinkWithTarget:self selector:@selector(doTransition)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; displayLink.frameInterval=1; // フレームレイト(30fps) displayLink.paused=YES; // アニメーション開始 } }
続いて、フィルタに必要な各種パラメータを設定するinitFilterメソッドです。IBOutletとしてスライダー(UISlider)をひとつ用意し、それを動かすことで切換え時のフラッシュの「光の強さ」を調整できるようにします。スライダーの最小値、最大値、初期値は、フィルタにセットするinputStriationStrengthパラメータのアトリビュートを調べる事で得られるので、それぞれをスライダー(imageSlider)側にセットします。それ以外に必要とされるパラメータについては、先んじて適切な値を代入しておきます。
- (void)initFilter // フィルタの初期化を行う { NSDictionary *attribute; displayLink.paused=YES; // アニメーションの一時停止 // CIFlashTransition(フラッシュ)初期パラメータ設定 transitionFilter=[CIFilter filterWithName: @"CIFlashTransition"]; [transitionFilter setValue:[CIVector vectorWithX:centerX Y:centerY] forKey:@"inputCenter"]; [transitionFilter setValue:[CIVector vectorWithX:centerX Y:centerY] forKey:@"inputExtent"]; [transitionFilter setValue:[CIColor colorWithRed:0.0 green:1.0 blue:1.0 alpha:1.0] forKey:@"inputColor"]; [transitionFilter setValue:[NSNumber numberWithFloat:2.58] forKey:@"inputMaxStriationRadius"]; [transitionFilter setValue:[NSNumber numberWithFloat:1.38] forKey:@"inputStriationContrast"]; [transitionFilter setValue:[NSNumber numberWithFloat:0.85] forKey:@"inputFadeThreshold"]; attribute=[[transitionFilter attributes] objectForKey:@"inputStriationStrength"]; // inputStriationStrengthパラメータの最小値、最大値、初期値を得てスライダーに設定 imageSlider.minimumValue=[[attribute objectForKey:kCIAttributeSliderMin] floatValue]; imageSlider.maximumValue=[[attribute objectForKey:kCIAttributeSliderMax] floatValue]; imageSlider.value=[[attribute objectForKey:kCIAttributeDefault]floatValue]; displayLink.paused=NO; // アニメーションの再開 baseTime=0; }
最後は、DisplayLinkから定期的に呼ばれて、フィルタ処理(トランジション)を実行するdoTransitionメソッドです。2つの画像の切換えは、経過時間により順次反転させる(ソース画像とターゲット画像を逆にする)ように調整されています。フィルタ処理により出力された結果(画像)は(transitionFilter.outputImage)はCIImageですので、こちらをCGImageRefに変換した後、最終的にはUIImageにしてから、imageView (UIImageView)のプロパティにセットして表示させています。
- (void)doTransition // 2つの画像でCIFlashTransitionを実行する { CGImageRef cgimage; float time; CGRect drt; if( transitionFilter ) { if( baseTime==0.0 ) baseTime=displayLink.timestamp; // アニメーション開始からの時間経過 time=(displayLink.timestamp-baseTime)*0.5; if( fmodf( time,2.0 ) < 1.0 ) // 2秒で切り換る(ソースとターゲットを交換) { [transitionFilter setValue:sourceImage forKey:@"inputImage"]; [transitionFilter setValue:targetImage forKey:@"inputTargetImage"]; } else { [transitionFilter setValue:targetImage forKey:@"inputImage"]; [transitionFilter setValue:sourceImage forKey:@"inputTargetImage"]; } // CIFlashTransition(光の強さをセット) [transitionFilter setValue:[NSNumber numberWithFloat:imageSlider.value] forKey:@"inputStriationStrength"]; // 切換えタイミングのカーブはcos()を利用して滑らかにする [transitionFilter setValue:[NSNumber numberWithFloat: 0.5*(1.0-cos( fmodf(time,1.0)*M_PI))] forKey:@"inputTime"]; drt=[transitionFilter.outputImage extent]; cgimage=[ciContext createCGImage:transitionFilter.outputImage fromRect:drt]; imageView.image=[UIImage imageWithCGImage:cgimage]; // 結果の描画 CGImageRelease( cgimage ); } }
-
ビューコントローラの入れ子状態
この記事は、2013年04月22日に掲載されました。ユーザにカテゴライズされた複数の機能を提供しようとした場合、iOSデバイスでは、ウィンドウは画面サイズと一致していますので、OS Xの様にマルチウィンドウを利用することはできません。そこで、スクリーン内を幾つかのエリアに分け、それぞれの機能を持たせようとすると、ひとつのビューコントローラで複数のビュー(UIView)を管理し、内部的に処理を分岐&分担させる必用があります。 OS Xで言えば、ウィンドウ内を幾つかの表示領域(ペイン)で区分けし、それぞれの領域に別カテゴリーの機能を実装することに一致します(Xcode自身が良い例)。
iPhoneは画面が小さいので、領域を分けることはせず、UINavigationController や UITabBarControllerなどで別のビューコントローラに切り替えるのが常道です。iPadの場合は少し画面が広いので、ポップオーバーを用いたり、UISplitViewControllerでエリアを2つに分けて使う仕組みも用意されています。iOS 6では、そうした仕組みに加えて、Storybordの編集時に、ビューコントローラ(UIViewController)を入れ子状態にして配置することが可能となりました。
例えば、Xcode 4.6の雛形から「Single View Application」を選んでプロジェクトを新規作成します。この雛形には、ViewControllerというクラスが定義されており、読み込まれるMainStoryboard.storyboardには、そのビューコントローラが登録されています。そこで、ViewControllerクラスのソースファイルとヘッダファイルであるViewController.hとViewController.mをコピーし、新たに2種類のビューコントローラクラス(ViewController1とViewController2)を作成します。
この2つのビューコントローラを、最初からあるViewControllerと入れ子にして配置してみましょう。 ビューコントローラを入れ子にする作業は簡単です。Storybord編集画面でオブジェクトライブラリから「Container View」を選んで、親となるビューコントローラ上にドラッグ&ドロップするだけです。
それにより、それぞれのContainer Viewに対してビューコントローラクラスを設定できるようになります。 ここでは、上部に配置したビューコントローラをViewController1クラスに、下部に配置したビューコントローラをViewController2クラスに設定します。分かり易いように、それぞれにラベルを付けて自身のクラス名を表示しておきます。ちなみに、今までお世話になってきたnibファイル(.xlib)では、こうした編集はできませんのでご注意ください(オブジェクトライブラリにContainer Viewが存在しない)。
ビルドを実行しiOSシミュレータを起動してみると、新しい2つビューコントローラ(ViewController1 と ViewController2)の管理領域(UIView)が、用意されていたビューコントローラ(ViewController)上にレイアウトされていることが分かります。
では、この機能はどんな時に有用なのでしょうか? 例えば、GLKitを利用し画面の複数箇所で3D描画を実行しようとしても、GLKViewControllerとGLKViewが密接に結びついているために困難であることが分かります。具体的には、GLKViewControllerのviewプロパティに代入されるビューが、GLKViewそのものでないとダメな仕様になっているわけです。そこで、別のビューコントローラ上に複数のGLKViewControllerを配置してやれば、それぞれのエリアで別々の3D描画を行う事が可能となります。こうした例からも、とても簡単な仕組みなのですが、その効果は絶大であることが理解できます。
このビューコントローラの入れ子機能、同じくiOS 6から採用されたオートレイアウト機能と組み合わせて「近い将来に大画面用のiOSアプリ(例えばiTV)が開発可能になる証拠では?」と感じているのは筆者だけでしょうか(笑)。
-
iOS 6でEAGLViewをスクリーンキャプチャする
この記事は、2013年02月17日に掲載されました。OpenGL ESで描画した画像のスクリーンキャプチャ(スクリーン画像からUIImagを作成)を実行したい場合があります。GLKitを利用していれば、GLKViewのsnapshotメソッドを使うことで、以下のように簡単に実行できるようになりました。
UIImage *image; image=[gview snapshot]; // GLKViewの表示内容をUIImageとしてキャプチャ
しかし、iOS 4やiOS 3ではGLKitは使用できませんので、そうしたバージョンがインストールされているデバイスまで起動対象にすると、この方法は不可となります。そこで、GLKitを使わずOpenGL ESのレンダリング画面(EAGLView)をスクリーンキャプチャする方法が、以下のテクニカルQ&Aに紹介されています。
QA1704「OpenGL ES View Snapshot」
実は、iOS 5までは、ここに紹介されているやり方で問題なくEAGLViewの画面をスクリーンキャプチャできました。しかし、iOS 6では若干注意する点があります。EAGLViewの初期化を行う場合には、だいたい以下のような処理をソースコードとして記述します。
EAGLContext *context; if( self=[super initWithFrame:frame] ) { eaglLayer=(CAEAGLLayer *)self.layer; eaglLayer.opaque=YES; eaglLayer.drawableProperties=[NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES],kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil]; context=[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; ....
ここで、drawablePropertiesにセットするkEAGLDrawablePropertyRetainedBackingの値をYESにする必要があります。こうしておかないと、OpenGL ESでレンダリングした結果がフレームバッファに残りません。よって、データを抽出しても真っ白な画像しか作成できません。iOS 5までは、これをNOに設定しておいても、問題なく画像データを抽出することができました。ひょっとしたら、こちらの方がバグだったのかもしれません? とりあえずOSのバージョンアップと共に仕様が変更されたわけです。テクニカルQ&Aにはソースコードも掲載されていますが、分かりやすく書き直してみましたので参考にしてみてください。引数で渡すのはEAGLViewです。そこから画像の矩形サイズ(横幅と高さ)を得て画像を呼び込むためのバッファ(メモリ領域)を確保しています。その後、glPixelStorei()とglReadPixels()でOpenGL ESのフレームバッファ内の画像を読み込みます。後は、一般的なCore Graphicsの手法でCGImageRefを作成して、最終的にはそこからUIImageを得ています。
UIImage *eglViewCapture( EAGLView *eaglview ) { NSInteger widthInPoints,heightInPoints,i; NSInteger width,height,dataLength; CGColorSpaceRef colorspace; UIImage *image=nil; CGContextRef cgcontext; unsigned long *data; CGFloat scale; CGImageRef iref; CGDataProviderRef ref; width=eaglview.frame.size.width; // ビュー矩形領域の横幅 height=eaglview.frame.size.height; // ビュー矩形領域の高さ dataLength=width*height*sizeof(unsigned long); // 読み込む画像の総容量 if( data=(GLubyte*)malloc( dataLength ) ) // 読み込み用メモリ確保 { glPixelStorei( GL_PACK_ALIGNMENT,4 ); // 画像保フォーマット指定と読み込み glReadPixels( 0,0,width,height,GL_RGBA,GL_UNSIGNED_BYTE,data ); for( i=0;i<width*height;i++ ) // RGBカラーのアルファ値を設定し直す *(data+i)|=0xff000000; // データプロバイダを用意 ref=CGDataProviderCreateWithData( NULL,data,dataLength,NULL ); colorspace=CGColorSpaceCreateDeviceRGB(); // 設定するカラー環境を用意 if( iref=CGImageCreate( width,height,8,32,width*4,colorspace, kCGBitmapByteOrder32Big|kCGImageAlphaPremultipliedLast, ref,NULL,true,kCGRenderingIntentDefault ) ) // まずはCGImageを作成 { // UIImage作成用のコンテキストをセット if( UIGraphicsBeginImageContextWithOptions ) // iOS 4.0以上はこちら { scale=eaglview.contentScaleFactor; // スケールファクタで調整 widthInPoints=width/scale; heightInPoints=height/scale; UIGraphicsBeginImageContextWithOptions( CGSizeMake(widthInPoints, heightInPoints),NO,scale ); } else // iOS 3.xの場合はこちら { widthInPoints=width; heightInPoints=height; UIGraphicsBeginImageContext( CGSizeMake(widthInPoints, heightInPoints)); } cgcontext=UIGraphicsGetCurrentContext(); // CGContextRefを得る CGContextSetBlendMode( cgcontext, kCGBlendModeCopy ); // 描画モード CGContextDrawImage( cgcontext,CGRectMake ( 0.0,0.0,widthInPoints,heightInPoints ),iref ); // CGImageを描画 image=UIGraphicsGetImageFromCurrentImageContext(); // UIImageを得る UIGraphicsEndImageContext(); // UIImage作成用のコンテキストを解放 CGImageRelease( iref ); // CGImageを解放 } CFRelease( colorspace ); // カラー環境を解放 CFRelease( ref ); // データプロバイダーを解放 free( data ); // 確保したメモリの解放 } return image; // 作成されたUIImageを返す }
このルーチンで注目すべきは以下の箇所です。for( i=0;i<width*height;i++ ) // RGBカラーのアルファ値を設定し直す *(data+i)|=0xff000000;
これを実行しておかないと、OpenGL ESのレンダリング処理の実装の仕方によってはアルファ値が正しく設定されておらず、UIImageに変換した時に何も表示されない画像(アルファ値がゼロ)が出来上がってしまいます。テクスチャを使えば大丈夫のようですが、頂点カラーの表示だけを用いたレンダリングではダメな場合があるようです。ご注意ください。 -
Core Imageでビデオ映像にフィルタ処理
この記事は、2012年12月17日に掲載されました。今回は、キャプチャセッションで得られたビデオフレーム画像に対し、Core Imageを使いフィルタ処理を行ってからスクリーンに再度表示する方法を解説します。一度、静止画像としてキャプチャしてからフィルタ処理を行えば簡単ですが、リアルタイム(高速)にフィルタ効果をプレビューする場合には、前回と同様に、キャプチャセッションのメデイアデータ出力形式として「サンプルバッファ(CMSampleBufferRef)」を利用することになります。
まず、どんな画像フォマットでビデオフレームを得るかを指定します。今回も32BGRAを利用しますが、その他にyuvsや2vuyなどのフォーマットも利用可能です。ビデオのフレームデータは設定したデリゲートメソッドに1フレーム単位で入ってきますので、GCDデスパッチキューを作り別スレッドでデリゲートメソッドが実行されるようにします。メソッド名はやはりcaptureOutput:didOutputSampleBuffer:fromConnection:となります。前回は、この処理をメインスレッドで行いましたが、今回はそうでなくても問題なく処理が可能です。
まずは、ビデオフレームの画像処理に用いるCore Imageのフィルタを準備しておきます。今回は色相調整用のCIHueAdjustフィルタを使います。このフィルタのパラメータの変更は、ツールバー上のスライダーでリアルタイムに調整できるようにしておきます。Core Imageに関しては以下の2つのドキュメントを参照してください(和訳は無し)。
「Core Image Programming Guide」(Core Imageプログラミングの詳細な解説)
「Core Image Filter Reference」(Core Image搭載のフィルタのリファレンス)
-(void)setupFilter // Core Imageフィルタ・セットアップ { cicontext=[CIContext contextWithOptions:nil]; // 先んじていCIContextを作成する filter=[CIFilter filterWithName:@"CIHueAdjust"]; // 色相調整フィルター作成 [filter setDefaults]; // 初期値をセット hueValue=0.0; // 色相パラメータ初期値 }
キャプチャセッションのセットアップ処理では、デバイス出力としてAVCaptureVideoDataOutputを選び、利用フォーマット(32BGRA)を選択した後にデリゲートメソッド用のGCDデスパッチキューをセットします。今回は、キャプチャセッション自身とフィルタ処理後の両方をスクリーン表示させますので、ビデオプレビュー用レイヤーのAVCaptureVideoPreviewLayerも準備しておきます。もし、フィルタ処理後の映像のみを表示したければ、ソースコードのプレビューレイヤ設定の箇所は省略してもかまいません。
- (BOOL)setupAVCapture // キャプチャセッション・セットアップ { NSDictionary *rgbOutputSettings; AVCaptureDeviceInput *deviceInput; NSError *error=nil; CALayer *rootLayer; AVCaptureSession *session; AVCaptureDevice *device; NSUInteger ret=NO; session=[[AVCaptureSession alloc] init]; // キャプチャセッションのインスタンス作成 // 対象メデイアのプリセット(写真用の最大解像度) [session setSessionPreset:AVCaptureSessionPresetPhoto]; // ビデオ映像読み込みデバイス使用(ディフォルトは背面カメラ) device=[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; // デバイス入力インスタンス作成 deviceInput=[AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; if( ! error ) { if( [session canAddInput:deviceInput] ) // セッションにデバイス入力を追加 [session addInput:deviceInput]; // 映像フレーム出力インスタンス作成(ビデオのフレームデータを処理する) videoDataOutput=[[AVCaptureVideoDataOutput alloc] init]; // 出力映像フォーマット(32BGRA) rgbOutputSettings=[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCMPixelFormat_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]; [videoDataOutput setVideoSettings:rgbOutputSettings]; // フォーマット設定 // 逐次ビデオフレームを破棄 [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; // ビデオフレーム処理の直列ディスパッチキュー作成 videoDataOutputQueue=dispatch_queue_create( "VideoDataOutputQueue",nil ); // デリゲート用のディスパッチキューを設定 [videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue]; // セッションにビデオフレーム出力(AVCaptureVideoDataOutput)を追加 if( [session canAddOutput:videoDataOutput] ) [session addOutput:videoDataOutput]; // セッションで使うビデオプレビュー用レイヤー作成 previewLayer=[[AVCaptureVideoPreviewLayer alloc] initWithSession:session]; [previewLayer setBackgroundColor:[[UIColor blackColor] CGColor]]; // 背景は黒 [previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect]; // 表示形式 rootLayer=[previewView layer]; // previewViewのレイヤーを得る [rootLayer setMasksToBounds:YES]; // マスクを一致させる [previewLayer setFrame:[rootLayer bounds]]; // previewLayerの矩形枠設定 [rootLayer addSublayer:previewLayer]; // previewLayerを配置 [session startRunning]; // 映像プレビュー開始 ret=YES; } return ret; }
次は、1フレームごとのビデオフレームデータを得るためのデリゲートメソッドです。CMSampleBufferRefからCMSampleBufferGetImageBuffer()ルーチンを使いCore Videoのピクセルバッファ(CVPixelBufferRef)を得ます。このピクセルバッファには画像データが32BGRAフォーマットで格納されています。続いてフィルタ処理の対象とするCIImageを、initWithCVPixelBuffer:options:メソッドで作成します(対象はUIImageではないので注意!)。回転とミラー反転などの座標変換後に、得られたCIImageを入力画像としてパラメータのinputAngleを変更し色相調整フィルタ処理を実行します。変更値はツールバー上のスライダーからの値です。そして結果としての出力画像を新規のCIImageとして得ることができます。
得られたCIImage(フィルタ処理済み)をスクリーン上に配置したUIImageViewへ表示します。この処理はメインスレッドで行う必要がありますので、dispatch_async()のブロック処理として実装します。手順としては、createCGImage:fromRect:でCIImageからCGImageRefを作り、さらにimageWithCGImage:メソッドでUIImageを作成してUIImageViewのimageプロパティにセットします。
// ビデオデータ読み込みデリゲート (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer: (CMSampleBufferRef)sampleBuffer fromConnection: (AVCaptureConnection *)connection { CIImage *ciimage,*ciimage1,*ciimage2; CVPixelBufferRef pixelBuffer; CFDictionaryRef attachments; CGAffineTransform tt; // CMSampleBufferRefからピクセルバッファ(CVPixelBufferRef)を得る pixelBuffer=CMSampleBufferGetImageBuffer( sampleBuffer ); // 追加情報を得る attachments=CMCopyDictionaryOfAttachments( kCFAllocatorDefault,sampleBuffer, kCMAttachmentMode_ShouldPropagate ); // CIImageを作成する ciimage=[[CIImage alloc] initWithCVPixelBuffer:pixelBuffer options: (__bridge NSDictionary *)attachments]; tt=CGAffineTransformMakeRotation( -M_PI/2.0 ); // 90度回転 if( isFrontCamera==YES ) // 前方カメラの場合ミラー反転させる tt=CGAffineTransformScale( tt,1.0,-1.0 ); ciimage1=[ciimage imageByApplyingTransform:tt]; // CIImageの表示方向を指定する [filter setValue:ciimage1 forKey:@"inputImage"]; // 入力画像設定とフィルタ処理実行 [filter setValue:[NSNumber numberWithFloat:hueValue] forKey:@"inputAngle"]; if( ciimage2=filter.outputImage ) // フィルタ処理の出力画像を得る { // メインスレッドディスパッチキュー dispatch_async( dispatch_get_main_queue(), ^{ CGImageRef cgimage; cgimage=[cicontext createCGImage:ciimage2 fromRect:[ciimage2 extent]]; imageView.image=[UIImage imageWithCGImage:cgimage]; CGImageRelease( cgimage ); // UIImageViewに表示する }); } if( attachments ) CFRelease(attachments); }
以上の処理で、ビデオフレーム画像に対してリアルタイムでフィルタ処理を実行し、その結果を再度スクリーンに表示することが可能です。もし、UIImageViewへの表示では処理が遅いと感じるようなら、CALayerへダイレクトに表示させることも可能です。ぜひ、チャレンジしてみてください。
-
ビデオ映像をOpenGLテクスチャとして使う
この記事は、2012年10月27日に掲載されました。AV Foundationは、iOS 4.0 以降とOS X 10.7(Lion)以降で利用できるフレームワークでして、タイムベースのオーディオ&ビジュアルメディアの作成、記録、再生などに活用します。こフレームワークを用いれば、ビデオカメラから取り込んだ映像(フレーム画像)を、ダイレクトにOpenGLテクスチャとして活用できます。今までもこうした処理を実現する方法は幾つかありました。しかし、逐次画像変換による処理スピードの低下が問題だったため、iOS 5からCore Videoに画像変換処理を経由しないテクスチャ利用の仕組みが導入されています(CVOpenGLESTextureCache.h)。
ビデオ映像を得るためには、先んじてキャプチャセッション(AVCaptureSession)を作成し、それのセットアップを行います。1フレーム毎のビデオフレーム画像を「サンプルバッファ(CMSampleBufferRef)」として入手するため、デバイス出力としてAVCaptureVideoDataOutputを作成してセッションに登録しておきます。デリゲートでサンプルバッファから得られたピクセルバッファ(CVImageBufferRef)をOpenGLテクスチャとするためには、CVOpenGLESTextureCacheCreate()ルーチンを使い、キャッシュ用のリファレンス(CVOpenGLESTextureCacheRef)を準備しておく必要があります。
// キャッシュリファレンス作成(CVImageBufferRefとテクスチャ変換用) CVOpenGLESTextureCacheCreate( kCFAllocatorDefault,NULL, glContext, NULL,&videoTextureCache) )
OpenGLでの処理の制限上、デリゲートメソッドはメインランループ(スレッド)上で実行されるように、別スレッド指定ではなく、dispatch_get_main_queue()を引数に渡してメインスレッド用のデスパッチキューを作成します。
// デリゲート用のディスパッチキューを設定(メインスレッド) videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
以下は、ビデオフレーム画像をOpenGLテクスチャとして利用するためのキャプチャセッションのセットアップ例です。 今回はスクリーン全体に対してOpenGL経由の表示が行われますので、キャプチャセッション自身のビデオプレビューは必要ありません。そのため、それに関係する処理はすべて外してあります。
- (BOOL)setupAVCapture // キャプチャセッション・セットアップ { NSDictionary *rgbOutputSettings; AVCaptureDeviceInput *deviceInput; NSError *error=nil; AVCaptureDevice *device; NSUInteger ret=NO; // キャッシュリファレンス作成(CVImageBufferRefとテクスチャ変換用) if( ! CVOpenGLESTextureCacheCreate( kCFAllocatorDefault,NULL,glContext, NULL,&videoTextureCache) ) { session=[[AVCaptureSession alloc] init]; // キャプチャセッションインスタンス作成 // 対象メデイアのプレセット( 写真用の最大解像度) [session setSessionPreset:AVCaptureSessionPresetPhoto]; // ビデオ映像読み込みデバイス使用(デフォルトは背面カメラ) device=[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; // デバイス入力インスタンス作成 deviceInput=[AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; if( ! error ) { // セッションにデバイス入力(AVCaptureDeviceInput)を追加 if( [session canAddInput:deviceInput] ) [session addInput:deviceInput]; // 映像フレーム出力インスタンス作成(ビデオフレームデータを得るため) videoDataOutput=[[AVCaptureVideoDataOutput alloc] init]; // 出力映像フォーマット(32BGRA) rgbOutputSettings=[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCMPixelFormat_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]; // ビデオフォーマット設定 [videoDataOutput setVideoSettings:rgbOutputSettings]; // 逐次ビデオフレームを破棄 [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; // デリゲート用のディスパッチキューを設定(メインスレッドで実行) [videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()]; // セッションに映像フレーム出力(AVCaptureVideoDataOutput)を追加 if( [session canAddOutput:videoDataOutput] ) [session addOutput:videoDataOutput]; [session startRunning]; // キャプチャセッション開始 ret=YES; } } return ret; }
以下がビデオフレームデータを得るためのデリゲートメソッドです。最初にサンプルバッファからピクセルバッファ(CVPixelBufferRef)を得ます。その後、CVOpenGLESTextureCacheCreateTextureFromImage()ルーチンでOpenGLテクスチャ(CVOpenGLESTextureRef)を作り、それを使いglBindTexture()を実行してから、glTexParameteri()でテクスチャの表示用パラメータを設定します。
// ビデオデータ読み込みデリゲート (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer: (CMSampleBufferRef)sampleBuffer fromConnection: (AVCaptureConnection *)connection { size_t width,height; CVPixelBufferRef pixelBuffer; // CMSampleBufferRefからピクセルバッファ(CVPixelBufferRef)を得る pixelBuffer=CMSampleBufferGetImageBuffer( sampleBuffer ); width=CVPixelBufferGetWidth( pixelBuffer ); // ピクセルバッファの横幅 height=CVPixelBufferGetHeight( pixelBuffer ); // ピクセルバッファの高さ [self cleanUpTexture]; // 現在にテクスチャを解放(自作メソッド) // CVPixelBufferRefからテクスチャを作成(Core Video) if( ! CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, videoTextureCache,pixelBuffer,NULL,GL_TEXTURE_2D,GL_RGBA, width,height,GL_BGRA,GL_UNSIGNED_BYTE,0,&glTexture) ) { // OpenGLテクスチャの使用宣言とテクスチャ用パラメータの設定 glBindTexture( CVOpenGLESTextureGetTarget(glTexture), CVOpenGLESTextureGetName(glTexture) ); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE); } } - (void)cleanUpTexture // テクスチャの削除 { if( glTexture ) { CFRelease( glTexture ); // CVOpenGLESTextureRefを削除する glTexture=NULL; } CVOpenGLESTextureCacheFlush( videoTextureCache,0 ); // キャッシュクリア実行 }
こうして得られたテクスチャ画像は、iOS 5.0から導入されたGLKitを利用して3Dオブジェクトに貼付けて描画するのが簡単です。
そこで、MOSAでは、2012年12月13日(木) 筆者によるiOSアプリでのOpenGL ESとGLKitの活用方法やGLKitが追加搭載された背景を解説するセミナー「OpenGL環境にGLKitが追加された背景を探る」を開催いたします。本セミナーでは、iOSアプリでのOpenGL ESとGLKitの活用方法やGLKitが追加搭載された背景を解説しあます。具体的には、OpenGL ESやGLKitを用いた効果的で高速な2D描画、アニメーション、画像処理などについてサンプルソースコードを提示しながら詳しく解説を致します。多くの方のご参加をお待ちしております!
-
キーフレームアニメーションの実行
この記事は、2012年09月15日に掲載されました。前回は、明示的アニメーションの仕組みと、簡単な例について解説しました。今回は、明示的アニメーションを使う目的はコレと言っても良い、キーフレームアニメーションについて解説したいと思います。
CAKeyframeAnimationクラスてがサポートしているキーフレームアニメーションは、CABasicAnimationに似ていますが、対象となるプロパティ値の配列(複数のプロパティ値)を保持することができます。これらをキーフレーム値と呼びます。配列に保存されたキーフレーム値は、アニメーションの実行中にそれぞれ順番に補間されて表示に使われます。キーフレーム値は、CAKeyframeAnimationオブジェクトのpathプロパティに設定したCore Graphicsパス図形か、valuesプロパティに設定したオブジェクトの配列のどちらかとなります。つまり、pathプロパティがセットされている場合にはvaluesプロパティは無視されるわけです。
以下は、Core Graphics APIで定義したパス図形(ベジュ関数)の軌跡上を移動するアニメーションの一例です。
(IBAction) doKeyFrameAnime // パスに沿って移動させるアニメーション { CALayer *layer; CAKeyframeAnimation *anime; CGMutablePathRef path; layer=myView.layer; path=CGPathCreateMutable(); // Core Graphicsパス図形作成 CGPathMoveToPoint( path,NULL,74.0,74.0 ); // パス図形の定義 CGPathAddCurveToPoint( path,NULL,74.0,500.0,320.0,500.0,320.0,74.0 ); CGPathAddCurveToPoint( path,NULL,320.0,500.0,566.0,500.0,566.0,74.0 ); // パス図形によるキーフレーム値(CGPoint)は位置(position)を示す anime=[CAKeyframeAnimation animationWithKeyPath:@"position"]; anime.path=path; // Core Graphicsパス図形を設定 anime.duration=5.0; // 1サイクル5秒 anime.removedOnCompletion=NO; // 終了時にアニメを消さない anime.FillMode=kCAFillModeForwards; // 終了時の表示を維持する anime.rotationMode=kCAAnimationRotateAuto; // 移動方向に向き回転する [layer addAnimation:anime forKey:nil]; // アニメーション実行 CFRelease( path ); }
rotationModeプロパティにkCAAnimationRotateAutoをセットすると、移動アニメーションを実行する時に、進行方向に対していつも正面を向くようにCALayerの表示が回転されます。また、kCAAnimationRotateAutoReverseをセットすると進行方向に対して逆を向くように回転します。このプロパティに何もセットしなければ、アニメーション中にCALayerの回転は起こりません。pathプロパティではなく、対象オブジェクトの配列(NSArray)をvaluesプロパティにセットすると、任意タイプのCALayerプロパティの変化をアニメーション化することが可能です。例えば、frameプロパティの変化ならCGRect構造体(NSValueにラップ)を、transformプロパティの変化ならCATransform3D構造体(NSValueにラップ)を、contentsプロパティならCGImageを配列に保存することになります。以下は3秒間で2つの画像を切り替えるアニメーションです。表示の切り替えにはフェイドエフェクト(アルファ値が徐々に切り替わる)が利用されます。
- (IBAction) doKeyFrameAnimation // 2つの画像を切り替えるアニメーション { UIImage *image1,*image2; CALayer *layer; CAKeyframeAnimation *anime; image1=[[UIImage imageNamed:@"image1.png"]retain]; // 最初の画像 image2=[[UIImage imageNamed:@"image2.png"]retain]; // 次の画像 layer=myView.layer; // values配列のキーフレーム値は表示画像(contents)を示す anime=[CAKeyframeAnimation animationWithKeyPath:@"contents"]; anime.values=[NSArray arrayWithObjects:(id)image1.CGImage, (id)image2.CGImage,nil]; anime.duration=3.0; // 1サイクル3秒 anime.removedOnCompletion=NO; // 終了時にアニメを消さない anime.FillMode=kCAFillModeForwards; // 終了時の表示を維持する [layer addAnimation:anime forKey:nil]; // アニメーション実行 }
CAKeyframeAnimationではtimingFunctionプロパティは無視され、timingFunctionsプロパティに設定されたCAMediaTimingFunctionオブジェクトの配列が使われます。
@property(copy) NSArray *timingFunctions; // timeFunctionの代わり
またdurationプロパティの方は有効ですが、keyTimesプロパティを使いタイミングをより細かく調節できます。keyTimesプロパティには、各キーフレームセグメントの再生時間を定義するNSNumberオブジェクトの配列を代入します。配列内の値は浮動小数点数であり0.0から1.0の範囲に制限されます。それぞれがValues配列内の各要素に対応しており、対応するキーフレーム値の再生時間を、そのアニメーションの合計再生時間に対する割合として定義しています。配列に保存する浮動小数点数値は、直前の値よりも大きいか同じでなければいけません。以下の例では、4座標を指示して移動アニメーションを実行しています。アニメーションの1サイクルは10秒で終わりますが、その時間配分がkeyTimesプロパティで指示されています。また、その配分や保管方法はcalculationModプロパティで指定できます。
- (IBAction) doKeyFrameAnime // 4座標を経路にして移動させるアニメーション { CGPoint pt1,pt2,pt3,pt4; CALayer *layer; CAKeyframeAnimation *anime; layer=myView.layer; pt1.x=pt1.y=pt2.x=pt4.y=200.0; // 移動座標の代入(4点) pt2.y=pt3.x=pt3.y=pt4.x=600.0; // 移動アニメーションを4座標をCGPoint値で指示 anime=[CAKeyframeAnimation animationWithKeyPath:@"position"]; anime.values=[NSArray arrayWithObjects:[NSValue valueWithCGPoint:pt1], [NSValue valueWithCGPoint:pt2], [NSValue valueWithCGPoint:pt3], [NSValue valueWithCGPoint:pt4],nil]; anime.duration=10.0; // 1サイクル10秒 anime.calculationMode=kCAAnimationCubic; // スプライン補間 // 各ポイントまでの時間配分を0.0から1.0の浮動小数点で指示 anime.keyTimes=[NSArray arrayWithObjects: [NSNumber numberWithFloat:0.0],[NSNumber numberWithFloat:0.1], [NSNumber numberWithFloat:0.5],[NSNumber numberWithFloat:1.0f],nil]; [layer addAnimation:anime forKey:nil]; // アニメーション実行 }
calculationModは5種類から選択可能です。例ではkCAAnimationCubicが選択されており、時間も各座標への移動経路もスプライン補間されます。スプライン補間方法は、tensionValues 、continuityValues 、biasValuesの3つのプロパティで調整することも可能です。
CA_EXTERN NSString * const kCAAnimationLinear; // リニア補間 CA_EXTERN NSString * const kCAAnimationDiscrete; // 補間なし CA_EXTERN NSString * const kCAAnimationPaced; // ペース変更なし(無視) CA_EXTERN NSString * const kCAAnimationCubic; // スプライン補間 CA_EXTERN NSString * const kCAAnimationCubicPaced; // 同上ペース変更なし @property(copy) NSArray *tensionValues, *continuityValues, *biasValues;
今回は、明示的アニメーションを使う目的はコレと言っても良い、キーフレームアニメーションについて解説しました。今回をもってCore Animationに関する解説は終了となります。予定されているMOSADeN Onlineの改装にあわせて、次回からは別の形でiOSやOS Xの旬な話題を提供できればと考えております。 -
明示的アニメーションで変化を拾う
この記事は、2012年08月27日に掲載されました。前回は、UIViewアニメーション機能について詳しく解説しました。今回は、いよいよ明示的アニメーションの詳細に進みます。その先には、キーフレームアニメーションのようなCore Animationスペシャルな機能が待っています。
明示的アニメーションにおいて、処理の中心となるクラスは「CATransition」と「CABasicAnimation」と「CAKeyFrameAnimation」です。もう一つのCAPropertyAnimationクラスは、CABasicAnimationとCAKeyFrameAnimationクラスが継承しているCAAnimationクラスの抽象サブクラスです。
「CABasicAnimation」は、CALayerの2つのプロパティ値(開始値と終了値)の間を線形補間したアニメーションを実行します。こうした種類のアニメーションのほとんどは、暗黙的アニメーションとして実行することも可能です。
「CAKeyframeAnimation」は、キーフレームアニメーションをサポートします。アニメーション化するCALayerプロパティのキーパスと、アニメーションの各場面のプロパティ値の配列、およびキーフレームの時間とタイミング関数の配列を指定してアニメーションを実行します。アニメーションが実行される度に指定された補間を使って各値が設定し直されます。
「CATransition」は、CALayerのコンテンツ全体に作用するトランジション(場面転換)エフェクトを提供します。トランジションエフェクトの種類としてはフェード、プッシュ、リビールなどが用意されています。OS Xに限り、Core Imageのカスタムフィルタを指定することによって、デフォルトのトランジションエフェクトを拡張できます。
まずは簡単な明示的アニメーションの簡単な例を取り上げてみます。CALayerの矩形サイズが縮小(縦横100ピクセル)される動きをアニメーション化したものです。この明示的アニメーションには、CABasicAnimationクラスを利用します。
- (IBAction)doExplicitAnimation:(id)sender // iOSの場合の明白的アニメーション { CABasicAnimation *anime; CALayer *layer; CGRect srt,drt; layer=myView.layer; // 画像レイヤを得る(UIView) // アニメション作成(対象はboundsプロパティ) anime=[CABasicAnimation animationWithKeyPath:@"bounds"]; srt=layer.bounds; // 開始矩形サイズ drt=CGRectInset( srt,50,50); // 終了矩形サイズ(縦横100ピクセル縮小) // 開始と終了値をvalueWithCGRectでNSValueへ変換して代入する anime.fromValue=[NSValue valueWithCGRect:srt]; anime.toValue=[NSValue valueWithCGRect:drt]; [layer addAnimation:anime forKey:@"boundsAnimation"]; // アニメを実行 }
animationWithKeyPath:メソッドで渡している文字列「bounds」がアニメーション(変化)の対象に指定するプロパティ名となります。 その後CABasicAnimationオブジェクトのfromValueプロパティに開始値(NSValue)を、toValueプロパティに終了値(NSValue)を代入してからアニメーションの対象とするCALayerオブジェクトに対しaddAnimation:forKey:メッセージを送ります。この場合forKey:に渡す引数はnilでもかまいません(その場合は名前指定なし)。注意点は、fromValueとtoValueプロパティに渡すのは構造体や数値そのものではなくて、それらをラップした「オブジェクト」だということです。上の例の場合、iOSではプロパティに渡すべき値はCGRect構造体ですが、OS XではNSRect構造体となりますので注意してください。CGRect構造体はvalueWithCGRect:メソッドでNSValueオブジェクトにラップしてからプロパティに代入します。各構造体からNSValueへの変換メソッドは、ヘッダーファイルのUIGeometory.hやCATransform3D.hに定義されています。
+ (NSValue *)valueWithCGPoint:(CGPoint)point; + (NSValue *)valueWithCGSize:(CGSize)size; + (NSValue *)valueWithCGRect:(CGRect)rect; + (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform; + (NSValue *)valueWithCATransform3D:(CATransform3D)t;
プロパティへ浮動小数点や整数を渡す場合には、NSNumberオブジェクトでラップします。これを担当するメソッドはNSNumberクラス(NSNumber.h)に定義されています。例えば、CALalyerのopacity(アルファ値)を1.0からゼロへ変化させると、表示された画像がだんだんと透明になって消えていくアニメーションを実現できます。この設定には、numberWithFloat:メソッドを使っていることが分かります。
anime.fromValue=[NSNumber numberWithFloat:1.0]; anime.toValue=[NSNumber numberWithFloat:0.0];
CAAnimationでは「キーパス」を使うことで選択した構造体の各フィールドにアクセスすることができます。これは、いちいち構造体フィールド全部をfromValueやtoValueプロパティに渡す必要がないことを意味しています。例えばtransform(CATransform3D)プロパティのマトリックス演算子のうち、Z軸方向の回転角度(ラディアン)だけを変更するアニメーションを行う場合には、次のように記述できます。このアニメーションでは、CALayer自身が360度回転するのに1秒かかり、それを全部で3回転させます。
#define M_PI 3.14159265358979323846 layer=myView.layer; anime =[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; anime.duration=1.0; anime.repeatCount=3; anime.fromValue=[NSNumber numberWithFloat: 0.0]; anime.toValue=[NSNumber numberWithFloat:M_PI*2]; [layer addAnimation:anime forKey:nil];
明示的アニメーションで、どのようなキーパスが利用できるかかについて把握するには、 和訳されたドキュメント「Core Animationプログラミングガイド」のp27「キーパスを使った変換の変更」の一覧表を参考にするのが近道です。以下は、画面上にレイアウトしたUIView(UIImageViewなどでもOK)を心臓の鼓動のように1回だけ「ドックン」と脈動させるルーチン例です。 明示的アニメーションでは、アニメーションの時間軸を制御するタイミング関数(三次元ベジェ曲線)を選択可能です。kCAMediaTimingFunctionDefaultを使うと、ゆっくりとスタートし、徐々に加速、そしてまた減速して止まるという時間軸カーブに沿ったアニメーションを実現できます。例としては、MacのDockに登録されているアイコンのジャンプのような動きだと思ってください。このカーブをリニア(直線)のkCAMediaTimingFunctionLinearに切り換えて、アニメーションの雰囲気がどんな風に変わるのかを試してみるのも面白いかもしれません。
// 脈動アニメーション void pulseAnimation( UIView *view,float duration,float scale,id delegate ) { CABasicAnimation *anime1,*anime2; CATransform3D trans3d; CAAnimationGroup *group; CALayer *layer; layer=[view layer]; anime1=[CABasicAnimation animationWithKeyPath:@"opacity"]; // アルファ値対象 anime1.fromValue=[NSNumber numberWithFloat:1.0]; anime1.toValue=[NSNumber numberWithFloat:0.5]; anime2=[CABasicAnimation animationWithKeyPath:@"transform"]; // 座標変換対象 trans3d=CATransform3DMakeScale( 1.0,1.0,1.0 ); anime2.fromValue=[NSValue valueWithCATransform3D:trans3d]; trans3d=CATransform3DMakeScale( scale,scale,scale ); // 拡大スケール指定 anime2.toValue=[NSValue valueWithCATransform3D:trans3d]; group=[CAAnimationGroup animation]; // グループ指定 group.delegate=delegate; group.duration=duration; // 1サイクルに有する時間 group.autoreverses=YES; // リバースアニメーション group.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; // 制御関数指定 group.animations=[NSArray arrayWithObjects:anime1,anime2,nil]; // グループ登録 [layer addAnimation:group forKey:@"startAnimation"]; // アニメーション開始 }
CAAnimetionクラスにはCAAnimationDelegateとして2つのメソッドが定義されており、delegateプロパティをセットしておくことでアニメーションの開始時と終了時に適切な処理を呼び出すことが可能です。このデリゲートをうまく利用すれば、あるアニメーションが終了した時点で別のアニメーションを開始するようなことが可能です。
- (void)animationDidStart:(CAAnimation *)anim; // 開始時 (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag; // 終了時
今回は、明示的アニメーションの仕組みと、簡単な例について解説しました。次回は、明示的アニメーションを使う目的はコレと言っても良い、キーフレームアニメーションについて解説したいと思います。 -
お手軽なUIViewアニメーションを使う
この記事は、2012年05月23日に掲載されました。前回は、CATransactionクラスの仕組みと、UIView自身のアニメーション機能が存在していることについてお話しました。今回は、UIViewアニメーション機能についてさらに詳しく解説いたします。
まず最初に、UIViewクラスのアニメーション機能を利用した一例を見てみます。前回で解説したトランザクション定義との類似性が理解できます。この時、アニメーションを引き起こすプロパティ変更はUIView対象であり、CALayer対象ではありませんので注意してください。iOSの場合、この機能のおかげでCALayerに対して直接アニメーションを実行すべきケースは少ないと思われます(OS Xにはない)。
- (IBAction)doAnime:(id)sender // UIViewのアニメーション { CGRect drt; [UIView beginAnimations: nil context:NULL]; // アニメ定義開始 [UIView setAnimationDuration:3.0]; // 1サイクル3.0秒 [UIView setAnimationRepeatAutoreverses:YES]; // 巻き戻しを行う [UIView setAnimationRepeatCount:2]; // 2回繰り返す drt=myView.frame; drt.origin.x+=100; myView.frame=drt; [UIView commitAnimations]; // アニメ定義終了 }
対象クラスがUIViewを継承していれば、コントロールやツールバーなどでも同様な手段が取れます。つまり、UIKitで使えるユーザインターフェース(UI)関連のオブジェクトは、ほとんど全てアニメーションの対象となるわけです。以下は、ツールバーをじわっと(0.5秒)消していくアニメーションの一例です。
- void setToolbarAlpha( UIToolbar *toolbar,CGFloat alpha ) { [UIView beginAnimations:nil context:NULL]; // アニメ定義開始 [UIView setAnimationDuration:0.5]; // 1サイクル0.5秒 toolbar.alpha=alpha; // ツールバーを非表示にする場合はアルファ値をゼロに [UIView commitAnimations]; // アニメ定義終了 }
続いて、UIViewのトランジション(切り替え)アニメーションの例です。 ビューを追加や削除する操作が引きがねとなりトランジション(めくり効果)アニメーションが実行されます。
- (void)close // 画像を閉じる { [UIView beginAnimations:nil context:NULL]; // アニメ定義開始 [UIView setAnimationDuration:1.0]; // 1サイクル1.0秒 [UIView setAnimationTransition:UIViewAnimationTransitionCurlDown forView:myViewController.view cache:YES]; // トランジション指定 [self.view removeFromSuperview]; // スパービューから外す [UIView commitAnimations]; // アニメ定義終了 }
次の例は、transformプロパティを変更することで、UIViewをアニメーションさせながら回転&ミラー反転させるルーチンです。引数のdur(NSTimeInterval)でアニメーションを何秒実行するのかを指示します。回転とミラー反転の組み合わせから7種類の変換タイプが選択できます。ここでの対象はUIImageViewですので、このビューに表示されている画像(UIImage)も指示に従い回転やミラー反転表示されます。
#define M_PI 3.14159265358979323846 void rotateView( UIImageView *iview,NSUInteger type,NSTimeInterval dur ) { CGAffineTransform tt; [UIView beginAnimations:nil context:NULL]; // アニメ定義開始 [UIView setAnimationDuration:dur]; // 1サイクルの時間指示 if( type==0 ) // 正常位置 iview.transform=CGAffineTransformIdentity; else if( type==1 ) // +90度 iview.transform=CGAffineTransformMakeRotation( M_PI/2.0 ); else if( type==2 ) // -90度 iview.transform=CGAffineTransformMakeRotation( -M_PI/2.0 ); else if( type==3 ) // 180度 iview.transform=CGAffineTransformMakeRotation( M_PI ); else if( type==4 ) // 上下反転 iview.transform=CGAffineTransformMakeScale( 1.0,-1.0 ); else if( type==5 ) // 左右反転 iview.transform=CGAffineTransformMakeScale( -1.0,1.0 ); else if( type==6 ) // 上下+90度 { tt=CGAffineTransformMakeScale( 1.0, -1.0 ); iview.transform=CGAffineTransformRotate( tt, M_PI/2.0 ); } else if( type==7 ) // 左右+90度 { tt=CGAffineTransformMakeScale( -1.0,1.0 ); iview.transform=CGAffineTransformRotate( tt, M_PI/2.0 ); } [UIView commitAnimations]; // アニメ定義終了 }
iOS 4.0から、UIViewのアニメーション処理をブロック(Block)で引き渡すことのできるメソッドが用意されました。それはらは、UIViewのUIViewAnimationWithBlocksカテゴリーにまとめられています。現在は5種類のクラスメソッドが定義されています。このうち幾つかのメソッドでは、アニメーション実行時のオプション(反転や速度カーブの種類など)をフラグ値(UIViewAnimationOptions)としてセットすることが可能となっています。・UIViewAnimationWithBlocksカテゴリー
+ (void)animateWithDuration:(NSTimeInterval)duration delay: (NSTimeInterval)delay options:(UIViewAnimationOptions)options animations: (void (^)(void))animations completion:(void (^)(BOOL finished))completion; + (void)animateWithDuration:(NSTimeInterval)duration animations: (void (^)(void))animations completion:(void (^)(BOOL finished))completion; + (void)animateWithDuration:(NSTimeInterval)duration animations: (void (^)(void))animations; + (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations: (void (^)(void))animations completion:(void (^)(BOOL finished))completion; +(void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration: (NSTimeInterval)duration options:(UIViewAnimationOptions)options completion: (void (^)(BOOL finished))completion;
例えば、2つのUIImageViewを用意し、片方のalphaプロパティ値に0.0を代入し非表示としておきます。ボタンタップで両ビューの表示と非表示をアニメーション(3秒間)させながら切り換えるのには、iOS 3.0までは以下の様なソースコードを記述していました。
- (IBAction)switchDisplay // 通常処理(ブロックを利用しない) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:3.0]; imageView1.alpha=0.0; imageView2.alpha=1.0; [UIView commitAnimations]; }
ブロック処理を渡せるアニメーション用クラスメソッドを使うと、まったく同じ処理を簡潔に記述できます。この処理において切り換え方向を逆転させるには、現在のビューのalphaプロパティ値をチェックしておけばOKです。
- (IBAction)switchDisplay // ブロックを利用 { [UIView animateWithDuration:3.0 animations: ^{ imageView1.alpha=0.0; imageView2.alpha=1.0; }]; }
こうしたクラスメソッドには、アニメ終了時の処理をブロックとして記述できるタイプもあります。 以下は、ビデオキャプチャ時にスクリーン(flashView)を一瞬だけ光らせる処理(フラッシュ効果)をブロック処理アニメーションで実装した例です。キー値監視により、 isCapturingStillImageプロパティが変化した時に実行されます。アニメ終了時のブロック処理では、不要となったビュー自体を破棄していることが分かります。
// フラッシュ効果(capturingStillImageプロパティが変化した時呼ばれる) (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(void *)context { BOOL isCapturingStillImage; isCapturingStillImage=[[change objectForKey:NSKeyValueChangeNewKey] boolValue]; if( isCapturingStillImage ) { // フラッシュ用ビューを作成 flashView = [[UIView alloc] initWithFrame:[previewView frame]]; [flashView setBackgroundColor:[UIColor whiteColor]]; // 背景は白色 [flashView setAlpha:0.0]; // アルファ値はゼロ [[[self view] window] addSubview:flashView]; // ウィンドウに配置 [UIView animateWithDuration:0.4 animations: ^{ [flashView setAlpha:1.0]; // 透明->白(0.4秒) }]; } else { [UIView animateWithDuration:0.4 animations: ^{ [flashView setAlpha:0.0]; // 白->透明(0.4秒) } completion:^(BOOL finished) { [flashView removeFromSuperview]; flashView=nil; }]; } }
今回は、UIViewアニメーション機能についてさらに詳しく解説しました。次回は、いよいよ明示的アニメーションの詳細に進みます。その先には、キーフレームアニメーションのようなCore Animationスペシャルな機能が待っています。 -
暗黙的アニメーションは忘れましょう!
この記事は、2012年04月26日に掲載されました。前回は、Core Animationの特徴となっている「暗黙的アニメーション」と「明白的アニメーション」の違いを解説しました。 今回は、CATransactionクラスとUIView自身のアニメーション機能についてお話したいと思います。
Core Animationで言う所の暗黙的(Implicit)と 明示的(Explicit)アニメーションですが、両方ともあまり一般的ではない表現です。これを、自動と手動アニメーションという表現に変えてやれば、もう少し分かりやすいかもしれません。引き続き、もう少し詳しく暗黙的アニメーションを見てみます。CALayerのプロパティの変更は、トランザクション(一括処理)としてCATransactionクラスにより管理されており、必要であればアニメーション化されます。つまり暗黙的アニメーションの方では、こうしたトランザクションがメインスレッドに作成され、実行ループの次の反復時に自動的に実行されるわけです。前回紹介した以下の処理は、
drt=myLayer.frame; drt.origin.x+=100; myLayer.frame=drt;
実際には…
[CATransaction begin]; // トランザクション開始 drt=myLayer.frame; drt.origin.x+=100; myLayer.frame=drt; [CATransaction commit]; // トランザクション実行
といった具合に処理されていることになります。
Core Animationにより、クラスメソッドのbeginとcommitが実行ループに自動的に挿入されているわけです。このように、プロパティの変更をbeginとcommitで挟んでアニメーションを実行させる方法を、明示的なトランザクションと呼ぶようです(挾まないのは暗黙的なトランザクション)。いよいよ分かりにくい話になってきましたが(笑)、こうしなくてもアニメーションは実行されるので、あまり利用すべき状況を思い浮かべられません。メインスレッドではない別スレッドからCALayerのプロパティを変更することでアニメーションを実行したければ、明示的トランザク ションを使う必要がありそうです。
CATransactionクラスには、トランザクションを制御するための便利なクラスメソッドがありますので、それらを活用することも可能です。例えば、アニメーションの1サイクルの時間はデフォルトで1.0秒でしたが、以下のどちらかのメソッドを利用すれば、その長さを任意の時間(例では3.0秒)とすることが可能です(後者が便利)。
[CATransaction setValue:[NSNumber numberWithFloat:3.0] forKey:kCATransactionAnimationDuration]; [CATransaction setAnimationDuration:3.0];
また暗黙的なアニメーションを実行中に、CALAyerのプロパティ変更をアニメーションとして表示したくない場合には、以下のどちらかのクラスメソッドを、トランザクション処理の途中に入れてやります(後者が便利)。その後、setDisableActions:メソッドの引数にNOを渡せば、アニメーションの実行は再開されます。
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; [CATransaction setDisableActions:YES];
暗黙的なトランザクションでは、すべてのアニメーションが実行ループのあるタイミングで一斉に再生されることになります。これをもう少し複雑に制御し、複数アニメーションをうまくネスティングさせたい時なども、明示的なトランザクションを利用することができます。最も外側のトランザクションがコミットされた場合のみ、それぞれのアニメーションか再生されます。簡単な例を上げると以下の様なソースコードとなります。ただし、これをiOSのメインスレッドで実行すると通常の暗黙的トランザクションとまったく同じ処理がなされます(beginとcommitは無視される)。
[CATransaction begin]; // 外側のトランザクション [CATransaction setAnimationDuration:3.0]; drt=myLayer.frame; drt.origin.x+=100; myLayer.frame=drt; [CATransaction begin]; // 内側のトランザクション drt=myView.frame; drt.origin.y+=100; myView.frame=drt; [CATransaction commit]; // 内側のトランザクション [CATransaction commit]; // 外側のトランザクション
つまり、最初のX方向アニメションが終了するまで次のY方向アニメーションの実行を待機してくれるわけではないので、結局は同時にX,Y方向へ移動するアニメーションとなるわけです。複数の明示的アニメーションを強制的に同時実行したい時には、この方法ではなく、CAAnimationGroupクラス(CAAnimation.h)を使うと便利です。以下の例では、theAnimation1とtheAnimation2の両アニメーションを5.0秒のサイクルで同時に実行します。
gp=[CAAnimationGroup new]; // 先んじて2つのアニメーションは定義済み gp.duration=5.0; gp.animations=[NSArray arrayWithObjects:theAnimation1,theAnimation2,nil]; [theLayer addAnimation:gp forKey:nil];
とりあえずCATransactionの解説をしてみましたが、OS Xの場合はともかく、iOSの場合には、ここまでの話しは忘れてもらってもかまいません(暗黙的アニメーションも含めて)。何故かというと、iOSのUIViewクラスにはCATransactionと類似したアニメーション機能が用意されているからです。こうした機能はOS Xで使うNSViewには用意されていないためiOS独自の機能なのですが、CATransactionよりこちらを活用した方が便利で簡単なのです。以下が、UIViewクラスのUIViewAnimationカテゴリーに定義されているアニメーション関連のクラスメソッドです。
・UIViewAnimationカテゴリー
+ (void)beginAnimations:(NSString *)animationID context:(void *)context; + (void)commitAnimations; // アニメーション定義開始と終了 + (void)setAnimationDelegate:(id)delegate; // 開始と終了時に呼ばれるデリゲート + (void)setAnimationWillStartSelector:(SEL)selector; // 開始時に呼ばれる処理セット // - (void)animationWillStart:(NSString *)animationID context:(void *)context + (void)setAnimationDidStopSelector:(SEL)selector; // 終了時に呼ばれる処理セット // - (void)animationDidStop:(NSString *)animationID finished: (NSNumber *)finished context:(void *)context + (void)setAnimationDuration:(NSTimeInterval)duration; // 1サイクルの時間 + (void)setAnimationDelay:(NSTimeInterval)delay; // 遅らせる時間 + (void)setAnimationStartDate:(NSDate *)startDate; // 開始日付 + (void)setAnimationCurve:(UIViewAnimationCurve)curve; // 時間軸制御のカーブ種類 + (void)setAnimationRepeatCount:(float)repeatCount; // 繰り返し回数 + (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses; // 巻き戻しあり + (void)setAnimationBeginsFromCurrentState:(BOOL)fromCurrentState; // 4.0以前のみ + (void)setAnimationTransition:(UIViewAnimationTransition)transition forView:(UIView *)view cache:(BOOL)cache; // トランジション種類 typedef enum { // トランジションの種類 UIViewAnimationTransitionNone, UIViewAnimationTransitionFlipFromLeft, UIViewAnimationTransitionFlipFromRight, UIViewAnimationTransitionCurlUp, UIViewAnimationTransitionCurlDown, } UIViewAnimationTransition; + (void)setAnimationsEnabled:(BOOL)enabled; // アニメーション実行ON/OFF + (BOOL)areAnimationsEnabled; // アニメーシ実行ON/OFFの現在の状態
今回は、CATransactionクラスの仕組みと、UIView自身のアニメーション機能が存在していることについてお話しました。次回は、UIViewアニメーション機能についてさらに詳しく解説いたします。
-
暗黙的と明白的アニメーション
この記事は、2012年04月13日に掲載されました。前回は、OS XとiOSにおけるCore Animetionを利用する場合の仕組みの違いなどを調べてみました。今回は、Core Animationの特徴となっている「暗黙的アニメーション」と「明白的アニメーション」の違いを簡単に解説したいと思います。
OS Xの場合、一度ビュー(NSView)にCALayerを割り当ててしまえば、Objective-Cのドット構文を使うことで、CALayerのプロパティを変更して暗黙的アニメーション(Implicit)を実行することが可能となります。つまり、特別にアニメーション設定を行わなくても、CALayerに対するプロパティの設定変更(表示位置の移動など)をCore Animationが自動でアニメ化してくれるわけです。例えば、以下の様なソースコードを記述すれば、レイヤ表示が右方向へ100ピクセル移動するアニメーションが実行されます。
- (IBAction)doImplicitAnimation:(id)sender // OS Xでの暗黙的アニメーション { CALayer *myLayer; CGRect drt; myLayer=[myView layer]; // NSViewからCALayerを得る drt=myLayer.frame; drt.origin.x+=100; // レイヤの表示位置を右へ移動する myLayer.frame=drt; }
この時のアニメーションに有する時間(duration)はディフォルトとして1.0秒がセットされています。この値は実行時に変更することが可能です(次回解説の予定)。また、現バージョンのiOSで暗黙的アニメーションを行う場合には、CALayerのdelegateプロパティを設定しておくこと(nil以外のselfなど)が必要ですのでご注意ください。
delegateをセットする方法以外で暗黙的アニメーションが実行できないかと色々と試してみましたが、どうもダメでそうです。結局のところ、これが仕様かどうかは謎なのですが、iOSのUIViewについては暗黙的アニメーションを使わずとも別途アニメーション機能を備えていますので、そちらを使用した方が細かな制御が可能となります(こちらも次回解説の予定)。暗黙的アニメーションをソースコードで記述すると以下の様になります。
- (IBAction) doImplicitAnimation:(id)sender // iOSでの暗黙的アニメーション { CALayer *myLayer; CGRect drt; myLayer=myView.layer; myLayer.delegate=self; // これを設定しないと暗黙的アニメを行わない drt=myLayer.frame; drt.origin.x+=100; myLayer.frame=drt; }
iOSではCALayerの代わりにUIViewのプロパティを操作することでも実行可能です。
- (IBAction) doImplicitAnimation:(id)sender // UIViewのプロパティを変化させる { CALayer *myLayer; CGRect drt; myLayer=myView.layer; myLayer.delegate=self; // これを設定しないと暗黙的アニメをしない。 drt=myView.frame; drt.origin.x+=100; myView.frame=drt; // CALayerの代わりにUIViewのプロパティ変更でもOK }
OS Xの場合には、以下の様にNSViewのエリア内しかアニメーションは表示されませんが、iOSでのアニメーションは、オリジナルのビュー矩形領域を外れていてもスクリーン内ならどこでも表示されます。iOSのUIWindowクラスはUIViewを継承していますが、OS XのNSWindowクラスはNSViewを継承していませんので、こうなるのだと思われます(多分)。
Core Animatinoには、ソースコードによる細かな設定でアニメーションを制御するための明白的アニメーション(Explicit)も用意されています。こちらでは、アニメーションの表示時間、繰り返し回数、移動パス、長さや速度変化を指示するカーブの種類などを設定することが可能です。個々のアニメーションはそれぞれが別スレッドとして実行されますので、その終了や開始はデリゲートメソッドで認識する仕組みとなっています。
以下は、CALayerの矩形サイズが縮小(縦横とも100ピクセル)される動きをアニメーション化したメソッドです。animationWithKeyPath:メソッドで渡している文字列(この例ではbounds)がアニメーション対象として指定するプロパティ名となります。 その後CABasicAnimationオブジェクトのfromValueプロパティに開始値(NSValue)を、toValueプロパティに終了値(NSValue)を代入してからアニメーションの対象とするCALayerオブジェクトに対しaddAnimation:forKey:メッセージを送ります。この場合forKey:に渡す引数はnilでもかまいません。
- (IBAction)doExplicitAnimation:(id)sender // OS Xの場合の明白的アニメーション { CABasicAnimation *anime; CALayer *layer; CGRect srt,drt; layer=[myNSView layer]; // NSViewから画像レイヤ(CALayer)を得る anime=[CABasicAnimation animationWithKeyPath:@"bounds"]; // アニメション作成 anime.repeatCount=5; // 拡大アニメを5回繰り返す anime.duration=2.0; // 1回のサイクルに2秒かける anime.autoreverses=YES; // 自動で逆向きに再生(リバース再生) srt=layer.bounds; // 開始矩形サイズ drt=CGRectInset( srt,50,50); // 終了矩形サイズ(縮小) anime.fromValue=[NSValue valueWithRect:NSRectFromCGRect( srt )]; anime.toValue=[NSValue valueWithRect:NSRectFromCGRect( drt )]; [layer addAnimation:anime forKey:@"boundsAnimation"]; // アニメションを実行 }
- (IBAction)doExplicitAnimation:(id)sender // iOSの場合の明白的アニメーション { CABasicAnimation *anime; CALayer *layer; CGRect srt,drt; layer=myUIView.layer; // UIViewから画像レイヤ(CALayer)を得る anime=[CABasicAnimation animationWithKeyPath:@"bounds"]; // アニメション作成 anime.repeatCount=5; // 拡大アニメを5回繰り返す anime.duration=2.0; // 1回のサイクルに2秒かける anime.autoreverses=YES; // 自動で逆向きに再生(リバース再生) srt=layer.bounds; // 開始矩形サイズ drt=CGRectInset( srt,50,50); // 終了矩形サイズ(縮小) anime.fromValue=[NSValue valueWithCGRect:srt]; // iOSはvalueWithCGRectを使用可 anime.toValue=[NSValue valueWithCGRect:drt]; [layer addAnimation:anime forKey:@"boundsAnimation"]; // アニメション実行 }
注意点は、fromValueとtoValueプロパティに渡す引数は構造体や数値そのものではなくて、それらをラップした「オブジェクト」だということです。上の例の場合、iOSでプロパティに渡すのはCGRect構造体ですが、OS XではNSRect構造体ですのでご注意ください。CGRect構造体はvalueWithCGRect:メソッドでNSValueオブジェクトにラップしてからプロパティに代入します。各構造体からNSValueへの変換メソッドは、ヘッダーファイルのUIGeometory.hやCATransform3D.hに定義されていますので参照してみてください。
今回は、Core Animationの特徴となっている「暗黙的アニメーション」と「明白的アニメーション」の違いを簡単に解説しました。次回は、CATransactionクラスとUIView自身のアニメーション機能についてお話したいと思います。
-
UIViewに割り当てられたCALayer
この記事は、2012年03月27日に掲載されました。前回は、Core Animationでアニメーションの対象となるCALayerのサブクラスの種類について解説しました。今回は、OS XとiOSにおけるCore Animetionを利用する場合の仕組みの違いなどを調べてみます。
まずはいつものことですが、Core Animationフレームワークに関する技術ドキュメントを入手し、それを読み込んでフレームワークの使い方を習得する必要があります。Core AnimationはiOSとOS Xで利用できますので、それぞれの技術リソースは、別々の「Developer Library」サイトから入手します。ただし、サンプルソースコード以外はほとんど共通です。最近では(Core Animationに限ったことではなく)両OSに共通の技術資料が多くなってきているのが、これからの未来を暗示しているようで興味深い傾向だと思います(笑)。
iOS Developer Libraryサイトに登録されているCore Animationに関するリソースは、そんなに多くはありません。また、登録されているサンプルソースコードもわずかです。ライブラリでの検索では「QuartzCore」と入力して絞り込みをしてください。現在、何故かサンプルソースコードの検索結果にQuartz Composer関連が含まれていますが、こちらのフレームワークは未だiOSではサポートされていません。Apple側の登録ミスだと思われますので、ご注意ください。 iOS版のCore Animationについては、以下の4つを参照することになります。フレームワークのリファレンス(クラスやメソッド)は「Core Animation Reference Collection」にまとめられています。
iOS関連サイト:http://developer.apple.com/library/ios/navigation/
「Core Animation Cookbook」(Core Animationを利用した簡単な例題 )
「Core Animation Programming Guide」(Core Animationの利用方法の基本)
「Animation Types and Timing Programming Guide」(種類やタイミング)
「Core Animation Reference Collection」(Core Animation APIリファレンス)iOSアプリでの使用方法は「Core Animation Programming Guide」を参照することになります。このドキュメントと「Animation Types and Timing Programming Guide」に関しては、以下のサイトに日本語訳版が登録されていますので、そちらを参照すべきでしょう。
和訳サイト:https://developer.apple.com/jp/devcenter/ios/library/japanese.html
「Core Animation プログラミングガイド」
「アニメーションのタイプとタイミング」iOSと比較して、Mac OS X Developer Libraryサイトに登録されている技術リソースは多岐にわたります。サンプルソースコードも沢山登録されていますので、実際にiOSで利用する前にいくつか試してみることをお奨めします。OS X版のCore Animationに関する技術ドキュメントについては、以下の2つが追加されています。
OS X関連サイト:http://developer.apple.com/library/mac/navigation/
「Animation Overview」(アニメーション技術一般とその実現方法の解説)
「Core Animation Overview」(Core Animation導入についての解説 )前回もお話しましたが、Core Animationのによるアニメーション処理は、NSViewやUIViewが所有するCALayerクラスやそのサブクラスを対象として実行されます。そして、実際にアニメーションをコントロールするのは、CAAnimationクラスとそのサブクラスです。以下が「Core Animation Programming Guide」ドキュメントに掲載されているCore Animationの各クラスの構成図です(少し内容が古いですが…)。
赤丸で囲まれた「CAEAGLLayer」のみが、OpenGL ESで利用するために用意されているiOS独自のクラスです。青丸で囲まれた複数クラスは、OS X独自のクラスであり、iOSでは実装されていません。またこの構成図には記載がありませんが、現バージョンのiOSやOS Xでは、 前回取り上げたCALayerのサブクラスである「CAEmitterLayer」「CAGradientLayer」「CAShapeLayer」「CAReplicatorLayer」などが利用可能です。iOSでは、これらに加えて「AVPlayerLayer」や「CATransformLayer」なども追加されています。CALayerクラスはアニメーション表示をする画像や図形などのコンテンツ、表示状態、矩形枠などを保持しています。また親となるレイヤ上に複数の子レイヤを配置(レイアウト)することも可能です。そうした仕組み自体は、UIViewやNSViewと似ていますが、実はこのCALayerについてはiOSとOS Xではシステムによる実装形態が違い、iOSの方が後発のためかモダンな実装となっています。
UIKitのUIViewにはデフォルトでCALayerオブジェクトが割り当てられていますが、OS XのAppKitのNSViewでは割り当てられていません。そのため、OS Xではアニメーションを実行する前に、まずソースコードでCALayerオブジェクトを作成し、その後対象となるNSViewブジェクトへCALayerオブジェクトを渡す必要があります。
以下は、OS Xで背景用と画像用のCALayerを2つ作成しておいて、背景用のCALayerにもうひとつの画像用CALayerをレイアウトした後、対象となるNSViewにセットしているソースコード例です。
- (void)awakeFromNib // OS XでCALayerを作成する場合 { CALayer *imageLayer; CALayer *backLayer; NSBitmapImageRep *bitmap; CGImageRef image; CGColorRef color; backLayer=[CALayer layer]; // 背景用のレイヤ作成 imageLayer=[CALayer layer]; // 画像用のレイヤ作成 imageLayer.frame=CGRectInset( [myView bounds],20,20 ); // 画像表示枠 bitmap=[NSBitmapImageRep imageRepWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"Image" ofType:@"jpg"]]; image=[bitmap CGImage]; // JPEGファイルから画像の読み込み imageLayer.contents=(__bridge id)image; // コンテンツをセット(ARC用) color=CGColorCreateGenericRGB( 0.0,1.0,0.0,1.0 ); // 背景は緑色 backLayer.backgroundColor=color; CGColorRelease( color ); [backLayer addSublayer:imageLayer]; // 背景レイヤのサブレイヤにする [myView setLayer:backLayer]; // ビューにレイヤを設定 [myView setWantsLayer:YES]; // ビューのレイヤを有効にする }
上記と比較して、次のiOS用ソースコードでは、背景カラーについては、UIViewに用意されているCALayerオブジェクト(背景用)をlayerプロパティから得ることで設定しています。- (void)viewDidLoad // iOSでCALayerを作成する場合 { CALayer *imageLayer; CALayer *backLayer; UIImage *image; imageLayer=[CALayer layer]; // 画像用のレイヤ作成 imageLayer.frame=CGRectInset( myView.bounds,20,20 ); // 画像表示枠 image=[UIImage imageNamed:@"image.jpg"]; // JPEGファイルを読み込む imageLayer.contents=(id)image.CGImage; // コンテンツをセットする backLayer=myView.layer; // 背景用のレイヤはUIViewから得る backLayer.backgroundColor=[UIColor greenColor].CGColor; // 背景は緑色 [backLayer addSublayer:imageLayer]; // 背景レイヤのサブレイヤにする }
一度UIViewやNSViewにCALayerを割り当ててしまえば、Objective-Cのドット構文を使うことでCALayerの各種プロパティ値を変更できます。CALayerのプロパティとしては以下の様な種類が用意されています。frame、bounds、position、anchorPoint、anchorPointZ、cornerRadius、transform、zPosition、backgroundColor、backgroundFilters、contents、mask、contentsGravity、sublayers、masksToBounds、borderColor、opacity、sublayerTransform、borderWidth、filters、shadowColor、shadowOffset、shadowOpacity、shadowRadius、compositingFilter
今回は、OS XとiOSにおけるCore Animetionを利用する場合の仕組みの違いなどを調べてみました。次回は、Core Animationの特徴となっている「暗黙的アニメーション」と「明白的アニメーション」の違いを解説したいと思います。
-
CALayerのサブクラスを調べる
この記事は、2012年03月21日に掲載されました。前回は、 認識しておいた方が良いCore Imageに関する注意点について解説しました。今回からは、本格的にCore Animationフレームワークや、そこで利用されているCALayerクラスの話しに突入したいと思います。
Core Animationのによるアニメーション処理は、NSViewやUIViewが所有するCALayerクラスを対象として行われます。Apple社の説明では、CALayerはモデルクラスなのだそうですが、ちょっと戸惑うクラス分けです(笑)。AppKit(OS X)のNSViewでは、CALayerを新規作成して追加する必要がありますが、UIKit(iOS)のUIViewでは、それを作成した時点で、既にCALayerが割り当てられています。
CALayerには目的に応じて幾つものサブクラスが用意されており、それぞれに特化したコンテンツの表示や、Core Animationによる独自のアニメーションを実行することが可能となっています。今回は、こうしたCALayerのサブクラスの種類を確認してみたいと思います。CALayerのサブクラスの実装の状況はOS XとiOSで若干異なりますが、iOS 5では、以下のサブクラスを使用することができます。
● CAShapeLayer
CAShapeLayerクラスは、Core Graphicsのパス図形を表示するよう特化したサブクラスです。パス図形を表示するためには、pathプロパティにCGPathRefを設定します。このレイヤにはパス図形に関する幾つかのプロパティを設定することができますが、その内容は、Core Graphics APIで設定できるパス図形の各種設定(幅、カラー、角処理、破線指定など)とほぼ一致しています。
● CAGradientLayer
CAGradientLayerクラスは、レイヤの矩形領域内にグラデーションを描画する目的に特化したサブクラスです。どんなカラーからどんなカラーへとグラデーションを変化させて行くのかの情報は、colorsプロパティにCGColorRefの配列として保存しておきます。また、各色の変化の開始位置はlocationsプロパティにNSNumberとして保存します(指定しないと均等に展開)。グラデーションの開始位置と終了位置はstartPointとendPointプロパティに設定します。locatonsプロパティも含めて、どちらも(0.0~1.0)に正規化された座標値です。
●CAReplicatorLayer
CAReplicatorLayerクラスは、自分自身に配置したサブレイヤをコピーして繰り返し表示させる機能を持っています。繰り返しの個数はinstanceCountプロパティで設定し、同時に繰り返し表示するサブレイヤの座標値、カラーなどを徐々に変化させるためのプロパティが用意されています。instanceDelayプロパティは、サブレイヤ全体に対して何らかのアニメーションを実行する場合、展開されたレイヤに対しアニメションの開始時間を少しずつ遅らせるために設定します。 また、preservesDepthプロパティをYESにすることで2D画面を擬似的に3次元に展開した表示も可能となります。
● CATiledLayer
CATiledLayerクラスは大きく複雑な画像を段階的(複数タイルに分けて)に表示する場合に利用します。レイヤの表示時、このクラスを継承したサブクラスのdrawRect:メソッドはタイル数だけ呼ばれます。その時の描画コンテキスト(CGContextRef)の座標変換マトリックス(CGAffineTransform)のスケーリング要素(構造体メンバ)には、 levelsOfDetailプロパティとlevelsOfDetailBiasプロパティで指示したスケール値が入っていますので、それを参照し適切な画像データを選択する処理が可能となります。例えば、スケール値が大きければより高解像度の画像を用意しておく等です。
例えば、 levelsOfDetail=4とするとスケール値「CGContextGetCTM(context).a」が4段階で変化するよう指示したことになります。この場合、levelsOfDetailBias=0(バイスなし)であれば、スケール値は1.0、0.5、0.25、0.125の4段階変化します。また、levelsOfDetailBias=4(バイス4)であれば4段階大きい方へシフトして、スケール値は16、8、4、2の4段階変化します。小さい方へシフトする場合には、levelsOfDetailBiasにマイナスの値を代入します。
● CAEmitterLayer
CAEmitterLayerクラスは、小さなレンダリング画像パタンを多数集めてパーティクル(粒子)効果を表現するために利用するレイヤです。パーティクルで表現する現象の例としては、 炎、煙、雲、雨、花火など多数あります。CAEmitterLayerが多数のパーティクルをまとめて表示しますが、各パーティクルはCAEmitterCellクラスの方で定義し、そちらに用意されている多数のプロパティで、その特徴付けを行います。
● CAScrollLayer
CAScrollLayerクラスは、レイヤの部分的な表示を簡素化するために用意されています。このクラスのサブクラスを作成し、用意されているスクロール用メソッドでレイヤのスクロールを行うと、drawRect:メソッドが呼ばれます。その中で、スクロールにより変化したvisibleRectプロパティ(CGRect)を参照して適切な部分描画を行います。ちなみに、このレイヤにはキーボード処理やマウスイベント処理、表示可能なスクロールバーなどのUI関連のメソッドは提供されていません。
● CATransformLayer
CATransformLayerクラスは、正確な3D座標で配置されたサブレイヤの階層を維持しており、これらの(Z=0)面への投影(描画)は行いません。また、通常の2D表示でサポートされるフィルタ処理などの幾つかの機能は働きません。このレイヤに特別なプロパティはありません。
● CAEAGLLayer
CAEAGLLayerクラスは、OpenGL ESの描画環境を提供します。OpenGL表示を行うためのビューを作成するには、以下の様にUIViewを継承した後layerClassクラスメソッドをオーバライドします。 このCALayerのサブクラスはOS Xには実装されていません。このサブクラスが定義されているヘッダファイルはOpenGLES.hです。
+ (Class) layerClass { return [CAEAGLLayer class]; }
● AVPlayerLayer
AVPlayerLayerクラスは、映像(ムービー)再生の出力結果を表示するために用意されているサブレイヤです。こちらはAVFoundationフレームワークに属しており、映像再生のメソッドと一緒に利用されます。このサブクラスが定義されているヘッダファイルはAVPlayerLayer.hです。
残念ながら、こうしたCALayerのサブクラスを用いたApple社のiOS用サンプルソースコードはほとんどありません。しかし、OS X側にはCALayerサブクラスを用いたサンプルソースコードが多数用意さいれていますので、ぜひ一度試してみてください(以下参照)。
「Fire」CAEmitterLayerを利用した炎と煙のパーティクルアニメーション
「Fireworks」CAEmitterLayerを利用した花火のパーティクルアニメーション
「ReplicatorDemo」CAReplicatorLayerを利用したカラーチャートアニメーション
「Gradients」CAGradientLayerを利用したグラデーション描画デモ
「CALayerEssentials」色々なサブレイヤのデモ(iOS実行不可もあるので注意)
「CocoaSlides」登録された画像ファイルでスライドショーを実行するデモ
「CoreAnimationKioskStyleMenu」CoreAnimationのキオスクモードでの利用
「CoreAnimationText」CAShapeLayerを利用したテキスト表示のデモ
「ImageTransition」CATransitionとCIFilterを使ったトランジションの実行
「LayerFlip」CALayerの表示を左右反転させるデモ
「NineSlice」画像の一部分を拡大表示するデモ今回はCore Animationでアニメーションの対象となるCALayerのサブクラスの種類について解説しました。次回は、OS XとiOSにおけるCore Animetionを利用する場合の仕組みの違いなどを調べてみます。
-
CIImage発、CGImageRef経由、UIImage行き
この記事は、2012年02月27日に掲載されました。前回は、 Lion(OS X 10.7)とiOS 5から追加されたCore Imageによる画質改善と顔認識機能を紹介しました。今回は、Core Animationフレームワークや、そこで利用されているCALayerクラスの話しに移りたいと思います。
と思っていましたが、その前にどうしても取り上げておいた方が良いCore Imageに関する注意点がありますので、今回は予定を変更してそれについて解説を致します。話の対象は、Core Animationのフィルタ処理の対象となる画像「CIImage」についてです。
Apple社が提供しているiOS版サンプルソースコードの中で、Core Imageに関するサンプルはたったひとつしかありません。それが「PocketCoreImage」です。このiOSアプリでは、UIViewのサブクラスであるFilteredImageViewクラスを用意し、そこにCore Imageによるフィルタ処理の結果(CIImage)を描画しています。描画処理を行っているFilteredImageViewクラスのdrawRect:メソッドは、以下の様に記載されています。
- (void)drawRect:(CGRect)rect // オーバライドされた描画用メソッド { [super drawRect:rect]; if( !_filteredImage ) return; CGRect innerBounds=CGRectMake( 5,5,self.bounds.size.width-10, self.bounds.size.height-10 ); [[UIImage imageWithCIImage:_filteredImage] drawInRect:innerBounds]; }
この処理内容内容を見ると、UIImageクラスにはimageWithCIImage:メソッド(iOS 5から利用可能)が用意されており、それによってCIImageをUIImageに変換可能なようです。このサンプルでも、そのメソッドを利用してdrawInRect:でフィルタ処理の結果(CIImage)を現在のビューの表示コンテキストに描画しています。これを見たプログラマーなら「CIImageからUIImageを作るのは簡単なのだから、いちいちFilteredImageViewクラスなど作成せずとも、変換結果をUIImageViewにセットすれば確認できるぞ!」思うはずです。つまり以下の様な記述をすればOKで、フィルタ処理の結果はUIImageViewですぐに確認できるはずです。
IBOutlet UIImageView *imageView; // 結果表示用UIImageビュー imageView.image=[UIImage imageWithCIImage:_filteredImage];
ところが、この処理を実行しても何故だかUIImageViewに画像は表示されません?以下の様に、一度CIImageをCGImageRef(Core Graphicsネイティブ画像)に変換した後に、されにUIImageに変換することで大丈夫となります。
CIImage *outputImage; // フィルタ処理後のCIImage画像 - (void)diplayImage // UIImageViewにフィルタ処理の結果を描画する { CGImageRef cgimage; CIContext *context; CGRect drt; drt=[outputImage extent]; // 画像フレーム(矩形枠) context=[CIContext contextWithOptions:nil]; // CIContext作成 cgimage=[context createCGImage:outputImage fromRect:drt]; // CGImageRefへ imageView.image=[UIImage imageWithCGImage:cgimage]; // UIImageへ CGImageRelease( cgimage ); }
この問題は、フィルタ処理をした画像を写真ライブラリに保存しようとした時にも発生します。普通に考えれば、UIImageWriteToSavedPhotosAlbum()ルーチンを使い以下の様に実行すればライブラリに保存されるはずです。
UIImageWriteToSavedPhotosAlbum( [UIImage imageWithCIImage:outputImage], self,@selector(image:didFinishSavingWithError:contextInfo:),nil );
ところが不思議なことに、これを実行するとエラーが生じて写真ライブラリへの保存に失敗します。やはりUIImageViewの場合と同様に、CIImageから一度CGImageRefを経由してUIImageを作成すると問題なく成功します。
CGImageRef cgimage; CIContext *context; UIImage *image; CGRect drt; drt=[outputImage extent]; // 画像フレーム(矩形枠) context=[CIContext contextWithOptions:nil]; // CIContext作成 cgimage=[context createCGImage:outputImage fromRect:drt]; // CGImageRefを作成 image=[UIImage imageWithCGImage:cgimage]; // CGImageRefからUIImageを作成 if( image ) UIImageWriteToSavedPhotosAlbum( image,self, @selector(image:didFinishSavingWithError:contextInfo:),nil);
ちなみに、UIGraphicsBeginImageContext()とUIGraphicsEndImageContext()を利用した別手段もあります。オフスクリーンコンテキストで新規UIImageを作成しています。
UIImage *image; CGRect drt; drt=[outputImage extent]; // 画像フレーム(矩形枠) UIGraphicsBeginImageContext( drt.size ); // オフスクリーンContext作成 [[UIImage imageWithCIImage:outputImage] drawInRect:drt]; // 画像の描画 image=UIGraphicsGetImageFromCurrentImageContext(); // 新規にUIImage作成 UIGraphicsEndImageContext(); if( image ) UIImageWriteToSavedPhotosAlbum( image,self, @selector(image:didFinishSavingWithError:contextInfo:),nil);
ただし、既にUIUmageに変換してUIImageViewへセットしてあるとすれば、以下の処理のみでOKです。こうなれば簡単ですね。
if( imageView.image ) UIImageWriteToSavedPhotosAlbum( imageView.image,self, @selector(image:didFinishSavingWithError:contextInfo:),nil);
iOSが4.0以上なら、ALAssetsLibraryクラスを利用してCGImageRefをそのまま写真ライブラリへ保存する手もあります。こちらは、画像のファイル保存がバックグランドで処理されますので、対象デバイスがiOS 4.0以上であれば一番推奨される一手でしょう。処理に使用されるメモリ容量も、この方法が一番少ないかもしれません(多分)。
CIContext *context; ALAssetsLibrary *library; CGImageRef cgimage; CGRect drt; drt=[outputImage extent]; // 画像フレーム(矩形枠) context=[CIContext contextWithOptions:nil]; // CIContext作成 cgimage=[context createCGImage:outputImage fromRect:drt]; // CGImageRefを作成 library=[ALAssetsLibrary new]; // アセットライブラリ作成 [library writeImageToSavedPhotosAlbum:cgimage metadata:[outputImage properties] completionBlock:^(NSURL *assetURL, NSError *error) // 終了時のブロック処理 { CGImageRelease(cgimage); }];
詳しくは調べていませんが、どうもdrawInRect:のみ(描画系メソッドのみ?)CIImageからダイレクト変換したUIImageに対応している雰囲気で、それ以外の処理はCGImageRefを経由したUIImageでないと対処できないようです。CIImageはOpenGL ESのテクスチャ形式の画像(GPU経由アクセスのビデオメモリ)だと思われますので、その辺りでUIKitからの操作に色々と制限が付いているのかもしれません。これがバグなのか?仕様なのか?判断はできませんが(多分仕様)、iOSのバージョンアップにより改善されることをの望みたいと思います。今回は予定を変更し、取り上げておいた方が良いCore Imageに関する注意点について解説しました。次回からは、本当にCore Animationフレームワークや、そこで利用されているCALayerクラスの話しに突入したいと思います。
-
こんな機能もあったのか!
この記事は、2012年02月16日に掲載されました。前回は、iOS 5版のCore Imageについての注意点について解説しました。今回は、Core Imageの最新機能を取り上げ、そこからCALayerやCore Animationへと話題を広げて行きたいと思います。
今回は、 Lion(OS X 10.7)とiOS 5から追加された「画質改善」と「顔認識」機能を紹介します。Apple社のスペシャルイベントなどで紹介されていたこの機能、いったどこに存在するのか疑問だったのですが、何とCore Imageに実装されていたわけです。最初の「画質改善」は、特定のフィルタで処理を行うのではなく、入力画像をCore Imageに解析させて、より見栄えのよい結果へと自動で画質を改善(エンハンス)させます。例えば、露出不足やコントラスト不足や肌色の彩度異常などを自動で修正します。加えて、特別な機能として「赤目」画像を修正することもできます。WWDC2001のセッションによると、このエンハンス処理に利用されるフィルタは以下の5種類だそうです。
この中のCIRecEyeCorrectionとCIFceBalanceは単独フィルタとしてはカテゴリに登録されておらず、リファレンスにも使用方法の掲載がありません(謎)。 画質改善機能を利用するのには、CIImageクラスに定義されている以下のどちらかのメソッドを使います。
- (NSArray *)autoAdjustmentFilters - (NSArray *)autoAdjustmentFiltersWithOptions:(NSDictionary *)dict
以下が、自動で入力画像の画質改善を実行するメソッドです。非常に簡単に処理を実装できることが分かります。
- (CIImage *)doEnhance:(CIImage*)inputimage // 画質改善処理 { CIImage *image; NSArray *array; if( image=inputimage ) { // 自動エンハンス処理 array=[image autoAdjustmentFiltersWithOptions:nil]; for( CIFilter *filter in array ) // 配列個数分ループ { // 対象フィルタに順次入力画像を渡す [filter setValue:image forKey:@"inputImage"]; image=filter.outputImage; } } return image; // 結果画像を返す }
autoAdjustmentFiltersWithOptions:メソッドには、NSDictionary経由でオプションを指示できます。オプション無しはnilを代入すればOKです。 例えば、赤目処理だけを実行したい場合には、kCIImageAutoAdjustEnhanceキーにfalse値を、赤目以外の処理だけを実行したい場合には、kCIImageAutoAdjustRedEyeキーにfalse値を代入して渡します。
続いて「顔認識」の紹介です。この機能を利用するのもとても簡単です。 まずは、オプション(NSDictonary)を設定し、以下のクラスメソッドでCIDetectorクラスのインスタンスを作成しておきます。
- (CIDetector *)detectorOfType:(NSString*)type context:(CIContext *)context options:(NSDictionary *)options - (void)viewDidLoad // CIDetectorインスタンスを作成しておく { NSDictionary *options; [super viewDidLoad]; // オプション値はCIDetectorAccuracyHighでキーはCIDetectorAccuracy options=[[NSDictionary alloc] initWithObjectsAndKeys: CIDetectorAccuracyHigh,CIDetectorAccuracy,nil]; faceDetector=[[CIDetector detectorOfType:CIDetectorTypeFace context:nil options:options]retain]; // CIDetectorインスタンス作成 }
オプションでは、CIDetectorAccuracyキーにより認識率を優先するのか?処理速度を優先するのか?を選択できます。例えばCIDetectorAccuracyHigh値を代入しておけば、認識率を上げることを優先して処理が行われます。ちなみに、現バージョンがサポートしている顔認識のタイプはCIDetectorTypeFace(人間の顔)だけとなります(笑)。
顔認識の実行は、以下のメソッドで行います。結果はCIFeatureオブジェクトの配列で返されます。つまり、写真に写っているのは複数人でもOKということになります(人数に限界があるかどうかは未確認)。この時に渡すオプションは画像の回転情報です。
- (NSArray *)featuresInImage:(CIImage *)image options:(NSDictionary *)options
CIFeatureクラスには、顔枠(フレーム)を示すboundsプロパティ(CGRect)や、両目の位置を示すleftEyePositionとrightEyePositionプロパティ(CGPoint)、そして口の位置を示すmouthPositionプロパティ(CGPoint)が保存されます。顔認識が成功したら、これらのプロパティを参照して各エリアに対して適切な処理を行います。
以下のdoFaceDitectメソッドでは、インスタンス変数として用意しておいたUIImageViewのimageViewに画像を保存しておき、顔認識を実行後に顔フレームや両目、口の位置に図形を描いてからUIImageに変換してimageViewに保存し直しています。
- (void)doFaceDitect // 顔認識処理を実行して各ポイントを描画する { NSArray *features; CIImage *cimage; CGRect drt,prt; CGSize size; CGContextRef ctx; CGPoint pt; // UIImageからCIImgaeを作る if( cimage=[[CIImage alloc] initWithCGImage:imageView.image.CGImage options:nil] ) { features=[faceDetector featuresInImage:cimage options:nil]; // 顔認識を実行 [cimage release]; if( features ) { size=imageView.image.size; UIGraphicsBeginImageContext( size ); // UIImage作成開始 ctx=UIGraphicsGetCurrentContext(); CGContextTranslateCTM( ctx,0.0,size.height ); CGContextScaleCTM( ctx,1.0,-1.0 ); // 座標を上下ひっくり返す drt=CGRectMake( 0,0,size.width,size.height ); // 描画フレーム CGContextDrawImage( ctx,drt,imageView.image.CGImage); // CGImage描画 CGContextSetLineWidth( ctx,3.0 ); // ライン幅設定 for( CIFaceFeature *ff in features ) // 認識顔の個数分ループ { // 顔枠の表示色は赤 目の表示色は青 口の表示色は緑とする CGContextSetStrokeColorWithColor( ctx,[UIColor redColor].CGColor ); CGContextStrokeRect( ctx,ff.bounds ); // 顔認識の矩形枠を描画 CGContextSetStrokeColorWithColor( ctx,[UIColor blueColor].CGColor ); if( ff.hasLeftEyePosition ) // 左目位置が認識されているか? { pt=ff.leftEyePosition; prt=CGRectMake( pt.x-4,pt.y-4,8,8 ); CGContextStrokeEllipseInRect( ctx,prt ); // 右目に円を描画 } if( ff.hasRightEyePosition ) // 右目位置が認識されているか? { pt=ff.rightEyePosition; prt=CGRectMake( pt.x-4,pt.y-4,8,8 ); CGContextStrokeEllipseInRect( ctx,prt ); // 左目に円を描画 } CGContextSetStrokeColorWithColor( ctx,[UIColor greenColor].CGColor); if( ff.hasMouthPosition ) // 口の位置が認識されているか? { pt=ff.mouthPosition; prt=CGRectMake( pt.x-4,pt.y-4,8,8 ); CGContextStrokeEllipseInRect( ctx,prt ); // 口に円を描画 } } // 作成されたUIImage画像をUIImageViewへ登録 imageView.image=UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // UIImage作成終了 } } }
写真の顔は少し傾いていたり横を向いていたりしても認識されますが、写真の方向には注意してください(90度回転など)。写真自体の方向が正しくないと、顔が正面を向いていても認識はされませんのでご注意ください。
今回は、 Lion(OS X 10.7)とiOS 5から追加されたCore Imageによる画質改善と顔認識機能を紹介しました。次回からは、Core Animationフレームワークや、そこで利用されているCALayerクラスの話しに移りたいと思います。
-
もっとビルトイン・フィルタを!
この記事は、2012年01月30日に掲載されました。前回は、 Xcode 4での開発途中でプロジェクト名やアプリ名を変更する操作について解説しました。 今回は、iOS 5の登場でiOSアプリ上で利用可能になった「Core Image」の話しに戻りたいと思います。
iOS 5で実装されたCore Imageの機能についても、OS Xの場合と同様に「Developer Library」サイトに登録されている以下のドキュメントで確認することができます。残念ながら、日本語に訳されているドキュメントはありません。
Developer Libraryサイト:http://developer.apple.com/library/ios/navigation/
「Core Image Programming Guide」(Core Imageプログラミングの詳細な解説)
「Core Image Filter Reference」(Core Image搭載のフィルタのリファレンス)
「Core Image Reference Collection」(Core Imageの全APIのリファレンス)注意すべきは、iOS版の「Core Image Programming Guide」の最初の方には次のような記載があることです。どうも、このドキュメントはOS X用の資料の転用であり、完全にiOS 5に対応させた形で書かれてはいないようです。つまり、ここに書かれていることがすべてiOS側で実現できるかどうかは定かではないということです。
iOS版のCore Imageについて認識しておく点は、iOS版はOS X版のサブセットだと言うことです。例えば、OS Xの場合には「Built-in Filters」として、開発者の独自フィルタ・モジュール(以前に解説したImage Unit)をプラグインとして提供できますが、その機能は iOSではサポートされていません。基本的に、どんな機能が省略されているかについては、ヘッダファイルのCoreImeg.hを見ると理解できます。このヘッダファイルには、Core Imageで使える機能をクラス別に分けたヘッダファイルとして定義されています。 以下がその内容です。
#import <Foundation/Foundation.h> #import <CoreImage/CIColor.h> #import <CoreImage/CIContext.h> #import <CoreImage/CIFilter.h> #if !TARGET_OS_IPHONE #import <CoreImage/CIFilterGenerator.h> // iOSでは使用不可 #import <CoreImage/CIFilterShape.h> // iOSでは使用不可 #endif #import <CoreImage/CIImage.h> #if !TARGET_OS_IPHONE #import <CoreImage/CIImageAccumulator.h> // iOSでは使用不可 #import <CoreImage/CIImageProvider.h> // iOSでは使用不可 #import <CoreImage/CIKernel.h> // iOSでは使用不可 #import <CoreImage/CIPlugIn.h> // iOSでは使用不可 #import <CoreImage/CIRAWFilter.h> // iOSでは使用不可 #import <CoreImage/CISampler.h> // iOSでは使用不可 #endif #import <CoreImage/CIVector.h> #import <CoreImage/CIDetector.h> #import <CoreImage/CIFeature.h>
ここで「#if !TARGET_OS_IPHONE」によりiOS環境で除外されている定義が、iOSでは利用できないクラスやメソッドです。例えば、先ほど紹介したフィルタプラグインに関係するクラスやメソッドは、CIPlugin.hやCIKernel.hなどに定義されていますので、そうした機能はiOSでは利用できないことが理解できます。
もうひとつ参考にしたい資料は「WWDC 2011セッションビデオ」です。ここでの話の内容はプレビュー版iOS 5が対象なのですが、非常に分かりやすい解説とデモ(いくらかのソースコード)が紹介されていますので、Apple社とデベロッパー契約を結んでいる方は、ぜひ一度視聴されることをお勧めします。セッション名は「Using Core Image on iOS & Mac OS X」です。
WWDC2011サイト:http://developer.apple.com/videos/wwdc/2011/index.php
サイトのタイトルの一番左側の三角をクリックすると、そのセッションの簡単な説明が表示され、Web経由やiTunes経由でのビデオ映像の視聴が可能です。ただし、セッションは1時間近い長さなので、先んじてセッションのスライド資料(PDF)をダウンロードして目を通しておくのが良いでしょう。
このセッションを見ると分かるのですが、OS X版とiOS版のCore Imageにおける最大の差異は、用意されているビルトイン・フィルタの数が異なることです。 OS Xには150以上ものフィルタが用意されていますが、WWDC2011のセッションでは「ビルトインフィルタは16」と紹介されています(威張ることでは無い…)。ただし、iOS 5の正式版ではその数が48に増えているようです。 とは言っても、筆者が直ちに利用してみたいと考えていたトランジション系のフィルタや、ぼかしやシャープネスといった利用頻度の高いフィルタが用意されていません(涙)。
そこで、利用可能なフィルタの種類を確認したいわけですが、ドキュメントの「Core Image Filter Reference」もプレビュー版のiOS 5が対象なようでして、正確な種類が把握できません。ヘッダファイル(CIFilter.h)に一覧でも記載されていれば良いのですが、どうもそれも無いようで、以下のCIFilterクラスのクラスメソッドを使い、自分で入手するのが近道です。
+ (NSArray *)filterNamesInCategory:(NSString *)category; + (NSArray *)filterNamesInCategories:(NSArray *)categories;
そこで、カテゴリに属するフィルタをすべて列挙するサンプルアプリ「FileType」を作成してみました。このアプリを使えば、将来的にiOS 5.1や5.2(もしくは6.0)が登場した時に、利用可能なフィルタが増えているかどうかを簡単に確認できます。サンプル「FilterType」のXcode 4プロジェクトは、以下のサイトからダウンロード可能です。
サンプルアプリ:http://www.ottimo.co.jp/library/iPad/FilterType.zip
Core Imageのフィルタはその用途によりカテゴリごとにまとめられています。 CIFilterクラスのfilterNamesInCategory:メソッドは、カテゴリ名を指示することで特定のカテゴリに属するフィルタ名(NSString)を配列(NSArray)として返します。また、 filterNamesInCategories:メソッドでは、配列(NSArray)で複数のカテゴリを指定できます。また、こちらの引数にnilを渡すことで、利用できるすべてのフィルタ名を得ることも可能です。
現在、CIFilter.hに定義されている機能別カテゴリは全部で14あります。 残念ながら実装されている全カテゴリ名を返すクラスメソッドはありませんので、このサンプルではMasterlViewController.mにおいて、すべてのカテゴリ名を手動で配列に加えてアクセスできるようにしています。
- (void)viewDidLoad { [super viewDidLoad]; categolies=[[NSMutableArray array] retain]; // 14カテゴリ名を配列に登録 [categolies addObject:kCICategoryDistortionEffect]; [categolies addObject:kCICategoryGeometryAdjustment]; [categolies addObject:kCICategoryCompositeOperation]; [categolies addObject:kCICategoryHalftoneEffect]; [categolies addObject:kCICategoryColorAdjustment]; [categolies addObject:kCICategoryColorEffect]; [categolies addObject:kCICategoryTransition]; [categolies addObject:kCICategoryTileEffect]; [categolies addObject:kCICategoryGenerator]; [categolies addObject:kCICategoryReduction]; [categolies addObject:kCICategoryGradient]; [categolies addObject:kCICategoryStylize]; [categolies addObject:kCICategorySharpen]; [categolies addObject:kCICategoryBlur]; // 最初のカテゴリに属するフィルタ名を表示する [self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionMiddle]; }
サンプルのカテゴリ一覧をタップするとDetailViewController.mのshowFilterType:メソッドが呼ばれます。そのカテゴリに属するフィルタ名をUITextViewに表示するのには、 filterNamesInCategory:クラスメソッドを利用しています。
-(void)showFilterType:(NSString *)categoly { NSArray *array; NSString *str=@""; array=[CIFilter filterNamesInCategory:categoly]; // フィルタ名の配列を得る for( NSString *str1 in array ) str=[str stringByAppendingFormat:@"%@\n",str1]; // 表示文字列に追加 textView.text=str; // テキストビューに表示する }
今回は、iOS 5版のCore Imageについての注意点について解説しました。次回は、Core Imageの最新機能を取り上げ、そこからCALayerやCore Animationへと話題を広げて行きたいと思います。
-
くっきり、すっきり、名称変更!
この記事は、2012年01月12日に掲載されました。前回は、 ローカライズ用リソースファイルをプロジェクトに登録する時の注意点を取り上げました。今回は、Xcode 4での開発途中でプロジェクト名やアプリ名を変更する操作について解説したいと思います。
Xcode 4の雛形(テンプレート)から新規プロジェクトを作成した後に、アプリ名を変更したい場合が多々あります。例えば、最初の名称が気にくわなくなったとか、調べたら既にその名称はApp Storeで使われていたとかです。そればかりではなく、以前に作成したプロジェクトを転用するために、フォルダごとコピーしてからプロジェクト名やアプリ名を変更して編集を継続したい場合もあります。Xcode 3.2までは、プロジェクトメニューの項目に「名称変更…」があり、それを選択すれば簡単でしたが、Xcode 4では、そのメニュー項目が消えてしまっています。
「どうしたものか?」と調べてみたところ、デベロッパーからはこの件に対する疑問が多いようですね(笑)Apple社が提供している以下の「Technical Q&A」に回答がありました。
Technical Q&A QA1625「How do I rename my application in Xcode?」
例えば、テンプレートから作成した「MyApp」のプロジェクト名(アプリ名も同じ)を「YourApp」に変更してみます。このプロジェクト、最初の時点では以下のようなフォルダ内容となっています。
まずは、Finderでプロジェクトのフォルダ名を「YourApp」に変更します。この作業は、Xcode 4でプロジェクトをオープンしていない状態で行ってください。でないとXcodeからエラー表示がなされます。続いて「TARGET」を選択して、右側の「File Inspector」で表示されている「Project Name」を「YourApp」に変更します。
すると、ダイアログに名称変更の対象となるファイル一覧が表示されますので、確認の後に「Rename」ボタンをクリックして全ファイルの名称変更を実行します。
この時に「現在のスナップショットを残しますか?」と言った質問がなされますので、とりあえず「Enable」ボタンをクリックしてください。筆者の経験だと、「Disable」ボタンの方をクリックした時に正しく名所変更が成されない事がありましたので、ご注意ください。
ここまでの作業が終了した時点でのプロジェクトフォルダ内の様子は以下の通りです。また「Technical Q&A QA1625」で解説されている作業もこれで終わりです。
ところが名称変更後のプロジェクトを調べてみると、幾つか名称変更が成されていない箇所(積み残し)が見つかります。
・プロジェクトのファイル一覧にあるグループ名が「MyApp」のまま
・ツールバーに表示されているスキーム名が「 MyApp」のまま
・ソースファイルなどが保存されているフォルダ名が「MyApp」のままこのままでは少し(かなり)気持ちが悪いので、積み残しをすべて解消してみます。プロジェクトのグループ名やスキーム名は、それをダイレクトに変更してもXcode側のプロジェクト構成には大きな影響はありませんので、まずはグループ名を変更します。
続いてスキーム名の変更です。こちらの作業は、ポップアップメニューの「Manage Schemes…」を選択して表示されるダイアログから行います。
最後にプロジェクトのファイルが保存されているフォルダ名の変更です。こちらはFinderから行います。
しかしこのフォルダの名所変更を行うと、プロジェクトに登録されている各ファイルの相対パスが変化してしまい、ファイル一覧がリンク切れ(赤文字)表示となってしまいます。
これを解消するに、ファイルが含まれるグループ全体の相対パスを再設定してやります。一覧で「YourApp」グループを選択して「File Inspector」を表示し、「Path」の種類を変更するメニューの右下にある小アイコンをクリックします。オープンしたファイル選択ダイアログで「YourApp」フォルダを選択することでリンク切れはすべて解消されます。
最後の仕事は「TARGET」の「Build Settings」に登録されている「Info.plist」と「Prefix Header」ファイルの相対パスを手作業で修正することです。この作業を忘れると、両ファイルのリンク切れのためにビルドが正常に行えません。
これですべて完了ですが、リンク切れの状態でプロジェクトのInfo.plistの表示などをするとスキームの設定がクリアされてしまう場合があるようです。例えば、iOS用アプリのプロジェクトなのに、対象となる実行形式(Excutable)がOS X用と変わってしまう現象が発生します。この場合には、慌てず「Edit Scheme…」で適切な設定をし直せば問題は解消されます。すべての作業が終了したら一度ビルドを実行し、正常に終了するか?アイコン名は正しいか?などを確認してください。
今回は、 Xcode 4での開発途中でプロジェクト名やアプリ名を変更する操作について解説しました。 次回は、iOS 5の登場により、ようやくiOSアプリ上で利用が可能になった「Core Image」の解説に戻りたいと思います。
-
果敢にローカライズをしましょう!
この記事は、2011年12月22日に掲載されました。前回は、残り24枚のイエローカード(警告)を解消する作業を行いました。今回からは、筆者がXcode 4.2を活用していて気づいた幾つかの注意点を解説してみたいと思います。
まずは、ローカライズ用リソースファイル(.xibや.stringsなど)をプロジェクトに登録する時の注意点を取り上げます。各国語用にローカライズしたリソースファイルを作成すること自体はとても簡単です。 例えば「MyApp」プロジェクトが、以下の様なフォルダ内容だとします。この場合には、英語用ローカライズファイルが「en.lproj」フォルダにまとめられていることが分かります。
開発作業では、これに日本語用ローカライズファイルを追加したいとします。プロジェクトの一覧から「MyApp」(青いアイコン)を選択し、右側「PROJECT」の下に表示されている MyAppを選んでから「Info」タブをオープンします。すると一番下の「Localizations」にローカライズ対象となっている言語が並んでいますので、「+」ボタンをクリックしてポップアップメニューから「Japanese(ja)」を選びます。すると、その一覧に「Japanese」が追加表示されます。
これにより、自動的にプロジェクトフォルダ内に「ja.lproj」フォルダが作成され、その中に英語版からコピーされたすべてのリソースファイルが入ります。
それと同時に、ウィンドウ左のプロジェクト一覧にもそれらのファイルが登録されてグループ化されます。後は各ファイルをオープンして日本語用に編集すればOKです。例えば、nibファイルに用意したUIView上に「New」というボタンが配置してあれば、そのタイトルを「新規」と言う日本語に差し替えるといった作業を行います。
ところで追加後のファイル一覧を見てみると、新しく登録された日本語用ファイル(最後にJapanesが表示されている)の右側に「A」というアイコン表示がなされています。どうもこれは「Source Control」に関係する表示のようですが(新規追加されたという意味)、 複数開発者によるSource Controlを行っていなければ意味がありません。
何故表示されるのかは謎ですが、現在シーディングされているXcode 4.3では、このアイコンは表示されなくなっていますので、単純にバグなのかもしれません。Xcode 4.2で、このアイコンを表示させたくない場合には、先んじて「ja.lproj」フォルダを作成し、その中に必要なファイルをコピーしておいてから、ポップアップメニューで「Japanese(ja)」を選べば大丈夫なようです。
こうした操作を繰り返せば、日本語だけでなく、フランス語やドイツ語など複数言語にタイするローカライズ用リソースを作成&編集することが可能です。ところが、今回説明した方法には、ひとつ大きな問題点があります。それは、登録されたローカライズ用リソースファイルが、Xcodeにより「絶対パス」経由で管理されているのです。ファイルの「Identity」の「Location」を見ると、 英語用は「Relative to Group」ですが、日本語用は「Absolute Path」であることが分かります。そして何故だか、パスの種類を変更するポップアップメニューもハイライトでのままで利用できません。
この問題、筆者が自分のプロジェクトをzip圧縮して他人に送付した時に発覚しました。ファイルが絶対パスなので、自分のマシンとは異なるパス名の環境でオープンすると、以下のように追加したリソースファイルのアサインが外れてしまい、リンク切れでビルドができなくなってしまうわけです。
これを避けるには、ソースファイル等と同様に「ja.lproj」フォルダ内のファイルをドラッグ&ドロップで一つずつプロジェクトに登録するしかなさそうです。ただし、すべてまとめて(以下の場合は3ファイル)一覧へドラッグ&ドロップすると、InfoPlist.stringsファイルなどはグループ化されず、同じファイル名が2つ表示されてしまいますので注意しましょう。
今回は、 ローカライズ用リソースファイルをプロジェクトに登録する時の注意点を取り上げました。次回は、開発途中でプロジェクト名やアプリ名を変更する操作を解説したいと思います。
-
すべてのイエローカードを解消する
この記事は、2011年11月29日に掲載されました。前回は、Xcode 3.2.6用「しんぶんし」プロジェクトで使用していた各ファイルをXcode 4.2用プロジェクトへと移しました。今回は、残り24枚のイエローカード(警告)を解消する作業を行いたいと思います。
さっそくイエローカードの内容を確認してみると、24のうち19は同じ警告内容だということが分かります。その警告が発生しているソースコードの箇所は、以下のように記載されている部分です。
つまり、if文の中で(a=b)といった値の代入文を記述している箇所がマズイようです。しかし筆者的には、昔からこうした表記を多用していますので、今更これを書き換えるのは面倒です。これに限って言えば、ビルド時に警告が表示されないようにコンパイラのビルドオプションを変更するこよにします。対象となるビルドオプションは「Apple LLVM compiler 3.0 – Warnings」の「Missing Braces and Parentheses」です。これがデフォルトで「YES」になっていますので「NO」に切り替えてやります。
これだけの操作で、イエローカードの残り枚数が5枚とになりました。
後は、最初の警告から順番に修正を行っていきます。最初は、指摘されているメソッドが見つからないという意味です。これは当然と言えば当然で、draggingEnterd:メソッドで使用しているdraggingUpdated:メソッドが、参照より後ろ側で定義されています。よって、この2つのメソッド定義の順序を逆にするだけで警告は表示されなくなります。
残り4つの警告は、すべて「Deprecated API」に関するものです。 Deprecated APIとは、現在は使えているのですが、将来的にはフレームワークから削除される運命のCルーチンやObjective-Cメソッドを指しています。つまり「このAPIは今は使えますが、機能的に古くなった仕組みに含まれているので、OS XやiOSのバージョンアップのどこかの時点で存在しなくなります。なるべく使わないようにしましょう」と言う、Apple社からの警告メッセージです。
今回のソースコードで警告を受けているのは、NSOpenPanelクラスのrunModalForTypes:とfilenamesの2つのメソッドです。 その警告主旨や代用方法などは、関連ヘッダーファイル(この場合はNSOpenPanel.h)に記載されていることがありますので、まずはそちらを参照してみることをお勧めします。
/* Use -runModal instead. Set the -allowedFileTypes property instead of passing in the 'fileTypes'. */ - (NSInteger)runModalForTypes:(NSArray *)fileTypes NS_DEPRECATED_MAC(10_0, 10_6); /* Use URLs instead.*/ (NSArray *)filenames NS_DEPRECATED_MAC(10_0, 10_6);
というわけで、指示に従い以下のメソッドのソースコードを大きく修正し、NSURLベースのファイル選択方法へと書き換えます。
- (IBAction)addImage:(id)sender // 警告が表示された画像登録メソッド { NSArray *paths; NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setCanChooseDirectories:YES]; [panel setCanChooseFiles:YES]; [panel setAllowsMultipleSelection:YES]; if ([panel runModalForTypes:[NSImage imageUnfilteredTypes]] == NSOKButton) { if( paths=[panel filenames] ) [self addImagesPaths:paths]; } }
修正後のソースコードは以下のようになります。
- (IBAction)addImage:(id)sender // 警告を解除した画像登録メソッド { NSMutableArray *paths; NSArray *urls; NSOpenPane l*panel = [NSOpenPanel openPanel]; [panel setCanChooseDirectories:YES]; [panel setCanChooseFiles:YES]; [panel setAllowsMultipleSelection:YES]; if( [panel runModal]== NSOKButton ) { if( urls=[panel URLs] ) { paths=[NSMutableArray array]; for( NSURL *url in urls ) [paths addObject:[url path]]; [self addImagesPaths:paths]; } } }
こうしたDeprecated APIですが、歴史の浅いiOSでも既に幾つも存在しています。たとえばUITableViewCellクラス(UITableViewCell.h)に定義されているinitWithFrame: reuseIdentifier:メソッドは、iOS 3.0からDeprecated APIに指定されています。警告を表示させたくない場合には、代わりにinitWithStyle: reuseIdentifier:メソッドを使うことになります。
// Frame is ignored. The size will be specified by the table view width and row height. - (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_NA,__MAC_NA,__IPHONE_2_0,__IPHONE_3_0); // 代わりにこちらのメソッドを使用する - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
それから面白い警告としては以下のようなものもあります。unsignedで定義されている変数を「ゼロより小さい(day<0)」で比較しているソースコードへの指摘です。つまり、unsignedで定義されているわけですから、ゼロ以上に決まっているという指摘ですね。まあ言われてみれば当然なんですが、大きなお世話だと怒らないようにしましょう(笑)。何故なら、こうした細かな指摘により、大きなバグが発見される場合も多々あるからです。コンパイラに感謝の気持ちを忘れずに!
今回は、残り24枚のイエローカード(警告)を解消する作業を行いました。次回は、筆者がXcode 4.2を活用していて気づいた幾つかの注意点を書き出してみたいと思います。
-
イエローカードを解消する前に
この記事は、2011年11月17日に掲載されました。前回は、Xcode 4.2用のOS Xアプリのプロジェクトを新規作成してみました。今回は、このプロジェクトにXcode 3.2.6「しんぶんし」プロジェクトのファイル一式(ソースやリソース)を登録してみます。
前回、これから作業するアプリの名称を「Shinbunshi4」と付けましたが、引き続きiOS用「しんぶんし」プロジェクトもXcode 4.2用に切り替える予定なので、混乱しないように「Shinbunshi_OSX」と変更しておきます。まず最初にFinderを使い、新規作成したXcode 4.2用プロジェクトフォルダに、3.2.6用プロジェクトフォルダ内のソースファイルやリソース(画像やnibファイルなど)をすべて移動します。そして、それらをまとめてプロジェクトのファイル一覧に登録します。ちなみに、プロジェクトフォルダの中身は以下の様な状態になります。
こちらは、すべてのファイルのプロジェクトへの登録を完了した状態です。
今回のアプリでは「Quartz」フレームワーク(Framework)を使うので、それをリンクする指示が必要です。ビルド時にリンクするフレームワークは、「TARGETS」アイコンの「Summary」で表示される「Linked Framewroks and Libraries」にある「+」ボタンで追加します。フレームワークを新規追加すると、何故だかプロジェクトのファイル一覧の一番上に表示されてしまします(バグ?)。あまり美しくないので、アイコンをドラッグ&ドロップして「Frameworks」グループに移動させておきましょう。同時に、デフォルトのままの「Application Category」「Version」「Deployment target」(起動可能なOSXのバージョン)なども、本アプリで用いる内容に変更しておきます。
続いてPrefix headerファイル「Shinbunshi_OSX-Prefix.pch」に、どのソースファイルからでもQuartzフレームワークの内容を参照できるように、#import
の記述を追加しておきます。 次はアプリの動作環境を保持している「Info.plist」ファイルの微調整です。まず注意すべき点は、Xcode 4.2用のプロジェクトでは、その名称が「Shinbunshi_OSX-Info.plist」に変更されていることです。この「.plist」ファイルにデフォルトとして設定されている内容は以下の通りです。
ここに、以前使用していた「info.plist」の内容をマージしていきます。たとえば、アイコン名「Icon file」やクリエータタイプ「Bundle creator OS Type code」などです。こちらは、 Xcode上でカット&ペーストを使い項目内容を移動します。移動すべき一番の大物は、アプリのドキュメントや取り扱うメディアファイのルタイプを記述したドキュメントタイプの一覧「Document Types」です。こちらは、以前の「.plist」ファイルを直接テキストエディタでオープンし、XMLで記述されている対象項目を複製してしまうのが簡単です。以下が、そうした作業で完成した「Shinbunshi_OSX-Info.plist」ファイルの内容です。
ここまでで一度ビルドを実行してみます。これによりアプリが作成され、ちゃんと起動すれば、プロジェクトの移行は成功したことになります。ただし、前回と同様に25枚のイエローカード(警告)も表示されるはずです。この時点でレッドカード(エラー)が出た場合には、移行手順に何らかの間違いや設定不足があると思われますので、再度確認が必要です。続いて行うべき作業は、25枚のイエローカードを解消するためにソースコードの内容を修正(微調整)することです。
ソースコード全般をチェックする前に、それとは直接関係なく出されているイエローカードがあるので、まずはその対処をしてみます。警告内容は、以下の「InfoPlist.strings」テキストファイルに使われている文字エンコードがUnicode(UTF-16)であるというものです(Xcode 4は相当に神経質)。よって、このテキストファイルをUnicode(UTF-8)で再エンコードして保存し直せば、イエローカードを1枚だけ減らすことができるわけです。さて、残りは24枚です(笑)
今回は、「しんぶんし」のXcode 3.2.6用プロジェクトで使用していた各ファイルをXcode 4.2用プロジェクトへと移しました。次回は、残り24枚のイエローカードを解消する作業を行いたいと思います。
-
まずは新規プロジェクト作成から
この記事は、2011年10月26日に掲載されました。前回は、Xcode 3とXcode 4の違いなどSDKの各バージョンについて解説しました。今回からは、既存のXcode 3(3.2.6)のOS XやiOSのアプリ開発プロジェクトをXcode 4(4.2)対応に切り替える過程を解説します。
iOS 5の登場とともに、Apple社から「Xcode 4.2」が正式版がリリースされました。本連載でも、今後はLion(OS X 10.7)環境のXcode 4.2で作業を進めることにします。まず最初に何かひとつ、3.2.6用のOS Xプロジェクトを4.2用へと変換してみます。変換するプロジェクトとしては、随分と昔に本連載の技術解説で活用していた「しんぶんし」を使うことにします。「しんぶんし」プロジェクト(Xcode 3.2.6用)のフォルダ内容は以下の通りです。
とりあえず、このままのプロジェクトをXcode 4.2でオープンしてみます。オープン後にビルド設定の内容などを詳しく調べてみると、「Architectures」と「Base SDK」の項目が変更されています。「Architectures」には、32Bitだけでなく64Bitインテルが追加されており、もうPPC(PowerPC)は存在していません(当然です)。「Base SDK」の方は、OS最新バージョン(現在は10.7)用となっています。
続いて、これ以上何も手を加えず「しんぶんし」を64Bitモードアプリとしてビルドしてみます。するとイエローカード(ワーニング)が25枚も表示されますが(笑)レッドカードは表示されることなくビルドが終了して「しんぶんし」が無事起動しました。起動したアプリの各機能を一通りチェックしてみましたが、これといって問題は無いようです。まずは一安心といったところです。
この状態のまま、表示されたワーニングを直してしまえば終わりなのですが、それでは面白くありません。また、Xcodeのメジャーバージョンアップでは、古いプロジェクトをそのまま引き継ぐと何かしらトラブルに遭遇することがあります。これは、ビルドにおけるオプション設定の違いなどが原因だったりしますが、たまに理解不能、解析不能のトラブルに遭遇したりします。筆者的には正しくプロジェクトが変換されているどうか?いつも「疑心暗鬼」(信じていない)なのです(笑)。ここは、すっきり、くっきり、気持ち良く4.2のプロジェクトを新規作成し、そこに現在のプロジェクト環境を移動させたいと思います。この作業を行うことで、新規OS Xアプリ開発ではどんな点に注意すれば良いのかも明確になります。表示された25のワーニングについては、移行後の環境で調査して対処することにします。
Xcode 4.2を起動して、Fileメニューから「New Project…」(Shift+Comand+Nキー)を選びます。すると、以下のシートがオープンしますので、左側のターゲットOSと作成タイプの一覧から「Mac OS X」直下の「Application」を選び、右側に表示されたテンプレートアイコンから「Cocoa Application」プロジェクトテンプレートを選択します。
テンプレートを選択した後にアプリ名を入力し、モデルオブジェクトの管理に「Core Data」の能力を使用するのか?開発時に「Unit Test」を行うのか?などをチェックボックスで選んでから、プロジェクトフォルダとして保存します。ちなみにアプリ名には日本語を使わない方が良いでしょう(入力は可能ですが)。これから開発するアプリの「英語名」と一致させておくことで、後から煩わしい変更作業をする必要がなくなります。この作業で興味深い点は、テンプレート作成段階でもう「App Store Category」が設定できることです。Apple社の進む方向が垣間見られますね(笑)。
Xcode 4.2のOS Xプロジェクト作成作業が4.0や4.1の場合と大きく異なる点は、アプリ名を入力するパネルに「Use Automatic Reference Counting」(LLVMコンパイラ 3.0から使用可能)チェックボックスが追加されていることです。この新しい機能「ARC」については、またの機会に詳しく解説したいと思います(今回は不使用とする)。出来上がったXcode 4.2用プロジェクトのフォルダの中身は、以下の様になります。
今回は、Xcode 4.2用のOS Xアプリのプロジェクトを作成してみました。次回は、このプロジェクトにXcode 3.2.6「しんぶんし」プロジェクトのファイル(ソースやリソース)を順次登録していきます。
-
Xcode 4の時代がやって来た
この記事は、2011年09月15日に掲載されました。前回は、オリジナルのフィルタ(Image Uint)を作成して、それをシステムに追加登録する方法を調べてみました。今回からは、Frameworkの話から少し離れて、Xcode 3のプロジェクトをXcode 4へ移す作業などを紹介して行きたいと思います。
Xcode 4は、前バージョンのXcode 3(最終バーションは3.2.6)から大きく仕様が変わり、まったくの別の開発ツールへと変貌しています。書店に並ぶiOS関連の書籍も、徐々にXcode 4を用いた開発手法に内容が切り替わってきていますので、書籍を購入する時にはXcode 4対応かどうかをご確認ください。例えば、今までは別ツール(アプリ)として提供されていた「Interface Builder」もビルトインされました。Interface Builderは、GUI(グラフィカルユーザインターフェース)をレイアウトしソースコードとコネクトする重要な役割を持ちましたが、その使い勝手も大きく変わっています。ところで、現在、開発者が使えるOS XとiOS用のSDK(Software Development Kit)は、開発者シーディング(テスト配布)中の物も含めると5バージョンあります。
(1)Xcode 3.2.6(OS X 10.6 SnowLeopard専用)
Base SDKとして10.4や10.5用が必要なCocoaアプリや、PowerPC対応アプリ、またCarbonアプリについては、Xcode 3.2.6を使いビルドするしか手はありません。ところが、Xcode 3.2.6はOS X 10.7(Lion)へのインストールが不可であり、iOS 5用のフレームワークを使うiOSアプリの開発もできません。ちなみに、筆者もLionへのインストールをチャレンジしてみましたがダメでした。一瞬うまく完了したかの様に見えるのですが、ツールなどのアプリがインストールされません。また、途中で「iTunesを閉じてください!」といった間違ったメッセージが表示されて、それ以上作業が進まないこともありました。
と言うわけで、筆者のように、未だに商用のCarbonアプリのメンテナンスをしている開発者は、SnowLeopardとXcode 3.2.6の環境を手放せないことになります。 また、パッケージリリースの関係上、現バージョンのプロジェクトをXcode 4へ移植したくないケースも多々あります。そうした観点からも、iOSやOSXの開発環境を一元化するために、Apple社には何とかLionで起動できるXcode 3のバージョンを提供して欲しいのですが、まあ無理でしょうね(涙)。Xcode 4とは異なり、Xcode 3.2.6はちゃんと日本語にローカライズされていますので、Xcode 4(英語版のみが提供)のGUIに意味不明な項目がある場合には、Xcode 3.2.6での表記と比較確認するという手もあります。
Xcode 3.2.6は、非契約デベロッパーとして登録していれば、 こちらのサイトから無料で入手可能です。
(2)Xcode 4.0.x(OS X 10.6 SnowLeopard専用)
OS X 10.6(SnowLeopard)専用のXcode 4です。 SnowLeopardでXcode 4を利用して開発作業を行う場合にインストールします。 現時点では4.0.2が最新バージョンです。 ただし、iOS 5用のフレームワークを必要とするiOSアプリの開発はできません。 多分これについては、これ以上のバージョンアップは成されないでしょう。基本的には、iOS 5の登場と共にXcode 4.2.xに置き換えられると思われます。各バージョンのXcode 4(SDK)は、アップルのデベロッパーサイト(閲覧にはApple社とのデベロッパー契約が必要)から直接ダウンロードすることが可能です。
(3)Xcode 4.1.x(OS X 10.7 Lion専用)
OS X 10.7(Lion)専用のXcode 4です。LionでXcode 4を使い開発作業を行う場合にインストールします。機能的には、Xcode 4.0.2のLion版だと思えばOKです。Lionユーザは「Mac App Store」において無料で入手可能です(何故だかバージョンは4.1.1)。 こちらも、iOS 5用のフレームワークを必要とするiOSアプリの開発はできません。 Xcode 4.0.xと同様、iOS 5の登場と共にXcode 4.2.xに置き換えられると思われます。
(4)Xcode 4.2.x(OS X 10.6 SnowLeopard専用)
現在、契約デベロッパーだけにシーディング中のOS X 10.6(SnowLeopard)専用のXcode 4.2です。 SnowLeopardのみにインストール可能で、iOS 5用のフレームワークを必要とするiOSアプリの開発も可能です。Xcode 4.0.xの後継バージョンに成るものと予想されます。
(5)Xcode 4.2.x(OS X 10.7 Lion専用)
現在、契約デベロッパーだけにシーディング中のOS X 10.7(Lion)専用のXcode 4.2です。 Lionのみにインストール可能で、iOS 5用のフレームワークを必要とするiOSアプリの開発が可能です。 Xcode 4.1の後継バージョンに成るものと予想されます。 将来的には、SnowLeopard用と統合されてひとつのアプリになると嬉しいのですが、ひょっとすると無理なのかもしれませんね…。
現状の流れからすると、OS XとiOS用SDKは、将来的にはXcode 4.2以降のバージョンに集約されると思われます。しかし、対応すべき色々なOS Xバージョンや開発ターゲット(アプリの種類)を引きずっている開発者の方は、筆者と同じく、幾つかのSDKバージョンを併用する必要があるでしょう。複数のSDKをインストールする時の注意点は、どのバージョンも同じ「Developer」フォルダに開発環境をインストールしてしまうことです。もし、Xcode 3とXcode 4を併用したければ、インストール時に、先んじて作成しておいた別フォルダへどちらかをインストールします。例えば「Xcode 3」というフォルダを作り、そちらへXcode 3のみをインストールするわけです。こうした操作はSDKのインストーラーから実行できます。インストールは、最新のソフトモジュールを上書きしない対策として、 古いバージョンから新しいバージョンへと順次行う事をお奨めします。
今回は、Xcode 3とXcode 4の違いなどSDKの各バージョンについて解説しました。次回からは、既存のXcode 3.2.6のOS XやiOSのアプリ開発プロジェクトをXcode 4用に切り替える過程を実況する予定です(笑)。
-
Image Unitを開発するには
この記事は、2011年09月03日に掲載されました。前回は、Core Imageを用いた処理で「ImageKit」を活用するサンプルを取り上げてみました。今回は前回の続きと、オリジナルのフィルタを作成して、それをシステムに登録する手段を調べてみます。
まずは前回の疑問点の解決です。疑問点は「Filter Browserでフィルタ名をダブルクリックした時に、そのパラメータ入力用に「Filter User Interface View」をウィンドウ右下のビューにレイアウトしている処理はどこだ?」でした。それは、以下のFilterViewクラス(Filter User Interface Viewを表示するためのビュー)のsetFilters:メソッドです。
- (void)setFilters:(NSArray *)newFilters { CIFilter *newFilter = [newFilters lastObject]; [filterView removeFromSuperview]; [filterView release], filterView = nil; filterView = [newFilter viewForUIConfiguration:[self interfaceConfiguration] excludedKeys:[NSArray arrayWithObject:kCIInputImageKey]]; [filterView retain]; [self addSubview:filterView]; [self setFrame:[filterView frame]]; }
どこから呼ばれるかと言うと、アプリのモデルオブジェクトであるImageDocument(NSDocumentのサブクラス)のインスタンス変数であるfilters(NSMutableArray)に、作成したフィルタ(CIFilter)を追加た時点で呼ばれます。そのためには、モデルコントローラ(NSArrayController)と配列(NSMutableArray)そして、配列の要素をセットした時のビュー側の処理をバインディング(Binding)する必要があります。そうした設定は、nibファイルのImageDocument.xibと、以下のImageWindowControllerクラスのawakeFromNibメソッド(nibファイルから呼ばれた時に一度だけ実行)で行われています。
- (void)awakeFromNib { NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:NO], NSAllowsEditingMultipleValuesSelectionBindingOption, nil]; [filterView bind:@"filters" toObject:filtersController withKeyPath:@"selectedObjects" options:options]; }
もし、awakeFromNibメソッドにソースコードによるバインディング処理が記述されていなければ、何が何だかさっぱり分からないところです。勉強やメンテナンスのためソースコードを追いかける場合、アプリ内でバインディングが多用されていると、各処理の見通し(視界)がとても悪くなります。Apple社がiOSにバインディング機能を追加しないのもこうした理由からかもしれません。筆者もバインディングは嫌いなので、商用アプリには使用したことはありません(笑)。
さて話は変わります。今度は、オリジナルのフィルタを作成し、それをシステムに追加登録する仕組みについて調べてみます。Core Imageでは、複数のフィルタを「Image unit」と呼ばれるplug-in(NSBundleクラスを用いたパッケージ)にまとめてシステムに追加登録することが可能です。追加した「Image Unit」は、Core Imageに対応しているアプリからデフォルトのフィルタと同じ様に利用することができます。
Image Unitの中の各フィルタの機能を有したソースコードを「カーネル」( Kearnel)と呼びます。 カーネルの作成には「CoreImage kernel language」を使うのですが、この言語は、OpenGLのGLSL(OpenGL Shading Language)のサブセットとして文法が定義されているので、カーネルの正体はOpenGLのシェーダプログラムであることが理解できます。開発言語の仕様(文法)等は、ドキュメントの「Core Image Kernel Language Reference」を参照します。また「Descioptino Propaty List」は、フィルタのカテゴリーや引数のタイプと個数を含んだXMLファイルです。
まず、独自フィルタの作成ですが、そのためにはカーネルをプログラミングすることになります。その詳細は、ドキュメントの「CoreImage Programming Guide」のp41「Creating Custom Filters」の章を参照します。それをどうやってplug-inにビルドするのかは、p59「Packaging Filters as Image Units」の章を参照します。この章では、Apple社が提供しているサンプルソースコードの「CIDemoImageUnit」を例題に解説が進められています。
以下が、Xcodeでビルドした「CIDemoImageUnit.plugin」パッケージの中身です。「funHouseMirror.cikernel」と「MyKernelFilter.cikernel」が、フィルタをプログラミングした2つのカーネルファイル(テキスト)です。そして「Description.plist」の方が、先ほど紹介した「Descioptino Propaty List」のXMLファイルとなります。
各カーネルの動作確認(実際の効果)やデバッグには、SDKに含まれるグラフィックツールの「Quartz Composer」を使います。カーネルのソースコードファイルをQuartz Composerのエディタウィンドウにドラッグ&ドロップすると、それがパッチとしてウィンドウに表示されます。
その状態で供給する画像ファイルや引数(パラメータ)を入力プラグに接続すれば、ビューアウィンドウでその結果を確認できます。 ソースコードの方は、インスペクタ(Inspector)ウィンドウで確認と編集が可能です。
Image Unitの開発に関する詳細な技術に関しては、ドキュメントの「Image Unit Tutorial」を参照します。こちらでは、色々なフィルタを作成する時に参考となる数多くのカーネルプログラムが紹介されています。そして出来上がったImage Unitは、以下のどちらかのディレクトリに保存することで有効になります。
「/Library/Graphics/Image Units」か「 ~/Library/Graphics/Image Units」
これで、Core Image対応のアプリからオリジナルフィルタが利用できるようになるはずでした。 そう! 確かに昔はこれでOKだったのですが(笑)、現在のOS Xバージョンではセキュリティの関係で、Image Unit配布にはApple社側との認証手続きが必要な様です。その方法については、ドキュメント「Image Unit Tutorial」の p65「Preparing an Image Unit for Distribution」とp68「Completing the Necessary Licensing and Trademark Agreements」の章に詳しく記述されています。何だか面倒くさそうなのですが(笑)ご興味ある方は、ぜひチャレンジしてみてください。
今回は、前回の続きの解説と、オリジナルのフィルタを作成して、それをシステムに追加登録する方法を調べて見みました。次回は、Frameworkの話から少し離れて、Xcode 3のプロジェクトをXcode 4へ移す経験談を(最近筆者が苦労した…)紹介したいと思います。
-
Filter Browserをオープンする
この記事は、2011年08月13日に掲載されました。前回は、CIModTransitionフィルタを利用したトランジション・サンプルソースコードの詳細を解説しました。今回は、Core Imageを用いた処理で「ImageKit」を活用するサンプルを取り上げてみたいと思います。
以前にも言及しましたが、Mac OS Xには画像を操作するための便利なフレームワークImageKitが用意されています。このフレームワーク、残念ながらiOS 5には搭載されていないのですが、Core Imageが搭載されたことを考えると、そのうち機能の一部がやって来る可能性があります。そう言う意味では、Mac OS XでCore Imageのフィルタを活用する時に(Mac OS X 10.5から可能)いかにImageKitを用いるのかを勉強しておくことは大いに意味があります。
今から使うCore Imageフィルタをユーザに選択させるには、専用のパネルウィンドウ「Filter Browser」(IKFilterBrowserPanelクラス)をオープンします。また、フィルタのパラメータ入力には「Filter User Interface View」(IKFilterUIViewクラス)を利用します。これらの機能に関しては、ImageKitに関する技術ドキュメント「Image Kit Programming Guide」に詳細が記載されています(最新版の改訂日は2008-06-09)。Core Imageに関連する技術内容は、69ページから始まる「Browsing Filters and Setting Input Parameters」で詳しく解説されています。
今回は、ImageKitを利用したApple社提供のサンプルソースコードを探し出し、それを参照してみましょう。Core Imageでのフィルタ選択やそのパラメータ入力を試すのには、Developerフォルダ(Developer/Application/Graphic Tools/)にあるツール「Core Image Fun House」を使うのが便利です。前にも言及しましたが、このツールのサンプルソースコード(FunHouse)は「Mac OS X Developer Library」サイトから入手することが可能であり、各機能の使用方法をソースコードで追いかけられます。
しかし、FunHouseのプロジェクトをオープンして「KFilterBrowserPanel」や「IKFilterUIView」でソースコード内を検索してみると、両クラスは利用されていないことが分かります。つまり、このアプリはImageKitの機能を使わず、フィルタ選択やパラメータ入力を実現しているわけです。これでは参考になりませんので、別サンプルを探してみました。昔々、FunHouseと似たアプリで「IUUIDemoApplication」というサンプルが存在していました。こちらはFunHouseとは異なり、ちゃんとImageKitの機能を使いフィルタ選択などを実現しています。
ところが 現在、このサンプルソースコードは「Mac OS X Developer Library」サイトから削除されています。それでは、他に参考になる物はないのかと探してみたところ「CIRAWFilterSample」というサンプルを発見しました。
ウィンドウ右下の「Add…」ボタンをクリックすると、Filter Browserがオープンします。それに関する処理は、 NSWindowControllerのサブクラスとして用意されている ImageWindowControllerクラスに実装されています。ImageWindowControllerのクラス定義(ImageWindowController.h)を見てみると、オープンされたパネルのインスタンスはfilterBrowserPanel(IKFilterBrowserPanel)に保存されます。
@interface ImageWindowController : NSWindowController { IBOutlet ImageView *imageView; IKFilterBrowserPanel *filterBrowserPanel; IBOutlet NSArrayController *filtersController; IBOutlet FilterView *filterView; }
続いてクラス実装のImageWindowController.mです。Filter Browserで何か操作が行われた場合には、その操作内容により異なるタイプの通知(NSNotification)が届くことになります。通知にどんなタイプがあるかは、ヘッダファイルの IKFilterBrowserPanel.hを参照してください。クラス初期化のinitメソッドでは、IKFilterBrowserFilterDoubleClickNotification通知が発生(ユーザがフィルタ名をダブルクリック)した時に、filterBrowserDoubleClicked:メソッド(ハンドラ)が呼ばれるように準備が成されています。
- (id)init { self=[super initWithWindowNibName:@"ImageDocument"]; if( self != nil ) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(filterBrowserDoubleClicked:) name:IKFilterBrowserFilterDoubleClickNotification object:nil]; } return self; }
次は「Add…」ボタンをクリックした時のアクションメソッドです。まだFilter Browserがオープンされていなければ、filterBrowserPanelWithStyleMask:メソッドで作成します。StyleMask値はウィンドウのアピアランスを決定するために指示する「Window Style Masks」に準拠します(背景をメタル調に変更することも可能)。
そしてbeginWithOptions:modelessDelegate:didEndSelector:contextInfo:メソッドで、Filter Browserをオープンします。Filter Browserをモードレスダイアログではなく親ウィンドウ上のシートとしてオープンしたい場合には、beginSheetWithOptions:modalForWindow:modalDelegate:didEndSelector:contextInfo: メソッドの方を利用します。
- (IBAction)showFilters:(id)sender; { if( filterBrowserPanel==nil ) { filterBrowserPanel = [[IKFilterBrowserPanel filterBrowserPanelWithStyleMask:0] retain]; [filterBrowserPanel setFrameAutosaveName:@"FilterBrowserPanel"]; } [filterBrowserPanel beginWithOptions:nil modelessDelegate:self didEndSelector: @selector(browserPanelDidEndSelector:returnCode:contextInfo:) contextInfo:nil]; }
以下は、 beginWithOptions:modelessDelegate:didEndSelector:contextInfo:メソッドで定義した、Filter Browserのセッションが終了した時に呼ばれるハンドラです。ただしウィンドウのクローズボタンを押した時に呼ばれるわけではありません。自身でパネル上に「閉じる」等のボタンを配置して、そのアクションメソッドとしてfinish:を呼んだ場合に呼ばれるようです(未確認)。
- (void)browserPanelDidEndSelector:(NSOpenPanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { [filterBrowserPanel close]; [filterBrowserPanel release], filterBrowserPanel = nil; }
最後が、Filter Browserに表示されたフィルタ名をユーザがダブルクリックしたときに呼ばれる通知用ハンドラです。フィルタ名からCIFilterを作成し、ディフォルトのパラメータをセットした後に、本アプリのモデルオブジェクトであるImageDocument(NSDocumentのサブクラス)のインスタンス変数であるfilters(NSMutableArray)に作成したfilter(CIFilter)を追加しています。
- (void)filterBrowserDoubleClicked:(NSNotification *)notification { if( [self document]==[[NSDocumentController sharedDocumentController] currentDocument] ) { NSString *filterName=[notification object]; CIFilter *filter=[CIFilter filterWithName:filterName]; [filter setDefaults]; [[[self document] mutableArrayValueForKey:@"filters"] addObject:filter]; } }
アプリ側の挙動を見ると、Filter Browserでフィルタ名をダブルクリックした時に、フィルタを作成するのと同時に、そのパラメータ入力用に「Filter User Interface View」もウィンドウ右下の領域にレイアウトされます。ここまでのソースコードの流れでは、そうした処理を実行している箇所が無いのですが、これはどうたことなのでしょうか? その秘密は次回に明かされます(笑)。
今回は、Core Imageを用いた処理で「ImageKit」を活用するサンプルを取り上げてみました。次回は今回の続きを解説し、オリジナルのフィルタを作成して、それをシステムに登録するのにはどうしたら良いのかも調べてみます。
-
トランジション用フィルタを使う
この記事は、2011年07月20日に掲載されました。前回は、Core Imageのフィルタ処理で必要となるメモリ容量について考えました。今回は、前回に取り上げたCIModTransitionフィルタを利用したサンプルソースコードの詳細を解説したいと思います。
2つの画像のトランジション処理は、NSViewのサブクラスであるCIExposureViewクラスを作成し、すべてそのクラスに実装することにします。Interface Builderでウィンドウ上にCIExposureViewをレイアウトし、その下にトランジションのinputRadiusパラメータ(中心から穴のふちまでの距離を指定する値をNSNumberで設定)を変更するためのスライダーを配置します。スライダーの最大値と最小値はゼロと60.0に、現在の値を30.0にセットしておきます。
続いて、CIExposureViewのクラス定義(CIExposureView.h)です。 トランジションで切り替える2つの画像(sourceImageとtargetImage)は、アプリのリソースから読み込んでCIImageに保存します(NSImageではないので御注意)。radiusValueには、ウィンドウに配置したスライダーの値を反映させます(デフォルトは30.0)。imageWidthとimageHeightは読み込んだ画像の横幅と高さ(ピクセル値)、baseはトランジションのアニメーション表示で使う経過時間を得るための基準時間です。
@interface CIExposureView: NSView { CIContext *context; // フィルタ処理実行環境 CIFilter *transition; // COre Imageのフィルタ CIImage *sourceImage; // 画像(1) CIImage *targetImage; // 画像(2) float radiusValue; // nputRadiusパラメータ float imageWidth; // 画像の横幅(ピクセル) float imageHeight; // 画像の高さ(ピクセル NSTimeInterval base; // アニメーション用基準時間 } @property(nonatomic,retain)CIContext *context; @property(nonatomic,retain)CIFilter *transition; @property(nonatomic,retain)CIImage *targetImage; @property(nonatomic,retain)CIImage *sourceImage;
CIExposureViewクラスがnibファイルから読み込まれインスタンス化される時に、パラメータの初期値代入、2つの画像の呼び込み、トランジション・アニメーション用タイマー作成などの初期処理を行います。最後に、addTimer:forMode:メソッドでタイマーの各種モードを設定していますが、これはスライダーを動かしている間もタイマー処理を継続させるためです。興味ある方は、ソースコードから最後の2行を外して試してみてください。スライダーをつまむとアニメーションが停止することが分かります。
- (void)awakeFromNib // 画像を読み込む(初期化処理) { NSTimer *timer; NSURL *url; radiusValue=30.0; imageWidth=320.0; imageHeight=240.0; [[self window] setPreferredBackingLocation: NSWindowBackingLocationVideoMemory]; url=[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource: @"Dog" ofType:@"jpg"]]; // Dog.jpgを読み込む self.sourceImage=[CIImage imageWithContentsOfURL:url]; url=[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource: @"Bird" ofType:@"jpg"]]; // Bird.jpgを読み込む self.targetImage=[CIImage imageWithContentsOfURL:url]; timer=[NSTimer scheduledTimerWithTimeInterval:1.0/30.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES]; // タイマー作成 base=[NSDate timeIntervalSinceReferenceDate]; // アニメ処理の基準時間 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // タイマーの動作モード設定 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSEventTrackingRunLoopMode]; }
以下がタイマーに設定されたtimerFiredです。このメソッドは1/30秒毎に繰り返し呼ばれます。処理内容は、setNeedsDisplay:にYESをセットするだけですが、結果的にCIExposureViewのdrawRect:メソッドを呼び出し、ビューの内容を再描画させます。
- (void)timerFired // タイマー処理 { [self setNeedsDisplay:YES]; // ビューの再描画を指示する }
次は、ウィンドウ上のスライダーを動かした時のアクションメソッドです。まずは、radiusValueにスライダ値を代入します。そして、CIFilterオブジェクト(transition)を解放してnilを代入してからsetNeedsDisplay:にYESを代入します。これによりdrawRect:メソッドが呼び出されますが、この時にsetupTransitionメソッドが再度呼ばれ、新パラメータを用いてフィルタを作り直します。
- (IBAction)sliderChanged:(id)sender // スライダーのアクションメソッド { radiusValue=[sender floatValue]; // スライダー値を得る self.transition=nil; // 現在のCIFilterオブジェクトを解放する [self setNeedsDisplay:YES]; // ビューの再描画を指示する }
ビューの再描画は、drawRect:メソッドをオーバライドすることで実現します。まずは、現在の時間からbaseを引くことで、起動時からの経過時間を得ます。contextがnilの場合には、contextWithCGContext:メソッドで CIContextオブジェクトを作成して代入します。この処理はアプリ起動時に一回だけ行われます。また、transitionがnilならCIFilterオブジェクトを作成します。ことらはアプリ起動時とスライダー値が変更された時に実行されます。後は、imageForTransition:メソッドで得られたトランジションの結果画像を drawImage:メソッドで描画します。
- (void)drawRect:(NSRect)rect // ビューの描画(タイマーから間接的に呼ばれる) { float time; CGRect crt; time=0.5*( [NSDate timeIntervalSinceReferenceDate]-base ); // 経過時間を得る if( context==nil ) // フィルタ処理の実行環境(CIContext)を作成 self.context=[CIContext contextWithCGContext: [[NSGraphicsContext currentContext]graphicsPort] options:nil]; if( transition==nil ) [self setupTransition]; // フィルタ(CIFilter)を作成 crt=NSRectToCGRect( rect ); [context drawImage:[self imageForTransition:time+0.1] atPoint:crt.origin fromRect:crt]; // トランジション処理の結果を描画 }
残りは、前回にも提示した2つのメソッドです。setupTransitionメソッドは アプリ起動時とスライダーの値の変更時に呼ばれます。初期パラメータを設定してCIModTransitionフィルタ(CIFilter)を作成しtransitionに代入します(M_PIは円周率で31415…)。imageForTransition:メソッドの方は、時間の経過と伴にフィルタ状況(表示結果)を変化させます。経過時間により2つの画像の役割を入れ換えている点に注目してください。トランジション時間の設定でにcos()ルーチンを用いているのは、経過時間軸に対してリニアでない滑らかな画像のトランジションを実現するためです(ドックのアイコンジャンプなどと同じ)。
- (void)setupTransition // フィルタの作成 { self.transition=[CIFilter filterWithName: @"CIModTransition" keysAndValues: @"inputCenter",[CIVector vectorWithX:imageWidth*0.5 Y:imageHeight*0.5], @"inputAngle",[NSNumber numberWithFloat:M_PI*0.1], @"inputRadius",[NSNumber numberWithFloat:radiusValue], @"inputCompression",[NSNumber numberWithFloat:10.0],nil]; } - (CIImage *)imageForTransition:(float)time // フィルタのパラメータ変更 { if( fmodf( time,2.0 ) < 1.0 ) // 時間によってトランジションの方向を変える { [transition setValue:sourceImage forKey:@"inputImage"]; [transition setValue:targetImage forKey:@"inputTargetImage"]; } else { [transition setValue:targetImage forKey:@"inputImage"]; [transition setValue:sourceImage forKey:@"inputTargetImage"]; } [transition setValue:[NSNumber numberWithFloat:0.5*(1-cos( fmodf(time,1.0) *M_PI))] forKey:@"inputTime"]; // トランジション時間をセット return [transition valueForKey:@"outputImage"]; // 処理結果の画像を返す }
今回は、前回に取り上げたCIModTransitionフィルタを利用したトランジション・サンプルソースコードの詳細を解説しました。次回からは、Core Imageを用いた処理で「Image Kit」を活用する例題を取り上げてみたいと思います。
本日のサンプルのXcodeプロジェクトはこちらから…。
-
フィルタ処理で消費されるメモリ
この記事は、2011年07月04日に掲載されました。前回は、Core Imageを自作アプリで活用するために、まず最初に読まなければいけない技術ドキュメントや簡単なフィルタ処理のソースコードを紹介しました。今回からは、もう少し異なるフィルタ処理の例をソースコードと共に紹介したいと思います。
契約デベロッパーは、昨年同様に割と早くWWDC2011で開催されたセッションのビデオを見ることが出来るようになりました。その中には、iOS 5におけるCore Imageに関してのセッションも幾つかあるので、ぜひ参照してみてください(解説は英語ですが)。そうしたセッションを既にチェックされた方は、iOS 5のCore Imageの仕様について疑問を持たれたかもしれません(笑)。仕様の詳細についてはiOS 5が正式リリースされるまで公に話せませんが、Core Imageでの画像処理(フィルタ処理)のメモリ使用量については、それを活用する観点からも正しく認識しておく必要があります。
Core Imageは、高速フィルタ処理を行うために搭載GPUの能力をフル活用します。演算処理は設定によりCPUでも実行できるのですが、GPUを使うより何十倍も遅くなります。特にCPUの演算能力が低いiOSデバイスでは、その能力差はさらに開くと予想されます。実際、iOSデバイスではCPU演算で処理させるのは現実的ではないでしょう。その反面、GPUで演算処理を行うには、その対象となるデータ(画像)はビデオメモリに展開されている必要があります。OpenGL的に言えば、こうしたデータはテクスチャー画像と同じく、同サイズ(幅と高さ)のレンダーバッファに保存されます。
Core Imageを使い、その画像のブライトネス(露出)やコントラストを変更する場合には、ビデオバッファはその画像と同サイズ(1枚)を用意すればOKです。つまり、ターゲット画像の各RGBピクセルに演算処理したものが、そのまま最終結果となります。これは繰り返しの処理やUndo処理を前提としていない場合です。前提にすれば、もう1枚バッファが必要となります。
ところが、シャープネスを変更しようとすれば、マトリックス演算のため必ずオリジナル画像は保持しておく必要があります。そのため結果を保存するビデオバッファが必ずもう1枚必要となります。つまり、必要とされるビデオメモリーが2倍になるわけです。
今回は2画像のトランジション(遷移)を行うフィルタ(CIFilter)を紹介します。 利用するCIFilterフィルタのタイプは「CIModTransition(モッドトランジション)」です。このCIModTransitionフィルタは、不規則な形状の穴を通して別画像を表示しながら、ある画像から別の画像への遷移を実行します。こうしたトランジション処理の場合には、最低でも3枚のビデオバッファが必要となるわけです(処理内容によってはもっと多い)。
搭載量の少ない主メモリをビデオメモリと分け合っているiOSデバイスでは、メモリ使用量を増やすとシステム全体の不安定要因を増大させてしまいます。ですから、あまり複雑なフィルタ処理は御法度だと想像が付きます(物理的に無理)。近い将来、こうした物理的な制限が緩和されれば、iOSのCore ImageもMac OS X同等のフル仕様に準拠するかもしれません。
それでは、さっそくトランジションフィルタのサンプルを見ていきましょう。まず最初に、CIModTransitionフィルタに設定できるパラメータの内容を確認してみます。
・inputImage
変換元の画像をCIImageで設定する。
・inputTargetImage
遷移の終着点となる画像をCIImageで設定する。
・inputCenter
トランジション領域の中心をX およびY座標で指定するCIVectorで設定する。
・inputRadius
中心から穴のふちまでの距離を指定する値をNSNumberで設定する。
・ inputTime
トランジション時間を指定する値をNSNumberで設定する。デフォルト値はゼロで、範囲はゼロ〜1となる。
・ inputAngle
穴の角度を指定する値をNSNumberで設定する。デフォルト値は2で、範囲は -2π〜2πとなる。
・inputCompression
穴に適用する圧縮を指定するスカラー値をNSNumberで設定する。デフォルト値は300 で、範囲は1〜800となる。
setupTransitionメソッドでは、上記のパラメータの幾つかを設定してCIModTransitionフィルタ(CIFilter)を作成しています。 imageForTransition:メソッドの方は、時間の経過と伴にフィルタ状況(表示結果)を変化させるために、幾つかのパラメータの設定し直しを担当しています。
- (void)setupTransition // フィルタの作成 { self.transition=[CIFilter filterWithName: @"CIModTransition" keysAndValues: @"inputCenter",[CIVector vectorWithX:imageWidth*0.5 Y:imageHeight*0.5], @"inputAngle",[NSNumber numberWithFloat:M_PI*0.1], @"inputRadius",[NSNumber numberWithFloat:radiusValue], @"inputCompression",[NSNumber numberWithFloat:10.0],nil]; } - (CIImage *)imageForTransition:(float)time // フィルタのパラメータ変更 { if( fmodf( time,2.0 ) < 1.0 ) { [transition setValue:sourceImage forKey:@"inputImage"]; [transition setValue:targetImage forKey:@"inputTargetImage"]; } else { [transition setValue:targetImage forKey:@"inputImage"]; [transition setValue:sourceImage forKey:@"inputTargetImage"]; } [transition setValue:[NSNumber numberWithFloat: 0.5*(1-cos( fmodf(time,1.0)*M_PI))] forKey:@"inputTime"]; return [transition valueForKey:@"outputImage"]; }
今回は、Core Imageのフィルタ処理で必要となるメモリ容量について考えました。次回は、今回取り上げたCIModTransitionフィルタを利用したサンプルソースコードの詳細を解説したいと思います。
-
Core ImageがiOSにやってきた!
この記事は、2011年06月16日に掲載されました。前回は、Core Imageの機能を確認するために「Core Image Fun House」を紹介しました。今回は、Core Imageを自作アプリで活用するために、まず最初に読まなければいけない技術ドキュメントと簡単なフィルタ処理のソースコードを紹介したいと思います。
予想通りWWDC2011のキーノートで、iOS 5にCore Imageが実装されることが判明しました。これにより、複雑な画像フィルタ処理を自分自身でアプリに実装する必要がなくなります。フィルタ処理を売り物にしているアプリは、デベロッパー全員が似た機能を簡単に使えるようになるので競争が厳しくなるかもしれませんが、まったく別系統アプリで、UIがらみでフィルタをちょいと使いたいような場合には大いに助かります(笑)。こうなると、最後の大物「Quartz Composer」がiOSに搭載される日も近いような気もしますが、こちらに関しても本連載で取り上げる予定です。
Apple社が提供しているCore Imageに関する技術ドキュメントについては、まず最初に以下を参照します。
「CoreImage Programming Guide」(CoreImageプログラミングの詳細な解説)
「Core Image Filter Reference」(CoreImage搭載のフィルタのリファレンス)
「Image Unit Tutorial」(Mac OS Xに搭載されているImage Unitフィルタの解説)
「Core Image Kernel Language Reference」(フィルタ用言語のリファレンス)
「Core Image Reference Collection」(CoreImageの全APIのリファレンス)Core Imageのアプリでの使用方法に関しては「CoreImage Programming Guide」を参照することになります。実はこれのかなり古い版が、日本のアップルサイトに和訳されて「Core Image プログラミングガイド」として掲載されています(PDFではない)。iOSで採用されたことから、近々に最新版が和訳されることを期待したいと思います。どんなフィルタが存在するのか?その種類(現在130以上ある)やパラメータの設定については「Core Image Filter Reference」の方を参照します。自分自身でCore Imageのフィルタ用Plug-inを作成したい場合には、「Image Unit Tutorial」や「Core Image Reference Collection」を参照します。
Mac OS Xには、画像を操作するための便利なフレームワーク「ImageKit」が用意されています。Mac OS X 10.5からは、アプリからフィルタの種類を選択する時に、ImageKitの「Filter Browser」(IKFilterBrowserPanel クラス)を使えるようになりました。またフィルタのパラメータ設定には、同じくImageKitの「Filter User Interface View」(IKFilterUIViewクラス)を利用できます。これらの機能に関しては「ImageKit」側の技術ドキュメントに詳細が記載されていますので、以下のドキュメントも参照されることをお勧めします。ちなみにImageKitについても、本連載でその詳細を解説する予定です。
「Image Kit Programming Guide」(ImageKitの利用方法の解説)
「CIFilter Image Kit Additions」(Mac OS X 10.5でのCIFilterクラス拡張)
「Image Kit Reference Collection」(ImageKit APIに関するリファレンス)まずは自作アプリにCore Imageのフィルタ処理を実装してみましょう。使用するフィルタは画像の露出を調整できる「CIExposureAdjust」にします。先ほど紹介した「Core Image Filter Reference」で処理内容を調べると、以下の様に記載されています。演算処理は「s.rgb * pow(2.0, ev)」です。処理時のevは露出パラメータで、-10.0から10.0の浮動小数点です。
XcodeプロジェクトのMainMenu.nibファイルのウィンドウ(NSWindow)に、UIViewを継承したCIExposureViewとスライダー(NSSlider)をレイアウトし、ソースコードでCIExposureViewクラスを実装します。スライダーは、値を-10.0から10.0まで変更できるように設定しておきます。 以下がクラス定義をしているCIExposureView.hです。
@interface CIExposureView:NSView { CIFilter *exposurefilter; CIImage *sourceImage; float exposureValue; } - (IBAction)sliderChanged:(id)sender; @end
フィルタ処理の結果は、UIViewを継承したCIExposureViewクラスのdrawRect:メソッドで描画します。フィルタ処理する画像はCIImage(NSImageやCGImageRefではない)として用意しておき、処理の環境の方はCIContext(NSGraphicsContextやCGContextRefではない)で指示する必要があります。関連する変数定義やメソッドの先頭2文字はCGではくCIですので注意してください(実に紛らわしい)。 以下はクラス実装を行うCIExposureView.mに記述した2つのメソッドです。
- (IBAction)sliderChanged:(id)sender // スラーダーのIBAction { exposureValue=[sender floatValue]; // 露出のパラメータを得る [self setNeedsDisplay:YES]; // 画像の再描画指示 } - (void)drawRect:(NSRect)rect { CIContext *context; // NSGraphicsContextやCGContextRefではない! CGRect crt; // Quartz2Dと同じくCGRectを使う if( context=[[NSGraphicsContext currentContext] CIContext] ) { if( ! exposurefilter ) { if( sourceImage=[[CIImage imageWithContentsOfURL:[NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource:@"Rose" ofType:@"jpg"]]] retain] ) // 画像を読み込む exposurefilter=[[CIFilter filterWithName:@"CIExposureAdjust" keysAndValues:@"inputImage",sourceImage,nil ] retain]; // フィルタ作成 } if( exposurefilter ) { crt=NSRectToCGRect( rect ); // Quartz2Dと同じくCGRectを使う [exposurefilter setValue:[NSNumber numberWithFloat:exposureValue] forKey:@"inputEV"]; // 露出のパラメータ設定(-10.0~10.0) [context drawImage:[exposurefilter valueForKey:@"outputImage"] atPoint:crt.origin fromRect:crt]; // 結果を描画 } } }
今回は、Core Imageを自作アプリで活用するために、まず最初に読まなければいけない技術ドキュメントや簡単なフィルタ処理のソースコードを紹介しました。次回はもう少し異なるフィルタ処理の例を、ソースコードと共に紹介したいと思います。
本日のサンプルのXcodeプロジェクトはこちらから…。
-
Core Imageとは何ぞや?
この記事は、2011年06月02日に掲載されました。今回から、Apple社が提供しているドキュメントやサンプルソースコードを参照しながら、Mac OS X特有のフレームワークやプログラミングモデルを解説していきます(iOSも意識して)。まず最初に、来週開催のWWDC2011で発表されるiOS 5に実装されていそうな「Core Image」を取り上げてみます。
Core Imageフレームワークは、Mac OS X 10.4から採用されました。それ自身はCore Animationと同じくQuartzCoreフレームワークに属しています。Macアプリでの役割はリアルタイムイメージプロセッシング(フィルタや合成処理)です。具体的には、GPUの高い演算能力を使い、表示画像に対して高速なフィルタ処理を実行します。このフレームワーク、Apple社が提供しているあらゆるアプリで活用されているのですが、何故だかiOSには実装されていませんでした。これは、GPUのシェーダプログラムによるリアルタイムレンダリングが、OpenGL ES 2.0でしかサポートされていないのが原因なのかもしれません(もしくは古いデバイスのGPU能力不足?)。
実は、Core ImageがiOS 5で採用されそうだと考える証拠があります。それは、iPad 2からデフォルトで搭載された「Photo Booth」アプリの存在です。リアルタイムでエフェクトをかけならがカメラ撮影ができるアプリですが、このアプリの原型はMac OS Xにも搭載されています。こちらを一目見れば、内部でCore Imageが使われていることは明白ですので、iOSでも同様に使われていると推測したわけです(隠しAPIとして)。もしiOSで採用されれば、色々と面白い目的に活用できるので有り難いのですが、こればかりはApple社の方針に関わることなので実現するかどうかは不明です。
手っ取り早くCore Imageの実力を試してみるのには、SDKインストール先のDeveloperフォルダ内(Developer/Application/Graphic Tools/)に保存されている「Core Image Fun House」というツールを使ってみます。これは、Core Imageが実現しているフィルタ処理の動作確認を行うためのツールアプリです。
このアプリを起動し、画像を選択してウィンドウに表示したら、右側に表示されるフィルターのパラメータ設定パネル(Effect Stack)の「+」ボタンをクリックします。すると、カテゴリー別にフィルタ種類を選択するためのブラウザ(Image Units)が表示されます。
ここで試してみたいフィルタを選ぶと、その処理のパラメータを入力できるパネルが既存の下方に追加されますので、そこでパラメータを変更して効果を確認します。フィルタの種類は下に向かってどんどん追加して行くことができます。次の例では「Box Bluer」(ぼかし)フィルタがひとつ追加され、そのパラメータをスライダーで変更できるようになった状態を示しています。
このツールですが、面白いことに「Mac OS X Developer Library」サイトからソースコード(Xcodeプロジェクト)を入手することができます。ですから、このアプリでCore Imageが実行していることは、すべてソースコードとして追いかけることが出来ます。これはデベロッパーとして大変ありがたいことです(Sample Codeの「FunHouse」で検索)。
このツールを使ってみると、Core Imageが内部で何をしているのかだいたい検討がつきます。前にも言及しましたが、こうしたフィルタ処理はGPUの演算能力を使うので、その性能はMacに搭載さいれているGPUの処理能力に依存します。例えば、メニューから「Preferences…」を選び、環境設定ダイアログで「Use Software Renderer」をチェックしてみてください。こうするとCPUの能力のみでフィルタ処理が行われるように変更されます。わざと重たいフィルタ処理を選んでGPUとの速度差を体感してみるのも一興です。
今回は、Core Imageの能力を確認するために「Core Image Fun House」を紹介しました。次回は、Core Imageの機能を自作アプリで活用するために、まず最初に読まなければいけない技術ドキュメントや簡単な処理用ソースコードなどを紹介したいと思います。
-
残り物には福があるのか?
この記事は、2011年05月21日に掲載されました。前回で「Carbon視点でiPhone探求」は一旦終了です。iOS側に新しい革新技術でも投入されたら、また何かしらアプリ開発を例題に再開したいと思います。今回からは、iOSの環境と比較しながらMac OS Xアプリの開発についての解説をしたいと思います。本連載は「Carbon視点でCocoa探求 」(2007年)で始まり、「Carbon視点でiPhone探求」(2008年)と続いて、またまた「Mac OS X」の話に戻るわけです。まさに「Back to the Mac」ですね!
iOS登場時、Mac OS Xから移植されていたフレームワークは限定されていました。実際には移植されていたかもしれませんが、開発者にはオープンになっていませんでした。コピー&ペーストもできなかった時代が懐かしく思い出されます(笑)。そのiOSも4.3になり、Mac OS Xのフレームワークの大部分は既に移植済み。最近では、マルチコアCPUに処理を最適化するための技術「BlocksとGCD(Grand Central Dispatch)」までもMac OS Xと同等に利用できるなど、そのペースはさらに加速しています。細かなものを除き、Mac OS Xでしか利用できない大規模なフレームワークやプログラミング技術は少なくなりました。次に示すのがその例です。
・Cocoa Binding(プログラミングモデル)
・ガーベージコレクション(メモリ管理)
・QuartzComposer(マルチメディア)
・AppleScript(スクリプト関連)
・OpenCL(マルチコア演算処理)
・Core Image(画像エフェクト)
・Image Capture (画像ファイル取り込み)
・ImageKit(画像ファイル関連)
・PDFKit(PDFハンドリング)
・QuickTime(マルチメディア)
・QTKit(ビデオ関連)
・IMKit(日本語入力)
・IOKit(デバイスドライバ)アプリ開発のプログラミングに直接関係する技術としては「Cocoa Binding」と「ガーベージコレクション」があります。iOSでガーベージコレクションが使えないのは、メモリ消費やバッテリ消費に問題があるのでしょうか?それともデベロッパーにメモリ管理の知識をしっかり植え付けさせるために導入を控えているのでしょうか(笑)。筆者は、Macアプリでもガーベージコレクションは使わないので、これに関しては影響はありません。「Cocoa Binding」(バインディング)とは、専用コントローラでモデルとビューの「値の同期」を提供する技術です。モデルオブジェクトには、「Core Data」のNSManagedObjectクラスなどが使われます。Xcode 3でのバインディング操作は別ツールのInterface Builderで行いましたが、Xcode 4からは、その機能がビルトインされています。
ビューとコントローラの接続には慣れていて直感的で分かり易いのですが、モデルとビューの接続では状況が違ってきます。どこか一カ所設定を間違うと誤動作するのは前例と同じですが、後からの判読性が極端に悪く、間違いを見つけるのに大変苦労します。Bindingを使えば、ちょっとした定型アプリなら即座に開発できて大変重宝しまが、メンテナンスを考えると、ソースコードで記述した方が正解だと考えるプログラマーがいるのも事実です(筆者のこと)。Apple社がCocoa Bindingの仕組みをiOSに導入しないのにはそうした理由があるのかもしれません。iOS 5で導入されるかどうか分かりませんが、Xcode 4.1(シーディング中)にはその気配が無いようです。AppleScript、IMKit、OKitなどは、iOSのセキュリティ制限から今のところ移植され可能性は無いと思います。しかし、IMKitに関してはぜひ実装してもらいたですね。システム上で色々な日本語入力方法が使えるのは、iOSデバイスユーザとしては嬉しいことです。ATOK変換や7noteの手書き入力などがどんなアプリでも利用できることを夢見てます。それとは逆に、QuartzComposerやCore Imagなどは、将来のiOSに実装される可能性は高いでしょう。特にCore Imageについては、iOS 5で実装されそうな気配です。iPad 2に搭載されている「Photo Booth」などのアプリを見ると、既に隠れAPIとして実装済みかもしれません。
QTKit、ImageKit、PDFKitなどは、Mac OS Xでのアプリ開発で非常に便利に使わせてもらっているので、ユーザインターフェースをマルチタッチ(UIKit)版に改良したタイプの搭載を望みます。 カバーフローやサムネイルでの画像選択とか、ちょっとした画像の拡大縮小、回転、トリミングなどが手軽に行えるツールに期待したいところです。Image Captureは、iOSデバイスでは「写真」アプリがその機能を受け持ち、アプリ側からは間接的に画像や映像を参照できますので、今のところそんなに不便は感じません。OpenCLについては、iPad 2などで大規模演算処理を行う需要が無い限りは実装はされないでしょうが、高度なメデイア処理など…これからは何が起こっても不思議ではありません。
QuickTimeIはMac OS Xでも64Bitアプリでは使用不可なので、Mac OS X 10.7(Lion)では、別フレームワークである、AVFoudnation、Core Meda、Core VideoとOpenGLの連合軍に取って替わられそうです。興味深いのは、AVFoundationは既にiOSには実装済みですが、Mac OS X 10.6(Snow Leopard)には実装されていないことです。まさに掟破りの逆卍フレームワークです(笑)。ところが、これらのフレームワークの関連ヘッダファイル(.h)を見ると(例はAVBase.h)Mac OS X 10.7で実装される準備が既に完了していることが分かります(そこにはiOS 5の記述も…)。
// AVBase.h #import <Availability.h> #import <Foundation/NSObjCRuntime.h> // Pre-10.7, weak import #ifndef __AVAILABILITY_INTERNAL__MAC_10_7 #define __AVAILABILITY_INTERNAL__MAC_10_7 __AVAILABILITY_INTERNAL_WEAK_IMPORT #endif // Pre-5.0, weak import #ifndef __AVAILABILITY_INTERNAL__IPHONE_5_0 #define __AVAILABILITY_INTERNAL__IPHONE_5_0 __AVAILABILITY_INTERNAL_WEAK_IMPORT #endif
世の中には未だにiOSをMac OS Xサブセットと言う人がいますが、それは現場を知らないデベロッパーとは無関係な方々です。状況は刻一刻と変わっており、後発であることから、iOSのフレームワークの方がCocoaやObjective-Cとの相性で一歩先んじているケースも多々あります。そしてiOSで実績を積んだフレームワークが、逆にMac OS Xに還元される時代が到来しています。将来的に2つのOSがどの様に融合していくのか(させて行くのか)Macデベロッパーの楽しみ(苦しみ)は尽きることはありません。
次回からは、Apple社が提供しているサンプルソースコードを参照しながら、Mac OS X特有のフレームワークやプログラミングモデルを解説していきます(iOSも意識して)。まずは何から始めるのか思案どころですが、6月のWWDCで登場するiOS 5で実装されていそうな「Core Image」を取り上げてみます。