- クオータニオンを用いた座標変換を利用してボーンを描画する -
ボーンとは、3Dのキャラクタを動かす骨組みです。人間で言えば骨です。
今回は「クォータニオンを使ってボーンを描く」ということを考えてみたいと思います。

・ボーンのベクトル
ボーンはx軸、y軸、z軸の中で、どこかの軸上のベクトルとすれば考えやすいです。
ここではボーンのベクトルをx軸上にあるベクトルと考えます。


・親子関係
ボーンには親子関係があります。

人間の腕と考えてみてください。赤いボーンと緑のボーンの間の黒い点は肩の関節です。
緑と青のボーンの間の黒い点は肘の関節としてみることができます。

肩を回せば自然と二の腕以下の部分も一緒に回りますね?
肘の関節を動かせば、それ以下の手や指も一緒についてきます。
つまり、肩の関節の座標系を移動・回転させると、それにぶら下がっているボーンも一緒に移動・回転するということです。
この関係を、肩の関節(座標系)が親、子は肘の関節(座標系)となります。
親子関係として考えます。また、座標系の移動にはglTranslate*()(実際に使いやすいのは座標値の加減算)をし、回転にはクォータニオンを使います。


・親の座標系、子の座標系
ここで前回の「クォータニオンを用いた座標変換」の考え方を使います。

まず、親座標系でボーンのベクトルを決めます。
ボーンはX軸上のベクトルとすると決めましたので、X軸上のベクトルをとります。

図のようなベクトルをボーンとします。
このベクトルをクォータニオンで変換します。わかりやすくするためにY軸周りを90度するクォータニオンにしましょう。
そして、変換後のベクトルを子座標系のボーンとします。
なぜ、変換したボーンが子座標系のX軸上のベクトルになるかというと、クォータニオンは新たな座標系を表現し、
親座標系のX軸上のベクトルを、新たな座標系のX軸上のベクトルに変換したという見方ができるからです。
変換後は以下のようになります。

クォータニオンでY軸周りに90度回転させました。(回転方向は気にしないでください)
これを子座標系のX軸上のベクトルとするので、子座標系は次のような座標系になります。

これがY軸周りに90度回転させたクォータニオンによって表現される座標系、つまり子座標系です。

そしてこの子座標系を親として、新たなクォータニオンを作成し、Y軸周り90度回転のクォータニオンと合成すれば、
この子座標系を親とした子座標系のX軸上のベクトルに変換することができます。

たとえば、子座標のZ軸周りでさらに90度回転させるクォータニオンを作るとします。
回転軸は当然、子座標のZ軸です。これは子座標の親座標(一番大元の直交座標系)では(0, 0, 1)です。
これを「Y軸周りで90度回転させるクォータニオン」で回転させます。これが子座標のZ軸、つまり回転軸です。
この軸周りに90度回転させ、描画すればいいのです。
このとき、「Y軸周りで90度回転させるクォータニオン」と「子座標のZ軸周りで90度回転させるクォータニオン」
を合成すれば、一番大元の直交座標系から一気に、子座標の座標系→さらに子座標の系へと変換することができます。
これをどんどん繰り返すことで、ボーンを次々に描画することができます。

・クォータニオンの合成
クォータニオンの合成とは、クォータニオンの掛け算をするということです。
たとえば、Aという回転をするクォータニオン、Bという回転をするクォータニオンがあります。
そして、A * B をしてCというクォータニオンを作成します。このCというクォータニオンは、
回転Aをしてさらに回転Bをするというクォータニオンになります。これがクォータニオンの合成です。

筆者の使用例を書いておきます。
Aの回転軸を、
Va = (xa, ya, za)
Bの回転軸を、
Vb = (xb, yb, zb)
とします。Aの回転をするには、
A1 = (cos(θ/2); xa * sin(θ/2), ya * sin(θ/2), za * sin(θ/2))
A2 = (cos(θ/2); -xa * sin(θ/2), -ya * sin(θ/2), -za * sin(θ/2))
という2つのクォータニオンをつくり、両辺から掛け算すると回転が表現できます。
Bの場合もおなじですね。
ここで、C1 = A1 * B1という計算をしてから共役のクォータニオンC2を作成します。
C2の成分は、実部はC1の数をそのまま、虚部はC1の虚部に-1を掛けたものです。
このC1, C2を、
C2 * P * C1
と、両辺から掛けることによって、回転A・回転Bをした後の座標が得られます。
この辺は色々やり方があると思うので、そのときに応じたやり方をしてください。


