【Processing】公式サイトにあったFlockingを理解する過程

Webデザイン
Photo by Sigmund on Unsplash

こんにちは。コンスキです。

今回は、とあるコードを理解していく過程をここに示しておきます。

理解するコード

今回理解しようと思っているのはProcessingの公式サイトにあったソースコードです。

次のような「Flacking(群れ)」のシミュレーションを実行するコードになってます。

コードの全体像はこちらにあります。

理解したい理由

このシミュレーションを理解したい理由は次の2つです。

  • クラスがたくさん使われているコードに慣れたいため
  • 改良して別のシミュレーションに使いたいため

クラスがたくさん使われているコードに慣れたい

僕は普段、ほとんどクラスを使いません。

クラスに対する理解が足りていないためです。

また、これまでのプログラミングではそこまで無理して使う必要がないと思っていたためです。

しかし、実現したい動作を実現することができないことがよくあります。

クラスをうまく使えたら、そんな動作を実現できていたかもしれません。

今回理解したい「Flacking」のコードには、いくつかのクラスが宣言が使われています。

こちらのコードを理解する中でクラスの使われるコードに慣れて、積極的にクラスを使えるようになりたいです。

改良して別のシミュレーションに使いたい

Processingで作品を作ってみたいです。

最初はシンプルなものでもいいと思うのですが、どうせなら「すごい!」思えるような作品を作りたいです。

「Flocking」というシミュレーションは、それ単体でもすごいので、応用したら作品の完成度を上げられるのではないかと思っています。

コードを読む上で気をつけたいこと

コードを読む上で気をつけたいことは次の3つです。

  • あらかじめどんなコードが必要か知っておく
  • 理解する範囲を度々区切って読む
  • 細かい部分まで完璧に理解しようとしすぎない

あらかじめどんな機能が必要か知っておく

初めはコードについて何も知らない状態です。

その状態でコードの最初から最後まで、順番に理解していくのはとても難しいことだと思います。

そこで、シミュレーションをしっかり観察して、コードの理解を始める前にどんな機能が必要なのかを考えてこうと思います。

その後、似ているシミュレーションについてネットで調べます。

理解する範囲を度々区切って読む

全体をまんべんなく読んでいると、どの部分のコードも理解が中途半端になってしまい、なかなか理解が進まない気がします。

「ここを理解する!」と決めて、狭い範囲のコードの理解に集中しようと思います。

部分的に理解できたら、他の部分の理解が早まるかもしれません。

細かい部分まで理解しようとしすぎない

「Flocking」というシミュレーションは、シミュレーションというだけあって、難しい数学が出てきそうです。

数学を完璧に理解しなくても、シミュレーションの概要が理解できれば、コードの改良をできると信じます。

事前準備

自分で予想する

シミュレーションの動きを見て次のような機能が必要だと予想しました。

  • 決められた方向の三角形を描く
  • 三角形が次に進む座標を計算する
  • 三角形の速度を計算する
  • クリックした位置に三角形を描く

見ればわかる、あまりにも当たり前なことかもしれません。

しかし、難しいコードは、わかりやすいコードの部分の理解から、だんだんわかっていくものだとおもいます。

インターネットを使う

インターネットで「Flacking simulation」と調べました。

すると、動き回っていた一つ一つの三角形は3つのルールに従っていることがわかりました。

関係ないですが「デザインの4原則?」に似ています。

1. 衝突回避

他の三角形にぶつからないようにある程度近づいたら距離を離します。

2. 整列

移動する速度と方向を近くの三角形に合わせます。

3. 接近

近くに三角形があったらその三角形の方に近づきます。ある程度近づいたら一定の距離を保ちます。

全体を見る

「理解する範囲を区切って読む」ことに注意したいと先ほど書きました。

しかしながら、はじめはどこにどんな感じのコードが書いてあるのかすらわかりません。

そこで、まず全体を軽く見渡します。

Processing公式サイトに載っていたソースコードを下に示してあります。

Flock flock;

void setup() {
  size(640, 360);
  flock = new Flock();
  // Add an initial set of boids into the system
  for (int i = 0; i < 150; i++) {
    flock.addBoid(new Boid(width/2,height/2));
  }
}

void draw() {
  background(50);
  flock.run();
}

// Add a new boid into the System
void mousePressed() {
  flock.addBoid(new Boid(mouseX,mouseY));
}
// The Boid class

class Boid {

  PVector position;
  PVector velocity;
  PVector acceleration;
  float r;
  float maxforce;    // Maximum steering force
  float maxspeed;    // Maximum speed

    Boid(float x, float y) {
    acceleration = new PVector(0, 0);

    // This is a new PVector method not yet implemented in JS
    // velocity = PVector.random2D();

    // Leaving the code temporarily this way so that this example runs in JS
    float angle = random(TWO_PI);
    velocity = new PVector(cos(angle), sin(angle));

    position = new PVector(x, y);
    r = 2.0;
    maxspeed = 2;
    maxforce = 0.03;
  }

  void run(ArrayList<Boid> boids) {
    flock(boids);
    update();
    borders();
    render();
  }

  void applyForce(PVector force) {
    // We could add mass here if we want A = F / M
    acceleration.add(force);
  }

  // We accumulate a new acceleration each time based on three rules
  void flock(ArrayList<Boid> boids) {
    PVector sep = separate(boids);   // Separation
    PVector ali = align(boids);      // Alignment
    PVector coh = cohesion(boids);   // Cohesion
    // Arbitrarily weight these forces
    sep.mult(1.5);
    ali.mult(1.0);
    coh.mult(1.0);
    // Add the force vectors to acceleration
    applyForce(sep);
    applyForce(ali);
    applyForce(coh);
  }

  // Method to update position
  void update() {
    // Update velocity
    velocity.add(acceleration);
    // Limit speed
    velocity.limit(maxspeed);
    position.add(velocity);
    // Reset accelertion to 0 each cycle
    acceleration.mult(0);
  }

  // A method that calculates and applies a steering force towards a target
  // STEER = DESIRED MINUS VELOCITY
  PVector seek(PVector target) {
    PVector desired = PVector.sub(target, position);  // A vector pointing from the position to the target
    // Scale to maximum speed
    desired.normalize();
    desired.mult(maxspeed);

    // Above two lines of code below could be condensed with new PVector setMag() method
    // Not using this method until Processing.js catches up
    // desired.setMag(maxspeed);

    // Steering = Desired minus Velocity
    PVector steer = PVector.sub(desired, velocity);
    steer.limit(maxforce);  // Limit to maximum steering force
    return steer;
  }

  void render() {
    // Draw a triangle rotated in the direction of velocity
    float theta = velocity.heading2D() + radians(90);
    // heading2D() above is now heading() but leaving old syntax until Processing.js catches up
    
    fill(200, 100);
    stroke(255);
    pushMatrix();
    translate(position.x, position.y);
    rotate(theta);
    beginShape(TRIANGLES);
    vertex(0, -r*2);
    vertex(-r, r*2);
    vertex(r, r*2);
    endShape();
    popMatrix();
  }

  // Wraparound
  void borders() {
    if (position.x < -r) position.x = width+r;
    if (position.y < -r) position.y = height+r;
    if (position.x > width+r) position.x = -r;
    if (position.y > height+r) position.y = -r;
  }

  // Separation
  // Method checks for nearby boids and steers away
  PVector separate (ArrayList<Boid> boids) {
    float desiredseparation = 25.0f;
    PVector steer = new PVector(0, 0, 0);
    int count = 0;
    // For every boid in the system, check if it's too close
    for (Boid other : boids) {
      float d = PVector.dist(position, other.position);
      // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < desiredseparation)) {
        // Calculate vector pointing away from neighbor
        PVector diff = PVector.sub(position, other.position);
        diff.normalize();
        diff.div(d);        // Weight by distance
        steer.add(diff);
        count++;            // Keep track of how many
      }
    }
    // Average -- divide by how many
    if (count > 0) {
      steer.div((float)count);
    }

