Vertex Texture Fetchによるスキニング

Vertex Texture Fetchを利用して、スキニングメッシュのサンプルを動かしてみました。
ひにけにxnaさんのところでは、XNAを用いての解説があったので、
ここでは、普通のDirectX SDKとC++による組み合わせでこれをやってみたいと思います。

■ 使用するもの

  • SkinnedMeshのサンプルプログラム
  • ShaderModel 3.0に対応したグラフィックボード
    • Radeon X1000シリーズを除く。これはVTFに対応していないため。

■ ソースコード

* 各準備

まずは、ボーンのマトリックスを格納するためのテクスチャを準備します。
ここではグローバルにおいて、手抜きしました。
また、毎回書き換えるので、ダイナミックテクスチャにしています。

// グローバル
const int MaxMatricesPalette = 128; // ボーン用パレットの数
const int BoneTextureWidth = 128; // ボーン用テクスチャの横方向サイズ
LPDIRECT3DTEXTURE9 g_pBoneTexture;
// テクスチャの作成箇所
pd3dDevice->CreateTexture(
  BoneTextureWidth, 4, 1, D3DUSAGE_DYNAMIC,
  D3DFMT_A32B32G32R32F, D3DPOOL_DEFAULT, &g_pBoneTexture, NULL );

* ボーン用テクスチャの概要

ボーン用のテクスチャは、横方向に使用したいボーンの数、縦方向に4というサイズで作成しました。
というのも、行列は4x4のfloat要素で、今回のテクスチャはRGBA32Fという形式のため、1テクセルに4要素格納できます。
そのため、4テクセル使えば、1つの行列が生成できるということを目的にしています。

横方向(U方向)はボーンのインデックスを示すことにして、
V方向には行列の行を格納することにしました。
このほうが配列っぽく見えるかなと思っています。

* マトリックスパレット数調整のための変換部分

またついでに、ConvertToIndexedBlendedMesh を呼び出している部分も
これらの設定のボーン数をみるように修正が必要です。

pMeshContainer->NumPaletteEntries = min(
  // ここをボーン用テクスチャの横方向サイズとするなど
  MaxMatricesPalette,
  pMeshContainer->pSkinInfo->GetNumBones() );

* マトリックスパレットの設定部分の修正

マトリックスパレット設定部分を次のように変更します。

// first calculate all the world matrices
for( iPaletteEntry = 0; iPaletteEntry < pMeshContainer->NumPaletteEntries; ++iPaletteEntry )
{
  iMatrixIndex = pBoneComb[iAttrib].BoneId[iPaletteEntry];
  if( iMatrixIndex != UINT_MAX )
  {
    D3DXMatrixMultiply( 
      &matTemp, 
      &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], 
      pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
    D3DXMatrixTranspose( &g_pBoneMatrices[iPaletteEntry], &matTemp );
  }
}
{
  D3DLOCKED_RECT rc;
  hr = g_pBoneTexture->LockRect( 0, &rc, NULL, D3DLOCK_DISCARD );
  if( SUCCEEDED(hr) ) {
    for( int i = 0; i < pMeshContainer->NumPaletteEntries; ++i ) {
      float* p = reinterpret_cast( rc.pBits );
      p += i * 4;
      for( int j = 0; j < 4; ++j ) {
        for( int k = 0; k < 4; ++k ) {
          p[k] = g_pBoneMatrices[i].m[j][k];
        }
        p+= rc.Pitch / sizeof(float);
      }
    }
    g_pBoneTexture->UnlockRect( 0 );
  }
  g_pEffect->SetTexture( "gBoneTexture", g_pBoneTexture );
}

テクスチャにボーンのマトリックスを格納していきます。
注意点としてはテクスチャのY方向へずらすときは、ロックしたときのピッチバイト数でずらさないといけない点です。
それと、シェーダー内ではマトリックスとしては取れず、
float4の4つの組で取れる感じになります。
そのためシェーダーに送る際に転置しておきます。
こうすることで、内積を利用して計算が可能になります。

*プロジェクション用行列の設定修正

サンプルのSkinnedMeshのサンプルではmViewProjというシェーダー定数に
実はプロジェクション用の行列しか送っていないです。
ビュー行列はマトリックスパレットに対して適用していました。

この部分を変更します。
mViewProjにg_matProjを設定している部分で、g_matViewとの演算結果を格納するようにしておきます。

■シェーダーコード

ほとんどの部分はサンプルのそのままです。
変更点は以下の通り

  • tex2Dlodにて該当するマトリックスパレット相当のテクセルから値を取得
  • 浮動小数用にサンプラを設定
  • VTF使うために、vs_3_0を設定。あわせてピクセルシェーダーも
