マルチなテクスチャ

・基礎シリーズ第五弾

 また機能の説明と使い方です……。しかも今回はかなり説明的。次こそエフェクトな話……の前に、加算合成か映り込みについて書いた方が良いかも。

 あと、”マルチな”と言っても某ゲームとは一切関係ありません。やったこと無いし>某ゲーム
タイトルが思い付かなかったのでこうなりました。”マルチテクスチャ”に”な”を付けただけ……。

 今回は、ちょっと当たり前な事しか書けそうにないです。なんだか自分で勉強しながら書いてるだけのような感じです。それに覚えたての事を書いているので、嘘を書くかもしれません。まず疑ってかかりましょう。間違いを見つけたら、お手数ですが掲示板に書くかメールしてください。

 マルチテクスチャをすでに使った事がある、またはマルチテクスチャを使うつもりが無い人は流し読みでいいでしょう。全部読もうとすると、疲れる上に時間を食います。文の量では、画像が少ない変わりに2回分はあります。

 

・まず用語解説

 テクスチャを重ね合わせると、表現の幅が広がります。DirectX6では、ハードが対応していれば一度のポリゴン描画(シングルパス)で8枚までのテクスチャを同時に重ねて描画する事ができます。それを、シングルパス マルチプルテクスチャブレンディング(single-pass multiple texture blending)と言います。……今、長いなぁと思いませんでしたか?僕は思いましたが。
ハードがシングルパスのマルチプルテクスチャに対応してないため、複数回のポリゴン描画でテクスチャの重ね合わせをしてマルチテクスチャを実現する場合はマルチパステクスチャブレンディング(multipass texture blending)という呼び方をします。一度で済むのがシングルパス、複数回なのがマルチパス。

 マルチプルテクスチャとマルチパステクスチャ、似てますがやってることは違います。結果は同じにすることができますが。マルチテクスチャと言うと、どっちのことかわかりませんよね。広義のマルチテクスチャが前者と後者両方で、狭義のマルチテクスチャが前者だけ、といったところでしょうか。まぁα合成など他の用語も使い方に広義と狭義はありますが。
以後、広義も狭義も両方ひっくるめてマルチテクスチャと表記します。どっちの意味で使っているかは書かないので文の前後で判断してください(ひどい)

 

・威力を知れ!(大袈裟)

 テクスチャを重ね合わせると、表現の幅が広がります。で、どう広がるのか?


ちょっと陰影のある
煉瓦の壁(上に光源)

  
マルチテクスチャで
丸い陰影を付ける

 ちょっとリアルになってません?……なってない?なんだか上に光源がありそうな感じがすると思うんですが。
こういう処理も、ハードが対応していれば高速に描画できます。対応していない場合はマルチパスで描画しなくてはいけません。

 マルチプルテクスチャの良い所は、どういう合成をするかを設定できる事です。特にα値がRGB値と別なのが良いですねぇ。α合成と同時に使うとかなりいろいろなことができそうです。
反面、残念ながらマルチパステクスチャで代替できない合成の仕方が出てきますが。

 マルチプルテクスチャを使うためにする事は、主に頂点フォーマットの自作(構造体作るだけ)とテクスチャステージの設定(数行加えるだけ)の2つです。サンプルを見ながらやったら10分でできました。

 まずは頂点フォーマット作りから説明します。

 

・FlexibleVertexFormat

 D3DVERTEX・D3DLVERTEX・D3DTLVERTEXでは、テクスチャのUV値(座標)をテクスチャ一枚分しか指定できません。
DirectX6になって頂点フォーマットを自分で作る事ができるようになりました。これをFlexible Vertex Format(可変頂点フォーマット)と言います。マルチテクスチャを使うなら、複数のテクスチャのUV値が指定できる頂点フォーマットを作る必要があります。なんだか面倒くさそうですけど、今までと変わる所はそれほど多くありません。しかも簡単。

 まず頂点フォーマットに使える要素の説明。

