2013年1月31日木曜日

ドット絵におけるsvgのパフォーマンス検証

ここ数日ドット絵からsvgを生成するスクリプトを書いていたのですが,一点気になることがありました.生成するsvgの内容によって描画パフォーマンスに驚くほどの差が出たのです.となればその差はどこから生まれているのかに興味が湧いてきます.そこで今回はsvgの描画パフォーマンスについて検証してみましょう.

※検証コードへのリンクを追加しました.

ドット絵をsvgに変換する意義


ドット絵と言えば,かつてファミリーコンピュータを代表とした家庭用ゲーム機におけるグラフィック表現として標準的なものでした.昨今では3dを主体としたグラフィックが主流となってしまいましたが,ドット絵のもつ独特な味わい深さから,今日でも一表現手法として様々な場面で利用されています.

さて,本来ドット絵はラスタ画像として扱われることが多いのですが,これをsvgとする意味はあるのでしょうか?実は有るのです.

ドット絵の特徴として,拡大表示時にドットの境界をはっきりとさせたいと言ったことがあります.風景画像において,アンチエイリアス処理を使うことで美しい拡大結果が得られるのとは対照的な要望です.

よってこれらをwebで利用する場合,その利用場面に応じてアンチエイリアスの有無を切り替えたいところなのですが,一般的に用いられているcssにはこれを制御するための仕組みが存在しません.例えばかつてcss3にはimage-rendering(optimize-contrast)と呼ばれるプロパティが存在していましたが,css3の策定過程で削除されてしまいました

一方svgにはこのラスタ画像の描画品質に関わる属性としてimage-renderingが定められているのですが,明確なアンチエイリアス処理の切り替えとしては記述されておらず,ブラウザ毎の実装の差として現れてしまっています.

かいつまんで言うと,chrome(webkit?)において現状アンチエイリアスを切る方法が存在しないのです.

従ってこの問題を解決するのであれば,ベクタグラフィックをくっきりと描画することが出来るsvgの力を借りる必要があるのです.

svgでドット絵を表現するストラテジ


