技術

CALayerをビットマップ化

CALayerって影を付けるとやたらと遅くなるのでビットマップ化するようにしたところ速度が劇的に改善(というか影で劇的に遅くなるのを回避しただけだけど)したのでサンプルコードをご紹介。

#import <Foundation/Foundation.h>
#import <QuartzCore/CALayer.h>

@interface CALayer (Additions)
- (void)bitmapnize;
@end

@implementation CALayer (Additions)
void reverseAllShadowOffsetY(CALayer *layer) {
    for (CALayer *l in layer.sublayers) {
        if (l.shadowOffset.height != 0.0f) {
            l.shadowOffset = CGSizeMake(l.shadowOffset.width, -l.shadowOffset.height);
            reverseAllShadowOffsetY(l);
        }
    }
}

- (void)bitmapnize {
    CGContextRef context = NULL;
    CGColorSpaceRef colorSpace;
    int bitmapByteCount;
    int bitmapBytesPerRow;
    
    CGFloat scale = self.contentsScale;
    
    int pixWidth = (int)([self bounds].size.width * scale);
    int pixHeight = (int)([self bounds].size.height * scale);
    
    bitmapBytesPerRow = (pixWidth * 4);
    bitmapByteCount = (bitmapBytesPerRow * pixHeight);
    
    colorSpace = CGColorSpaceCreateDeviceRGB();
    context = CGBitmapContextCreate(NULL,
                                    pixWidth,
                                    pixHeight,
                                    8,
                                    bitmapBytesPerRow,
                                    colorSpace,
                                    kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);
    if (context == NULL) {
        NSLog(@"Failed to create context.");
        return;
    }
    
    // 全体を上下逆にしても影だけy座標が反転してしまう(右下に出てた影が右上に出るなど)ので予め反転させておく
    reverseAllShadowOffsetY(self);
    
    CGContextTranslateCTM(context, 0, pixHeight);
    CGContextScaleCTM(context, scale, -scale);
    // context にレンダリング
    [self renderInContext:context];  // sublayers もレンダリングされる
    CGImageRef image = CGBitmapContextCreateImage(context);
    
    self.sublayers = nil;
    self.contents = (id)image;
    
    CFRelease(image);
}
@end

ビットマップ化の処理自体はお決まりというかどこかで見たことのある処理かと思います。
それにretina対応のコードと影のオフセットが上下逆になってしまう問題への対処のコードを追加して、カテゴリでCALayerを拡張するようにしてます。

なのでこのソースをファイルに保存して#importするだけで、(これもまたネーミングセンスがないですが)bitmapnizeというメソッドがCALayerに自動的に追加されます。

処理の内容としては、CALayerの内容をいい感じにビットマップ化したのち、(ここ重要)サブレイヤーを全て削除してビットマップの内容に置き換えます。
破壊的です。

本当は、CALayerを継承して随時サブレイヤーにもアクセス可能で何回もbitmap化可能なクラスがより使い勝手がいいんでしょうけど、今回そこまでは必要なかったので手っ取り早くカテゴリで、かつ破壊的なやり方を実装してます。
まぁ多分このソースをそういう使い勝手の良いクラスに昇華させるのは簡単なんじゃないかなと思います。

ビットマップ化の処理自体は重いはずなので、初期化処理で一度だけとか、内容を変える必要があるときだけとかに使うべきです。

あと影。
影はレイヤーのboundsの外に描画されるので、そのままbitmapnizeしても影が途切れてしまいます。
なので少し大きめのレイヤーの中に含めるようにして、そのレイヤーに対してbitmapnizeするとうまくいきます。

bitmapnizeすると本当にただの1枚絵になるというある意味究極的な方法なので、その後の速度的なデメリットはCALayerを使う限りは無いはずです。

あとレイヤーのcontentsScaleを見て処理するので、予めそこに[[UIScreen mainScreen] scale]をセットしとくとretinaディスプレイでも綺麗に描画出来ます。

あと副作用的ではありますが、ビットマップ化するので、サブレイヤーで複雑な階層を構築して表現したイメージへ、contentsプロパティからアクセス出来るようになります。
つまり以下のような事が可能になります。

CALayer *a = [CALayer layer];
・・・a内部に色々レイヤーを追加して複雑なグラフィックを表現するための処理・・・
[a bitmapnize];

CALayer *b = [CALayer layer];
b.contents = a.contents; // これだけでaの内容がbにコピー出来る

contentsプロパティへのセットの処理が具体的にどうか(同じビットマップデータを共有するのか、複製されるのか)は知りませんが、内容のみをコピーする処理を楽に書けるのは間違いないでしょう。
といってもこれがどのくらい有用なのかは謎。(例えば、Objective-Cの標準的な方法で単にインスタンスをコピーする事に対するアドバンテージはあまりなさげな気が。)

でまぁ、これを書く前に影用のレイヤーを別にする方法(多分表面の物体と同じ形で黒いのを用意してそれを少しずらして裏に配置する方法かな)はどうかなんてアドバイスを頂いたんですが、考え方は確かにシンプル。
とはいえ、今回のこれと同じレベルの使い勝手のものを作るとなると、

  • 表のCALayerをコピーして黒く塗りつぶす。単にCALayerといってもコードで描画された図形から、ファイルから読み込まれた画像、その他諸々の場合に対処しなければならない。
  • 影レイヤーにガウシアンフィルタとかを掛ける。
  • 表と影のレイヤーをまとめて扱うために、さらにコンテナレイヤーでまとめる。これは例えば単純計算で1000レイヤーまではなんとかグリグリ動かせてた環境で、333レイヤーまでしかグリグリ動かせなくなる事になる。コンテナを無視しても半分。

というのを実装しなければなりません。
もしかして、影のデータを手作業で予め作っとけという話だったのだろうか(例えばフォトショのマクロとかで自動化は出来るだろうけど、ユーザが変更可能な内容に対しては製作時の自動化は役に立たないし)。
上記の実装を今回のビットマップ化の処理より簡単に書く方法があるのを私が知らないだけという可能性も高いので自信はないのですが。

なんか取り留めの無い内容になってしまいましたが、今回これを実装しようと思ったのは、iPhoneのホーム画面のようなアイコンの並び替え処理の動きを、アプリで再現したくて普通に実装したら、アイコンにも影、テキストにも影がついてるので、各アイコンがふるふるするときや並び替えするときに、fpsが著しく落ちたからです。
今回これを実装したおかげで、以下のように滑らかに動くようになりました。
滑らかには見えないところもあるかもしれませんが、操作してる人がぎこちないだけで、実際ヌルヌルです。
iPhoneのホーム画面みたいな動き その3 – YouTube

iPhoneプログラミングはOpenGLESばかりやってたので、この辺のAPIはよく知らなかったんですが、結構使いやすい印象を受けました。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください