テクスチャマッピング
前回、今回と3Dエンジンを使って2Dのグラフィック表示を行っています。最低限DirectDrawの2D描画機能は実装したいところです。ですから、画像ファイルを読み込んでそれを表示するくらいの機能はないと話しになりません。今回はそれを実装します。
GDI+などの描画機能で画像を表示するには、画像を持ったオブジェクト(Bitmapオブジェクトとか)から別の画像オブジェクトへ画像を転送します。ですが、Direct3Dには基本的にそのような機能はありません。そこで、それに近いテクスチャーマッピングを利用して画像の表示を行います。
テクスチャマッピングとは、画像から必要な部分を切り出して、それをポリゴンに貼り付ける処理のことです。本来は、物体の模様なんかを画像として用意しておいて物体に貼り付けるもので、複雑な図形をすべて頂点計算して表示していると非常に時間がかかってしまうため、テクスチャマッピングを利用して処理を軽くしていることが多いようです。ポリゴンに画像を張るというのが、テクスチャマッピングの重要な概念になります。
今回のサンプルプログラム
GraphicsAで作ったプログラムをさらに改良します。四角形に画像を貼り付けて画像を表示します。ゲーム的には、かなり実用的です。
Direct3DSample3左の画面が今回のサンプルプログラムです。前回の四角に加えて、画像も表示してクルクルまわします。 |
プロジェクトは前回のものに改造を加えるので、前回のコードをベースに作っていきます。DirectGraphicsAのサンプルプログラムをダウンロードして置いてください。
テクスチャ座標
コードの前に、テクスチャ座標の説明をします。テクスチャマッピングでは、画像の必要な部分をポリゴンに対して貼り付けます。その必要な部分を示すのがテクスチャ座標です。テクスチャ座標にはUV座標というものが使われています。左上の原点を(0,0)として横軸にU軸、縦軸にV軸を使用し、右下が(1,1)になるように座標を取っています。この座標を使って、テクスチャ画像内の任意の点を表します。
このテクスチャ座標を各頂点に設定し、画像の任意の位置を切り出して表示するのです。
CLKsTextureクラス
テクスチャの予備知識はこのくらいにして、テクスチャ画像を保管しておくCLKsTextureクラスを作ります。DirectXにTextureという画像を保存しておくクラスがあるのですが、画像以外にも情報を保管しておくと後々便利なことが多いので、今回はテクスチャ周りを一括管理できるクラスを作ることにします。今回のクラスには、@Direct3D.Textureオブジェクトの保持とAファイル名を渡すと自動的に画像ファイルを読み込んでTextureオブジェクトを作る機能を実装します。
using System; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; using System.Drawing; namespace dgsample3 { public class CLKsTexture { private Texture tex; private static Device dev; public static Device Direct3DDevice { set { dev = value; } } public Texture Texture { get { return tex; } } public CLKsTexture(string FileName, Size size) { tex = LoadTexture(FileName, size); } private Texture LoadTexture(string fileName, Size size) { Texture tex; Bitmap src = new Bitmap(fileName); Bitmap texbit = new Bitmap(size.Width,size.Height); Graphics g = Graphics.FromImage(texbit); g.DrawImage(src, 0, 0, size.Width, size.Height); g.Dispose(); src.Dispose(); tex = Texture.FromBitmap(dev,texbit,Usage.None,Pool.Managed); texbit.Dispose(); return tex; } } } |
これが、テクスチャーを管理するCLKsTextureクラスです。実際は、ただTextureオブジェクトを持っているだけなのですが、コンストラクタにファイル名を渡すと、自動的にTextureオブジェクトを作ってくれるのでちょっとだけ便利です。Textureインスタンスを作る際に、Direct3D.Deviceオブジェクトを渡さないといけないのですが、CLKsDGクラスにはDeviceを取得できる機能がないのでstatic変数としてDeviceを保持してあります。CLKsDGの初期化の部分でこの変数にインスタンスを設定すれば問題ないですね。
今回、「tex」「dev」変数はprivateで宣言しておき、アクセスにはプロパティを使っています。プロパティはあたかも変数に設定、取得するようにメソッド呼び出しを行う機能で、C#特有のものです。設定、取得のどちらか一方しか出来ないようにも定義できます。今回はdevのアクセスはDirect3DDeviceプロパティで値の設定だけを、texはTextureプロパティで値の取得だけが出来るようになります。C#のコード的にはこのように直接変数をいじらず、publicメソッドやプロパティを使ってアクセスするのが綺麗なコードだそうです。プログラムの規模が大きくなると有り難味が分かるそうですが、小さいうちは面倒なだけですね。プロパティにの定義法については他のサイトで調べてみてください…
LoadTextureメソッド
さて、画像ファイルの読み込みとTextureインスタンスを生成するLoadTextureメソッドの説明をします。引数はファイル名とテクスチャのサイズを受け取って、Direct3D.Textureオブジェクトを返します。このテクスチャのサイズとは、実際にメモリ上に格納する画像のサイズで、読み込む先の画像のサイズではありませんので注意してください。というのも、テクスチャとして使える画像のサイズは512×512とか、256×256とか特定のサイズしか扱えません。そこで、読み込みもとの画像と一番近そうなサイズを自分で選択して画像のサイズを変え、それをもとにテクスチャを生成しています。コードでは.NET標準の描画機能であるGDI+を使っているので、少し難しそうですね。
private Texture LoadTexture(string fileName, Size size) { Texture tex; Bitmap src = new Bitmap(fileName); Bitmap texbit = new Bitmap(size.Width,size.Height); Graphics g = Graphics.FromImage(texbit); g.DrawImage(src, 0, 0, size.Width, size.Height); g.Dispose(); src.Dispose(); tex = Texture.FromBitmap(dev,texbit,Usage.None,Pool.Managed); texbit.Dispose(); return tex; } |
説明しやすいようにもう一度切り出して書きました。.NETで画像を扱えるオブジェクトはいくつかありますが、今回はBitmapオブジェクトを使います。サイズ変更前の画像を格納しているのが「src」オブジェクトで、テクスチャ生成のもととなるのが「texbit」オブジェクトです。Bitmapクラスのコンストラクタの中にファイル名を渡すと、その画像を読み込んでオブジェクトを生成する機能があるのでそれを利用してsrcオブジェクトを生成しています。一方texbitはsizeで指定された大きさの空のオブジェクトを生成しています。
次に、srcからtexbitへ画像を転送します。GDI+では、画像の転送先のBitmapからGraphicsオブジェクトを取得し、それを使って画像の転送を行います。BitmapからGraphicsを取得するにはGraphicsのstaticメソッドであるFromImageを使い、画像の転送にはDrawImageメソッドを使います。DrawImageには多くのオーバーロードメソッドがあるので、一度調べてみるといいと思います。
texbitへ画像を転送したら、いよいよTextureオブジェクトの生成です。BitmapからDirect3D.Textureを生成するには、staticメソッドFromBitmapを使います。第一引数にはstaticで保持してあるDirect3D.Deviceを、第二引数にBitmapオブジェクトを、そのほかの引数には…このとおり渡してください。こうして生成したTextureオブジェクトを返せば機能的に完成です。
もう一つ、GDI+周りで作ったオブジェクトは解放をしなくてはなりません。Textureオブジェクトを作り終わった時点で、src・texbit・gのオブジェクトはもう不要ですね。C#では基本的にいらなくなったオブジェクトは自動的に消してくれるのですが、ここら辺のオブジェクトは参照変数が消えても、インスタンスが消えない場合があります。そのため明示的にインスタンスを消す必要性があります。それを行っているのが各クラスの「Dispose」メソッドです。
CLKsDGクラスを改良
CLKsTextureクラスの定義が終わったところで、前回まで作っていたCLsDGクラスにテクスチャマッピングの機能を追加しましょう。まずは、先ほど作ったCLKsTextureクラスのstatic変数devにDirect3D.Deviceを渡しておかないといけません。
//CLKsDGコンストラクタ public CLKsDG(System.Windows.Forms.Control wind) { Microsoft.DirectX.Direct3D.PresentParameters param = new PresentParameters(); param.Windowed = true; param.SwapEffect = SwapEffect.Discard; param.PresentationInterval = PresentInterval.Immediate; device = new Microsoft.DirectX.Direct3D.Device(0, Microsoft.DirectX.Direct3D.DeviceType.Hardware,wind, CreateFlags.SoftwareVertexProcessing, param); device.RenderState.CullMode = Cull.None; device.RenderState.Lighting = false; //頂点用オブジェクトの生成 verts = new CustomVertex.TransformedColoredTextured[4]; vertexBuffer = new VertexBuffer( typeof(CustomVertex.TransformedColoredTextured), 4, device, 0, CustomVertex.TransformedColoredTextured.Format, Pool.Managed); //CLKsTextureクラスにデバイスをセット CLKsTexture.Direct3DDevice = device; } |
Direct3DDeviceプロパティに代入するだけで設定は完了です。
Textureを描画-DrawTextureメソッド
次に、Texture描画用に新しくメソッドを追加します。描画先の情報と、テクスチャ座標、あとはDirect3Dの恩恵を多少なり受けられるように頂点色の情報を受け取ってテクスチャを描画するDrawTextureメソッドを作ります。
public void DrawTexture(CLKsTexture Tex, int x, int y, int w, int h, float tu, float tv, float tw, float th, Color color) { verts[0].X=x-w/2; verts[0].Y=y-h/2; verts[0].Tu = tu; verts[0].Tv = tv; verts[1].X=x+w/2; verts[1].Y=y-h/2; verts[1].Tu = tu + tw; verts[1].Tv = tv; verts[2].X=x-w/2; verts[2].Y=y+h/2; verts[2].Tu = tu; verts[2].Tv = tv + th; verts[3].X=x+w/2; verts[3].Y=y+h/2; verts[3].Tu = tu + tw; verts[3].Tv = tv + th; for (int i = 0; i < 4; i++) { verts[i].Z=0f; verts[i].Color = color.ToArgb(); verts[i].Rhw = 1f; } device.SetTexture(0,Tex.Texture); this.drawRectPrim(verts); } |
上がDrawTextureメソッドのコードです。前回作ったDrawBoxとほとんど変わっていませんね。各頂点のTu,Tvのところに受け取ったテクスチャ座標の値を計算して代入しています。注意しないといけないのは、テクスチャ座標に渡すのは基本的に0〜1のfloat型の値だということです。読み込んだ先の画像の座標ではありません。ちなみに、twの値に負の値を指定すると画像が反転して表示も出来ます。それ以外にも1以上の値を設定すると画像を繰り返して表示できたりとDirectDrawではなかなか実現が難しかったような機能も簡単に処理できます。アクションゲームなどを作っていると、同じ画像を反転させて表示したいことが多いので、結構実用的な機能です。
テクスチャをマッピングする場合、当然転送もとのTextureオブジェクトを指定しなくてはいけません。今回、DrawTextureメソッドでは第一引数にCLKsTexture型のオブジェクトを受け取っているため、そこからDirect3D.Textureオブジェクトを取得してDirect3D.Deviceに設定します。それを行っているのが太字の「device.SetTexture(0,Tex.Texture);」の部分です。
テクスチャの指定も描画前に事前に設定しておきます。テクスチャの設定には「SetTexture」メソッドを使用します。第一引数はテクスチャステージというもので、今のところは0を指定しておいてください。第二引数にCLKsTextureが保持しているTextureオブジェクトを渡します。こうしてテクスチャを設定しておけば、ポリゴンを描画したときにテクスチャを張ってくれるようになります。
Textureの取り消し
これで、テクスチャの描画はできるようになりました。ですが、このままプログラムを実行すると前回作ったDrawBoxメソッドを使ってもテクスチャが描画されてしまいます。なぜかというと、Direct3D.Deviceにテクスチャを指定したままだからです。そこで、DrawBoxに少し修正を加え、指定したテクスチャを取り消す処理をします。
public void DrawBox(int x, int y, int w, int h) { verts[0].X=x-w/2; verts[0].Y=y-h/2; verts[0].Tu = 0f; verts[0].Tv = 0f; verts[1].X=x+w/2; verts[1].Y=y-h/2; verts[1].Tu = 1f; verts[1].Tv = 0f; verts[2].X=x-w/2; verts[2].Y=y+h/2; verts[2].Tu = 0f; verts[2].Tv = 1f; verts[3].X=x+w/2; verts[3].Y=y+h/2; verts[3].Tu = 1f; verts[3].Tv = 1f; for (int i = 0; i < 4; i++) { verts[i].Z=0f; verts[i].Rhw = 1f; } verts[0].Color = Color.Red.ToArgb(); verts[1].Color = Color.Blue.ToArgb(); verts[2].Color = Color.Green.ToArgb(); verts[3].Color = Color.Yellow.ToArgb(); //テクスチャーの消去 device.SetTexture(0,null); this.drawRectPrim(verts); } |
drawRectPrimを呼び出す前に、「device.SetTexture(0,null);」と追加してください。SetTextureメソッドでは、第二引数にnullを渡すことで、設定したテクスチャを取り消して、マッピングを行わないように出来ます。
画像を描画する
新機能の実装が完了したので、早速使ってみましょう。今回もMainメソッド内ですべて処理しています。
static void Main() { //メインループ //講座で紹介したFPS安定タイマーを使用しています。 Form1 frm = new Form1(); double nextframe = (double)System.Environment.TickCount; float wait = 1000f/60f; frm.Show(); //フォームの表示 frm.ClientSize = new Size(800,600); CLKsDG dgdevice = new CLKsDG(frm); //テクスチャーの読み込み CLKsTexture logo = new CLKsTexture(@"logo.jpg",new Size(512,256)); while(frm.Created) { if ((double)System.Environment.TickCount >= nextframe) { if ((double)System.Environment.TickCount < nextframe + wait) { //描画処理 //前回と同じなので省略 //テクスチャーの描画 dgdevice.DrawTexture(logo, 400+(int)(300*Math.Cos(Rad+Math.PI/2)), 300+(int)(200*Math.Sin(Rad+Math.PI/2)), 256,128,0,0,1,1,Color.White); dgdevice.DrawTexture(logo, 400+(int)(300*Math.Cos(Rad-Math.PI/2)), 300+(int)(200*Math.Sin(Rad-Math.PI/2)), 256,128,0,0,1,1,Color.FromArgb(g,b,r)); dgdevice.End(); dgdevice.Present(); } nextframe += wait; } Application.DoEvents(); } } |
テクスチャを読み込みは、CLKsTextureクラスのコンストラクタにファイル名とサイズを渡すだけで出来ます。サイズは上で説明したとおり512,256,128,64など2の乗数しか使えないので、今回はもとの画像サイズに近い512×256を指定しています。当然ですが、CLKsTextureの生成はループ内で行わないでください。画像は最初に読み込めば十分ですので。
ループ内でDrawTextureを呼び出しています。第一引数に先ほど生成したlogoオブジェクトを渡していますね。また、最後の引数colorには片方にWhite(白)を、もう片方には変化する色を渡してます。Whiteを渡すと、読み込んだ画像と同じ色で表示しますが、別の色を渡すと色が変化します。このように、テクスチャは頂点色と混ぜて使うことが出来るので、一つの画像で様々なバリエーションの画像転送が出来ます。ゲームを作っていると、これも便利な機能の一つでしょう。
まとめ
これで、画像を表示することが出来るようになりました。しかも、上下左右反転、色変化の機能も実装できてしまいました。Direct3Dってホント便利ですね。これだけあれば、簡単なゲームくらいはもう作れてしまうでしょう。(文字が描画できないのが難点か…)
次回は画像表示にひとつ欠けている機能、スプライト転送とついでにアルファブレンディング(半透明合成)を実装したいと思います。ここまで来れば、2D描画機能としては申し分ないですね。
追加(2006/03/17)
今回紹介したサンプルは、機種によっては正しくテクスチャーマッピングされない場合があります。おそらく、ライティング(照明)の設定が正しくされていないことが原因です。「ライトのオフ」方法を次のアルファブレンディングの回で紹介していますので、そちらをご覧ください。
追加A(2006/04/18)
今回紹介したサンプルは、機種によっては正しくテクスチャーマッピングされない場合があります。頂点の「Rhw」の値を必ず1に設定してください。