Bulletによる物体のマウスピック

マウスドラッグによる物体の移動の概要

Bulletのデモや実験のサンプルプログラムではマウスドラッグで剛体をつかんで動かすような動作ができるようになっている. これはデモ用のプログラム内部で,(1)マウスクリックした点との交差判定(光線(Ray)と剛体の衝突判定), (2)交差点にConstraintを設定,を行うことで実現している.

以下でそれぞれの実装手順を順番に説明していく.

マウスクリックした点との交差判定

OpenGL(glew)のMouse関数にクリックしたときに視点座標とクリックした位置ベクトルを取得するコードを追加する. まず,実験のサンプルプログラムでは左ドラッグで視点移動になっているので(if(button == GLFW_MOUSE_BUTTON_LEFT)のところ), そこを変更していく.

if(button == GLFW_MOUSE_BUTTON_LEFT){
  if(action == GLFW_PRESS){
      // 視点,クリック位置の取得処理をここに記述
      // 元々あった視点移動の処理は動的剛体をクリックしなかった場合に行うようにする
  }
}

実験サンプルプログラムでの視点座標の取得方法は衝突判定の説明ページの練習問題8と同じ. クリック位置ベクトルの取得にはGetRayTo関数を用いる.

glm::vec3 ray_from0, ray_to0;
g_view.CalLocalPos(ray_from0, glm::vec3(0, 0, 0));
g_view.GetRayTo(x, y, FOV, ray_to0);
    

得られた視点座標,クリック位置ベクトルはbtVector3型の変数に代入しておこう.

btVector3 ray_from = btVector3(ray_from0[0], ray_from0[1], ray_from0[2]);
btVector3 ray_to = btVector3(ray_to0[0], ray_to0[1], ray_to0[2]);

視点座標が光線の原点,クリックした位置が光線のターゲット位置(光線方向=ターゲット位置-原点)となる.

視点座標とクリック位置ベクトルを使ってマウスクリックした位置にあるオブジェクトを調べる. そのためにBulletワールドのrayTest関数を用いる. この関数ではこちらが指定した光線とワールド内のオブジェクトとの衝突判定を行ってくれる. rayTest関数は以下のようにして用いる.

pre class = "code"> btCollisionWorld::ClosestRayResultCallback ray_callback(ray_from, ray_to); g_dynamicsworld-*>rayTest(ray_from, ray_to, ray_callback);

ここでray_fromとray_toはそれぞれ光線の原点とターゲット位置(btVector3)である. 衝突が検出されたかどうかは,ClosestRayResultCallbackクラスのhasHitメンバ関数で確かめることができる. また,m_collisionObjectメンバ変数(publicメンバ変数)に衝突オブジェクト(btCollisionObject), m_hitPointWorldメンバ変数に衝突点(btVector3)がそれぞれ格納されている. さらに衝突オブジェクトが床などのstatic rigidbodyやkinematic rigidbodyの場合は動かす必要は無いので, btRigidBodyのisStaticObjectメンバ関数やisKinematicObjectメンバ関数を使って除外するようにする. これらの処理を行うコードを以下に示す.

if(ray_callback.hasHit()){
  // 光線と衝突した剛体
  const btCollisionObject* obj = ray_callback.m_collisionObject;
  btRigidBody* body = const_cast<btRigidBody*>(btRigidBody::upcast(obj));

  // 衝突点座標(ジョイントになる位置座標)
  btVector3 picked_pos = ray_callback.m_hitPointWorld;

  if(!(body->isStaticObject() || body->isKinematicObject())){
    // ここに拘束条件を追加するコードを記述する
  }
}

Constraintの設定

光線との衝突点座標をジョイントとして"Point to Point Constraint"(btPoint2PointConstraint)を設定する. 設定するためにはまず,以下のようにして衝突点の剛体ローカル座標における位置を求める必要がある.

btVector3 local_pos = body->getCenterOfMassTransform().inverse()*picked_pos;
body->setActivationState(DISABLE_DEACTIVATION);

2行目は静止状態(getActivationStateで状態を取得)にしないようにピックしたオブジェクトを設定している. これは静止状態(Deactivate)では拘束条件がうまく働かないことがあるためである. 衝突点ローカル座標を求めたら,その点をpivotInAとして1つのオブジェクトを空間中に拘束するようにbtPoint2PointConstraintを追加する (6DOF Constraintでもよい).

if(g_pickconstraint){
  g_dynamicsworld->removeConstraint(g_pickconstraint);
  delete g_pickconstraint;
}
g_pickconstraint = new btPoint2PointConstraint(*body, local_pos);
g_dynamicsworld->addConstraint(g_pickconstraint, true);

