■メモ書き Top

無料レンタルサーバー

●1.MultiRenderTargetSurface(マルチレンダーターゲットサーフェイス)

シェーダーを使用する場合、何かとお世話になるマルチレンダーターゲットサーフェイス。実はいろいろ注意する点があり、間違った使い方をすると 正しくレンダリングできなくなります。Microsoft DirectX 9.0 Update (October 2004) 日本語ドキュメントによると

(1) 一部の実装では、複数レンダー ターゲットのすべてのサーフェイスが、フォーマットは異なっても、ビット深度は同じでなければならない場合があります。
(2) 複数レンダー ターゲットのすべてのサーフェイスは、幅と高さが同じでなければなりません。
(3) 一部の実装では、Z テストとステンシル テスト以外に、ピクセル シェーダ後の処理を実行できません。つまり、ディザリング、アルファ テスト、フォグ、ブレンド、ラスタ処理、マスキングを実行できません。

(1)がちょっと曲者です。ここでウィンドウモードで実行し、かつ2枚のマルチレンダーターゲットサーフェイスを使用するとします。 インデックス0番目にバックバッファのサーフェイスをそのまま使用する場合、バックバッファのフォーマットは Windowsの画面のプロパティで設定しているフォーマットを使用するため、実行環境によってビット深度が変わります(16Bit Or 32Bit)。 したがって環境によってはレンダーターゲットサーフェイス同士のビット深度が一致しなくなることがあります。 ウィンドウモードで実行することを考慮しない場合は問題ないですが、ウィンドウモードで実行する場合は注意する必要があります。

(2)は書いてあるとおりです。

(3)も注意が必要です。完全に調べきってはいないですが、どうもマルチレンダーターゲットサーフェイスを使用している場合 固定機能パイプラインのアルファブレンディングを使用すると正しくレンダリングできないようです。ただし一部の実装では とあるので実行できる環境もあると思われます。


●2.プリミティブのレンダリング

プリミティブのレンダリングで使用するメソッドにはIDirect3DDevice9::DrawPrimitive と IDirect3DDevice9::DrawPrimitiveUPがあります。
(1) IDirect3DDevice9::DrawPrimitive
VRAMに確保した頂点情報をもとにレンダリングします。頂点情報の参照はIDirect3DDevice9::SetStreamSource()を使用します。
(2) IDirect3DDevice9::DrawPrimitiveUp
システムメモリ上に確保した頂点情報を元にレンダリングします。

一般的にVRAM上に確保した頂点情報を参照した方が高速に処理されるといわれています。 しかし、VRAM上に確保した頂点情報を更新する場合ロックする必要があります。 このロック処理処理が実は負荷が高いです。ですので頻繁にロック処理を行う場合、逆に負荷が高くなるという場合もあります。


●3.ステートブロック

ステートブロックを使用すると、レンダー ステート、頂点ステート、ピクセル ステートの一括変更ができます。 まあそれだけなんですが、アルファブレンドの合成方法の変更などのように一度に複数の設定を変更する場合など重宝します。

・初期化のサンプル

//Direct3Dデバイス インターフェースの宣言
LPDIRECT3DDEVICE9 m_pD3DDevice9;

//ステートブロックインターフェースの宣言
LPDIRECT3DSTATEBLOCK9 m_pAlphaAlignment;

//ステートブロックの記録開始
m_pD3DDevice9->BeginStateBlock();

//線形合成の設定
m_pD3DDevice9->SetRenderState( D3DRS_BLENDOP,   D3DBLENDOP_ADD );
m_pD3DDevice9->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA );
m_pD3DDevice9->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );

//ステートブロックの記録終了
m_pD3DDevice9->EndStateBlock( &m_pAlphaAliginment );

・使用時のサンプル

//線形合成を設定する
m_pAlphaAlignment->Apply();
開放は例によってIDirect3DStateBlock9::Releaseメソッドで。

IDirect3DStateBlock9インターフェースは、デバイスロストが発生した場合リストアする必要があるので注意してください。


●4.HLSLのコンパイル

当サイトでは、fxファイル(シェーダーのソースコードを記述しているテキストファイル)をロードし、D3DXCreateEffectFromFile関数内でコンパイルし、実行する方法を紹介しています。 この方法の場合、各シェーダークラスの初期化時にコンパイルすることになるのでその分無駄な処理が発生します。 また配布時にテキストファイルであるfxファイルも一緒に配布するので改ざんされる可能性もありよろしくありません。

これに対処する方法は2通りあります。

(1) バイナリデータをオブジェクトファイルに出力する
実行時にコンパイルする必要はありませんが、オブジェクトファイルを一緒に配布する必要があります。

(2) バイナリデータをヘッダファイルに出力する
実行時にコンパイルする必要はなく、配布するファイルもありません。ヘッダファイルなのでプロジェクトにインクルードしてコンパイルし、実行ファイルに組み込みます。

次にコンパイルするまでの手順を解説します。

・fxファイルをコンパイルするための準備

1.まずfxファイルをコンパイルする環境を作成します。 といってもfxc.exeファイルをシステムフォルダ(通常はC:\Windows\System32)内にコピーするだけです。 fxc.exeファイルは、DirectXをインストールしたフォルダのUtilities\Bin\x86フォルダ内にあります(DirectX 9.0 SDK December 2004の場合)。

・fxファイルのコンパイル

1.コマンドプロンプトを起動します。

2.CDコマンドでfxファイルをおいているディレクトリに移動します。fxファイル名はここではSample.fxとします。

3.fxcコマンドを入力し、fxファイルをコンパイルします。上記の環境の場合コマンドは次のようになります。
fxc /T fx_2_0 /Fo Sample.fxo Sample.fx

/T オプションは、シェーダーのバージョンを指定します。 DirectX 9.0 SDK December 2004に添付されているコンパイラ(fxc.exe)でコンパイルした場合fx_2_0しか使用できません。 ですがSM3.0で記述されたfxファイルを使用しても問題なく動作します。イマイチ意味不明だが、fx_2_0としとけばいいと思われます。
/Fo オプションを指定するとオブジェクトファイルを作成します。パラメータは2つで左からコンパイルして作成されたファイル名、コンパイルするfxファイル名となります。 /Fo オプションを /Fhと変更するとヘッダファイルを出力します。この場合出力するファイル名の拡張子をfxoからhに変更します。

