技術

Assimp v3.0 Data Structures 訳

AssimpのドキュメントのData Structuresのページを翻訳しました。

正直いうと結果的には欲しかった情報がズバッと得られたわけではないんですが、せっかく訳したので公開。
翻訳の精度は良くないのでご注意ください。
特にボーンやアニメーションの節はよく分かってません。

データ構造

Assimpライブラリは構造体の集合としてインポートされたデータを返す。

aiScene構造体はデータのルートを表し、インポートされたファイルから読み込まれた全てのノート、メッシュ、マテリアル、アニメーション、テクスチャへaiScene構造体を通してアクセス出来る。
aiScene構造体は、Assimp::Importer::ReadFile()やaiImportFile()やaiImportFileEx()の呼び出しが成功した場合に得る事ができる。
この辺の詳細については使い方のページを参照。

デフォルトでは、OpenGLが使ってるような右手座標系で3Dデータを提供する。
この座標系では+Xが右方向、-Zが奥方向、+Yが上方向を表す。
いくつかのモデリングツール、例えば3D Studio Maxなどは、この座標系(もしくはこの座標系を回転させた亜種)を使用する。
これとは対照的に、DirectXに代表されるように左手座標系を使用している環境もある。
ReadFile()関数にaiProcess_MakeLeftHandedフラグを渡すと、左手座標系用のデータが得られる。

デフォルトでは、面の向きは反時計周り(CCW)となっている。
aiProcess_FlipWindingOrderフラグを使用すると、時計回り(CW)のデータが得られる。

  x2

              x1
          x0

得られるポリゴンは文字通りポリゴン(一般的な多角形)であり、凹包(180°以上の角がある)だったり、自己交差(ポリゴンに含まれる辺が交差)してたり、非平面である(全ての角が同一平面上にない)場合があるが、Assimp内蔵の三角形分割処理(aiProcess_Triangulateフラグを使用することによって行われる)は後者2つの場合は処理しない。
(後者2つはどちらも頂点が4つ以上ある場合の話であり、多分分割はされるが”適切に”分割されないという意味だと思う。)

得られるUV座標系は、左下が原点である。

0y|1y ---------- 1x|1y
  |                |
  |                |
  |                |
0x|0y ---------- 1x|0y

aiProcess_FlipUVsフラグを使用すると、左上を原点としたUV座標が得られる。

Assimpの中で利用される行列は全て行優先である。
つまりOpenGLの行列のレイアウトと同じように、行毎にメモリに保持される。
平行移動部分を含んだ典型的な4×4行列は以下のようになる。

X1 Y1 Z1 T1
X2 Y2 Z2 T2
X3 Y3 Z3 T3
 0  0  0  1

(X1, X2, X3)はX軸に対応する基底ベクトル、(Y1, Y2, Y3)はY軸に対応する基底ベクトル、(Z1, Z2, Z3)はZ軸に対応する基底ベクトル、(T1, T2, T3)は平行移動部分である。
この行列を転置すればDirectXで使うことができる。

11.24.09: 混乱を避けるためAssimpで使用しているクォータニオンの向きをより一般的なものに変更した。
AssimpをSVNREV 502以前からアップデートした場合、新しいクォータニオンの向きに適合するようにアニメーションのローディングのためのコードを変更する必要がある。

ノード階層

ノードはシーンの中において名付けられた小さな実体で、親ノードから見て相対的な位置や向きの情報を持っている。
シーンのルートノードから始まり、全てのノードは0〜n個の子ノードを持つような階層構造が形成される。
ノードはシーンを構築するための土台を構成する。
ノードは0〜n個のメッシュを参照し、ノードはメッシュのボーンによって参照され、ノードはアニメーションのキーシーケンスによってアニメーションされ得る。
DirectXではこれらをフレームと呼び、他ではオブジェクトと呼ばれたりする。
AssimpではこれはaiNodeに対応する。

ノードは潜在的に、単数もしくは複数のメッシュを参照することができる。
メッシュはノードの中に保持されるのではなく、aiSceneの中でaiMeshの配列として保持される。
ノードはメッシュをその配列インデクスによってのみ参照する。
これは、複数のノードが(インスタンシングを簡単に実現するために)同じメッシュを参照する場合があることを意味する。
この方法によって参照されるメッシュはノードのローカル座標系に存在する。
グローバル座標系におけるメッシュの位置が必要な場合は、そのメッシュを参照するノードや、そのノードの親ノードの変換行列を全て適用する必要がある。

多くのファイルフォーマットは複雑なシーンを完全にサポートせず、一つのモデルのみに対応している。
しかし、.3ds, .x, .colladaのようなより複雑なフォーマットのシーンは
、ノードとメッシュの自由で複雑な階層を含むことが出来る。
それらを扱うために、以下の擬似コードのような再帰的なフィルタ関数を利用することをおすすめする。

