2012年

6月

28日

OpenGLでGPUスキニング【行列パレット編】

前回言っていた通り、今回はOpenGLでスキンメッシュアニメーションをやります。

 

使用するラッパーライブラリは前回と同じく「OpenTK」を、シェーダは「Cgfx」がサポートされていないので「GLSL」を利用します。

 

 

ちなみに参考までにですが、私のOpenGL環境はこのようになってます。

 

 OpenGL Version:3.3.11554 3.3.11631 Compatibility Profile Context
ビデオカードのドライバを更新したら、ちょっとだけバージョン上がりました。

 

GLSL Version:3.30

Renderer(GPU)ATI Radeon HD 4600 Series

VenderATI Technologies Inc.


拡張機能は書くと長くなるので省略。

 

自分の環境でOpenGLのバージョンが幾つまで対応してるか確認したい方はコチラをどうぞ。

OpenGL Extensions Viewer

 

使い方はコチラ

OpenGL バージョンの確認方法 ~ ビデオカードのチェック

 

私の場合、OpenGL4.0以上だとサポートされてない機能がいくつかありました。3.3以下は全て完全に対応してるようです。

 

項目

GLSLについて

OpenGLシェーダの初期化&作成の手順

GLSLの組み込み変数

GLSLへスキニング情報を送る

GLSLについて

 

GLSLでは「HLSL」「Cgfx」と違い、頂点シェーダとピクセルシェーダを別ファイルに分けて使用するようになっています。頂点シェーダは「.vert」形式、ピクセルシェーダ(フラグメントシェーダ)は「.frag」形式というファイルでそれぞれ用意する必要があります。

 

ちなみに中身はどちらもC言語で構成されています。

補足

HLSL

正式名:High Level Shader Language(ハイレベル シェーダ ランゲージ)

マイクロソフト社が開発・サポートしてるC言語ベースの高水準シェーダ言語。

DirectXやXNA等のマイクロソフト製品に対応している。

 

Cg/Cgfx

正式名:C for Graphics(グラフィックスのためのC言語)

NDIVIA社が開発・サポートしてる高水準シェーダ言語。DirectX、OpenGLの双方に対応。

構文がHLSLに良く似ている理由は、HLSLの開発にNDIVIA社が協力した為らしい。

 

GLSL

正式名:OpenGL Shading Language(OpenGL シェーディング ランゲージ)

3DLabs社が開発した、C言語ベースの高水準シェーダ言語。

名前の通り、OpenGLに対応。OpenGL 2.0から標準機能として取り込まれた。

 

OpenGLシェーダの初期化&作成の手順

 

・頂点シェーダ・フラグメントシェーダの生成

・シェーダプログラムの作成

 


頂点シェーダ・フラグメントシェーダの生成


まずはそれぞれのシェーダファイルを読み取ります。ファイル読み込みにはC#の標準クラス「System.IO.StreamReader」を使います。

 

まずは頂点シェーダの作成方法から。

 

// ファイル読み込み

StreamReader read = new StreamReader("simple.vert");

string code = read.ReadToEnd();

read.Close();

 

// 頂点シェーダの作成

int vertex_shader = GL.CreateShader(ShaderType.VertexShader);

 

// ソース割り当て

GL.ShaderSource(vertex_shader, code);

 

// ソースのコンパイル

GL.CompileShader(vertex_shader);

 

// シェーダのコンパイル情報を取得

string info = GL.GetShaderInfoLog(vertex_shader);

 

// シェーダのコンパイル結果を取得

int status_code = 0;

GL.GetShader(vertex_shader, ShaderParameter.CompileStatus, out status_code);

 

// コンパイル失敗

if (status_code == (int)All.False)
    throw new Exception("頂点シェーダの作成に失敗しました.\n" + info);


次にフラグメントシェーダの作成。

ソースの割り当てからコンパイルまでは頂点シェーダと同じなので省略します。


// ファイル読み込み

StreamReader read = new StreamReader("simple.frag");

string code = read.ReadToEnd();

read.Close();

 

// フラグメントシェーダの作成

int fragment_shader = GL.CreateShader(ShaderType.FragmentShader);

 


シェーダープログラムの作成


シェーダプログラムを作成し、頂点シェーダとフラグメントシェーダを割り当てます。

 

// プログラムの作成

int program = GL.CreateProgram();

 

// 頂点シェーダの割り当て
GL.AttachShader(program, vertex_shader);

 

// フラグメントシェーダの割り当て