次にオブジェクトファイルを使用するための手順を解説します。

//Direct3Dデバイス インターフェースの宣言
LPDIRECT3DDEVICE9 m_pD3DDevice9;

//エフェクトの設定や問い合わせなどに使用するインターフェースの宣言
LPD3DXEFFECT m_pEffect;

//コンパイルエラーの一覧が格納されたバッファ
LPD3DXBUFFER pErr = NULL;

D3DXCreateEffectFromFile( m_pd3dDevice, _T("Sample.fxo"), NULL, NULL, D3DXSHADER_SKIPVALIDATION, NULL, &m_pEffect, &pErr );
これはオブジェクトファイルを使用した場合のサンプルです。第5引数はコンパイル オプションです。あらかじめコンパイルしてエラーがないことを確認したファイルを使用するので、D3DXSHADER_SKIPVALIDATIONを指定します。

//Direct3Dデバイス インターフェースの宣言
LPDIRECT3DDEVICE9 m_pD3DDevice9;

//エフェクトの設定や問い合わせなどに使用するインターフェースの宣言
LPD3DXEFFECT m_pEffect;

//コンパイルエラーの一覧が格納されたバッファ
LPD3DXBUFFER pErr = NULL;

D3DXCreateEffect( m_pd3dDevice, g_Sample, sizeof( g_Sample ), NULL, NULL, D3DXSHADER_SKIPVALIDATION, NULL, &m_pEffect, &pErr );
次にヘッダファイルを使用した場合のサンプルです。第2引数にヘッダファイルにあるDWORD配列のグローバル変数名を指定し、第3引数に配列のサイズを指定します。


●5.ピクセルとテクセル

ピクセルとテクセルについて説明します。

ピクセルは、シーンを描画するレンダーターゲットサーフェイス上の単位となります。
テクセルは、テクスチャー上の単位となります。

ブラーフィルターなどのポストエフェクトを適応する場合、レンダーターゲットサーフェイスと ウィンドウを覆う2Dオブジェクトを使用してレンダリングしますが レンダーターゲットサーフェイスおよび2Dオブジェクトの大きさがウィンドウの大きさと同じであっても、 テクスチャー上の座標として使用されるテクセルと描画先のピクセルの座標は完全に一致しません。

図1
図1:この図はピクセルのイメージです。左上を注目してください。左上端の座標が(-0.5,-0.5)となっています。 これはピクセルの座標はセルの中心位置となるからです。

図2
図2:この図では(0.0,0.0)〜(4.0,4.0)間の矩形領域のピクセルを青く塗りつぶすことを考えています。 矩形の輪郭部分のセルが白と青が混在した中途半端な状態で塗りつぶされています。しかしそれぞれのピクセルにはひとつの色しか描画できないため この図のようにレンダリングすることはできません。

図3
図3:この図ではラスター化を行っています。ラスタ化とはどのピクセルを描画するかをハードウェアが決定する処理です。 表示される内容はあっていますが、表示位置が x および y 方向に -0.5 セルずれています。

図4
図4:次に図のような 4 x 4 のサイズのテクスチャーを左上にレンダリングすることを考えます。

図5
図5:テクスチャー座標のサンプリング位置の図です。黒いドットがピクセル位置です。左上のほうに隙間が開いているのがわかると思います。 このためポストエフェクト処理を行ったときのイメージは行わない場合に比べてほんの少し右下にずれます。 これはピクセルの場合は左上のセルの中心を原点としますが、テクセルは左上のグリットを原点とするためです。

図6
図6:この問題を回避するためにテクセルのサンプリング位置を x および y 方向に +0.5 セルずらします。

この処理を当サイトではD3D2DSQUAREクラス内で行っています。D3D2DSQUAREクラスの全ソースは表面化散乱(Subsurface Scattering)にあるのでそちらを参照してください。 ここでは必要な部分のみ紹介します。

---UPrimitive.cpp---


HRESULT D3D2DSQUARE::Resize( UINT Width, UINT Height )
{
   HRESULT hr = -1;

   m_Width = Width;
   m_Height = Height;

   D3D2DVERTEX* vtx;
   hr = m_pd3d2DVertex->Lock( 0, 0, (void**)&vtx, 0 );
   if( FAILED( hr ) )
      return -3;

   vtx[0].x = 0.0f;           vtx[0].y = 0.0f;            vtx[0].z = 0.0f; vtx[0].rhw = 1.0f;
   vtx[1].x = (float)m_Width; vtx[1].y = 0.0f;            vtx[1].z = 0.0f; vtx[1].rhw = 1.0f;
   vtx[2].x = 0.0f;           vtx[2].y = (float)m_Height; vtx[2].z = 0.0f; vtx[2].rhw = 1.0f;
   vtx[3].x = (float)m_Width; vtx[3].y = (float)m_Height; vtx[3].z = 0.0f; vtx[3].rhw = 1.0f;

   vtx[0].tu = 0.0f + 0.5f / (float)m_Width; vtx[0].tv = 0.0f + 0.5f / (float)m_Height;        <--ここ
   vtx[1].tu = 1.0f + 0.5f / (float)m_Width; vtx[1].tv = 0.0f + 0.5f / (float)m_Height;        <--ここ
   vtx[2].tu = 0.0f + 0.5f / (float)m_Width; vtx[2].tv = 1.0f + 0.5f / (float)m_Height;        <--ここ
   vtx[3].tu = 1.0f + 0.5f / (float)m_Width; vtx[3].tv = 1.0f + 0.5f / (float)m_Height;        <--ここ

   vtx[0].color = 0xFFFFFFFF;
   vtx[1].color = 0xFFFFFFFF;
   vtx[2].color = 0xFFFFFFFF;
   vtx[3].color = 0xFFFFFFFF;

   m_pd3d2DVertex->Unlock();

   hr = S_OK;

   return hr;
}

