p5.jsで「パーリンノイズ」のスゴさを思い知る


2017年07月31日
With
p5.jsで「パーリンノイズ」のスゴさを思い知る はコメントを受け付けていません

知ってよかったp5.jsでのお絵描きテクニック」で、3つめに挙げたのは「noise関数」でした。


■ [普及版]ジェネラティブ・アート―Processingによる実践ガイド

こちらの本では、実践編に入ってすぐ、第2部「ランダム性とノイズ」のしょっぱなで「noise関数」が出てきます。70年代にBASICをかじって、その後ほったらかしだった僕にとって、乱数を使うための命令(関数にあらず)は「random()」しか記憶になかったのですが、現代、特にジェネラティブアートの世界では、用途に応じて普通に「noise()」が使われているようです。

「noise()」は「パーリンノイズ(Perlin noise)」と呼ばれる技法を実装した関数とのこと。ググってみたら、Wikipediaに項目がありました。

パーリンノイズ – Wikipedia

「パーリン」というのは、この技法を開発した「Ken Perlin」の名前からとられたのですね。もともとは、CGのテクスチャを数学的に生成するための技法として作られたのだとか。

noise関数の機能をざっくり言えば「ランダム性のある数値を生成する。ただし、完全なランダムではなく、“自然な感じで変化する”一連の数値を得たいときに有効」みたいな感じです。

ノイズテスト1「マウスで動くオーロラ」

次のデモは、p5.jsのリファレンスページでnoise関数のサンプルとして掲載されているものをちょっとだけ改変したものです。

●[DEMO]ノイズテスト1「マウスで動くオーロラ」(クリックすると別ウインドウが開きます)

ブラウザをアクティブにし、カンバス上でマウスポインタを動かすと、ポインタの動きに合わせて、描かれている模様が連続的に変化します。

ソースコードは10行ちょい。

var noiseScale = 0.02;

function setup() {
    createCanvas(400, 300);
}

function draw() {
    background(0);

    for (var x=0; x < width; x++) {
    var noiseVal = noise((mouseX+x)*noiseScale, mouseY*noiseScale);
    strokeWeight(1);
    stroke(noiseVal*255);
    line(x, 0, x, mouseY+noiseVal*80);
  }
}

noise()を使うと、ほんのこれだけの記述で、面白く変化する絵が描けてしまうのですね。感動です。ちなみに、ここではオーロラ(実際には、画面上部から引かれた太さ1ドットの直線の集まり)の「波打ち具合」と、線の「濃淡」がnoise()で算出されています。

p5.jsのリファレンスを見てみると、noise()は1つ以上、3つまでの引数をとり、戻り値として0以上1未満の数値を得られるとあります。

・p5.js | reference – noise()

noise()の使い方を理解するために、さらに自分でいくつかスケッチを描いてみました。

ノイズテスト2「ドット山脈」

●[DEMO]ノイズテスト2「ドット山脈」(クリックすると別ウインドウが開きます)

動かすと、画面の左側から右側に向けてポツポツと白い点が打たれていきます。一番右まで来たら、また左に戻って延々と繰り返します。

スクリーンショットに説明を付けておきましたが、一番上が「random()」、下の4つが「noise()」を使って座標を出したものです。

p5.jsでnoise()を使う場合、何らかの形で最初の座標値(引数)を用意し、フレームごとにそれを少しずつ増やしたり減らしたりするというのがオーソドックスなやり方になります。上のデモで、下4つの点集合は、その「増やし方」を変えたものです。

動かしてしばらく放っておくと一目瞭然ですが、random()だと毎回完全にランダムな座標が選ばれるため散布が「帯」のようになっていきます。一方、noise()の変化量「0.02」だと、緩急のある稜線のように点が打たれていきます。変化量を増やしていくと徐々に連続性が目立たなくなり、変化量「0.2」ではかなりrandom()で生成したものに近くなります。

この「変化量」は、どんな結果がほしいのかによって最適なものが決まってきますが、ひとまず最初は「0.01」「0.02」あたりにしておいて、動きを見ながら微調整するというのが良さそうです。

var range = 90;
var noiseVar = 1;
var noiseVar2 = 1;
var noiseVar3 = 1;
var noiseVar4 = 1;
var noiseScale = 0.02;
var dotX = 0;
var dotY = 100;
var noiseX = 0;
var noiseY = 200;
var noiseY2 = 300;
var noiseY3 = 400;
var noiseY4 = 500;

