過去の独りごち/独りごとはこちら
過去のJavaアプレットは
こちら

 

8/20 VertexShader事始め

 昨日に引き続いて、今日はVertexShader(以下、VS)についての特集というよりむしろ僕の学習メモです。

 VSはレンダリングパイプラインの中ではPSの前段にあたり、VSに入力されるデータとは、DrawPrimitiveメソッドで渡されるVertexBufferに格納された頂点データそのものです。この頂点データを、画面に出力できるようワールド→ビュー→スクリーン変換を行い、スクリーン座標系に変換し、頂点の法線と光源ベクトルなどからライティングを行って頂点の色を決定するのが基本になるわけです。

入出力をおさえてみる

 まずは前回同様、VSにどんなデータが流れてきて、どんなデータを出せばいいのか、それからおさえてみます。

 入出力の方法自体はPSと同じで、それぞれレジスタにマップされています。ただし、出力情報がPSの場合は色データ1つだけだったのに対し、VSでは頂点の位置や色など少々多めになっています。このため、入力用/汎用レジスタ群と出力用レジスタ群が分かれています。

 まずは、入力用/汎用レジスタについて

 

名前 アクセス制限 用途
a0 (addres register) 1 書き込み専用 cレジスタの間接参照に使います。
整数の4次元ベクトルですが、そのうちx要素だけが有効です。
c (constant register) 96以上 読み込み専用 定数の格納に使います。
一般に、頂点変換用の行列やライトなどの情報をアプリケーション本体のコードから渡すために使われます。
d3dcaps9.MaxVertexShaderConstでレジスタ数が確認できます。
r (temporary register) 12 読み書き可 汎用レジスタです
v (input register) 16 読み込み専用 頂点データが入ります。
格納される内容は頂点フォーマットに従います。

 a0レジスタ以外は4次元のSingle型によるベクトルです。vレジスタに具体的に何がどうやって格納されるのかについては、後で使い方と一緒に述べます。

 次に、出力レジスタについて

 

名前 アクセス制限 用途
oD (vertex color register) 2 書き込み専用 頂点色を出力するのに用います。
一般に、oD0がディフューズ、oD1がスペキュラ色の格納に使われるようです。
oFog (fog register) 1 書き込み専用 フォグの濃度を出力するのに用います。範囲は0.0〜1.0
これも四次元のSingle型のベクトルですが、x要素だけが参照されます。
oPos (position register) 1 書き込み専用 変換後の頂点の位置を出力するのに用います。
oPts (point size register) 1 書き込み専用 ポイントサイズを出力するのに用います。
ポイントスプライト用かな?
oT (texture coordinate register) 8 書き込み専用 テクスチャ座標を出力するのに用います。

レジスタの機能が分かった所で、やってみましょう

頂点フォーマットをおさえる

 レジスタの紹介はしたものの、引っかかるのが入力レジスタ群のうち vレジスタです。このレジスタに頂点フォーマットをどうやって認識させるのかという問題があるわけですね。この作業は多少面倒かもしれませんが、ガマンしましょう。

 ここでは、例として、ROKLoaderに使われている、六角大王の頂点データを扱うための、TROKVertexを取り上げて説明します。

 ROKVertexは、以下に示すように

TROKVertex = packed record
  Case Integer of
    0: (
      x,y,z:Single;     //頂点
      nx,ny,nz:Single;  //法線
      diffuse:D3DCOLOR;   //ディフューズ
      tu,tv:Single;       //テクスチャ座標
      );
    1:(
      pos:D3DVector;
      normal:D3DVector;
      );
end;

 これをVertexShaderに認識させるために、以下のような配列定数を宣言します。