tuおよびtvはテクセルの座標です。D3DPT_TRIANGLESTRIPを使用してレンダリングするため Z の字を描く感じでテクセルを設定していきますが、 tu、tvそれぞれピクセルの半分のサイズ分だけずらしています。 m_Width と m_Height 変数はレンダーターゲットサーフェイスのサイズです。 表面化散乱(Subsurface Scattering)ではこのあたりについてろくに説明してなかったんですがこういう理由のためなのです。

参考サイト:テクセルからピクセルへの直接的なマッピング (Direct3D 9)
画像は上記のサイトのものを拝借してます。


●6.シェーダー内でZ値計算

被写界深度や、デプスバッファシャドーのようにZ値をレンダーターゲットサーフェイスに出力し、 その値を参照し何らかの処理を行うことがあります。 Z値の計算には注意すべきことがあります。 この辺の説明をするには同次座標系について説明しなければならんのですが、 はっきりいって自分には無理!!
ってなわけで詳しく説明されているサイトを紹介しますので詳細はそちらを参照してください。

○×(まるぺけ)つくろーどっとコム
IKD氏のサイトです。情報量も多いですし、見やすく丁寧に解説されています。こことは大違い。

計算式を使用しないでものすごく大雑把に説明するとこんな感じです。

頂点をワールド×ビュー×遠近射影行列変換した結果は遠近射影行列の作成時に使用するD3DXMatrixPerspectiveFovLH関数の第4引数の最近距離(zn)が 0.0f に、 第5引数の最遠距離(zf)がそのままzfの値になるように線形的に変換されるようです。
しかしレンダーターゲットサーフェイスに出力するときは ( 0.0f 〜 1.0f ) の範囲内に収める必要があるため このまま出力することはできません。zfで割り算してもいいのですが、w成分で割った方が、手前のZ値の解像度が高くなるため 深度テストによる精度が高くなるという利点があるそうです。

そんなわけで当サイトのサンプルでもZ値をW値で割ってるんですが、ちょっと待て。陰面処理を行う場合はそれでいいですが、シェーダー内でZ値を参照する場合は、zfで割った方がいいのでは。 最初、遠近射影行列を使用してソフトパーティクルを実装したとき、 カメラがパーティクルから離れるとパーティクルの消える範囲が大きくなるという困った現象が発生したんです。 Z値が線形になってないんだろうなとは思ったので、サンプルではZ値の出力用としてワールド×ビュー×正射影行列も渡してそっちで行列変換した座標の Z値 / W値 をレンダーターゲットサーフェイスに出力するようにしてましたが、 zfで割ったら線形になるのであれば、そうした方が負荷が小さくなるしいいよな。

そんなわけでサンプル修正しました。参照したことある方には申し訳ないですが。つーかシェーダーよりここ更新した方が自分的には勉強になるな。


●7.座標系について

3Dで物体を表示するにはいくつかの座標系を経由します。 各座標系は次の順番で変換していきます。

ローカル座標系 → ワールド座標系 → ビュー座標系 → 射影座標系 → スクリーン座標系

(1)ローカル座標系

モデリングツールでモデルを作成するときの座標系です。要するにモデル基準の座標系です。この座標系でモデルの状態を表現するには、 増加分の行列を次々積算していきます。

例えば、モデルがZ+方向に毎フレーム1づつ移動している場合

D3DXMATRIX matTranslation;

D3DXMatrixTranslation( &matTranslation, 0.0f, 0.0f, 1.0f );
matWorld *= matTranslation;
IDirect3DDevice9::SetTransform( D3DTS_WORLD, &matWorld );
matWorldは毎フレーム初期化せず、値を保持しています。この値に相対距離をセットしたmatTranslationを積算します。 モデルが3軸回転する場合、この座標系を使用すると楽だったりします。

(2)ワールド座標系

これは基準とする座標系上でモデルの状態を表現するための座標系です。

例えば、モデルがZ+方向に毎フレーム1づつ移動している場合

D3DXMATRIX matWorld, matTranslation;

D3DXMatrixTranslation( &matTranslation, 0.0f, 0.0f, z );
matWorld = matTranslation;
IDirect3DDevice9::SetTransform( D3DTS_WORLD, &matWorld );
D3DXMatrixTranslation関数の第4引数はワールド座標系上でのモデルのZ値です。 この値は毎フレーム1づつ加算されていきます。

(3)ビュー座標系

これはカメラを基準とした座標系です。 ワールド座標系ではモデルごとに行列を設定するのに対し、ビュー座標系はシーン内のすべてのモデルに一様に適応する 行列です。

(4)射影座標系

ここでは遠近射影座標系について説明します。遠近射影座標系は、遠近法に基づきパースをつける座標系です。 視錐台と呼ばれる3次元空間内で実際に画面上に表示される領域(視認できる領域)を立方体に座標変換します。 視錐台は図7のような形をしているため、カメラに近いほど見える領域が小さくなり、遠くなるほど大きくなります。

図7

図7の中の前方クリップ面と後方クリップ面は、視界の手前と奥の視認できる面です。 現実には後方クリップ面より遠くにあるモデルが見えないなどということはありえないのですが、 コンピュータの世界では無限に遠くにある距離を扱うことはできないので制限を設けています。 ただこれにより突然見えなくなる領域が発生して不自然になるため、後方クリップ面付近に距離フォグをかましてごまかす というテクニックが一昔使われていました。最近では遠方まで表現しているゲームがほとんどです。 実際どうやっているかは不明ですが、LODシステムを使用して最適化を行ってるのでしょうか?

話を戻して、視錐台を立方体に変換しますが、このとき立方体の大きさが図8のようになります。

図8

図8のように[ x, y ]の範囲をそれぞれ [ -1.0f 〜 1.0f ] の範囲に、[ z ]の範囲を [ 0.0f 〜 1.0f ] に変換します。

(5)スクリーン座標系

最後にスクリーン上に表示するための座標系に変換します。

 → 図9

射影座標系は上で説明したとおりの範囲となりますが、スクリーン座標系では [ x, y ] の範囲が [ 0, 0 ] 〜 [ ディスプレイの解像度の横幅, ディスプレイの解像度の縦幅 ] となります。 図9はディスプレイの解像度が [ x, y ] = [ 1024, 768 ] のときの図です。 変換は簡単で、原点の位置が左上になっていることと、Y軸が反対になっていることを考慮して行列を作成するだけです。

