2011/06/25

いまさら DirectX10(はじめの一歩篇)

※ このブログはただのお勉強日記だよ! 参考になるような解説記事は一切ないよ!

今は昔。

ぼくもネイティブな 3D プログラミングなるものに憧れ、当時主流だった DirectX9 をつまみ喰いした事がありました。
当時、DirectX9 をあれこれ試したときに作ったプログラミングノート
あれから時代は移ろい、いつの間にか最新のトレンドも DirectX11 に代わってしまったようです。

そんなわけで、そろそろ最新の API を学び直す時期かなぁと思い立ったわけですが、残念ながらぼくのパソコン(のグラボ)は DirectX11 には対応していないので、仕方なく DirectX10 に手をつけてみる事にしました。

さてさて。

DirectX10 からは固定機能パイプラインが無くなり、プログラマブルシェーダの理解が不可欠となってしまいました。 Vista 時代を迎えてかなり思い切った仕様変更だと思います。

何を隠そう、ぼくはこのシェーダに手を付ける事が怖くて、DirectX9 の頃まではほぼ固定機能パイプラインに頼りっきりだったため、DirectX10 になって開発の敷居ががくんと上がってしまいました。

こうやってどんどん梯子を外されていくうちにゲイツ様への忠誠心が確実に薄らいでいますが、それはさておき。

今回は、おっかなびっくり DirectX10 なるものに触れてみようかと思います。



【Win32 API 直叩きでウィンドウをつくる】

まずは Windows 使いにとってすっかりおなじみとなった、「ただウィンドウを表示する」という初歩的なプログラムからスタート。

ただし、ミューテックス(セマフォ)によって多重起動を禁止していたり、画面のサイズを固定したりと、ちょこちょこ小賢しいギミックを混ぜてあります。

※ 多重起動を許可したアプリケーションで静的変数やグローバル変数を使うと、すべてのウィンドウでメモリ空間を共有する事になるため、場合によっては致命的なバグになります。今回は手抜きのために DirectX 関連でグローバル変数(しかもポインタ)を使うつもりでいるため、多重起動を禁止にしました。


このコード自体は DirectX とは一切関係ないですが、誰が作ってもだいたい一緒になる割に、書くのがそれなりに厄介な代物なので、一応テンプレートにして置いておくことにしました。



【はじめての DirectX10】

上記のコードに、DirectX10 (Direct3D) を初期化するコードを追加します。

どうも DirectX9 時代とレンダリングパイプラインが変わったせいか、初期化のコードだけでも以前とはだいぶ違う印象です。

一言で言うと、『DirectX9 と同じ事を再現するだけでも、手続きがより繁雑になっている』といったところでしょうか。

DirectX10 からは、様々なリソースがかなり抽象的に扱われるようになったらしく、それらをパイプラインにバインドするだけでもワンクッション処理が必要だったりして、正直面倒なのです。



最初はヘタにプログラムの構造を複雑化したりはせず、もとのソースに愚直に書き足していった方が理解が早まるので(ぼくの場合は)、ひたすらべた書きしました。

初期化全体の流れは、説明されれば『あー、なるほどね』と思えるものの、個々の処理に関しては今ひとつ必然性が見えないもの(なんでこれがここで必要になるの?という関数とか)もいくつかあるので、上記のコードを『ゼロから書け』と言われたら心が折れそうな勢いです。

そして、地味に嫌らしいのが、参照カウンタの挙動が旧バージョンから密かに変更されているという現象。これ、無意識のうちに凶悪なバグを生むので、留意しないとたいへんな事になります。



【HLSL ではじめてのシェーダ】

DirectX9 までは、ポリゴン1枚を表示する程度ならば、固定機能パイプラインを使ってさっくり作れたものでした(いやそれでも充分に面倒だったのだけど)。

ですが、これからはどんなに単純なレンダリングであってもプログラマブルパイプラインを通してレンダリングする必要があるため、HLSL なるシェーダ言語を使わなければならないそうです(DirectX8 時代はシェーダ言語ではなくアセンブラでした)。

まずは簡単なところから、実際に手を動かして、マウスで三角形ポリゴンをぐりぐり動かすサンプルを作ってみる事に。

【Simple.fx】



【Main.cpp】


Simple.fx が、くだんのシェーダファイルです。

シェーダは C 言語っぽい書き方が可能ですが、微妙に異なる構文がかえっていやらしい。

さらに、シェーダとアプリケーション側(C++)で整合性をとるため、慣れないうちは多少の煩わしさに耐えねばなりません。

たとえば、アプリケーション側のデータをシェーダに流し込むために必要なコンスタントバッファの構造体も、仕様を統一しなければならないのです(これが仕方のない事であるというのは重々解っているつもりではありますが……)。