FlexibleVertexFormat(FVF)用フラグ

フラグ名 用途 形式
D3DFVF_XYZ

D3DFVF_XYZRHW

D3DFVF_RESERVED1

D3DFVF_NORMAL

D3DFVF_DIFFUSE

D3DFVF_SPECULAR

D3DFVF_TEX0〜
    D3DFVF_TEX8

頂点座標(変換前)

頂点座標(変換後)

使用しない

法線ベクトル

頂点色(拡散色)

鏡面反射色

テクスチャの枚数

FLOAT * 3

FLOAT * 4

DWORD

FLOAT * 3

DWORD

DWORD

FLOAT * 2
    * 枚数

 これらを組み合わせて頂点フォーマットを作ります。ちなみに、DrawPrimitiveで頂点形式を指定する時に使うD3DFVF_VERTEX・D3DFVF_LVERTEX・D3DFVF_TLVERTEXも、これらのフラグの組み合わせです。D3DTYPES.Hで以下のように定義されています。
#define D3DFVF_VERTEX ( D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 )
#define D3DFVF_LVERTEX ( D3DFVF_XYZ | D3DFVF_RESERVED1 | D3DFVF_DIFFUSE | D3DFVF_SPECULAR | D3DFVF_TEX1 )
#define D3DFVF_TLVERTEX ( D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_SPECULAR | D3DFVF_TEX1 )

 用途についてはD3DVERTEX・D3DLVERTEX・D3DTLVERTEXを使っていればわかると思うので、簡単に説明して終わりにします。

D3DFVF_XYZ 変換前の頂点の座標。使うのはX・Y・Zの3つ。
D3DFVF_XYZRHW こっちは変換後の頂点の座標。使うのはX・Y・Z・RHWの4つ。
D3DFVF_RESERVED1 DrawPrimitiveでは使用しないDWORD型のメンバ。D3DLVERTEXには、位置(XYZ)と色(DIFFUSE)の間に使用しないDWORD値があり、それに使うフラグ。
D3DFVF_NORMAL 頂点の法線ベクトル。色の情報はLightStateに設定されているマテリアルを使用する。
D3DFVF_DIFFUSE 頂点の色。ライティング後の拡散反射色に使用。
D3DFVF_SPECULAR 鏡面反射色。Direct3Dでは、テクスチャブレンド後この色を加算合成する(Rampでは白の半透明を使用)。
D3DFVF_TEX0〜D3DFVF_TEX8 テクスチャの枚数。2枚分ならD3DFVF_TEX2を使用。D3DFVF_TEX0は指定無しでも同じです(フラグの値が0x0000)。

 D3DFVF_RESERVED1がちょっと特殊ですね。D3DLVERTEXの中身は以下のようになってますが、その中のdwReservedに使います。

XYZ RESERVED1 DIFFUSE SPECULAR TEX1
  dwX     dwY     dwZ   dwReserved  dcColor  dcSpecular   dvTU     dvTV  

 D3DFVF_RESERVED1は座標情報と色の情報の間に、使用しない32bit長の部分がある時に使用します。自分で頂点フォーマットを作る場合はあまり使わないかもしれません。

 

 さて、実際に使ってみますが、既存の頂点形式を使うのと比べても変更点は少ないです。元々D3DFVF_VERTEXなどがFVFですしね。
まず頂点データ用の構造体を作ります。構造体の名前、メンバ名は自由です。

例)

struct MYVERTEX
{
FLOAT x, y, z;
DWORD color;
FLOAT tu1, tv1;
FLOAT tu2, tv2;
};

 この例だとメンバ名が単純でわかりにくいですが、例えばテクスチャを1枚目が模様、2枚目が陰影(ライトマップ)という使い方なら、tu1・tu2じゃなくてtexu・lmapuみたいな名前にしておくと間違えにくくわかりやすくなります。