ディスプレイの解像度の横幅を W, 縦幅を H とするとスクリーン座標系へ変換する行列は

D3DXMATRIX matScreen, matScaling, matTranslation;

D3DXMatrixScaling( &matScaling, W * 0.5f, -H * 0.5f, 1.0f );
D3DXMatrixTranslation( &matTranslation, W * 0.5f, H * 0.5f, 0.0f );
matScreen = matScaling * matTranslation;
となります。matScreenがスクリーン座標系に変換するための行列です。

なんか途中、脱線しましたが、以上です。数式を交えて勉強したい方は例によって他のサイトを参照してください。


●8.tex2Dproj

tex2Dprojはシェーダーの組み込み関数です。射影テクスチャーの参照に使用するとドキュメントにはありますが、ようするに 4次元の頂点を射影座標系に行列変換してその位置にあるテクセルをサンプリングする関数です。 当サイトでは、デプスバッファシャドーなどで使用していますが、Z値が出力されたテクスチャーを サンプリングする場合に使用するので、利用頻度が非常に高い、重要な関数です。

さて4次元の頂点を行列変換するため、行列を引数として関数に渡す必要があります。 この行列についての説明は「7.座標系について」でほとんど終了です。まずそちらを参照してください。

...............

OKかな?では参照したつもりで追加説明です。

行列の作成は「7.座標系について」でやっていることとほとんど同じで、 最後の(5)スクリーン座標系がテクスチャー座標系に変わるくらいです。 テクスチャー座標系は図10のようになります。

図10

左上が原点となっていて、Y軸がひっくり返っているところが同じです。しかし縦横の幅が 1.0 になっているところが異なります。 テクスチャー座標系へ変換する行列は次のようになります。

D3DXMATRIX matTexture, matScaling, matTranslation;

D3DXMatrixScaling( &matScaling, 0.5f, -0.5f, 1.0f );
D3DXMatrixTranslation( &matTranslation, 0.5f + 0.5f / W, 0.5f + 0.5f / H, 0.0f );
matTexture = matScaling * matTranslation;
ここで W はテクスチャーの横幅、 H は縦幅となります。1ピクセルの半分の大きさを加算する理由については、「5.ピクセルとテクセル」を参照してください。 ただこの加算は、やらなくてもちょっと誤差が出る程度なので基本問題ないです。 しかし、サンプラーステートのD3DSAMP_MINFILTERとD3DSAMP_MAGFILTERをD3DTEXF_POINTでなくD3DTEXF_LINEARにしているとレンダーターゲットサーフェイスとテクスチャーのサイズが同じ場合でもブラーがかかってしまうため問題となります。 安全のためにも加算しておく方がいいかも。というわけで一部のサンプルソース修正しました。と思って修正したけど戻しました。
とりあえずで修正したけど、正直設計がよくないです。1つのシェーダー内でtex2Dprojを使用してサイズの異なる複数のテクスチャーをサンプリングする場合とか考えると正直めんどくさい。 その割りに修正してもしなくてもほとんど違いないし。 そんなわけで当サイトではこれまでどおり1ピクセルの半分の長さを加算しないことにしますが、サンプラーステートの設定は必ず正しく行ってください。
まあこの辺は、サンプラーステートの設定を固定機能パイプラインを使用して設定しているのが問題なんですけどね。シェーダー内でも設定できるのでそうした方が設定し忘れとかなくなると思います。やっぱりシェーダー内で設定した方がいいのかな。


●9.今更ランバート拡散照明

ほんと今更だ(笑)。まあランバート拡散照明に関連はしますが、メインは平行光源をローカル座標系に変換する際の注意点です。

さてその辺の説明をする前に前置きとして簡単にランバート拡散照明について説明します。 ランバート拡散照明によるライティング計算で使用するパラメータは、モデルが持つ法線ベクトルと太陽光(平行光源)です。この2つのベクトルの内積の結果を元に ライティング処理を行います。 当サイトのランバート拡散照明では、計算をローカル座標系で処理しています。 ローカル座標系で処理するとは、モデルは動かさない、つまりモデルの法線ベクトルは行列変換せず、その分平行光源の方向ベクトルを行列変換させることです。 平行光源の変換で使用する行列は、ワールド座標系の行列の逆行列です。逆行列の性質は、任意の行列AとAの逆行列の積が単位行列になる、です。

A * A-1 = A-1 * A = E ( A:任意の行列 A-1:逆行列 E:単位行列 )

要するにA行列が右に移動したらその逆行列は左に移動、A行列が右に回転したらその逆行列は左に回転するといった感じになります。(この説明はあくまで概念です)

ではなぜ一般的なワールド座標系を使用せず、ローカル座標系を使用しているかというと最適化のためです。 ワールド座標系の場合、頂点シェーダー内で法線ベクトルの行列変換が必要となります。しかしローカル座標系ではその必要はありません。 平行光源の方向ベクトルはどちらの場合でも頂点シェーダー内では行列変換しません。 ただしローカル座標系の場合、ランバート拡散照明クラス内で平行光源の方向ベクトルにワールド座標系の行列の逆行列をかける計算をしますが、頂点ごとの処理数とモデルごとの処理数とでは モデルごとの処理数の方が圧倒的に少ないのでローカル座標系の方が負荷が少ないといった理屈です。

次に法線ベクトルや平行光源の性質を説明します。これらのベクトルは単位ベクトルつまり長さが1のベクトルである必要があります。でないと内積の計算が正しく行えません。 また行列変換を行う場合、平行移動を行わないようにします。そもそもベクトルとは方向と長さの性質を持ちます。ここで平行移動を行うと方向が変わってしまいます。(図11)

図11

ではメインの話、平行光源をローカル座標系で変換する際の注意点です。

