クリスマスだしp5.jsでDLA雪片を降らせてみたよ2019


2019年12月24日
With
クリスマスだしp5.jsでDLA雪片を降らせてみたよ2019 はコメントを受け付けていません。

もーね、「あっ!」という間に年の瀬ですよ。1つ前のエントリが「新年のごあいさつ」ってどーなのよ、オレ。本当にそれでいいの? オレ。

今年1年、p5.jsそのものの動向は比較的マメにウォッチしながらも、肝心のスケッチのほうはほぼ壊滅状態だったのですが、せめて、昨年、一昨年と12/24にアップしてきた「p5.jsで雪片降らせてみる」シリーズは今年もやっておこうと、年内納品の仕事そっちのけでVS Codeに向かっている次第です。

なお、この記事は恐れ多くも「Processing Advent Calendar 2019」の24日目にエントリさせていただきました。「Processing Advent Calendar」は、p5.jsを触り始めた3年ほど前から、毎年楽しく拝見しているのですが、参加は今年が初めてです。今年も既に多くの方々が、いろんな切り口でProcessing/p5.jsの勉強の仕方、楽しいスケッチ、スゴイ使い方を続々紹介されています。

僕は普段、仕事としてコードを書いたり読んだりすることはなく、完全な趣味としてコーディングを楽しんでいる立場なのですが、プロのエンジニアやデザイナー、あるいは教育者として、Processing/p5.jsに触れておられるいろんな方の視点は、どれも大変興味深く、強力なモチベーションになっています。いつも本当にありがとうございます。

さて、今年の「p5.jsで雪片降らせてみる」スケッチですが、こんな感じになりました。

●クリスマスだしp5.jsでDLA雪片を降らせてみたよ2019(クリックで別タブが開きます)
●同上 600×600px版(クリックで別タブが開きます)

上のほうのリンクはブラウザウインドウのリサイズに対応していますので、Chromeあたりで開いていただいて、Windowsの場合は「F11」なんかでフルスクリーン表示にしていただくと、

こんな感じでスクリーンセーバー風にもできます。ランダム生成されるさまざまな形の雪片が、ゆらゆらクルクルする姿を、ボンヤリと眺めていただければ。(コードはこのエントリの最後に)

今年は「DLA」でやってみた

「雪片降らせてみる」は、2017年の年末にフラクタルの習作として描いた「コッホ雪片」の画像をランダムに選んで降らせてみたのが最初でした。昨年は、前年に挫折した「雪片の逐次生成」をテーマに、反復関数系の一種である「Chaos Game」と呼ばれるアルゴリズムを使って、それっぽい形をランダムに描いて降らせることに挑戦しました。

●クリスマスだしp5.jsでコッホ雪片を降らせてみたよ(2017)
●クリスマスだしp5.jsでフラクタル雪片を降らせてみたよ2018

で、今年のテーマは「ランダム生成で、去年よりもさらに雪片ぽく見える形を降らせたい」。使えそうなアルゴリズムはあるかしらんと探していたら、皆さんご存じシフマン先生の「The Coding Train」で、なんと昨年のクリスマスイブ(!)にこんな回をやってました。

●Coding Challenge #127: Brownian Tree Snowflake

…うん。これだね。コレだよ。もう、これがヒラヒラ降ってくりゃ、それで万事オッケーだよ。

この雪片生成スケッチでシフマン先生がシミュレーションしているのは「Diffusion Limited Aggrigation(DLA)」と呼ばれる現象です。日本語にすると「拡散律速凝集」となります。カタカナだと「カクサンリッソクギョーシュー」。おいしそうな字面です。

●拡散律速凝集(Wikipedia)

DLAは「ブラウン運動する粒子が核となるクラスタに取り込まれクラスタを成長させる過程」のことで、自然界だと菌コロニーや鉱物結晶の成長過程などなど、いろんな系に見られるのだそうです。少し調べてみたら、ProcessingやTypescriptで、DLAのシミュレーションをされている方もいらっしゃいました。いずれもたいへん興味深いので、合わせてぜひ。