function setup() {
    createCanvas(600, 600);
    background(0);
}

function draw() {
    stroke(255);

    // random()
    var randVal = random() * range - (range/2);
    point(dotX, dotY+randVal);
    dotX += 1;
    
    // noise()
    var noiseVal = noise(noiseVar) * range -(range/2);
    point(noiseX,noiseY+noiseVal);
    noiseVar += noiseScale;
 
    var noiseVal = noise(noiseVar2) * range -(range/2);
    point(noiseX,noiseY2+noiseVal);
    noiseVar2 += noiseScale*2;

    var noiseVal = noise(noiseVar3) * range -(range/2);
    point(noiseX,noiseY3+noiseVal);
    noiseVar3 += noiseScale*5;

    var noiseVal = noise(noiseVar4) * range -(range/2);
    point(noiseX,noiseY4+noiseVal);
    noiseVar4 += noiseScale*10;

    noiseX += 1;
    if (dotX > width){ dotX = 0; noiseX =0};
}

ノイズテスト3「挙動不審な円」

noise()は、「ランダムだけれどもスムーズに変化する動き」を作りたいときに便利です。これまでに何度か紹介した「Wave Clock」でも、各種の座標やスピードを変化させるためにnoise()が使われていました。

次のデモは「動き」の生成にrandom()とnoise()を使って違いを比較したものです。

●[DEMO]ノイズテスト3「挙動不審な2つの円」(クリックすると別ウインドウが開きます)

ウインドウを開くと、画面に赤と青の丸が出てきて動き始めます。赤丸が「random()」、青丸が「noise()」で動いています。赤丸がブルブルと震えて見えるのに対し、青丸はある程度スムーズに、慣性をつけながら移動しているように見えますね。

座標の更新方法ですが、赤丸はフレームごとにx座標、y座標のそれぞれに対して「random(-2, 2)」で-2以上2未満の乱数を発生させ、それを加えています(個人的には、p5.jsのrandom関数はそういう形で乱数の範囲を指定できるというのが新鮮でした)。

青丸では、最初に「noiseValX」「noiseValY」という2つの変数に対して、Math.random()*100で0~100未満の初期座標値(丸の座標ではなく、noise関数が使うノイズ面上の座標)を設定しています。それを引数として、xとyのそれぞれに対し、フレームごとに「-1.85~2.15」の移動量をnoise()で発生させて加え、その後、ノイズ座標値を「0.02」ずつ増加させて、次のフレームに移るという処理をしています。

ちなみに、バラバラに変化させたい要素に対して、それぞれ別の「初期値」を用意しておくというのは、noise()を使う際のポイントです。noise()では、通常、プログラムを一度起動すると、終了して起動し直すまでシード値が固定され、同じ引数(座標値)に対しては、同じ値を返すようになります。例えばこのデモの場合、xとyの移動量を出すnoise()に同じ引数を与えてしまうと、毎フレームごとのxとyの変化量は全く同じになります。つまり、直線上しか移動しなくなってしまうのです。よく考えると当たり前なのですが、慣れるまではうっかりしがちなのでご注意を。

var rndX = 300;
var rndY = 150;
var nisX = 300;
var nisY = 450;
var noiseValX = Math.random()*100;
var noiseValY = Math.random()*100;

function setup() {
    createCanvas(600, 600);
}

function draw() {
    background(255);
    noStroke();
    fill(255, 0, 0);
    ellipse(rndX, rndY, 100);
    fill(0, 0, 255);
    ellipse(nisX, nisY, 100);

    rndX += random(-2, 2);
    rndY += random(-2, 2);

    nisX += noise(noiseValX)*4-1.85;
    nisY += noise(noiseValY)*4-1.85;
    noiseValX += 0.02;
    noiseValY += 0.02;

    if (rndX > width) { rndX = width; }
    if (rndX < 0) { rndX = 0; }
    if (rndY > height) { rndY = height; }
    if (rndY < 0) { rndY = 0; }
    if (nisX > width) { nisX = width; }
    if (nisX < 0) { nisX = 0; }
    if (nisY > height) { nisY = height; }
    if (nisY < 0) { nisY = 0; }
}

ノイズテスト4「なんちゃってヒートマップ」

p5.jsのリファレンスによれば「noise()は1~3個の引数をとる」とあります。次はそれを利用したサンプルです。