void LAMBERT1::SetMatrix( D3DXMATRIX* pMatWorld, D3DXVECTOR4* pLightDir )
{
   if( m_pEffect )
   {
      D3DXMATRIX m;
      D3DXVECTOR4 v;

      //ワールド × ビュー × 射影
      m = (*pMatWorld) * m_matView * m_matProj;
      m_pEffect->SetMatrix( m_pWVP, &m );

      //平行光源の方向ベクトルを設定する
      D3DXMatrixInverse( &m, NULL, pMatWorld );
      D3DXVec4Transform( &v, pLightDir, &m );
      //正規化する
      D3DXVec3Normalize( (D3DXVECTOR3*)&v, (D3DXVECTOR3*)&v );
      m_pEffect->SetVector( m_pLightDir, &v );
   }

   //シェーダーが使用できないときは、固定機能パイプラインのマトリックスを設定する
   else
      m_pd3dDevice->SetTransform( D3DTS_WORLD, pMatWorld );
}

これはランバート拡散照明をシェーダーで処理するLAMBERT1クラスの行列変換関連のメンバ関数です。LAMBERT1クラスの全体像はランバート拡散照明を参照してください。 平行光源の方向ベクトルの変換を順を追って説明します。
D3DXMatrixInverse()関数で逆行列を取得します。ここではワールド座標系の行列の逆行列を取得しています。
次に取得した逆行列を使用して平行光源の方向ベクトルを行列変換しています。
次に正規化を行っています。正規化を行うのはワールド座標系の行列でスケーリングを行っている場合、平行光源の方向ベクトルにもスケーリングがかかるため、単位ベクトルに変換する必要があるからです。

さてワールド座標系の行列の逆行列を使用して変換するため、当然平行移動が行われることがあります。 このソースだけ見ると、平行移動を無効にする処理が含まれていないように見えます。しかし引数のpLightDirのw成分が0.0fとなっているため平行移動が無効になります。 当サイトのサンプルを参照するときは平行光源の方向ベクトルのw成分は必ず 0.0f にするようにしてください。

まあ、安全性を考えると次のように修正した方がいいとは思うけどね。たいした負荷にもならないし。

void LAMBERT1::SetMatrix( D3DXMATRIX* pMatWorld, D3DXVECTOR4* pLightDir )
{
   if( m_pEffect )
   {
      D3DXMATRIX m;
      D3DXVECTOR4 v;

      //ワールド × ビュー × 射影
      m = (*pMatWorld) * m_matView * m_matProj;
      m_pEffect->SetMatrix( m_pWVP, &m );

      //平行光源の方向ベクトルを設定する
      D3DXMatrixInverse( &m, NULL, pMatWorld );
      m._41 = 0.0f;    // -> 追加
      m._42 = 0.0f;    // -> 追加
      m._43 = 0.0f;    // -> 追加
      D3DXVec4Transform( &v, pLightDir, &m );
      //正規化する
      D3DXVec3Normalize( (D3DXVECTOR3*)&v, (D3DXVECTOR3*)&v );
      m_pEffect->SetVector( m_pLightDir, &v );
   }

   //シェーダーが使用できないときは、固定機能パイプラインのマトリックスを設定する
   else
      m_pd3dDevice->SetTransform( D3DTS_WORLD, pMatWorld );
}

参考書籍:DirectX 9 シェーダプログラミングブック


●10.スクリーン・アスペクト比

アスペクト比は、2次元形状の物の長辺と短辺の比率のことです。ここではスクリーン・アスペクト比について説明します。 一昔前のパソコンの画面解像度は横と縦が[640×480]や[1024×768]など 4:3 の比率が一般的でした。 最近ではパソコンでテレビやDVDなどを視聴することが増えてきたなどの理由で、[1680×1050]などの横長のディスプレイが多くなってきました。 [1680×1050]の解像度の場合スクリーン・アスペクト比は 16:10 となります。

さて、ディスプレイの種類によってスクリーン・アスペクト比が変わるということは、ゲーム作成の場合は結構困ったことになります。 スクリーン・アスペクト比が16:10のような横長のディスプレイで3Dゲームを実行すると、横方向の視野が広くなります。 そのためゲームの種類によっては、これが原因でゲームバランスを崩しかねないという問題が発生します。 まあ横長になっていい場合もあると思いますが。

次にサンプルイメージを紹介しながら簡単に説明していきます。なお、スクリーン・アスペクト比が16:10の環境でフルスクリーンモードで 実行することを条件とします。フルスクリーンモードを条件としているのはウィンドウモードの場合このような問題は発生しないからです。

図12

図12は次の条件となります。
・遠近射影行列のアスペクト比を4:3
・ビューポートの開始位置をディスプレイの左上、大きさをディスプレイの解像度と同じ

全体的に横長になっています。スクリーン・アスペクト比を特に意識しないでコーディングした場合こんな感じになります。筆者が作成した自作ゲームのGrowWingもこうなります。 これは駄目パターンです。

図13

図13は次の条件となります。
・遠近射影行列のアスペクト比を16:10
・ビューポートの開始位置をディスプレイの左上、大きさをディスプレイの解像度と同じ

ピクセルのアスペクト比が正しく1:1になっています。ただし横方向に視野が広くなっています。FPSゲームなんかはこれでいいのではないでしょうか。

図14

図14は次の条件となります。
・遠近射影行列のアスペクト比を4:3
・ビューポートをディスプレイの中心位置に配置し、大きさをアスペクト比が4:3となるような任意のサイズ

