C#/Direct3Dを使った2D描画 の履歴(No.3)
更新Bitmap 画像をスクリーンに高速描画する†
Graphic.Draw だと速度やちらつきが問題になる場合に、 DirectX を使って高速&きれいに描画する方法を調べた。
3D の表示が目的ではない。
DirectDraw はもう古い†
昔は 2D グラフィックを高速に表示させるには DirectDraw というのを使ったが、今はお勧めされないらしい。
C# から DirectDraw 関連の関数を呼ぼうとすると、 激しく Deprecated の表示が出る。
Direct3D ももう古い?†
C# を始めとする Managed な環境から呼び出せる DirectX を Managed DirectX と呼び、MDX と略すらしいんだけれど、 すでに Microsoft は MDX を見限っているらしい。
http://bbs.wankuma.com/index.cgi?mode=al2&namber=10894&KLOG=24
http://www.gamedev.net/community/forums/topic.asp?topic_id=541254
http://mainori-se.sakura.ne.jp/slimdxwiki/wiki.cgi?page=SlimDX+%A4%C8%A4%CF
確かにいろいろ試していると、ドキュメントが足りなかったり、 仕様と異なる動きをする部分があったりで、結構危うい。
今回はこういった情報に行き着く前に(無駄に?)骨を折ってしまったので 一応ここにまとめておく。
ここで試した範囲では、一部機能(Sprite.Draw2D)を除きまともに使えている。
参照の追加†
Direct3D を使うには、プロジェクトの [参照設定] に、
- Microsoft.DirectX
- Microsoft.DirectX.Direct3D
- Microsoft.DirectX.Direct3DX
を入れておく。
大まかな流れ†
- Direct3D の画面を表示したいコントロール上に Direct3D.Device を作成
- Device の照明やZバッファの機能はオフにしておく
- Device 上に 2D 表示用の画面である Direct3D.Sprite を作成
- Sprite に座標変換方法を指定
- Direct3D.Texture に Bitmap をロード
- Texture から Sprite へ画像を転送
- Device 表示を更新
コード例†
LANG:C#(linenumber) using Direct3D = Microsoft.DirectX.Direct3D; using System.Drawing.Imaging; Direct3D.Device device = null; Direct3D.Sprite sprite; Direct3D.Texture texture; void InitializeDirect3D() { // デバイスの作成 device = Direct3DUtils.CreateDevice(panel1); // スプライトの作成 sprite = new Direct3D.Sprite(device); sprite.Transform = Microsoft.DirectX.Matrix.Identity; // テクスチャの読み込み var bmp = new Bitmap("some.jpg"); texture = Direct3DUtils.CreateTexture(sprite, bmp.Size); Direct3DUtils.CopyBitmapToTexture(fore, bmp); // フレームレートの設定 const int FrameRate = 30; timer1.Interval = 1000 / FrameRate; // 描画の開始 timer1.Enabled = true; } void FinalizeDirect3D() { timerFrame.Enabled = false; if ( device!=null ) { fore.Dispose(); back.Dispose(); sprite.Dispose(); device.Dispose(); device = null; } } bool working = false; private void timerFrame_Tick(object sender, EventArgs e) { // 再入防止 if ( working ) return; try { working = true; // デバイスの描画開始 device.BeginScene(); // 背景を塗りつぶす device.Clear(Direct3D.ClearFlags.Target, Color.Black, 1.0f, 0); // スプライトの描画開始 sprite.Begin(Direct3D.SpriteFlags.AlphaBlend); // ビットマップの (0, 0, 100, 100) の部分を、 // ビットマップ上の (0, 0) が (50, 20) に来る位置に、 // 半透明(Transparency = 0.5) で描画 var sourceRect = new Rectangle(0, 0, 100, 100); var center = Microsoft.DirectX.Vector3(0, 0, 0); var position = Microsoft.DirectX.Vector3(50, 20, 0); var Transparency = 0.5; var color = Color.FromArgb((int)Math.Round(Transparency*255), Color.White); sprite.Draw(texture, sourceRect, center, position, color); // 描画の終了 sprite.End(); device.EndScene(); // 画面に表示 device.Present(); } finally { working = false; } }
ユーティリティルーチン†
LANG:C#(linenumber) using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using System.Drawing; using System.Drawing.Imaging; using Direct3D = Microsoft.DirectX.Direct3D; using System.Runtime.Serialization; namespace PictureSaver { class Direct3DUtils { public static Direct3D.Device CreateDevice(Control control) { // デバイス作成用パラメータ var param = new Direct3D.PresentParameters[] { new Direct3D.PresentParameters() }; param[0].AutoDepthStencilFormat = Direct3D.DepthFormat.D15S1; param[0].BackBufferCount = 1; param[0].BackBufferFormat = Direct3D.Manager.Adapters.Default.CurrentDisplayMode.Format; param[0].BackBufferHeight = control.ClientRectangle.Height; param[0].BackBufferWidth = control.ClientRectangle.Width; param[0].DeviceWindow = control; param[0].EnableAutoDepthStencil = false; param[0].MultiSample = Direct3D.MultiSampleType.None; param[0].MultiSampleQuality = 0; param[0].PresentationInterval = Direct3D.PresentInterval.Immediate; param[0].PresentFlag = Direct3D.PresentFlag.None; param[0].SwapEffect = Direct3D.SwapEffect.Discard; param[0].Windowed = true; // デバイスを作成し、照明とZバッファを無効にする var device = new Direct3D.Device(Direct3D.Manager.Adapters.Default.Adapter, Direct3D.DeviceType.Hardware, control, Direct3D.CreateFlags.HardwareVertexProcessing, param); device.RenderState.ZBufferEnable = false; device.RenderState.Lighting = false; return device; } public static Microsoft.DirectX.Direct3D.Texture CreateTexture(Direct3D.Sprite sprite, Size size) { var textureSize = CalcTextureSize(size); return new Direct3D.Texture(sprite.Device, textureSize.Width, textureSize.Height, 0, Direct3D.Usage.Dynamic, Direct3D.Format.A8R8G8B8, Direct3D.Pool.Default); } public static Size CalcTextureSize(Size size) { // Direct3D のテクスチャの幅と高さは2の累乗でないといけない int width, height; for ( width = 1; width < size.Width; width *= 2 ) ; for ( height = 1; height < size.Height; height *= 2 ) ; return new Size(width, height); } public static void CopyBitmapToTexture(Direct3D.Texture texture, Bitmap bmp) { Size size; int[] pixels = GetPixelsFromBitmap(bmp, out size); // テクスチャをロック // Bitmap のデータとタテヨコが逆になってるので Height、Width の順 Direct3D.SurfaceDescription desc = texture.GetLevelDescription(0); UInt32[,] buf = (UInt32[,])texture.LockRectangle( typeof(UInt32), 0, Direct3D.LockFlags.None, desc.Height, desc.Width); try { // タテ・ヨコを逆にしてコピー // ビットマップからはみ出たところは Transparent にしておく for ( int y = 0; y < desc.Height; y++ ) for ( int x = 0; x < desc.Width; x++ ) if ( x < size.Width && y < size.Height ) { buf[y, x] = (UInt32)pixels[y * size.Width + x]; } else { buf[y, x] = (UInt32)Color.Transparent.ToArgb(); } } finally { // テクスチャをアンロック texture.UnlockRectangle(0); } } public static int[] GetPixelsFromBitmap(Bitmap bmp, out Size size) { // Bitmap からピクセル情報を抜き出し int の配列に移して返す // pixels[y * size.Width + x] の形で ARGB データにアクセスできる int[] pixels; BitmapData bmpdata = bmp.LockBits( new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); try { size = new Size(bmpdata.Width, bmpdata.Height); pixels = new int[size.Width * size.Height]; IntPtr ptr = bmpdata.Scan0; System.Runtime.InteropServices.Marshal.Copy(ptr, pixels, 0, size.Width * size.Height); } finally { bmp.UnlockBits(bmpdata); } return pixels; } } }
Direct3D.Sprite.Transform について†
これの動きがかなり危うい気がする。
特に、Sprite.Draw2D の影響が Sprite.Transform に残る場合が あるようなので、おかしいと思ったら Sprite.Transform を 上書きしてみると良い。
どういう訳か、Sprite.End() の直前に Sprite.Transform = Microsoft.DirectX.Matrix.Identity; を入れるだけで動作が 変わったりしたこともあった。
Microsoft.DirectX.Matrix について†
Sprite.Transform を設定したり、ベクトル計算をしたりするのに使う、 アフィン変換用の4次元正方行列。
不思議な仕様なので始めははまるかも。
LANG:C#(linenumber) using Microsoft.DirectX; Matrix m; m = Matrix(); // 単位行列になりそうだが、実際にはゼロ行列 m = Matrix.Zero; // ゼロ行列 m = Matrix.Identity; // 単位行列 m = Matrix.Scaling(2, 1, 1); // X 方向に 2 倍する行列 var m1 = Matrix.Translation(0.1f, 0, 0); // X 方向に 0.1 ずらす行列 var m2 = Matrix.RotationX((float)(30*Math.PI/180)); // X 軸を中心に 30 度回転 m = Matrix.Multiply(m1, m2); // ずらしてから回転する行列 m = Matrix.Invert(m); // その逆行列
などの static 関数群と、m.Scale(x, y, z) のような普通の関数群がある。
不思議なのは m.Scale(x, y, z) のような関数の仕様で、
LANG:C#(linenumber) Matrix m = Matrix.Scaling(2, 1, 1); // X 方向に 2 倍する行列 m.Scale(3, 1, 1); // さらに 3 倍?
この結果は X 方向に 6 倍する行列になるかと思いきや 3 倍する行列になる。
つまり、
LANG:C# m = Matrix.Scaling(2, 1, 1);
と、
LANG:C# m.Scale(2, 1, 1);
とは同じ意味になる。
複合した行列を作るには、
LANG:C# var m= Matrix.Scaling(2, 1, 1); m.Multiply(Matrix.Scaling(3, 1, 1));
のようにする。
ベクトルとのかけ算はこう。
LANG:C# var v = new Vector3(1, 1, 0); v.TransformCoordinate(m);
static でない関数群は掛け算にしておいてくれた方が 分かりやすいし、使いやすい気がするんだけど・・・
Sprite.Draw†
Draw / Draw2D は、パラメータの意味をよく理解しないと、 思ったように動かない。
Sprite.Draw には、いくつかの形式があるものの、 一番使われると思うのが次の物。
LANG:C# public void Draw( Texture srcTexture, // 描画するテクスチャ Rectangle srcRectangle, // テクスチャ上の矩形領域 Vector3 center, // 切り出したテクスチャ上の基準点をテクスチャ上の相対座標で指定 Vector3 position, // 基準点を置く位置をスプライト上の座標で指定 Color color); // 半透明、明度の低下を指定
color の定義が int の物は、バグがあるとか無いとかの情報も あったので、この形を使うのが良さそうだ。
- Texture は、グラフィックメモリにロードしたビットマップデータを表す物。
縦横のサイズを2の冪乗にしなければならない。
通常は、1つの大きなテクスチャに、いくつもの画像を含めておいて、 その一部を選んで画面に表示することで、グラフィックメモリの無駄をなくす。 - srcRectangle で、テクスチャ上のどの部分を切り出して表示するかを、 テクスチャ上の座標で指定する。
- center は、切り出したテクスチャの上に基準点を定義する物。 切り出したテクスチャの左上からの相対座標で指定する。
- position は prite 上の座標で、切り出したテクスチャの基準点を この位置に置くよう指定するもの
- color は、テクスチャ描画時にテクスチャ上の各ピクセルの ARGB 値に
かけ算する値を指定する
テクスチャ自身の A 値が全て 255 であれば、通常は背景は透過しないが、 ここで指定する color の A 値を例えば 128 にしておけば、描画には A 値として 255 * 128 / 255 = 128 が使われるため、背景が半分透過する
同様に、RGB 値もかけ算されるので、暗くして、あるいは特定の色成分を 減衰させて描画することができる
Sprite.Draw2D†
Draw よりもっとやっかいなのが Draw2D
Sprite.Transform に関するバグ†
テクスチャを拡大・縮小・回転して表示できるので、 非常に便利なのだけれど、たいてい思ったように動かない。
1つ決定的なのは、Draw2D を使った後、Sprite.Transform の値が勝手に変わってしまうというバグがあること。
その状態を放置すると、それ以後 Draw で描いた画像がすべて変形してしまう。
Draw2D を使ったら、必ず Sprite.Transform を上書きすべき。
パラメータの意味†
ドキュメントとかなり異なる。
LANG:C# public void Draw2D( Texture srcTexture, // テクスチャ Rectangle srcRectangle, // テクスチャ上の矩形 Rectangle destinationRectangle, // 表示サイズ(サイズ部だけが意味を持つ) Point center, // テクスチャ上の相対座標でテクスチャの基準点を指定 float rotationAngle, // 回転角 Point position, // 表示位置 Color color); // 透明度・色に対する補正値
srcTexture から srcRectangle を切り出して、その基準点を center に取るところまでは Draw と同じ。この基準点は次の平行移動に用いられる物で、 回転中心になる物では無いので注意。
center を position へ平行移動する前に(!)、座標変換が行われる。
srcRectangle.Size を destinationRectangle.Size にする比率で座標変換される。
したがって、center で指定した基準点は position ではなく、
X 座標が position.X * destinationRectangle.Width / srcRectangle.Width、
Y 座標が position.Y * destinationRectangle.Height / srcRectangle.Height、
の位置に置かれる。
そして、その後に 「原点」 を中心として(!) rotationAngle だけ回転される。
非常に使いづらい。
使いやすい Draw2D†
指定した矩形領域を基準点を中心に回転し、
指定座標に指定の大きさで表示する、
とっても当たり前な動作を行うルーチン。
LANG:C#(linenumber) using Microsoft.DirectX.Direct3D; using Microsoft.DirectX; public static void Draw2D(Sprite sprite, Texture texture, Rectangle src, Point center, Rectangle dest, double angle, Color color){ // 指定量の拡大縮小 var m = Matrix.Scaling((float)dest.Width / src.Width, (float)dest.Height / src.Height, 1); // 回転 m.Multiply(Matrix.RotationZ((float)angle)); // 指定位置まで動かす m.Multiply(Matrix.Translation(dest.X, dest.Y, 0)); var mold= sprite.Transform; try { if ( m != mold ) sprite.Transform = m; // 基準点を原点に合わせて表示する sprite.Draw(texture, src, Vector3.Empty, new Vector3(-center.X, -center.Y, 0), color); }finally{ if ( m != mold ) sprite.Transform = mold; } }