2012年

7月

02日

OpenGLでGPUスキニング【頂点テクスチャ編】

OpenGLでGPUスキニング【行列パレット編】からの続き。

 

前回はボーン行列をMatrix配列でGLSLへ渡してスキニングをしていましたが、レジスタ数の上限により一定数を超えるボーンは渡せない欠点がありました。

 

今回ではその問題を解決すべく、頂点テクスチャを使ってスキニングをします。

頂点テクスチャとは?

Vertex Texture Fetch(バーテックス テクスチャ フェッチ)

日本語名:頂点テクスチャ

 

頂点シェーダ内からテクスチャの読み込みが出来る機能のこと。

 

色データ(0~255)以外の情報を格納することができ、定数レジスタより大量のデータを扱うことが可能。ピクセルシェーダでのテクスチャ読み込みと同様、自由にテクスチャ座標を指定でき、複数回読み込むことが出来ます。

 

CPUで時間の掛かる演算処理をGPUで処理させる時のデータ受け渡しに使われたりします

(アプリケーション←→GPU間でのデータのやり取りが可能)。

 

最近のゲームでも使われている技術で、使い方しだいでゲームの見栄えを美しく、高速に表現することが出来ます。

 

私はスキニング以外で使用したことがないのであまり詳しい説明が出来ません。頂点テクスチャについてもっとよく知りたい方は以下のサイトをご参照ください。この説明文も一部こちらから引用しております。

 

頂点テクスチャってなに? ひにけにXNA

 

他にもディスプレースメントマッピングという手法に使用されるようです。

コチラのサイトでは中々面白い使い方をしています。

【4Gamer.net】 - 西川善司の3Dゲームエクスタシー(ページの中盤辺り)

頂点テクスチャを使ってみよう

実際に頂点テクスチャを利用してみましょう。まずは頂点テクスチャの作成方法から。

といっても特別な処理は必要なく、従来のテクスチャ作成と同じ方法で作成出来ます。

 

※OpenTKを使用しています

 

01  // テクスチャ領域確保
02  GL.GenTextures(1, out bindID);
03  GL.BindTexture(TextureTarget.Texture2D, bindID);
04
05  if (!GL.IsTexture(bindID) || GL.GetError() != ErrorCode.NoError)
06      throw new Exception("テクスチャ名生成に失敗しました.");
07
08  // フィルタは補間をかけない ミップマップもかけない
09  GL.TexParameter(TextureTarget.Texture2D,

10        TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);