ピクセルのアスペクト比が正しく1:1になっています。この設定の強みはスクリーン・アスペクト比にかかわらず、 ゲーム画面を表示する領域のアスペクト比を一定にできることです。 横スクロールシューティングゲームなどの場合これがいいと思います。

   //Direct3Dデバイス
   LPDIRECT3DDEVICE9 m_pd3dDevice;
   
   //Direct3Dの初期化は終わってるものとして...
   
   //ディスプレイの解像度
   //アスペクト比は16:10
   UINT nWidth  = 1680;
   UINT nHeight = 1050;

   //ゲームをレンダリングする領域の解像度
   //アスペクト比は4:3
   UINT nW      = 1024;
   UINT nH      = 768;

   D3DVIEWPORT9 vp;

   //スクリーン全体を真っ黒に初期化する
   vp.X = 0;
   vp.Y = 0;
   vp.Width  = nWidth;
   vp.Height = nHeight;
   vp.MinZ = 0.0f;
   vp.MaxZ = 1.0f;
   m_pd3dDevice->SetViewport( &vp );
   m_pd3dDevice->Clear( 0L,
                        NULL,
                        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
                        0x0,
                        1.0f,
                        0L
                      );

   //実際にレンダリングする領域を設定する
   vp.X = ( nWidth  - nW ) / 2;
   vp.Y = ( nHeight - nH ) / 2;
   vp.Width  = nW;
   vp.Height = nH;
   vp.MinZ = 0.0f;
   vp.MaxZ = 1.0f;
   m_pd3dDevice->SetViewport( &vp );

   D3DXMATRIX matPProj;

   //遠近射影行列を作成する
   D3DXMatrixPerspectiveFovLH( &matPProj,
                               D3DX_PI/4.0f,
                               (float)nW / (float)nH,    // -> ここでアスペクト比を設定
                               30.0f,
                               700.0f );
   m_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matPProj );

図14のパターンのサンプルソースです。

結局どこに注意すればいいのかというと、遠近射影行列のアスペクト比とビューポートのアスペクト比は必ず同じになるようにするということです。


●11.アルファ ブレンディングと陰面処理

陰面処理とは視点からあるモデルの陰になっていて見えない部分を描画しないようにする処理のことです。 これを使用するためモデルの描画順序にかかわらず前後関係が正しく処理されます。 Direct3DではZバッファを使用するZバッファ法を採用しています。 これはZバッファのZ値(奥行き)とモデルのZ値を比較し、モデルのZ値の方が小さい(手前にある)ときは モデルを描画し、モデルのZ値でZバッファを更新し、 モデルのZ値の方が大きい(奥にある)ときはモデルを描画せず、Zバッファを更新しない、といった感じです。

さてこのZバッファ法ですが、不透明モデルを描画するときはなんら問題はないですが、半透明モデルを描画するときは 困った問題が発生します。

パターン1

図15

トラが燃えています(笑)。シチュエーションはどうでもいいか。まあトラの周辺にパーティクルを複数散らしているところです。 このパーティクルのスプライト(矩形)がおかしな具合に切り取られている部分があります。 これは手前にあるパーティクルでZバッファを更新し、そのZバッファを使用して奥にあるパーティクルを切り取るためです。 これに対応するには半透明モデルを描画するときはZバッファへの書き込みをしないようにします。


   //**********************************************************
   //STEP1:まず不透明モデルをすべて描画する
   //**********************************************************
   
   //アルファ ブレンドを無効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
   
   //ライティングする。
   m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE );
   
   //Zバッファへの書き込みを有効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );

   //この設定ですべての不透明モデルを描画していく…

   //**********************************************************
   //STEP2:次に半透明モデルをすべて描画する
   //**********************************************************

   //アルファ ブレンドを有効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );

   //アルファ ブレンドを加算合成する。
   m_pd3dDevice->SetRenderState( D3DRS_BLENDOP,   D3DBLENDOP_ADD );
   m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA );
   m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE );

   //パーティクルはライティングしない。
   m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );

   //Zバッファへの書き込みを無効にする。ただしZバッファを参照し陰面処理は行う。
   //Zバッファは不透明モデルを描画して作成したものを使用するため、不透明モデルによる切り取りは行われる。
   m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, FALSE );

   //この設定ですべての透明モデルを描画していく…

図16

とりあえずOKです。しかしこれですべてパターンで問題なく描画できるわけではありません。 今回はアルファ ブレンドを加算合成で処理しましたが、加算合成はバックバッファに単純に色を 加算していくだけなので半透明モデルどうしの前後関係が問題になりません。 しかし線形合成の場合はモデルの前後関係が重要になる場合があります。

パターン2

図17

まあミサイルの飛行機雲と思ってください。途中曲がったりしてるけど、飛行機雲が画面左下手前から右上奥方向に伸びています。 これは正しく描画できてないんですけど、この画だけではよくわからんですな。

図18

これはOKです。きれいに描画できてます。違いは一目瞭然です。

さて図18ではスプライトの表示位置のZ値でいったん降順にソートしてから描画しています。 そのため奥にあるスプライトから順に描画されていきます。 ソートロジックについてはソートを参照してください。詳しく説明してないけど(笑)。

さて線形合成の場合、常にソートする必要があるのかというとそうでもないと思います。 例えば煙です。確かにソートしないと前後関係が正しくなりませんが、生成と消滅を頻繁に繰り返したり、表示位置が常に移動したりと 動きが激しいので案外気づきにくいのではないでしょうか。最もこの辺は描画結果を検証して、臨機応変に対応すべきことですけどね。

さてこれですべての状況に対応できるかというと残念ながらこれでもだめなんです。

パターン3

図19

画面の左右に柵を配置しています。柵は前後関係では右側の柵を奥に配置してありますが、右側の柵が手前に描画されています。 しかし表示位置では右側の柵が手前となっているため、Z値で降順にソートした場合、左側の柵から描画されるため このようになります。

図20

上空から見たときのイメージです。●がそれぞれの柵の表示位置です。

この様な現象が発生する状況は、スプライトの向きが異なるもの同士の場合です。 パーティクルの場合、通常ビルボード処理を行い 常にカメラ位置に対し正面を向くようにするためこのような現象は発生しません。 さてこれに対応するには、アルファ テストを使用します。

図21

OKです。左側の柵が手前に描画されました。


   //**********************************************************
   //STEP1:まずカラーキーを使用しないモデルをすべて描画する
   //**********************************************************
   
   //アルファ ブレンドを無効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
   
   //ライティングする。
   m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE );
   
   //Zバッファへの書き込みを有効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );

   //アルファ テストを無効にする
   m_pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, FALSE );

   //この設定ですべての不透明モデルを描画していく…

   //**********************************************************
   //STEP2:次にカラーキーを使用するモデルをすべて描画する
   //**********************************************************

   //アルファ ブレンドを有効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );

   //線形合成
   m_pd3dDevice->SetRenderState( D3DRS_BLENDOP,   D3DBLENDOP_ADD );
   m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA );
   m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );

   //柵はライティングしない。
   m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );

   //Zバッファへの書き込みを有効にする。
   m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );

   //アルファ テストを有効にする
   //テストに合格すると、レンダー ターゲット サーフェイスに書き込まれる
   m_pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, TRUE );

   //この値とテクスチャーのアルファ値とを比較する
   m_pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0x00000080 );

   //D3DRS_ALPHAREFで指定した値 > テクスチャーのアルファ値 のときレンダー ターゲット サーフェイスへ書き込みしない
   //つまりテクスチャーのアルファ値が黒っぽい色のときレンダー ターゲット サーフェイスに書き込まない
   m_pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL );

   //この設定ですべてのカラーキーを使用するモデルを描画していく…

