2015年10月3日土曜日

意外と仕組みがわからなかったcanvas

やりたいことははっきりしているが、実際どうやっているのかいまいちなことをただひたすら試行錯誤していました。
やりたいことはWindowsで言うところのROPを使いたいだけだったのですが、どこの属性なのか全く見当がつかずただやみくもに試行錯誤の末ようやく実現できたというだけのお話しです。

最初はCSSでウェッブページで扱うように簡単に表現できることから、まさかここまで見つからないとは思いもよらず途方にくれたので愚痴のつもりでだらだらやっていきますね。

やりたいことはcanvasのdraw~(RectangleとかCircleとか)の一般的な描画で単純な上書きではなく演算を行いたいということでした。

見た順番に書いていくと、似たような機能としてsetMaskFilterがあります。具体的なフィルターとしてBlurMaskFilterやEmbossMaskFilterがあります。
フィルターによって描画しようとするものを加工して描画させます。(漠然としすぎていますが(笑))
MaskFilterは描画しようとするもののエッジ部分を加工するのですが、残念ながら描画先との計算は行われません。BlurMaskFilterでぼやかされた部分のアルファチャネルが変化しているので似たような状態はなるのですが、実際にはダメです。実際に重なり合った部分が描画先のものに上書きされてしまうので思ったような効果が出せません。アルファチャンネルのおかげで似たような結果にはなるだけに、うまく効果を出せれば実現できるかと試行錯誤してみましたがやはりだめでした。


次にそれらしいものとしてsetColorFilterがあります。こちらの具体的なフィルターとしてColorMatrixColorFilter, LightingColorFilter, PorterDuffColorFilterがあります。
いろいろありますが、内容的には特定の色や値をもとにして実際の色をどのように描画するか変化させることができます。
3種類ありますが、結局描画しようとしてるものの色が変化するだけで描画先との演算は行われません。
はじめどのように作用するのかわからなく、PorterDuffColorFilterをnewするときに色を指定するのですがこの意味が理解できませんでした。メソッド名のColorFilterと言われれば確かに結果はその通りなのですが、もうすでに錯乱状態に陥っていてまともな思考ができなくなっています(笑)
PorterDuff.Modeとか指定できるし、どうしてそれを描画しようとするものと、描画先で使ってくれないのかと。

途方に暮れつつもcanvasやpaintクラスのメソッドを見てほかにもないのかといろいろと見たり、実際に検索してみたりしましたが、思ったような記事がなくやはり途方にくれました。

そんな中ふとPorterDuffXfermode(PorterDuff.Mode.xxxx)という部分が目に飛び込んできました。
使用しているメソッド名は、setXfermodeでした。
やりたいことは描画しようとする物と描画しようとしている先で演算したいということなので、演算方法を指定しているということはまさしくこれが見つけたかったメソッドではないかと直感しました。

Windowsで言うところのROP演算で、描画元と描画先の演算を行ってくれます。PorterDuffの説明でSrcとDstの話が必ず出てくるので、MaskFilterやColorFilterで行えるものと思ってしまったのが今回の長いドハマりの始まりでした。
Googleで公開されているドキュメントを何度も見てメソッド自体は知っていましたがまさか。というのが本音で、解ってしまえば、非常に簡単というお話です。

具体的にどのような説明があるかといえば、
Pass null to clear any previous xfermode. As a convenience, the parameter passed is also returned.

google翻訳
以前のxfermodeをクリアするには、nullを渡します。便利なように、渡されたパラメータも返されます。
こんな説明だけじゃどんな機能なのかわからないのも無理がないです。xfermodetっていう言葉はものすっごい一般的な言葉なのかな?私は解りませんでしたが。

さて実際に使えるとわかってしまうと、MaskFilterもColorFilterもとても重宝そうな代物です。
しかしながら、描画要素ごとに変化させるには非効率になる形で実装されています。
下のものはColorFilterの色を変化させて同じビットマップの丸を描画させています。
もとのビットマップは白い丸とその周りにガウスぼかしを行ってアルファ値を落とした白がある画像を使っています。
元が白なのでColorFilterではsetColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY))として表示させる色を変化させています。
この場合は、描画要素ごとに new  PorterDuffColorFilterが行われてしまうのでとても短命なオブジェクトが使い捨てされ続けています。実装している形を見ると、colorの再指定も可能なようにhideメソッドで実装されていたりしますが、最終的にupdateというメソッド内で
    private void update() {
        destroyFilter(native_instance);
        native_instance = native_CreatePorterDuffFilter(mColor, mMode.nativeInt);
    }
という形で破棄されて再作成されている様子がうかがえます。

Filterに限ったことではないのですが、Androidのjavaは結局ネイティブコードを実行するように実装されているために、どうしてもnative~という呼び出しで終わっているものが多く、canvasの描画が重たいと言われている原因なのだと思います。
ネイティブコードで描画部分を直接呼び出せれば、javaからのjni呼び出しのオーバーヘッドがなくなるのでかなりの高速化が見込まれます。が、直接呼び出せるのかはまだ試していません(笑)以前NDKで処理を軽くしようとしたときは、canvasを直接操作するのをあきらめた経緯があります。
NDK内部からjavaを呼び出して見たりといろいろ足掻いたのですが、SDKのソースを見る限りそのまま呼び出せそうなので時間があれば試して見たいのですが。

少し話がそれてしまいましたが、最初は処理が軽くなりそうなのでBitmapを用意して描画させてみました。ここまで来てしまうと、Bitmapを加工しながら描画するよりはdrawCircleなどの描画で済ませてしまった方がよさそうに感じてきます。
ぼやかしてる効果はMaskFilterを使ってはみたいのですが、MaskFilterは描画処理が重たくなってしまいます。
PCだと、ただ処理が重たいだけなら見苦しいだけなのですが、Androidの場合は消費電力や発熱してしまうためにPC以上に神経を使わなければなりません。が、重たい処理書いてたりもしますけど(笑)

ここまでくるとやっぱりNDK使わなきゃダメかな。

0 件のコメント:

コメントを投稿