・練習して実感しよう
1回目に作成した関数を用いて、実際にプログラムを組んでみると座標変換がわかりやすいです。
(1, 0, 0)のベクトルをボーン、(0, 1, 0)のベクトルを回転軸、角度は90度とし、
RotateByQuaternion()の引数に与えます。返ってくるデータは(1, 0, 0)の変換後のベクトルです。
このベクトルをglVertex*()などで描画してみてください。
余裕があれば変換後の座標系のY軸、Z軸も描いてみてください。
Y軸はX軸の法線ベクトルです。Z軸はX軸とY軸の外積で求めます。
ベクトルの成分を色々変化させて試してください。

さらに、クォータニオンの合成をして、その次の子のボーンも描画してみてください。
たとえば、glTranslate*()でボーンの先へ移動し、そこを新たな座標系の原点としてクォータニオンを作成し、
各座標系を表すクォータニオンすべてを合成して、そのクォータニオンで(x, 0, 0)を変換すればいいのです。
xの値を変えることで各ボーンの長さも変化させることができます。

色々なツールを見るとボーンがカッコイイ形になっています。
慣れてきたらボーンのデザインを考えるのも楽しいと思います。



ボーンはいつもX軸上にあり、ボーンの座標系がクォータニオンによってねじれていると考えます。



・ボーンの描画をするサンプルプログラム

[実行したところ]



わかりやすいようにOpenGLの座標系を描画し、XZ平面に床のようなものを作成してあります。
最上位ボーンは赤、最下位ボーンは青です。



//ボーン
struct Bone
{
Vector position; //親座標系でのボーンの位置
float boneLong; //ボーンベクトル(XVector)の長さ

Vector XVector; //親座標系で見たボーン(子座標系でのx軸のこと)
Vector YVector; //親座標系で見たボーンの法線(子座標系でのy軸のこと)

Vector parentXVector; //このボーンの親座標のX軸上のベクトル
Vector parentYVector; //このボーンの親座標のY軸上のベクトル

Quaternion boneCood; //親座標をボーンの座標系に変換するクォータニオン(初期状態を描画するときに使うクォータニオン)
Vector boneVector; //ボーンのベクトル
Vector axisVector; //ボーンを回転するときに、回転軸となるベクトル
};



