見出し画像

Processing でグラフを描く㉑ ボイドモデル

Processing でグラフを描く 第21回目です。

前回「ライフゲーム」で人工生命を創造し、まるで生きているかのようなドットの明滅を観察しました。周辺セルの状態により「生存、死亡」を決める簡単なルールで、画面上に人工生命を召喚できるのは驚きでした。

今回は人工生命の第2弾「ボイドモデル」で遊びます。ボイド(boids)は鳥っぽい(bird + oid)が語源の言葉で、鳥の群れをシミュレーションするための人工生命モデルです。

ボイドモデル

ボイドモデルはアメリカのアニメーション・プログラマであるクレイグ・レイノルズが考案、作成した人工生命モデルです。コンピュータ上の鳥たちは3つのルールに従って、画面上を自由に動きます。鳥たちは驚くほど自然な動きを見せ、簡単なルールで群れの動きを再現できることを示しました。

ボイドモデルのルール

  1. 集合 鳥は群れの中心に向かって動く

  2. 分離 鳥同士はぶつからないように距離を取る

  3. 整列 鳥は周りの鳥の動きと同じ方向に動く

この3つのルールを含む、鳥の群れが動き回るプログラムを、Python で作ります。まずは画面上に鳥を表示して自由に動かすプログラムを書きます。

鳥が自由に動き回るプログラム

import random

bird_list = []  # 鳥を入れておくための空のリスト
COLORS = [
    '#FFA5CC',  # pink
    '#FFC726',  # yellow
    '#FF3424',  # red
    '#80FF25',  # green
    '#A0D4FF',  # skyblue
    '#CCCCCC',  # white
    '#FEA89F',  # lightpink
    '#2A2B4F',  # darkblue
]
BIRD_NUM = 50
RANGE_OF_DIRECTIONS = [0, 360]


def setup():
    size(600, 600)
    for i in range(BIRD_NUM):
        bird_list.append(Bird(i))


def draw():
    background(0)
    # 鳥の群れを表示
    for bird in bird_list:
        bird.update()
        bird.draw()


class Bird:
    def __init__(self, bird_id):
        """鳥の初期化"""
        self.bird_id = bird_id
        self.direction = random.uniform(
            radians(RANGE_OF_DIRECTIONS[0]),
            radians(RANGE_OF_DIRECTIONS[1])
        )
        self.position = [random.uniform(0, width), random.uniform(0, height)]
        self.velocity = [cos(self.direction), sin(self.direction)]
        self.radius = random.uniform(5, 6)
        self.acceleration = [0, 0]
        self.acceleration_to_cohere = [0, 0]
        self.acceleration_to_separate = [0, 0]
        self.acceleration_to_align = [0, 0]
        self.col = random.choice(COLORS)

    def update(self):
        self.position[0] += self.velocity[0]
        self.position[1] += self.velocity[1]
        # 鳥が壁にぶつかったら反対側に通り抜ける
        if self.position[0] > width or self.position[0] < 0:
            self.position[0] = abs(self.position[0] - width)
        if self.position[1] > height or self.position[1] < 0:
            self.position[1] = abs(self.position[1] - height)

    def draw(self):
        pushMatrix()
        translate(self.position[0], self.position[1])
        rotate(self.direction)
        stroke(255)
        fill(self.col)
        ellipse(0, self.radius, 5, 5)
        ellipse(0, -self.radius, 5, 5)
        ellipse(0, 0, self.radius * 2, self.radius * 2)
        fill(0)
        ellipse(self.radius * cos(PI / 6), self.radius * sin(PI / 6), 3, 3)
        ellipse(self.radius * cos(-PI / 6), self.radius * sin(-PI / 6), 3, 3)
        popMatrix()
boids free

randomモジュールを使って、鳥の位置、方向をランダムに決めました。鳥同士は相互作用しないので、鳥は、初めに決められた方向に一直線に動きます。壁をすり抜けて反対側に現れるように実装しました。

円を5つ使ってカラフルな鳥を作ったのですが、何かに似ているような気がします。まあ気にしないで次に進みましょう。

集合ルールを追加する

import random

bird_list = []  # 鳥を入れておくための空のリスト
COLORS = [
    '#FFA5CC',  # pink
    '#FFC726',  # yellow
    '#FF3424',  # red
    '#80FF25',  # green
    '#A0D4FF',  # skyblue
    '#CCCCCC',  # white
    '#FEA89F',  # lightpink
    '#2A2B4F',  # darkblue
]
BIRD_NUM = 50
RANGE_OF_DIRECTIONS = [0, 360]
POWER_OF_COHERE = 0.1
RADIUS_OF_COHERE = 200


def setup():
    size(600, 600)
    for i in range(BIRD_NUM):
        bird_list.append(Bird(i))


def draw():
    background(0)
    distances_list = distances_of_vectors([bird.position for bird in bird_list])
    # 鳥の群れを表示
    for bird in bird_list:
        bird.cohere(distances_list)  # 結合
        bird.update()
        bird.draw()


class Bird:
    def __init__(self, bird_id):
        """鳥の初期化"""
        self.bird_id = bird_id
        self.direction = random.uniform(
            radians(RANGE_OF_DIRECTIONS[0]),
            radians(RANGE_OF_DIRECTIONS[1])
        )
        self.position = [random.uniform(0, width), random.uniform(0, height)]
        self.velocity = [cos(self.direction), sin(self.direction)]
        self.radius = random.uniform(5, 6)
        self.acceleration = [0, 0]
        self.acceleration_to_cohere = [0, 0]
        self.acceleration_to_separate = [0, 0]
        self.acceleration_to_align = [0, 0]
        self.col = random.choice(COLORS)

    def update(self):
        self.acceleration = add_vectors(
            self.acceleration_to_cohere,
            self.acceleration_to_separate,
            self.acceleration_to_align,
        )
        self.velocity = unit_vector(add_vectors(self.velocity, self.acceleration))
        self.direction = atan2(self.velocity[1], self.velocity[0])
        self.position[0] += self.velocity[0]
        self.position[1] += self.velocity[1]
        # 鳥が壁にぶつかったら反対側に通り抜ける
        if self.position[0] > width or self.position[0] < 0:
            self.position[0] = abs(self.position[0] - width)
        if self.position[1] > height or self.position[1] < 0:
            self.position[1] = abs(self.position[1] - height)

    def draw(self):
        pushMatrix()
        translate(self.position[0], self.position[1])
        rotate(self.direction)
        stroke(255)
        fill(self.col)
        ellipse(0, self.radius, 5, 5)
        ellipse(0, -self.radius, 5, 5)
        ellipse(0, 0, self.radius * 2, self.radius * 2)
        fill(0)
        ellipse(self.radius * cos(PI / 6), self.radius * sin(PI / 6), 3, 3)
        ellipse(self.radius * cos(-PI / 6), self.radius * sin(-PI / 6), 3, 3)
        popMatrix()

    def cohere(self, distances_l):
        near_bird_ids = [d[1] for d in distances_l[self.bird_id] if 0 < d[0] < RADIUS_OF_COHERE]
        if len(near_bird_ids) > 0:
            near_bird = [bird_list[bird_id] for bird_id in near_bird_ids]
            center_of_near = center_of_vectors([bird.position for bird in near_bird])
            self.acceleration_to_cohere = scale_vector(
                POWER_OF_COHERE,
                unit_vector(subtract_vectors(center_of_near, self.position))
            )
        else:
            self.acceleration_to_cohere = [0, 0]


def distances_of_vectors(vectors):
    vector_num = len(vectors)
    distance_list = [[0] * vector_num for _ in range(vector_num)]
    for i in range(vector_num):
        distance_list[i][i] = (0, i)
        for j in range(vector_num):
            if i < j:
                distance = distance_vectors(vectors[i], vectors[j])
                distance_list[i][j] = (distance, j)
                distance_list[j][i] = (distance, i)
    return distance_list


# 以下、ベクトル操作の関数
def add_vectors(*vectors):
    return list(map(sum, zip(*vectors)))


def subtract_vectors(v1, v2):
    return list(v1 - v2 for (v1, v2) in zip(v1, v2))


def center_of_vectors(vectors):
    return scale_vector(1.0 / len(vectors), add_vectors(*vectors))


def length_vector(v):
    return sqrt(sum([coord ** 2 for coord in v]))


def distance_vectors(v1, v2):
    return length_vector(subtract_vectors(v1, v2))


def scale_vector(scalar, v):
    return list(scalar * coord for coord in v)


def unit_vector(v):
    if length_vector(v):
        return scale_vector(1. / length_vector(v), v)
    else:
        return v
cohesion

先ほどの自由な動きに「集合ルール」を追加しました。集合ルールは自分から見て半径200 の範囲にいる鳥の重心を求めて、そちらの方向に力が発生するようにしました。鳥の速度は加速度によって方向が変わり、鳥が集合していきます。

集合ルールのみだと、鳥の群れは小さくなりすぎて密集してしまいました。次は「分離ルール」を追加しましょう。

分離ルールを追加する

# 初期値を追記
POWER_OF_SEPARATE = 0.1
RADIUS_OF_SEPARATE = 25

def draw():
    background(0)
    distances_list = distances_of_vectors([bird.position for bird in bird_list])
    # 鳥の群れを表示
    for bird in bird_list:
        bird.cohere(distances_list)  # 結合
        bird.separate(distances_list)  # 分離(追記)
        bird.update()
        bird.draw()


