#2 なめらかな多角形から始まるジェネラティブアート

シリーズ概要

下記のツイートのpolygon distortionを作るまでの工程をわかりやすく解説し、更により良いものにしていくまでを道のりをいくつかのnoteに分けて書いていこと思います。作成ツールはprocessingを用います。(processing初めての方はなめらかサンショウウオさんnoteやサイトにチュートリアルあるのでそちらも参考に。)

#2 distortion編

今回は上記のツイートの左上の画像にあるモノを作って行きます。
前回のコードをベースに作るので今回は前回より短くなると思います。さらっと読めるようにします。

論理的な解説

まず、前回作ったものがどんなものだったかを考えて見ましょう。

この三角形を例に考えます。
毎フレーム三角形の頂点が外側に広がることで三角形の積層させ作りました。3つの頂点を、頂点a,b,cとします。各フレームの頂点aは全て同じ直線上に並び他の頂点も同様にそれぞれ直線上に並んでいます。

これをもっと頂点が自由に動くように書き換えます。
x軸方向にもy軸方向にも自由に動くようにします。
この頂点の動きはみんな大好きパーリンノイズで実装します。毎フレーム頂点のx座標にnoiseの返り値を加え、y座標も同様に加えます。これで連続的に頂点が自由に動きそうですね。
こちらからは以上です。

実際のコード

float[] x;
float[] y;
int shapeRes = 100;
int centerX, centerY;
float startRadius = 200;
float[] XnoiseArg, YnoiseArg; //x座標用とy座標用
float Xincrease = 0.1;//x座標用の引数を更新する用
float Yincrease = 0.1;//y座標用の引数を更新する用

float deltaLength = 2;

void setup() {
  fullScreen();
  background(255);
  noFill();
  stroke(0, 5);

  centerX = width /2;
  centerY = height/2;

  float angle = TWO_PI/shapeRes;

  x = new float[shapeRes];
  y = new float[shapeRes];

  XnoiseArg = new float[shapeRes];
  YnoiseArg = new float[shapeRes];
  for (int i =0; i < shapeRes; i++) {
    XnoiseArg[i] = random(10000);
    YnoiseArg[i] = random(10000);
  }

  for (int i = 0; i < shapeRes; i++) {
    x[i] = cos(angle *i) * startRadius;
    y[i] = sin(angle *i) * startRadius;
  }
}

void draw() {
  translate(centerX, centerY);

  beginShape();

  curveVertex(x[shapeRes-1], y[shapeRes-1]);
  for (int i = 0; i < shapeRes; i++) {
    curveVertex(x[i], y[i]);
  }
  curveVertex(x[0], y[0]);
  curveVertex(x[1], y[1]);

  endShape();

  for (int i = 0; i < shapeRes; i++) {
    float adjust = deltaLength * 0.4625; //noiseの偏り修正用
    x[i] +=noise(XnoiseArg[i])*deltaLength - adjust; //修正
    y[i] +=noise(YnoiseArg[i])*deltaLength - adjust; //修正

    XnoiseArg[i] += Xincrease;
    YnoiseArg[i] += Yincrease;
  }
}

//sキーで画像を保存
void keyPressed(){
  if(key == 's'){
    saveFrame("2nd/"+frameCount + ".png");
  }
}

前回のコードとほとんど変わりませんので、変わった箇所だけ解説してます。上から解説していきます。

float[] XnoiseArg, YnoiseArg; //x座標用とy座標用
float Xincrease = 0.1;//x座標用の引数を更新する用
float Yincrease = 0.1;//y座標用の引数を更新する用

まずはnoise( )メソッドに渡す引数として使う配列が2つになりました。前回は頂点の中心からの距離をnoise( )で制御していたので配列は1つでしたが、今回はx座標とy座標をそれぞれnoise( )で制御するので2つになります。
それに合わせて配列の要素の値を毎フレーム更新する値も2つにしました。これは現状同じ値なので1つにまとめてもいいと思います。
次にsetup( )内を1箇所解説します。

float angle = TWO_PI/shapeRes;

