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()の実装のみですが、長くなったので次回に。

0 件のコメント: