視点を動く物体の後ろに固定する方法

視点の剛体運動に合わせた移動

拘束条件のページの課題 で4つの車輪と車体を組み合わせた車を作成し,それをモータにより動かすということを行った. このとき,視点位置と視線方向はその動きとは無関係であったが, ゲームやシミュレーションを行うアプリケーションにおいては視点が車を追従すると, その動きをとらえやすく便利である. ここではそのような視点の動きをサンプルプログラム2で実現する方法について説明する.

まず前提として,視点の動きはサンプルプログラム2で提供されているrxTrackballクラスで制御しているとする. rxTrackballクラスはマウスの動きにあわせた回転,平行移動をそれぞれ四元数,ベクトルの演算で実現している. そして,回転は四元数を回転行列に変換し,glMultMatrixでその行列を描画に反映, 平行移動はglTranslateを使って描画に反映している.

Bulletでも物体の姿勢は四元数(btQuaternion),位置はベクトル(btVector3)で表されているので, 車の場合ならば車体となっているbtRigidBodyからこれらの情報を取得して利用すれば視点位置を車に追従させることができる. ただし,rxTrackballクラスを使う場合は以下の2点に注意する必要がある.

1つ目の注意点から,平行移動や回転は物体の動きとは逆にする必要があるということが分かる(ワールドそのものを逆方向に動かすことで,あたかも視点を動かしているように見せている). つまり,平行移動ベクトルにはマイナスの符号を付け,回転には共役四元数を用いる必要がある.
2つ目の注意点から,先に回転を適用後,平行移動を行う.この場合,平行移動する前に車の座標値も回転させなければならない.

上記の点に注意しつつ,以下のコードを参照してほしい.

void MoveCameraWithRigid(rxTrackball &view, btRigidBody *body)
{
    btVector3 r = btVector3(0, 2, 7); // 剛体の後ろに+7,上に+2の位置に視点を設定

    // 剛体の位置と姿勢(四元数)
    btTransform trans;
    body->getMotionState()->getWorldTransform(trans);
    btVector3 p = trans.getOrigin();
    btQuaternion q = trans.getRotation();
    q = q.inverse();

    // 視点変更用のトラックボールの回転中心が常に原点にあるので,
    // 先に回転させてから,平行移動を行う

    // 視線方向を回転
    double qd[4];
    qd[0] = q[3]; qd[1] = q[0]; qd[2] = q[1]; qd[3] = q[2]; 
    view.SetQuaternion(qd);

    // 剛体の後ろに視点を移動
    btVector3 epos = quatRotate(q, p)+r;
    view.SetTranslation(-epos[0], -epos[1]);
    view.SetScaling(-epos[2]);
}

この関数ではまず,引数として与えたbtRigidBodyからbtMotionState,btTransformを介して, 物体の現在の位置(p)と姿勢(q)を取得する. qに関してはbtQuaternionのinverseメンバ関数を用いて共役四元数にしている. 位置ベクトルpについては回転した後に反転させる.
位置を取得したら,次に視線方向を回転させる. 注意として,rxTrackball内では独自に定義した四元数関数を用いており, そちらでは角度成分が最初の要素になっており,角度成分が最後の要素になっているBulletのbtQuaternionと違っているため, その変換を行ってから,rxTrackballクラスのSetQuaternionメンバ関数で姿勢を渡す.
最後に平行移動を行う. 座標系自体が回転しているので,すでに取得してあった物体の位置pもまた回転させる必要がある(quatRotate(q, p)の部分). 回転したらその位置を後ろにずらし(+r),rxTrackballクラスのSetTranslationとSetScalingメンバ関数を用いて, 平行移動を設定する.このとき,上記の注意点で述べたとおり,符号を反転させてから渡す.

MoveCameraWithRigid関数を適当なキーを押したときに呼び出せば,視点をその位置に持ってこれる. また,Timer関数などで常に呼び出すようにすると車の動きに追従するようになる.


視点を車の後ろに設定した例

回転の制限

車の例では上記の方法で問題ないが,例えば球体に視点を追従させようとすると, 視点に対して上下方向の回転が問題となる. これを防ぐためには,上記関数で取り出した物体の姿勢を表す四元数qをオイラー角に変換し, 視点に対する左右方向の回転(これをyawという)だけを取り出して,また四元数に戻せばよい.

Bulletの四元数クラスbtQuaternionにはオイラー角への変換関数getEularZYXがあったのだが, バージョンによってはないこともある.その場合は以下のような関数を使うとオイラー角に変換できる.

void QuaternionToEuler(const btQuaternion &q, btVector3 &euler)
{
  btScalar w = q.getW();
  btScalar x = q.getX();
  btScalar y = q.getY();
  btScalar z = q.getZ();
  euler[0] = asin(-2.0*(x*z-w*y));					// yaw
  euler[1] = atan2(2.0*(y*z+w*x), (w*w-x*x-y*y+z*z));	// pitch
  euler[2] = atan2(2.0*(x*y+w*z), (w*w+x*x-y*y-z*z));	// roll
}

QuaternionToEuler関数でオイラー角に変換した後,横方向の回転(yaw)だけを残して, また,四元数に戻して視点移動に使えばよい. この処理を追加したバージョンの視点移動関数を以下に示す.

void MoveCameraWithRigid(rxTrackball &view, btRigidBody *body)
{
  btVector3 r = btVector3(0, 2, 7);

  // 剛体の位置と姿勢(四元数)
  btTransform trans;
  body->getMotionState()->getWorldTransform(trans);
  btVector3 p = trans.getOrigin();
  btQuaternion q = trans.getRotation();

  // 視点に対して横方向の回転(yaw)だけを残す
  btVector3 e;
  QuaternionToEuler(q, e);
  q.setEulerZYX(0.0, e[0], 0.0);
  
  q = q.inverse();	

  // 視点変更用のトラックボールの回転中心が常に原点にあるので,
  // 先に回転させてから,平行移動を行う

  // 視線方向を回転
  double qd[4];
  qd[0] = q[3]; qd[1] = q[0]; qd[2] = q[1]; qd[3] = q[2]; 
  view.SetQuaternion(qd);

  // 剛体の後ろに視点を移動
  btVector3 epos = quatRotate(q, p)+r;
  view.SetTranslation(-epos[0], -epos[1]);
  view.SetScaling(-epos[2]);
}

戻る