2009年8月9日日曜日

なんだか多忙

仕事やなんやかんやで急に忙しくなってしまい、暫く更新が滞りそうです。
決してかわいいテクスチャが描けないわけで(Ry...

2009年8月2日日曜日

クララが立ったー、クララが歩いたー


クララというのは、このモデルにつけた名前です。
いや、どうみても姿は野郎なんですけどね。
とりあえず、歩いてる雰囲気はでてるんじゃないかと...
動きがぎこちないのはクララが病み上がりということで...

しかし、歩きのアクションひとつつくるのもかなり面倒くさいですね。というか、それっぽく見せるのが難しい。こればっかりはセンスの問題もあるし...

とりあえずアクションはこの辺でおいといて、せっかくなのでテクスチャでも貼ってやるかな。かわいい顔の。

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);
}

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

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