4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Processingを利用して魚群再現をする(1)

Last updated at Posted at 2021-11-01

#魚群再現事始め
近年盛んになりつつあるProcessingを用いたジェネラティブアートの世界ですが、
某アニメを視聴して魚群の動きを再現することが可能かを検証するべく、実際に実装してみることにしました。実行環境についてはProcessing3.0を利用していきます。

#群体シミュレーションについて
群体シミュレーションをする場合であれば、Boidsアルゴリズムを利用することが一般的で、Boidsアルゴリズムの実装についてはhttps://qiita.com/odanny/items/e0c0a00e13c2b4839cec の記事を参考にしてもらうのが良いと思います。
簡単に説明すると、各オブジェクト間で

  • 分離 (Separation)
  • 整列 (Alignment)
  • 結合 (Cohesion)

の三種類の力を働かせることになります。
本実装では、3種類の力に加えて、壁に対して拒絶間(斥力)が働くように実装していきます。

#Fishクラスの実装
まずは、魚のクラスを実装していきます。
各変数はprivateにすることで外部からアクセスすることを防ぎます。
各変数についてはposが位置、velocityが速度、accelが加速度になります。
また、変数は現時点で魚のイメージの代わりに円を利用しているので、dは直径として利用する変数になります。

class Fish {
  private PVector pos;
  private PVector velocity;
  private PVector accel;
  private float d;
  private float radius;
  private float COEF_0, COEF_1, COEF_2, COEF_3;

Fish(float xpos, float ypos, float xsp, float ysp, float xac, float yac, float diameter) {
    pos=new PVector(xpos, ypos);
    velocity=new PVector(xsp, ysp);
    accel=new PVector(xac, yac);
    d=diameter;
  }

これからclassのメンバ関数を実装するのでこの時点で括弧は閉じません。
それでは、オブジェクト固有の関数を実装していきます。

##目視できる範囲の設定
###魚オブジェクトの捜索
魚オブジェクトの目視範囲の設定を行います。
キャンバス全体を目視範囲とすると、対象となる他のオブジェクトや壁の量があまりに大きくなり、例外が画面外に出てしまった場合には魚たちが画面外に飛んで行ってしまいます。
まずは、目視範囲の魚オブジェクトの捜索から始めます。

  private Fish[] SearchBoid(Fish[] fishes, float radius) {
    ArrayList<Fish> boid=new ArrayList<Fish>();

    PVector diff;
    for (int i=0; i<fishes.length; i++) {
      diff=PVector.sub(fishes[i].pos, pos);
      if (diff.mag()<radius && diff.mag()!=0) {
        boid.add(fishes[i]);
      }
    }

    Fish [] rslt=boid.toArray(new Fish[boid.size()]);
    return rslt;
  }

オブジェクトは配列として参照渡しを行います。
設定した半径の中に存在する魚オブジェクトをArrayListとして定義し、配列に変換したものを返します。

##Rule0(壁に対する斥力)
まず、壁に対する斥力を設定します。
壁を有無をintの二次元配列Fieldとして参照渡しを行い、
目視範囲内に壁が存在する場合、斥力を感じるように設定していきます。

  private PVector Rule0(int[][] Field, float radius) {
    PVector direction=new PVector(0, 0);
    for (int i=int(pos.x-radius); i<int(pos.x+radius); i++) {
      for (int j=int(pos.y-radius); j<int(pos.y+radius); j++) {
        //半径範囲内である場合
        if (sqrt(pow(pos.x-i, 2)+pow(pos.y-j, 2))<radius) {
          //境界範囲外の場合
          if ((i<0)||(Width<=i) || (j<0 || Height<=j)) {
            PVector diff=new PVector(pos.x-i, pos.y-j);
            direction=PVector.add(direction, PVector.div(diff, pow(diff.mag(), 2)));
          }
          //壁の存在を感知した場合
          else if (Field[i][j]==1) {
            PVector diff=new PVector(pos.x-i, pos.y-j);
            direction=PVector.add(direction, PVector.div(diff, pow(diff.mag(), 2)));
          }
        }
      }
    }
    return direction;
  }

本コードでは距離に対して二乗の大きさの斥力を加えます。
これは上記の記事のコードを踏襲したものであるため、魚同士の分離にも適用しています。

##Rule1(分離)
周囲の魚オブジェクトに対する斥力を適用していきます。
斥力の大きさはRule0と同様に距離に対する二乗の力が発生するように設定します。

 private PVector Rule1(Fish[] fishes) {

    PVector A=new PVector(0, 0);

    if (fishes.length!=0) {
      for (int i=0; i<fishes.length; i++) {
        //他の魚オブジェクトからの距離
        PVector diff = PVector.sub(pos, fishes[i].pos);
        if (diff.mag()!=0) {
          A = PVector.add(A, PVector.div(diff, pow(diff.mag(), 2)));
        }
      }
      A = PVector.div(A, fishes.length);
    }
    return A;
  }

##Rule2(整列)
視界の範囲内に存在する魚群の平均速度に合わせるように力が働きます。
この時、PVectorは2次元のベクトルなので、平均の速度から自分の速度を引きます。