●[DEMO]ノイズテスト4「なんちゃってヒートマップ」(クリックすると別ウインドウが開きます)

ウインドウを開くと、画面上で色つきの雲のようなものがうねうねと変化しはじめます。ソースコードは、約20行です。

var noiseVal = 50;
var cellSize = 5;
var zseed = 0;

function setup() {
    createCanvas(375, 375);
    colorMode(HSB);
    background(255);
    noStroke();
}

function draw() {
    for (var y=0; y*cellSize < height; y++) {
        for (var x=0; x*cellSize < width; x++) {
            var fillCol = noise(x/noiseVal, y/noiseVal, zseed)*480;
            fill(fillCol, 100, 100, 1);
            rect(x*cellSize, y*cellSize, cellSize, cellSize);
        }
    }
    zseed += 0.005;
}

ポイントですが、まず7行目の「colorMode」で、色の指定方法をデフォルトのRGBからHSBに変えています。赤(R)、緑(G)、青(B)の強さをそれぞれ指定して色を作るRGBに対し、HSBでは色相(H)、彩度(S)、明度(B)を指定します。色相は環状に配置された色のグラデーションで、0~360の範囲で指定できます。彩度や明度を変えずに、色だけを次々と変化させるようなものを作りたい場合には、変数1個で色相を指定できるHSBのほうが、RGBよりも使いやすいケースが多そうです。ちなみに、RGBとHSBでは、アルファ値(透明度)の指定の仕方が変わります。RGBでは「0~255」で指定するのに対し、HSBではデフォルトで「0~1」で指定します。これも、慣れるまでは間違いやすいので要注意です。

実際の描画ですが、カンバス全体を2行目で指定しているサイズ(cellSize=1辺の長さ)の正方形セルで埋め、その表示色をnoise()で変化させています。

セルを描く座標を出すために、draw()(p5.jsにおいて、実行時に毎フレーム自動で実行されるシステム関数)の中で「x」「y」の2変数を宣言しているのですが、このサンプルでは、この2つをそのまま、セルを描く「色」(変数名fillCol)を決めるnoise()の引数として与えています。具体的には15行目の

var fillCol = noise(x/noiseVal, y/noiseVal, zseed)*480;

です。xとyはfor文によって、隣り合ったセルでそれぞれ1ずつ変化するのですが、これだとnoiseの引数の変化量としては大きすぎるため、1行目であらかじめ設定した「noiseVal」(50)で割ったものを引数としています。これによって、第1、第2引数は隣り合ったセル同士で「0.02」ずつ変化することになります。ちなみに、この調整を行わないで「x」「y」をそのまま引数として与える(変化量=1)と、次の画像のように非常に粗い、文字どおり「ノイズ」のような色変化になります。

この2つの引数だけでも画像は生成できるのですが、一度生成された画像はカンバス上で変化しません。前出のとおり「基本的に一度起動したプログラムの中では、noise()に同じ引数を与えると同じ値を返す」ので、毎フレーム同じセルは同じ色になるのです。ちなみに、ブラウザをリロードするとシード値が再セットされるため、同じ引数から別のパターンの画像が生成されます。

今回は、フレームごとに「うねうね」と変化する感じにしたかったので、そのために第3引数を使いました。サンプル中で「zseed」と名付けた変数です。zseedは、draw()の最後で毎フレーム「0.005」ずつ増加させ、これによってセルの色がじんわりと変化するアニメーション効果を出しています。

noise()は、引数の数にかかわらず「0~1未満」の数値を返すので、その値に「480」を掛けて、色相環上の色(fillCol)を指定しています。HSBのHは通常「0~360」で指定しますが、今回の用途において、きっちりその範囲で指定してしまうと数値の両端にある「赤」が出る頻度が他の色と比べて少なくなるので、わざと大きめの数値を指定して赤が多めに出るようにしています。fillColが360以上の場合は、360相当の「赤」として指定されます。

このサンプルでは、xとyを2次元平面上での「座標軸」、zseedを「時間軸」の変化に対応する引数として使っています。「CGのテクスチャを作成するための手法」として作られたという開発経緯を考えると、こういう使い方が本来のパーリンノイズの用法に近いのでしょうね。

ノイズテスト5~6「気まぐれなベジェ曲線たち」

