「がんばれない」けど「がんばりたい」

ITエンジニアの仕事のこと。AI、機械学習、ディープラーニング。地頭力。車のこと。

Flow Field|Nature of Code

Vehicleオブジェクトを使用した記事も3つ目になります。これまでの記事で取り上げた事は、

flowFieldSample3DPerlinNoise from Kazuyoshi Ueno on Vimeo.

  1. 静的なオブジェクトではなく命を持ったオブジェクトを表現したい。

  2. 「生きているもの」を動かす。動く事は環境に依存する。

  3. また、そのオブジェクトが動くために発揮出来るファクターは色々あるが「力」と「スピード」に着目した。

  4. ファクターの大きさは…例えば象の様な大きい生物なのか?または蟻のような小さいものか?

  5. たとえ象の様に大きな「力」を持っていても「スピード」は、他の生き物よりも劣るかもしれない。

  6. 同じ象でも、その瞬間での「環境」によって発揮出来る「力」は常に最大ではないかもしれない。

などのコンセプトを基に記事を書いてきました。補足事項ですが「力」と「スピード」は可能な限り最大値を発揮出来るようにコーディングしてきました。


静的オブジェクトとの違いは次フレームのvelocityとlocationを決める時のコンセプトでした。具体的には風や重力といった外的要素の「加速度」のみを採用し、静的オブジェクトの次フレームでのvelocityとlocationを決める為に静的オブジェクトの持つ「加速度」に対して加算していました。これに対し命を持ったオブジェクトには次のvelocityとlocatioinを決める時に「加速度」ではなく「velocity」を基に計算された力Steerを「加速度」に加算して行きました。

ofVec3f steer = desired - velocity;

このコンセプトによって、ある時はスピードも速くて方向転換も速い。ある時はスピードも遅くて方向転換も遅い。等の表現を作る事が出来る様になり、動きをより自然に表現出来るようになりました。 Vehicleクラス(車)をモチーフとして使用した理由は、ある瞬間において、どんなに潜在力(トルク)があったとしても、環境に寄っては曲がりにくい!という事をシミュレートしやすいのと、勉強する事にあたってイメージしやすい事だと思っています。

さて、やっと今回の課題説明に入ります。これまではtargetの位置や環境の条件からdesiredを導いてきましたがdesiredになりうるvelocityが画面のグリッド上に並んでいたら?という事を考えて行きます。これを応用?すると、流体的な動きなどにもできる。今回のコードで出てくるクラスは以下。

FlowField
- グリッドを配列で管理
- 各グリッド内にはdesiredとなるベクトルを保持 - グリッドの大きさを指定出来る

Vehicle - 特に新しいコンセプトはない


FlowFieldクラス#1

上でも書いた様に得に難しい事はないが、忘れそうな事を。グリッドを保持する配列は2次元なのですがc++で2次元配列を作るにはダブルポインタを使います。ここまでは想像していたのですが、newで確保出来る配列は1次元までなので、まず1次元ずつ確保していく必要があります(というか、そう書いてあった)。

//ofVec3fの2次元配列をつくる

//ダブルポインタ
ofVec3f **fields;

//cols|グリッドの横数
//1つ目の配列|ofVec3f型のcols数分の先頭アドレスをfieldsに
fields = new ofVec3f*[cols];

//1次元配列へ添字で指定したアドレスに対して
//更にrows分の大きさのメモリを確保し、その先頭アドレスを代入
for (int i = 0; i < cols; i++)
{
  fields[i] = new ofVec3f[rows];
}

これでfields[i][j]としてアクセス出来るようになります。

■ FlowFieldクラス#2

Vehicleが現在居る位置がFlowField内のどのグリッド上に居るかを調べて、そのグリッドに設定されているofVec3fを返すメソッドを用意し、これをdesiredとします。これは簡単なのですが、普段こういう計算に慣れていないと、ちょっと、うっってなりました。x、y方向ずつで考えるのは良しとして。。x方向で考えると「今居る位置(location.x)が幾つグリッド幅を持っているか?」という表現になります。最終的にはfields[i][j]としてfeilds配列の要素を取り出す必要があるのでi,jは整数にする必要がある事と、配列数範囲外のアドレスにアクセスしてしまわないように制限も設ける必要があります。そうしないと、とんでもない値が帰ってきますね。制限するにはofClampを使用しています。

ofVec3f lookup(ofVec3f location)
{
  //resolution はグリッドの幅pixel
  int retColumn = int ofClamp( (location.x / resolution), 0, cols-1 );
  int retRow    = int ofClamp( (location.y / resolution), 0, rows-1 );
  return fields[retColumn][retRow];
}

■ FlowFieldクラス#3

field[i][j]のベクトルを表示する。シミュレーションするだけであれば得にベクトルを画面に表示する必要はないのですが、勉強と確認の為に描画メソッドを用意しています。forで各fieldsのベクトルを取り出し、基準点は各グリッド(正方形)の左上からグリッドの高さの半分下がったところにし、そこから線を引いて描画しています。ここで工夫しているところは、vectorを取り出して、そのx,y成分を起点からofLineで引いても良いのですがatan2でベクトルの方向(角度)を求め、起点を中心に角度分z軸で回転させてからベクトルの長さ分、線を引いています。ここ極座標系な考え方になります。イメージは、こんな感じ。

flow_field_02

コンパスで円を描くのと同じっぽいです。角度0°とした時、ベクトルの座標は(length, 0)です。その状態から角度分回転すれば良いという考え方。これが描画スピードとして最適かは未検証ですが極座標系の練習ですね。注意点としてはatan2で求めた角度に対して-1を掛けてあげること。ofRotateは引数が+の場合は時計回りになります。角度が+の場合Y値は基準点よりも上側でないとダメ。-1を掛けてあげないと、基準点よりも下側になってしまいます。

■ Vehicleクラス

desiredを求めるためにfollowというメソッドを用意しています。別にseekメソッドの実装を変更しても良いのですが意味的に新たに追加しています。ここでは引数にFlowFieldの参照を渡しています。理由はメソッド内でdesiredを求めるのですが、今回は上述の様にFlowFieldクラスのlookupメソッドで返却されるベクトルをdesiredとするためです。

void follow(FlowField flow)
{
    //desired velocityをFlowFieldからさがす
    ofVec3f desired = flow.lookup(location);
    desired.normalize();
    desired *= maxspeed; //FlowField内のvectorのlengthが1以内なので。

    ofVec3f steer = desired - velocity;
    steer.limit(maxforce);

    applyForce(steer);
}