//
// Skinned Mesh Effect file (with VTF)
//
float4 lhtDir = {0.0f, 0.0f, -1.0f, 1.0f};    //light Direction
float4 lightDiffuse = {0.6f, 0.6f, 0.6f, 1.0f}; // Light Diffuse
float4 MaterialAmbient : MATERIALAMBIENT = {0.1f, 0.1f, 0.1f, 1.0f};
float4 MaterialDiffuse : MATERIALDIFFUSE = {0.8f, 0.8f, 0.8f, 1.0f};
float  BoneTexWidthInv;
// Matrix Pallette
static const int MAX_MATRICES = 26;
float4x3    mWorldMatrixArray[MAX_MATRICES] : WORLDMATRIXARRAY;
float4x4    mViewProj : VIEWPROJECTION;
texture     gBoneTexture;
sampler2D   gBoneSampler = sampler_state {
  texture = gBoneTexture;
  MipFilter = NONE;
  MagFilter = POINT;
  MinFilter = POINT;
  AddressU = Clamp;
  AddressV = Clamp;
};
///////////////////////////////////////////////////////
struct VS_INPUT
{
float4  Pos             : POSITION;
float4  BlendWeights    : BLENDWEIGHT;
float4  BlendIndices    : BLENDINDICES;
float3  Normal          : NORMAL;
float3  Tex0            : TEXCOORD0;
};
struct VS_OUTPUT
{
float4  Pos     : POSITION;
float4  Diffuse : COLOR;
float2  Tex0    : TEXCOORD0;
};
float3 Diffuse(float3 Normal)
{
  float CosTheta;
  // N.L Clamped
  CosTheta = max(0.0f, dot(Normal, lhtDir.xyz));
  // propogate scalar result to vector
  return (CosTheta);
}
VS_OUTPUT VShade(VS_INPUT i, uniform int NumBones)
{
  VS_OUTPUT   o;
  float3      Pos = 0.0f;
  float3      Normal = 0.0f;
  float       LastWeight = 0.0f;
  // Compensate for lack of UBYTE4 on Geforce3
  int4 IndexVector = D3DCOLORtoUBYTE4(i.BlendIndices);
  // cast the vectors to arrays for use in the for loop below
  float BlendWeightsArray[4] = (float[4])i.BlendWeights;
  int   IndexArray[4]        = (int[4])IndexVector;
  for( int iBone = 0; iBone < NumBones-1; iBone++ ) {
    LastWeight = LastWeight + BlendWeightsArray[iBone];
    float tx = (IndexArray[iBone]) * BoneTexWidthInv;
    float4 uv0 = float4( tx, 0, 0, 1 );
    float4 uv1 = float4( tx, 1.0/ 4.0, 0, 1 );
    float4 uv2 = float4( tx, 2.0/ 4.0, 0, 1 );
    float4 vx = tex2Dlod( gBoneSampler, uv0 );
    float4 vy = tex2Dlod( gBoneSampler, uv1 );
    float4 vz = tex2Dlod( gBoneSampler, uv2 );
    float  w = BlendWeightsArray[ iBone ];
    Pos.x += dot( vx.xyzw, float4(i.Pos.xyz,1) ) * w;
    Pos.y += dot( vy.xyzw, float4(i.Pos.xyz,1) ) * w;
    Pos.z += dot( vz.xyzw, float4(i.Pos.xyz,1) ) * w;
    Normal.x += dot( vx.xyz, i.Normal ) * w;
    Normal.y += dot( vy.xyz, i.Normal ) * w;
    Normal.z += dot( vz.xyz, i.Normal ) * w;
  }
  LastWeight = 1.0f - LastWeight;
  float tx = (IndexArray[NumBones-1]) * BoneTexWidthInv;
  float4 uv0 = float4( tx, 0, 0, 1 );
  float4 uv1 = float4( tx, 1.0f/ 4.0, 0, 1 );
  float4 uv2 = float4( tx, 2.0f/ 4.0, 0, 1 );
  float4 vx = tex2Dlod( gBoneSampler, uv0 );
  float4 vy = tex2Dlod( gBoneSampler, uv1 );
  float4 vz = tex2Dlod( gBoneSampler, uv2 );
  Pos.x += dot( vx.xyzw, float4(i.Pos.xyz,1) ) * LastWeight;
  Pos.y += dot( vy.xyzw, float4(i.Pos.xyz,1) ) * LastWeight;
  Pos.z += dot( vz.xyzw, float4(i.Pos.xyz,1) ) * LastWeight;
  Normal.x += dot( vx.xyz, i.Normal ) * LastWeight;
  Normal.y += dot( vy.xyz, i.Normal ) * LastWeight;
  Normal.z += dot( vz.xyz, i.Normal ) * LastWeight;
  // transform position from world space into view and then projection space
  o.Pos = mul(float4(Pos.xyz, 1.0f), mViewProj);
  // normalize normals
  Normal = normalize(Normal);
  // Shade (Ambient + etc.)
  o.Diffuse.xyz = MaterialAmbient.xyz + Diffuse(Normal) * MaterialDiffuse.xyz;
  o.Diffuse.w = 1.0f;
  // copy the input texture coordinate through
  o.Tex0  = i.Tex0.xy;
  return o;
}
int CurNumBones = 2;
VertexShader vsArray[4] = { 
  compile vs_3_0 VShade(1),
  compile vs_3_0 VShade(2),
  compile vs_3_0 VShade(3),
  compile vs_3_0 VShade(4)
};
float4 mainPS( VS_OUTPUT _In ) : COLOR
{
 return _In.Diffuse;
}
//////////////////////////////////////
// Techniques specs follow
//////////////////////////////////////
technique t0
{
 pass p0
 {
  VertexShader = (vsArray[CurNumBones]);
  PixelShader  = compile ps_3_0 mainPS();
 }
}

■実行結果

こんな感じになります。
理由はピクセルシェーダーでディフューズ用のテクスチャ等を処理せず、
単純にライトの強度を出しているからです。

しかし、これがVTF利用して動いています。

今は、横方向128でやりましたが、これを256にすることで
ボーンの上限256個までを一度に使うことが出来るようになります。

DirectX プログラミング
すらりんをフォローする
すらりん日記

コメント

  1. けん より:

    シェーダ内のBoneTexWidthInvにはどのような値を入れるのでしょうか?

  2. けん より:

    すいません解決しました。1.0/ボーンテクスチャ横サイズですね。

タイトルとURLをコピーしました