cacheAsBitmapの法則

FlashのcacheAsBitmapの謎が解けました。cacheAsBitmap有効な場合、「ビットマップとして半透明になった感じ」を期待しているところで、なぜか「下位構造のシェイプが全部透けてる感じ」になります。

フィルタがひとつでもあれば、ビットマップとして半透明(中身のシェイプが透けていない)な絵になることから、「cacheAsBitmapを明示的に有効にしても、フィルタがないとビットマップキャッシュが効かない」ように見えます。

「何もしないフィルタを入れる」裏技を適用していないcacheAsBitmap=trueは、本当にビットマップをキャッシュしているんでしょうか?

FlashのcacheAsBitmapは、マニュアルの説明では、

「filtersがあると自動的に有効になる」

と説明されています。そこから、明示的に有効にした場合とフィルタによって自動的に有効化された場合とで、同じひとつの描画処理を通るんだと思えます。そのため、図の「フィルタなし・cacheAsBitmap=true」の見た目になったとき、なにか失敗したように感じます。

また、FlashのIDEでは、アルファ100%未満のシンボルの「ビットマップキャッシュを使用」チェックをつけると、編集画面内のプレビューでは「中身が透けていない半透明」になります。「ビットマップキャッシュを有効にできたときは、中身が透けないのだから、中身が透けてしまうのは何かまだ足りないのかな」と思ってしまいます。

でも、本当はそうじゃなかった。

結論を言うと、間違っているのはFlash IDEで、マニュアルには説明が足りていません。

じっさいに複雑な幾何図形を描画させて、実行パフォーマンスを比べてみました。すると、フィルタがあるかないかに関係なく、cacheAsBitmapが真か偽かだけが描画性能に関係していることがわかりました。cacheAsBitmapが有効になっているとき、たしかにビットマップキャッシュ機能は生きています。

テスト図形

  • cacheAsBitmap=false で露骨に遅い。
  • cacheAsBitmap=true で露骨に速い。
  • フィルタがあるとき、cacheAsBitmap=trueと同等の速さ。

そこで疑うのは、例によってマニュアルのニュアンスですね。どうやら、そもそも「cacheAsBitmapには単一の仕様があり、trueかfalseで二分岐するだけ」と思ってしまったことが間違っていたようです。実測の結果考えられる仕様は、

  • 「cacheAsBitmapの仕組みには、フィルタがあるときとないとき用で、異なる2つの実装がある」
  • 「この暗黙的な内部分岐は、グラフィックの見た目に影響する」

です。メタコードで書いてみました。

DisplayObjectにフィルタがあるとき
  1. ターゲットDisplayObjectのアルファ値を一時的に100%としてレンダリング
  2. ビットマップフィルタを適用。
  3. フィルタ適用後のビットマップに、もとのアルファ値をかける。
  4. キャッシュに保存。
  5. ステージに合成。
if not displayObject.cache:
  tmp = displayObject.alpha
  displayObject.alpha = 1.0
  bitmap = render(displayObject)
  foreach(filter in filters):
    filter.applyTo(bitmap)
  bitmap.alpha = tmp
  displayObject.cache = bitmap
else:
  bitmap = displayObject.cache
draw(bitmap)

中身が透けていない絵がキャッシュされる。

DisplayObjectにフィルタがないとき
  1. ターゲットDisplayObjectのアルファ値を、下位構造のすべてのシェイプに適用する。
  2. シェイプをレンダリングしたビットマップを生成。
  3. キャッシュに保存。
  4. ステージに合成。
if not displayObject.cache:
  foreach(shape in displayObject.allShapes):
    shape.alpha *= displayObject.alpha
  bitmap = render(displayObject)
  displayObject.cache = bitmap
else:
  bitmap = displayObject.cache
draw(bitmap)

中身が透けた絵がキャッシュされる。

ちなみにcacheAsBitmapが無効のとき
  1. ターゲットDisplayObjectのアルファ値を、下位構造のすべてのシェイプに適用する。
  2. ラスタライズしつつ、ステージに合成。

中身が透けた絵がキャッシュされず画面に描かれる。


filters = [new ColorMatrixFilter()] これ、バグ回避用のコードじゃなくて、ほんとに画像処理順序を使い分けるためのフラグだったんだ…。フラグならフラグで、DisplayObject.alphaPostMultiplyみたいな名前の、「DisplayObjectのalphaプロパティが、事後乗算されるか否か」を明示的に表すプロパティを別途設けるべきだったはず。「複雑なシェイプが高速化される」ことと「アルファが事後乗算される」ことを分けて説明でき、率直に内部仕様を表せる。シェイプの中身が透けないようにしたいと思ったとき、公式な方法があれば、「何もしないフィルタ」なんて裏技を持ち出す必要がない。ランタイム上で行儀悪いばかりか、普通のことをやるのに妙なトリックが必要だというシチュエーションは、複数人作業の生産性にマイナスの影響を与えます。

hogeSprite.filters = [
  new ColorMatrixFilter()
]; // 事後アルファ乗算で中の絵が透けないように

なんじゃこのコメント、ってなる。

hogeSprite.alphaPostMultiply = true;
// 事後アルファ乗算で中の絵が透けないように

と書いてあれば、コードとコメントが意味合致。ていうか、上のほうがもしコメントなしだったら、誰が理解できるのかな。

ブラックボックスのインターフェースが明確でないと、かならずいつか、仕様の誤解からくる致命的な問題に遭遇する----。

----あ、ていうか、FlashのIDEがもろにcacheAsBitmapの仕様間違って実装してんじゃん。身内でも理解できてないってことだよね、これ。