int LoadBoneData()
{
Quaternion allQua, callQua; //一番上の座標系から操作している座標系までを合成したクォータニオン(初期状態の姿勢のクォータニオン)
Quaternion qRot, cqRot; //各座標系にさらに回転を加えるためのクォータニオン
int boneDataCnt = 3; //ボーンのデータ数
int i;

for (i = 0; i < boneDataCnt; i++)
{
//一番最初に描画するボーンの時
if (i == 0)
{
//ボーン0の親座標系でのX軸上のベクトル(ボーンのベクトル)
b[i].parentXVector.x = 1.0;
b[i].parentXVector.y = 0.0;
b[i].parentXVector.z = 0.0;

//親座標系のY軸上にあるベクトル
b[i].parentYVector = GetVerticalVector(b[i].parentXVector);

//ボーン0の位置を決めておく
b[i].position.x = 0.3;
b[i].position.y = 0.3;
b[i].position.z = 0.3;

//親座標系からみた子座標系のx軸(ボーンの方向) XVector長さをボーンの長さとする
b[i].XVector.x = 1.0;
b[i].XVector.y = 1.0;
b[i].XVector.z = 1.0;
}
else
{
//ボーン1つ前のボーンの先の位置を次のボーンの座標系の原点
b[i].position = b[i-1].boneVector;

//親座標系からみた子座標系のx軸(変換前)
b[i].XVector.x = 1.0;
b[i].XVector.y = 0.0;
b[i].XVector.z = 0.0;

//変換後。これが実際の親座標系からみた子座標系のx軸
b[i].XVector = QuaternionToVector( MultiplyQuaternion( MultiplyQuaternion(callQua, VectorToQuaternion(b[i].XVector) ), allQua) );

//1つ前のボーンの座標系が親座標系になる(元々の座標系から一気に変換する)
b[i].parentXVector = QuaternionToVector(MultiplyQuaternion( MultiplyQuaternion( callQua, VectorToQuaternion(b[0].parentXVector) ), allQua ));
b[i].parentYVector = QuaternionToVector(MultiplyQuaternion( MultiplyQuaternion( callQua, VectorToQuaternion(b[0].parentYVector) ), allQua ));
}

//ボーンの長さを取得
b[i].boneLong = VectorLong(b[i].XVector);
//親座標系からみた子座標系のy軸(これをボーンの法線とする)
b[i].YVector = GetVerticalVector(b[i].XVector); //ボーンのベクトルに対するXY平面上の法線を取得
//とりあえず、各座標系のY軸周りの回転だけを行うとする(ボーンの回転制御時に使用)
b[i].axisVector = NormalizeVector(b[i].YVector);

//描きたいボーンの座標系が親座標と同じ座標系になる場合、このボーンの座標系=親座標系ってことなので単位クォータニオンにする
if (b[i].parentXVector.x == b[i].XVector.x && b[i].parentXVector.y == b[i].XVector.y && b[i].parentXVector.z == b[i].XVector.z)
{
b[i].boneCood.t = 1.0;
b[i].boneCood.x = 0.0;
b[i].boneCood.y = 0.0;
b[i].boneCood.z = 0.0;
}
else {
//親座標系から子座標系へ変換するクォータニオンを作成
b[i].boneCood = QuaternionTransChildCoodinate(b[i].parentXVector, b[i].parentYVector, b[i].position, b[i].XVector, b[i].YVector);
}

if (i == 0)
{
//allQua, callQuaをボーン0の座標系に変換するクォータニオンで初期化
allQua = b[i].boneCood;
callQua = QuaternionConjugate(b[i].boneCood);
}
else
{
//作成したクォータニオンを、ボーン0の親座標からb[i-1]の座標系へ変換するクォータニオンと合成(ボーン0の親座標系→b[i]の座標系への変換)
allQua = MultiplyQuaternion(allQua, b[i].boneCood);
callQua = QuaternionConjugate(allQua);
}

//描画用のボーンのデータを作成
b[i].boneVector.x = b[i].boneLong;
b[i].boneVector.y = 0.0;
b[i].boneVector.z = 0.0;

//ボーンの先頭位置を計算。これが次のボーンの座標系の原点となる
b[i].boneVector = QuaternionToVector( MultiplyQuaternion( MultiplyQuaternion( callQua, VectorToQuaternion(b[i].boneVector) ), allQua ) );
}

return 0;
}

実際に描画するためには、glTranslate*()でb[i].positionに移動し、
原点からb[i].boneVectorに線を引けば単純なボーンを描くことができます。


QuaternionTransChildCoodinate()のソースも置いておきます。
この関数でやっていることは、親座標系のX軸(parentXVector)、 Y軸(parentYVector)を
親座標系からみたときの子座標系のX軸(childXVector)、Y軸(childYVector)に、軸を合わせるということです。
合わせる際の回転軸は外積で求めることができ、角度は内積の2式使って算出できます。
回転軸と角度がわかればクォータニオンを作成可能ですので、
X軸を合わせるクォータニオン、Y軸を合わせるクォータニオンをそれぞれ作成し、合成したものを返します。


(childPositionは子座標系の原点となる点ですが、この点に移動する処理はおそらく必要ないと思います。)

