メイキング: the generative octopus(p5.js)
English version
中文版
ezirarosです。よろしくお願いします。
今年からfxhashでジェネレーティブアートをNFTに作り始めました。
このプログラミング(p5.js)で描かれたタコは先週発表した作品で、この記事は開発プロセスの一部を記録します。まだこのタコをご覧になっていない場合は、読み進める前にご覧ください。
始める前に:なぜタコを?
このタコを作る前、Vogueのユーチューブチャンネルでフランスの女優Mathilde Warnierを紹介した動画を見て、彼女のインスタを見つけました。そしてこの写真に出くわしました。(タコの写真です。私の写真じゃなくてコピペしません。)
「これをプログラミングで描きたい!」とピンと閃いた。
いくつかの画像を検索し、美しいアプリMilanoteに保存した後、タコを作り始めました。
プロセス
下記は大まかな手順です
紙からスタート:タコの基本構造を描く(この段階で体と足の交点をプログラムの中心に決めた)
実装:
マインキャンバスを作成する
Head クラスとLegクラスを作成する
2つのクラスをheadとlegs(インスタンスの配列)にインスタンス化する
headとlegsのdrawメソッドを呼び出し,それぞれメインキャンバスでレンダリングする
Frameクラスを作成する
Bubbleクラスを作成する
プリセットを設定する
このタイプのジェネレーティブアートがよくわからない人へ
この作品はフレームごとに描かれています。つまり、描く前に全体的な外観を計算するのではなく、一度に1フレームずつリアルタイムで計算した後、前の画面に最新の結果を描くことです。
このgifを例にしたら
各フレームで前の状態をクリアするとこの感じになります。
難点と解決策
以下は12つを書きました
1. 足に「z-index」を付ける
足のクラス(Leg)では、createGrphicsで独自のp5.Graphicsオブジェクトを作成しました。それでレンタリング後、マインキャンバスに置きます。
なのでマインキャンバスのdrawは次のようになります
for (let i = 0; i < legs.length / 2; i++) {
legs[i].draw();
legs[legs.length - 1 - i].draw();
if (i == 0) {
head.draw();
}
}
bubbles.do('draw'); // 'do' is my custom function for array
2. 輪郭を描く
足を描くために二つのクラスを作りました。LegとLegNodeです。
一つLegには多くのLegNodeがあります。最新の位置・サイズ・方向はLegに保存し、前の位置・サイズ・方向はLegNodeに保存します。二つのLegNodeの共通外接線を描いたら輪郭になります。
輪郭を手描きっぽくするため、lineを使わなくで、いくつかの小さい点で描きます。点のサイズと透明度はランダムです。
この技はOrr Kislevから学びました。(ありがとうOrr Kislevさん)彼はこれを説明するために素晴らしいアニメーションを作りました。
3. 吸盤
最も重要なルールは「内側に描く」ことです。
たとえば、isRightLegs(index <4)&&右へ移動すると(direction.x> 0)、下部に描きます。下へ移動すると、左に描きます。しかし、上または左に移動している場合は描きません。
frameCountに応じて徐々に増減するrevealRateという属性を設定しました。それは吸盤のサイズと位置を影響します。吸盤の角度は足の方向と一緒です。
楕円を描くにはellipseではなく、scale(1, n)+circleの方が使いやすいと思います。
4. 足の動き
Legクラスにはposition(vec2)・direction(vec2)・rotation(float)三つの値があります。
10フレームごとにrotationをわずかにランダムに変化して、directionを回転します。そしてdirectionを正規化して、positionへ加えます。
if (frameCount % 10 == 0) {
this.rotation += fxRandom(-1, 1);
}
this.dir.rotate(this.rotation).normalize();
this.pos.add(this.dir);
足が同じ方向で回転し続けるかどうかを判断するため、各フレームのrotationを一つ配列に保存しました。
ずっと同じ方向になったら逆にします。
Legクラスにはposition(vec2)・direction(vec2)・rotation(float)三つの値があります。
10フレームごとにrotationをわずかにランダムに変化して、directionを回転します。そしてdirectionを正規化して、positionへ加えます。
if (frameCount % 10 == 0) {
this.rotation += fxRandom(-1, 1);
}
this.dir.rotate(this.rotation).normalize();
this.pos.add(this.dir);
足が同じ方向で回転し続けるかどうかを判断するため、各フレームのrotationを一つ配列に保存しました。
ずっと同じ方向になったら逆にします。
const n = 50; // the length of cache
this.rotationCache.push(this.rotation);
if (this.rotationCache.length > n) {
this.rotationCache.shift();
}
if (this.rotationCache.every(d => d > 0) || this.rotationCache.every(d => d < 0)) {
this.rotation *= -1;
}
}
5. テクスチャ
それは無数の小さい点で描かれています。circle(x, y, size)を私が作った関数circleNoise(x, y, size)に置き換えられました。円の中にランダムに数十の小さい点を描きます。
明るさの異なる小さい点の比率を変えたら異なる結果を得ることができます。
for (let i = 0; i < dotLength; i++) {
radius = fxRandom(0, this.size / 2);
x = fxRandom(-1, 1) * radius;
y = Math.sqrt(radius * radius - x * x, 2) * fxRandomSign();
pg.circle(center.x + x, center.y + y, fxRandom(2, 3));
}
6. 顔の向きを変える
translate(width/2, height/2);
scale(-1, 1);
translate(-width/2, -height/2);
7. 頭
頭は足と同じ方法で描きます。透明ならわかりやすく見れます。
8. Frame
Frameクラスにはx・y・width・height・frameWeightがあります。
そしてconfigにはdepthがあります。
足のindexをdepthに変換してconfig.depthと比べたら、遮られたかどうかわかります。
遮られた場合、
Frame1なら、範囲外には描きません。
Frame2なら、遮られた部分をerase()で消去します。
9. 足を前に動かせるように
1つの足に2つのキャンバスを作成しました。1つはフレームの後ろのスペース用で、もう1つは前のスペース用です。
10. 泡
泡は円や楕円ではありません。代わりに、ランダムな十角形で描きます。それはもっと有機的に見えると思いますから。
canvas.beginShape();
for (let i = 0; i < 10; i++) {
radius = this.size * fxRandom(1, 1.4);
angle = i / 10 * 360;
x = canvas.sin(angle) * radius;
y = canvas.cos(angle) * radius * this.compressY;
canvas.vertex(x, y);
}
canvas.endShape(canvas.CLOSE);
11. カラーパレット
ランダム関数で色を拾うことが好きですが、(たとえばfxRandom(360)で、メインカラーの色相を取得し、次に特定な数値(120や200とか)で加算または減算すると、セカンダリカラーの色相を取得します。)今回は手動で選択しました。
まずはいくつかのスタイル種類を作成しました
const categorySettings = [
{ name: CATEGORY_BLACK },
{ name: CATEGORY_BLACK2 },
{ name: CATEGORY_WHITE },
...
];
そして色相
const hueNameMap = [
{
name: COLOR_YELLOW,
hue: 45,
briBias: 0,
},
{
name: COLOR_BLUE,
hue: 210,
briBias: 0,
},
//....
];
それで1つプリセットを作る時、複数なプリセットを自動作成することができます。
hueNameMap.forEach(m => {
let hue = m.hue.length > 1 ? fxRandom(m.hue[0], m.hue[1]) : m.hue;
// pushPreset is a custom function, first param is the name of target category
pushPreset(CATEGORY_BLACK, {
name: capitalize(m.name),
hue,
briBias: -50,
noiseBias: -1,
sat: 0,
bgColor: [hue, 10, 80],
bubbleColor: [0, 0, 100, .5],
frame2Color: ['#fff'],
});
});
hueNameMap.forEach(m => {
pushPreset(CATEGORY_SMOKE, {
name: capitalize(m.name),
hue: m.hue.length > 1 ? fxRandom(m.hue[0], m.hue[1]) : m.hue,
briBias: 0 + m.briBias,
noiseBias: -2,
opacity: .06,
sat: 25,
bgColor: [0, 0, 20],
fgColor: [0, 0, 20],
bubbleColor: ['#fff'],
frameColor: ['#fff'],
satFade: -.5 + (m.satFadeBias || 0),
});
});
最後に、合計106セットのプリセットが生成されます。
12. パフォーマンスチューニング
悪い場合には10fps未満でした。変更したピクセルのみを更新して改善します。
足にpixelAreaを加えました。
this.pixelArea = {
x: this.pos.x,
y: this.pos.y,
width: 20,
height: 20,
};
移動中Legは、現在の位置がpixelAreaのうちにあるかどうかを確認します。そうでない場合は、pixelAreaを更新します。最後に、pixelAreaの中のピクセルをメインキャンバスに置きます。
before:
masterCanvas.image(this.canvas, 0, 0);
after:
this.pixel = this.canvas.get(pa.x, pa.y, pa.width, pa.height);
masterCanvas.image(this.pixels, pa.x, pa.y);
頭も同じようにして終わります。今は私のmacbook 2012で見ても大体30fpsぐらいです。
追記
私は、(仕事じゃない場合)作成する前に明確に考えるタイプではありません。代わりに、旅行のような、生成された結果に連れて行くことが好きです。(それでコードがめちゃくちゃになることがよくあります)
上記のノートはあくまでたくさんの方法の中での一つだけで、一番いい方法ではないかもしれませんが、あなたのお役に立てば嬉しいです。
また、二次流通マーケットは値下がりしているので、お気に入りのタコがあったらぜひ!
twitter: @s_r_r_z_