GL.AttachShader(program, fragment_shader);

 

// プログラムをOpenGLとリンク

GL.LinkProgram(program);

 

// プログラムのリンク情報を取得

string info = GL.GetProgramInfoLog(program);

 

// プログラムのリンク結果を取得

int status_code = 0;

GL.GetProgram(program, ProgramParameter.LinkStatus, out status_code);

 

// リンク失敗
if (status_code == (int)All.False)

    throw new Exception("プログラムリンクに失敗しました.\n" + info);


今後は描画前にGL.UseProgram(int program)を呼び出すことで、シェーダプログラムを有効化することが出来ます。

 

GLSLの組み込み変数

 

参考元:床井研究室

アプリケーションとシェーダ間、シェーダプログラム間でデータをやり取りするためにGLSLには以下の修飾子が用意されています。全てグローバル変数で有効です。

 

attribute

アプリケーションからシェーダプログラムへ渡したい変数につける修飾子。

この修飾子をつけた変数は頂点シェーダでのみアクセスが可能で、値を読み取ることが出来ます。頂点データのgl_Vertexgl_Normalがコレにあたります。

 

const

値が不変の変数につける修飾子。CやC#のconstと同じく定数定義に使用されます。

gl_MaxLightsgl_MaxTextureUnitsがコレに当たります。

 

uniform

アプリケーションからシェーダプログラムへ渡したい変数につける修飾子。

この修飾子をつけた変数は頂点シェーダ、フラグメントシェーダのいずれからでもアクセスが可能で、値を読み取ることが出来ます。gl_ModelViewProjectionMatrixgl_LightSourceがコレに当たります。

 

varying

頂点シェーダからフラグメントシェーダへ渡したい変数につける修飾子。

頂点シェーダとフラグメントシェーダの両方で変数宣言する必要があります。

この変数には、アプリケーションからアクセスすることはできません。

gl_FrontColorgl_BackColorがコレに当たります。

 

GLSLの関数や予約語について知りたい方は以下のサイトをご参照ください。

yunoの雑記帳 - GLSL

 

GLSLへスキニング情報を送る

 

・ボーンインディセス・ボーンウェイト

・行列パレット

 


ボーンインディセス・ボーンウェイト


頂点には「座標」「法線」 「UV座標」 「頂点色」の情報がありますが、スキンメッシュの場合さらに「影響するボーンの番号と重み(ボーンインディセス、ボーンウェイト)」情報が必要になります。

 

ところで、OpenGLでは頂点情報を送るための関数がいくつか用意されています。

座標 glVertexPointer

法線 glNormalPointer

UV座標 glTexCoordPointer

頂点色 glColorPointer

 

ですが、ボーンウェイトとボーンインディセスの情報を送る関数は存在しません(OpnGLの拡張機能や、組み込み系〔OpenGL ES〕には関数が用意されてるようです)。

 

ではどうやって情報を送るのかというと、この関数を使います。

 

頂点データ受け渡し関数

public OpenTK.Graphics.OpenGL.GL.VertexAttribPointer(

                    int index,

                    int size,

                    VertexAttribPointerType type,

                    bool normalized,

                    int stride,

                    IntPtr  pointer)

 

index:アトリビュートの番号

size:1頂点辺りのデータ個数

type:変数データの型 有効値は以下の通り

GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、

GL_INT、GL_UNSIGNED_INT、GL_FLOAT、GL_HALF_FLOAT、GL_DOUBLE、

GL_FIXED、GL_INT_2_10_10_10_REV、GL_UNSIGNED_INT_2_10_10_10_REV

 

normalized:自動正規化の確認

pointer:頂点データのポインタ(配列でも可)

 

GLSLソース側でattribute修飾子をつけたユーザー変数に頂点データを渡すことが出来ます。

※この関数を使うにはGLEWが必要です。OpenTKには標準機能として備わってます。

 

修正:

そもそもGLSLとはGLEWの拡張機能の一つらしいです。GLSLをやるにはGLEWの導入は必須になるとのことです。

まず頂点シェーダ側でこのように定義しておきます。

 

simple.vert

attribute vec4 indices; // ボーンインディセス

attribute vec4 weights; // ボーンウェイト

3

// メイン関数

int main

