前回はBoidモデルのプログラミング方法について説明しました。
今回は魚の移動方向のプログラミングについてお話ししたいと思います。
目次
魚の移動方向
魚はこれまで説明したように、Boidモデルの3つの行動規則によって移動方向が決まります。
- 分離(Separation)
- 整列(Alignment)
- 結合(Cohesion)
それぞれの力(移動方向)を計算する式と速度や位置の更新方法は、第2回目で説明したとおりです。
ここでは魚の移動方向と速度、位置の更新部分のプログラムを作成します。
魚の移動方向のプログラム
魚の移動方向(力)は、粒子法プログラムのparticleForce関数で計算します。粒子法では粒子は圧力や粘性力、重力によって力を受けますが、魚は先程の3つの行動規則による力を受けます。
function particleForce() {
for (let i = 0, n = p.length; i < n; i++) {
if (!p[i].active) continue;
let forceSeparation = new Vector();
let forceAlignment = new Vector();
let forceCohesion = new Vector();
let velAlignment = new Vector();
let center = new Vector();
let numAlignment = 0, numCohesion = 0;
p[i].forNeighbor(cell, function(pNeighbor, rv) {
if (p[i] !== pNeighbor) {
const r = rv.magnitude();
// separation force
if (r > 0 && r <= radiusSeparation) {
forceSeparation.x += rv.x / (r * r);
forceSeparation.y += rv.y / (r * r);
}
// alignment force
if (r <= radiusAlignment) {
velAlignment.x += pNeighbor.velocity.x;
velAlignment.y += pNeighbor.velocity.y;
numAlignment++
}
// cohesion force
if (r <= radiusCohesion) {
center.x += pNeighbor.position.x;
center.y += pNeighbor.position.y;
numCohesion++;
}
}
});
if (numAlignment > 0) {
velAlignment.x /= numAlignment;
velAlignment.y /= numAlignment;
forceAlignment = velAlignment.sub(p[i].velocity);
}
if (numCohesion > 0) {
center.x /= numCohesion;
center.y /= numCohesion;
forceCohesion = center.sub(p[i].position);
}
// update
forceSeparation.unit();
forceAlignment.unit();
forceCohesion.unit();
p[i].force.x = wSeparation * forceSeparation.x + wAlignment * forceAlignment.x + wCohesion * forceCohesion.x;
p[i].force.y = wSeparation * forceSeparation.y + wAlignment * forceAlignment.y + wCohesion * forceCohesion.y;
}
}
pは魚を表しており、全ての魚でループして計算していきます。
魚にかかる力はそれぞれ、forceSeparationが分離、forceAlignmentが整列、forceCohesionが結合による力を表します。
p[i].forNeighbor(cell, function(pNeighbor, rv)は、粒子法と同じで、近傍の魚に対してループして和をとっていきます。
分離(Separation)
分離による力は先のページの(2-1)式で計算できます。
// separation force
if (r > 0 && r <= radiusSeparation) {
forceSeparation.x += rv.x / (r * r);
forceSeparation.y += rv.y / (r * r);
}
rは近傍の魚と自分との距離で、半径radiusSeparationの円内にいる魚に対して、(2-1)式の和を計算します。
整列(Alignment)
整列による力は、(2-2)式、(2-3)式で計算します。
// alignment force
if (r <= radiusAlignment) {
velAlignment.x += pNeighbor.velocity.x;
velAlignment.y += pNeighbor.velocity.y;
numAlignment++
}
radiusAlignmentより距離の近い魚に対して(2-2)式で群れの速度velAlignmentを計算しています。numAlignmentは群れの魚の数をカウントしています。
if (numAlignment > 0) {
velAlignment.x /= numAlignment;
velAlignment.y /= numAlignment;
forceAlignment = velAlignment.sub(p[i].velocity);
}
力forceAlignmentは、(2-3)式で計算しています。
結合(Cohesion)
結合による力は(2-4)式、(2-5)式で表されます。
// cohesion force
if (r <= radiusCohesion) {
center.x += pNeighbor.position.x;
center.y += pNeighbor.position.y;
numCohesion++;
}
結合は、半径radiusCohesionの円内にいる他の魚の群れの中心centerを(2-4)式から計算し、
if (numCohesion > 0) {
center.x /= numCohesion;
center.y /= numCohesion;
forceCohesion = center.sub(p[i].position);
}
(2-5)式から力forceCohesionを求めています。
力の合成
求めた3つの力は、(2-6)式で重み係数をかけて足し合わせます。
// update
forceSeparation.unit();
forceAlignment.unit();
forceCohesion.unit();
p[i].force.x = wSeparation * forceSeparation.x + wAlignment * forceAlignment.x + wCohesion * forceCohesion.x;
p[i].force.y = wSeparation * forceSeparation.y + wAlignment * forceAlignment.y + wCohesion * forceCohesion.y;
各力は、.unit()で単位ベクトルにした後、重み係数wSeparation、wAlignment、wCohesionをかけて合成しています。
速度と位置の更新
魚の速度と位置の更新は、粒子法と同じくmotionUpdate関数で行います。
function motionUpdate() {
setParticleToCell();
particleForce();
for (let i = 0, n = p.length; i < n; i++) {
if (!p[i].active) continue;
p[i].velocity.x += p[i].force.x * timeDelta;
p[i].velocity.y += p[i].force.y * timeDelta;
p[i].velocity.limitMax(maxVelocity);
p[i].position.x += p[i].velocity.x * timeDelta;
p[i].position.y += p[i].velocity.y * timeDelta;
if (p[i].position.x < region.left) {
p[i].position.x = region.right - (region.left - p[i].position.x);
} else if (p[i].position.x > region.right) {
p[i].position.x = region.left + (p[i].position.x - region.right);
}
if (p[i].position.y < region.bottom) {
p[i].position.y = region.top - (region.bottom - p[i].position.y);
} else if (p[i].position.y > region.top) {
p[i].position.y = region.bottom + (p[i].position.y - region.top);
}
}
}
setParticleToCell()は、粒子を空間分割法のセルに分ける関数です。
p[i].velocity.x += p[i].force.x * timeDelta;
p[i].velocity.y += p[i].force.y * timeDelta;
p[i].velocity.limitMax(maxVelocity);
p[i].position.x += p[i].velocity.x * timeDelta;
p[i].position.y += p[i].velocity.y * timeDelta;
粒子法の速度と位置はLeap-Frog法で更新しましたが、今回は単純に(2-7)式と(2-8)式よりオイラー的に更新します。ただ、魚の速度に関しては、大きくなりすぎないように.limitMaxメソッドで、速度に制限を設けています(最大値はmaxVelocity)。
motionUpdate関数では次に境界条件の取り扱いがプログラムされていますが、これは次回説明することにしましょう。
まとめ
今回は、魚の移動方向と速度、位置の更新部分のプログラムについて説明しました。
次回は、境界条件や初期条件の取り扱いについて説明します。