ホーム < ゲームつくろー! < DirectX技術編 < ボーンの回転角度制限判定


その20 ボーンの回転角度制限判定


 ボーンを回転させること自体はそれほど難しくありません。3Dのボーンならば初期位置(X軸方向)からクォータニオンで回転させるのが楽です。自由に回転できるボーンは触手などに使われたりもしますが、多くのボーンは回転角度に制限が設けられています。通常、ボーンの回転角度の制限はXYZ軸の回転角度に対して設定されます。いわゆるオイラー角です。目的点へ向けた回転はクォータニオンで行うけど、角度制限はオイラー角・・・。このなんとも悩ましい関係を解決しなければ、ボーンの威力は半減してしまいます。この章ではクォータニオンをオイラー角に変換する方法について考えてみます。



@ オイラー角とクォータニオンの接点である回転行列

 オイラー角による軸回転は、各軸回転行列の連続的な掛け算です。例えばDirect3Dならば軸回転の順序はZXYです。各軸回転の行列はDirectXのマニュアルにも掲載されていますので、ここでは冗長のため記述を避けますが、この順序による軸回転をいっぺんに表す回転行列は次のようになります。

CZ・CY + SZ・SX・SY SZ・CX -CZ・SY + SZ・SX・CY 0
-SZ・CY + CZ・SX・SY CZ・CX SZ・SY + CZ・SX・CY 0
CX・SY -SX CX・CY 0
0 0 0 1

ここで、

 CX : cos(θx)  SX : sin(θx)
 CY : cos(θy)  SY : sin(θy)
 CZ : cos(θz)  SZ : sin(θz)

です。この行列はZ、X、Y軸回転を表す回転行列を掛け算すると得られます。
 一方、クォータニオンからはD3DXQuaternionRotationMatrix関数によって回転行列を得る事が出来ます。すなわち、上の行列の成分値を得る事が出来るわけです。その数値をうまく用いれば、クォータニオンをオイラー角に変換できます。

 4×4の回転行列の左上をM11、右上をM14、左下をM41、右下をM44となるよう成分値に名前をつけます。
 まず、M32に注目します。ここは-SXです。よって、ここからX軸の回転角度θxが得られます(asinを使います)。この範囲は-90度〜+90度です。
 次にM31を見ましょう。ここはCX・SYとなっていますね。X軸の回転角度θxはもうわかっていますから、CXも既知。よって、SYがわかります。同様にM33からはCYがわかります。SYとCYがあると、atan2関数を用いてY軸回転がわかります(atan2(SY, CY))。この範囲は-180度〜+180度までです。
 続いてM12とM22を見ると、いずれもCXを含んでいます。CXはもう分かっていますから、ここからSZ、CZがそれぞれ得られます。よって、同様にしてatan2からZ軸回りも判明します。この範囲は-180度〜+180度までです。

 このように、ちょっとした方程式を解く事によって、回転行列から結構素直にオイラー角は出てきます。ただし、大きな注意があります。それはM32のSXが1の時です。これは、CXが0である事を示しています。ということは、M32からSYを計算できない事になります(SY = val / CX = val / 0でゼロ除算)。つまり、X軸回転角度が±90度の時には、上記の計算は意味を成さなくなります。いったいこれはどういうことなのか?感の良い方ならもうお気付きかと思いますが、これこそが「ジンバルロック」です(ジンバルロックについてはその10をどうぞ)。

 ボーンをクォータニオンで回転させるときには、ジンバルロックを避けるため、X軸回転角度が±90度にならないように意図的な制限を掛けるべきでしょう。例えば-80度〜+80度の範囲を超えるようならば強制的に回転角度を境界値にしてしまうなどです。

 オイラー角が求まれば、それが制限範囲内か外かを判定するのは簡単ですね。もし外ならば、最大角もしくは最小角に値を固定してしまいます。すべての軸に対してオイラー角が決定したら、いわゆる通常の回転行列を用いてボーンを本当に回転させます。これで、制限角度付きのボーン回転が実現できます。



