top of page

Processing サトクリフ五角形

執筆者の写真: NUMNUM

こんにちは、NUMです。


今回はアドベントカレンダーに初挑戦してみようと思います!


Processing Advent Calendarとはクリスマスまでの日数をカウントダウンするアドベントカレンダーの習慣にもとづいて、毎年12月1日から25日までの期間限定でProcessingに関する記事や作品を一日づつ投稿するイベントです。


参加者はProcessing界隈なら誰でも知っている方々ばかりですね!僕は6日を担当します。


テーマはサトクリフ五角形です。

サトクリフ五角形は、ジェネラティブアートのバイブルでお馴染みの

「ジェネラティブ・アート Processingによる実践ガイド」の最終章に紹介されています。


買った当初はProcessingを始めて数ヶ月だったので、意味が分からず放置していました笑

今読み返してみるとすんなり入ってきたので記事にしてみようと思います。


この記事は理解に重きを置いているので、本書プログラムの基本構成は変えてませんが、冗長な記述をかなりリファクタリングしているのでご容赦ください。


サトクリフ五角形

サトクリフ五角形とは、五角形の各辺の中点から対角の外周頂点方向に垂線を引くと別の五角形ができます。そうすると内部に五角形が6つ出来ているので、再度同じことを繰り返します。



再帰の深さを増やすと鱗のような模様になります。



概要

プログラムの概要としては以下です。

  1. 一番外側の五角形を作成、頂点を配列に格納

  2. 各辺の中点を配列に格納

  3. 中点から対角の頂点方向に垂線(定数倍した)を引き、中心方向の端点を配列に格納

  4. 1,2,3の点を使用して五角形を作成する


要は五角形が出来るたびに、1,2,3の処理が行われるようにすればサトクリフ五角形ができるという仕組みです。


これを実現するに際に「再帰」を使います。


実装

プログラムを理解する上で重要な箇所を解説していきます。


Main:


float structFactor = 0.22; // 値が小さいほど、細かい構造となる
int maxlevels = 5; // 最大再帰の深さ
int arraySize = 5; // 多角形の頂点数

void setup() {
  background(240);
  size(800, 800);
  pixelDensity(displayDensity());
  
  stroke(0);
  strokeWeight(2);
  FractalRoot fr = new FractalRoot(width/2, height/2, 300);
}

Main関数では外側の五角形を作成するためにFractalRootクラスのインスタンスを生成します。引数は位置(x, y)と大きさを指定します。


FractalRoot fr = new FractalRoot(width/2, height/2, 300);


FractalRoot:


/**
 * フラクタルの形状を生成するルートクラス
 */
class FractalRoot {
  PVector[] points = new PVector[arraySize]; // 頂点の配列
  Branch rootBranch;

  /**
   * FractalRootのコンストラクタ
   * 
   * @param x 中心点のX座標
   * @param y 中心点のY座標
   * @param r 半径
   */
  FractalRoot(int x, int y, int r) {
    for (int ang = 10; ang < 370; ang+=72) {
      float cosx = x + r * cos(radians(ang));
      float siny = y + r * sin(radians(ang));
      points[int(ang/72)] = new PVector(cosx, siny);
    }
    rootBranch = new Branch(0, points);
  }
}

FractalRootクラスは外側の五角形の頂点、Branchの情報を保持します。

円周の各点を72°毎に取得することで五角形を生成します。

Branchはインスタンスが生成されると描画、再帰レベルに応じて内部に五角形を生成します。



Branch:


/**
 * フラクタルの枝(ブランチ)を表すクラス
 */
class Branch {
  int level; // 現在の再帰レベル
  PVector[] outerPoints = new PVector[arraySize]; // 外周の頂点配列
  PVector[] midPoints = new PVector[arraySize];   // 各辺の中点配列
  PVector[] projPoints = new PVector[arraySize];  // 中点から引いた垂線の頂点配列
  Branch[] myBranches = {}; // サブブランチの配列

  /**
   * ブランチのコンストラクタ
   *
   * @param lev 現在の再帰レベル
   * @param points 頂点の配列
   */
  Branch(int lev, PVector[] points) {
    level = lev;
    outerPoints = points; // 外周の頂点を取得
    calcMidPoints();      // 各辺の中点を取得
    calcStructPoints();   // 中点から引いた垂線の頂点を取得
    addChildBranch();
    drawMe();
  }