●ブラウン運動もランダムウォークも無し! Processing で DLA 風の画像を作成|deconbatch|note

●DLAってアートみたいだよね。綺麗だ。- @RyosukeCla | Qiita

「雪片ぽい形」をどうやって作ってる?

さて、上で紹介させていただいた各記事を見ていただいても分かるのですが、通常のDLAシミュレーションだと、生成される画像は、シフマン先生がビデオで紹介している「雪片」よりも、もっと有機的というか「不規則」な感じになるはずなんですよね。「どうやって雪片ぽくしてるんだろう?」と思いながらビデオを見ていたら、非常に単純な方法で目から鱗でした。

ざっくり説明してしまうと、canvasの端っこから中央に向かってランダムウォークで移動する粒子を放ちながら、クラスタを成長させます。クラスタがcanvas端に届くサイズまで成長したら完成。できあがりはこんな感じの枝になります。

枝ができたら、上下を反転させて枝の上にもう1回描画。

さらにこれを、根本を軸にして60度ずつ回転させながら追加で5回描けば…

完成! 美しい。「反転」には「scale()」を、「回転」には「rotate()」を使う、非常にシンプルかつオーソドックスな方法でした。

プログラムの話

さてさて、理屈は分かったので、この方法で雪片描いて降らせましょう。

ちなみに、シフマン先生はこのスケッチをProcessing(Java)で解説していたのですが、僕は、ビデオを見ながらp5.js向け(Javascript)に書き換えています。シフマン先生のお手本を、そのままp5.jsで動くようにしたのがこちら。

●Brownian Tree Snowflake powered by p5.js(クリックで別タブが開きます)

「Processingからp5.jsへの書き換え」は、単純なスケッチであれば割と簡単にできることが多いです。書店で売られているクリエイティブコーディングの入門書は、サンプルコードがProcessing向けのものが多いのですが、ちょっとの変更でp5.jsに対応するケースが多々ありますので「p5.jsでクリエイティブコーディングしたいけど、入門書のサンプルがProcessingばっか…」とヘコんでいらっしゃる方は、ぜひ書き換えにチャレンジしてみるとよいと思います。手前ミソですが、過去にこんなエントリも書いてますので、よければご参考までに。

●「p5.jsでやってみよう」と思ったワケ

●Processingからp5.jsへの移植は(難しいことしなければワリと)簡単

あと、今回のコードをp5.js向けに書き換えていて思い出したのですが、Processingのコードにはよく「ArrayList<>」というフレーズが出てきます(先ほどのシフマン先生のスケッチにも出てきました)。これ、Java(Processing)で「アレイリスト」と呼ばれる便利機能を備えた配列を使いたい際の宣言なのですが、Javascript(p5.js)には、パッと見、対応するものがないので、特に最初のころは、どう変えればいいのか途方に暮れていたような気がするんですね。

話のスコープを「Processingからp5.jsへの書き換え」という範囲に限定し、なおかつ不正確なことを承知で言ってしまうと「Javascript(p5.js)の配列(Array)は、Java(Processing)だとアレイリスト(ArrayList)を使わないとできないことが最初からできるので、書き換え時にはArrayListの部分を、そのまんま配列で置き換えちゃえば、あらかた動く」となります。細かいこと考えずに「えいやっ」とやっちゃえば、けっこうなんとかなるかと。ちゃんと検証したわけでなく、自分がこれまでに試した範囲での印象なので、もしかするとメソッドの名前や使い方に違いがあって、そのままでは動かないケースもあるかもしれません。もしそういうケースに遭遇したら、対応するものをリファレンスで探して直しましょう。で、その後、時間がある時で結構ですので、僕にも教えてください(笑)。

こわごわ「createGraphics」を使ってみた

複数のDLA雪片を降らせるにあたって、今回はp5.jsの「createGraphics」というメソッドを使いました。createGraphicsを使うと、メモリ上に「p5.jsの関数を使ってスケッチを描けるグラフィックオブジェクト」を生成できます。このグラフィックオブジェクトは「image」関数を使って、イメージオブジェクトと同じようにメインのcanvas上に表示できます。

