p5.jsのnoise関数で作る「エセ群行動」


2017年08月08日
With
p5.jsのnoise関数で作る「エセ群行動」 はコメントを受け付けていません

今回は、まず次のスケッチを動かしてみてください。

●[DEMO]noise関数による「エセ群行動」(クリックすると別ウインドウが開きます)

プログラム自体は、前回の「グローテスト」をベースにしており、起動すると画面の中心付近からブワッと90個の光点が出てきます。グローテストとの違いは、数秒もすると光点が赤、緑、青の3つの「群れ」に分かれて動いているように見えるところです。

AIの世界では「群行動」はメジャーなテーマのひとつで、鳥が群れて飛ぶ様子をシミュレーションした「Boids」という有名なアルゴリズムもあります。p5.jsの公式サイトにも「Flocking」という名前で、このアルゴリズムを実装したスケッチが公開されています。

Flocking – p5.js | examples

Flockingの場合、群れを構成する各インスタンスは「他のインスタンスと適当な距離を保つ」「群れとスピードを合わせて同じ方向に向かって動く」「群れから離れすぎないように群れの中心に向かって動く」というシンプルなアルゴリズムを持っており、それを同時かつ大量に動かすことで、画面上にとても自然な「鳥の群れ」を再現しています。

さて、先ほど見ていただいた「エセ群行動」。画面に描かれた90個のインスタンスは、実は他のインスタンスの状態をまったく感知せずに動いています。「エセ」たるゆえんです。

赤、緑、青の3種の光点は、それぞれ「FfObj」「FfObj2」「FfObj3」という3つのオブジェクトから作られており、「FfObj2」「FfObj3」は、どちらも「FfObj」の継承です。各オブジェクトの違いは「色」と「フレームごとの移動量を決定する際に使うnoise関数の初期座標値」のみです。

それぞれの初期座標値は、

  • 青(FfObj) : noiseX…998.5~1001.5、noiseY…-1001.5~-998.5
  • 赤(FfObj2): noiseX…4998.5~5001.5、noiseY…-5001.5~-4998.5
  • 緑(FfObj3): noiseX…9998.5~10001.5、noiseY…-10001.5~-9998.5

としており、これにインスタンス生成時に決定された0.015~0.017(全色共通)の数値がフレームごとに加えられます。実質、この違いだけで「3つの群れ」っぽい動きが生成されています。

以前のエントリ(p5.jsで「パーリンノイズ」のスゴさを思い知る)で「noise関数は、プログラムを一度起動すると、終了して再起動するまでシード値が固定され、同じ引数に対しては同じ数値を返す」と説明しました。これについて試しているときに気がついたのですが「座標値が非常に近い場合、変化量も近ければ、戻り値のパターンが似る」のです。

各光点の動きを観察していると分かりますが、同じ色の光点は、完全に同じパターンで動いているわけではないにも関わらず、何となく「だいたい似たようなところ」に集まる動きを見せます。ちなみに、初期値と変化量を同じにすると、動きは完全にシンクロします。このあたり、理屈は何となく分かる気がするのですが、やっぱり不思議ですね。スゴいぞ、パーリンノイズ。

各インスタンスが、他のインスタンスとの関係に関する情報や、それを処理するアルゴリズムを持っていないため、このスケッチは「群行動」アルゴリズムとしては完全に「エセ」ですが、それでもnoise関数を工夫して使うことで非常に面白い動きが作れることが分かっていただけたと思います。

ちなみに「Flocking」は主に「鳥」の群れを意味する単語だそうです。このスケッチでの光点の動きは、どちらかというと「虫」の群れに近いので「Fake Swarming」と名付けてみました。

var _imgSize = 400;
var _img, _img2, _img3;
var _ffCount = 90;
var _ff = [];

function setup() {
    createCanvas(600, 600);
    background(0);
    noStroke();
    imgSet();
    for (var i=0; i<_ffCount; i++) {
        if (i<_ffCount/3) {_ff[i] = new FfObj3();}
        else if ( i>=_ffCount/3 && i<_ffCount/3*2 ) { _ff[i] = new FfObj2();}
        else { _ff[i] = new FfObj(); }
    }
}