void CopyNodesWithMeshes( aiNode node, SceneObject targetParent, Matrix4x4 accTransform)
{
    SceneObject parent;
    Matrix4x4 transform;

    // ノードがメッシュを持つ場合、そのためのシーンオブジェクトを生成する
    if( node.mNumMeshes > 0)
    {
        SceneObjekt newObject = new SceneObject;
        targetParent.addChild( newObject);
        // メッシュのコピー
        CopyMeshes( node, newObject);

        // この新しいオブジェクトが全ての子ノードの親になる
        parent = newObject;
        transform.SetUnity();
    } else
    {
        // メッシュを持たない場合、そのノードをスキップするが、変換行列はキープする
        parent = targetParent;
        transform = node.mTransformation * accTransform;
    }

    // 子ノード全てについて処理を続ける
    for( all node.mChildren)
        CopyNodesWithMeshes( node.mChildren[a], parent, transform);
}

この関数は、ノードがもし子ノードを持つならシーングラフの中にそのノードをコピーする。
その場合、コピーされたノードとそのメッシュをインポートするために新しいシーンオブジェクトが生成される。
そうでない場合、新たなオブジェクトは何も生成されない。
オブジェクトは親であるtargetParentに追加されるが、そのとき潜在的に変換行列によってグローバル座標系にしたがうように変換される。
この関数は、ボーンノード(それ自身はメッシュを参照せず、他のメッシュやノードのためのボーンの階層を構成するもの)をフィルタリングする場合にも役立つ。
(正直言って微妙なコードだけど、要は全ノードをトラバースしながら全頂点データをグローバル座標に変換するコードで、そんなふうにノードをトラバースするような処理は再帰を使うと楽に書けるよという事を示しているんだと思う。)

メッシュ

インポートされたシーンの全てのメッシュはaiSceneの中にaiMesh*の配列として保持される。
ノードはその配列のインデクスでメッシュを参照し、また、ノードはメッシュに座標系を提供する。
一つのメッシュは常に単一のマテリアルのみを使用する。
もしモデルが部分的に違うマテリアルを使用している場合、その部分のメッシュは他の部分のメッシュと分けられて同じノードから参照される。
ノードがメッシュを参照するのと同じ方法でメッシュはマテリアルを参照する。
マテリアルはaiSceneの中に配列として保持され、メッシュはその配列のインデクスのみを保持する。

aiMeshは一連のデータチャンネルの連なりとして定義されている。
データチャンネルはインポートされたファイルの内容によって定義される。
デフォルトでは、ファイルの中にあるメッシュの情報で表現されるデータのみを持つ。
常に存在することが保証されたチャンネルは、aiMesh::mVerticesとaiMesh::mFacesのみである。
他のチャンネルが存在するかどうかについては、NULLポインタチェックをするかaiMeshが提供するヘルパ関数を使用することで確認できる。
追加のデータチャンネルについてAssimpに計算もしくは再計算させるために、Importer::ReadFile()にいくつかのポストプロセスフラグを渡すこともできる。

現時点では、単一のaiMeshは複数の三角形やポリゴンを含む可能性がある。
ひとつの頂点は常にひとつの位置を持ち、追加で法線や接線や従法線をひとつずつ持つことが出来る。
また、0〜AI_MAX_NUMBER_OF_TEXTURECOORDS(実際の値は現在4である)個のテクスチャ座標、0〜AI_MAX_NUMBER_OF_COLOR_SETS(同じく4)個の頂点カラーを持つことが出来る。
aiMeshは追加でボーンの集合を表すaiBoneの配列を持つ場合もある。
ボーンの情報を得る方法については後に説明する。

マテリアル

マテリアルシステムのページを参照。

ボーン

メッシュはaiBone構造体としてボーンの集合を持つ場合がある。
ボーンは骨格の動きに合わせてメッシュを変形させるための手段を意味する。
それぞれのボーンは名前と、影響を与える頂点の集合を持つ。
そのオフセット行列はメッシュ空間からそのボーンのローカル空間へ変換するために必要な変換行列を定義している。

ボーンの名前を利用して、ノード階層の中から対応するノードを検索することが出来る。
そのノードは、メッシュのスケルトンを定義するボーンの集合の中に相対的に存在する。
残念な事に、メッシュ内のボーンによって使用されてないノードがある場合があるが、その場合でもそのノード(ボーン)が子ノード(子ボーン)を持つなら、そのボーンは依然としてメッシュに影響を与える。
(ここ分かりづらいけど、親ボーンが影響を与える頂点の集合を持たなくても親ボーンを動かせば子ボーンも動くので、子ボーンが頂点に影響を与えるなら結果的に親ボーンの動きで頂点が変形するという意味かなぁ。多分。)
なので、ボーンの階層を構築する際には以下の手順に従うことをおすすめする。

a) map(多分STLのmap的な意味)やそれに似たものを利用して”どのボーンノードがスケルトンに必要とされているか”を保持するためのコンテナを作成し、全てのノードについてfalseで初期化。
b) メッシュの全てのボーンに対してボーン毎に以下の処理を行う。
b1) ボーンの名前を利用してシーンの中から対応するノードを検索。
b2) そのノードについてaのコンテナにtrueをマーク。
b3) そのノードの親ノードを全て辿って同じようにtrueをマーク。
c) 以下をノード階層の中で再帰的に繰り返す。
c1) そのノードがaのコンテナにマークされていたら、それをスケルトンにコピーし、その子ノードについても同じようにチェックする。
c2) そのノードがaのコンテナにマークされていなかったら、何もせず、その子ノードについても処理しない。

理由:
変換行列のチェインを無事に維持するために全ての親ノードが必要である。
多くのファイルフォーマットやモデリングツールのスケルトンのノード階層は、メッシュノードの子またはメッシュノードの兄弟となっているが、これは必要条件ではないのでそれに依存すべきではない。
ルートノードに一番近いスケルトンのノードはスケルトンのルートであり、そこから階層をコピーを開始する。
使用されてないボーンノードはの枝はスキップ出来る。
これが上のアルゴリズムで、マークされていないボーンノードの枝全体をスキップする理由である。

あなたがボーンを扱うコードを書くときは、インポートされた階層のサブセットであるスケルトンと共にメッシュを保持するようにしなければならない。

(このあたりは非常に分かりづらい。ボーンのデータの持ち方はAssimpでは制限してないので、自力で扱いやすいように再構築してね!という意味かなぁ。どうだろう。)

アニメーション

インポートされたシーンは、0〜n個のaiAnimationを持つ。
この文脈におけるアニメーションとは、限られた時間間隔を通して階層の中のあるひとつのノードの姿勢を定義する一連のキーフレームの集合である。
この主のアニメーションはスケルトンを用いたスキニングで利用される事が多いが、他の場合にも利用出来る。

aiAnimationは間隔を持つ。
それだけではなく、ticksで与えられる全てのタイムスタンプも持つ。
正しいタイミングを得るために、全てのタイムスタンプの最小単位はaiAnimation::mTicksPerSecondでなければならない。
ファイル形式やエクスポーターの組み合わせによっては、この情報がファイルに常に格納されるわけではないことに注意。
その場合、mTicksPerSecondは情報がないことを示すために0に設定される。

aiAnimationは複数のaiNodeAnimから構成される。
各ボーンアニメーションは、ノード階層の中のそのボーンに影響を受ける一つのノードにのみ影響を与える。
このノードは、3つの個別のキーシーケンスを格納する。
ひとつは位置のためのベクトルキーシーケンス、ひとつは回転のためのクォータニオンキーシーケンス、もうひとつは拡大縮小のためのベクトルキーシーケンスである。
全ての3Dデータは、ノードの親の座標系にローカルであり、ノードの変換行列によって同じ空間に存在することを意味する。
アニメーションがその名前によって存在しないノードを参照場合もあり得るが、通常はそういうデータは作るべきではない。

そのようなアニメーションを適用するためには、メッシュの中の実在するボーンを参照するアニメーションを識別する必要がある。

a) 現在のアニメーション時間より前の一連のキーを探す。
b) オプション: その一連のキーとそれに続く一連のキーとの間で補間する。
c) 計算した位置、回転、スケールから変換行列を作成する。
d) 影響を受けるノードの変換行列に計算した変換行列を適用する。

クォータニオンへ、またはクォータニオンからの変換についてヒントが欲しければ、Matrix&Quaternion FAQを参照。
通常、全く必要ではないが、もしそうなってしまった場合は、キーのスケーリングに対数補間を利用することをおすすめする。

テクスチャ

通常、テクスチャは別のファイルに保存される。
しかしモデルファイルに直接テクスチャデータを含める事が可能なファイルフォーマットもある。
そのようなテクスチャはaiTexture構造体としてロードされる。
2つの場合がある。

1) テクスチャが圧縮されていない場合。
そのカラーデータはaiTexture::mWidth * aiTexture::mHeight個のaiTexel構造体としてaiTexture構造体に直接保持される。
各aiTexelは一つのピクセル(テクセル)を表現する。
カラーデータはunsigned RGBA8888フォーマットで保持され、そのフォーマットはDirectXやOpenGLにおいて使いやすくなっている。
(色成分の入れ替えが必要になる場合もある)
RGBA8888はよく知られ、使いやすく、最近のグラフィックAPIでネイティブサポートされているため採用された。

2) aiTexture::mHeight == 0の場合。
テクスチャは、DDSやPNGのような圧縮フォーマットで保持されている。
ここでいう圧縮とは、テクスチャデータが実際に圧縮されてなければならないという意味ではないが、テクスチャがモデルファイルから分離されたハードディスク上の別のファイルとして保存され、その情報がモデルファイルの中から見つかった状態である。
それらのテクスチャをロードするためには適切なデコーダ(libjpeg, libpng, D3DX, DevILのような)が必要である。
aiTexture::mWidthはテクスチャのサイズが何バイトであるかを示し、aiTexture::pcDataは生の画像データへのポインタであり、aiTexture::archFormatHintはゼロもしくはテクスチャフォーマットの拡張子を保持する。
これはAssimpがファイル形式を決定することが出来た場合のみ設定される。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください