2009年7月31日金曜日

骨挿入中


顔がないとちょっとホラーになってしまうので、とりあえずって感じで乗っけました。頂点数350ぐらい。超ローポリですな。
で、歩きのアクションを作成中です。
うーむ、こりゃ色気も何もないな。まいっか、今回の目的はそれじゃないし。

2009年7月29日水曜日

モデリング


ボーンアニメーションの骨格ができたので(しゃれじゃなくて)、もうちょっとましなものを動かそうとBlenderでモデリング中です。
簡単なロボットみたいなものにするつもりでしたが、ちょっと凝ってしまいました。無駄に指までつけてます。

顔は...まだです。いや、つけないかも。モデリングも楽しいんだけど、根気がいりますな。

2009年7月26日日曜日

そして時は動き出す


残すは表示処理のみです。とはいっても表示のための座標変換は既に終わっているのでそのまま出力するだけです。
void Model::draw() const {
for (unsigned int faceIdx = 0; faceIdx < numFaces; faceIdx++) {
Face *f = &faces[faceIdx];
bool useTexture;
int lastImgIdx = -1;
// テクスチャの準備
:
// 描画
glBegin(GL_QUADS);
std::vector<unsigned int>::iterator itv;
for (unsigned int i=0; i<f->numVerts; i++) {
FaceVertex* fv = &f->verts[i];
if (useTexture) glTexCoord2f(fv->u, fv->v);
Vertex *v = &vertices [fv->idx];
if (curAction) {
// アニメーション中
glVertex3f(v->anim.x, v->anim.y, v->anim.z);
} else {
// 静止中
glVertex3f(v->rest.x, v->rest.y, v->rest.z);
}
}
glEnd();
}
}

テクスチャの処理は前と変わっていないので省いています。アニメーション中かどうかで、表示する頂点を切り替えているぐらいで他は特に変わっていません。

結果は上の動画で見られます。この動きは一応想定したものです。壊れたり暴走しているわけではありませんので、念のため。

しかし、最初はなかなかうまく動かず苦労しました。考え方がおかしいのかと疑ったり、ソースを最初から見直したりしてて、結局今回使った実例で学ぶゲーム3D数学の行列クラスのソースが本に書いてあることと食い違っていることに気づきました。クォータニオンを行列に変換する部分です。

void Matrix4x3::fromQuaternion(const Quaternion &q) {
:
m11 = 1.0f - yy*q.y - zz*q.z;
m12 = xx*q.y + ww*q.z;
m13 = xx*q.z - ww*q.x;

m21 = xx*q.y - ww*q.z;
m22 = 1.0f - xx*q.x - zz*q.z;
m23 = yy*q.z + ww*q.x;

m31 = xx*q.z + ww*q.y;
m32 = yy*q.z - ww*q.x;
m33 = 1.0f - xx*q.x - yy*q.y;
:
}

問題の箇所の抜粋ですが、ここの
m13 = xx*q.z - ww*q.x;

は、書籍の本文に書かれてる内容だと、どうみても
m13 = xx*q.z - ww*q.y;

なんですよねぇ。で、直してやってみたら動きました。ヤレヤレだぜ。でもこれサポートサイトの正誤表にも載ってないんですよね。せっかくなので教えてやろうと思い辞書をひきひき英文書いてメール送ってみたら、エラーメールが返ってきましたよ。トホホ。

とにかくボーンアニメーションの基本部分ができたので、もちょっとましなモデルを作って歩かせたりしてみたいと思います。

2009年7月23日木曜日

神様、仏様、クォータニオン様

アニメーションを開始するには次のメソッドにアクション名("Walk"とか"Jump"とか...ファイルに出力したアクション名)を渡してアクションを指定します。

bool Model::startAction(std::string action) {
const Action *a = findAction(action);
if (a == NULL) return false;
curAction = (Action *)a;
timer.start();
return true;
}

で、肝心の描画ですが、モデルの描画はstep()とdraw()の2つの関数に大きく分割しています。
step()は描画前の計算を行う関数で、経過時間に応じてボーンと頂点の座標変換を行っています。そして結果をdraw()で描画します。
まず、step()の方ですが、次のような内容です。

void Model::step() {
static float fps = frames_per_sec / 1000.0f;
if (curAction != NULL) {
curAction->step((unsigned int)(timer.getTicks() * fps));
for (unsigned int idx = 0; idx < numVerts; idx++) {
Vertex *v = &vertices[idx];
if (v->boneWeights.size() > 0) {
Vector3 vec(0.0, 0.0, 0.0);
std::vector<BoneWeight *>::iterator itw;
// 頂点ブレンディング
for (itw = v->boneWeights.begin(); itw != v->boneWeights.end(); ++itw) {
BoneWeight *bw = *itw;
Vector3 tmp;
if (bw->bone->armature->isSingular) {
tmp = v->rest * bw->bone->am;
} else {
tmp = v->rest * bw->bone->armature->im * bw->bone->am * bw->bone->armature->m;
}
vec += bw->weight * tmp;
}
v->anim = vec;
} else {
v->anim = v->rest;
}
}
}
}

curActionは上のstartAction()で指定されたアクションです。アクションが指定されていない場合は何もしていません。ccurAction->step()に経過時間を渡して、頂点の座標変換に必要なボーンの変換行列を計算しています。その後、各頂点毎に重みが設定されている場合は頂点ブレンディングの計算をしています。bw->bone->armature->isSingularをみてアーマチャ行列が逆行列を持つかどうかの確認をしています。逆行列を持つ場合はそれを使ってアーマチャ座標に変換した後、ボーンの変換を適用し、再度ワールド座標に戻しています。重みが設定されていない頂点の場合はボーンの影響を受けないので変換のない通常(休止ポーズ用)の行列を使うようにしています。

ボーン行列を計算するcurAction->step()は次のようになっています。

void Model::Action::step(unsigned int frameNum) {
Matrix4x3 mat;
if (lastFrame == 0) return;
unsigned int fnum = frameNum % lastFrame;
Keyframe *prevFrame = NULL;
Keyframe *nextFrame = NULL;
std::vector<Keyframe *>::iterator itf;
// 前後のフレームを探す
for (itf = frames.begin(); itf != frames.end(); itf++) {
Keyframe *frame = *itf;
prevFrame = nextFrame;
nextFrame = frame;
if (fnum < nextFrame->num) {
break;
}
}
std::vector<ActionKey *>::iterator itk;
// 前のキーフレーム
if (prevFrame != NULL) {
for (itk = prevFrame->keys.begin(); itk != prevFrame->keys.end(); ++itk) {
ActionKey *key = *itk;
Bone* bone = key->bone;
bone->curFrameNum = frameNum;
bone->prevFrameNum = prevFrame->num;
bone->prevLoc = &key->loc;
bone->prevQ = &key->quat;
}
}
// 次のキーフレーム
for (itk = nextFrame->keys.begin(); itk != nextFrame->keys.end(); ++itk) {
ActionKey *key = *itk;
Bone* bone = key->bone;
// 線形補間
if (prevFrame != NULL && bone->curFrameNum == frameNum) {
float t = (float)(fnum - bone->prevFrameNum) / (float)(nextFrame->num - bone->prevFrameNum);
Quaternion q = slerp(*bone->prevQ, key->quat, t);
mat.fromQuaternion(q);
Vector3 loc = (*bone->prevLoc - key->loc) * t;
mat.setTranslation(loc);
} else {
mat.fromQuaternion(key->quat);
}
bone->setupMatrix(mat);
}
}

うーむ長いですね...無理に読まなくていいですが、やってることは、渡された経過時間に相当する前後のフレームを探して、クォータニオンの球面線形補完を使い、ボーンの回転量を補間して求めているだけです。球面線形補完という何やら難しげな言葉がでてきましたが、要は2つのクォータニオンと経過時間を元に補間をするための計算で、実例で学ぶゲーム3D数学で提供されていたslerp()という関数をそのまま使っています。この関数を使いたいがためにクォータニオンを使ったといっても過言ではありません。計算が終わったらその結果をbone->setupMatrix()に渡してボーンに行列を設定しています。(結局行き着く先は行列なんですね...)
bone->setupMatrix()は次のようになっています。

void Bone::setupMatrix(Matrix4x3& mat) {
if (isSingular) {
am = mat;
} else {
am = im * mat * m;
}
if (parent) am = am * parent->am;
}

isSingularで逆行列を持つかどうかを確認して、逆行列を持つ場合はそれを使って一旦ボーン座標に変換してから引数の行列を適用しています。逆行列を持たない場合は引数の行列をそのまま代入しています。そして親のボーンがある場合は親のボーンの行列も掛けています。これを忘れてはいけません。また行列の計算全般に関することですが行列の掛け算は可換ではないので掛ける向きも重要です。

といったところで、残すはdraw()の実装のみですが、長くなったので次回に。

2009年7月20日月曜日

さらに読み込むべし

次はアクションとキーフレームの読み込みです。


Model::Action* Model::readActionLine(std::istream& in) {
std::string name;
in >> name;
Action *a = new Action(name);
actions.push_back(a);
return a;
}


アクションの行にはアクション名しか出力してなかったので、単純にアクション名を読み込んでアクションクラスのインスタンスを生成し、actionsというベクタに追加しています。

キーフレームの読み込みはちょっと長いです。

void Model::readKeyframeLine(std::istream& in, Action& action) {
unsigned int num;
std::string boneName;
float x, y, z;
float qw, qx, qy, qz;

if (armature == NULL) {
std::cerr << "found keyframe line without previous armature line" << std::endl;
return;
}
in >> num;
Keyframe *f = new Keyframe(num);
do {
in >> boneName >> x >> y >> z >> qw >> qx >> qy >> qz;
if (in.fail()) {
break;
} else {
Bone *bone = armature->findBone(boneName);
if (bone != NULL) {
ActionKey *key = new ActionKey();
key->bone = bone;
key->loc.x = x;
key->loc.y = y;
key->loc.z = z;
key->quat.w = qw;
key->quat.x = qx;
key->quat.y = qy;
key->quat.z = qz;
f->addKey(key);
} else {
std::cerr << "not found bone named " << boneName << std::endl;
}
}
} while (true);
action.addKeyframe(f);
}

引数で渡されるActionは直前に生成したアクションクラスのインスタンスです。読み込んだキーフレームはこのアクションのキーフレームとして追加します。
キーフレームクラスのインスタンスを生成し、それにキー情報(ActionKeyクラスのインスタンス)をボーンの数だけ追加しています。キー情報には、関連するボーンのポインタと平行移動の座標とクォータニオンを保持させています。関連するボーンはアーマチャのfindBone()でボーン名から取得しています。key->locはVector3クラスのインスタンス、key->quatがQuaternionクラスのインスタンスです。それぞれ実例で学ぶゲーム3D数学で使われているソースを利用しています。キーフレームの数だけこの読み込みが行われることになります。

そして、頂点ブレンディング用の重みの読み込みです。

void MyModel::readWeightLine(std::istream& in, Bone& bone) {
unsigned int idx;
float weight;
do {
in >> idx >> weight;
if (in.fail()) {
break;
} else {
if (idx < numVerts) {
Vertex* v = &vertices[idx];
v->addBoneWeight(&bone, weight);
} else {
std::cerr << "vertex idx is out out range for weight. idx=" << idx << std::endl;
}
}
} while (true);
}

引数で対象となるボーンを渡しています。頂点番号と重みのペアの繰り返しを読み込んで、該当する頂点にボーンと重みを追加しています。最終的に頂点に追加されたボーンの数だけ変換行列をブレンドしてやることになります。

と、こんな感じで読み込みが終わったので、次はいよいよ表示、つまりアニメーションの実装です。

2009年7月19日日曜日

読み込みまくるべし

Blenderで出力したファイルをC++で読み込み表示することになるわけですが、ただ読み込むだけではアニメーションさせることはできません。出力したファイルには行列やクォータニオンの値が含まれており、それらを使った計算が必要になります。これを手作りで実装するのは我ながらさすがに無謀なのでライブラリを使うことにします。ちょうどこの本(実例で学ぶゲーム3D数学)で使われているソースが使えそうだったのでそのまま使わせてもらいます。Vector3とかMatrix4x3とかQuaternionとかまさにうってつけのクラスが揃っています。

まず、アーマチャの読み込みですが次のような感じです。

void Model::readArmatureLine(std::istream& in) {
std::string name;

in >> name;
if (in.fail()) {
std::cerr << "failed in reading armature line" << std::endl;
return;
}
armature = new Armature(name);
Armature *a = armature;
in >> a->m.m11 >> a->m.m12 >> a->m.m13
>> a->m.m21 >> a->m.m22 >> a->m.m23
>> a->m.m31 >> a->m.m32 >> a->m.m33
>> a->m.tx >> a->m.ty >> a->m.tz;

if (determinant(a->m) > 0.00001f) {
a->isSingular = false;
a->im = inverse(a->>m);
} else {
a->isSingular = true;
a->im.identity();
}
}

Armatureクラスのインスタンスを生成して、読み込んだアーマチャ行列の各要素を取り込んでいます。a->mとa->imはそれぞれMatrix4x3クラスのインスタンスでimはmの逆行列です。Blenderで出力する時に逆行列は出力しなかったのでここで求めるようにしています。determinant()というのは上の本のソースで提供されている関数で行列の行列式を求める関数です。行列式が0の行列は逆行列を持ちません。そのため行列式の値を調べて逆行列の計算をしています。inverse()というのが逆行列を求める関数です。

ボーンの読み込みは次のような感じです。

Model::Bone* Model::readBoneLine(std::istream& in) {
static Bone *lastBone = NULL;
unsigned int level;

if (armature == NULL) {
std::cerr << "found bone line without previous armature line" << std::endl;
return NULL;
}
std::string name;
in >> level >> name;
Bone *b = new Bone(name);
in >> b->m.m11 >> b->m.m12 >> b->m.m13
>> b->m.m21 >> b->m.m22 >> b->m.m23
>> b->m.m31 >> b->m.m32 >> b->m.m33
>> b->m.tx >> b->m.ty >> b->m.tz;

if (determinant(b->m) > 0.00001f) {
b->isSingular = false;
b->im = inverse(b->m);
} else {
b->isSingular = true;
b->im.identity();
}

armature->addBone(lastBone, level, b);
lastBone = b;
return b;
}

アーマチャの読み込みと似たような感じですが、ボーンはアーマチャに属する(ぶら下がる)形になるため、ボーンの前にアーマチャが生成されていることが前提となります。そのためarmatureがNULLでないかどうか確認しています。そして読み込んだボーン行列の各要素をボーンに取り込んでいます。行列式の値を確認して逆行列を求めているところはアーマチャと同様です。そのあとボーンをアーマチャに追加しています。lastBoneとlevelは追加すべきボーンの階層位置を判断するためにaddBone()で使います。
Armature::addBone()の中身は次のような感じです。

void addBone(Bone *relBone, unsigned int level, Bone *bone) {
if (level == 0) {
if (root != NULL) {
delete root;
}
root = bone;
root->armature = this;
root->parent = NULL;
} else {
if (root == NULL) {
std::cerr << "no root bone set";
return;
}
assert(relBone != NULL);
Bone *parent = relBone->findAncestor(level-1);
if (parent != NULL) {
bone->armature = this;
parent->addChild(bone);
} else {
std::cerr << "not found parent of level=" << level-1;
}
}
}

level=0の場合はルートボーンと判断しています(Blenderでの作成時にルートボーンは1つだけという前提にしています)。それ以外は引数で渡されたrelBone(直前に読んだボーン)とlevelを元に自分がぶら下がるべきボーン(親)がどれになるかを判断して、そのボーンの子ボーンとして追加しています。findAncestor(level-1)は指定した階層になるまで遡って先祖を探すボーンのメンバ関数です。次のような内容です。

Bone* findAncestor(unsigned int level) const {
unsigned int curLevel = getLevel();
const Bone *bone = this;
while (bone != NULL && level != curLevel) {
bone = bone->parent;
--curLevel;
}
return (Bone *)bone;
}


...と長くなったので続きはまた。ちょっとややこしいですが、実装方法は人それぞれなので真似しなくていいです。^_^;
何にしても、もう少しで動き始めます。

2009年7月16日木曜日

頂点ブレンディング

ボーンアニメーションのために、もう1つ出力しないといけない情報が残っていました。
頂点ブレンディングです。頂点ブレンディングとは簡単に言ってしまうと、面の曲がりを滑らかにするための手法ということでしょうか。
頂点ブレンディングを使わずに単純に各頂点をボーンに割り当てると、ボーンとボーンの間の頂点が回転によってお互いに行き違いになって見た目がくしゃっとなってしまいます。(上の画像の左側) 実際、ボーンとボーンの間の頂点は、どちらか一方のボーンだけじゃなく両方のボーンの影響を加味して変換してやる必要があるのです。
そのために、頂点ブレンディングを使います。例えば、A、B2つのボーンがあるとした場合、A、Bの間にあるような頂点は、A、B両方のそれぞれから影響を受けるような設定にします。具体的には
Aの変換 x 0.5 + Bの変換 x 0.5
というふうに、どのボーンからどれだけ影響を受けるかという重みを各頂点に設定することになります。また0.5ずつじゃなく、
Aの変換 x 0.7 + Bの変換 x 0.3
などとすることもできます。各重みの合計が1.0(=100%)になるようにする必要があります。また、A、B、Cの3つのボーンの影響を受ける頂点は、
Aの変換 x 0.6 + Bの変換 x 0.3 + Cの変換 x 0.1
などとします。影響を受けるボーンが4つ以上でも同様です。重みを加味してやると上の画像の右側のような曲がり方になります。曲がり方は各ボーンへの重みを変えることで調整することができます。

重みを出力する行は次のようなフォーマットです。
W 頂点番号1 重み1 頂点番号2 重み2 頂点番号3 重み3

識別子のWはWeightのWです。ボーン単位でそのボーンに重みが設定されている頂点の番号と重みを出力します。
以前つくったボーンの情報(B行)を出力するwriteBone()関数の中で以下のようにW行を出力するようにしました。

def writeBone(out, mesh, bone, level):
:
grp = mesh.getVertsFromGroup(bone.name, True)
if len(grp) > 0:
out.write("W")
for i in grp:
out.write(" %d %f" % (i[0], i[1]))
out.write("\n")
if bone.hasChildren():
for b in bone.children:
writeBone(out, mesh, b, level+1)

meshのgetVertsFromGoup()で頂点と重みのリストを取得して出力しています。

これでひとまず必要な情報は全てファイルに出力したので、次はC++での読み込みになります。

2009年7月13日月曜日

キーフレーム


前回出力したボーンの情報では、アーマチャ座標からボーン座標へ変換する行列は書き出していましたが、アニメーションのための回転行列は書き出していませんでした。



V x Am(-1) x Fm(-1) x Fr x Fm x Um(-1) x Ur x Um x Am
のFrやUrの部分です。

この回転行列(場合によっては移動もあり)こそが実際のアニメーションの部分になるんですが、ここは時間の経過に伴い変化する部分でもありますので、ボーンの情報とは別に出力します。
Blenderのデフォルトでは、25fpsでアニメーションが作れます。これは変えられるんですが今回はこれをこのまま使います。つまり1秒間に25回描画することになります。
4秒のアニメーションだと100回の描画になります。といっても各フレーム全てでモデルにポーズをつけてやる必要はなく、動きの節目となるようなフレームだけポーズを作成して、そのフレーム間はコンピーュータに補間させてやります。これはキーフレームアニメーションと呼ばれています。
例えば振り子の場合、それぞれ最も左右に振れた位置の状態だけをキーフレームとして作成してやればその他のフレームでの振り子の位置は自動的に計算されます。この計算の方法も差を均等に割るだけの線形補間や、曲線を使った補間等あるのですが、今回は線形補間だけにしておきます(振り子で線形補間だと動き的におかしいですが...)。0℃の回転位置だったボーンが10フレーム後に30℃回転したとすると、5フレーム目では15℃と計算されます。

Blenderではキーフレームはアクション単位で作成されます。アクションというのは「歩く」とか「ジャンプ」といった、一連の動作を表す単位です。アクションを切り替えることで、それぞれのアニメーションを実行することができます。
このアクションの情報は次のフォーマットで書き出します。
A アクション名

アクション名だけです。前回、アーマチャの行にAではなくSを使ったのは、このアクション行にAを使いたかったからです。

次にキーフレームですが、キーフレームの情報として書き出す必要があるのは、各キーのフレーム番号と対象となるボーンの名前と、その時の変換行列です。
ですが、ボーンの回転については、行列よりクォータニオンを使った方が便利なのです。何が便利なのかというのは、実例で学ぶゲーム3D数学という本に詳しく書かれているんですが、簡単に言うと、クォータニオンを使うとキーフレーム間の補間が行列よりも簡単で計算も早く、ジンバルロックという厄介な問題も避けられます。そしてクォータニオンの表現には4つの数値しか使いません(行列で3Dの回転を表現するには9つの数値が必要です)。キーフレームによる回転アニメーションに非常に適した表現方法なのです。そして、Blenderでもこのクォータニオンを提供してくれています。なので回転についてはクォータニオンの4つの数値を書き出すようにします。但しクォータニオンは回転だけを表現しますので、平行移動の部分については別途書き出してやります。結果的に次のようなフォーマットになります。
K フレーム番号 ボーン名1 平行移動1(x座標 y座標 z座標) クォータニオン1(w x y z) ボーン名2 平行移動2(x座標 y座標 z座標) クォータニオン(w x y z) ...

