前回は、とりあえず魚を泳がせました。
今回は、BOIDの基本的な部分を組み込んで、群れをなして泳がせてみます。
今回のソースは、次のものです。
今回のメインは boid.cpp です。
Game Programming Gems を参考にしました(それのシンプルバージョンとも言う)。
関数名なども、なるべくあわせるようにしています。
Craig Raynolds は、1987 年の SIGGRAPH で、
「Flocks, Herds, and Schools : A Distributed Behavioral Model」を発表しました。
この論文は、3つの理解しやすいルールを規定するだけで、動物の群れをシミュレーションできるというものです。
そのルールとは、
| Separation | (引き離し) | : | 一定距離より近くに間を取らない |
| Alignment | (整列) | : | 仲間とスピードと方向を合わせる。 |
| Cohesion | (結合) | : | グループの中心へ向かおうとする。 |
です。
ここで出てくる BOID という単語は、(鳥)バードとアンドロイドを組み合わせた造語で、鳥だけでなく、魚や馬の群れにも
この単語は使われています。
今回のプログラムでは、魚の一匹一匹を表現するクラスを CBoid クラスとします。
われわれは、歩くときや会話するときなど、自然といい感じの距離を取って生活します。
これが『引き離し』です。
プログラムでは、次のように実装します。
関数の返り値が、『引き離し』による速度変化量(力)になります。
// ----------------------------------------------------------------------------
// Rule #1 (Separation) 仲間との距離をある大きさに維持することによって、ぶつかるのを避ける。
//-----------------------------------------------------------------------------
D3DXVECTOR3 CBoid::KeepDistance (void)
{
// 距離によって速さを調整する
float ratio = m_dist_to_nearest_flockmate / SEPARATION_DIST;
if (ratio < MIN_URGENCY) ratio = MIN_URGENCY;
if (ratio > MAX_URGENCY) ratio = MAX_URGENCY;
// もっとも近い仲間の方向を求める(その軸に向かう)
D3DXVECTOR3 change = m_nearest_flockmate->m_pos - m_pos;
D3DXVec3Normalize(&change, &change);
if (m_dist_to_nearest_flockmate < SEPARATION_DIST) {
change *= -ratio; // 非常に(SEPARATION_DISTより)近かったら、遠くへ話す
} else if (m_dist_to_nearest_flockmate > SEPARATION_DIST) {
change *= ratio; // 非常に(SEPARATION_DISTより)遠かったら、近寄る
} else {
change *= 0.0f; // いい感じの位置だったら、調整をしない
}
return (change);
}
m_pos が、各 BOID の位置です。
m_nearest_flockmate が、一番近い位置にいる仲間(のCBoidクラス)です。
基本的には、一番近い仲間への方向(change)を求め、適切な距離(SEPARATION_DIST)よりも近いか遠いかで、
その方向に近づいたり、遠ざかります。
遠くにいればいるほど、群れに戻ろうと必死になるので、一番近い仲間との距離(m_dist_to_nearest_flockmate)
に応じて、速さ(ratio)を変えます。
但し、泳ぐ速さには、限界がありますし、ひとかきで進む大きさがあるので、
力の上限と下限を設けます。
さて、次は、『整列』です。
とりあえず、一番近い仲間の速度を見て、その速度にだんだんあわせるようにします。
// ----------------------------------------------------------------------------
// Rule #2 (Alignment) 仲間とスピードと方向を合わせる。
//-----------------------------------------------------------------------------
D3DXVECTOR3 CBoid::MatchHeading (void)
{
// 一番近い仲間の速さの向きを調べる
D3DXVECTOR3 change = m_nearest_flockmate->m_vel;
D3DXVec3Normalize(&change, &change);
// だんだん速度をあわせるように調整する
change *= MIN_URGENCY;
return (change);
}
最後に、『結合』です。
仲間の中心位置を求めて、そちらに動かします。。
// ----------------------------------------------------------------------------
// Rule #3 (Cohesion) 自分のまわりの群れのグループの中心へ向かおうとする。
//-----------------------------------------------------------------------------
D3DXVECTOR3 CBoid::SteerToCenter (void)
{
D3DXVECTOR3 change;
// 見える仲間の中心位置を求める
D3DXVECTOR3 center = D3DXVECTOR3(0,0,0);
for (int i = 0; i < m_num_flockmates_seen; i++) {
if (VisibleFriendsList[i] != NULL){
center += VisibleFriendsList[i]->m_pos;
}
}
center /= m_num_flockmates_seen;// 位置の合計を見える仲間の数で割って、中心を求める
// 中心への向きを求めて、だんだんそちらによるように動かす
change = center - m_pos;
D3DXVec3Normalize(&change, &change);
change *= MIN_URGENCY;
return (change);
}
m_num_flockmates_seen に、見える仲間の数が入っています。
VisibleFriendsList[] は、線形リストで、見える仲間が納められています。
以上の話は、既に見える範囲の仲間を見つけた後のお話です。
見える範囲の仲間及び、一番近い場所にいる仲間を見つける方法を説明します。
先ずは、仲間を見つける方法です。
下の関数が見つける関数です。
// ----------------------------------------------------------------------------
// 仲間 (ptr) が見えるか?
//-----------------------------------------------------------------------------
float CBoid::CanISee (CBoid *ptr)
{
if (this == ptr) return (INFINITY); // 自分は見えない(はるかかなたにいる)
// 距離の計算
D3DXVECTOR3 d = m_pos - ptr->m_pos;
float dist = D3DXVec3Length(&d);
// 視野の範囲にいたら、その距離を返す
if (m_perception > dist) return (dist);
// 見えなかった
return (INFINITY);
}
仲間(ptr)に対して、距離(dist)を計算します。
目の届く範囲(m_perception)よりも近くにいたら、その距離を返します。
位置が遠くて、見つからなかった場合には、見つからなかったとして、INFINITY を返します。
今回は、視野角による効果(目は前についているので、後ろは見えない)を入れていません。
視野角を入れると、玉突き事故がおきやすくなります。
おそらく、そちらのほうがリアルです。
今のが、一つ一つの可視判定でした。
1つのBOIDに関する可視判定は、全ての BOID に対して、以上の判定を行います。
また、それぞれの判定で、距離が返ってきますので、返ってきた値を調べて、
一番近かったら、一番近いBOIDに設定します。
// ----------------------------------------------------------------------------
// 仲間を探す
//-----------------------------------------------------------------------------
int CBoid::SeeFriends (CBoid *first_boid)
{
// 可視リストを初期化する
ClearVisibleList();
for (CBoid *flockmate = first_boid; flockmate != NULL; flockmate = flockmate->GetNext()) {
float dist;
// 可視判定
if ((dist = CanISee(flockmate)) != INFINITY) {
AddToVisibleList(flockmate);// 見えたら可視リストに追加
// 一番近いのか判定
if (dist < m_dist_to_nearest_flockmate) {
m_nearest_flockmate = flockmate; 一番近い仲間のポインタを保存
m_dist_to_nearest_flockmate = dist; 距離を代入
}
}
}
return (m_num_flockmates_seen);
}
それぞれのBOIDは、線形リストで下のように繋がれています。
GetNext()で、次のBOIDを持ってきて、無ければ (NULLならば) 終了です。
ちなみに、以上の関数の初期化や追加は、次のようになっています。
// ----------------------------------------------------------------------------
// 仲間の可視リスト構造(初期化)
//-----------------------------------------------------------------------------
void CBoid::ClearVisibleList (void)
{
// それぞれのリストを初期化
for (int i = 0; i < MAX_FRIENDS_VISIBLE; i++) {
VisibleFriendsList[i] = NULL;
}
// 他の変数の初期化
m_num_flockmates_seen = 0;
m_nearest_flockmate = NULL;
m_dist_to_nearest_flockmate = INFINITY;
}
// ----------------------------------------------------------------------------
// 可視リストへ追加
//-----------------------------------------------------------------------------
void CBoid::AddToVisibleList (CBoid *ptr)
{
if (MAX_FRIENDS_VISIBLE <= m_num_flockmates_seen) return;// バッファがいっぱいで追加できない
VisibleFriendsList[m_num_flockmates_seen++] = ptr; // リストの最後に追加
}
では、以上を組み合わせて、一フレームでの BOID の動きにまとめます。
先ほど解説した、SeeFriends で、視界に入る仲間を見つけます。
仲間が入れば、Raynolds のルールに従って、移動をします。
移動量から、向きや速度を決定して、最後に世界から飛び出たら、修正します。
// ----------------------------------------------------------------------------
// フレームごとのアップデート
//-----------------------------------------------------------------------------
void CBoid::FlockIt (int flock_id, CBoid *first_boid)
{
// Step 1: 前の時間に決定した速度を使って位置の更新
m_oldpos = m_pos; // 前の座標を保存しておく
m_pos += m_vel; // 移動
// Step 2: 仲間を探す
this->SeeFriends (first_boid);
D3DXVECTOR3 acc = D3DXVECTOR3(0,0,0);
// Step 3: 群れの動作
if (m_num_flockmates_seen) {
// Step 4: Rule #1 (Separation)
// 仲間との距離をある大きさに維持することによって、ぶつかるのを避ける。
acc += KeepDistance();
// Step 5: Rule #2 (Alignment)
// 仲間とスピードと方向を合わせる。
acc += MatchHeading();
// Step 6: Rule #3 (Cohesion)
// 自分のまわりの群れのグループの中心へ向かおうとする。
acc += SteerToCenter();
}
// Step 7: 巡航
acc += Cruising();
// Step 8: 加速の制限 (MAX_CHANGEより強い力は出せません)
if (D3DXVec3Length(&acc) > MAX_CHANGE) {
D3DXVec3Normalize(&acc, &acc);
acc *= MAX_CHANGE;
}
// Step 9: 速度変化
m_oldvel = m_vel; // 前の速さを取っておく
m_vel += acc; // 加速
// Step 11: 速すぎたときに、速度を制限する(MAX_SPEEDより速く泳げません)
if ((m_speed = D3DXVec3Length(&m_vel)) > MAX_SPEED) {
D3DXVec3Normalize(&m_vel, &m_vel);
m_vel *= MAX_SPEED;
m_speed = MAX_SPEED;
}
this->ComputeRPY(); // Step 12: 回転の計算
this->WorldBound(); // Step 13: 世界の境界の処理
}
最終的に速度変化を求めた後に、MAX_CHANGE や、MAX_SPEED を使って、速度変化や、速度に制限をつけています。
筋力の限界や、水の抵抗があるので、何らかの方法で限界を入れなければなりません。
おそらく、この方法が簡単でしょう。
Cruising という、紹介していない速度変化があります。
これは、一人で泳いでいるときの魚の運動です。
ここでは、一定の速度(巡航速度:DESIRED_SPEED)で泳ぐようにします。
また、以上の動きではつまらないので、首振りを入れます。
これは、巡航速度に近づけるときに、ランダムに少し行き過ぎさせ、不安定にすることによって、ユラユラとした動きを作ります。
// ----------------------------------------------------------------------------
// 通常巡航
//-----------------------------------------------------------------------------
D3DXVECTOR3 CBoid::Cruising (void)
{
// 巡航速度(DESIRED_SPEED)にちかづける
float diff = (m_speed - DESIRED_SPEED)/ MAX_SPEED;
float urgency = (float) fabs(diff);
if (urgency < MIN_URGENCY) urgency = MIN_URGENCY;
if (urgency > MAX_URGENCY) urgency = MAX_URGENCY;
// ランダム性を入れて、首振り運動をさせる.
float jitter = RAND();
if (jitter < 0.45f) {
change.x += MIN_URGENCY * SIGN(diff);
} else if (jitter < 0.90f) {
change.z += MIN_URGENCY * SIGN(diff);
} else {
change.y += MIN_URGENCY * SIGN(diff);// あまり縦にはゆれない
}
// 速度変化を力に追加する
D3DXVECTOR3 change = m_vel;
D3DXVec3Normalize(&change, &change);
change *= (urgency * SIGN(-diff));
return (change);
}
最後に向きの変更です。
pitch と yaw は、速度の経度及び緯度として求められます。
roll は、速度変化の速度に直行する部分の大きさを見て、横に大きく動くようなら、大きく傾くようにしました。
// ----------------------------------------------------------------------------
// Roll Pitch Yaw の回転の計算
//-----------------------------------------------------------------------------
void CBoid::ComputeRPY (void)
{
float roll, pitch, yaw;
// 受けた力を計算する
D3DXVECTOR3 d = m_vel - m_oldvel;
D3DXVECTOR3 lateralDir;
D3DXVec3Cross(&lateralDir, &m_vel, &d);
D3DXVec3Cross(&lateralDir, &lateralDir, &m_vel);// 受けた力の速度に直交する成分(回転成分)
D3DXVec3Normalize(&lateralDir, &lateralDir);
// トルク計算
float lateralMag = D3DXVec3Dot(&d, &lateralDir);
// roll
if (lateralMag == 0) {
roll = 0.0f;
} else {
roll = (float) -atan2(GRAVITY, lateralMag) + PI/2.0f;
}
// pitch
pitch = (float) -atan(-m_vel.y / sqrt((m_vel.z*m_vel.z) + (m_vel.x*m_vel.x)));
// yaw
yaw = (float) atan2(-m_vel.x, -m_vel.z);
// 出力
m_ang.x = pitch;
m_ang.y = yaw;
m_ang.z = roll;
}
あと、今回は、水槽に関して、周期的な境界条件を使っています。
理由は、そうしないとぶつかるからで、次の回でちゃんと水槽の中を泳がせます。
あと、CBoid の線形リストの説明をしていないですが、これは、基本的なアルゴリズムの本を読んでください。
また、初期化の説明もしていませんが、位置や速度を適当に設定しているだけです。
あと、毎フレーム、各 CBoid に関して、FlockIt をしているということでしょうか。
表示されている魚は CFish クラスとして表現されています。
前回説明したモデル表示の CMyModel クラスと、今回の CBoid の多重継承になっています。
CBoid クラスで、決定した座標を、CMyModel クラスの座標にコピーして、表示します。
そのまま、CBoid クラスの位置を採用しても良いのですが、ゲームなどで用いるときは、
登場シーンやパワーアップシーンなどの、さまざまな演出が入りますので、
分離しておくほうが良いのではないでしょうか?
とりあえず、群れをなして動くものを作れました。
後は、障害物を避けさせたり、『敵』を導入したいと思います。
今回気がついたのですが、BOID一個一個がモデルを読みにいくので、非常な無駄が発生しています。
これを回避する、リソース管理もしたいです。
魚にアニメも入れたいですし、fps に依存したい動きもさせたいです。
あと、足りないの在ったら、いってください。