…ん? 分からない? そうですね… 普通のp5.jsの使い方だと、最初にsetup内に「createCanvas」と書いて、絵を描いていくメインのカンバスのサイズなんかを設定しますよね。「createGraphics」では、そのメインのカンバスの、好きなところに張り付けて使える付箋紙みたいなものを作れるんです。もちろん、その付箋紙にもp5.jsの関数を使って絵を描けます。アイデアしだいでいろいろなことに使える便利な機能です。

今回のスケッチでは、スタート時にシフマン先生の方法で「100×100px」のDLA雪片画像をcreateGraphicsで25個、一気に作っています。で、そのグラフィックオブジェクトを「座標」「落下速度」「回転方向/速度」「表示倍率」などのプロパティと共にさらにオブジェクト化し、配列に格納してフレームごとの絵を更新しています。

「createGraphics」に関しては、過去に大きめの絵を複数枚組み合わせて「万華鏡」のようなスケッチを作ろうとしたときに使ったことがあったのですが、「PCブラウザだと大丈夫だけど、モバイルブラウザだと確実に落ちる(再現性100%)」という不具合に見舞われ、今回もそのまま使うかどうかには、ちょっと迷いました。とりあえず「100×100px×25個」くらいの量であれば、モバイル(私物のiPhone 7 SafariとAndroid Chromeで確認)でも落ちずに動いてくれているようです。

●p5.jsで万華鏡風のスケッチを描いてみた

左右の動きは「風まかせ」にしてみた

昨年までの「降らせてみる」スケッチでは、各雪片の「左右」の動きを、各オブジェクトに持たせた「横方向移動量」のプロパティとnoiseで付けていました。

この方法だと、雪片ごとに色んな動きが生まれたのですが、よくよく考えてみたら、雪片の左右の動きって、本来であれば、吹いている「風」の方向と強さに由来するものだよなということに今さら気付きまして、今年のスケッチでは全体に影響する「風」を設定し、方向と強さをnoiseで変化させて、雪片はその影響で左右に動きながら落ちていくというロジックにしてみました。

各雪片の個性は若干弱まりましたが、ランダム生成で2つと同じ形のないDLA雪片が「風まかせ」にたゆたう姿を眺めていると、canvasの向こうをゆっくりと流れている気流までもがイメージされるような気がして、それはそれで程よい「侘び具合」になったのではないでしょうか。異論は認めます。

さてさてさて、何とか今年も「降らせてみたよ」が描けてホッとしております。これも「Advent Calendarにエントリしてしまった以上、やらねば」という良い意味でのプレッシャーがあったからかと。「Processing Advent Calendar 2019」は明日が最終日です。

そういえば、2014年あたりから開発が続けられてきた「p5.js」も、来年早々についに「バージョン1」になるそうで、これをきっかけに、これまで以上にいろんな立場でクリエイティブコーディングを楽しむ人が増えるといいなぁと、今からワクワクしています。周辺ライブラリも新しいのがいろいろ出てきているので、そのあたりなんぞも試しつつ、僕自身ももっといっぱいスケッチを描く2020年にしようと決意を固めております。

みなさまも良いお年をお迎えくださいませ。Merry Christmas, Happy New Year and Happy Coding !!!

const flakeCount = 25;
let flakeArray = [];
let tmpStg, flake, wind, wdnois;

function setup() {
	createCanvas(windowWidth, windowHeight);
	imageMode(CENTER);
	for (let i = 0; i < flakeCount; i++) {
		flakeArray.push(new Snowflake());
	}
	wind = 0;
	wdnois = random();
}

function draw() {
	background(10);
	wind = noise(wdnois) * 4 - 2;
	flakeArray.forEach(obj => { obj.display(); obj.update(); });
	wdnois += 0.002;
}

class Snowflake {
	constructor() {
		this.flakeimg = setStg();
		this.scl = random(.3, 1.7);
		this.loc = createVector(random(0, width), random(0, height));
		this.spd = random(.3, 1.5);
		this.rotspd = random(30, 150);
		if (random() < .5) { this.rotspd *= -1; };
	}