A ボーンをクォータニオンで回す関数

 ボーンはボーン座標内でベクトルとして表現されます。目的点も同様です。

上の図で、ボーンを目的点側に回転させる行為は、青いベクトルを赤いベクトルに回転させてあわせることと同じです。ボーンのベクトルをb、目的点のベクトルをoとしましょう。この2つのベクトルがあると、その法線を外積から求める事が出来ます(b×oとします)。求めた法線がクォータニオンの回転軸となります。上の図が左手系座標ならば、回転軸は画面の向こう側に向かいます。では、回転角度はプラスかマイナスか?これは常に「プラス」側です。それは、求める法線が常に目的点がプラス側に来るように計算されるためです。クォータニオンの便利さはこういうところにも現れます。

 両ベクトルの内積から回転させる角度を求めれば、クォータニオンQ、共役クォータニオンRを作成できます。

 Q = (x, y, z, w) = ( x*sin(θ/2), y*sin(θ/2), z*sin(θ/2), cos(θ/2) )
 R = (x, y, z, w) = ( -x*sin(θ/2), -y*sin(θ/2), -z*sin(θ/2), cos(θ/2) )

もっともこれはD3DXQuaternionRotationAxis関数およびD3DXQuaternionConjugate関数を使っても作成できます。作成した2つのクォータニオンを用いると、ボーンを次のようにして回転させることができます。

 After = R * Qb * Q

ここで、Afterは回転後のボーンのベクトル(標準化されたクォータニオンになっています)、Qbは回転前のボーンのベクトル(標準化してクォータニオンにします)です。
 不思議な事に、たぶんですがDirect3DXのヘルパー関数には、あるベクトルを回転軸で回して別のベクトルにする関数がありません(見当たらないだけかもしれませんが・・・)。そこで、さっくりと作ってしまいましょう。

 作成するのは、回転させたいベクトルと回転軸、回転角度を与えると回転後のベクトルを算出してくれるD3DXVec3Rotate関数です。

ベクトルを回転させる関数
D3DXVECTOR3 *D3DXVec3Rotate(
   D3DXVECTOR3 *pOut,
   CONST D3DXVECTOR3 *pAxis,      // 回転軸
   FLOAT Angle,                             // 回転角度
   CONST D3DXVECTOR3 *pV          // 回転させたい位置ベクトル
)
{
   // 回転軸と角度からクォータニオンを作成
   D3DXQUATERNION Q, R, Qv, Out;
   D3DXQuaternionRotationAxis(&Q, pAxis, Angle);
   D3DXQuaternionConjugate(&R, &Q);

   // ベクトル回転
   D3DXVECTOR3 V;
   FLOAT fLength = D3DXVec3Length(pV);   // 長さ
   D3DXVec3Normalize(&V, pV);   // 回転させるベクトルの標準化
   Qv.x = V.x;
   Qv.y = V.y;
   Qv.z = V.z;
   Qv.w = 0.0f;
   D3DXQuaternionMultiply(&Qv, &R, &Qv);
   D3DXQuaternionMultiply(&Qv, &Qv, &Q);

   pOut->x = Qv.x * fLength;
  pOut->y = Qv.y * fLength;
   pOut->z = Qv.z * fLength;

   return pOut;
}

 これで、任意のベクトルを任意の軸で好きな角度だけ回す事ができます。この関数、使えますよ(^-^)b。



B 角度制限付きベクトル回転関数

 この章でせっかく角度制限を付けた回転ができるようになったのですから、それを実現する関数も作ってしまいましょう。
 作成するのは、回転前のベクトル、回転軸、回転角度、回転制限角度を与えると指定の方向に回転を試み、制限に引っかかったら制限境界角度に補正してベクトルを回転させるD3DXVev3RotateReg関数です。尚、回転の順番はZXYとし、Z、Y軸の回転制限は-180〜180度の間、X軸の回転制限は-80度〜+80度の間とします。これはジンバルロックを防ぐためです。
 甘い部分もありますが、これによって角度制限の付いたボーンの動きがある程度できるようになると思います。

