2010年11月14日日曜日

SlimDXでDirect2Dの座標変換

今回は座標変換です。

座標変換と言っても、アフィン変換と呼ばれるものです。「変換」ではなく「移動」と言うこともあります。
さて、私は高校数学をやっていないので、塾で生徒に教えるときのために買った『シグマベスト 理解しやすい数学Ⅲ+C』という本で点の移動の説明を引用します。

座標平面上の各点Pに対し、同じ座標平面上の点Qがただ1つ対応しているとき、この対応を座標平面上の移動(変換)といい、Qをこの移動による点Pの像という。

だってさ。
アフィン変換を色々組み合わせれば超格好いいことができるんじゃないかとか思っているアナタ。もといワタシ。この説明を読む限りではそんな格好いいことはできません。
「ただ1つ対応しているとき」という部分に注目しましょう。これはつまり、1つの点が2つになったり、存在しなかった点が突如現れたり、あるいは元あった点が消えたりしないということです。

さらにアフィン変換は一次変換です。一次って何か? 中学で一次関数という言葉を聞いたことがありませんか。あれは、ピーっと一直線の線でした。それと同じ一次です。つまり、アフィン変換では、元の図形を直線的に変換します。四角形が楕円になったり、波線になったりしないということです。

じゃあ何ができるのか?

できるのは、拡大縮小回転せん断平行移動の4つと、その組み合わせです。

ググれば無限にサイトがヒットします。完璧に書かれたサイトはまだ見たことがありませんが、いくつかのサイトを読めば自然と分かるんじゃないでしょうか。

このサイトが必要なことを1ページにまとめてあって分かりやすい感じです。

そのサイトの補足をしますと、最後の[3.5. 連続する変換の表現]に、「計算は右から順に行っていく」とあります。そこを補足しますと、右から1つ1つしなくても、先に3つの変換をかけ算して1つの変換に変えることができます。
例えば、平行移動→回転→平行移動→拡大というのをやりたいときに、平行移動したら座標がこうなって~、次に回転したらこうなって~……なんて一つ一つ計算しなくても、先に4つの変換を掛け合わせた平回平拡変換行列を作っておけば、一回の計算で一連の計算と同じ効果が得られます。……だよね? 間違ってないよね?

もう1つ、そのサイトでは
こんな形の方程式を使っています。私もこのほうが分かりやすいと思うのですが、Direct2Dでは、行列を転置させた
という形の方程式を使っています。

SlimDXのDirect2DにはMatrix3x2という構造体があります。これが行列です。3x2というのは(゚w゚)と同じセンス悪い顔文字です。このMatrix3x2にM11、M12、M21、M22、M31、M32というプロパティがあるのですが、それぞれ次のように対応しています。


さて、Matrix3x2構造体ですが、この構造体、残念なことに回転や平行移動の行列を簡単に生成したり、行列同士のかけ算を簡単にやってくれる関数を持っていません!
元のDirect2Dにはもちろんあります。SlimDXが鼻くそだということです。

フォーリンラブ おぱんつの神様 きっとあなたを感じてる~♪

ま、実際のところ、簡単にやってくれる関数があったとしても行列を理解していないと使い切れなません。せっかくですから、自分で作ってみることをお勧めします。


ここから簡単に使い方を説明します。

Direct2Dでは色々な場面で座標変換が行えますが、一番簡単な所はレンダーターゲットです。
レンダーターゲットのTransformプロパティに作ったMatrix3x2構造体を指定してやると良いです。
例えば、何もしない単位行列を指定するサンプルは次のようになります。
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,"sample.png");

            var matrix = Matrix3x2.Identity;
            
            target.Transform = matrix;

            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;
        }
    }
}

と、こんな感じです。簡単に説明しましょう。Direct2Dの基本については[SlimDXでDirect2D]を参考にされてください。


            var bitmap = CreateBitmapFromFile(target,"sample.png");
ここで"sample.png"という画像ファイルを読み込んでいます。サンプルの下の方に書いているCreateBitmapFromFile関数を使っています。画像ファイルについて詳しくは[SlimDXでDirect2Dの画像ファイル読み込み]を参考にされてください。


            var matrix = Matrix3x2.Identity;
ここで単位行列を作っています。単位行列は変換を行わない行列です。SlimDXでは他の行列は簡単に作れないくせして、なぜか単位行列だけは簡単に作れるようになっています。


            target.Transform = matrix;