11
12  GL.TexParameter(TextureTarget.Texture2D,

13        TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
14
15  // ラッピングはClampモード(0~1以外は端っこを引き伸ばす)
16  GL.TexParameter(TextureTarget.Texture2D,T

17          TextureParameterName.TextureWrapS, (int)TextureWrapMode.Clamp);

18
19  GL.TexParameter(TextureTarget.Texture2D,

20          TextureParameterName.TextureWrapT, (int)TextureWrapMode.Clamp);
21
22  // データ形式の設定
23  GL.TexImage2D(
24          TextureTarget.Texture2D, // テクスチャターゲット
25          0, // ミップマップレベル
26          PixelInternalFormat.Rgba32f,  // テクスチャのピクセルフォーマット
27          width, height, // テクスチャの縦横幅(データの要素数)
28          0, // ボーダー
29          PixelFormat.Rgba, // ピクセルの配列形式
30          PixelType.Float, // 1ピクセルのデータ形式
31          IntPtr.Zero); // ピクセルデータは空
32
33  if (GL.GetError() != ErrorCode.NoError)
34      throw new Exception("形式設定に失敗しました.");

今回重要なところは26行目。テクスチャのピクセルフォーマットをこの形式にします。

 

浮動小数点テクスチャ形式

PixelInternalFormat.Rgba32f(GL_RGBA32F)

R、G、B、Aそれぞれに32bitのデータが割り当てられる(128bit形式)。

 

この形式にすると、1ピクセル辺りで128bitのデータを割り当てることが出来るようになり、座標(Vector4)角度(Quaternion)といったデータをテクスチャに1ピクセルずつ保持できるようになります。

 

ちなみにDirectXだとD3DFMT_A32B32G32R32F、XNAだとSurfaceFormat.Vector4がこれにあたります。

 

データを渡すにはglTexImage2Dを使います。31行目ではIntPtr.Zero(null)を渡していますが、これだとピクセルにごみデータが入るので初期化をしておきましょう。

 

int size; // データの要素数 ここではボーン数が設定されているとする

Vector4[] translations = new Vector4[size]; // ボーンの座標データ配列

 

// 座標の初期化

for(int i = 0; i < size; i++)

    transforms[i] = new Vector4(0, 0, 0, 1)  

 

// データ設定

GL.TexImage2D(
         TextureTarget.Texture2D,
         0,
         PixelInternalFormat.Rgba32f,
         size, 1,
         0,
         PixelFormat.Rgba,
         PixelType.Float,
         transforms); 

頂点テクスチャでスキニング CPU側

ではこれを使って実際にスキニングをやってみましょう。まずはアプリケーション側から。

 

今回モデルとモーションにMMDを使ってますが、MMDのアニメーションには拡大値情報がないのでボーンの移動と回転のアニメーションだけをやります。

 

またこれにより、行列パレットと比べるとGPUへ送る情報量を半減させることが出来ます(Vector4=128bit、 Quaternion=128bit、 Matrix=512bit)。 

 

まずボーンの平行移動・回転の情報を格納するテクスチャを作成します。

 

int[] ids = {0, 0}; // テクスチャ領域

Vector4[] init_data = new Vector4[mode.Bones.Count]; // ボーンの初期データ配列

 

// 初期値設定

for(int i = 0; i < mode.Bones.Count; i++)

    init_data[i] = new Vector4(0, 0, 0, 1)  

 

for(int i = 0; i < ids.Length; i++)

{

     // テクスチャ領域確保
     GL.GenTextures(1, out ids[i]);
     GL.BindTexture(TextureTarget.Texture2D, ids[i]);

 

      // フィルタは補間をかけない ミップマップもかけない
      GL.TexParameter(TextureTarget.Texture2D,

          TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);


      GL.TexParameter(TextureTarget.Texture2D,

            TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);

      // ラッピングはClampモード(0~1以外は端っこを引き伸ばす)
     GL.TexParameter(TextureTarget.Texture2D,T

          TextureParameterName.TextureWrapS, (int)TextureWrapMode.Clamp);


     GL.TexParameter(TextureTarget.Texture2D,

          TextureParameterName.TextureWrapT, (int)TextureWrapMode.Clamp);

     // データ形式の設定
     GL.TexImage2D(
         TextureTarget.Texture2D,
         0,
         PixelInternalFormat.Rgba32f,
         mode.Bones.Count, 1,
         0,
         PixelFormat.Rgba,
         PixelType.Float,
         init_data);

}

 

int posID = ids[0]; // ボーン平行移動値のテクスチャ

int rotID = ids[1]; // ボーン回転値のテクスチャ

次にボーンの変形行列の更新をします。

今回は行列ではなく、座標と角度の2つの変数を用意します。

 

// シェーダに渡すボーンデータ

Vector4[] translations = new Vector4[model.Bones.Count];

Quaternion[] rotations = new  Quaternion[model.Bones.Count];

 

// ボーンのワールド姿勢行列の更新

for(int i = 0; i < model.Bones.Count; i++)

{

    // ローカル姿勢をワールド姿勢として代入

    Bone current = model.Bones[i];

    current.AbsoluteTranslation = current.LocalTranslation;

    current.AbsoluteRotation = current.LocalRotation;

 

    // 親ボーンのワールド姿勢行列を合成

    Bone parent = model.Bones[i].Parent;


    if(parent != null)

    {

        // 平行移動の算出
        Vector3 newTranslation =

                Vector3.Transform(current.LocalTranslation, parent.AbsoluteRotation);

        current.AbsoluteTranslation = newTranslation + parent.AbsoluteTranslation;

        // 回転部分の結合(座標系が変わると演算順が逆になるので注意)
        current.AbsoluteRotation = parent.AbsoluteRotation * current.LocalRotation;
    }

}

 

// ボーンの初期逆姿勢とワールド姿勢を合成して、ボーンの変形量を求める

for(int i = 0; i < model.Bones.Count; i++)

{

    // 平行移動の算出
    Vector3 newTranslation  = Vector3.Transform(

            current.InvertBindPoseTransation, current.AbsoluteRotation);

    transations[i] = new Vector4(newTranslation + current.AbsoluteTranslation, 1);

    // 回転部分の結合
    rotations[i] = current.AbsouteRotation * current.InvertBindPoseRotation;

}

 最後に演算結果をテクスチャへ書き込みます。

 

// Uniform番号の取得(初期化時に一回だけやればいい 毎フレームやると重い)

int locate_pos = GL.GetUniformLocation(effect, "BoneTranslationTexture")


// テクスチャユニットの切り替え(平行移動テクスチャはTextureUnit1に登録)

GL.ActiveTexture(TextureUnit.Texture1);

GL.Uniform1(locate_pos, TextureUnit.Texture1 - TextureUnit.Texture0);

 

// 現在のユニットテクスチャを有効化する

GL.Enable(EnableCap.Texture2D);

 

// ボーン平行移動テクスチャの更新

GL.BindTexture(TextureTarget.Texture2D, posID); 

 GL.TexImage2D(
    TextureTarget.Texture2D,
    0,
    PixelInternalFormat.Rgba32f,
   mode.Bones.Count, 1,
   0,
   PixelFormat.Rgba,
   PixelType.Float,
   transations); 


 

// ボーン回転テクスチャのUniform変数の番号

int locate_rot = GL.GetUniformLocation(effect, "BoneRotationTexture")


// テクスチャユニットの切り替え(回転テクスチャはTextureUnit2に登録)

GL.ActiveTexture(TextureUnit.Texture2);

GL.Uniform1(locate_rot, TextureUnit.Texture2 - TextureUnit.Texture0);

 

// 現在のユニットテクスチャを有効化する

GL.Enable(EnableCap.Texture2D);


// ボーン 回転テクスチャの更新

GL.BindTexture(TextureTarget.Texture2D, rotID);
GL.TexImage2D(
    TextureTarget.Texture2D,
    0,
    PixelInternalFormat.Rgba32f,
   mode.Bones.Count, 1,
   0,
   PixelFormat.Rgba,
   PixelType.Float,
   rotations);

 

// ボーンテクスチャのサイズを渡す

int locate_size = GL.GetUniformLocation(effect, " BoneTextureSize")

GL.Uniform2(locate_size , new Vector2(model.Bones.Count, 1));

頂点テクスチャでスキニング GPU側

最後にGLSL側の処理。

基本的に前回とやっていることは同じですが、頂点テクスチャから平行移動値と回転値を取り出す(フェッチ)作業が加わります。

 

sample.vert

attribute vec4 BoneWeights; // 頂点ウェイト
attribute vec4 BoneIndices; // ボーンインディセス
uniform vec2 BoneTextureSize; // ボーンテクスチャサイズ

// ボーン用頂点テクスチャ
uniform sampler2D BoneRotationTexture;
uniform sampler2D BoneTranslationTexture;

//----------------------------------------------------------------------------
// クォータニオンヘルパーメソッド
//======================================

// クォータニオンと平行移動から行列に変換する
mat4 CreateTransformFromQuaternionTransform( vec4 quaternion, vec4 translation )
{
    vec4 q = quaternion;
    float ww = q.w * q.w - 0.5f;
    
    vec3 v00 = vec3( ww       , q.x * q.y, q.x * q.z );
    vec3 v01 = vec3( q.x * q.x, q.w * q.z,-q.w * q.y );
    vec3 v10 = vec3( q.x * q.y, ww,        q.y * q.z );
    vec3 v11 = vec3(-q.w * q.z, q.y * q.y, q.w * q.x );
    vec3 v20 = vec3( q.x * q.z, q.y * q.z, ww        );
    vec3 v21 = vec3( q.w * q.y,-q.w * q.x, q.z * q.z );
    
    return mat4(
        2.0f * ( v00 + v01 ), 0,
        2.0f * ( v10 + v11 ), 0,
        2.0f * ( v20 + v21 ), 0,
        translation.xyz, 1
    );
}

// 複数のクォータニオン+平行移動のブレンディング処理
mat4 BlendQuaternionTransforms(
        vec4 q1, vec4 t1,
        vec4 q2, vec4 t2,
        vec4 q3, vec4 t3,
        vec4 q4, vec4 t4,
        vec4 weights )
{
    return
        CreateTransformFromQuaternionTransform(q1, t1) * weights.x +
        CreateTransformFromQuaternionTransform(q2, t2) * weights.y +
        CreateTransformFromQuaternionTransform(q3, t3) * weights.z +
        CreateTransformFromQuaternionTransform(q4, t4) * weights.w;
}

//----------------------------------------------------------------------------
// 頂点テクスチャからボーン情報の抽出(フェッチ)
//======================================
mat4 CreateTransformFromBoneTexture(vec4 boneIndices, vec4 boneWeights)
{

    // ボーン番号を0~1の範囲に設定(UV座標化)する
    vec2 uv = 1.0f / BoneTextureSize;
    uv.y *= 0.5f;
    
    vec4 texCoord0 = vec4( ( 0.5f + boneIndices.x ) * uv.x, uv.y, 0, 1 );
    vec4 texCoord1 = vec4( ( 0.5f + boneIndices.y ) * uv.x, uv.y, 0, 1 );
    vec4 texCoord2 = vec4( ( 0.5f + boneIndices.z ) * uv.x, uv.y, 0, 1 );
    vec4 texCoord3 = vec4( ( 0.5f + boneIndices.w ) * uv.x, uv.y, 0, 1 );

    // テクスチャから回転部分の取り出し(フェッチ)
    vec4 q1 = texture2D( BoneRotationTexture, texCoord0.st );
    vec4 q2 = texture2D( BoneRotationTexture, texCoord1.st );
    vec4 q3 = texture2D( BoneRotationTexture, texCoord2.st );
    vec4 q4 = texture2D( BoneRotationTexture, texCoord3.st );

    // テクスチャから平行移動部分の取り出し(フェッチ)
    vec4 t1 = texture2D( BoneTranslationTexture, texCoord0.st );
    vec4 t2 = texture2D( BoneTranslationTexture, texCoord1.st );
    vec4 t3 = texture2D( BoneTranslationTexture, texCoord2.st );
    vec4 t4 = texture2D( BoneTranslationTexture, texCoord3.st );
    
    return BlendQuaternionTransforms(
                    q1, t1,
                    q2, t2,
                    q3, t3,
                    q4, t4,
                    boneWeights );
}


// 頂点シェーダメイン関数

void main(void)
{
    // スキニング
    mat4 skinTransform =

            CreateTransformFromBoneTexture(BoneIndices, BoneWeights);


    gl_Position = gl_ModelViewProjectionMatrix * skinTransform * gl_Vertex;

}

 

各テクスチャから平行移動と回転のデータを抽出してボーン行列を作成します。あとは前回と同じです。

 

クォータニオンから回転行列に変換する方法はコチラを参照してみてください。座標系の違いにより若干変わりますが、やっていることは同じです。

 

ちなみにこのコードは「ひにけにXNA」様で公開されているTexSkinningSampleに同梱されているHLSLシェーダをGLSL用に修正を加えたものです。

頂点テクスチャスキニングの実行結果を動画(.swf形式)にしました。

IK・物理演算は入れてないので、動きがカタかったり足が棒になってますが正常動作です。

 

モデル情報:

頂点数:9333

面数:12083

材質:5

骨数:167

 

再生にはFlash Player必須です。

ファイルサイズの上限により、多少画質が荒くなってますがご了承ください。

 

以下のモデルはボーンが347本ありますが、頂点テクスチャ化してるので問題なく動きます。

コメントをお書きください

コメント: 4
  • #1

    AA (日曜日, 19 8月 2012 20:06)

    ボリュームの調整が出来ません。

  • #2

    ze10 (日曜日, 19 8月 2012 20:08)

    >>AA様

    はい、出来ませんよ。
    PCのマスタ音量を設定してください。

  • #3

    lailai (火曜日, 21 8月 2012 00:17)

    これは目から鱗な情報です。
    AndroidのOpenGL ES 2.0で試してみたいと思います。

  • #4

    ze10 (火曜日, 21 8月 2012 00:30)

    >>lailai様

    こんな記事を参考にしていただき、ありがとうございます。
    OpenGL ESの場合、呼び出すメソッドが変わったりするので記事のままでは実装できないと思います。ご注意ください。