では,svgでドット絵を表現する際,どのような方針が考えられるでしょう?概ね次の二つが考えられるでしょう.
  1. ソースとなるドット絵の1ピクセル毎にrect要素(正方形) をあてがって,svg上に擬似ラスタ画像を構成する.
  2. ドット絵を構成する色毎にpath要素を用意し,ジグザグなパス領域を定義する.
    つまり前者は「簡単な図形×大量の要素」ストラテジ,後者は「複雑な図形×少量の要素」ストラテジという事となるでしょう.いずれも描画内容そのものに差はありませんが,前者の方は構造が単純なため,スクリプトを記述するのが簡単そうです.

    顕在化するパフォーマンス問題


    そこでまず始めに作ってみたスクリプトがこちらです.先ほどの前者「簡単な図形×大量の要素」ストラテジに則って実装しています.
    1. canvasでドット絵を読み込む.
    2. ピクセルデータを取得し,svg要素にrect要素によるピクセルデータを挿入していく.
    3. スクリーンに描画する.
    実際のコードはこれ以外の機能が含まれているので若干複雑ですが,非常に素朴な実装となっています.

    しかし,いざスクリプトを実行してみると非常にマズイ問題が発生しました.画像が極々小さいうちは良いのですが,少しでも画像サイズが大きくなると急激に描画処理が重くなるのです.特にfirefoxで顕著で,生成したsvgグラフィックを表示するだけでブラウザがフリーズする始末なのです.

    改善策・なんとか要素数を減らせないか?


    この原因は何でしょうか?そこで「要素数が膨大すぎるのではないか?」という仮説を立てました.一般にhtmlにおいて膨大なdomが文書のパフォーマンスに影響を及ぼすことは知られていますので,svgでも同様であろうとの類推からこう考えました.

    要素数を減らすには,同色のピクセルを単一のpath要素に集約すれば良いはずです.そうすれば,path図形は複雑化すると思われるが,少なくとも色数までは要素数を減らすことが出来る…つまり「複雑な図形×少量の要素」ストラテジということになります.

    この方針に従って改良したものがこちらのスクリプトです.

    そしてこのスクリプトで生成したsvgを表示させてみると,先程とは処理が桁違いに軽快であることが判りました.先程はサイズが大き過ぎてブラウザがフリーズしてしまったドット絵が,実用的な速度で動作するのです.

    パフォーマンスの差を検証する


    このようにストラテジを変更することで劇的にパフォーマンスが改善することは判りましたが,実際には内部構造が異なるなど,このままでは経験則の域を出ません.

    従って改めて検証用のコードを記述し,そのパフォーマンスの違いについて検討してみましょう.

    「簡単な図形×大量の要素」ストラテジ(実行)※実行注意!
    <!DOCTYPE html>
    <html>
     <head>
     <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
     <style type="text/css">
     </style>
     <script src="domlike.js"></script>
     <script type="text/javascript">
    window.onload = function(){
     var size = 500;
     var svg = new domLike("svg");
     svg.setAttribute("width", size+"px");
     svg.setAttribute("height", size+"px");
     for(var j = 0; j<size; j++){
      for(var i = 0; i<size; i++){
       var path = new domLike("path");
       var d = "M"+i+","+j+"h1v1h-1z ";
       path.setAttribute("d", d);
       path.setAttribute("fill", "blue");
       svg.appendChild(path);
      }
     }
     var source = svg.toString();
     var start = new Date();
     document.body.innerHTML = source;
     var end = new Date();
     var div = document.createElement("div");
     div.innerHTML = end - start;
     document.body.appendChild(div);
    };
     </script>
     </head>
     <body>
     </body>
    </html>

    「複雑な図形×少量の要素」ストラテジ (実行
    <!DOCTYPE html>
    <html>
     <head>
     <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
     <style type="text/css">
     </style>
     <script src="domlike.js"></script>
     <script type="text/javascript">
    window.onload = function(){
     var size = 500;
     var svg = new domLike("svg");
     svg.setAttribute("width", size+"px");
     svg.setAttribute("height", size+"px");
     var d ="";
     for(var j = 0; j<size; j++){
      for(var i = 0; i<size; i++){
       d += "M"+i+","+j+"h1v1h-1z ";
      }
     }
     var path = new domLike("path");
     path.setAttribute("d", d);
     path.setAttribute("fill", "blue");
     svg.appendChild(path);
     var source = svg.toString();
     var start = new Date();
     document.body.innerHTML = source;
     var end = new Date();
     var div = document.createElement("div");
     div.innerHTML = end - start;
     document.body.appendChild(div);
    };
     </script>
     </head>
     <body>
     </body>
    </html>
    ポイントは次の通りです.
    • svgのソースコードをスクリプトで生成し,その結果をスクリーンに表示する.
      (domLikeオブジェクトは,ソースコードを生成する自作のヘルパオブジェクトです.)
    • svgを表示する前後の時刻を保持しておき,その差分を表示する.
    • path要素の生成部が異なっている.
      • 前者では500×500のpath要素を挿入している.
      • 後者では500×500ピクセル分のpathセグメントを連結し,単一の要素を挿入している.

    検証結果


    これらのコードをwebブラウザ毎に実行した結果を下にまとめます.
    firefox 21 1 2 3 4 5 平均
    簡単な図形×大量の要素 5086 5225 5332 5346 5338 5265.4
    複雑な図形×少量の要素 1350 1344 1355 1349 1346 1348.8
    chrome 24 1 2 3 4 5 平均
    簡単な図形×大量の要素 14243 15751 14227 14013 13949 14436.6
    複雑な図形×少量の要素 571 584 565 579 552 570.2
    opera 12.12 1 2 3 4 5 平均
    簡単な図形×大量の要素 3218 3134 3212 3216 3205 3197
    複雑な図形×少量の要素 1219 1163 1213 1169 1211 1195

    どのブラウザでも二つのストラテジ間で大きな差が出ていることが判ります.
    なお,数値上firefoxがchromeに優っているように見える部分もありますが,firefoxではsvgを表示する前(おそらくdomの構築が完了したタイミング)で数値が出力されているように見えるため,実際の体感速度はより劣ったものとなります.

    考察


    この結果から何が判るでしょう?
    • svgにおいても肥大したdomは描画パフォーマンスに多大な影響を及ぼす.
      ソースコードをツリー構造に変換する際のコストが非常に高い.
    • 一方複雑なd属性はそれほど描画パフォーマンスに影響を及ぼすことはない.
      おそらく,描画手順としてdomにぶら下がっているだけなので,描画処理に対するコストのみで済んでいる.(事前の内部分析等が行われていない)
    このように比較的当たり前な結果となりましたが,d操作がそれほど重い操作とならなかった事が驚きです.

    とは言え,今回の検証結果はドット絵を基準としていたため,d操作の内部にベジェ曲線が全く含まれていない点に注意する必要があります.

    従ってベジェ曲線を含んでいた場合は,また異なる結果となることが予想されます.

    結論


    • svgの描画パフォーマンスを向上させるには要素数をなるべく減らし,dom構築のコストを下げよう.
    • 特にドット絵においては同色のドットをまとめることで劇的なパフォーマンスの向上が期待できる.

    補足・ベジェ曲線の場合


    上記でベジェ曲線についての疑問が出てきたため,追加で調べて見ました.使用したコードは次のとおりです.二つ目のコードの1部分を下記のように書き換えています. (実行
    ベジェ曲線とは言え,単に直線を再定義しただけです.

    for(var j = 0; j<size; j++){
     for(var i = 0; i<size; i++){
      d += "M"+i+","+j+"c0.5,0 0.5,0 1,0c0,0.5 0,0.5 0,1c-0.5,0 -0.5,0 -1,0z ";
     }
    }

    結果は次のとおりです.ベジェ曲線によるパス図形の複雑度の増加も少なからずパフォーマンスに影響していることが判ります.

    firefox 21 1 2 3 4 5 平均
    簡単な図形×大量の要素 5086 5225 5332 5346 5338 5265.4
    複雑な図形×少量の要素 1350 1344 1355 1349 1346 1348.8
    ベジェ曲線 2803 2794 2814 2814 2812 2807.4
    chrome 24 1 2 3 4 5 平均
    簡単な図形×大量の要素 14243 15751 14227 14013 13949 14436.6
    複雑な図形×少量の要素 571 584 565 579 552 570.2
    ベジェ曲線 1915 1917 1938 1903 1839 1902.4
    opera 12.12 1 2 3 4 5 平均
    簡単な図形×大量の要素 3218 3134 3212 3216 3205 3197
    複雑な図形×少量の要素 1219 1163 1213 1169 1211 1195
    ベジェ曲線 3134 3087 3122 3153 3131 3125.4

    つまり全て直線で構成可能なドット絵をsvgに書き出すのは実は理に叶っているのです
    (しかもsvgz形式にするとびっくりするほど圧縮が効くなど,副次的に様々な効果が得られるのが面白いところです)

    0 件のコメント:

    コメントを投稿