  private PVector Rule2(Fish[] fishes) {
    PVector B = new PVector(0, 0);
    PVector rslt=new PVector(0, 0);

    if (fishes.length!=0) {
      for (int i=0; i<fishes.length; i++) {      
        B = PVector.add(B, fishes[i].velocity);
      }
      B = PVector.div(B, (fishes.length));
      rslt=PVector.sub(B, velocity);
    }
    return rslt;
  }

##Rule3(結合)
自分の視界の中の魚群の平均の位置に引き寄せられるように力が働きます。
Rule2と同様に自らの位置を引きます。

  private PVector Rule3(Fish[] fishes) {
    PVector C=new PVector(0, 0);
    PVector rslt=new PVector(0, 0);

    if (fishes.length!=0) {
      for (int i=0; i<fishes.length; i++) {
        C=PVector.add(C, fishes[i].pos);
      }
      C=PVector.div(C, fishes.length);
      rslt=PVector.sub(C, pos);
    }

    return rslt;
  }

##update関数の実装
publicで定義したupdate関数によって、Rule0からRule3を実行し、設定した係数をかけた合計を加速度とします。加速度を求めたうえで速度の更新、位置の更新を行います。
また、velocityを一定以上までになると、その範囲内に収まるように調整します。理由としては、速度が大きすぎると、画面外に飛び出してしまい魚が返ってこなくなることがあるためです。

  public void update(Fish[] fishes) {
    COEF_0=0.1;
    COEF_1=5;
    COEF_2=0.7;
    COEF_3=0.017;
    radius=20;

    Fish[] boids=SearchBoid(fishes, radius);
    PVector NUM0=PVector.mult(Rule0(Field, radius), COEF_0);
    PVector NUM1=PVector.mult(Rule1(boids), COEF_1);
    PVector NUM2=PVector.mult(Rule2(boids), COEF_2);
    PVector NUM3=PVector.mult(Rule3(boids), COEF_3);

    accel=PVector.add(PVector.add(NUM0, NUM1), PVector.add(NUM2, NUM3));

    velocity=PVector.add(velocity, accel);

    if (10<velocity.mag()) {
      velocity=PVector.mult(velocity.normalize(), 10);
    }

    pos=PVector.add(pos, velocity);
  }

##その他の関数
クラス内部に動作をサポートする関数を作成します。
RandomActive関数は動作の複雑性を増すためにランダムなベクトルを速度に足し合わせます。また、魚を表示するために専用の関数を設定した関数がShowFish関数になります。

  public void RandomActive() {
    PVector rand_vel=new PVector(0.1*randomGaussian(), 0.1*randomGaussian());
    velocity=PVector.add(velocity, rand_vel);
    pos=PVector.add(pos, velocity);
  }

  public void ShowFish() {
    ellipse(pos.x, pos.y, d, d);
  }

#プログラム本体の実装
次に、クラス外部のプログラムをまとめて示します。
全てのFishオブジェクトの配列をfishes、場所の定義をint型の配列Fieldで実現します。また、Fieldの定義に対して、画面上の表示はdraw関数内部でrect関数を利用して、直接書き込んでいますが全体の走査で自動化することも可能です。

int Width=1000;
int Height=1000;
int FishNum=500;

Fish [] fishes = new Fish[FishNum];
int [][] Field= new int[Width][];

void setField(int x_min, int y_min, int x_len, int y_len) {
  for (int i=x_min; i<x_min+x_len; i++) {
    for (int j=y_min; j<y_min+y_len; j++) {
      Field[i][j]=1;
    }
  }
}

void settings() {
  size(Width, Height);
}


void setup() {
  background(0, 0, 0);
  fill(255, 255, 255);
  noStroke();

  for (int i=0; i<FishNum; i++) {
    fishes[i]=new Fish(random(30, Width-30), random(30, Height-30), random(3), random(3), 0, 0, 5);
  }

  //場の設定
  for (int i=0; i<Width; i++) {
    Field[i]=new int[Height];
    for (int j=0; j<Height; j++) {
      if (i<30 || Width-30<i) {
        Field[i][j]=1;
      } else if (j<30 || Height<j) {
        Field[i][j]=1;
      } else {
        Field[i][j]=0;
      }
    }
  }
  setField(350, 400, 250, 200);
}


void draw() {
  background(0, 0, 0);
  fill(0, 0, 0);
    
  //Field
  fill(255, 0, 0);
  rect(350, 400, 250, 200);
  rect(0, 0, 30, Height);
  rect(0, 0, Width, 30);
  rect(Width-30, 0, 30, Height);
  rect(0, Height-30, Width, 30);

  fill(255, 255, 255);
  for (int i=0; i<FishNum; i++) {
    fishes[i].ShowFish();
  }

  for (int i=0; i<FishNum; i++) {
    fishes[i].update(fishes);
    fishes[i].RandomActive();
  }
}

#実行した場合の動作
以下に、実際に動作させたときの感じを示します。若干動作が変わっている可能性がありますが、大体動作はこんな感じになると思います。

test.gif

#これからの実装について
現時点で意思を持って動いているようには見えていても、魚が泳いでいるようには見えません。
原因としては壁や魚同士の斥力の設定(壁や魚に衝突しているように見える)や各係数の設定が原因であると考えられます。そのため、(2)以降は以下のようなことをやっていく予定です。

  • カメラを利用したFieldの設定 (動作速度の向上)
  • 魚群のアニメーション作成
  • パラメータの調整

(2)の記事についてはこちら

#注意
開発を進めながら投稿しているため、特定時点のプログラムを再現したものを掲載しています。そのため、そのままコピペしても動作しない可能性があります。その場合、都度修正して実行してみてください。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?