前回は毎フレームdraw( )内で頂点の座標を算出するのにangle変数が必要だったのでグローバルな位置で定義していました。
今回は頂点の初期の位置を定める為に使い、draw( )では不必要になるのでローカルに定義してあります。
次にdraw( )内を1箇所解説します。

for (int i = 0; i < shapeRes; i++) {
    float adjust = deltaLength * 0.4625; //noiseの偏り修正用
    x[i] +=noise(XnoiseArg[i])*deltaLength - adjust; //修正
    y[i] +=noise(YnoiseArg[i])*deltaLength - adjust; //修正

    XnoiseArg[i] += Xincrease;
    YnoiseArg[i] += Yincrease;
  }

この記述は頂点の座標を次のフレームの為に更新する記述で。
前回、「noiseがそもそもそういうものなのか負の値に偏りがあるみたいで...」と書きましたが、前回はその偏りを利用して多角形の広がりを再現していました。今回はその偏りを出来るだけ無くさないと以下の画像のように全ての頂点が負の方向に動いてしまい形を成さないので、セルフで調整します。
(noiseの偏り問題についての検証はこちらを参考にして下さい。)

実行結果

shapeRes = 4
startRadius = 300
float deltaLength = 2

shapeRes = 10
startRadius = 300
deltaLength = 4

shapeRes = 1000
startRadius = 300
deltaLength = 10

shapeRes = 1000
startRadius = 250

deltaLength = 4

これは冒頭のツイートの画像に近いですね。良き良き。

納得できないので修正

ここまでで一応はpolygon distortionは出来上がりです。
個人的には先程作ったものはカオスになりがちだったり、気に入らない実行結果になるパラメータの組み合わせが多くて好きくないです。
パーリンノイズの偏りもマジックナンバーで補正しているので場合によっては偏りが出てしまったりコードの読み手に意図が伝わらないのでよろしくないですね。
なので書き換えます。
実行結果がカオスになりがちだったのは頂点の動きが自由過ぎた事が原因だと考えてみます。前回のアルゴリズムは頂点は直線上を移動するという自由度が比較的に低いものでしたが、先程作ったアルゴリズムは理論的には頂点が何処にでも行く可能性がある自由度が高いものでした。この中間を狙って見たいと思います。
(実行結果はおおよそ同じものになります。)

論理的な解説

頂点の移動範囲を制限します。ある範囲より外側に広がらないようにします。これまで頂点の座標に毎フレーム値を加算していましたが、初期の座標にnoise( )で得た値を加えた数値をそのフレームでの座標にしたいと思います。下記のコードのx, yが座標になるイメージです。

flaot initialX = 0,initial y = 0; //基準になる初期座標
float x, y; //実際の座標
float Xargument, Yargument;
float increase = 0.01;
float amplitude = 10; //amplitude:振幅

draw(){
    x = initialX + noise(Xargument)*amplitude - amplitude/2;
    y = initialY + noise(Yargument)*amplitude - amplitude/2;
    point(x, y);
    Xargument += increase;
    Yargument += increase;
}

initialXとinitialYを基準となる固定値として使い、noise( )を使ってinitialの座標の周り半径amplitudeピクセル内で動かします。
これを各頂点で行い、曲線で結びます。

実際のコード

float[] x;
float[] y;
int shapeRes = 200;
int centerX, centerY;
float startRadius = 300;
float[] Xargument, Yargument;
float Xincrease = 0.01;
float Yincrease = 0.01;

float amplitude = 150;

void setup() {
  fullScreen();
  background(255);
  //pixelDensity(displayDensity());
  noFill();
  stroke(0, 5);

  centerX = width /2;
  centerY = height/2;

  float angle = TWO_PI/shapeRes;

  x = new float[shapeRes];
  y = new float[shapeRes];

  Xargument = new float[shapeRes];
  Yargument = new float[shapeRes];
  for (int i =0; i < shapeRes; i++) {
    Xargument[i] = random(10000);
    Yargument[i] = random(10000);
  }

  for (int i = 0; i < shapeRes; i++) {
    x[i] = cos(angle *i) * startRadius;
    y[i] = sin(angle *i) * startRadius;
  }
}