アルファ テストの実装方法のサンプルソースです。 アルファ テストを有効にするとなにができるかというと、テクスチャーのアルファ値によってレンダーターゲットサーフェイスへ描画するかしないかの制御が可能になります。 描画しなければZバッファへの更新も行いません。つまりアルファ テストによって陰面処理の制御が可能になるということです。 この方法が使用できるのはサンプルの柵のように半透明の部分が存在せず、描画するか、しないかの区別がきっちり分かれているモデルの描画の場合です。 煙のように半透明の部分が存在する場合は使用できません。

これで3パターン紹介しました。アルファ ブレンディングの描画方法の対策としてはこれですべてだと思います。 まとめると

1.パターン1
  半透明モデルを加算合成する場合は、Zバッファへの書き込みを無効にして描画します。

2.パターン2
  半透明モデルを線形合成する場合は、Zバッファへの書き込みを無効にして、 半透明モデルの表示位置のZ値で降順にソートして描画します。ただし状況によってはソートしなくていい場合もあります。

3.パターン3
  モデルをマスク処理する場合は、アルファ テストを使用して描画します。

です。使用方法に合わせて臨機応変に対応してください。ただしすべてのパターンに共通していえることは、 不透明モデルをすべて描画してから半透明モデルを描画するようにすることです。


●12.リフレッシュレートとFPS

リフレッシュレート(垂直同期周波数)とは、ディスプレイが1秒間に描画できる画面の数のことで、単位はHz(ヘルツ)で表されます。 1秒間に描画できる横方向のラインの数は、水平走査周波数といい、 この水平走査周波数が例えば[ 46KHz ]で、画面の解像度が[ 1024 x 768 ]の場合、垂直走査周波数は46000/768≒60Hzとなります。

フレームレート(FPS)とは3DCGの表示や動画の再生において、1秒間に何回画面を書き換えることができるかを示す指標です。 これはプログラムの処理の負荷率に影響されます。負荷の高い処理を行うとフレームレートが低下します。 瞬間的にフレームレートが低下するようなゲームだとクソゲームのレッテルを貼られるのでフレームレートがほぼ一定になるように設計します。

さて筆者が使用している開発環境は、液晶モニターでリフレッシュレートが[ 60Hz ]、ビデオカードは[ GeForce Go 7600 ]です。 この環境でサンプルを実行するとフレームレートが何故か常に60FPSとなります。どんなに軽いプログラムでも60FPSを超えることがありません。 この現象ですが、どうもビデオカードの方で自動的にフレームレートをモニターのリフレッシュレートにあわせているようです。

図22

これはNVIDIAの設定画面です。この中の「垂直同期」の行が「3Dアプリケーション設定を使用します。」となっています。 この設定の場合、強制的にフレームレートがアプリケーションで設定したリフレッシュレートと同じくなります。

図23

「垂直同期」の設定を「強制オフ」に変更しました。 この場合フレームレートがリフレッシュレートの設定に影響されなくなります。 この状態でサンプルを実行すると60FPSから330FPSになりました。

たいしたネタでなくてすいません。


●13.FPSを一定にする

