Chapter 44

ノイズ

miku
miku
2021.11.15に更新

一般的にノイズというと雑音の事を指すが、ここで扱うのは繋がった滑らかなランダムな値を作る関数のことだ。ノイズ関数自体の実装については後の章で扱うので、この章ではp5.jsに用意されているノイズ関数の扱い方について学ぶ。

noise()

ノイズ関数である noise()random() と同じようにランダムな値を返すのだが、繋がった値を返すという特徴がある。たとえば1~10までの範囲の整数しか返さないランダムを作成したとしよう。random() を利用して作成した場合は 8,1,2,5,9,9,4 ...とサイコロを振ったように値が返るだろうが、noise() を利用した場合は 5,5,6,7,7,8,7 ...のように繋がった形のランダムを返す。つまりランダムの次の値が前の値と近いものが返る。

noise()random() の違いはグラフにして比較するとわかりやすい。

noise()random() と同じように 0~1 の範囲で値を返す。それを 0~height まで拡げたものを y に、時間を x にしてグラフで表現したものが上記になる。
左が noise() を利用したもので、前の値と今の値が近いので山を描いたような形になる。右が random() を利用したもので、擬似乱数である以上、前の値とかけ離れた値が出るケースもあるので、グラフにするとギザギザな形になる。

このように noise() の内部では滑らかな山を生成するのだが、滑らかな山から滑らかな値が作られるとは限らない。たとえば実際に山を歩く人のy座標を記録しておいて上記のようにグラフにする。人間にとっては滑らかであっても、巨人が一歩で数百メートルも移動したら滑らかでなくなるということもありえるだろう。つまり滑らかな値が作れるかどうかは noise() の内部で生成される山だけではなく歩幅にも依存する。

左も右も noise() で生成された同じ形の山を歩いたy座標の記録だが歩幅が違う。右が左の1/5のペースで歩いた場合だ。

ノイズの利用方法としては、歩幅をこちら側で制御してノイズに渡し、返ってくる値を大きさや位置に適用できるように変換して、滑らかにそれらを変化させる、というケースが多い。

ノイズの利用例として noise() の返り値を円の大きさに変換したのが上記になる。参照している山は同じなので、歩幅によって円の大きさの変化が決まるのを確認してほしい。

これらの事象を踏まえて、実際に noise() の使い方を見てみよう。

noise(0); // 0.5819991305078442

noise(x) のように noise() の引数としてx座標を指定すると、そのx座標で山のどこに立っているかを 0~1 の範囲で返す。参照される山は同じなので noise(x) === noise(x) は必ず true になる。ただしプログラムを実行するたびに山の形が変わるので、noise(x) の値も毎回変わることに注意する。

あとは x の値を変化させて noise() から値を取得すればいい。

let x;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = 0;

  console.log(getNoise());
  console.log(getNoise());
}

function getNoise() {
  const ret = noise(x);
  x += 0.01; // 歩幅を足して移動する
  return ret;
}

noise() を参照するたびに、x の値を増やし、返ってくる値が変わるようにする。歩幅に依存して noise() の返り値によってできるグラフのなだらかさが決まる。

利用する側にとってどの歩幅が最適かはまちまちだが、p5.js側では 0.005〜0.03 の歩幅がほとんどの場合で利用ができると明記しているので参考にしてほしい。

let x;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = 0;
}

function draw() {
  clear();

  const y = noise(x);
  x += 0.005;

  const d = y * min(width, height) * 0.8;
  circle(width / 2, height / 2, d);
}

noise() の返り値は 0~1 の範囲なので、それを円の大きさに変換する場合は、円の大きさの最大値を掛ければいい。

シード

function setup() {
  createCanvas(windowWidth, windowHeight);

  noiseSeed(0);
  console.log(noise(100)); // 0.4480771946400637
  console.log(noise(200)); // 0.4813198729098076

  noiseSeed(0);
  console.log(noise(100)); // 0.4480771946400637
  console.log(noise(200)); // 0.4813198729098076
}

noiseSeed() にシード値を指定することで、シード値に依存にした山の形を選択することができる。つまり固定のシード値を指定すれば、毎回同じノイズが返る。

