2010年10月31日日曜日

SlimDXでDirect2Dの画像ファイル読み込み

この記事は色々問題があったので新しく書き直しました
[SlimDXでDirect2Dの画像ファイル読み込み 【書き直しver】]

色々図形を描いたり、文字を描いたりしても全然実践に役立ちませんね。

重要なのは絵を表示すること!

これは凄い難関でした。
というのも、Direct2Dではファイルから直接画像を読み込む方法が無いのです。そういうことは他の優れたライブラリに任せてしまおうということです。

Direct2Dは画像読み込みをWICというやつに任せる気だったようです。Direct2DにはWICのビットマップを読み込むCreateBitmapFromWicBitmap関数が用意されています。それを使うと簡単です。しかし! その関数はSlimDXが対応していないのです。最低。

でも、対応していなくても使えないことはないはずだと考え、色々頑張ってできるようにしました。


Direct2Dでの画像描画は、Bitmapクラスを作り、それを色々して描画するという手順です。
このとき、画像ファイルからBitmapクラスを作りたいのですが、画像ファイルからBitmapクラスにデータを読み込むところをDirect2Dは自作しないといけないのです。

Direct2Dで画像ファイルを読み込む方法に考えられるのは、GDI+を使う方法、WICを使う方法、他の非標準ライブラリを使う方法、Direct3D10を介する方法の4つです。
非標準ライブラリは知らないし、Direct3Dを使うには私の知識が足らなすぎるので、GDI+とWICを利用する方法を採ることにします。

GDI+とWICはC#から本格的にプログラミングを始めた私には馴染みのない言葉でした。C++とかでは出てくる低レベルな言葉です。C#ではそれらをラップしてあります。ではC#では何なのかと言いますと、

GDI+はC#におけるフォームアプリケーションのことです。

そして

WICはC#においてWPFで利用されている画像処理のことです。


それでは2通りの方法を示しましょう。今回は細かく説明したら埒があかないし、誰の得にもならないので説明しません。


<GDI+を使った方法>

これについては、先駆者がいらっしゃいます。このおじさんです。このページでSlimDXにおける方法も書かれています。C++屋さんのようですが、軽々とC#のコードを書いてしまわれるとは恐ろしい限りです。
ちなみにいくつか海外のコードも発見したのですが、同じ方法でした。
そのままも何ですので、C#3.0風に書き換えました。

using System;
using System.Drawing;
using GDI = System.Drawing;
using System.Drawing.Imaging;

using SlimDX;
using SlimDX.Direct2D;
using D2D = SlimDX.Direct2D;
using SlimDX.Windows;

namespace Direct2DSample
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new RenderForm("SlimDX - Direct2D Sample");
            var factory = new Factory();

            var target = new WindowRenderTarget(factory,new WindowRenderTargetProperties(){
                Handle = form.Handle,
                PixelSize = form.ClientSize
            });

            var bitmap = CreateBitmapFromFile(target,"file");

            MessagePump.Run(form, () =>
            {
              target.BeginDraw();
              target.Clear();

              target.DrawBitmap(bitmap);

              target.EndDraw();
            });

            foreach (var item in ObjectTable.Objects)
                item.Dispose();
        }

        public static D2D.Bitmap CreateBitmapFromFile(RenderTarget target,string filename){
          var srcBitmap = (GDI.Bitmap)GDI.Bitmap.FromFile(filename);

          var bitmapData = srcBitmap.LockBits(new Rectangle(0,0,srcBitmap.Width,srcBitmap.Height),ImageLockMode.ReadOnly,System.Drawing.Imaging.PixelFormat.Format32bppPArgb);

          var stream = new DataStream(bitmapData.Scan0,bitmapData.Stride*bitmapData.Height,true,false);
          var bitmap = new D2D.Bitmap(target,srcBitmap.Size,stream,bitmapData.Stride,new BitmapProperties(){
            PixelFormat = new D2D.PixelFormat(SlimDX.DXGI.Format.B8G8R8A8_UNorm,AlphaMode.Premultiplied)
          });

          srcBitmap.UnlockBits(bitmapData);

          return bitmap;
        }
    }
}

頑張って解読してください。解読しなくてもCreateBitmapFromFile関数の所と一番上のエイリアスをコピーすればどこでもそのまま利用できます。
あ、関数以外はちゃんと説明します。


            var bitmap = CreateBitmapFromFile(target,"file");
ここでファイルからビットマップを作っています。
このとき使っているCreateBitmapFromFile関数の中身を今回作ったわけですが、どうなっているのかは、ぶっちゃけ気にする必要はありません。
ビットマップを作るときはレンダーターゲットが必要になります。


              target.DrawBitmap(bitmap);
ここで作ったビットマップを描いています。
座標を指定していないので(0,0)の位置に描画されます。座標を指定するには、第二引数にRectangleを与えてやります。
ちなみに、ビットマップを描画する方法は、このDrawBitmap関数を使う方法よりも、ビットマップブラシというのを使って描画する方法のほうが汎用性が高いので、たぶんそっちを使うのが普通になるでしょう。