{

     ----------省略 ----------


次にアプリケーション側の処理。シェーダで定義したユーザアトリビュート変数に番号を割り当てます。

 

アトリビュートバインド関数

public OpenTK.Graphics.OpenGL.GL.BindAttribLocation(

                    int program,

                    int index,

                    string name

) 

 

program:シェーダプログラムの番号

index:割り当てるアトリビュートの番号

name:頂点シェーダで定義したアトリビュート変数の名前

先ほどのシェーダプログラム作成のコードを一部改変しました。

アプリケーション側の処理はこんな感じです。

 

// プログラムの作成

int program = GL.CreateProgram();

 

// 頂点シェーダの割り当て
GL.AttachShader(program, vertex_shader);

 

// フラグメントシェーダの割り当て

GL.AttachShader(program, fragment_shader);

 

// Attribute1にindices変数、Attribute2にweights変数を割り当てる

GL.BindAttribLocation(program, 1, "indices");

GL.BindAttribLocation(program, 2, "weights");

 

// Attribute1とAttribute2を使えるようにする

GL.EnableVertexAttribArray(1);

GL.EnableVertexAttribArray(2);

 

// プログラムをOpenGLとリンク

GL.LinkProgram(program);

 

// プログラムのリンク情報を取得

string info = GL.GetProgramInfoLog(program);

 

// プログラムのリンク結果を取得

int status_code = 0;

GL.GetProgram(program, ProgramParameter.LinkStatus, out status_code);

 

// リンク失敗
if (status_code == (int)All.False)

    throw new Exception("プログラムリンクに失敗しました.\n" + info);

 

これでAttribute番号1からindicesへ、Attribute番号2からweightsへアクセスが出来るようになりました。これでGL.VertexAttribPointerを使って値渡しが出来るようになります。

 

このアトリビュート割り当ては、必ずプログラムをリンクする前に行ってください。リンクした後にやるとバインドに失敗します。

描画するときの頂点データの送り方。

 

// 頂点データ(既に定義されて、有効な値が設定されているとする)

Vector3[] Vertices; // 座標配列

Vector3[] Normals; // 法線配列

Vector2[] Texcoords; // UV座標配列

Vector4[] BoneIndices; // ボーンインディセス

Vector4[] BoneWeights; // ボーンウェイト

 

// シェーダプログラムの利用開始

GL.UseProgram(program);


// 頂点データの設定

GL.VertexPointer(3, VertexPointerType.Float, 0, Vertices);

GL.NormalPointer(NormalPointerType.Float, 0, Normals);

GL.TexCoordPointer(2, TexCoordPointerType.Float, 0, Texcoords);

 

/*

 * ボーンウェイトの設定

 * Attribute番号2 一頂点辺りの要素数は4つ 型はfloat 渡すときに自動正規化する

 */

GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, true, 0, Weights);


/*

 * ボーンインディセスの設定

 * Attribute番号1 一頂点辺りの要素数は4つ 型はfloat 渡すときに自動正規化しない

 */

GL.VertexAttribPointer(1, 4, VertexAttribPointerType.Float, false, 0, Indices);

 

/*

 * ボーンインディセスはこの関数でも渡せる データを整数型として送る場合に使う

 * Attribute番号1 一頂点辺りの要素数は4つ 型は符号無し整数型

 */

GL.VertexAttribIPointer(1, 4, VertexAttribIPointerType.UnsignedInt, 0, Indices);


あとはglDrawArraysglDrawElementsを呼び出せば、頂点シェーダの「indices」変数と「weights」変数に値が渡ります。

 


行列パレット


最後に、シェーダへのボーン変形行列の渡し方。

まずはアプリケーション側の処理。

 

/*

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

 *シェーダで宣言したユーザーユニフォーム変数「Transforms」へアクセスします

 */

int location = GL.GetUniformLocation(program, "Transforms")

 

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

Matrix4[] transforms = new Matrix4[model.Bones.Count];

 

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

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

{

    // まずはローカル姿勢行列を代入

    model.Bones[i].AnsoluteTransform = model.Bones[i].Transform;

 

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

    if(model.Bones[i].Parent != null)

      model.Bones[i].AbsoluteTransform *= model.Bones[i].Parent.AbsoluteTransform;

}

 

/*

 * ボーンの初期姿勢行列とワールド姿勢行列を合成して、ワールド変形行列を求める

 * 初期姿勢の状態から行列に更新が無ければ、演算結果は単位行列になる

 */

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

    transforms[i] = model.Bones[i].InvertBindPose *

                            model.Bones[i].AbsoluteTransform;

 

// 一次元float配列に変換する

float[] f_transforms = new float[transforms.Length * 16];

 