noise関数をテストするためのスケッチとして、最後にこんなものを作ってみました。

●[DEMO]ノイズテスト5「気まぐれなベジェ曲線」(クリックすると別ウインドウが開きます)

Illustratorなどのベクター系グラフィックツールを使ったことがある方にはおなじみの「ベジェ曲線」。始点、終点に加えて「コントロールポイント」を指定することで得られる曲線です。

p5.jsでは、「beginShape」「endShape」「vertex」「bezierVertex」という複数の関数を組み合わせることで、ベジェ曲線が描けます。

サンプルでは2つのコントロールポイントを持つ50本のベジェ曲線を最初に同じ場所へ描き、そこから全100個のコントロールポイントの座標をnoise()でフレームごとに変化させて各曲線の形状を徐々に変えています。なんかこう、触手っぽい感じになりますね。ソースコードでコメントアウトしている33行目の「drawpoint(h);」を復帰させると、各コントロールポイントの動きも画面上で赤い点として確認できます。

なお、このサンプルでは、それぞれの線がボンヤリと発光する「グロー」のような効果を出してみたくなり、線の描画時に、まず太めの線でごく薄くラインを書き、再び同じ座標に強い線を重ね描きするという処理を入れています。そのため、実際には_lineCountの倍の数(サンプルでは100)の線を毎フレーム描いており、結果的にかなり負荷の高いスケッチとなってしまいました。もし、重すぎてうまくアニメーションしないようでしたら、ソースコードで7行目の_lineCountの数値を10や20まで減らして試してみて下さい。逆に、ハイパワーなマシンなら線の数を数百まで増やして絵の変化を見てみるのも面白いと思います。

var _startX = 150, _startY = 150;
var _endX = 450, _endY = 450;
var _pointInit = [300, 300, 300, 300];
var _pointArr = [];
var _noiseVal = 0.005;
var _lineColr = 200, _lineColg = 200, _lineColb = 255;
var _lineCount = 50;

for (var g=0; g<_lineCount; g++) {
            _pointArr[g] = [];
}

function setup() {
    createCanvas(600, 600);
    noFill();
    setpoints();
    background(0, 0, 30);
}

function draw() {
    background(0, 0, 30);

for (var h=0; h<_lineCount; h++) {

    stroke(_lineColr, _lineColg, _lineColb, 40);
    strokeWeight(7);
    drawline(h);

    stroke(_lineColr, _lineColg, _lineColg, 255);
    strokeWeight(.7);
    drawline(h);

    //drawpoint(h);

    }

update();
}

function setpoints() {
    for (var h=0; h<_lineCount; h++) {
        for (var i=0; i<4 ; i++) {
            _pointArr[h][i]= _pointInit[i];
        }
        for (var i=4; i<8 ; i++) {
            _pointArr[h][i] = random(-500, 500);
        }
    }
}

function drawline(h) {
    noFill();
    beginShape();
    vertex(_startX , _startY);
    bezierVertex(_pointArr[h][0],_pointArr[h][1], _pointArr[h][2],_pointArr[h][3], _endX, _endY);
    endShape();
}

function drawpoint(h) {
    noStroke();
    fill(255, 0, 0);    
    ellipse(_pointArr[h][0],_pointArr[h][1], 5);
    ellipse(_pointArr[h][2],_pointArr[h][3], 5);
}

function update() {
    for (var h=0; h<_lineCount; h++) {
        for (var i=0; i<4 ; i++) {
            _pointArr[h][i] = _pointArr[h][i]+(noise(_pointArr[h][i+4])*4-1.89);
            if (_pointArr[h][i]<-150) {_pointArr[h][i]=-150; }
            if (_pointArr[h][i]>width+150) {_pointArr[h][i]=width+150; }
            _pointArr[h][i+4] += _noiseVal;
        }
    }
}

このスケッチでは、すべてのコントロールポイントをnoise()で動かしています。起動後、はじめのうちはそれなりに面白いパターンの動きが多く見られるのですが、しばらく放っておくと、徐々に拡散しきって全体の動きに面白みが感じられなくなってくる…ような気がしました。

で、そのあたりを何とかできないかなと、若干の変更を加えたのが次のスケッチです。

●[DEMO]ノイズテスト6「荒ぶるベジェ曲線」(クリックすると別ウインドウが開きます)

