【Processing作例】公開作品のソースコードを改造して楽しむクリエイティブコーディング

2025年5月27日火曜日

Processing 作例

t f B! P L
タイトル画像

公開されている Processing 作品を改造して、パーティクルが自律的に色別の集団に分かれる(ように見える)作例を作りました。

そのプログラムのソースコードを、制作のポイントと合わせて紹介します。

 

English version available here.


改造元作品のProcessingコード

今回の作例は、ひさだん(@hisadan)さんが Twitter で公開されたコードを改造して制作したものです。

動きが自律的・有機的に見えて、とても魅力的です。私も真似してみたくなりました。

 

ソースコードを読むときのポイント

まずは、Twitter 投稿用に圧縮されているコードを整形してみます。



/**
 * Ref. https://x.com/hisadan/status/1913976946687422638
 */

int i, s, n = 999;
float a[] = new float[n * 2];
float x, y, d, r, e, f;

void setup() {
  size(800,800);
  colorMode(HSB);
  background(0);

  // 点をランダムに初期配置
  // a[i] が x 座標、 a[i + n] が y 座標
  for (i = 0; i < n; i++) {
    d = random(250);
    r = random(TAU);
    a[i] = d * sin(r);
    a[i + n] = d * cos(r);
  }
}

void draw() {
  // 残像のための半透明塗り
  fill(0,2);
  square(0, 0, 800);
  
  // 動きの計算
  for(i = 0; i < n; i++) {
    for(s = 0; s < n; s++) {
      x = 0;
      y = 0;
      d = dist(a[i], a[i + n], a[s], a[s + n]);
      e = (a[s] - a[i]) / d / d;
      f = (a[s + n] - a[i + n]) / d / d;

      if (i != s) {
        if (i % 2 == s % 2) {
          x += d > 100 ? -e : e;
          y += d > 100 ? -f : f;
        } else {
          x += d > 50 ? e : -e;
          y += d > 50 ? f : -f;
        }
      }

      a[i] += x;
      a[i + n] += y;

    }

    // スケッチ中心からの距離で色を決定
    stroke(mag(a[i], a[i + n]), 255, 255);
    point(a[i] + 400, a[i + n] + 400);
  }
}  

ざっくり塊ごとに何をしているのかコメントを入れてあります。

改造のためにコードを読むときはこの程度の理解で十分で、あとは手を動かしながら見ていきます。

 

クリエイティブコーディング的改造のポイント

コードを読んでばかりではつまらないので、クリエイティブコーディングの流儀に従い(?)、理解はそこそこに改造を始めてしまいます。

改造の際に、初期値を変えるなどの簡単な変更から始めていくと、プログラムの動作をスムーズに理解できます。動きを変えるなど難しい変更に進むころには、コードの肝はどこなのか、どこが魅力を生み出すポイントなのか見えてくるでしょう。

 

描画数を変える


int i, s, n = 999;

極端に 1万とか 10 とかに変更して結果を見ます。

 

描画のやり方を変える


  // 残像のための半透明塗り
  fill(0,2);
  square(0, 0, 800);

透明度を無しにしたり、思いっ切り透明にしてしまう。


    // スケッチ中心からの距離で色を決定
    stroke(mag(a[i], a[i + n]), 255, 255);
    point(a[i] + 400, a[i + n] + 400);

例えばこのように変えてみる。

 

動きを変える


      d = dist(a[i], a[i + n], a[s], a[s + n]);
      e = (a[s] - a[i]) / d / d;
      f = (a[s + n] - a[i + n]) / d / d;

位置の変化量を決める部分なので、ここは重要ポイントのひとつです。
改造のやり甲斐があり、かつ、難しいところでもあります。


        if (i % 2 == s % 2) {
          x += d > 100 ? -e : e;
          y += d > 100 ? -f : f;
        } else {
          x += d > 50 ? e : -e;
          y += d > 50 ? f : -f;
        }

ここは何してるのか解らない、いや何をしてるかは解るけど、何でこんなことしてるのか解らないところです。

と、いうことは、ここがこのコードの肝なんです、きっと。

コードの「肝」部分の変更例

 

改造後作品のProcessingコード

改造後のProcessing作品のコードがこちらです。


/**
 * パーティクルが自律的に色別の集団に分かれる(ように見える)Processing サンプルコード
 * Ref. https://x.com/hisadan/status/1913976946687422638
 *
 * @author @deconbatch
 * @version 0.1
 * @license CC0 1.0 https://creativecommons.org/publicdomain/zero/1.0/deed.ja
 * Processing 4.3.3
 * 2025.05.22
 */