識別子はキーフレームのKです。キーフレームのフレーム毎にこの行を出力しますが、複数のボーンが対象になるので、そのボーンの数だけ同じ行に変換情報(平行移動とクォータニオン)を出力します。
各キーフレームでのボーンの座標(平行移動とクォータニオン)は、ポーズボーン(ボーンとは別のオブジェクトです)というオブジェクトで提供されているのでこれを使います。

Pythonのコードは次のようになります。

actions = Armature.NLA.GetActions() # list of actions
:
for a in actions:
act = actions[a]
out.write("A %s\n" % a)
frames = act.getFrameNumbers()
for f in frames: # repeat each key frame
writeKeyframe(out, f, arm_ob.getPose())

アクションのリストを取得して、各アクションについて処理しています。名前を出力した後、そのアクションのキーフレームをwriteKeyframe()で出力しています。
writeKeyframe()は次のようになります。

def writeKeyframe(out, frame, pose):
out.write("K %d" % frame)
Blender.Set('curframe', frame)
pbones = pose.bones.values()
for pb in pbones:
loc = pb.loc # location of this pose bone
quat = pb.quat # quaternion of this pose bone
out.write(" %s %f %f %f %f %f %f %f" % (pb.name, loc.x, loc.y, loc.z, quat.w, quat.x, quat.y, quat.z))
out.write("\n")

渡されたフレーム番号をBlenderの現在のフレーム番号に設定して、ポーズボーンを取得しています。ポーズボーンから平行移動の座標とクォータニオンが取得できるのでそれを出力しています。
わざわざ現在のフレーム番号を設定しなくてもそのフレームのポーズを取得できる方法があるのかもしれませんが、ちょっと分かりませんでした。なので、処理の頭で、

curFrame = Blender.Get('curframe')
と、念のため、処理前のフレーム番号を保存しておいて、処理の終了時に、

Blender.Set('curframe', curFrame)
と、戻すようにしてます。

なんだか小難しい話が続きますが、ボーンアニメーションを扱おうとするとしようがないです。自分の備忘録としても書いてますので...
もしかしたらいろいろ間違ったことを書いてるかもしれませんがその場合はご容赦ください。ついでに指摘して頂けると有り難いです。

続きはまた。

2009年7月12日日曜日

ボーン、ボーン、ボーン


いよいよ、ボーンアニメーションに挑戦です。
基本的な考え方は、ボーンに関連づけられた頂点をボーンの動き(主に回転)に追随させるということです。
しかし、各頂点に単純にボーンの行列を掛ければいいという話でもありません。各頂点の座標系(モデル座標)とボーンの座標系(ボーン座標)が異なっているため、各頂点の座標を一旦、ボーンの座標に変換した後、ボーンの行列を掛け、その後またモデル座標に戻してやる必要があります。モデル座標からボーン座標へ変換するには、モデル座標を一旦ワールド座標に変換し、それをボーン座標に変換する必要があります。結局全体的には次のような流れになります。
モデル座標 → ワールド座標変換 → ボーン座標変換 → 移動/回転 → ワールド座標変換 → モデル座標変換

なかなか大変ですね。
今までファイルに書き込んでいた頂点座標はモデル座標だったんですが、計算が増えてしまうので、モデルを作る時にモデルの初期位置を(0, 0)、回転もしない状態でつくるように制限を設けることにします。こうすれば、ファイルに書き込んだ座標がそのままワールド座標として考えることができます。すると次のように考えることができます。
モデル座標(=ワールド座標) → ボーン座標変換 → 移動/回転 → モデル座標変換


次にボーン座標についてですが、Blenderでボーンを作成するには、まずアーマチャというものを作成する必要があります。アーマチャはボーンを保持する入れ物のようなものです。アーマチャも位置や回転の情報をもっており、各ボーンはこのアーマチャ相対で位置や回転の情報をもっています。つまり、ボーン座標を求めるには、まずアーマチャ座標に変換した後、ボーン座標に変換してやる必要があります。う〜ん、分かりにくいですね。
モデル座標(=ワールド座標) → アーマチャ座標変換 → ボーン座標変換 → ボーン行列(移動/回転)適用 → アーマチャ座標変換 → モデル座標変換


さらに面倒くさいのは、ボーンは階層を構成するということです。図のモデルを腕に例えて左のボーンが上腕、それに繋がる右のボーンが前腕とします。上腕に関連づけられた頂点の座標変換は単純にボーン座標で上腕のボーンの変換行列を掛ければよいですが、前腕に関連づけられた頂点の場合は、まず前腕のボーンの変換行列を掛けたあと、さらに上腕のボーンの行列を掛けてやる必要があります。前腕は前腕だけの動きだけでなく、上腕の動きの影響もうけるからです。また、前腕の行列を掛けて上腕の行列を掛けるという順番も大事です。これが逆だとうまくいきません。行列同士の掛け算は可換ではないのです。階層がさらに深くなると最下層から上の層に向かって行列の掛け算を繰り返すことになります。例えば前腕の右に手のひらのボーンがあるとすると、手のひらのボーンに関連づけられた頂点の変換を求めるには、手のひらボーンの変換 × 前腕ボーンの変換 × 上腕ボーンの変換となります。
整理すると、前腕に関連付けられた頂点の座標を、ボーンの動きに追随させるには以下の計算が必要になります。
モデル座標(=ワールド座標) → アーマチャ座標変換 → 前腕の行列変換 × 上腕の行列変換 → モデル座標変換


