四角形によるジェネレイティブ・デザインの作成過程

2021年6月15日火曜日

Processing アニメーション 作例 中級者向け

t f B! P L

👉 Read this article in English.


モノクロのシンプルな形状が次々と変化していくアニメーション作品です。

例によって最初からこういうものを作ろうと意図していたわけではありません。 コードを書きながらいろいろと遊んでいる間に思いついたアイディアを盛り込んでいって、最終的にこのような形になりました。
その制作の過程を振り返ってみたいと思います。


オリジナルのアイディア

最初のきっかけは ntutae さんの #つぶやきProcessing 作品でした。 とても不思議な動きをするアニメーション作品です。


このコードを弄って遊んでるうちに、 'rect()' の幅を大きくすると元ネタとはまたちょっと違う面白さが現れました。


”幅を調整するといろいろ面白いことになりそうだ”、これがとっかかりのアイディアです。


静止画で面白さ探求

動きより形の面白さの方に興味が湧いたので、静止画の制作を進めていきました。

幅の調整だけではなく、描画する線の太さや 'rect()' 同士の距離を変えたり、'rect()' じゃなくて 'ellipse()' にしたり、'blendMode()' を変えたりといろいろ試しながら自由に遊びます。

その中で、太い線を使って 'blendMode(DIFFERENCE)' で描画するとこういうものが作れました。


これはいい! この方向で制作を進め、四角形の位置、サイズ、線の幅の関係を試行錯誤しながら調整していき、ランダムに面白い図形が描けるような値を探しました。


コードでいうとこの部分になります。


shapes.add(getShape(rSize * 2.0));
weights[i] = rSize * floor(random(6));


複数の面白い静止画をモーフィングさせたらもっと面白そう

面白い図形が描けるようになりましたが、見た目あまりにもシンプル過ぎるし、図形そのものに意味づけも無いので、静止画としてはちょっと物足りない気がします。
そこで、これらの図形を「簡単モーフィング」で動かすことにしました。

「モーフィング」と言ってますが、ただの線形補間です。
以前はこの作品(Johnny on the Monorail)のように手計算していました。


float xF = _to.get(f).x * _rate + _from.get(f).x * (1.0 - _rate);


でも 'Processing' には 'lerp()' というその名もズバリの関数(Linear Interpolate の略)があったんですね。 今回はこれを使いコードをシンプルにできました。


 float x = lerp(shapeFrom.get(i).x, shapeTo.get(i).x, easeRatio);


いくつかの図形を順にモーフィングさせて、最後の図形から最初の図形に変化するようにしてやればループアニメーションの出来上がりです。


これは永遠に見ていられる。 これでいつ永遠の命を手に入れても大丈夫!


理想のクリエイティブ・コーディング作品

私にとって理想の作品は「毎回実行するたびに新鮮な驚きを私に与えてくれるもの」で、それをいかに実現するかが毎回のテーマです。
今回のコードではここにその工夫が詰まっています。


float shapeBase = random(TWO_PI);
float shapeDiv  = random(HALF_PI);

float theta = HALF_PI * floor(8 * sin(shapeBase + shapeDiv * i));


今回はこのキモとなる部分がコード的にもなかなか綺麗に収まったので満足です。


"Processing" のサンプルコード

GPL で公開します。どうぞご自由にお使いください。このコードを利用して何か作品を作ってもらえるととても嬉しいです。

本コードはスクリーン上には何も描画しません。描画の結果は静止画のファイルとして保存されます。それらのファイルを ffmpeg 等で動画に変換できます。


/**
 * Good Times Bad Times.
 * It's a media art that displays the morphing generative design of rectangles.
 * original idea : https://openprocessing.org/sketch/1125777
 * 
 * @author @deconbatch
 * @version 0.1
 * @license GPL Version 3 http://www.gnu.org/licenses/
 * Processing 3.5.3
 * 2021.05.23
 */

void setup() {
  size(720, 720);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  rectMode(CENTER);
  smooth();
  noLoop();
}