  /**
   * 現在のブランチとサブブランチを描画
   */
  void drawMe() {
    for (int i = 0; i < arraySize; i++) {
      PVector p1 = outerPoints[i];
      PVector p2 = outerPoints[(i + 1) % arraySize];
      line(p1.x, p1.y, p2.x, p2.y);
    }
    for (Branch br : myBranches) {
      br.drawMe();
    }
  }

  /**
   * 各辺の中点を計算
   */
  void calcMidPoints() {
    for (int i = 0; i < arraySize; i++) {
      PVector p1 = outerPoints[i];
      PVector p2 = outerPoints[(i + 1) % arraySize];
      midPoints[i] = calcMidPoint(p1.x, p1.y, p2.x, p2.y);
    }
  }

  /**
   * 中点から引いた垂線の頂点を計算
   */
  void calcStructPoints() {
    for (int i = 0; i < arraySize; i++) {
      PVector mp = midPoints[i];
      PVector op = outerPoints[(i + 3) % arraySize]; // 中点と対角の外周頂点を取得
      projPoints[i] = calcStructPoint(mp.x, mp.y, op.x, op.y);
    }
  }

  /**
   * 五角形内部の6つの五角形をmyBranchesに追加
   */
  void addChildBranch() {
    if (level + 1 < maxlevels) {
      // 中心の五角形を配列に追加
      Branch childBranch = new Branch(level + 1, projPoints);
      myBranches = (Branch[])append(myBranches, childBranch);

      // 外周の五角形を5つ配列に追加
      for (int i = 0; i < arraySize; i++) {
        
        // 取得したい点が別インデックスに存在するための調整
        int nexti = i - 1;
        if (nexti < 0) {
          nexti += arraySize;
        }
        PVector[] newPoints = { projPoints[i], midPoints[i], outerPoints[i], midPoints[nexti], projPoints[nexti] };
        childBranch = new Branch(level + 1, newPoints);
        myBranches = (Branch[])append(myBranches, childBranch);
      }
    }
  }
}

/**
 * 2点間の中点を計算
 *
 * @param x1 最初の点のX座標
 * @param y1 最初の点のY座標
 * @param x2 2番目の点のX座標
 * @param y2 2番目の点のY座標
 * @return 中点のPVector
 */
PVector calcMidPoint(float x1, float y1, float x2, float y2) {
  float calcX = calcMid(x1, x2);
  float calcY = calcMid(y1, y2);
  return new PVector(calcX, calcY);
}

/**
 * 2つの値の中間値を計算
 *
 * @param a 最初の値
 * @param b 2番目の値
 * @return 中間値
 */
float calcMid(float a, float b) {
  return a + ((b - a)/2);
}

/**
 * 構造点(投影点)を計算
 *
 * @param mpX 中点のX座標
 * @param mpY 中点のY座標
 * @param opX 対角点のX座標
 * @param opY 対角点のY座標
 * @return 構造点のPVector
 */
PVector calcStructPoint(float mpX, float mpY, float opX, float opY) {
  float calcX = mpX - ((mpX - opX) * structFactor);
  float calcY = mpY - ((mpY - opY) * structFactor);
  return new PVector(calcX, calcY);
}

Branchクラスはコンストラクタで以下情報を取得します。

  1. 外周の頂点配列(赤点)

  2. 各辺の中点配列(青点)

  3. 中点から引いた垂線の頂点配列(黄点)


  1. 五角形内部にできる6つの五角形(子Branch)


サトクリフ五角形において重要なのは中点、垂線の長さの計算です。


/**
 * 2つの値の中間値を計算
 *
 * @param a 最初の値
 * @param b 2番目の値
 * @return 中間値
 */
float calcMid(float a, float b) {
  return a + ((b - a)/2);
}

中点座標は2点のx、yに対して上記計算をします。

※引数a、bにはx1、x2とy1、y2をどの順番で入れても同じ結果になります。



/**
 * 中点から引いた垂線の頂点を計算
 */