//親座標系(現在の座標系)を子ボーン(ボーンは常にx軸上にあるとする)の座標系に変換するクォータニオンを作成する
Quaternion QuaternionTransChildCoodinate
(
Vector parentXVector, //親座標系のX軸となっているベクトル
Vector parentYVector, //親座標系のY軸となっているベクトル
Vector childPosition, //親座標系から見た時の子座標系の原点となる点
Vector childXVector, //親座標系からみた、子座標系のX軸
Vector childYVector //親座標系からみた、子座標系のY軸
)
{
//引数を正規化したもの
Vector nparentXVector, nparentYVector, nchildXVector, nchildYVector;

Vector parentX, parentY; //親座標系のX軸・Y軸のベクトル
Vector axis; //クォータニオンの回転の軸になるベクトル
Vector v_transY; //回転後Y軸
float angle; //回転する角度
Quaternion q_parentX, q_parentY; //parentX, parentYをクォータニオンに置き換えたもの(実部の0を加えただけ)
Quaternion qx; //親座標系のX軸を子座標系のX軸に合わせるクォータニオン
Quaternion qy; //親座標系のY軸を子座標系のY軸に合わせるクォータニオン
Quaternion cq; //共役のクォータニオンを格納する用
Quaternion transX, transY; //qxを使って回転した後の親座標系のX軸・Y軸

//引数のベクトルを正規化
nparentXVector = NormalizeVector(parentXVector);
nparentYVector = NormalizeVector(parentYVector);
nchildXVector = NormalizeVector(childXVector);
nchildYVector = NormalizeVector(childYVector);

//子座標系の原点に移動
glTranslatef(childPosition.x, childPosition.y, childPosition.z);

//親座標系X軸のベクトル
parentX.x = nparentXVector.x;
parentX.y = nparentXVector.y;
parentX.z = nparentXVector.z;

//親座標系Y軸のベクトル
parentY.x = nparentYVector.x;
parentY.y = nparentYVector.y;
parentY.z = nparentYVector.z;

axis = outerProduct(parentX, nchildXVector); //外積で回転軸を求める
angle = getAngleFromInnerProduct(parentX, nchildXVector); //内積から角度を求める

axis = NormalizeVector(axis);

qx = QuaternionRotationAxis(axis, angle); //親座標系X軸を、子座標系のX軸に合わせるクォータニオンを作成する
cq = QuaternionConjugate(qx); //共役のクォータニオンの作成

//親座標系X軸をクォータニオンに置き換え
q_parentX.t = 0.0;
q_parentX.x = parentX.x;
q_parentX.y = parentX.y;
q_parentX.z = parentX.z;

//qxで親座標系のX軸を回転
transX = MultiplyQuaternion(cq, q_parentX);
transX = MultiplyQuaternion(transX, qx);

//親座標系Y軸をクォータニオンに置き換え
q_parentY.t = 0.0;
q_parentY.x = parentY.x;
q_parentY.y = parentY.y;
q_parentY.z = parentY.z;

//qxで親座標系のY軸を回転
transY = MultiplyQuaternion(cq, q_parentY);
transY = MultiplyQuaternion(transY, qx);

//クォータニオンをベクトルにする
v_transY.x = transY.x;
v_transY.y = transY.y;
v_transY.z = transY.z;

//変換されたY軸と、親座標系からみた子座標系のY軸の角度差を計算
angle = getAngleFromInnerProduct(v_transY, nchildYVector);

//回転軸を子座標系のX軸にする
axis.x = transX.x;
axis.y = transX.y;
axis.z = transX.z;

axis = NormalizeVector(axis);

//qxでの回転後Y軸と、子座標系のY軸を合わせるクォータニオンを作成
qy = QuaternionRotationAxis(axis, angle);

//x軸とy軸を子座標系に合わせるクォータニオンを作成し、それを返す
return MultiplyQuaternion(qx, qy);
}



以下の関数のソースは簡単に作成できるので省略。
簡単にどういう関数なのか紹介。

QuaternionToVector … クォータニオン(t; x, y, z)の実部を除いたベクトル(x, y, z)にする。
VectorToQuaternion … ベクトル(x, y, z)を実部が0のクォータニオン(0; x, y, z)にする。
VectorLong … ベクトルの長さを返す関数。
GetVerticalVector … あるベクトルに対して垂直な、XY平面上のベクトルを返す。
NormalizeVector … 正規化されたベクトルを返す。
QuaternionConjugate … 共役クォータニオンを返す。
outerProduct … 外積を求める。
getAngleFromInnerProduct … 2つの内積の式から角度を求める。
QuaternionRotationAxis … RotateByQuaternionの★印まで行い、calcBを返す。


inserted by FC2 system