const
ROKVertex_decl:Array[0..4] of D3DVERTEXELEMENT9 = (
    ( Stream:0 ; Offset:0 ;   _Type:D3DDECLTYPE_FLOAT3 ;
   Method:D3DDECLMETHOD_DEFAULT ; Usage:D3DDECLUSAGE_POSITION ; UsageIndex:0 ),
    ( Stream:0 ; Offset:12 ;  _Type:D3DDECLTYPE_FLOAT3 ;
   Method:D3DDECLMETHOD_DEFAULT ; Usage:D3DDECLUSAGE_NORMAL ; UsageIndex:0 ),
    ( Stream:0 ; Offset:24 ;  _Type:D3DDECLTYPE_D3DCOLOR ;
   Method:D3DDECLMETHOD_DEFAULT ; Usage:D3DDECLUSAGE_COLOR ; UsageIndex:0 ),
    ( Stream:0 ; Offset:28 ;  _Type:D3DDECLTYPE_FLOAT2 ;
   Method:D3DDECLMETHOD_DEFAULT ; Usage:D3DDECLUSAGE_TEXCOORD ; UsageIndex:0 ),
    ( Stream:$FF ; Offset:0 ; _Type:D3DDECLTYPE_UNUSED ;
   Method:D3DDECLMETHOD_DEFAULT ; Usage:D3DDECLUSAGE_POSITION ; UsageIndex:0 )
  );

 上から順に、TROKVertexの各メンバに対応しているというのが分かると思います。各メンバの意味ですが

  1. Stream : 何番のVertexStreamからデータを取ってくるか
  2. _Type : データの型
  3. Method : データを取り込む際に一定の操作が必要な場合、その操作は何か(ディスプレースメントマップのルックアップとか)
  4. Usage : 何に使うデータか
  5. UsageIndex : 同じUsageを持つ要素が複数ある場合、出現する順にこのメンバを0、1、2…と振る必要があるそうです

 一番下の要素は終端マーカーであり、TROKVertexの内容とは無関係です。終端マーカーは元々D3DDECL_ENDという定数としても定義されているのですが、配列定数の宣言時には、他の構造型定数は使用できないみたいで、展開した形で書いちゃってます(^^;)

 こうして、VertexShaderに頂点フォーマットを認識させるための荷札が出来たわけです。では、これをDirect3DDeviceに渡して行きましょう。

var
  VDecl:IDirect3DVertexDeclaration9;
begin
  DG.D3DDevice.CreateVertexDeclaration(@ROKVertex_decl, VDecl);
  DG.D3DDevice.SetVertexDeclaration(VDecl);

 IDirect3DVertexDeclaration9インターフェイスを生成し、D3DDevice.SetVertexDeclarationメソッドによってD3DDeviceに渡します。

 使用が終わったら

DG.D3DDevice.SetVertexDeclaration(Nil);
VDecl.Release;

  このようにSetVertexDeclarationにNilを指定してから、IDirect3DVertexDeclaration9オブジェクトを解放します。

 実を言いますと、オブジェクトモーフ用など、複数のストリームから頂点データを読み込むような必要が無ければ、これに関しては何もやらなくてもVertexShaderはちゃんと動いてくれるみたいです。とはいえ、面倒なのは頂点フォーマットをVS用に宣言しなおす部分くらいなので、これは新しく頂点フォーマットを作る都度行えば問題ないでしょう。

VSアセンブラでゴリゴリと

 いよいよ、VertexShaderアセンブラでの作業に移ります。

 とりあえず、ディフューズだけ考慮したライティング(平行光源1つ)と、特に工夫の無いテクスチャマッピングを施すようなシェーダを作ります。尚、入力には先ほどのTROKVertexが来ると想定します。

    vs_1_1  //バージョンの宣言

    // レジスタへ頂点データを割り当てる
    dcl_position v0              // v0に位置ベクトル
    dcl_normal v4                // v4に法線ベクトル
    dcl_color0 v7                // v7にディフューズ色
    dcl_texcoord0 v8             // v8にテクスチャ座標

    //c4〜c7に行列が入っていると仮定
    //c12にローカル座標系での光線
    m4x4 oPos, v0, c4            // ビュー、投影行列で位置ベクトルを変換
    dp3  r0, v4, c12             // 光線ベクトルと法線の内積を計算
    mul  oD0, -r0.x , v7         // ディフューズ色と↑で計算した内積の負の数を乗算して
                                 // 頂点のディフューズ色を決定
    mov  oT0.xy , v8             // テクスチャ座標はそのままで

 とりあえず、vshader.vshとして保存しておきますか。

 中身についての解説ですが、dcl_** 命令によって、入力情報である頂点データをvレジスタ群に割り当て、それをもとに計算します。尚、前回はコメントをセミコロンで区切ってましたけど、実は//でもコメントになります。

 その他の注意事項については、コメントを参照してください(^^;)

 計算といっても、あんまりする事ないんですね、実は。1頂点ごとに行わなければならない計算というのは当然最小限に絞らないとパフォーマンスはガタガタ低下していくので、VSアセンブラはなるだけシンプルに、と。1ピクセルごとに実行されるPSほど神経質にならくても良いという考え方もありますけどね。

 で、PSアセンブラ同様、c:\dxsdk\bin\dxutils\vsa.exe でアセンブルします