ここで単位行列をレンダーターゲットに与えています。ちなみにレンダーターゲットは元から単位行列を持っているので、ぶっちゃけ意味のないコードです。
単位行列以外の行列を作ったときにここでその行列を与えてやるということです。


              target.DrawBitmap(bitmap);
ここで画像を描画しています。行列によって変換された形で描画されます(もしかしたら描画された後で変換しているかも)。
このサンプルの場合は単位行列ですので、何も変換されずにそのまま表示されます。


さて、基本となる拡大縮小、回転、せん断、平行移動の4つの変換を見てみましょう。
上のサンプルの単位行列を作るところで、代わりにこれらを作ってやれば変換ができます。
コピペした後で数値を代入してあげてください。




<拡大縮小>
x軸方向にSx倍、y軸方向にSy倍する
            var matrix = new Matrix3x2(){
                M11 = (float)sx,
                M22 = (float)sy,
            };
例:Sx=2
例:Sy=0.5




<回転> ―参考[回転移動を行列で表す]
原点を中心にφ°回転させる。
            var matrix = new Matrix3x2(){
                M11 = (float)Math.Cos(phi),
                M12 = (float)Math.Sin(phi),
                M21 = (float)-Math.Sin(phi),
                M22 = (float)Math.Cos(phi),
            };
例:φ=30°=30π/360rad




<せん断(歪み)>
x軸方向にφx°、y軸方向にφy°ゆがめる。
            var matrix = new Matrix3x2(){
                M11 = 1F,
                M12 = (float)Math.Tan(phiy),
                M21 = (float)Math.Tan(phix),
                M22 = 1F,
            };
例:φx=10°=10π/360rad
例:φy=45°=45π/360rad




<平行移動>
x軸方向にTx、y軸方向にTy平行移動する。
            var matrix = new Matrix3x2(){
                M11 = 1F,
                M22 = 1F,
                M31 = (float)tx,
                M32 = (float)ty
            };
例:Tx=60
例:Ty=30



あー疲れた。この記事何日かかってるんだろう。

問題! 偉い人にもなれず、エロい人にもなれなかった中途半端な人をなんと呼ぶでしょう?


さて、基本となる4つの変換はできました。普通これだけで十分ですが、ちょっと変わったことをしたいときってありますよね?
主に、拡大縮小や回転の中心を原点以外にするという場合でしょう。
あ、反転させたい場合もありますね。
そういう場合は4つの変換を組み合わせれば簡単にできます。組み合わせるときは行列同士をかけ算してやれば良いのです。


……でもさぁ、

何個も行列作ったり、かけ算したりってめんどくさくない? SlimDXがそういうのを簡単にできるクラスをちゃんと用意してくれてれば何も困ることは無かったのに。わざわざこんな記事を書く必要も無かったのに。
Matrixクラスではちゃんと用意してあるのになんでMatrix3x2では用意してないんだYO!
いずれは作ってくれると思うけどね。

そういうわけで、一般的な変換を簡単に行うためのクラスを私がわざわざ作成しました←何様
Matrix2Dという構造体です。Matrix3x2とは暗黙の型変換を行っているので、特別な使い方はありません。
ただし、SlimDXはもとよりMatrix3x2→Matrix3x2Fという過程を経てDirect2Dを使っており(ソース見た感じちょっと違うかも)、そこにMatrix2D→Matrix3x2→Matrix3x2Fと一段階追加されるわけですから、少しだけ遅くなると思います。

ここからダウンロード



Matrix2Dを使って中心を指定した回転はこうなります。
            float phi = (float)(60/360.0*Math.PI);
            float pointx = (float)(bitmap.Size.Width/2.0);
            float pointy = (float)(bitmap.Size.Height/2.0);
            var matrix = Matrix2D.Rotation(phi,pointx,pointy);



反転させるのはこうです。
            var matrix = Matrix2D.Reversal(false,true,bitmap.PixelSize);

この場合上下反転です。左右反転したい場合は今falseになっている所をtrueにします。


これで座標変換についてほとんどのことはできるようになったと思います。
後は、どのタイミングで変換を行うかというのが疑問ですね。 Transformプロパティを持っているのはレンダーターゲットの他に、ブラシがあります。それ以外にも引数にあったり、名前が微妙に違ったりして、色々な場面で変換を行えます。

私はグラフィックライブラリで実践を行ったことがないので、普通はどのタイミングでするとかわかりません。……まあ、やりたいときにやればいいんじゃないですか?


1 件のコメント:

  1. 現在、SharpDxを使っているのですが、アフィン変換に四苦八苦
    してました。参考例、画像入りで分かりやすかったです。
    ありがとうございました。

    返信削除