モデル座標からアーマチャ座標への変換は、頂点座標にアーマチャの行列の逆行列を掛けてやれば求められます。同様にアーマチャ座標からボーン座標への変換はボーンの行列の逆行列を掛けてやります。変換が終わった座標は、ボーン行列を掛けてアーマチャ座標へ戻した後、アーマチャ座標を掛けてモデル座標へ戻してやります。

頂点座標をV、アーマチャの行列をAm、上腕の行列をUm、上腕の回転行列をUr、前腕の行列をFm、前腕の回転行列をFr とすると、前腕に関連づけられた頂点をボーン回転に追随させるための計算式は次のようになります。
V x Am(-1) x Fm(-1) x Fr x Fm x Um(-1) x Ur x Um x Am

※(-1)のついたものは逆行列です。

Pythonでアーマチャとボーンそれぞれの行列を得られるのでそれを書き出してやります。逆行列もBlenderが提供している、Mathutils.Matrixのinvert()で求められるんですが、今回はC++側で計算することにしました。なのでここでファイルに書き出すのは正行列だけです。アーマチャの情報は次のようなフォーマットで書き出します。
S アーマチャ名 アーマチャ行列

SはスケルトンのSです。アーマチャのAでもよかったんですが、ちょっとAは他で使いたいので...
ボーンのフォーマットは次のようになります。
B レベル ボーン名 ボーン行列

BはボーンのBです。レベルはボーンの階層を示す番号です。0はルートを示し、1,2と増えるごとに深い階層を示します。

実際のコードは次のようになります。

if obj.parentType == Object.ParentTypes['ARMATURE']:
# armature
arm = arms[obj.getParent().name]
arm_ob = bpy.data.objects[arm.name]
m = arm_ob.mat # get matrix
out.write("S %s %f %f %f %f %f %f %f %f %f %f %f %f\n"
% (arm.name,
m[0][0], m[0][1], m[0][2],
m[1][0], m[1][1], m[1][2],
m[2][0], m[2][1], m[2][2],
m[3][0], m[3][1], m[3][2]))
level = 0
for k in arm.bones.keys():
b = arm.bones[k]
if not b.hasParent():
# root bone
writeBone(out, mesh, b, level)
:

アーマチャオブジェクトの名前と行列を書き出しています。行列は4列目は不要なので4x3の形式で書き出しました。
次にボーンですが、ボーンはアーマチャにぶら下がる形になっているのでそのボーンのルートから始めて関数を再起的に呼び出してレベルをインクリメントしながら書き出しています。
writeBone()は次のような関数です。

def writeBone(out, mesh, bone, level):
m = bone.matrix['ARMATURESPACE']
out.write("B %d %s %f %f %f %f %f %f %f %f %f %f %f %f\n"
% (level, bone.name,
m[0][0], m[0][1], m[0][2],
m[1][0], m[1][1], m[1][2],
m[2][0], m[2][1], m[2][2],
m[3][0], m[3][1], m[3][2]))
if bone.hasChildren():
for b in bone.children:
writeBone(out, mesh, b, level+1)

m = bone.matrix['ARMATURESPACE']で、アーマチャ座標相対でボーンの行列が取得できます。アーマチャと同じように4x3の行列で書き出しています。ボーンにぶら下がるボーンがあればさらにwriteBone()を再帰呼び出しします。

今日はひとまずここまで。一口にボーンアニメーションと言ってもやっぱり大変ですね。多分読んでも分かり難いと思います。実はある程度動くところまで出来てはいるのですが、説明が長くなりそうなので小出しにしていきたいと思います。

2009年7月10日金曜日

ちょっと修正


前回のソースをちょっとだけ修正しました。
面単位で毎回テクスチャの設定をするのはあんまりなので、下のように修正しました。