function draw() {
    background(0);
    for (var i=0; i<_ffCount; i++) {
        _ff[i].drawImg();
    }
    for (var i=0; i<_ffCount; i++) {
        _ff[i].drawMe();
        _ff[i].updateMe();
    }
}

function imgSet() {
    _img = createImage(_imgSize, _imgSize);
    _img.loadPixels();
    for (var i = 0; i < _img.width; i++) {
        for (var j = 0; j < _img.height; j++) {
            var pixAlpha = 255/(dist(_img.width/2, _img.height/2, i, j)-1)*1.47;
            if (pixAlpha < 1.89) { pixAlpha=0; }
            _img.set(i, j, color(210, 210, 255, pixAlpha));
        }
    }
    _img.updatePixels();

    _img2 = createImage(_imgSize, _imgSize);
    _img2.loadPixels();
    for (var i = 0; i < _img2.width; i++) {
        for (var j = 0; j < _img2.height; j++) {
            var pixAlpha = 255/(dist(_img2.width/2, _img2.height/2, i, j)-1)*1.47;
            if (pixAlpha < 1.89) { pixAlpha=0; }
            _img2.set(i, j, color(255, 210, 210, pixAlpha));
        }
    }
    _img2.updatePixels();

    _img3 = createImage(_imgSize, _imgSize);
    _img3.loadPixels();
    for (var i = 0; i < _img3.width; i++) {
        for (var j = 0; j < _img3.height; j++) {
            var pixAlpha = 255/(dist(_img3.width/2, _img3.height/2, i, j)-1)*1.47;
            if (pixAlpha < 1.89) { pixAlpha=0; }
            _img3.set(i, j, color(210, 255, 210, pixAlpha));
        }
    }
    _img3.updatePixels();
}

function FfObj() {
    this.pX = width/2 + random(-10, 10);
    this.pY = height/2 + random(-10, 10);
    this.noiseX = 1000+random(-1.5, 1.5);
    this.noiseY = -1000+random(-1.5, 1.5);
    this.noiseScale = random(0.015, 0.017);
}
FfObj.prototype.updateMe = function() {
    this.pX += noise(this.noiseX)*6-2.8;
    this.pY += noise(this.noiseY)*6-2.8;
    if (this.pX < 0) { this.pX = 0;}
    if (this.pX > width) { this.pX = width;}
    if (this.pY < 0) { this.pY = 0;}
    if (this.pY > height) { this.pY = height;}
    this.noiseX += this.noiseScale;
    this.noiseY += this.noiseScale;
}
FfObj.prototype.drawImg = function() {
    image(_img, this.pX -_img.width/2 , this.pY -_img.height/2);
}
FfObj.prototype.drawMe = function() {
    fill(220, 220, 255, 255);
    ellipse(this.pX, this.pY, 6);   
}

function FfObj2() {
    this.uber = FfObj.prototype;
    FfObj.call(this);
    this.noiseX = 5000+random(-1.5, 1.5);
    this.noiseY = -5000+random(-1.5, 1.5);
}
FfObj2.prototype = Object.create(FfObj.prototype);
FfObj2.prototype.constructor = FfObj2;
FfObj2.prototype.drawImg = function() {
    image(_img2, this.pX -_img2.width/2 , this.pY -_img2.height/2);
}
FfObj2.prototype.drawMe = function() {
    fill(255, 220, 220, 255);
    ellipse(this.pX, this.pY, 6);   
}

function FfObj3() {
    this.uber = FfObj.prototype;
    FfObj.call(this);
    this.noiseX = -10000+random(-1.5, 1.5);
    this.noiseY = 10000+random(-1.5, 1.5);
}
FfObj3.prototype = Object.create(FfObj.prototype);
FfObj3.prototype.constructor = FfObj3;
FfObj3.prototype.drawImg = function() {
    image(_img3, this.pX -_img3.width/2 , this.pY -_img3.height/2);
}
FfObj3.prototype.drawMe = function() {
    fill(220, 255, 220, 255);
    ellipse(this.pX, this.pY, 6);   
}