class Bird:
    # メソッドを追記
    def separate(self, distances_l):
        near_bird_ids = [d[1] for d in distances_l[self.bird_id] if 0 < d[0] < RADIUS_OF_SEPARATE]
        if len(near_bird_ids) > 0:
            near_bird = [bird_list[bird_id] for bird_id in near_bird_ids]
            center_of_near = center_of_vectors([bird.position for bird in near_bird])
            self.acceleration_to_separate = scale_vector(
                -POWER_OF_SEPARATE,
                unit_vector(subtract_vectors(center_of_near, self.position))
            )
        else:
            self.acceleration_to_separate = [0, 0]
cohesion + separation

集合ルールに加えて、「分離ルール」を追加しました。分離ルールは自分から見て半径25の中にいる鳥の重心を求めて、その方向と反対方向に力が発生するようにしました。近づきすぎると離れる方向に速度が変化するため、密集することを防止できました。

かなり自然な動きに近づいてきました。3つ目のルール「整列ルール」を追加します。

整列ルールを追加する

# 初期値を追記
POWER_OF_ALIGN = 0.1
RADIUS_OF_ALIGN = 100

def draw():
    background(0)
    distances_list = distances_of_vectors([bird.position for bird in bird_list])
    # 鳥の群れを表示
    for bird in bird_list:
        bird.cohere(distances_list)  # 結合
        bird.separate(distances_list)  # 分離
        bird.align(distances_list)  # 整列(追記)
        bird.update()
        bird.draw()


class Bird:
    # メソッドを追記
    def align(self, distances_l):
        near_bird_ids = [d[1] for d in distances_l[self.bird_id] if 0 < d[0] < RADIUS_OF_ALIGN]
        if len(near_bird_ids) > 0:
            near_bird = [bird_list[bird_id] for bird_id in near_bird_ids]
            self.acceleration_to_align = scale_vector(
                POWER_OF_ALIGN,
                unit_vector(add_vectors(*[bird.velocity for bird in near_bird]))
            )
        else:
            self.acceleration_to_align = [0, 0]
cohesion  + separaion + alignment

整列ルールを加えると、動きがさらに自然になります。整列ルールは、自分から見て半径100 にいる鳥たちが動いている方向を計算して、その平均の方向に力が発生するようにしました。

集合ルールだけだと、ほぼ一直線に群れに向かって飛んでいくのですが、整列ルールがあると周りの鳥と整列する力が加わるため、ゆっくりと集合していくことが確認できました。集合してからは誰がリーダーと決めてはいませんが、自然に同じ方向を向いて群れとして進んでいきます。

これでボイドモデルは完成です。

パラメーターを変更(分離ルールのみ)

RANGE_OF_DIRECTIONS = [0, 360]
POWER_OF_COHERE = 0
RADIUS_OF_COHERE = 200
POWER_OF_SEPARATE = 0.5
RADIUS_OF_SEPARATE = 80
POWER_OF_ALIGN = 0
RADIUS_OF_ALIGN = 100
separation only

本プログラムのパラメーターは7つあります。このパラメーターを変化させることで、ボイドモデルで鳥の動きを実験することができます。集合モデルのみのシミュレーションはやりましたので、分離モデルのみのシミュレーションをやってみましょう。

集合する力、整列する力を 0 にしたところ、鳥は等間隔を取って、ほぼ定位置にとどまることがわかりました。皆が皆を排斥した孤独の世界といったところでしょうか。

パラメータを変更(整列ルールのみ)

RANGE_OF_DIRECTIONS = [0, 360]
POWER_OF_COHERE = 0
RADIUS_OF_COHERE = 200
POWER_OF_SEPARATE = 0
RADIUS_OF_SEPARATE = 25
POWER_OF_ALIGN = 0.5
RADIUS_OF_ALIGN = 100
alignment only

整列する力のみ加えたところ、あらゆる方向に動いていた鳥たちが、すぐに群れとして同じ方向に進むようになってしまいました。これは同調圧力が強すぎる集団を表しています。鳥たちが互いに近づくことはなく、ただ平行に移動している世界は恐ろしいものを感じました。

まとめ

非常に単純化していますが、ボイドモデルを使って、集団にある力が働いた時に個々がどのように動くかをシミュレートできました。私は専門外ですが、行動学や社会学的にボイドモデルを考察しても面白いと思いました。

パラメータは7つあるので、まだまだ実験できるのですが、文字数が10000文字を超えたので、ここで終わることにします。読者の皆様は、ぜひコードをコピペしてボイドモデルで遊んでください。面白い動きをする条件を見つけた方はコメントで教えてくださいね。


前の記事
Processing でグラフを描く⑳ ライフゲーム
次の記事
Processing でグラフを描く㉒ 鳥と餌のシミュレーション

その他のタイトルはこちら


この記事が気に入ったらサポートをしてみませんか?