ホーム < ゲームつくろー! < DirectX10技術編

その5 DirectX10版高速フォント表示


 マルペケではDirectX9技術編のその5で「高速フォント表示」について紹介しました。これは書き込み可能なテクスチャにフォントの情報を打ち込み、そのテクスチャを板ポリゴンに貼り付け、アルファブレンドによって画面に描画する方法でした。これと同じ事はDirectX10でもできます。

 この章ではDirectX10でも同じ方法でフォント文字を描画してみます。ただ、本章の真の目的はDirectX10で書き込み可能テクスチャを作成し、それを板ポリゴンに貼り付ける方法を見る事です。これだけでもかなり大変なのがDirectX10です(-_-;



@ 書き込み可能テクスチャの作り方

 DirectX9には3種類のテクスチャがありました(IDirect3DTexture9、IDirect3DCubeTexture9、IDirect3DVolumeTexture9)。これがDirectX10になると、どどっと8種類にも増えました:

Texture1D
Texture1DArray
Texture2D
Texture2DArray
TextureCube
Texture3D
Texture2DMS
Texture2DMSArray

 1D、2D、3Dはそれぞれ次元数を表しています。1次元テクスチャは高さ1ピクセルのテクスチャです。2Dは一番なじみのある長方形型のテクスチャです。3Dはそれに奥行きが加わったいわゆる「ボリュームテクスチャ」を表します。Arrayとついているのは、配列状のテクスチャを表します。DirectX10ではテクスチャを配列で保持できるようになりました。これを使うと例えばテクスチャアニメーション(パラパラアニメ)を実現できそうです。MSとあるのは「マルチサンプリング」の事で、いわゆるアンチエイリアスがかかったテクスチャが作成できます。

 これらテクスチャはファイルから作成する事もできます(D3DX10CreateTextureFromFile関数)。ただ、本章で作るのは空のテクスチャです。これはデバイスから直接作成できます。DirectX9の時は、IDirect3DDevice::CreateTextureに値を与えていく事で作成できました。しかしDirectX10では作成するテクスチャの特性を一度D3D10_TEXTURE2D_DESC等のテクスチャ作成用の構造体に定義する必要があります:

Texture2Dを作るための構造体
typedef struct D3D10_TEXTURE2D_DESC {
    UINT Width;
    UINT Height;
    UINT MipLevels;
    UINT ArraySize;
    DXGI_FORMAT Format;
    DXGI_SAMPLE_DESC SampleDesc;
    D3D10_USAGE Usage;
    UINT BindFlags;
    UINT CPUAccessFlags;
    UINT MiscFlags;
} D3D10_TEXTURE2D_DESC;

WidthHeightは作成するテクスチャの縦横サイズです。
MipLevelsはミップマップのレベル数を指定します。0にすると1×1になるまでミップマップを作成してくれます。
ArraySizeは配列テクスチャのテクスチャ数を指定します。1枚ならば1で構いません。
Formatはテクスチャのフォーマットです。これも沢山の種類がありますが、いわゆる32ビットの整数テクスチャとしてDXGI_FORMAT_R8G8B8A8_UNORMが良く使われます。
SampleDescはマルチサンプルの属性値を指定します。
UsageはテクスチャへのCPU及びGPUのアクセス権を指定します。これは4種類ありますが、D3D10_USAGE_DEFAULTだとGPUからのみのアクセスが可能です。CPUでも書き込めるようにするにはこれをD3D10_USAGE_DYNAMICにする必要があります。
BindFlagsはこのテクスチャがどういう目的で使用されるかを指定します。良く使われるのがD3D10_BIND_SHADER_RESOURCE(シェーダの入力テクスチャとして使用)かD3D10_BIND_RENDER_TARGET(レンダーターゲットとして使用)です。
CPUAccessFlagsはテクスチャのCPUアクセスを制御するフラグを指定します。D3D10_CPU_ACCESS_WRITEにすると書き込みを許可します(書き込み可能なテクスチャのみ)。D3D10_CPU_ACCESS_READにすると読み込みを可能にします。
MiscFlagsはその他の属性を表すフラグです。3種類ありますが、わりとマニアックなので0で構いません。


 なかなかに細かいですよね。

 今回は「CPUから書き込み可能でシェーダに渡せる2Dテクスチャ」を作成したいと思っていますので、次のような設定になります:

D3D10_TEXTURE2D_DESC desc;
memset( &desc, 0, sizeof( desc ) );

desc.Width     = 256;
desc.Height    = 256
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format    = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count= 1;      // サンプリングは1ピクセルのみ
desc.Usage     = D3D10_USAGE_DYNAMIC; // CPU書き込み可能
desc.BindFlags = D3D10_BIND_SHADER_RESOURCE; // シェーダリソース
desc.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE; // CPUから書き込みアクセス可

UsageをD3D10_USAGE_DYNAMICにするのがポイントです。

 こうして作成したテクスチャ属性構造体をデバイスに渡すと、テクスチャを作成してくれます:

ID3D10Texture2D* pTexture;
pDev->CreateTexture2D( &desc, 0, &pTexture );



A テクスチャへの書き込み

 DirectX9の場合、テクスチャへ色を直接書き込むにはIDirect3DTexture9::Lockメソッドでロックして書き込み先のポインタを取得し、Unlockで閉じていました。DirectX10の場合これはID3D10Texture2D::Map及びUnmapメソッドに置き換わっています。

 まず書き込みを開始するにはID3D10Texture2D::Mapメソッドを呼び出します。これにはお約束の手順があります:

D3D10_MAPPED_TEXTURE2D mapped;
pTexture->Map(
    D3D10CalcSubresource( 0, 0, 1 ),
    D3D10_MAP_WRITE_DISCARD,
    0,
    &mapped );

BYTE* pBits = (BYTE*)mapped.pData;

 D3D10_MAPPED_TEXTURE2DはMap(ロック)した2Dテクスチャから書き込み先ポインタを受け取るための構造体です。Mapメソッドの第1引数にあるD3D10CalcSubresource関数は配列テクスチャから指定のテクスチャを指し示すためのインデックスを計算してくれるヘルパー関数です。2Dテクスチャが1枚(ミップマップレベルも1)の場合は0,0,1でお約束です。詳しくはいずれまた。D3D10_MAP_WRITE_DISCARDは書き込みのためにロックするためのフラグの1つです。第3引数はGPUがテクスチャにアクセス中だった場合のCPUの振る舞いを指定します。0にするとGPUがテクスチャのロックを解放するまでCPUはここで待ちます。D3D10_MAP_FLAG_DO_NOT_WAITを指定するとCPUのアクセスがブロックされた場合に失敗として制御を直ぐに返してくれます。第4引数にD3D10_MAPPED_TEXTURE2D変数を渡すと必要なアクセスポインタを返してくれます。

 こうしてMapしたテクスチャの書き込み先ポインタを取得できます。後は、通常のメモリブロックと同様に情報を書き込みできます。@でDXGI_FORMAT_R8G8B8A8_UNORMなどの整数テクスチャを指定した場合、メモリ上での並びは「ARGB」と反対になります!注意して下さい。

 ロックした(Mapした)テクスチャをアンロックするにはID3D10Texture2D::Unmapメソッドを用います:

pTexture->Unmap( D3D10CalcSubresource( 0, 0, 1 ) );

ロックしているのは配列テクスチャの1つなので、アンロックする時にも同じインデックスを指定する必要があるためD3D10CalcSubresource関数を再び使う必要があります。


 肝心のフォント情報を得てテクスチャに書き込む部分はDirectX9技術編その5と全く同じです。詳しくはそちらをご覧ください。



B エフェクトにテクスチャをアタッチする

 Aで作成したテクスチャを板ポリゴンに貼り付けたいのですが、この作業はシェーダ内で行われます。つまり、エフェクトにテクスチャをアタッチする必要があるわけです。

 エフェクトにID3D10Textureをそのまま渡すのかと思いきや、DirectX10はちょっと別の方法を取ります。エフェクト(ID3D10Effect)とテクスチャ(ID3D10Texture)の間を取り持ってくれるID3D10ShaderResourceViewというオブジェクト(シェーダリソースビュー)を作る必要があります。

 ID3D10ShaderResourceViewを作るにはID3D10Device::CreateShaderResourceViewメソッドを用います:

ID3D10ShaderResourceView* pTexResView = 0;
pDev->CreateShaderResourceView( pTexture, 0, &pTexResView );

第1引数にテクスチャを渡します。第2引数にはシェーダリソースビューのタイプを表すD3D10_SHADER_RESOURCE_VIEW_DESCフラグを渡します。これは第1引数に渡したテクスチャが「不定フォーマット」で作成された場合のみ具体的にタイプを指定する必要があります。今回のテクスチャは32ビットの整数テクスチャをちゃんと指定しましたので0で構いません。第3引数にシェーダリソースビューが返されます。

 なんでこんな面倒な物を作る必要があるのか?これは「シェーダ内での変数とテクスチャを関連付けるため」です。

 シェーダ内では独自にテクスチャの変数名を用いてテクスチャを表します。プログラムではその名前に対してテクスチャをアタッチしてあげる必要があります。この作業をシェーダリソースビューが取り持ってくれるんです。

 シェーダ内のテクスチャ変数名にテクスチャビューが保持しているテクスチャをアタッチするには、次のような手順を踏みます:

ID3D10EffectVariable* pEffectVar = 0;
ID3D10EffectShaderResourceVariable* pTexVar = 0;
pEffectVar = pEffect->GetVariableByName( "inputTex" );
pTexVar = pEffectVar->AsShaderResource();
pTexVar->SetResource( pTexResView );

ID3D10EffectVariableはエフェクト内の変数を表すインターフェイスです。
3行目のように、ID3D10Effect::GetVariableByNameメソッドを用いると、シェーダ内の名前に対応するID3D10EffectVariableを返してくれます。ここからさらに「入力変数」として使うために、ID3D10EffectShaderResourceVariableという長いインターフェイスをID3D10EffectVariable::AsShaderResourceメソッドで取得します。最後にSetResourceメソッドでシェーダリソースビューを指定すると、「inputTex」というシェーダ内で指定したテクスチャ変数の名前とテクスチャとが関連付けられます。



C 左上(0, 0)にするために

 フォントテクスチャを貼り付けた板ポリゴンを画面に表示する時にワールド空間座標系で考えるのはかなり大変です。一方Direct9にあった「座標変換済み頂点」はスクリーン座標系で頂点座標を指定できたので楽でした。

 DirectX10にはFVFすら無くなっているので、座標変換済み頂点ももちろんありません。よって、自分ですべて考え設定する必要があります。

 頂点の指定はやはりスクリーン座標系にしたいと思います。つまり、画面の左上を(0,0)とし、右下を画面サイズとする指定方法です。この指定方法を実現する際の壁が2つあります。1つは頂点シェーダの出力座標です。スクリーンにポリゴンを表示させるには、頂点シェーダの出力座標を(-1,-1,0)〜(1,1,1)という長方形の空間(射影空間)に納めないといけません。よって、ローカルに指定した頂点座標をいったんこの範囲に変換する必要があります。

 もう1つの壁はY軸の方向です。頂点座標をスクリーン座標系で指定する時、Y軸は画面に対して下向きと考えます。しかし、頂点シェーダが処理する射影空間はスクリーンに対して上向きを正とします。そのため、頂点シェーダ内でY軸を反転させる必要があります。

 以上を満たすために、2D板ポリゴン用の特別な射影変換行列を作ります。


 2D板ポリゴンには遠近感を入れる必要はありません。それを満たす射影変換行列というと「正射影行列」です。正射影行列についてはDirectX9技術編その51「スクリーンにある2D板ポリゴンを行列で操作する」をご覧ください。正射影行列として非常に簡単なのはスケール変換をするだけのものです:


 元のスクリーン座標指定の頂点に上の正射影行列を掛けると、縦横のスケールが縮むので射影空間内に収まるようになります。ただ、このままだとY軸の方向が逆になります。また左上が(0,0)ではなくて射影空間の原点に来てしまいます。つまりオフセットが必要です。

 射影空間にスケーリングした後に、Y軸をひっくり返して(Y軸のスケールをマイナスにする)、原点を左上(-1.0, 1.0f, 0.0f)にオフセットする。これを全部含んだ正射影行列は次のとおりです:


 元のローカル座標に上の正射影行列をシェーダ内で掛け算すると、指定の範囲にポリゴンが描画されます。



D ブレンドステートの設定

 フォントテクスチャはアルファ情報を持っています。よって、出力する時にアルファブレンディングが必要です。DirectX9の時にはブレンディング設定はIDirect3DDevice9::SetRenderStateメソッドを通して行いました。DirectX10でもこれと似たような事をするのですが、よりオブジェクト指向色が強くなっています。

 DirectX10ではブレンディング用の構造体(D3D10_BLEND_DESC)が用意されています。それに各種ブレンディング設定をしてデバイスにセットする形式になっています:

D3D10_BLEND_DESC BlendStateDesc;
memset( &BlendStateDesc, 0, sizeof( BlendStateDesc ) );

BlendStateDesc.AlphaToCoverageEnable = FALSE;
BlendStateDesc.BlendEnable[ 0 ] = TRUE;
BlendStateDesc.SrcBlend = D3D10_BLEND_SRC_ALPHA;
BlendStateDesc.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;
BlendStateDesc.BlendOp = D3D10_BLEND_OP_ADD;
BlendStateDesc.RenderTargetWriteMask[ 0 ] = D3D10_COLOR_WRITE_ENABLE_ALL;
BlendStateDesc.SrcBlendAlpha = D3D10_BLEND_ONE;
BlendStateDesc.DestBlendAlpha = D3D10_BLEND_ZERO;
BlendStateDesc.BlendOpAlpha = D3D10_BLEND_OP_ADD;

 これがまた結構に面倒になりました(^-^;。
AlphaToCoverageEnableはAlpha to Coverage法によってアルファブレンドを行うかを指定します。これについては割愛致しますが、今はFALSE(通常のブレンド方法)を設定します。
BlendEnableはアルファブレンドを使用するかを設定します。これは要素数8の固定配列になっています。要素番号はレンダーターゲット番号と対応しています。つまり、MRT(マルチレンダーターゲット)の個々についてブレンド方法をしていできるんです。これはDirectX9ではできなかったので嬉しい追加機能だったりします。
SrcBlendDestBlendは重ねる色と元の色とのブレンド方法をD3D10_BLEND列挙型で指定します。この辺りはDirectX9でもお馴染みですね。
BlendOpは重ねる時の演算方法を指定します。
RenderTargetWriteMaskは重ねる時にRGBAのそれぞれの使用・未使用を指定できます。例えばD3D10_COLOR_WRITE_ENABLE_BLUEのみ指定すると、青色だけがブレンド対象になります。全部を対象とするには上のようにD3D10_COLOR_WRITE_ENABLE_ALLを指定します。

 構造体をゼロクリアした後に、基本的には上の全部を正しく設定する必要があります。


 構造体を作ったら、それをデバイスにセットします。するのですが、直接与える事はできなくて、一度ID3D10BlendStateインターフェイスを作る必要があります::

ID3D10BlendState* pBlendState = 0;
pDev->CreateBlendState( &BlendStateDesc, &pBlendState );
pDev->OMSetBlendState( pBlendState, D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xffffffff );

ID3D10BlendStateインターフェイスはID3D10Device::CreateBlendStateメソッドで上のようにブレンド構造体を渡して作ります。それをOMSetBlendStateメソッドに渡す事でブレンドステートを設定した事になります。第2引数に渡している4次元ベクトルはブレンドファクタの値です。ブレンドファクタとは各色のブレンド係数を自分で決められる素敵な機能ですが、ここでは説明を割愛します。第3引数にはレンダーターゲットのアップデートフラグを32ビットで指定します。レンダーターゲットは8枚あり、各色ごとの更新を指定できるようです。

 これでブレンドステートの設定もできました。



D シェーダ

 DirectX10は固定機能が廃止されていますので、シェーダも自分で書く必要があります。ポリゴンにテクスチャを貼り付けるには、ピクセルシェーダ内でテクスチャを拾って点を打ちます。今回のフォントテクスチャをポリゴンに穿つシェーダでは、頂点シェーダでCの正射影行列を掛け算します。ピクセルシェーダでは入力テクスチャを普通にフェッチします:

Texture2D inputTex;
float4x4 projMat : PROJECTION;
float2 offset;


SamplerState inputSampler
{
   Filter = MIN_MAG_MIP_LINEAR;
   AddressU = Wrap;
   AddressV = Wrap;
};


struct VS_INPUT {
   float4 Pos : POSITION_IN;
   float2 UV : TEXCOORD0_IN;
};

struct VS_OUTPUT {
   float4 Pos : SV_POSITION;
   float2 UV : V_TEXCOORD0;
};

struct PS_INPUT {
   float4 Pos : SV_POSITION;
   float2 UV : V_TEXCOORD0;
};


// 頂点シェーダ
VS_OUTPUT VS( VS_INPUT In )
{
   VS_OUTPUT Out = (VS_OUTPUT)0;

   Out.Pos = In.Pos + float4( offset, 0.0f, 0.0f );
   Out.Pos = mul( Out.Pos, projMat );
   Out.UV = In.UV;

   return Out;
}


// ピクセルシェーダ
float4 PS( PS_INPUT In ) : SV_Target
{
   return = inputTex.Sample( inputSampler, In.UV );
}


// テクニック
technique10 SimpleRender
{
   pass P0
   {
      SetVertexShader( CompileShader( vs_4_0, VS() ) );
      SetGeometryShader( NULL );
      SetPixelShader( CompileShader( ps_4_0, PS() ) );
   }
}


上のシェーダには描画位置を変更するためにoffsetという変数も追加されています。


 テクスチャをフェッチするためには「サンプラー(Sampler)」を定義する必要があります。サンプラーの設定はDirectX9と少し文法が異なります:

SamplerState inputSampler
{
   Filter = MIN_MAG_MIP_LINEAR;
   AddressU = Wrap;
   AddressV = Wrap;
};

Filterにはサンプリングする時のフィルタリング方法を指定します。DirectX10では次の指定が可能です:

サンプリングフィルタ指定(D3D10_FILTER)
D3D10_FILTER_MIN_MAG_MIP_POIN,
D3D10_FILTER_MIN_MAG_POINT_MIP_LINEAR,
D3D10_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT,
D3D10_FILTER_MIN_POINT_MAG_MIP_LINEAR,
D3D10_FILTER_MIN_LINEAR_MAG_MIP_POINT,
D3D10_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
D3D10_FILTER_MIN_MAG_LINEAR_MIP_POINT,
D3D10_FILTER_MIN_MAG_MIP_LINEAR,
D3D10_FILTER_ANISOTROPIC,
D3D10_FILTER_COMPARISON_MIN_MAG_MIP_POINT,
D3D10_FILTER_COMPARISON_MIN_MAG_POINT_MIP_LINEAR,
D3D10_FILTER_COMPARISON_MIN_POINT_MAG_LINEAR_MIP_POINT,
D3D10_FILTER_COMPARISON_MIN_POINT_MAG_MIP_LINEAR,
D3D10_FILTER_COMPARISON_MIN_LINEAR_MAG_MIP_POINT,
D3D10_FILTER_COMPARISON_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
D3D10_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT,
D3D10_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR,
D3D10_FILTER_COMPARISON_ANISOTROPIC,
D3D10_FILTER_TEXT_1BIT,

MINやMAGというのは縮小・拡大フィルタの事です。MIPはミップマップフィルタです。またPOINT(点サンプリング)、LINEAR(線形サンプリング)、ANISOTROPIC(異方性サンプリング)です。COMPARISONというのはDirecX10の新しい機能で、サンプリングする時に比較値と比較をしてサンプリングする事ができます。


 DirectX10でのテクスチャサンプリングはDirectX9のそれとは大きく変わりました。少しオブジェクト指向っぽくなっています:

inputTex.Sample( inputSampler, In.UV );

inputTexは入力テクスチャ変数です。そのSampleメソッドの引数にサンプラーとUVを指定します。DirectX9ではサンプラーがテクスチャを持っていましたが、DirectX10ではこれが分離されたわけです。



E DirectX10での出力結果

 何とも大変な設定の数々ですが、こうして色々と設定し実装する事で次のようなフォントを描画する事ができました:


 DirectX10、本当に大変です。でも、昨今のハイスペックで超複雑な商用ゲームのクオリティに答えるにはこの位の自由度がやはり必要です。超本格的にゲームを作りたい方はDirectX10以降で、手軽に作りたい方はDirect9を選択するのが賢明かなと、DirectX10プログラムを作るとひしひしと感じますね。

 上の出力を与えるサンプルプログラムをこちにアップしました。DirectX10の基本的な機能の一面がぎゅっと詰まっていると思いますのでご参照下さい。