あと、既存の頂点フォーマットの構造体を流用するなんて事もできます。要はDrawPrimitiveに渡すポインタが指すメモリの内容が正しければそれでいいです。

例)

struct MYVERTEX
{
D3DVERTEX v;
FLOAT tu2, tv2;
};

 頂点データの代入の時少しだけ面倒で見にくくなりますが(この例だとMYVERTEX.xだったのがMYVERTEX.v.xになる)、マクロとかでやってしまえば気にならないでしょう。

 

 次に、DrawPrimitive系を使う時に頂点形式をFVFのフラグで指定します。これもとっても簡単。いつもはD3DFVF_VERTEXなどを使っている所を、フラグの組み合わせを使って指定するだけです。構造体のメンバがX, Y, Z, DIFFUSE, TU1, TV1 ,TU2, TV2という順番なら、D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX2と指定します。……長い場合はマクロにした方が良いですね。

pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX2, myVertices, 4, NULL );

 以上。これだけで自作の頂点形式でポリゴンが書けます。あとは頂点データの代入などをマクロなどでやってしまえば、今までの頂点形式と同じように使えます。

 話が戻りますが、構造体のメンバの並びは決まっているみたいです。DrawPrimitiveでの頂点フォーマットの指定も並びまでは指定できないですし(フラグのorとってやってるので)。順番は以下のようになっているようです。

D3DFVF_XYZ D3DFVF_RESERVED1 D3DFVF_NORMAL D3DFVF_TEX0〜
    D3DFVF_TEX8
D3DFVF_XYZRHW D3DFVF_DIFFUSE D3DFVF_SPECULAR

 表の左から順に構造体のメンバにしてください。上と下に分かれているフラグは、どちらか片方だけ指定してください。例えばD3DFVF_NORMALD3DFVF_SPECULARは同時に使用できません。その他も同様です。
赤い文字で書いたフラグは、使わない場合は省略可能なフラグです。そういえば、RGB EmulationではD3DFVF_SPECULARを省略しても動きます。HALだと無理なのもありそう。省略しない方が吉?情報求ム。

 これで複数のテクスチャ座標が使える頂点フォーマットが使用できるようになりました。次は、どのようにテクスチャを合成するかを設定します。

 

・テクスチャステージ

 テクスチャとポリゴンの色を合成する事を、そのまんまテクスチャ合成(texture blending)と言います。……素直にテクスチャブレンディングって書きたいけど、DirectX5の日本語ヘルプがこうだからなぁ。αブレンディング→α合成との統一性もあるし。何より、こうすると見た目がすっきりして見やすい。

 テクスチャ合成のデフォルトはD3DTBLEND_MODULATE(色はDiffuse(ポリゴンの色) * テクスチャ、α値はDiffuseの色をそのまま使う)という設定です。これを今回は以下のような図で表します。

DIFFUSE TEXTURE
     
D3DTBLEND_MODULATE
Output

 これは、DiffuseとテクスチャをD3DTBLEND_MODULATEで合成して描画、という図です。

 Direct3Dでは、テクスチャ合成をどんな順番でどのテクスチャとどの要素で合成するかなどをテクスチャステージ(texture stage)で設定します。例えば、Diffuseに煉瓦のテクスチャを掛け合わせて、それにライトマップを掛け合わせるという処理(最初に使った画像の合成もこうしている)はこんなコードになります(α値の合成は省略)。

// ステージ0の設定
pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE );

// ステージ1の設定
pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT );
pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE );

 で、図で書くとこうなります。最初に使った画像の合成途中経過も付けてみましょう。

ス 
テ 
| 
ジ 
0 
 
 D3DTA_DIFFUSE 

 D3DTA_TEXTURE 
      
  D3DTOP_MODULATE
    
ス 
テ 
| 
ジ 
1 

D3DTA_TEXTURE

D3DTA_CURRENT
      