【Simple.fx】
  1. cbuffer global  
  2. {  
  3.     matrix g_mWVP;  
  4.     float4 g_PolyColor;  
  5. };  

【Main.cpp】
  1. struct ConstantBuffer {  
  2.     D3DXMATRIX  mWVP;  
  3.     D3DXVECTOR4 vColor;  
  4. };  

ほかにも、頂点シェーダやピクセルシェーダをコンパイルする際、各々の関数名を明示的に指定する必要があったりとか色々アレですが、ざっと触ってみた印象としては『こんなもんかぁ』という感じです。

ちなみに、InitPolygon から RenderPolygon までの一連の処理のわかりやすい解説が、MSDN にあがっていました。



で。

さっそく気になる点がひとつ。

ポリゴンの頂点座標は、以下のように InitPolygon 関数で決め打ちしてしまっています。
  1. // ポリゴンの初期化  
  2. // ========================================  
  3. HRESULT InitPolygon() {  
  4.     // 頂点バッファ作成  
  5.     Vertex vertices[] =  
  6.     {  
  7.         D3DXVECTOR3( 0.0f,  0.5f, 0.0f),  
  8.         D3DXVECTOR3( 0.5f, -0.5f, 0.0f),  
  9.         D3DXVECTOR3(-0.5f, -0.5f, 0.0f),  
  10.     };  
  11.   
  12.     D3D10_BUFFER_DESC bd;  
  13.     bd.Usage = D3D10_USAGE_DEFAULT;  
  14.     bd.ByteWidth = sizeof(Vertex) * 3;  
  15.     bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;  
  16.     bd.CPUAccessFlags = 0;  
  17.     bd.MiscFlags = 0;  
  18.   
  19.     D3D10_SUBRESOURCE_DATA initData;  
  20.     initData.pSysMem = vertices;  
  21.     if(FAILED(g_pDevice->CreateBuffer(&bd,   
  22.         &initData,   
  23.         &g_pVertexBuffer)))  
  24.     {  
  25.         return E_FAIL;  
  26.     }  
  27.   
  28.     // 頂点バッファをセット  
  29.     UINT stride = sizeof(Vertex);  
  30.     UINT offset = 0;  
  31.     g_pDevice->IASetVertexBuffers(0, 1, &g_pVertexBuffer, &stride, &offset);  
  32.   
  33.     return S_OK;  
  34. }  

並進や回転などの個別の運動はその都度コンスタントバッファに流し込めるので、形状が固定的なメッシュのレンダリング等はこれで充分でしょうが、たとえばデバッグ目的等で任意の長さの線分を空間中に惹きたい場合はどうするのかなぁ……(※ DirectX9 の頃までは、IDirect3DDevice9::DrawPrimitiveUP 関数で動的に線を引けたので、いちいち頂点バッファを使うまわりくどい真似はしませんでした)。

……と思ったけれど、よくよく考えたらコンスタントバッファにアクセスできるのだから、同様に頂点バッファに格納されている中身だって読み書きできて当たり前でした(てへぺろ

ただし、一度確保した頂点バッファへのポインタはしっかり保持し続けねばなりません(これも当然ですね)。

うーん。

なんというか、描画のパフォーマンスとグラフィックス表現の柔軟性を優先させた結果、データ結合度がものすごく高くなってしまっている気がして、個人的に少し気持ちが悪いです。
 慣れていくしかないのかなぁ……。



逆に、改良されたなと思ったのが、基本的にデバイスロストの回避処理をコーディングしなくてよくなった事。

従来ならば、(対策の甘い)DirectX プログラムを実行中に、別のフルスクリーンアプリケーション(スクリーンセーバー等)に切り替えたりすると、速攻でデバイスロストが発生してプログラムの再起動を余儀なくされたものです。

さらに、デバイスロストからの復活処理自体も、ヘタに書けばメモリリークをガンガン引き起こして本当に危なっかしかったのですが、そういった危険な橋を渡らずに済むのはよい事です。

試しに、上記のプログラムを実行中に『Ctrl + Alt + Delete』を押して、フルスクリーンのログオンフレームに強制的に切り替えてみました。

DirectX9 までは、たったこれだけの操作で簡単にデバイスロストが発生し、再起動を余儀なくされていましたが、今回は見事にびくともしませんでした。デバイスロスト回避コードを一切書いていないにも拘わらず、です。

このように、旧バージョンと比較して一長一短ありますが、せっかくだからもうちょっと使えるようになりたいなぁと思いました。まる。

1 件のコメント:

ひとことどうぞφ(・ω・,,)