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

シリーズ概要

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

#1 僕編

シリーズ初回の今回はまずこのnoteのメインビジュアルにある曲線的な三角形を作ります。これは僕のSNSアイコンとして使っているので僕編です。
僕の作り方を解説していきます。みんなも僕を作ってね~

論理的な解説

便宜上今回作るモノを「僕」と名前をつけます。
「僕」の構造は単純で三角形が毎フレーム描画されそれが集まってできているだけです。前のフレームで描画した三角形より少し大きくした三角形を描くので徐々に大きくなります。画像のようなイメージです。

processingにはtriangle()というメソッドが存在します。このメソッドは三角形を描くメソッドですが、「僕」は曲線的な三角形なのでこのメソッドは使いません。曲線を描くためのcurveVertex()というメソッドを使って曲線的な三角形を描きます。
このメソッドは頂点を複数個指定する事でそれらを曲線で結ぶメソッドです。三角形の3頂点を指定してあげれば曲線的な線がかけるのでこれで作っていきます。(正確には制御点と言われる曲線を描く為に必要な頂点も指定するので指定する頂点の数は6つになります。)
また前のフレームから少し大きな三角形を描くと言いましたが、例えば、頂点aは前の三角形より3ピクセル、頂点bは5ピクセル、頂点cは7ピクセル大きくするなど偏りを持たせれば不規則性が生まれて「僕」に近付きます。この偏りをパーリンノイズを使って実現すると疎密のある三角形が作れます。「僕」の出来上がりです。
じゃ、作ってみましょうか。

実際のコードと解説

実際のコードがこちら。

float[] x;
float[] y;
int shapeRes = 3; //この数を増やせば多角形にできるよ
float startRadius = 200; //内側の多角形の大きさ
float[] nosieArgument; //パーリンノイズの引数用
float increase = 0.8; //noiseArgumentの更新用
float angle;

float deltaLength = 2.5;//頂点の最大移動量