for (int i = 0; i < transforms.Length; i++)
{
    int matID = i  * 16;

   f_transforms[matID + 0] = transforms[i].M11;
   f_transforms[matID + 1] = transforms[i].M12;
   f_transforms[matID + 2] = transforms[i].M13;
   f_transforms[matID + 3] = transforms[i].M14;

   f_transforms[matID + 4] = transforms[i].M21;
   f_transforms[matID + 5] = transforms[i].M22;
   f_transforms[matID + 6] = transforms[i].M23;
   f_transforms[matID + 7] = transforms[i].M24;

   f_transforms[matID + 8] = transforms[i].M31;
   f_transforms[matID + 9] = transforms[i].M32;
   f_transforms[matID + 10] = transforms[i].M33;
   f_transforms[matID + 11] = transforms[i].M34;

   f_transforms[matID + 12] = transforms[i].M41;
   f_transforms[matID + 13] = transforms[i].M42;
   f_transforms[matID + 14] = transforms[i].M43;
   f_transforms[matID + 15] = transforms[i].M44;
}

 

// Transformsに行列データを渡す

GL.UniformMatrix4(location, transforms.Length, false, f_transforms);

 

次にシェーダ側の処理。

頂点シェーダにボーン行列の配列を宣言しておきます。

 

simple.vert

attribute vec4 indices; // ボーンインディセス

attribute vec4 weights; // ボーンウェイト

uniform mat4 Transforms[256]; // 行列パレット

4

// メイン関数

int main

{

8       // スキニング行列を求める

     mat4 skinTransform = mat4(0);

10     skinTransform += Transforms[int(indices.x)] * weights.x;

11     skinTransform += Transforms[int(indices.y)] * weights.y;

12     skinTransform += Transforms[int(indices.z)] * weights.z;

13     skinTransform += Transforms[int(indices.w)] * weights.w;

14    

15      // スキニング座標を求める

16     vec4 skinPosition = gl_ModelViewMatrix * skinTransform * gl_Vertex;

17 

18      // ピクセル座標へ変換

19     gl_Position = gl_ProjectionMatrix * skinPosition;

20

21      // スキニング法線を求める

22     vec3 normal = normalize(gl_NormalMatrix * mat3(skinTransform) * gl_Normal);

2} 


indicesはvec4型なので、インデックスとして渡すときはint型に直す必要があります。

 

あらかじめint[4]やbyte[4]で宣言しておけばその必要も無かったのですが、どうやらattribute要素をつけている場合は配列化は出来ないようでして、試してみたところエラーが出ました。

 

描画結果は以下のようになりました。

 

スキニングをかける前の状態

 

スキニングをかけた状態

左腕ボーンだけZ軸に90度回転させてみました。

終わりに

これでOpenGLでGPUスキンメッシュアニメーションが出来るようになりましたが、実際にはこの処理には一つ問題があります。

 

GPUにはレジスタというメモリ領域があり、変数を宣言する度に消費されていきます。

レジスタは32bitデータ一つにつき一つ消費されるようになっていて、変数一つに最大256個のレジスタを扱うことが出来る・・・らしいです。

 

行列は変数一つで4つのレジスタが消費されるらしいので、行列の配列要素の最大数は

256 / 4 = 64

 

となり、行列配列は最大約60個までの要素しか持つことが出来ません。

今回のプログラムでは256個宣言したので、本来ならばシェーダのコンパイルでエラーが出ます。

 

・・・のはずなのですが、私のPCでは512個くらい宣言した状態でも平常運転でした。

何故だろう?

 

まぁそれはおいといて、本来は約60個までしか扱えません。

 

例えば拡大・縮小情報を削り座標と回転量だけのアニメーションをするのであれば、Vector4(座標)とQuaternion(回転)要素だけあればいいので行列を使う場合と比べると情報量は半減し、使用できるレジスタは倍近くなります。が、これでも120個程度しか使えません。

 

今回3DモデルにMMDを利用していますが、簡単な形状のモデルデータならこの数でも何とかなりそうですが、最近のMMDモデルはボーン数が200本や300本を超えるものが出回っています(そんな大きなデータのモデルをゲームで使うのもどうかと思いますが・・・)。

 

そういうモデルを読み込みたい場合、このボーン量では困ります。

もっとボーンを増やすことは出来ないのでしょうか?

 

次回はその問題を解決できる技術「頂点テクスチャ」を使ったGPUスキニングをやります。

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