D3DTOP_MODULATE   
   
     
Output
   

 マルチプルテクスチャを試すなら、SDKに入っているmfctex.exeがすごく便利です。RGB値とα値のテクスチャステージをいろいろと試す事ができます。これをいじっていれば使い方がわかると思います。HALがマルチテクスチャに対応してない場合は、[Change Driver]でRGB Emulationを使ってください。

 ではまた順に説明しましょう。

 う、長くなりそうな気配が……。もう結構長いですけど。すでにここまででこのHTMLファイルのサイズが21KB行っちゃってるし。

 1つのテクスチャステージで1つの合成ができます。使えるテクスチャステージ数はハードによって異なります。
RGB値(色)とα値は別々に合成します。テクスチャステージに設定するのは、色の場合はD3DTSS_COLORARG1D3DTSS_COLORARG2D3DTSS_COLOROPの3つです。使い方はD3DTSS_COLORARG1D3DTSS_COLORARG2D3DTSS_COLOROPで合成する、という感じですね。α値の合成はD3DTSS_ALPHAARG1D3DTSS_ALPHAARG2D3DTSS_ALPHAOPです。

 D3DTSS_COLORARG1   D3DTSS_COLORARG2 
     
D3DTSS_COLOROP
Output

 このテクスチャ合成1回で、テクスチャステージ1つ分です。いつも使っているのはステージ0(1つめのテクスチャステージ)だけです。
それぞれの設定する事はこうなってます。

D3DTSS_COLORARG1 参照する色の要素その1  D3DTSS_ALPHAARG1 参照するα要素その1
D3DTSS_COLORARG2 参照する色の要素その2  D3DTSS_ALPHAARG2 参照するα要素その2
D3DTSS_COLOROP 色をどう合成するか  D3DTSS_ALPHAOP α値をどう合成するか

 

 ではそれぞれに指定するフラグの中でもメジャーな物を書いてみます。マイナーなのもやりたいですけど、長くなるのと、ハード・ドライバが対応してないのがオチだろうということで。まずCOLORARGALPHAARGで設定するフラグ。とりあえず3種類だけ。

D3DTA_CURRENT 前回の合成結果。図で言えば紫の矢印の先の画像を使います。ステージ0で使うとD3DTA_DIFFUSEと同じ効果。
D3DTA_DIFFUSE 頂点の色。D3DTLVERTEX・D3DLVERTEXでいうD3DTLVERTEX.dcColorの値をそのまま使う。
D3DTA_TEXTURE テクスチャ。予めSetTextureで設定された物を使う。ARG1に設定して使う(ARG2にはできない)。

 補足。RGB Emulationで、ステージ0でD3DTSS_COLORARG1D3DTA_CURRENTを設定して使うと、何故かUnsupportと出ます。ステージ0なら素直にD3DTA_DIFFUSEを使うか、D3DTSS_COLORARG2に設定しておきましょう。

 

 次にCOLOROPに設定する数値(D3DTEXTURESTAGESTATETYPE列挙型)。よく使うであろう5種類。

D3DTOP_DISABLE 合成しない。合成をしない分だけ速くなる。
D3DTOP_SELECTARG1 1つめのブレンド元(要素)をそのまま使う(ARG1の内容をそのまま使う)。ARG2は無視する。
D3DTOP_SELECTARG2 2つめのブレンド元(要素)をそのまま使う。D3DTOP_SELECTARG2の逆。
D3DTOP_MODULATE ARG1×ARG2
D3DTOP_ADD ARG1ARG2

 他にもD3DTOP_MODULATE2XとかD3DTOP_ADDSIGNEDとかD3DTOP_SUBTRACTなど便利そうな物は多いですが、実装されてなさそう、という問題があります。D3DTOP_SUBTRACTはDirect3Dで減算合成をする唯一の方法ですが、RGB Emulationからして対応してません。