void setup() {
  fullScreen();
  background(255);
  //pixelDensity(displayDensity()); //retinaディスプレイ用
  noFill();
  stroke(0, 20); //線の色

  angle = TWO_PI/shapeRes; //何度間隔で頂点を打つか (ラジアン)

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

  nosieArgument = new float[shapeRes];
  for (int i =0; i < shapeRes; i++) {
    nosieArgument[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(width /2, height/2);
  
  beginShape(); //curveVertex()を使う前にこれを書かく決まり

  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(); //curveVertex()を使った後にこれを書かく決まり

  //頂点の座標を更新
  for (int i = 0; i < shapeRes; i++) {
    float length = noise(nosieArgument[i])* deltaLength*2 -deltaLength;
    x[i] -= cos(angle*i) * length;
    y[i] -= sin(angle*i) * length;
    nosieArgument[i] += increase;
  }
}

void keyPressed(){
  if(key == 's'){
    saveFrame(frameCount + ".png");
  }
}

細かく説明していきます。まずは最初の変数定義の所から説明します。

float[] x;
float[] y;
int shapeRes = 3; //この数を増やせば多角形にできるよ
float startRadius = 200; //内側の多角形の大きさ
float[] nosieArgument; //パーリンノイズの引数用
float increase = 0.8; //noiseArgumentの更新用
float angle;
float deltaLength = 2.5;//頂点の最大移動量

x[ ]とy[ ]は頂点の座標を格納する配列です。
shapeResはshape resolutionの意で円を何分割するか、つまり何角形かという事です。ここを3にすると三角形がかけます。
startRadiusは最初に書かれる三角形の大きさを調整する変数です。
noiseArgument[ ]とincreaseはパーリンノイズ用の変数です。noiseArgumentが配列なのは頂点ごとに個々にパーリンノイズを効かせる為です。
毎フレームnoiseArgument[ ]を +increaseしてパーリンノイズを更新していきます。
angleは後で説明します。
deltaLengthは頂点が毎フレームどれくらい動くかをコントロールしています。2.5なので次のフレームの頂点の位置は現在のフレームの頂点の位置から-2.5 ~ +2.5ピクセルの範囲に収まります。
次はsetup( )の説明をします。

void setup() {
  fullScreen();
  background(255);
  //pixelDensity(displayDensity()); //retinaディスプレイ用
  noFill();
  stroke(0, 20); //線の色

  angle = TWO_PI/shapeRes; //何度間隔で頂点を打つか (ラジアン)

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

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

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

fullScreen( )で全画面のウィンドウで実行させます。
background( )で背景色の指定をしてます。今回は白ですね。
pixelDensity(displayDensity( ))はRetinaディスプレイのような実際の物理解像度が多いディスプレイに対応させ高解像度に描画するメソッドです。Retinaの人はコメントアウトしてもいいと思いますが、負荷が高くなりますし正直今回はあんまり変わらない気がします。
noFill( )を実行しないと三角形の内側が白で塗りつぶされて三角形が重ならなくなっちゃうので書いてあります。
stroke( )で三角形の線の色を指定しています。背景色とコントラスト高い色を選ぶといいと思います。
 angle = TWO_PI/shapeRes は、360度を頂点の数で割った角度値が入ります。(正確には度ではなくラジアンなので2πを割った値が入ります。)
イメージこんな感じ。

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

この2行は配列の初期化ですね。

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

パーリンノイズの引数に使う配列の初期化と適当な値を代入しておきます。

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

ここで最初に描く三角形の頂点の位置をx[ ], y[ ]配列に代入しています。
頂点1の座標が(x[1], y[1])で、頂点2の座標が(x[2], y[2])に、頂点iの座標が(x[i], y[i])になります。イメージこんな感じ。

次にdraw( )の説明をします。

void draw() {
  translate(width /2, height/2);
  
  beginShape(); //curveVertex()を使う前にこれを書かく決まり

  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(); //curveVertex()を使った後にこれを書かく決まり

  //頂点の座標を更新
  for (int i = 0; i < shapeRes; i++) {
    float length = noise(nosieArgument[i])* deltaLength*2 -deltaLength;
    x[i] -= cos(angle*i) * length;
    y[i] -= sin(angle*i) * length;
    nosieArgument[i] += increase;
  }
}

translate(width /2, height/2)は座標系を移動させています。通常画面左上が(0,0)であるのを、画面中央に原点をずらしています。原点の位置を画面の幅の半分右へ、画面の高さの半分下へ移動させています。
beginShape( )とendShape( ) の間にcurveVertexをかく決まりなので書いてあります。beginShape( )からendShape( )までの記述を詳しく見ていきましょう。

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]);

この部分が曲線を描く為の頂点を指定している箇所になります。
最初と最後のcurveVertexは制御点として使われ実際に線が引かれるのはその間の頂点だけです。なので4回curveVertexを実行すると、2個目の頂点から3個目の頂点の間に曲線が描画され、1個目と4個目の頂点に線が届きません。
4個の頂点で線が1本、5個の頂点で線が2本.....今回は三角形なので辺が3つ→線を3本描画したい→指定する頂点は6本という事になりますね。
for文の前に1個、for文で3つ個、for文の後に2個で合計6個指定していますね。これで三角形がかけます。
ここで最初と最後の制御点は何をしているのか気になる方がいると思います。それについてはちょっと長くなりそうなのでおまけに書きます。
draw( )内の残りの処理を解説します。

//頂点の座標を更新
for (int i = 0; i < shapeRes; i++) {
  float length = noise(nosieArgument[i])* deltaLength*2 -deltaLength;
  x[i] -= cos(angle*i) * length;
  y[i] -= sin(angle*i) * length;
  nosieArgument[i] += increase;
}

前述したように頂点の位置を少しずらします。
noise( )は0~1.0の小数を返します。
deltaLengthは2.5なのでlength変数には-2.5 ~ +2.5の値が入ることになりますね。この値を現在の座標から引いています。このコードだと外側に広がらず、大体同じ範囲を頂点が行き来するように思えますよね。noiseがそもそもそういうものなのか負の値に偏りがあるみたいで、現在の座標から引いておけば外に広がってくれます。逆に言えば内側に縮めたいなら「-」の部分を「+」にすれば出来ます。(noiseに偏りがある問題についてのについてはこちら)
最後に引数の配列の値も少しずらして次のフレームを迎えます。

おまけ

「僕」は作れましたが、まだまだ手を加えられる余地は沢山あります。
この作品の変えられそうな要素は
 ・ 頂点数
 ・ 最初の三角形の大きさ
・ 線の色
 ・ 線の太さ
 ・ 線の疎密の作り方
考えればもっと沢山ありそうですが、簡単な要素に手をつけてみましょう

頂点数

頂点数を変えてみましょう。
頂点数を決める変数はshapeResでしたね。
頂点数が3で三角形が描けるので、4にすれば四角形、5にすれば五角形といった感じです。2にすると線が書けますがあんまり面白くないです。1にすると何も書けない上にそもそもエラーが出るので動きません。。。
なので3以上の数を入れてみましょう。
こういった場合、極端な数や特殊な数を入れるとに面白い結果を得られることが多いです。0や1、-1や0.0001のような小数は特殊で極端ですが前述した理由から今回は無理そうです。なので極端に大きな値を入れてみましょう。
こんな感じになると思います。
頂点数10

頂点数100

頂点数10000

多角形といってきましたが、頂点数が増えれば円に近付くのでまた違った雰囲気になりますね。

最初の多角形の大きさ

最初の多角形、つまり一番内側の多角形の大きさも変えてみましょう。

最初の多角形の大きさと頂点数を変えただけでこれだけ変わるのは面白いですね。こうまでなってしまうと流石に書く前に結果を予想するのは難しいですね。この偶然性とおおよそ同じコードで沢山のバリエーションを作れる楽しさはジェネラティブアート特有ですね。あー良い。

curveVertexの制御点

最後にcurveVertexの制御点について説明します。
言葉で説明するのは難しいので具体例を挙げて説明していきます。
まず単純に以下のコードで曲線を書いてみます。

background(255); 
size(1400,700); 
noFill();
beginShape();
curveVertex(420,80);  ellipse(420, 80,10,10);
curveVertex(460,350);  ellipse(460, 350,10,10);
curveVertex(890,350);  ellipse(890, 350,10,10);
curveVertex(1110,350); ellipse(1110,350,10,10);
endShape();

このような結果になりました。

では5行目の頂点を変えて実行してみましょう。
コードはこちら

background(255); 
size(1400,700); 
noFill();
beginShape();
curveVertex(230,600);  ellipse(230, 600,10,10);
curveVertex(460,350);  ellipse(460, 350,10,10);
curveVertex(890,350);  ellipse(890, 350,10,10);
curveVertex(1110,350); ellipse(1110,350,10,10);
endShape();

この結果がこちら

曲線の端が前後の制御点に向かうように線が曲がっているのがわかりますね。
この結果から制御点は線の始点と終点の向きを調整する為にあることがわかります。
例えばもしこれがなかった場合、どうなるでしょうか?
以下のコードで再現してみました。

background(255); 
size(1400,700); 
noFill();
beginShape();
curveVertex(460,350);  ellipse(460, 350,10,10);
curveVertex(460,350);  ellipse(460, 350,10,10);
curveVertex(890,350);  ellipse(890, 350,10,10);
curveVertex(1110,350); ellipse(1110,350,10,10);
endShape();

最初の2点を同じ座標にする事で始点の向きを定めず、制御点が無い状態を再現しました。
結果はこんな感じです。

線の始まりと終わりが直線的になってしまっていますね。
この制御点の無い状態で今回作った物を作ろうとするとどうなるでしょうか?やってみましょう。
コードはこちらです。

float[] x;
float[] y;
int shapeRes = 3;
float startRadius = 200;
float[] nosieArgument;
float increase = 0.8;
float angle;

float deltaLength = 2.5;

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

  angle = TWO_PI/shapeRes;

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

  nosieArgument = new float[shapeRes];
  for (int i =0; i < shapeRes; i++) {
    nosieArgument[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(width /2, height/2);
  
  beginShape();

  curveVertex(x[0], y[0]); //ここ変えた!
  for (int i = 0; i < shapeRes; i++) {
    curveVertex(x[i], y[i]);
  }
  curveVertex(x[0], y[0]); 
  curveVertex(x[0], y[0]); //ここも変えた!

  endShape();

  for (int i = 0; i < shapeRes; i++) {
    float length = noise(nosieArgument[i])* deltaLength*2 -deltaLength;
    x[i] -= cos(angle*i) * length;
    y[i] -= sin(angle*i) * length;
    nosieArgument[i] += increase;
  }
}

void keyPressed(){
  if(key == 's'){
    saveFrame(frameCount + ".png");
  }
}

そして結果がこちら

三角形の頂点が一箇所なめらかに繋がって無いですね。
これはこれで悪くないじゃん。。。。笑
でも綺麗に繋げてなめらかな多角形を書きたいですね。
cueveCertexについての解説はこれでこれで終わりです。

終わり

プログラミングを教える仕事をしている事もあってなるべく細かくわかりやすく書いたつもりです。(面倒で省いた感もありますが)
解説が冗長だったり、足りてなかったりしたらコメントからでもtwitterからでも教えて下さい。何か質問とかがあればそれもなるべくお答えするつもりです。
それにしても解説まで書くのは大変なので有料noteも検討したい.....
でもなるべくopenにしたいし、支払い手段が無い学生が読めなくなっちゃうのも嫌だからどうしよっか...
そもそも有料化するほど価値あるのか🤔🤔🤔
次回も良かったら読んで下さい!



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

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

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

「スキ」ありがとうございます!拡散してもらえると、とっても嬉しいです!
49

梶田悠

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

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

コメント1件

これ面白いですね! 応用していろいろ楽しめそうです。
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。