<WICを使った方法>

このプログラムすごく頑張ったんだぜ! 7、8時間はかけたと思う。お金くれ。
時間がかかったのは上のGDI+みたいにストリームを使ってやろうとしたからです。結局できずにピクセルコピーの方法に切り替えました。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Media.Imaging;
using System.Windows.Forms;

using SlimDX;
using SlimDX.Direct2D;
using D2D = SlimDX.Direct2D;
using SlimDX.Windows;

namespace Direct2DSample
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new RenderForm("SlimDX - Direct2D Sample");
            var factory = new Factory();

            var target = new WindowRenderTarget(factory,new WindowRenderTargetProperties(){
                Handle = form.Handle,
                PixelSize = form.ClientSize
            });

            var bitmap = CreateBitmapFromFile(target,"file");

            MessagePump.Run(form, () =>
            {
              target.BeginDraw();
              target.Clear();

              target.DrawBitmap(bitmap);

              target.EndDraw();
            });

            foreach (var item in ObjectTable.Objects)
                item.Dispose();
        }

        public static D2D.Bitmap CreateBitmapFromFile(RenderTarget target,string filename){
          var decoder = BitmapDecoder.Create(new Uri(filename,UriKind.RelativeOrAbsolute),BitmapCreateOptions.None,BitmapCacheOption.Default);
          var frame = decoder.Frames[0];

          byte[] data = new byte[frame.PixelWidth*frame.PixelHeight*4];
          frame.CopyPixels(data,frame.PixelWidth*4,0);

          var bitmap = new D2D.Bitmap(target, new Size(frame.PixelWidth,frame.PixelHeight), new BitmapProperties(){
            PixelFormat = new D2D.PixelFormat(SlimDX.DXGI.Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied)
          });
          bitmap.FromMemory(data,frame.PixelWidth*4);

          return bitmap;
        }
    }
}

さっきと違うのは、CreateBitmapFromFile関数の中身だけです。
WPFを利用するプログラムをコンパイルするには長い参照を書かないといけません。私はdoskeyで
doskey wpf=csc $* /r:PresentationFramework.dll /r:PresentationCore.dll /r:WindowsBase.dll ^
    /r:System.Xaml.dll /lib:C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF
と書いておいて、
wpf test.cs /r:SlimDX.dll
とコンパイルしています。
VisualStudioでは参照を追加するだけなので簡単だと思います。

(2010/11/09追記)
このプログラムはGIFなど、表示できない画像形式がありました。すみません;
全ての画像形式に対応させた関数は次のようになります。検証していませんがたぶん少し遅くなります。

using WIC = System.Windows.Media;

        public static D2D.Bitmap CreateBitmapFromFile(RenderTarget target,string filename){
          var decoder = BitmapDecoder.Create(new Uri(filename,UriKind.RelativeOrAbsolute),BitmapCreateOptions.None,BitmapCacheOption.Default);
          var source = decoder.Frames[0] as BitmapSource;

          source = new FormatConvertedBitmap(source,WIC.PixelFormats.Bgra32,null,0.0) as BitmapSource;

          byte[] data = new byte[source.PixelWidth*source.PixelHeight*4];
          source.CopyPixels(data,source.PixelWidth*4,0);

          var bitmap = new D2D.Bitmap(target, new Size(source.PixelWidth,source.PixelHeight), new BitmapProperties(){
            PixelFormat = new D2D.PixelFormat(SlimDX.DXGI.Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied)
          });
          bitmap.FromMemory(data,source.PixelWidth*4);

          return bitmap;
        }

※Bgra32→Pbgra32



GDI+を使う方法と、WICを使う方法はどちらが良いのかと気になると思います。
速度の比較をしてみたところ、プログラムの中で1回目に利用するときはGDI+の方が微妙に速かったです。
ところが、同じプログラムの中で2回目以降はWICのほうがGDI+の3倍くらい速くなりました。ただし、たまに遅くなるときがあります。
WICの方法でストリームを使うことができれば、1回目もGDI+より速くなると思われます。理由はGDI+でピクセルコピーを使ったらWICよりも遅かったからです。挑戦してみてください。

機能で比較すれば、WICが圧倒的です。しかし、機能が高くてもその機能を使うことはまず無いでしょうから、あまり比較に意味はありません。

Direct2DはWPFとの連携が難しいので、GDI+を使う方が普通かもしれません。WICはコンパイルもめんどくさいですしね。

【結論】
WICはたまに遅いときがあるが、全体としてはかなり高速。利用するにはWPFを使う必要があるので扱いにくい。ピクセルフォーマットを変更したり、アニメーションGIFを扱うことができる。コンパイルが面倒。

GDI+は速度が安定している。扱いやすい。コンパイルが簡単。



(2010/11/09)文章がめちゃくちゃだったので書き直しました。それでもめちゃくちゃなのは気にしない。また、WICの方法でGIFに関する項を追記しました。


0 件のコメント:

コメントを投稿