	update() {
		this.loc.x += wind * this.scl * 1.5;
		this.loc.y += this.spd * this.scl;
		if (this.loc.y - 50 * this.scl > height) {
			this.scl = random(.3, 1.7);
			this.loc.y = -50 * this.scl;
			this.loc.x = random(0, width);
			this.spd = random(.3, 1.5);
			this.rotspd = random(30, 150);
			if (random() < .5) { this.rotspd *= -1; };
		}
		if (this.loc.x - 50 * this.scl > width) {
			this.loc.x = -50 * this.scl;
		}
		if (this.loc.x + 50 * this.scl < 0) {
			this.loc.x = width + 50 * this.scl;
		}
	}

	display() {
		push();
		translate(this.loc.x, this.loc.y);
		rotate(frameCount / this.rotspd);
		scale(this.scl, this.scl);
		image(this.flakeimg, 0, 0);
		pop();
	}
}

function setStg() {
	let current;
	flake = [];
	tmpStg = createGraphics(100, 100);
	tmpStg.colorMode(HSB);
	tmpStg.translate(tmpStg.width / 2, tmpStg.height / 2);
	tmpStg.rotate(PI / 6);
	tmpStg.noFill();
	let stcol = random(150, 330);
	tmpStg.stroke(stcol, 40, 100, .5);
	tmpStg.strokeWeight(2);
	let isFinish = false;
	while (!isFinish) {
		current = new Particle(tmpStg.width / 2, random(2));
		while (!current.finished() && !current.intersects()) {
			current.update();
		}
		flake.push(current);
		if (current.pos.x >= tmpStg.width / 2) {
			for (let j = 0; j < 6; j++) {
				tmpStg.rotate(PI / 3);
				for (let i of flake) {
					i.show();
				}
				tmpStg.push();
				tmpStg.scale(1, -1);
				for (let i of flake) {
					i.show();
				}
				tmpStg.pop();
			}
			isFinish = true;
		}
	}
	return tmpStg;
}

class Particle {
	constructor(x, y) {
		this.pos = createVector(x, y);
		this.r = 1;
	}

	update() {
		this.pos.x -= 1;
		this.pos.y += random(-3, 3);
		let angle = this.pos.heading();
		angle = constrain(angle, 0, PI / 6);
		let magnitude = this.pos.mag();
		this.pos = p5.Vector.fromAngle(angle);
		this.pos.setMag(magnitude);
	}

	show() {
		tmpStg.point(this.pos.x, this.pos.y);
	}

	finished() {
		return (this.pos.x < 1);
	}

	intersects() {
		let result = false;
		for (let i of flake) {
			let d = dist(i.pos.x, i.pos.y, this.pos.x, this.pos.y);
			if (d < this.r * 2) {
				result = true;
				break;
			}
		}
		return result;
	}
}

function windowResized() {
	resizeCanvas(windowWidth, windowHeight);
}

(以下、おまけ)

●DLA雪片を降らせてみたよ2019「一期一会強化版」(PC&Chromeでの実行推奨・クリックで別タブが開きます)

もともと、今年のスケッチは、画面下に雪片が消えたら、雪片画像を新たに作り直して、また新たな雪片として上から降らせる(つまり、二度と同じ形の雪片が降ってくることがない)という仕様にしていました。

Windows 10のChromeでは、多少古めのノートPCなどでも問題なく動いていたのですが、EdgeやFirefox、モバイル環境だと、この「雪片画像更新」のタイミングで、負荷が高くなりすぎて「カクつき」が起こってしまいました。そのため、最終的なスケッチでは決まった個数の雪片画像を最初に一気に作ってしまい、実行中には更新しない形にしました。

↑の「一期一会強化版」は、当初の構想どおり、新たな雪片画像をそのつど作り直す仕様です。試しに開いてみて、もし「カクつき」が気にならないようであれば、こちらもオススメです。「一度消えたら、もう二度と会えない」雪片たちとの一期一会を感じていただければ…。