Unity で簡単な Boids の群れモデルを実現する
About
CG で生物などの群れを表現するとき、有名な手法の1つが "Boid(s)" だと思います。ここでは Unity とその Regidbody を利用して、簡単な Boids を実装します。より高度で実用的な実装や、詳細な群れの表現などが必要な場合には、この記事を取っ掛かりに、Boids のモデルについて詳細に解説している記事を読むことを推奨します。この記事の内容は長めですが、実際のソースコードは非常にシンプルです(しかし強力)。
サンプルをダウンロードして、動かすなどしながら読み進めてください。簡単にしか書いていないので、テキストだけでは不十分だと思います。
- UnityBoidsSample.zip
- Unity 4.3 for Win で動作を確認しています。
- マウスで平面をなぞると、魚の群れが付いてきます。
Boids の論文は SIGGRAPH '87 などで発表されています。詳しくは著者である Craig Reynolds 氏のページを参照してください。http://www.red3d.com/cwr/boids/
このページは入門向けの内容ではなく高度な内容です。最低限の説明と、サンプル以外は用意されません。サンプルから読解できなければ内容を理解できないと思います。
このページで利用している用語は必ずしも CG や Boids の論文のそれと同じではありません。説明や簡単のために変更したりしています。
Abstract
Boids では、群れを表現するためには、(大まかに)次の3つの条件を成立させれば良いとされています。
- 群れの各個体は、群れ全体の中心(重心)へ移動しようとする。
- 群れの各個体は、互いに一定以上の距離を保つ。
- 群れの各個体は、群れ全体の移動速度や移動方向へ整列しようとする。
この3つが成立すれば、群れのように見えるわけです。
ここでは これにもう1つ条件を加えたいと思います。それは、群れはボスの方向へ進行する、という条件です。実用的なことを考えると、群れの進行方向はある程度制御できた方が良いので、この条件を加えます。特に求められる技術は変わりません。
フィールドと個体の用意
ここでは2次元の群れを表現します。2次元の群れが実現できれば、3次元の群れも実現できます。
2次元の群れを表現するにあたって、まずはフィールドを用意します。地面になる1枚の Plane と モデルを可視化するための Directional ライトが1つあれば良いです。
次いで、群れにする個体を用意します。モデルはどのようなものでも問題ありません。Prefab にモデルを追加して、そこに Rigidbody を追加します。Y 軸方向へ動かないようにするために、Inspector ビューから Rigidbody > Constrains の項目を開き、Freez Position の Y にチェックを入れます。
Boids を再現する
Boids のソースコードについて解説します。先に示した3つの条件を順に1つずつ解説する形になります。このソースコードは、シーンに空の GameObject を追加して、そこに追加しています。サンプルを確認してください。
群れを作る
まずは群れを作るところから入ります。指定した数だけ、群れにする個体を量産しましょう。ここでは実行時に1度だけインスタンスを追加することにします。
public なフィールドに個体の最大数を指定する変数を用意しておきます。量産する個体の GameObject も同じように public にしておくのが良いでしょう。これで Inspector ビューから群れにする GameObject とその最大数を変更できるようになります。
各個体をインスタンス化してシーンに追加します。初期位置はランダムにしておきましょう。また、群れの各個体は常に参照できるようにしておく必要があるので、シーンに追加すると同時に、配列として参照を維持します。
public int MaxChild = 30; public GameObject BoidsChild; public GameObject[] BoidsChildren; void Start () { this.BoidsChildren = new GameObject[MaxChild]; for (int i = 0; i < this.MaxChild; i++) { this.BoidsChildren[i] = GameObject.Instantiate(BoidsChild) as GameObject; this.BoidsChildren[i].transform.position = new Vector3(Random.Range(-50f, 50f), this.BoidsChild.transform.position.y, Random.Range(-50f, 50f)); } }
群れの中心を求める
条件の1つに、各個体が群れの中心へ移動しようとする、という条件があるので、まずは群れの中心を求めます。単純に各個体の座標から平均値を求めれば良いです。
ただし、ここでは、ボスの方向へ移動する、という条件も追加していますので、ボスの座標を最後に足しています。フィールドの BoidsBoss は、シーンに追加されたボスを設定します。BoidsCenter は群れの中心を可視化するために追加しています。Boids の本質的な仕組みには必要ありません。
public GameObject BoidsBoss; public GameObject BoidsCenter; void Update(){ Vector3 center = Vector3.zero; foreach (GameObject child in this.BoidsChildren) { center += child.transform.position; } center /= (BoidsChildren.Length - 1); center += this.BoidsBoss.transform.position; center /= 2; this.BoidsCenter.transform.position = center; …
条件-1 : 各個体は群れの中央へ移動しようとする
群れの中央が求められたので、各個体の進行方向を設定します。中央の座標からその個体の座標までの差分を求めれば、そのまま個体が必要な進行方向を示すベクトルが算出できます。各個体の進行方向は、追加した Rigidbody の velocity として設定します。
算出された値をそのまま個体の移動量として適用しても良いのですが、ここではより実用的な群れの表現のために工夫を加えます。Turbulence は乱れを示す係数です。その個体が元々進行しようとしていた力と、群れの中央へ移動しようとする力の割合を調節します。もし1が与えられれば、その個体は中央へ向かわずに、元々自分が進もうとしていた方向へ進みます。
public float Turbulence = 1f; … foreach (GameObject child in this.BoidsChildren) Vector3 dirToCenter = (center - child.transform.position).normalized; Vector3 direction = (child.rigidbody.velocity.normalized * this.Turbulence + dirToCenter * (1 - this.Turbulence)).normalized; direction *= Random.Range(20f, 30f); child.rigidbody.velocity = direction; }
後で数値を変えて遊べば直ぐに分かるのですが、殆どの場合に Turbulence の値は大きい方が生物らしさが現れます。この値の変更による見た目へのフィードバックは、その他の設定にも依存するのですがここでは割愛します。
個体が進行する速度も調節しましょう。個体ごとに速度のパラメータを持たせて各個体の個性を出すのも良いですが、ここではランダムです。表現する対象によって速度は適当に変更してください。
条件-2 : 各個体は一定の距離をとる
個体間の距離を算出して、指定した距離より短ければ片方の進行方向を変更するか、速度を減少する必要があります。ここでは進行方向を反転するように設定します。
foreach (GameObject child_a in this.BoidsChildren) { foreach (GameObject child_b in this.BoidsChildren) { if (child_a == child_b) { continue; } Vector3 diff = child_a.transform.position - child_b.transform.position; if (diff.magnitude < Random.Range(2, this.Distance)) { child_a.rigidbody.velocity = diff.normalized * child_a.rigidbody.velocity.magnitude; } } }
ここでも1つ工夫をしています。距離はフィールド変数 Distance によって指定しているのですが、最小値を 2、最大値を Distance としてランダムに変更しています。この設定によって各個体が侵入を許す距離が変動するのですが、生物の群れを表現する場合には、この方が自然に見えることが多いです。
固定値にすると個体の密度が高い場合には常に均等な距離を取ってしまい、どこか機械的な表現になります。現実的には生物もかなり高度なレベルで距離を取ったりするのですが、極端に表現した方が "らしさ" が増します。ランダムで取りうる値の範囲が小さいほど均整がとれた群れになるので、表現する対象によって適度に調整してください。
条件-3 : 各個体は平均移動ベクトルに合わせようとする
最後の条件です。各個体が持つ移動ベクトルから、群れ全体の平均移動ベクトルを算出して、各個体にその値を反映させます。平均移動ベクトルは群れの中心を求めた方法と同じようにして求めることができます。
ここで再びパラメータ Turbulence を活用します。Turbulence(乱れ) の値を利用して、個体の持つ移動ベクトルと、群れの移動ベクトルに引きずられる割合を調整します。値が大きいほど、個体は自分の移動ベクトルを優先し、群れの平均移動ベクトルを無視するようになります。したがって、1 を設定したときは、群れの中央にも向かわず、群れの速度も無視する個体が産まれることになります。
Vector3 averageVelocity = Vector3.zero; foreach (GameObject child in this.BoidsChildren) { averageVelocity += child.rigidbody.velocity; } averageVelocity /= this.BoidsChildren.Length; foreach (GameObject child in this.BoidsChildren) { child.rigidbody.velocity = child.rigidbody.velocity * this.Turbulence + averageVelocity * (1f - this.Turbulence); }
実のところ、この 条件-3 は実装しなくてもそれなりに動きます。Boids の群れモデルを基本的な実装で再現すると、都合上、個体数が増えるほどにオーダーが膨れ上がる上、平均化すると個体に表れる変化が小さくなります。したがって、不要なら条件3は無視しても良いかもしれません。例えば遠方の群れであったり、個体数があまりにも多い場合には、見た目にほとんど変化が現れない可能性があります。
より高度な群れの表現のために
実際には群れが分離したり、再度結合したりするわけですが、ここではそれらのアルゴリズムについては解説していません。より高度な、生物的な群れを表現するためには、更なるパラメータやアルゴリズムが必要です。
しかしながら簡単に実現できる群れの特徴も多くあります。例えば、群れ全体の平均移動ベクトルが小さく、ほとんど移動しないような場合に、各個体の速度が移動時のものと同じ程度では不自然です。ふつう、群れがあまり動いていないときは、食事のためであったりします(草食動物の群れなど)。あるいは危機的状況にないため、集団でいる必要がないのです(魚群など)。これらを表現するためには、例えば、平均移動ベクトルが設定値より小さいときは、各個体の距離を広くしたり、群れの平均移動ベクトルに合わせる比率を小さくすることなどが考えられます。
表現する対象によってここで紹介したパラメータを調整することはもちろん必要ですが、それだけでは不十分な場合が多いので、それぞれ工夫してみてください。ここではその例として、各個体の向き、回転の表現についていくつか紹介します。
各個体の向きを調整する
[Type1]
最も簡単なものは、個体の進行方向に合わせて回転させる方法です。群れ全体が移動しているときは一定の方向を向きますが、群れの移動が低速になると、各個体が衝突してバラバラの方向を向くようになり、移動時よりも統一感が損なわれます。群れの移動が低速になったタイミングで、各個体間の距離を広げて移動速度を落とせば、より生物らしく見えるでしょう。
[Type2]
餌に群がるような群れを表現するは、目標を見るようにすれば良いです。ここでは群れの中央に設定しています。例えば餌やプレイヤーが出現したときに、集団で向かっていくキャラクターを表現することができます。向きの設定や群れの移動方法を切り替える仕組みを実装する必要があります。
[Type3]
魚群のような、方向が揃うような群れは、群れの平均移動ベクトルを利用すれば表現できます。Type1 の方が生物の特性に近い挙動はしますが、魚群のようなものを表現する場合には、Type3 のような極端な表現の方がらしく見えます。ランダム性に乏しいですが群れ全体の移動量が少ないときにも各個体が同じ方向を向くので、特別な実装がなくても"らしく"見えます。遠くにいる背景のような群れであればこれで十分でしょう。もっともそのような場合にはパーティクルとビルボードを利用した方が良いでしょうが。
//Type1 child.transform.rotation = Quaternion.Slerp(child.transform.rotation, Quaternion.LookRotation(child.rigidbody.velocity.normalized), Time.deltaTime * 10f); //Type2 child.transform.LookAt(center); //Type3 child.transform.rotation = Quaternion.Slerp(child.transform.rotation, Quaternion.LookRotation(averageVelocity.normalized), Time.deltaTime * 3f);
Reference
サンプルプログラムに含まれる 3D モデルは次のページでフリーで配布されているものを活用しています。商用のものと比較してロイヤリティについて言及されていないので、問題が確認でき次第、配布するモデルを変更します。Thanks a lot.