vsa vshader.vsh

 生成されたvshader.vsoをD3DDeviceに渡してやれば、カスタムVSが使用できます。この方法もPSとほとんど同じ。

var
  ms:TMemoryStream;
  VS:IDirect3DVertexShader9;
begin
  ms:=TMemoryStream.Create;
  ms.LoadFromFile('vshader.vso'); //vsoファイルを読み込み
  DG.D3DDevice.CreateVertexShader(ms.Memory, VS); //デバイス

 こうして、PixelShaderオブジェクトを生成します。利用するときは

DG.D3DDevice.SetVertexShader(VS);

 まったく簡単です。

呼び出し側もきっちり

 お次は呼び出し側で行う処理についてです。今までの話の流れでは、DrawPrimitiveメソッドなどで描画させる前に、

  1. 位置ベクトル変換用の行列の準備
  2. 光線ベクトルのローカル座標への変換
  3. 上記をVSに渡すための処理

 が必要になってくるわけです。

//ワールド→ビュー→投影変換行列
var
  transMat,tmpTranMat:
begin
 //DG-Carad&SXLib用、変換行列の計算
  transMat:=Sender.MatrixOnRender;
  transMat:=NowCompositeMatrix(transMat,Scene.CameraFrame.ViewMatrix);
  transMat:=NowCompositeMatrix(transMat,Scene.ProjectionMatrix);

  //こっちは、RenderStateから直接読み込む方法、わりと実践向き
 //上のルーチンとやってる事は同じなので、どっちか好きなほうで
  DG.D3DDevice.GetTransform(D3DTS_WORLD, transMat);
  DG.D3DDevice.GetTransForm(D3DTS_VIEW, tmpTransMat);
  transMat:=NowCompositeMatrix(transMat, tmpTransMat);
  DG.D3DDevice.GetTransForm(D3DTS_PROJECTION, tmpTransMat);
  transMat:=NowCompositeMatrix(transMat, tmpTransMat);

  //転置する…VSの4x4行列はOpenGLと同じならびになってて、Direct3Dとは行・列が逆
  transMat:=NowTMatrix(transMat);

  //頂点変換用の行列を定数レジスタに突っ込む
  //Single型16個分をc4レジスタからc5,c6,c7レジスタに渡って突っ込む
  DG.D3DDevice.SetVertexShaderConstantF(4, @transMat, 4);

  //光線ベクトルの計算…ローカル座標系での光線ベクトルに変換
  lv:=NowTransform(Lights[0].Params.Direction,
      NowTMatrix(NowExtractRotation(Sender.MatrixOnRender)));
  //c12レジスタに突っ込む
  DG.D3DDevice.SetVertexShaderConstantF(12, @lv, 1);

 行列の略号がMtrxでなくMatになっているのがむずがゆい人も居るかもしれませんが、ガマンして(^^;)

 注意すべきは、4x4行列の行と列がVSとDirect3Dでは逆になっている点でしょうか。…うーむ。

 ともかく、こうしてVSを呼び出す前のお膳立てが整うわけです。

以上

 以上でVSの使用についての説明は終わりです。次回からはこれを利用して実際にVS/PSならではの表現技法などについて学習していきたいと思います。

 前回は、PixelShader(以下、PS)について取り上げたのですが、PSはVSの出力を受け取って、ポリゴンの色を決定する部分でして、つまりVSとの連携は必要不可欠なわけです。レンダリングパイプラインの中ではVSの後に実行されるPSの方を先に取り上げた理由は、なんかラクそうだからということに他ならないわけですが…

 ともかく、なんといいますか、VSはPSに比べると幾分面倒です。とはいえ、一度やってしまえば使いまわしや応用が効くものですし、言うまでも無く、非常に高いポテンシャルを持った代物です。

 

Taku Hayase(SANDMAN)

戻る