実行環境によってフレームレートは異なります。そのためどんなPC環境で実行してもフレームレートが一定になるようにする必要があります。 処理概要は、フレームレートが規定値以上となるときは描画処理をスキップしてフレームレートが一定になるようにします。

   
//フレームレート
//これはヘッダファイルなどで宣言する
const int FPS = 60;

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE /*hPrevInstance*/,
                     LPSTR     /*lpCmpLine*/,
                     INT       /*nCmdShow*/ )
{

   //いろいろ処理するが省略...

   BOOL PerformFlg = FALSE; //TRUE:パフォーマンスカウンタ使用可能 FALSE:使用不可

   LONGLONG NowTime     = 0;   //現在のカウント数
   LONGLONG LastTime    = 0;    //前のカウント数
   LONGLONG Frequency   = 0;   //1秒間あたりのカウント数
   LONGLONG OneFrameCnt = 0;   //1フレームレートあたりのカウント数

   //パフォーマンスカウンタの周波数(1秒間あたりのカウント数)を取得
   //パフォーマンスカウンタはハードウェアがサポートする場合のみ使用できることに注意
   if( ::QueryPerformanceFrequency( (LARGE_INTEGER*)&Frequency ) )
   {
      PerformFlg = TRUE;

      //1フレームレートあたりのカウント数を計算する
      OneFrameCnt = Frequency / FPS;

      //現在のカウンタを取得する
      ::QueryPerformanceCounter( (LARGE_INTEGER*)&LastTime );
   }
   
   //パフォーマンスカウンタが使用できないのでtimeGetTimeを使用する
   else
   {
      PerformFlg = FALSE;

      //timeGetTimeの精度がマシンによっては 5ミリ秒以上になる場合があるため、精度を1ミリ秒に変更する
      timeBeginPeriod(1);

      //1フレームレートあたりの処理時間をミリ秒で計算する
      OneFrameCnt = 1000 / FPS;

      //現在の時間を取得する
      LastTime = (LONGLONG)::timeGetTime();
   }

   do
   {
      //処理するメッセージがあるか
      if( ::PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
      {
         ::TranslateMessage( &msg );
         ::DispatchMessage( &msg );
      }

      else
      {
         //現在のカウント数を取得する
         if( PerformFlg )
            ::QueryPerformanceCounter( (LARGE_INTEGER*)&NowTime );

         //現在の時間を取得する
         else
         {
            NowTime = ::timeGetTime();

            //タイマーが巻き戻されたときの処理
            //正確に処理されないが瞬間的なことなので別に問題ないかと
            if( LastTime >= NowTime )
               LastTime = NowTime;
         }

         //レンダリングする時間になったか
         if( NowTime >= LastTime + OneFrameCnt )
         {
            //この関数内でいろいろ描画処理
            if( !MainLoop( hWnd ) )
               break;

            LastTime = NowTime;
         }
      }
   }while( msg.message != WM_QUIT );

   if( PerformFlg == FALSE )
      timeEndPeriod(1);

   //いろいろ処理するが省略...

}

フレームレートの調整はメッセージループ処理を修正することで対応します。::PeekMessageとか見たことありますよね?
フレームレートを一定にする(サンプルでは60に調整)処理の処理フローの概要は

1.時間を計測する(LastTime)
2.時間を計測する(NowTime)
3.NowTimeとLastTimeとの差を計算し、この結果が1フレームあたりに経過する時間以上の場合はレンダリング処理する
4.LastTimeにNowTimeの値を代入する
5.2へ

となります。

さてサンプルでは2種類の計測方法を使用しました。パフォーマンスカウンタ(::QueryPerformanceCounter関数)と::timeGetTime関数です。順に解説していきます。

パフォーマンスカウンタ

これは1ミリ秒よりも小さい間隔での測定が可能という非常に精度の高い時間測定方法です。 時間測定方法といいましたがこの関数から取得できる値はカウント数です。この値はCPUのクロック周波数と同期してカウントされます(マニュアルにそう書いてあるわけではないが多分そう)。 したがって実行環境によってCPUの性能も変わるため、基準となる値が必要となります。 その値は::QueryPerformanceFrequency関数から取得します。この関数からは1秒間あたりのカウント数が取得できます。 この値を元に1フレームあたりのカウント数を計算します。サンプルソースではOneFrameCnt変数に値を格納しています。

パフォーマンスカウンタを使用する上での注意点として、ハードウェア(CPU)がパフォーマンスカウンタをサポートしている必要があります。 使用できない場合の保険として::timeGetTimeでも処理できるようにしています。 もっともパフォーマンスカウンタをサポートしていないCPUがいまでも存在するのかは疑問ですが。

::timeGetTime

この関数は、システム時刻をミリ秒単位で取得します。システム時刻は Windows が起動してから経過した時間です。 この関数の場合、単位が時間となるため1フレームあたりの時間(ミリ秒)で計算する必要があります。 計算した値はOneFrameCnt変数に格納されます。

::timeGetTimeを使用する上での注意点として精度がマシンによっては5ミリ秒以上になる場合があることです。マニュアルにそう書いてあります。っていうか筆者が使用しているノートPC でこうなりました。そのため精度を1ミリ秒に変更する必要があります。::timeBeginPeriod関数で変更します。そしてタイマーの使用終了後に::timeEndPeriodでクリアします。
そしてもう一点::timeGetTimeは0ミリ秒から2^32ミリ秒の間を循環します。そのためPCをずっと起動しっぱなしの場合(といっても49.71日間起動しっぱなしってことあるか?) まったく画面が更新されない状態が発生する可能性があります。そのための処理も一応入れてます。正確ではないけどね。

最後にフレームレートはリフレッシュレートにあわせるのがベストらしいです。フレームレートとリフレッシュレートの両方を考慮した場合60に設定するのがベストでしょうか。


●14.開発ツールを Visual Studio .NET 2003 から Visual Studio 2008 に移行する

いい加減開発環境を最新化します。2010年3月時点では Visual Studio 2008 が最新バージョンです。( 一応Beta版として Visual Studio 2010 Beta 2 がでてます。) DirectX SDKもそのうち最新化するつもりです。

さてダウンロードとインストールについては、特別説明することもないです。ですが、開発時にはいくつか注意点があります。 ここではVisual Studio .NET 2003 から Visual Studio 2008 に移行した場合の注意点として説明します。

1.追加された C++ 標準組み込み関数
sprintf などの C++ 標準組み込み関数と同様の機能を持つ新しい関数が追加されています。新しい関数ではサフィックスに _s がついています。 sprintf の場合 sprintf_s が新しい関数となります。古い関数を使用するとコンパイル時にワーニングエラーになります。 ワーニングエラーなので、古い関数でもコンパイルはできます。 またリファレンスによると、古い関数についての説明として「"推奨されない" ということは、その関数が CRT から削除される予定だということではありません。」 との記述があるので、古い関数も今後も使用することが可能なようです。
さてこの新しい関数ですが、リファレンスによるといくつかのセキュリティ強化がなされているようです。 詳しくはリファレンスの「CRT のセキュリティ強化」を参照してください。

2.リンクエラー
Microsoft DirectX 9.0 SDK (December 2004)を使用している場合、 「fatal error LNK1104: ファイル 'libcp.lib' を開くことができません。」 って内容のリンクエラーが発生して実行ファイルを作成できません。 この libcp.lib ファイルはリファレンスによると Visual C++ 2005 から削除されたようです。
さらにDirectX SDKをインストールしたフォルダ内を libcp.lib で検索したところ dxreadme.htm ファイル内に libcp.lib に関する説明がありました。これによると 「サンプルをVisual Studio 2005ベータ1 Refreshを使用してコンパイルしないでください。 プロジェクトの設定によってエラーを回避することができます。 このエラーはアプリケーションの実行には影響しません。 これらの問題はVisual Studio 2005でサポートされます。」 みたいな感じの説明が書かれています。プロジェクトの設定は図24のようにして行います。
図24図24

libcp.lib が何者なのかは不明です。しかし、上記の設定を行えば、エラーを回避することができるようになります。 ところで最新のDirectX SDKの場合は、この問題は発生しなくなるのだろうか?

最初に説明したとおり、Visual Studio .NET 2003 から Visual Studio 2008 に移行した場合の注意点をあげましたが、 Visual Studio .NET 2003 から Visual Studio 2005 に移行した場合でも同様の問題が発生するかもしれません。 Visual Studio 2005 は使ってないので不明です。


Top
inserted by FC2 system