あとでMotion関数でも用いるのでConstraintは btPoint2PointConstraint*型のグローバル変数g_pickconstraintに格納してある.

Constraintを設定できたらマウスをドラッグしたときにジョイントの位置を変更する. まず,クリックしたときと同様にして光線の情報(ray_fromとray_toとする)を計算する. マウスの動きは2次元であるのでこれを3次元空間での動きに変換しなければならない. ここでは視点を中心とした球状に動かすとする. この場合,視点と最初の衝突点の間の距離が常に保たれるようにすればよい. Mouse関数内で衝突が検出されたときに

g_pickdist = (picked_pos-ray_from).length();
のようにして衝突点までの距離を保存しておく.g_pickdistは衝突点までの距離を保存するグローバル変数である. そして,マウスドラッグ時の処理を行うMotion関数内でray_fromとray_toを前と同じ手順で求めた後,
btVector3 dir = ray_to-ray_from;
dir.normalize();
btVector3 new_pivot = ray_from+dir*g_pickdist;
とすると新しいジョイント位置(ワールド座標)が求まる. ジョイントは2つのオブジェクトを接続するものであるが, 今回は1つのオブジェクト(pivotInA)とワールド座標(pivotInB)を接続しているので, btPoint2PointConstraintのsetPivotBメンバ関数を使ってnew_pivotを直接指定してやればよい(ローカル座標への変換は必要ない).
  • 最後にマウスボタンを放したときに設定していた拘束条件を破棄する.このとき,g_pickconstraint = 0と再初期化することを忘れないように.
  • これらの手順によりマウスによる物体の選択,移動が可能となる. また,Soft Constraintを使うとバネで引っ張るような効果も可能なので試してみよう (ただし,btPoint2PointConstraintにsetParamでCFMやERPを設定するとうまくいかないことがあるので, m_setting.m_tauを使うか,6DOF Constraintを使ってみるとよい).

    Mouse Pick
    マウスによるオブジェクト選択,移動

    btSoftBodyへの対応

    上記のマウスピックはまだbtSoftBodyには対応していない.
    btSoftBodyにも対応させるために, まず,光線と衝突した剛体が static or kinematicでないかを判断していたif文のところを以下のように書き換える.

    if(body && !(body->isStaticObject() || body->isKinematicObject())){
        // 元々の剛体に対するマウスピック処理
    }
    else{
        // ここにbtSoftBodyに対する処理を追加する
    }

    if文の最初に条件を追加して,剛体かどうかを判別している(Soft Bodyならbody==0となる).
    剛体でなかった場合はbtSoftBodyに変換して,再度光線との衝突判定を行い,もっとも衝突点に近いノードを検索する.

    // btSoftBodyへのキャスト
    btSoftBody* body = const_cast<btSoftBody*>(btSoftBody::upcast(obj));
    btSoftBody::sRayCast res;
    
    // btSoftBodyとの衝突判定
    body->rayTest(ray_from, ray_to, res);
    if(res.fraction < 1.0){// ray_fromからray_toの間に衝突点があった場合
        btVector3 intersect = ray_from+(ray_to-ray_from)*res.fraction; // 衝突点座標
        if(res.feature == btSoftBody::eFeature::Face){ // 面の場合(四面体メッシュを使った場合はTetra)
            btSoftBody::Face& face = res.body->m_faces[res.index]; // 衝突のあった面
    
            // 面を構成するノード(頂点)から衝突点に最も近いノードを探索
            btSoftBody::Node* node = face.m_n[0];
            for(int i = 1; i < 3; ++i){
                if((node->m_x-intersect).length2() > (face.m_n[i]->m_x-intersect).length2()){
                    node = face.m_n[i];
                }
            }
            g_picknode = node; // マウスピックされたノードをグローバル変数に確保しておく
            g_pickdist = (g_picknode->m_x - ray_from).length(); // 衝突点までの距離
        }
    }

    注意として,g_picknodeは0で初期化しておき,マウスボタンを放したとき(GLUT_UP)にもg_picknode=0としておくこと.
    最後にMotion関数でマウスドラッグ時に選択されたノードに力を加える. 衝突点までの距離(g_pickdist)から新しい頂点座標値を求める方法はbtRigidBodyの場合と同じなので省略する. 新しい頂点位置(目標座標)が計算できたら,現在の位置から目標座標へ向かうベクトル方向の力をノードに加える.

    if(g_picknode){
        g_picknode->m_f += (new_pivot-g_picknode->m_x)*10.0;
    }

    戻る