始めに
これまでに何度か空間に力場を設定して、Boidsシミュレーションに組み込んで意図する挙動をさせてきました。
ブログには書いていませんでしたが、twitterに載せたこちらも空間に力場を作って魚群を円状に動かしています。
水流による魚の制御
— したかみ (@CCPJ_a) June 13, 2022
簡単に作った状態なのでまだ綺麗ではないけど、ある程度の可能性を感じた pic.twitter.com/rC8t8vIDFp
しかし、力場を作るのにはいくつか問題点がありました。
- 3次元空間を格子状に分けるため、データ量が多くなる
- 空間を10 x 10 x 10で区切っても、1000個の格子ができる
- 空間を細かく区切るとデータ量が莫大になる
- 格子ごとの力場が不連続
- 個体の動きが少しの変化で大きく変化する
今回はこれらの問題を解決するために、線形補間を使って力場から受ける力を求めていきます。
空間を分割する
空間を分割する計算について簡単に説明します。
分割する格子の一辺の長さをgridScale
とします。格子の原点は最小の頂点とし、0番目の格子の頂点が空間の原点と重なるように配置します。
このように格子を配置することで、ある地点position
で属する格子のindex
は次のように求められます。
public static int3 CalculateGridIndex(float3 position, float gridScale) { return (int3) math.floor(position / gridScale); }
力場を設定する
分割した格子に力場を設定します。今回はデバッグ用に各格子の中央から原点へ向かう力を設定します。
先ほど作った格子のindex
を求めるメソッドを使って次のように書いています。
// 任意の格子のみに力を設定できるようNativeHashMapを使用 var vectorField = new NativeHashMap<int3, float3>(gridCount * gridCount * gridCount, Allocator.Persistent); var gridIndex = CalculateGridIndex(gridPosition, gridScale); var gridCenter = gridPosition + new float3(gridScaleHalf); var gridCenterToCenter = (float3)center - gridCenter; var vector = math.normalize(gridCenterToCenter); vectorField.TryAdd(gridIndex, vector);
最も近くにある原点を持つ格子を求める
ある地点の周りにある力場から線形補間を行います。
まずはある地点から最も近くにある原点を持つ格子を求めます。求める方法は次の通りです。
public static int3 CalculateOriginVectorFieldIndex( float3 position, float vectorFieldGridScale ) { var halfGridScale = vectorFieldGridScale / 2f; return (int3)math.floor((position + halfGridScale) / vectorFieldGridScale); }
ある地点が属する格子の半分より小さい場合は属する格子の原点が近く、反対に格子の半分以上の場合は次の格子の原点が近くなります。これらを判別するために、ある地点position
に格子の一辺の大きさの半分halfGridScale
を加えてから、格子のIndexを求める計算をします。
線形補間を行うときはこの格子を基準にしますので、ここでは基準格子と名づけます。
割合を求める
次に線形補間を行うための割合を求めます。求める処理は次の通りです。
public static float3 CalculateVectorFieldRate( float3 position, float vectorFieldGridScale ) { var originIndex = CalculateOriginIndex(position, vectorFieldGridScale); var originPosition = originIndex * vectorFieldGridScale; var rate = (position - originPosition) / vectorFieldGridScale; return rate + new float3(0.5f); }
割合を計算は、先ほど求めた基準格子の原点座標を求めます。次にある地点の座標と基準格子の原点座標との差を計算し、格子の一辺の長さで割ります。こうすることで -0.5 から 0.5 までの範囲の値が求まります。あとは 0.5 を加えて正規化することで、割合が求まります。
力場から受ける力を求める
最後に求めた割合を使って線形補間を行います。線形補間を行う力場は基準格子から一つ前の格子で行います。
今回は3次元の線形補間を行いますので、基準となる格子の数は8個です。プログラムでは基準格子の一つ前の格子を 0 とし、基準格子を 1 として表しています。格子 0 の値を使用する場合は 1 - rate
, 格子 1 の値を使用する場合は rate
を掛けて合計を求めます。
var fieldVector = (1 - rate.x) * (1 - rate.y) * (1 - rate.z) * vectorX0Y0Z0 + rate.x * (1 - rate.y) * (1 - rate.z) * vectorX1Y0Z0 + (1 - rate.x) * rate.y * (1 - rate.z) * vectorX0Y1Z0 + rate.x * rate.y * (1 - rate.z) * vectorX1Y1Z0 + (1 - rate.x) * (1 - rate.y) * rate.z * vectorX0Y0Z1 + rate.x * (1 - rate.y) * rate.z * vectorX1Y0Z1 + (1 - rate.x) * rate.y * rate.z * vectorX0Y1Z1 + rate.x * rate.y * rate.z * vectorX1Y1Z1;
動作確認
実際に力場を作って、線形補間で正しく力が求められているかを確認します。
始めに任意の地点から原点 (0, 0, 0) へ向かう力を力場に設定し、その力場をもとに線形補完で求められた力を表示してみます。
結果は以下の画像の通りです。赤い矢印が実際に力場が持つ力で、白の矢印が力場の力を線形補間して求めた力です。線形補間によって、白の矢印も原点に向かう力となっています。
次に、力場に設定されている力の数を減らしてみます。こちらでも先程と同様の結果が求められていることが分かります。
最後に力場にランダムな力を設定して、各地点での力を線形補間で求めてみます。ある程度、流れが分かるぐらいには力場から受ける力が求められていることが分かります。
※後ほどコードを整理してここに載せます。
まとめ
線形補間を用いることで、少ない力場の値からおおよその値が求められることが確認できました。最初に述べた力場の問題に関してもこの手法を使うことで、解決できそうです。
この手法を使うことで様々な応用ができそうですが、今回はここまでにします。 今後はBoidsシミュレーションに組み込んで魚群を操作することができるのかを試す予定です。
蛇足
補間の手法についてはあまり知識がなく、ChatGPTに質問してみるとスプライン補間や多項式補間など聞いたことあるような無いようなものが出てきました。
もしかしたら今回の内容も本当は別の名前があったのかもしれません。
今回は単純な線形補間で目的が達成できたのでここで終わりですが、余裕ややる気があれば他の補間も試してみたいです。