void Model::draw() const {
int lastImgIdx = -1;
for (unsigned int faceIdx = 0; faceIdx < numFaces; faceIdx++) {
Face *f = &faces[faceIdx];
bool useTexture;
// テクスチャの準備
if (f->imgIdx >= 0) {
glEnable(GL_TEXTURE_2D);
if (f->imgIdx != lastImgIdx) {
const Image *img = images.at(f->imgIdx);
img->setup();
lastImgIdx = f->imgIdx;
}
useTexture = true;
} else {
glDisable(GL_TEXTURE_2D);
lastImgIdx = -1;
useTexture = false;
}
:

lastImageIdxというフラグを使って面が参照している画像が変わった場合だけテクスチャを切り替えるようにしました。画像が1つだけならimg->setup()も1回しか呼ばれないことになります。あまりスマートじゃないですがとりあえず...

それから、前回のソースにまずいところがありました。ilLoadImage()で画像を読み込んで、glTexImage2D()に、ilGetData()から得られるポインタをそのまま渡していましたが、画像が複数になるとこれではうまくいきません。(後からロードした画像データで最初の画像データが無効になる?) そのため、ilLoadImage()で読み込んだ画像を、自分で確保した領域にコピーしておく必要があります。
Imageクラスに、

unsigned char *data;

というメンバ変数を追加し、

bool loadImage(const std::string& path) {
bool res;
ILuint image;
ilGenImages(1, &image);
ilBindImage(image);
if (ilLoadImage((wchar_t *)path.c_str())) {
width = ilGetInteger(IL_IMAGE_WIDTH);
height = ilGetInteger(IL_IMAGE_HEIGHT);
if(ilGetInteger(IL_IMAGE_ORIGIN) != IL_ORIGIN_LOWER_LEFT) {
iluFlipImage();
}
data = new unsigned char[width * height * 4];
ilCopyPixels(0, 0, 0, width, height, 1, IL_RGBA, IL_UNSIGNED_BYTE, data);
res = true;
} else {
ILenum err = ilGetError();
std::cerr << iluErrorString(err) << std::endl;
res = false;
}
ilDeleteImage(image);
return res;
}
と、ilCopyPixels()でコピーするようにしました。また、ilGenImages() で取得したimage(識別子)は画像のコピーが終わってしまえば不要なので、関数内で生成し、終わったら削除するようにしました。glTexImage2D()の呼び出しは次のように、

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

dataを直接渡しています。

上の画像は2枚のテクスチャを別の面に割り当てたものです。

2009年7月7日火曜日

テクスチャマッピン(*^ー゚)b グー...古いか..

昨日の続きです。

頂点や面についてはモデル(O)の行にそれぞれの数を出力して読み込む側で配列のサイズがわかるようにしてたんですが、画像ファイルについては、面の処理をしながら数えているので、モデル行に数が出力できません。一旦画像ファイルの数を数えてからモデル行を出力してもいいんですが、コードが冗長になってしまうので、C++の読み込む側で、配列じゃなくstd::vectorに読みながらつっこむようにしました。vectorを使えば予めサイズを知る必要がありません。配列を使ったり、vectorを使ったり一貫性が無い感じもしますが、まぁ、頂点や面は数が多いので速度重視で配列ということで...

I(画像)行の読み込みは次のような感じです。

/**
* 画像行の読み込み
*/
void Model::readImageLine(std::istream& in) {
std::string file;
in >> file;
if (in.fail()) {
std::cerr << "failed in reading image line" << std::endl;
return;
}
Image *img = new Image(file);
images.push_back(img);
}

imagesというのが画像の情報を保持するvectorです。Imageというのは画像の読み込み等を行うためのクラスです。次のようなメンバ関数をもっています。

bool Image::loadImage(const std::string& path) {
ilBindImage(image);
if (ilLoadImage(path.c_str())) {
width = ilGetInteger(IL_IMAGE_WIDTH);
height = ilGetInteger(IL_IMAGE_HEIGHT);
if(ilGetInteger(IL_IMAGE_ORIGIN) != IL_ORIGIN_LOWER_LEFT) {
iluFlipImage();
}
return true;
} else {
ILenum err = ilGetError();
std::cerr << iluErrorString(err) << std::endl;
return false;
}
}

imageはDevILのilGenImages()で確保した識別名です。これをバインドしてから、ilLoadImage()でファイルから読み込んでいます。自分の環境で読み込んだ画像が上下逆だったので、状態をみて、iluFlipImage()で反転させるようにしています。

実際に画像を出力する部分は次のような感じです。

void Model::draw() const {
// 面の数だけ繰り返し
for (unsigned int faceIdx = 0; faceIdx < numFaces; faceIdx++) {
Face *f = &faces[faceIdx];
bool useTexture;
// テクスチャの準備
if (f->imgIdx >= 0) {
glEnable(GL_TEXTURE_2D);
const Image *img = images.at(f->imgIdx);
img->setup();
useTexture = true;
} else {
glDisable(GL_TEXTURE_2D);
useTexture = false;
}
// 描画
if (f->mtlIdx >= 0) {
const Material *mtl = &materials[f->mtlIdx];
glColor3f(mtl->r, mtl->g, mtl->b);
}
glBegin(GL_QUADS);
std::vector<unsigned int>::iterator itv;
for (unsigned int i=0; i<f->numVerts; i++) {
FaceVertex* fv = &f->verts[i];
if (useTexture) glTexCoord2f(fv->u, fv->v);
Vertex *v = &vertices [fv->idx];
glVertex3f(v->x, v->y, v->z);
}
glEnd();
}
}

ちょっと長いですね。面に画像が割り当てられているかどうかに応じて、テクスチャをオンオフしてます。通常、あまり細かく面毎に画像を割り当てたりしない思うので、もう少し効率的にできると思います。要改善です。テクスチャ座標は面のFaceVertexという構造体に保持するようにして、この座標をglTexCood()に渡しています。

Image::setup()ではテクスチャの設定をしています。

void Image::setup() const {
ilBindImage(image);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glBindTexture(GL_TEXTURE_2D, texName);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, ilGetData());
}

ilGetData()で読み込んだ画像のポインタを得られるのでこれをglTexImage2D()に渡しています。

..ざっとですが、こんな感じです。画像とか扱いだすとやはりコード量も増えてきますね。乱雑な感じなのでちょっと整理せねば。
で、その後はいよいよボーンアニメーションに突入(の予定)です。

UVマッピングとか



Blnderで設定したテクスチャの情報もファイルに書き出すようにしました。

OpenGLで画像をテクスチャとして出力する場合は、テクスチャ座標(頂点と画像を位置合わせするための座標)が必要になるんですが、Blenderではこの座標は頂点ではなく面(Face)に格納されているようです。頂点を共有する隣り合った面で異なるテクスチャを使う場合のためでしょうか。ともかく、フェースの行を次のようなフォーマットにして、テクスチャ座標も出力するようにしました。
F マテリアル番号 頂点数 [頂点番号1 頂点番号2 頂点番号3 ...] [画像ファイル番号] [UV座標1 UV座標2 UV座標3 ...]

UV座標というのがテクスチャ座標のことです。その前の画像ファイル番号は、テクスチャで使う画像ファイルの番号です。画像ファイルは、次のようなI(ImageのI)という識別子の行に出力するようにしました。
I 画像ファイル名

この行の出現回数だけ画像番号が0, 1, 2...と増えていきます。今回は画像1つだけですが。

Pythonスクリプトは次のような感じになります。

images = [] # list of texture file name
:
# repeat each faces
for f in mesh.faces:
out.write("F %d %d" % (f.mat, len(f.verts)))
for v in f.verts:
out.write(" %d" % v.index)
if mesh.faceUV and f.image:
texName = os.path.split(f.image.filename)[1]
try:
texNum = images.index(texName)
except ValueError:
images.append(texName)
texNum = len(images) - 1
out.write(" %d" % texNum)
for uv in f.uv:
out.write(" %f %f" % (uv[0], uv[1]))
out.write("\n")

# texture image files
for img in images:
out.write("I %s\n" % img)

画像ファイル名は、アプリ側の場所とディレクトリが異なるのでパスは省いてファイル名だけを出力するようにしました。画像ファイル名も面(Face)から取得できるので、各面を処理しながら違う画像が使われていたらそれをリストに格納していき、面の処理が終わったあとにリストに溜めたファイル名を出力するようにしています。うーん、もっとスマートなやり方があるかもしれません。

で、モデルは簡単なんですがサイコロ(もどき)です。ちょっとめんどくさかったので....
BlenderでUVマッピングして保存した画像に、Gimpで数字を描いてやりました。
角の線がちゃんと合ってないんですが、まぁ練習ということで...

出力したデータは次のような感じです。
O Dice 1 8 6
M 0.800000 0.800000 0.800000
V 1.998771 1.000000 -1.000000
V 1.998771 -1.000000 -1.000000
V -0.001229 -1.000000 -1.000000
V -0.001229 1.000000 -1.000000
V 1.998771 0.999999 1.000000
V 1.998770 -1.000001 1.000000
V -0.001229 -1.000000 1.000000
V -0.001229 1.000000 1.000000
F 0 4 0 1 2 3 0 0.673504 0.005739 0.673504 0.255311 0.336261 0.255311 0.336261 0.005739
F 0 4 4 7 6 5 0 0.673504 0.754455 0.336261 0.754455 0.336261 0.504883 0.673504 0.504883
F 0 4 0 4 5 1 0 1.010747 0.754455 0.673504 0.754455 0.673504 0.504883 1.010747 0.504883
F 0 4 1 5 6 2 0 0.673504 0.255311 0.673504 0.504883 0.336261 0.504883 0.336261 0.255311
F 0 4 2 6 7 3 0 -0.000982 0.504883 0.336261 0.504883 0.336261 0.754455 -0.000982 0.754455
F 0 4 4 0 3 7 0 0.673504 0.754455 0.673504 1.004027 0.336261 1.004027 0.336261 0.754455
I dice.png


んーと、今日はもう疲れたのでC++側の方はまた次回に。C++での画像ファイル(dice.png)の読み込みは以前使ったDevILを使いました。前使ったはずなのにほとんど忘れかけてて思い出すのにちょっと手間取りました...う〜ん年はとりたくないもんだ。

2009年7月1日水曜日

色をつけたり...


モデルに色をつけてみました。色の設定はBlenderからもってきます。
色はmeshのmaterialに設定されているのでこれを使います。識別子Mの行にマテリアルの属性として色を書き出します。
M R値 G値 B値

R, G, Bはそれぞれ赤(R)、緑(G)、青(B)の量を表す0〜1の範囲の値です。
これを、モデルで使われている色の数だけ出力します。

またマテリアルは面単位で設定されるので面の行(識別子:Fの行)にマテリアルの番号を追加しました。どの面がどの色を使っているかを、マテリアルの番号で指定します。
F マテリアル番号 頂点数 頂点番号1 頂点番号2...

Pythonスクリプトは次のようになります。

:
# マテリアルの数だけ繰り返し
for m in mesh.materials:
col = m.getRGBCol()
out.write("M %f %f %f\n" % (col[0], col[1], col[2]))
:
# 面の数だけ繰り返し
for f in mesh.faces:
out.write("F %d %d" % (f.mat, len(f.verts)))
for v in f.verts:
out.write(" %d" % v.index)
out.write("\n")
:


C++側の読み込みは次のような感じです。

/**
* マテリアル行の読み込み
*/
void Model::readMaterialLine(std::istream& in) {
Matrial *m = materials[matCount++]; // MatrialはRGB値を保持する構造体
in >> m->r >> m->g >> m->b;
}

/**
* フェース行の読み込み
*/
void Model::readFaceLine(std::istream& in) {
Face* f = faces[faceCount++]; // Faceは面情報を保持する構造体
in > f->mat >> f->numVerts;
if (in.fail()) {
std::cerr << "failed in readling face info" << std::endl;
}
:
}
:


switch文の中が長くなってきたので、各行の読み込みを関数にしました。

描画は次のようになります。

glBegin(GL_QUADS);
for (unsigned int i=0; i<faceCount; i++) {
Face* f = faces[i];
 Material* m = materials[f->mat];
glColor3f(m->r, m->g, m->b);
for (unsigned int j=0; j<f->numVerts; j++) {
glVertex3f(v[faces[i]->verts[j]);
}
}
glEnd();

glVertexの前にglColorの呼び出しを追加しています。

色もつけたし、いよいよアニメーションかなぁ、その前にテクスチャの方をやるか...うーむ。