追記:SANDMANさんによると(情報ありがとうございました)、RivaTNTだとバンプマップ関連以外はほとんど使えるようです。テクスチャステージも4つまで使えるし。おそるべしRivaTNT……。

 バンプマッピングの時にもCOLORARGCOLOROPは使います(バンプマップ用のフラグもいろいろあります)が、今回は説明しません。

 

 さて、記述の仕方ですが、まずテクスチャをセットします。いつもは1枚分だけでしたが、今回は使う分だけ設定します。使用するのはいつものIDirect3DDevice3::SetTexture。例えばステージ0に煉瓦、ステージ1にライトマップをセットするなら、

// ptexRengaとptexLightmapは型がLPDIRECT3DTEXTURE2で、他で初期化されているものとする
SetTexture( 0, ptexRenga );
SetTexture( 1, ptexLightmap );

と記述します。いつも0にしていた所にステージ番号を入れるだけです。次にテクスチャステージの設定。こっちはIDirect3DDevice3::SetTextureStageStateを使います。前述の例をもう一度書きます。

// ステージ0の設定
pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); // SetTextureで設定した煉瓦のテクスチャが使われる
pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE );

// ステージ1の設定
pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE ); // SetTextureで設定したライトマップのテクスチャが使われる
pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT );
pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE );

 今見ると何をやってるかわかりますね。一緒に書いてあった画像付きの図も今見ればわかると思います、ステージ0とかの意味も。
α値もこれと同様に設定します。

// ステージ0の設定
pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );
pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1 );

// ステージ1の設定
pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAOP, D3DTOP_CURRENT );
pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1 );

 この記述では、ステージ0のテクスチャのα値を使ってます。ちょっと手抜きしてポリゴンのα値を無視してます。ステージ1のテクスチャのα値も無視してますね。ステージ0のα値をそのまま使ってます。

 次に……あ、これで終わりですね。後はDrawPrimitiveなどでポリゴンを描画するだけで、自動的にテクスチャが合成されます。いつもならここでSetRenderStateでマルチテクスチャを有効にさせるみたいな処理をするところですが、Direct3Dのマルチプルテクスチャはテクスチャステージの設定だけでマルチプルテクスチャブレンディングを行います。そのため、マルチテクスチャを使わない時は使用しないステージのD3DTSS_COLOROPD3DTSS_ALPHAOPD3DTOP_DISABLEに設定する必要があります。しないと勝手に合成します。

 ついでに。テクスチャステージの設定はマクロでやると簡潔になります。DirectX6のHTMLヘルプにこんなマクロが書いてあります。

#define SetTextureColorStage( dev, i, arg1, arg2 ) \
dev->SetTextureStageState( i, D3DTSS_COLOROP, op); \
dev->SetTextureStageState( i, D3DTSS_COLORARG1, arg1 ); \
dev->SetTextureStageState( i, D3DTSS_COLORARG2, arg2 );

 

 あー、長かった。やってることと言えば、構造体宣言してテクスチャステージ設定してDrawPrimitive使ってるだけだというのに、いろいろと説明したらこんなに長くなってしまった。
……あ、マルチパステクスチャの説明がまだだ。でももう見てる人もうんざりしてるだろうし。でもマルチプルテクスチャ使えなかったらマルチパステクスチャ使うしか無いし。……やっぱりやるか。どうせ簡単だし。

 

・マルチパステクスチャ

 ハードがマルチプルテクスチャに対応してない(VoodooBansheeもDirect3Dで対応してないそうです。TAOSさんありがとうございました)、そういう場合はHALで動かすならマルチパステクスチャを使わなければいけません。α合成に対応してないとそれもできませんが。

 マルチプルテクスチャは設定さえすれば自動的に合成してくれますが、マルチパステクスチャは一切を自分でやる必要があります。と言っても、やる事の多さはマルチプルテクスチャと似たようなものですが。

 普通に描画するのと違うのは、合成する分だけ繰り返し重ねて描画する所です。マルチプルテクスチャでの例を、マルチパステクスチャの要領でやってみましょう。

 まずSetTexture。これはいつもどおりです。

