Managed DirectX ゲーム画面のキャプチャ

開発日記らしく、今日も技術的なことを書こうかと思います。

VBでLoFを作っていたときにも搭載していた機能なのですが、ホームページ素材用にゲーム画面をキャプチャーする機能をC#版にも作ろうとしていました。今まではスクリーンショットを押してキャップっていたのですが、それではベストショットを取り逃してしまうためゲーム画面をひたすらキャプチャしてファイルに保存する機能を実装しました。

Direct3DXには、フロントバッファのデータを取ってくるメソッドが存在しており、当然Managedにもあったわけですが…レンダリングの際に毎回毎回そのメソッドを呼び出しているとレンダリングに1秒以上かかってしまい、あまりにも遅かったため別の方式をとることにしました。で、結局VB時代と同じ方法で…基本的にDirectXのサーフェイスクラスからはデバイスコンテキストが取得できるため、レンダリングの完了したPresent前のバックバッファから適当なHbitmapに画像を転送して、それを保存してやればゲーム画面を保存できるわけです。しかも、今回はC#を使っているため、Bitmapオブジェクトへと転送してしまえばそのままSaveメソッドを使って画像が保存できてしまいます。あ〜、世の中便利になりましたね。

さっそく、Managed DirectX関連のメソッドをあさって…まずはバックバッファの取得…これは簡単で…

Surface sf = D3DDev.GetBackBuffer(0,0,BackBufferType.Mono);

ってやってやれば、バックバッファが取得できます。でもって、次にデバイスコンテキストを取得します。

Graphics g = sf.GetGraphics();

あ〜簡単!なんてすばらしい世の中になったのでしょう。Managedでは.NET標準のGraphicsオブジェクトを取得してくれるメソッドかちゃんと用意されているのです。あっ、ちなみに、SurfaceLoader.Save(〜)とやるとエラーが起きます。原因は不明…他のページだと成功しているみたいなんですが。何でだろう?

で、後はこのGraphicsオブジェクトからBitmapオブジェクトを生成すればいいのですが…世の中そんなに甘くない。残念ながら、.NETは「BitmapからGraphicsは生成」できても「GraphicsからBitmapは生成」出来ないようなのです。しかも、Graphicsに対して書き込めても、そこから転送は出来ないようで…うん…困った…しかし、GraphicsからDCのハンドルは取得できるため、それを利用して…

結局、Win32APIに頼るはめになりました…前回の日記にも書きましたが、API…使うの面倒なんだよね。

あきらめて、新しく空のBitmapオブジェクトを作って、そこからGraphicsを作って、Hdc取得して、さっきのバックバッファのHdc間をBitBltで転送…で、転送したBitmapを保存して無事にキャプチャ成功です。あ〜面倒…

こんな感じで毎フレーム保存するとFPS20くらいはでました。早いな・・・このパソコン。まぁ、実用レベルでしょう。一応、そのコードの一部を書いておきます。


	//BitBltの宣言。面倒。
	[DllImport("gdi32.dll")]
	public static extern bool BitBlt(
		IntPtr hdcDest,
		int xDest,
		int yDest,
		int width,
		int height,
		IntPtr hdcSrc,
		int xSrc,
		int ySrc,
		System.Int32 rasterOp
		);
		


		//あっ、これやらないとバックバッファのDCが取得できないらしい。
		D3DDev.PresentationParameters.PresentFlag = PresentFlag.LockableBackBuffer;



					  
		//実際のキャプチャコード	
		saveCount++;
		string ss = "";
		for (i = (saveCount.ToString()).Length;i < 5;i++)
		{
			ss += "0";
		}
		ss += saveCount.ToString();
		s = saveImgPath + @"img" + ss + ".jpg";
					
		sf = D3DDev.GetBackBuffer(0,0,BackBufferType.Mono); //バックバッファの取得

		Graphics g,gd;
		g = sf.GetGraphics(); //Graphicsの取得
		Bitmap bb = new Bitmap(sf.Description.Width,sf.Description.Height,g);
		gd = Graphics.FromImage(bb);
		IntPtr srcHDC = g.GetHdc();  //Hdcの取得
		IntPtr destHDC = gd.GetHdc();
		BitBlt(destHDC,0,0,bb.Width,bb.Height,srcHDC,0,0,0xCC0020); //転送
		gd.ReleaseHdc(destHDC);  //ここら辺はマネージドじゃないので解放せねば
		g.ReleaseHdc(srcHDC);

		bb.Save(s,System.Drawing.Imaging.ImageFormat.Jpeg); //JPEGで保存。.NETって、ホント便利

		sf.ReleaseGraphics();
		bb.Dispose();
		g.Dispose();

すっかり忘れてましたが、

D3DDev.PresentationParameters.PresentFlag = PresentFlag.LockableBackBuffer;

をどっか(OnReset)あたりに書いておかないと、バックバッファのDC取得に失敗します。

これで、適当なパスにゲーム画面が連番で保存されます。