はじめに
GPU もしくは GPGPU 開発経験がある方なら恐らくわかると思いますが、GPU 演算において一番のネックは GPU と CPU の間のデータ転送スピードです。そのため、例えば GPGPU 開発でしたら一番重要なポイントは CPU からは必要なデータを GPU に転送してから、なるべく CPU との通信を一切せず、全ての演算を GPU で完結させて必要な結果データのみ CPU に返すことです。描画においても同様で、基本はなるべく必要なデータは全て一度の転送で終わらせて GPU に任せたい演算は GPU 内で完結させることを心がけることが重要だと思います。なのに…なんと iOS の CALayer は filters プロパティをサポートしないと今日はじめて知って絶賛 orz 状態なう。
filters プロパティとは
Apple 公式開発資料によりますと、要は CALayer に必要な CIFilter(Core Image Filter)オブジェクト配列を filters プロパティとして設定することで、レイヤー効果を与えることができます。例えばセピア色に変色したり、乗算ブレンドで色調整をしたりするにはとても手軽に扱えます。しかし、その注意点にも書いて有ります通り、iOS 上の CALayer はこのプロパティをサポートしていません!
ようわからんからとりあえずなんかフィルター掛けてみてよ
例えばの話(ちょっとpixivで絵素材をお借りします)、ここにバカ、もとい、チルノがあるとするじゃん?
でもこのままだとなんか迫力ないね(失礼)…よしちょっとフォトショで色を調整してなんか強そうにしてみましょう。とりあえずクリッピングマスクでレイヤーを一枚追加しましょう。そして #00FFFF
の色で塗りつぶしましょう。
クリッピングマスクとして追加するので、下のベースレイヤーの不透明部分のみ色がかかってしまいますが、このままではただのオーバーレイでありキャラがわからないから全然強そうに見えませんよね!そこでちょっと魔法を掛けます。右のレイヤー描画モードがありますので、追加したレイヤーの描画モードを「乗算」にします。するとあら不思議、なんか魔法のようにチルノがいっそアホ、じゃなくて、強く見えてきました!
「乗算」ってなに?
完全に初心者向けのような話ですが…まあそううざがらないでもうすこし付き合ってくださいな。まず一般的な画像の色は RGB で表現していることはお分かりかと思います。そこで各色要素をある値で掛ける(乗算する)と、違う値が当然出てきます。例えば上記の例は、クリッピングマスクの色が #00FFFF
となるので、RGB 要素はそれぞれ 0
、255
、255
となります。しかしこの値はあくまで UInt8
で表現するための値であって、実際はそれぞれ UInt8.max
、すなわち 255
で割った値が実際の RGB 値です。ですので実際のそれぞれの RGB 値は 0
、1
、1
です。そしてこれらの値を元画像のそれぞれの RGB 要素と掛けてみると、当然 R(Red
)は 0
と掛けるので 0
となり、G(Green
)と B(Blue
)は 1
と掛けるので元通りの値となります。そなわち、元画像を #00FFFF
で乗算フィルターかけると、赤みが全て抜けてしまい明るい緑っぽい青の色相になります。そのため最終的には上記の画像のようにバカ、じゃなくて強そうに見えます。
CIFilter で乗算フィルターを作る
さてみなさん、Xcode の Playground を開いて実際やってみましょう。画像を Playground に入れてファイル名に合わせばソースコードは下記のようにそのままコピペで使えます:
// Playground - noun: a place where people can play
import UIKit
// オリジナル画像を読み込もう(ファイル名の設定忘れないでね)
let baseImage = UIImage(named: "image.png")!
let baseCIImage = CIImage(image: baseImage)
// Core Image フィルターを #00FFFF の色で設定する
let filterColor = CIColor(red: 0, green: 1, blue: 1)
let colorGenerator = CIFilter(name: "CIConstantColorGenerator")
colorGenerator.setValue(filterColor, forKey: "inputColor")
let filterImage = colorGenerator.valueForKey("outputImage") as! CIImage
// オリジナル画像に先ほど設定した色で乗算で掛けましょう
let multiplier = CIFilter(name: "CIMultiplyCompositing")
multiplier.setValue(filterImage, forKey:"inputImage")
multiplier.setValue(baseCIImage, forKey:"inputBackgroundImage")
let filteredCIImage = multiplier.valueForKey("outputImage") as! CIImage
// 結果画像
let filteredImage = UIImage(CIImage: filteredCIImage)!
ここで問題点!
さて、これでフィルターを掛けられるのはいいのですが、実際に使う時は一つ面倒な点が出てきてしまい、それは画像を表示するときは UIImage
のままでは使えず、UIView
とか UIImageView
で設定しないと表示できません、ということです(もちろん drawAtPoint
メソッドもいいですがそれも UIView
か CALayer
に描画命令を入れているので、要するに UIView
オブジェクトがなければただ UIImage
オブジェクトのみでは表示できません)。最初に言ったとおり、CALayer
で filters
プロパティが使えない以上、フィルターを掛けた画像を表示するためには、
画像読み込み(CPU の仕事)→ 画像に
CIFilter を適用する(GPU の仕事)→ フィルター掛けられた画像を取得し、UIView に渡す(CPU)→ 画面に表示する(GPU)
という一連の流れとなり、ご覧のとおり CPU と GPU との通信は矢印の数だけ発生します。おまけに CIFilter
は結構いろんなオブジェクトをセットしたりゲットしたりと割と手間のかかる無駄な作業になるため、いくら Core Image
が速くても、非常に無駄なところで無駄な作業が発生してしまうのでとても効率が悪いです。
ちょっと改善
CIFilter
の設定は様々のオブジェクトをセットしたりゲットしたりするのが非常に非効率なので、直接低レベルな Core Graphics
関数で弄ってみます。CIFilter
で設定するときと同じように、ファイル名だけ合わせておけば下記のコードはコピペでそのまま使えます。
// Playground - noun: a place where people can play
import UIKit
// オリジナル画像を読み込もう(ファイル名の設定忘れないでね)
let baseImage = UIImage(named: "image.png")!
// オフスクリーン描画の初期化設定
UIGraphicsBeginImageContext(baseImage.size)
let ctx = UIGraphicsGetCurrentContext()
let rect = CGRect(origin: CGPointZero, size: baseImage.size)
// Core Graphics と UIImage は座標系違うので変換しておく
CGContextTranslateCTM(ctx, 0, baseImage.size.height)
CGContextScaleCTM(ctx, 1.0, -1.0)
// 描画モードを Multiply に設定しておく
CGContextSetBlendMode(ctx, kCGBlendModeMultiply)
// 元画像をオフスクリーンキャンバスで描く
CGContextDrawImage(ctx, rect, baseImage.CGImage)
// 元画像をクリッピングマスクとして描画領域を設定する
CGContextClipToMask(ctx, rect, baseImage.CGImage)
CGContextAddRect(ctx, rect)
// フィルターを設定する
let filterColor = UIColor(red: 0, green: 1, blue: 1, alpha: 1)
filterColor.setFill()
CGContextDrawPath(ctx, kCGPathFill)
// オフスクリーン描画結果を UIImage として出力する
let filteredImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
すると上記の CIFilter
を使った処理と同じ結果が得られます!
この方法では手間のかかるオブジェクトのセットやゲットをやらないので、CIFilter
を使うより更に単純なのでそれなりに高速化が確認できます。もちろん Playground でもその結果が確認できますが Playground はあくまでシミュレーターなのでただでさえ効率悪いからその違いはあまりにも目立ち過ぎます(当方では iMac 27'' Late 2012 エントリーモデルで、CIFilter
を使うと1秒以上かかるが直接 CGContext
で演算すると 0.1 秒以下で終わります)からちょっと微妙に参考にならないと思いますが、まあ実機で確認してもわかると思いますが相当速いです。for ループで回せば体感でわかるほど速いです。
タイトルに戻る…
でも結局のところ、CGContext
でオフスクリーン演算しても、CPU と GPU の間の通信回数は減らないので、できればやっぱりオンスクリーン演算で、
画像読み込み(CPU の仕事)→ Core Graphics でフィルターを掛ける(GPU)→ そのまま演算結果を描画する(GPU)
というきれいな流れにしたいのですね…OpenGL とか使わずに UIKit のみで実現できないのかしら…