ID3DX10Fontがいなくなった件

とりあえずそろそろFPS表示したいなぁとか思いつつ調べてみたら、DirectX11になってからSpriteさんとかFontさんがいなくなったらしい。ので、自分で作らなければならない。(実は11からも呼び出せるとかってオチじゃなければ(^^;)
ってことで、とりあえず昔懐かしのやり方でDIBSectionにDrawTextしてそこからテクスチャにしてビルボードで貼り付けるかー、って感じでチャレンジしてみた。(たぶんID3DX10Fontさんも似たような実装なのでは)



こんな感じ。

  • CreateDIBSection()でDIBビットマップを作る(サイズは文字表示領域の大きさに合わせる)
  • CreateFontIndirect()でお好みのフォントを作る
  • GetDC(NULL)+CreateCompatibleDC()でメモリDCを作る
  • SelectObject()でメモリDCにフォントとDIBビットマップを選択する
  • SetTextColor()で前景色を設定する
  • SetBkMode()で背景を透明に設定する
  • DIBのポインタ経由で直接ビットイメージをクリアする(背景色で塗りつぶす)
  • DrawText()で表示領域のお好みの位置へ文字を書き込む×表示したい文章の数繰り返し
  • メモリ上に.bmpファイルのイメージデータを作る(DIB+BMPヘッダ類)
  • D3DX11CreateShaderResourceViewFromMemory()でSRVを作る
  • 作った文字テクスチャを画面の適切な位置に貼り付ける


それで動かしてみたら、テクスチャ貼り付けシェーダ向けのcbufferを作ろうとしたところでID3D11Device::CreateBuffer()がE_INVALIDARGを返します〜\(^o^)/
ってなんでですか〜(゚д゚)

ということで、うまく動作しているときとの差分がバッファサイズくらいしかないのでヘルプを調べてみると、Windows DirectX Graphicsヘルプの「Direct3D 11」−「Reference」−「Direct3D 11」−「Resource」−「Structures」−「D3D11_BUFFER_DESC」ページ「Remarks」項に

If the bind flag is D3D11_BIND_CONSTANT_BUFFER then the ByteWidth value must be in multiples of 16, and less than or equal to D3D11_REQ_CONSTANT_BUFFER_ELEMENT_COUNT.

って書いてあったよ〜\(^o^)/
float4の倍数じゃないとダメってことかー。cbufferがfloat4を基本単位にしてそうなのは知ってたけど、ライブラリとかドライバ側で適当に処理してくれないのかー。
ちなみにD3D11_REQ_CONSTANT_BUFFER_ELEMENT_COUNTは手元のD3D11.hだと4096。

あと、D3DX11CreateShaderResourceViewFromMemory()はα値を反映してくれない気がする。。。



それで、動かしてみるとすげー遅い。8fpsとかどこのWin95ですかー。
まずCreateDIBSectionが重い。fpsから概算すると100msくらいかかってる気がする。あとDrawTextは10ptの文字10〜20文字程度を出すのに300usくらいかかってる気がする。
DIBとかは作りっぱにしておいて、DrawTextとDIBからテクスチャへの転送だけをやる方式に変更するとDrawTextの呼び出し回数だけが問題になる感じかな。毎フレーム数回呼ぶ程度なら大丈夫だと思うけど、数100回とか呼ぶとフレームレートがガタ落ちになるね。まぁふつうは文字列を何フレームも表示し続けるだろうから、一度作ったテクスチャをひたすら表示するだけになって大丈夫かな。

ということで、試しにあらかじめDrawTextしておいたDIBのポインタからテクスチャにID3D11DeviceContext::UpdateSubresource()でデータ転送しようとしたんだけど、そうしたら転送ができなかった…どういうことなの…
ID3D11DeviceContext::Map()だと4msくらいかかって遅いし、しかも上下反転してるけどそれでも転送はできる。引数がどこか間違ってるのかもしれないけど、正直何が悪いんだか…

とりあえずfps表示のみに絞ってテクスチャサイズをフルHDから192x108まで落とすとフレームレートに影響しなくなった。もともとフレームレートを落とさずにfps表示がしたかったので個人的には目的を達成できたのでもういいや。



最終的に落ち着いた方式のざっくりまとめ。
○初期化フェーズ

  • CreateDIBSection()でDIBビットマップを作る(サイズは文字表示領域の大きさに合わせる)
  • CreateFontIndirect()でお好みのフォントを作る
  • GetDC(NULL)+CreateCompatibleDC()でメモリDCを作る
  • SelectObject()でメモリDCにフォントとDIBビットマップを選択する
  • SetTextColor()で前景色を設定する
  • SetBkMode()で背景を透明に設定する
  • ID3D11Device::CreateTexture2D()でテクスチャを作る
  • ID3D11Device::CreateShaderResourceView()でSRVを作る
  • cbufferやら必要そうなものも作る

○テクスチャに文字を書き込む(数フレームに一度。文字を変更するときだけ)

  • DIBのポインタ経由で直接ビットイメージをクリアする(α値255の背景色で塗りつぶす)
  • DrawText()で表示領域のお好みの位置へ文字を書き込む

レンダリング(毎フレーム)

  • ID3D11DeviceContext::Map()してDIBポインタからテクスチャへデータを転送する(DIBを更新してなければ不要。書き込み時に上下を逆さまにする)
  • Vertex Shaderでぴったり文字表示領域の位置になるような場所に四角形ポリゴンを出力する
  • Pixel Shaderで文字テクスチャを読み出して背景色でなければ表示する

テクスチャとDIBビットマップを複数持てば、複数の表示領域ごとにバラバラに表示用データを持てるから作りだめ的なこともできるね。(ただし、DrawTextするごとにSelectObjectで切り替えが必要だけど)


ややこしい(文章で説明しにくい)ので、シェーダのコードだけ貼り付けておく。とりあえずビルボードのVertexShaderは他でも使いまわせそうかなぁ。

//------------------------------------------------------------------------------
//	文字列を表示するための専用シェーダ
//
//	Depth test Offを推奨
//	Texture #0に文字列を描画済みのテクスチャデータを入れておくこと
//------------------------------------------------------------------------------


//------------------------------------------------------------------------------
cbuffer VSGlobalSettings
{
	float4	g_draw_region;		// 描画範囲(left, top, width, height)
	float2	g_screen_size;		// 画面サイズ(width, height)
};
//------------------------------------------------------------------------------

sampler		g_sampler	: register(s0);
Texture2D		g_texture	: register(t0);

//------------------------------------------------------------------------------
struct VSoutPSin
{
	float4	position_	: SV_POSITION;
	float2	coord_	: TEXCOORD0;
};
//------------------------------------------------------------------------------


//------------------------------------------------------------------------------
//	座標値から -1.0〜+1.0 の値に変換する
float scaling(float value, float screen_size)
{
	return (value / screen_size) * 2.0f - 1.0f;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
//	ビルボードを作る
//
//	Projection Transform後の座標値を直接算出して出力する
//	入力は頂点データではなく、ID だけをもらう
VSoutPSin VS(uint id : SV_VertexID)
{
	VSoutPSin	output;

	float		left, right;	// X座標
	float		top, bottom;	// Y座標

	left = scaling(g_draw_region.x, g_screen_size.x);			// (left)       / 画面幅
	right = scaling(g_draw_region.x + g_draw_region.z, g_screen_size.x);	// (left+width) / 画面幅
	top = -scaling(g_draw_region.y, g_screen_size.y);			// (top)        / 画面高
	bottom = -scaling(g_draw_region.y + g_draw_region.w, g_screen_size.y);	// (top+bottom) / 画面高

	// IDに応じた座標値を返す。Z座標は念のため一番手前にしておく(Depth Test OnでもLessEqualなら通る)
	      if(id == 0){	// 左下
		output.position_ = float4(left, bottom, 0.0f, 1.0f);
		output.coord_    = float2(0.0f, 1.0f);
	}else if(id == 1){		// 左上
		output.position_ = float4(left, top, 0.0f, 1.0f);
		output.coord_    = float2(0.0f, 0.0f);
	}else if(id == 2){		// 右下
		output.position_ = float4(right, bottom, 0.0f, 1.0f);
		output.coord_    = float2(1.0f, 1.0f);
	}else             {	// 右上
		output.position_ = float4(right, top, 0.0f, 1.0f);
		output.coord_    = float2(1.0f, 0.0f);
	}

	return output;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
//	文字を描画済みのテクスチャからデータを持ってきて背景色でなければ描画に回す
//	今は固定値だがcbufferでα値を変更できるようにすると透過文字を作れる
float4 PS(VSoutPSin input) : SV_Target
{
	float4		texture_color;


	texture_color = g_texture.Sample(g_sampler, input.coord_);


	if(texture_color.w == 1.0f){
		discard;			// 背景色ならPixelを破棄する
	}

	// α値は背景色1、テキスト0でくるので反転させる
	texture_color.w = 1.0f - texture_color.w;

	return texture_color;
}
//------------------------------------------------------------------------------

DrawTextではα値を0固定で書き込むようなので、背景色をα値255としておいてシェーダ側でα値をもとにテキストなのか背景なのか判断してみた。
今回は背景ならPixelを破棄してフィルレートを削減する方向にしたけど、α合成で背景を0とする方法もある。あと、cbufferでテキストのα値をわたすように変更すれば任意の透明度で文字を表示することもできる。



ところで調べてみたら、Direct2DってやつのDirectWriteってので文字をテクスチャに書き込めるらしいので、DirectX11ではDirectWriteがあるからID3DX10Fontは削除されたってことなのかも。
あれ?でもDirect2Dのヘルプが見当たらない…MSDNのサイトしかない?



[2011/02/01 01:15]追記
UpdateSubresource()がダメな理由が判明した。
DirectX10のサンプルコードと丹念に比較して分かったことだけど、どうもテクスチャがD3D11_USAGE_DEFAULTじゃなきゃ転送されないっぽい。D3D11_USAGE_DYNAMIC+D3D11_CPU_ACCESS_WRITE指定だと転送されないんですね。
ってなんでですか〜(゚д゚)
いや、おかしいでしょ。Windows DirectX Graphics Documentationヘルプの「Direct3D 11」−「Reference」−「Direct3D 11」−「Resource」−「Enumerations」−「D3D11_USAGE」ページの「D3D11_USAGE_DYNAMIC」項に

<原文>
There are two ways to update a dynamic resource: if your data is laid exactly the way the resource stores it, use ID3D11DeviceContext::UpdateSubresource, otherwise, use a Map method. <スーパー意訳タイム>
「D3D11_USAGE_DYNAMIC」指定のリソースを更新するには2つの手段があります。手元のデータが転送先リソースとまったく同じ並びであれば、ID3D11DeviceContext::UpdateSubresource()を使ってください。データ並びが違うならMap()メソッドを使ってください。

って書いてあるじゃないかー!嘘付くんじゃねぇ、いい加減にしろ!(#^ω^)ピキピキ
1週間も無駄な作業をやってしまった…orz

そもそも同じページの「Resource Usage Restrictions」に「ID3D11DeviceContext::UpdateSubresource()はGPUアクセスですよ」って書いてあるけど、readなのかwriteなのか書いてないからすぐ下の表と見比べてもあってるのか分からないじゃん!ヽ(`Д´)ノ

テクスチャ作成のパラメータをまとめると、

  • Map()するなら3D11_USAGE_DYNAMICとD3D11_CPU_ACCESS_WRITEを指定する
  • UpdateSubresource()するならD3D11_USAGE_DEFAULTだけ(CPUフラグは0)

って感じか。
ちなみに転送時間はどちらでもあまり変わらない気がする。あと、結局UpdateSubresource()でも上下は反転してて、CreateDIBSection()するときにbmiHeader.biHeightを負の数(フルHDなら-1080)で指定したら反転する件は解消した。

ということで、教訓。

  • 動いてるソースを見ろ
  • ヘルプは疑ってかかれ
  • ヘルプは隅々まで読め

3つ目の教訓は今回と直接関係ないけど、たまにさらっと重要なことがヘルプに書かれているので。
例えば上記「D3D11_USAGE」ページの「Resource Bind Options」項の注4なんかに「D3D11_USAGE_DYNAMIC指定のテクスチャはtexture arrayとかmipmap chainにできないからよろしく」的なことがさらっと書かれている。