このスケッチで、noise()によってコントロールポイントの座標を変えているのは全80本の曲線のうち、最初の1本だけです。2本目以降は、配列上で自分の1つ前の曲線が前のフレームで持っていたコントロールポイント座標をコピーします。結果「80フレーム分軌跡を残しながら動く1本のベジェ曲線」になりました。

「荒ぶる」感じを出すのと、軌跡がそれなりの間隔を空けながら残りやすくなるように、座標変化の単位を大きくしたのですが、スピード感が出て、なかなか飽きのこないパターンを描いてくれるようになったのではないかと思います。「ランダム要素」と「規定要素」の適度な混ぜ具合が、面白いスケッチを作るためには大事なのだなぁと感じます。

var _startX = 150, _startY = 450;
var _endX = 450, _endY = 150;
var _pointInit = [300, 300, 300, 300];
var _pointArr = [];
var _noiseInit = [];
var _noiseVal = 0.025;
var _lineColr = 220, _lineColg = 220, _lineColb = 255;
var _lineCount = 80;

for (var g=0; g<_lineCount; g++) {
            _pointArr[g] = [];
    for (var f=0; f<8; f++) {
            _pointArr[g][f] = 0;
    }
}
for (var g=0; g<4; g++) {
    _noiseInit[g] = Math.random()*1000;
}

function setup() {
    createCanvas(600, 600);
    setpoints();
}

function draw() {
background(0, 0, 30);
noFill();
for (var h=0; h<_lineCount; h++) {
    stroke(_lineColr, _lineColg, _lineColb, 15);
    strokeWeight(7);
    drawline(h);

    stroke(_lineColr, _lineColg, _lineColg, 120);
    strokeWeight(.7);
    drawline(h);
    //drawpoint(h);
}
update();
}

function setpoints() {
    for (var h=0; h<_lineCount; h++) {
        _pointArr[h][0]= _pointInit[0];
        _pointArr[h][1]= _pointInit[1];
        _pointArr[h][2]= _pointInit[2];
        _pointArr[h][3]= _pointInit[3];
        _pointArr[h][4]= _noiseInit[0];
        _pointArr[h][5]= _noiseInit[1];
        _pointArr[h][6]= _noiseInit[2];
        _pointArr[h][7]= _noiseInit[3];
    }
    for (var i=4; i<8 ; i++) {
        _pointArr[0][i] = random(-500, 500);
    }
}


function drawline(h) {
    beginShape();
    vertex(_startX , _startY);
    bezierVertex(_pointArr[h][0],_pointArr[h][1], _pointArr[h][2],_pointArr[h][3], _endX, _endY);
    endShape();
}

function drawpoint(h) {
    noStroke();
    fill(255, 0, 0);    
    ellipse(_pointArr[h][0],_pointArr[h][1], 5);
    ellipse(_pointArr[h][2],_pointArr[h][3], 5);
    noFill();
}

function update() {
    for (var h=_lineCount-1; h>0; h--) {
        for (var i=0; i<4 ; i++) {
            _pointArr[h][i] = _pointArr[h-1][i];
            _pointArr[h][i+4] = _pointArr[h-1][i+4];
        }
    }
    for (var i=0; i<4 ; i++) {
         _pointArr[0][i] = _pointArr[0][i]+(noise(_pointArr[h][i+4])*150-73);
        if (_pointArr[0][i]<-400) {_pointArr[0][i]=-400; }
        if (_pointArr[0][i]>width+400) {_pointArr[0][i]=width+400; }
        _pointArr[0][i+4] += _noiseVal;
    }
}

※追記(2018/07/27)…noise関数を利用したスケッチはこちらのエントリでも紹介しています。よろしければ合わせてご覧下さい | p5.jsのnoise関数で作る「エセ群行動」

以上「知ってよかったp5.jsでのお絵描きテクニック」の3つめとなる「noise関数」について紹介してきました。Ken Perlinは、この手法を開発した功績が認められ、1997年に映画芸術科学アカデミーからアカデミー科学技術賞を授与されたのだとか(Wikipediaより)。たしかに、実装されたnoise関数を使っていろんなスケッチを作ってみると、その奥深さと便利さに驚かされます。ノイズを自在に扱えるようになれば、作れるスケッチの幅が一気に広がるはずです。このエントリで紹介したさまざまなサンプルから、その偉大さの一端を感じ取ってもらえたら嬉しいです。いやぁ、すごいぞパーリンノイズ。