void draw() {
  
  int   frmRate  = 30;
  int   frmMorph = frmRate * 1;       // morphing duration frames
  int   cycles   = 8;                 // animation cycle no
  int   frmMax   = frmMorph * cycles; // whole frames
  float rSize    = 25.0;

  // generate shapes
  ArrayList<ArrayList<PVector>> shapes = new ArrayList<ArrayList<PVector>>();
  float weights[] = new float[cycles];
  for (int i = 0; i < cycles; i++) {
    shapes.add(getShape(rSize * 2.0));
    weights[i] = rSize * floor(random(6));
  }

  // variables for animation
  ArrayList<PVector> shapeFrom = new ArrayList<PVector>();
  ArrayList<PVector> shapeTo   = new ArrayList<PVector>();
  float weightFrom = 0.0;
  float weightTo   = 0.0;
  int   cycleCnt   = 0;

  for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
    float frmRatio  = map(frmCnt, 0, frmMax, 0.0, 1.0);
    float easeRatio = InFourthPow(map(frmCnt % frmMorph, 0, frmMorph - 1, 0.0, 1.0));
  
    // shape for morphing animation. loops cyclic.
    if (frmCnt % frmMorph == 0) {
      cycleCnt   = frmCnt / frmMorph;
      shapeFrom  = shapes.get(cycleCnt);
      shapeTo    = shapes.get((cycleCnt + 1) % cycles);
      weightFrom = weights[cycleCnt];
      weightTo   = weights[(cycleCnt + 1) % cycles];
    }

    // draw
    blendMode(BLEND);
    background(0.0, 0.0, 100.0, 100.0);

    blendMode(DIFFERENCE);
    fill(0.0, 0.0, 100.0, 100.0);
    stroke(0.0, 0.0, 100.0, 100.0);
    strokeWeight(lerp(weightFrom, weightTo, easeRatio));
    for (int i = 0; i < shapeFrom.size(); i++) {
      float x = lerp(shapeFrom.get(i).x, shapeTo.get(i).x, easeRatio);
      float y = lerp(shapeFrom.get(i).y, shapeTo.get(i).y, easeRatio);
      rect(x, y, rSize, rSize);
    }

    blendMode(BLEND);
    casing();

    // for stop motion
    if (frmCnt % frmMorph == 0) {
      for (int i = 0; i < frmRate; i++) {
        saveFrame("frames/" + String.format("%04d", cycleCnt) + ".00." + String.format("%04d", i) + ".png");
      }
    }

    // for moving motion
    saveFrame("frames/" + String.format("%04d", cycleCnt) + ".01." + String.format("%04d", frmCnt) + ".png");
  }
  
  exit();
}

/**
 * getShape : generates the shape
 */
ArrayList<PVector> getShape(float _size) {

  int   pvNum     = 15;
  float shapeBase = random(TWO_PI);
  float shapeDiv  = random(HALF_PI);

  float x = 0.0;
  float y = 0.0;
  ArrayList<PVector> pvs = new ArrayList<PVector>();
  for (int i = 0; i < pvNum; i++) {
    float theta = HALF_PI * floor(8 * sin(shapeBase + shapeDiv * i));
    x += _size * cos(theta);
    y += _size * sin(theta);
    pvs.add(new PVector(x, y));
  }

  // centering the shape
  float xMin = width;
  float xMax = 0;
  float yMin = height;
  float yMax = 0;
  for (PVector pv : pvs) {
    xMin = min(xMin, pv.x);
    xMax = max(xMax, pv.x);
    yMin = min(yMin, pv.y);
    yMax = max(yMax, pv.y);
  }
  float xDiv = (width - xMin - xMax) * 0.5;
  float yDiv = (height - yMin - yMax) * 0.5;
  for (PVector pv : pvs) {
    pv.add(xDiv, yDiv);
  }

  return pvs;
}
  
/**
 * InFourthPow : easing function.
 */
private float InFourthPow(float _t) {
  return 1.0 - pow(1.0 - _t, 4);
}

/**
 * casing : draws fancy casing
 */
void casing() {
  float w = min(width, height) * 0.05;
  fill(0.0, 0.0, 0.0, 0.0);
  strokeWeight(w + 4.0);
  stroke(0.0, 0.0, 0.0, 100.0);
  rect(width * 0.5, height * 0.5, width, height);
  strokeWeight(w);
  stroke(0.0, 0.0, 100.0, 100.0);
  rect(width * 0.5, height * 0.5, width, height);
}


/*
Copyright (C) 2021- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

QooQ