void draw() {
  translate(centerX, centerY);

  beginShape();

  float xlength = noise(Xargument[shapeRes-1])*amplitude - amplitude/2;
  float ylength = noise(Yargument[shapeRes-1])*amplitude - amplitude/2;

  curveVertex(x[shapeRes-1]+xlength, y[shapeRes-1]+ylength);

  for (int i = 0; i < shapeRes; i++) {
    xlength = noise(Xargument[i])*amplitude - amplitude/2;
    ylength = noise(Yargument[i])*amplitude - amplitude/2;

    curveVertex(x[i]+xlength, y[i]+ylength);
  }

  xlength = noise(Xargument[0])*amplitude - amplitude/2;
  ylength = noise(Yargument[0])*amplitude - amplitude/2;
  curveVertex(x[0]+xlength, y[0]+ylength);

  xlength = noise(Xargument[1])*amplitude - amplitude/2;
  ylength = noise(Yargument[1])*amplitude - amplitude/2;
  curveVertex(x[1]+xlength, y[1]+ylength);

  endShape();

  for (int i = 0; i < shapeRes; i++) {
    Xargument[i] += Xincrease;
    Yargument[i] += Yincrease;
  }
}

//sキーで画像を保存
void keyPressed(){
  if(key == 's'){
    saveFrame("3rd/"+frameCount + ".png");
  }
}

少し長くなりましたが、変わったのはdeltaLength変数をamplitude変数に名前を変えたのと、draw( )内だけです。
draw( )内のbeginShape( )からendShape( )までの記述について解説して行きます。

  beginShape();

  float xlength = noise(Xargument[shapeRes-1])*amplitude - amplitude/2;
  float ylength = noise(Yargument[shapeRes-1])*amplitude - amplitude/2;

  curveVertex(x[shapeRes-1]+xlength, y[shapeRes-1]+ylength);

  for (int i = 0; i < shapeRes; i++) {
    xlength = noise(Xargument[i])*amplitude - amplitude/2;
    ylength = noise(Yargument[i])*amplitude - amplitude/2;

    curveVertex(x[i]+xlength, y[i]+ylength);
  }

  xlength = noise(Xargument[0])*amplitude - amplitude/2;
  ylength = noise(Yargument[0])*amplitude - amplitude/2;
  curveVertex(x[0]+xlength, y[0]+ylength);

  xlength = noise(Xargument[1])*amplitude - amplitude/2;
  ylength = noise(Yargument[1])*amplitude - amplitude/2;
  curveVertex(x[1]+xlength, y[1]+ylength);

  endShape();

基準点になるx[i]とy[i]の値は常に変わりません。
これまでは、

curveVertex(x[i], y[i]);

で済んでいた所が

xlength = noise(Xargument[i])*amplitude - amplitude/2;
ylength = noise(Yargument[i])*amplitude - amplitude/2;

curveVertex(x[i]+xlength, y[i]+ylength);

このようにXlengthとYlengthの計算の分だけ長くなってしまっていますが、やっている事は難しくありません。-amplitude ~ +amplitudeの範囲で返ってきたノイズの値を基準点に足し合わせて頂点の座標を指定しているだけです。なので頂点は基準点を中心とした半径amplitudeピクセル内を動くことになります。

実行結果

shapeRes = 200
startRadius = 300
amplitude = 150

shapeRes = 300
startRadius = 350
amplitude = 300

終わり

どうでしたでしょうか。
個人的には最初にやったやり方より、後にやったやり方の方がカオスになり過ぎなくて好きです。ただ後の方のやり方は制限をした分パラメータを変えても実行結果があまり変わらないのでそこは面白くないですね。
次回はここまでやった内容をベースに色や形などを変えてより良い実行結果を追求して行くつもりです。
良いなと実践してみてください。シェアやサポートもお待ちしてます~。

この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

note.user.nickname || note.user.urlname

頂いたお金は、クリエイティブコーディングの本を買ったり勉強するのに使います。

え、うそ、ありがとう.....愛していいですか?
9

梶田悠

ジェネラティブアートのレシピ帳

ジェネラティブアート系のソースコードを解説していくマガジンです。各投稿のサムネイルにあるものを作れるようになります。 何人かで回せたらいいなと思ってます、お声掛け下さい。
1つのマガジンに含まれています
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。