// ptexRengaとptexLightmapは型がLPDIRECT3DTEXTURE2で、他で初期化されているものとする
SetTexture( 0, ptexRenga );

 そして、テクスチャ合成の方。マルチプルテクスチャではテクスチャステージを設定しますが、マルチパステクスチャではステージ0の代用に普通のテクスチャ合成、それ以降のステージの代用にα合成を使います。
まずはステージ0の真似から。テクスチャ合成を設定したら1回目の描画。

pd3dDevice->SetRenderState( D3DRENDERSTATE_TEXTUREMAPBLEND, D3DTBLEND_MODULATE );
pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_LVERTEX, verVertices, 4, NULL );

 次にステージ1ですが、DrawPrimitive描画するポリゴンは1回目の描画と全く同じです。ポリゴンの位置も大きさも。違うのは、テクスチャ合成は使わずα合成を使うことです。
例ではD3DTOP_MODULATEを使っていました。α合成で代用するには、既に書かれたポリゴン(Dest)とこれから書くポリゴン(Src)を乗算合成みたいなこと(計算式はDest * Src)をしないといけない事になります。乗算合成の混合要素……「ブレンドファクター121」でやりましたね。SrcにDESTCOLOR、DestにZEROです(Src * Dest + Dest * 0)。

SetTexture( 0, ptexLightmap );
pd3dDevice->SetRenderState( D3DRENDERSTATE_TEXTUREMAPBLEND, D3DTBLEND_DECAL ); // ポリゴンやStateの設定次第では無しでも可
pd3dDevice->SetRenderState( D3DRENDERSTATE_ALPHABLENDENABLE, TRUE );
pd3dDevice->SetRenderState( D3DRENDERSTATE_SRCBLEND, D3DBLEND_DESTCOLOR );
pd3dDevice->SetRenderState( D3DRENDERSTATE_DESTBLEND, D3DBLEND_ZERO );
pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_LVERTEX, verVertices, 4, NULL ); // 1回目と同じ

 これでマルチパスによるテクスチャ合成ができます。やってることは簡単ですよね。

 マルチプルテクスチャにできて、マルチパステクスチャにはできないことはありますし、またはその逆もあります。あとハードやドライバが2枚までのマルチプルテクスチャに対応してないが4枚合成したい、という時はマルチプルテクスチャを使った後に重ねて書くという、マルチプルテクスチャとマルチパスの併用(マルチパスマルチプルテクスチャ?)をする必要があります。これもなかなか面白そうですけど。

 マルチパステクスチャにできないことで、一番困るのがステージ1以降にα値を使ったテクスチャ合成(D3DTSS_ALPHAOPとか)を使った場合です。α合成ではDestのα値が使えないため、マルチプルテクスチャではできるα値のテクスチャ合成もマルチパスではできないです。変わった混合要素を使ったりαテストを変わった事に使ったりしてるとこの問題が厄介になってきます。そんな、ステージ1以降にα値を使ったテクスチャ合成をするという変わった使い方も用途が限られますが。

 それと、ステージ0に半透明などα合成を使った場合も、マルチプルテクスチャでは大丈夫でもマルチパスでは問題が出たりします。Zバッファは半透明とか描画順に依存する物までは面倒見てくれませんから。

 

 あぁ、説明だけで終わってしまった。これではマルチテクスチャの重要性がわかってもらえない。これからのリアルへの鍵はマルチテクスチャが握っていると言っても過言ではないと思います、ホント。ゼルダの伝説(N64)なんかでは使われまくってます(こっちはマルチパスだと思いますが)。
ライトマップは使い方のほんの一例に過ぎません。リアルを目指すなら使い方を研究してみてください。

 

written by Y.Ohde  e-mail : oode@alles.or.jp

back