void setup() {

  int   frmMax   = 24 * 10; // 24fps x 10sec
  int   pNumMax  = 1800;
  int   pClass   = 6;
  float pIniRad  = 80.0;
  float pDistMax = 220.0;
  float distMag  = 12.0;
  float baseHue  = random(120.0, 300.0);
  
  PVector curP[] = new PVector[pNumMax];
  PVector prvP[] = new PVector[pNumMax];

  size(720, 720);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);

  for(int pCnt = 0; pCnt < pNumMax; pCnt++){
    float r = random(pIniRad);
    float t = random(TAU);
    curP[pCnt] = new PVector(r * cos(t), r * sin(t));
    prvP[pCnt] = new PVector(curP[pCnt].x, curP[pCnt].y);
  }
  
  translate(width * 0.5, height * 0.5);
  rectMode(CENTER);
  blendMode(BLEND);
  background(baseHue, 40.0, 10.0, 100.0);

  // iterate for the number of animation frames
  for (int frm = 0; frm < frmMax; frm++) {

    // for afterimage
    blendMode(BLEND);
    fill(baseHue, 40.0, 10.0, 20.0);
    noStroke();
    rect(0.0, 0.0, width, height);

    // calc position of points
    for (int mCnt = 0; mCnt < pNumMax; mCnt++) {
      for (int oCnt = 0; oCnt < pNumMax; oCnt++) {
        if (mCnt != oCnt) {

          float pDist = 1.0 + PVector.dist(prvP[mCnt], prvP[oCnt]);
          PVector pDiv = PVector.div(PVector.mult(PVector.sub(prvP[mCnt], prvP[oCnt]), distMag), pow(pDist, 2));

          if (mCnt % pClass == oCnt % pClass) {
            curP[mCnt].add(pDiv.mult((pDistMax - pDist) / pDistMax));
          } else {
            if (pDist > pDistMax) {
              curP[mCnt].sub(pDiv);
            } else {
              curP[mCnt].add(pDiv);
            }
          }
          
        }
      }
    }

    // calc min/max distance
    float drMin = 99.0;
    float drMax = -drMin;
    for (int mCnt = 0; mCnt < pNumMax; mCnt++) {
      float dR = PVector.dist(curP[mCnt], prvP[mCnt]) / pDistMax;
      drMax = max(drMax, dR);
      drMin = min(drMin, dR);
    }

    // draw lines
    blendMode(SCREEN);
    noFill();
    for (int mCnt = 0; mCnt < pNumMax; mCnt++) {
      float distRatio = map(PVector.dist(curP[mCnt], prvP[mCnt]) / pDistMax, drMin, drMax, 0.0, 1.0);
      float hueRatio = (mCnt % pClass) * 1.0 / pClass;
      float satRatio = floor(0.5 + mCnt / (pNumMax * 0.3)) / 3.0;
      float briRatio = floor(0.5 + mCnt / (pNumMax * 0.2)) / 5.0;

      stroke((baseHue + 90.0 + hueRatio * 120.0 + distRatio * 90.0) % 360.0,
             10.0 + satRatio * (1.0 - distRatio) * 80.0,
             20.0 + briRatio * 30.0 + distRatio * 30.0,
             100.0
             );
      strokeWeight(constrain(6.0 * (1.0 - distRatio), 1.0, 6.0));
      line(prvP[mCnt].x, prvP[mCnt].y, curP[mCnt].x, curP[mCnt].y);
    }

    // carry over
    cur2prv(curP, prvP);

    // make image files for animation frames
    saveFrame("frames/" + String.format("%04d", frm) + ".png");
  }

  exit();
}

/**
 * deep copy from c to p
 */
void cur2prv(PVector[] c, PVector[] p){
  for(int pCnt = 0; pCnt < p.length; pCnt++){
    p[pCnt].x = c[pCnt].x;
    p[pCnt].y = c[pCnt].y;
  }
}

 

コードの主な変更点

点の座標を PVector で管理しています。


    curP[pCnt] = new PVector(r * cos(t), r * sin(t));

位置を計算する点と前回の点の配列を分けて、ある点の計算結果が他の点の計算に影響しないようにしました。


  PVector curP[] = new PVector[pNumMax]; // current
  PVector prvP[] = new PVector[pNumMax]; // previous

位置計算部分は、 '(mCnt % pClass == oCnt % pClass)' の条件によって増分値の計算式を変えてみました。


          float pDist = 1.0 + PVector.dist(prvP[mCnt], prvP[oCnt]);
          PVector pDiv = PVector.div(PVector.mult(PVector.sub(prvP[mCnt], prvP[oCnt]), distMag), pow(pDist, 2));

          if (mCnt % pClass == oCnt % pClass) {
            curP[mCnt].add(pDiv.mult((pDistMax - pDist) / pDistMax));
          } else {
            if (pDist > pDistMax) {
              curP[mCnt].sub(pDiv);
            } else {
              curP[mCnt].add(pDiv);
            }
          }

計算に時間がかかるので、 draw() を使ったリアルタイムのアニメーションはやめました。代わりに動画生成用のフレーム毎の静止画を書き出す方式にしました。

 

さらなる改造のアイデア

このままのコードでも、変数の初期値を変えることで様々な結果が得られるよう制作しました。


  int   pNumMax  = 1800;
  int   pClass   = 6;
  float pIniRad  = 80.0;
  float pDistMax = 220.0;
  float distMag  = 12.0;

これらを固定値ではなく、フレームの進行に沿って値を変えていくのも面白いと思います。

 

まとめ

改造の結果、アート表現というより何かのシミュレーションのような作例となりました。

色別の点が自律的に動いて集まっていくように見えますが、実際は集まる点の集団別に色を変えてると言った方がいいでしょう。


      float hueRatio = (mCnt % pClass) * 1.0 / pClass;

実のところ、元のコードも、改造後のコードも、なぜこんな動きになるのかさっぱり理解できていません。

プログラムのコードを技術的に理解できなくても、手を動かせば改造して作品が作れるということです。これぞクリエイティブコーディングの醍醐味ではないでしょうか。

とはいえ、基となるコードが無ければこの作品は生まれませんでした。
ひさだん(@hisadan)さん、面白い作例コードを公開してくれてありがとうございます。

 

QooQ