2次元/3次元のノイズ

noise(x, y);
noise(x, y, z);

noise() には x だけではなく yz を追加して2次元あるいは3次元のノイズを作ることができる。

利用例としては2つのノイズを利用したいので、軸を分けて利用するというのがまずある。

noise(x); // 1つ目のノイズ
noise(0, y); // 2つ目のノイズ

山を歩く方向が違うので全く違うノイズが生成されると考えるとわかりやすいだろう。

もうひとつの利用例としては、(x, y) を利用して生成したノイズを色に変換して雲のような画像を作ったり、ノイズを高低差だと考えて地形生成に利用したりできる。後者の場合、3次元の利用で最も有名なのがマインクラフトのマップである。

const size = 10;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();

  const xn = ceil(width / size);
  const yn = ceil(height / size);
  for (let y = 0; y < yn; y++) {
    for (let x = 0; x < xn; x++) {
      const v = noise(x / 100, y / 100);
      fill(v * 255);
      rect(x * size, y * size, size, size);
    }
  }
}

2次元のノイズを色に変換して描画する例。このような描画をハイトマップと呼ぶ。

ずらす

noise(x);
noise(100 + x);

同じ次元であっても適当に100ぐらいずらせばまったく違うノイズになる。上の x はいつか 100 の位置に辿り着くが、歩幅がたとえば 0.01 だと、100 の地点まで来るのに10000歩は移動しないといけないのでノイズが被っていること懸念する必要はない。

noise(x);
noise(0.1 + x);

逆にかなり近い下駄を履かせると、上のノイズは下のノイズを追いかける形、つまり遅れて再生しているような印象を与える動きになる。

let x;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();

  x = 0;
}

function draw() {
  const y1 = noise(x) * height;
  const y2 = noise(0.1 + x) * height;

  clear();
  circle(width / 4, y1, 30);
  circle((width / 4) * 3, y2, 30);

  x += 0.01;
}

初期位置の差が 0.1 で、歩幅が 0.01 なので、左の円は右の円の10フレーム遅れで同じ動きをする。

ノイズを利用した作例

const size = 350;
let layer;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();

  layer = createGraphics(size, size);
  layer.noStroke();

  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const v = noise(x / 100, y / 100);
      drawPixel(x, y, v);
    }
  }
}

function draw() {
  clear();
  image(layer, 0, 0);

  layer.copy(layer, 1, 0, width - 1, height, 0, 0, width - 1, height);

  for (let y = 0; y < height; y++) {
    const x = size - 1;
    const v = noise((x + frameCount) / 100, y / 100);
    drawPixel(x, y, v);
  }
}

function drawPixel(x, y, v) {
  if (v <= 0.325) {
    layer.fill("#5e539e");
  } else if (v <= 0.5) {
    layer.fill("#5584c5");
  } else if (v <= 0.53125) {
    layer.fill("#7ac1a5");
  } else if (v <= 0.5625) {
    layer.fill("#a2d7a4");
  } else if (v <= 0.6875) {
    layer.fill("#d3d599");
  } else if (v <= 0.775) {
    layer.fill("#808080");
  } else {
    layer.fill("#fff");
  }

  layer.rect(x, y, 1, 1);
}

ハイトマップに色を付けて、海や地形、山を表現した作例。

ノイズを利用した作例 その2

function setup() {
  createCanvas(windowWidth, windowHeight);

  noFill();
  stroke(255, 50);
  noiseSeed(random(100));
}

function draw() {
  const x1 = width * noise(frameCount / 100, 0);
  const x2 = width * noise(frameCount / 100, 10);
  const x3 = width * noise(frameCount / 100, 20);
  const x4 = width * noise(frameCount / 100, 30);
  const y1 = height * noise(frameCount / 100, 40);
  const y2 = height * noise(frameCount / 100, 50);
  const y3 = height * noise(frameCount / 100, 60);
  const y4 = height * noise(frameCount / 100, 70);

  bezier(x1, y1, x2, y2, x3, y3, x4, y4);
}

ベジェ曲線の各点の座標をノイズを利用して動かす作例。