void calcStructPoints() {
  for (int i = 0; i < arraySize; i++) {
    PVector mp = midPoints[i];
    PVector op = outerPoints[(i + 3) % arraySize]; // 中点と対角の外周頂点を取得
    projPoints[i] = calcStructPoint(mp.x, mp.y, op.x, op.y);
  }
}
/**
 * 構造点(投影点)を計算
 *
 * @param mpX 中点のX座標
 * @param mpY 中点のY座標
 * @param opX 対角点のX座標
 * @param opY 対角点のY座標
 * @return 構造点のPVector
 */

PVector calcStructPoint(float mpX, float mpY, float opX, float opY) {
  float calcX = mpX - ((mpX - opX) * structFactor);
  float calcY = mpY - ((mpY - opY) * structFactor);
  return new PVector(calcX, calcY);
}

垂線を計算するとは言っても、中点座標の対角にある点を取得しているだけなので、厳密には違います。 例えば、i = 1要素目の中点の場合、対角点は i + 3要素目を取得する事で実現できます。


calcStructPoint関数は2点間の長さにstructFactorを乗算することで、長さを調整しています。structFactorが1より小さい場合、垂線の長さが短くなります。

個人的には0.25が綺麗な形になる印象です。



/**
 * 五角形内部の6つの五角形をmyBranchesに追加
 */
void addChildBranch() {
  if (level + 1 < maxlevels) {
    // 中心の五角形を配列に追加
    Branch childBranch = new Branch(level + 1, projPoints);
    myBranches = (Branch[])append(myBranches, childBranch);

    // 外周の五角形を5つ配列に追加
    for (int i = 0; i < arraySize; i++) {
      // 取得したい点が別インデックスに存在するための調整
      int nexti = i - 1;
      if (nexti < 0) {
        nexti += arraySize;
      }
      PVector[] newPoints = { projPoints[i], midPoints[i], outerPoints[i], midPoints[nexti], projPoints[nexti] };
      childBranch = new Branch(level + 1, newPoints);
      myBranches = (Branch[])append(myBranches, childBranch);
    }
  }
}

五角形内部の6つの五角形を取得するaddChildBranchメソッドの説明をします。

まず6つの五角形はクラス変数myBranchesに追加していきます。

メソッドの処理は、中心、周囲5つの五角形の取得という2構成からなっています。



Branch childBranch = new Branch(level + 1, projPoints);
myBranches = (Branch[])append(myBranches, childBranch);

中心の五角形はprojPointsそのものなので、Branchのインスタンス化でそのまま渡してあげれば良いです。levelは何回再帰したかを表す値なので、無限ループを防ぐためにlevel + 1を追加しています。



// 外周の五角形を5つ配列に追加
for (int i = 0; i < arraySize; i++) {

  // 取得したい点が別インデックスに存在するための調整
  int nexti = i - 1;
  if (nexti < 0) {
    nexti += arraySize;
  }
  PVector[] newPoints = { projPoints[i], midPoints[i], outerPoints[i], midPoints[nexti], projPoints[nexti] };
  childBranch = new Branch(level + 1, newPoints);
  myBranches = (Branch[])append(myBranches, childBranch);
}

5つの五角形はouterPoints、projPoints、midPointsの点を使用して作成していきます。

点は全て同じインデックスとは限らないので、iとnextiを使用して取得します。



まとめ

今回はProcessing Advent Calendarに初参加してみて、Processingコミュニティに少し貢献できたような達成感を感じれました。


Processingは4年ほど続けているのでアドカレの存在は知っていましたが、自分が記事を書いても良いのかという不安があって参加していませんでした。


ただ、どんなに簡単でも何かを書くことに意味があると思うので気にせず参加してみてください!僕もまた来年に枠があれば参加しようと思います!

最後に毎度お馴染み作品紹介をして終わろうと思います。

この作品はサトクリフ五角形で「形の誕生」を表現してみました。

抽象的な形は物質が誕生したばかりようなイメージが湧いたのでこの作品を作ってみました。


最後まで読んでいただきありがとうございました!

閲覧数:60回0件のコメント

最新記事

すべて表示

Comentários


bottom of page