制限付きベクトル回転関数
D3DXVECTOR3 *D3DXVec3RotateReg(
   D3DXVECTOR3 *pOut,
   CONST D3DXVECTOR3 *pAxis,      // 回転軸
   FLOAT Angle,                              // 回転角度
   CONST D3DXVECTOR3 *pV,          // 回転させたい位置ベクトル
   CONST D3DXVECTOR3 *pMin,       // 回転制限(最小値)
   CONST D3DXVECTOR3 *pMax       // 回転制限(最大値)
)
{
   /////////////////////////////////
   // 回転行列を作成
   //////
   // 回転後ベクトルの算出
   D3DXVECTOR3 After;
   D3DXVec3Rotate(&After, pAxis, Angle, pV);
   D3DXVec3Normalize(&After, &After);

   // 回転後ベクトルとZ軸方向のベクトルから
   // 回転軸と回転角度を算出
  D3DXVECTOR3 Norm;
   D3DXVec3Cross(&Norm, &D3DXVECTOR3(0,0,1), &After);
   D3DXVec3Normalize(&Norm, &Norm);
   FLOAT ang = (FLOAT)acos(D3DXVec3Dot(&D3DXVECTOR3(0,0,1), &After));

   // 回転行列を作成
   D3DXMATRIX RotMat;
   D3DXMatrixRotationAxis(&RotMat, &Norm, ang);


   ////////////////////////
   // 軸回転角度を算出
   /////

   // X軸回り
   FLOAT fXLimit = 80.0f/180.0f*3.14159265f;
   FLOAT fSX = -RotMat._32;    // sin(θx)
   FLOAT fX = (FLOAT)asin(fSX);   // X軸回り決定
   FLOAT fCX = (FLOAT)cos(fX);

   // ジンバルロック回避
   if( fabs(fX) > fXLimit ){
      fX = (fX<0)?- fXLimit: fXLimit;
      fCX = (FLOAT)cos(fX);
   }

   // Y軸回り
   FLOAT fSY = RotMat._31 / fCX;
   FLOAT fCY = RotMat._33 / fCX;
   FLOAT fY = (FLOAT)atan2(fSY, fCY);   // Y軸回り決定

   // Z軸回り
   FLOAT fSZ = RotMat._12 / fCX;
   FLOAT fCZ = RotMat._22 / fCX;
   FLOAT fZ = (FLOAT)atan2(fSZ, fCZ);

   // 角度の制限
   if(fX < pMin->x) fX = pMin->x;
   if(fX > pMax->x) fX = pMax->x;
   if(fY < pMin->y) fY = pMin->y;
   if(fY > pMax->y) fY = pMax->y;
   if(fZ < pMin->z) fZ = pMin->z;
   if(fZ > pMax->z) fZ = pMax->z;

   // 決定した角度でベクトルを回転
   D3DXMatrixRotationYawPitchRoll(
     &RotMat,
     fY,   // ヨー
     fX,   // ピッチ
     fZ    // ロール
   );

   
   D3DXVECTOR4 tmp;
   D3DXVec3Transform(
     &tmp,
     &D3DXVECTOR3(0, 0, D3DXVec3Length(pV)),
     &RotMat
   );

   pOut->x = tmp.x;
   pOut->y = tmp.y;
   pOut->z = tmp.z;

   return pOut;
}

 テストをしてみると、ちゃんと制限がかかってくれているようです。上の関数はDirect3DXが動く環境にコピペすればすぐに使えますので、どうぞお試しを(バクがありましたらどうぞお知らせ下さい)。


 その19および本章で、ボーンを動かすためのツールがそろってきました。少なくともこれらのツールをうまく扱う事で、FK(Foward Kinematics: 運動学)はできるのではないでしょうか?一方、IK(Inverse Kinematics: 逆運動学)を行うには、もう少し知識を要します。次の章では、IKのアルゴリズムとして有名なCCDを紹介します。