    // As long as the vector is greater than 0
    if (steer.mag() > 0) {
      // First two lines of code below could be condensed with new PVector setMag() method
      // Not using this method until Processing.js catches up
      // steer.setMag(maxspeed);

      // Implement Reynolds: Steering = Desired - Velocity
      steer.normalize();
      steer.mult(maxspeed);
      steer.sub(velocity);
      steer.limit(maxforce);
    }
    return steer;
  }

  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  PVector align (ArrayList<Boid> boids) {
    float neighbordist = 50;
    PVector sum = new PVector(0, 0);
    int count = 0;
    for (Boid other : boids) {
      float d = PVector.dist(position, other.position);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(other.velocity);
        count++;
      }
    }
    if (count > 0) {
      sum.div((float)count);
      // First two lines of code below could be condensed with new PVector setMag() method
      // Not using this method until Processing.js catches up
      // sum.setMag(maxspeed);

      // Implement Reynolds: Steering = Desired - Velocity
      sum.normalize();
      sum.mult(maxspeed);
      PVector steer = PVector.sub(sum, velocity);
      steer.limit(maxforce);
      return steer;
    } 
    else {
      return new PVector(0, 0);
    }
  }

  // Cohesion
  // For the average position (i.e. center) of all nearby boids, calculate steering vector towards that position
  PVector cohesion (ArrayList<Boid> boids) {
    float neighbordist = 50;
    PVector sum = new PVector(0, 0);   // Start with empty vector to accumulate all positions
    int count = 0;
    for (Boid other : boids) {
      float d = PVector.dist(position, other.position);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(other.position); // Add position
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      return seek(sum);  // Steer towards the position
    } 
    else {
      return new PVector(0, 0);
    }
  }
}
// The Flock (a list of Boid objects)

class Flock {
  ArrayList<Boid> boids; // An ArrayList for all the boids

  Flock() {
    boids = new ArrayList<Boid>(); // Initialize the ArrayList
  }

  void run() {
    for (Boid b : boids) {
      b.run(boids);  // Passing the entire list of boids to each boid individually
    }
  }

  void addBoid(Boid b) {
    boids.add(b);
  }

}

3つのコードを見ると、setup関数とdraw関数が「flacking.pde」にあることから、「flacking.pde」はメインのファイルだという感じがします。

残りの2ファイルには初めの行に、何が書いてあるファイルなのかが説明されています。

「boid.pde」はBoidクラスを宣言しているファイルみたいです。

「flack.pde」はBoidオブジェクトのリストだと書いてあります。

ちなみに度々出てくる「Boid」という単語の意味が気になったので調べてみました。

人工生命体のことです。

ということで、ここからは三角形のことをボイドと呼んでいきます。

メインのファイルを見てみる

一番最初に「flacking.pde」の中身を見てみます。

Flock flock;

void setup() {
  size(640, 360);
  // fullScreen();
  frameRate(120);
  flock = new Flock();
  // Add an initial set of boids into the system
  for (int i = 0; i < 150; i++) {
    flock.addBoid(new Boid(width/2,height/2));
  }
}

void draw() {
  background(50);
  flock.run();
}

// Add a new boid into the System
void mousePressed() {
  flock.addBoid(new Boid(mouseX,mouseY));
}

上のファイルにはコメントが書かれている部分が2つあったので、それぞれコメントを読んでいきます。

// Add an initial set of boids into the system
for (int i = 0; i < 150; i++) {
  flock.addBoid(new Boid(width/2,height/2));
}

まずはこの部分です。

コメントが意味しているのは次のようなことです。

システムに初期のボイド(三角形)のセットを加える

そういえばシステムを起動したときに、いくつかボイド(三角形)が表示されていました↓

2つ目のコメントがある部分がこちらです。

// Add a new boid into the System
void mousePressed() {
  flock.addBoid(new Boid(mouseX,mouseY));
}

こちらのコメントはおそらくこんな感じの意味になると思います。

システムに新しいボイドを加える

この部分のコードは、クリックしたらボイドが表示される処理をする部分だとわかりました。

次回に続きます

ここまで見てくださってありがとうございます。

こちら↓の記事に続きます。

コメント

タイトルとURLをコピーしました