AI Dev Lab
FluxBloom

FluxBloom ができるまで — Perlin と Voronoi で絵を生む

ジェネレーティブアートの開発ログ。 Perlin ノイズ・Voronoi 分割・L-System の枝分かれ・リサジュー曲線・曼荼羅など 6 種類のアルゴリズムを素の JavaScript で実装し、 種の値だけで同じ絵を再現できるようにするまでの記録です。

FluxBloom は、Perlin ノイズ・Voronoi 分割・L-System・リサジュー曲線・等高線・曼荼羅の 6 種類のアルゴリズムで抽象画を生成するエンジンです。全部素の JavaScript と Canvas で書いていて、種(シード)の値だけで同じ絵を完全に再現できます。

#🎯 「ランダム」と「再現できる」を両立させる

ジェネレーティブアートで最初に決めるのは、乱数をどう扱うかです。

Math.random() を使うと、気に入った絵が出ても二度と同じものを出せません。保存し忘れたら終わりで、URL で人に見せることもできない。

そこで全体をシード付きの疑似乱数で組みました。

ts
// 線形合同法。 同じ seed からは 必ず同じ列が出る
s = (s * 1103515245 + 12345) & 0x7fffffff

Math.random() は一切呼びません。粒子の初期位置も、色の選択も、揺らぎも、全部このシードから引きます。結果として URL にシードを載せるだけで、誰の環境でも同じ絵が出ます

#🌊 Perlin ノイズを自前で書く

6 アルゴリズムのうち 3 つ(フロー場・等高線・曼荼羅)が Perlin ノイズを使います。

Perlin ノイズは「なめらかにつながる乱数」です。ふつうの乱数は隣り合う点が無関係ですが、Perlin は近い座標なら近い値を返します。だから自然な流れや地形に見えるわけです。

実装は 3 つの部品でできています。

ts
// 1. 補間カーブ (5次エルミート) — 6t⁵ - 15t⁴ + 10t³
private fade(t: number): number { return t * t * t * (t * (t * 6 - 15) + 10) }

// 2. 勾配ベクトル — ハッシュ値から 8 方向を選ぶ
private grad(hash: number, x: number, y: number): number {
  const h = hash & 7
  const u = h < 4 ? x : y
  const v = h < 4 ? y : x
  return ((h & 1) ? -u : u) + ((h & 2) ? -2 * v : 2 * v)
}

fade の 5 次式は Ken Perlin が 2002 年に改良したもので、これを線形補間や 3 次式にすると格子が目に見えてしまいます。なめらかさの正体はこの一行です。

順列表は Fisher-Yates でシャッフルしてから、512 要素に 2 周分展開しています。

ts
this.p = new Uint8Array(512)
for (let i = 0; i < 512; i++) this.p[i] = perm[i & 255]

こうしておくと p[X + p[Y]] のような添字アクセスで範囲外を気にせず済みます。毎回 & 255 を書かなくてよくなる、実装上の定石です。

#🎨 フロー場 ── ノイズを「向き」として読む

いちばん気に入っているのがフロー場です。やっていることは単純で、

  1. 画面にランダムな点を撒く
  2. その点の位置の Perlin ノイズを角度として読む
  3. その方向に少し進む。これを繰り返して線を引く
ts
const ang = perlin.noise(x * noiseScale, y * noiseScale) * Math.PI * 4
x += Math.cos(ang) * stepLen
y += Math.sin(ang) * stepLen

ノイズの値を「高さ」ではなく「向き」として解釈するだけで、粒子が見えない流れに沿って泳ぎはじめます。数百本を薄い不透明度(0.06〜0.2)で重ねると、筆致のような密度差が生まれます。

不透明度を下げて本数で稼ぐ、というのがこの手の絵の基本でした。1 本 1 本を濃く描くと、ただの線の束にしかなりません。

#🌿 L-System ── 文字列の書き換えで木を作る

樹形は L-System(Lindenmayer システム)です。植物の成長を、文字列の置換規則としてモデル化する手法で、実装は再帰で書きました。

ts
function branch(x, y, len, ang, depth, colorIdx) {
  // 枝を 1 本描いて、 先端から 2 本に分岐して再帰
}

分岐角度と長さの減衰率を変えるだけで、シダにも針葉樹にも珊瑚にもなります。パラメータ 2 つで植物の種類が変わるのが L-System の面白さです。

#🔷 Voronoi ── 最近傍で塗り分ける

Voronoi は「各ピクセルが、どの種に一番近いか」で色を決める分割です。定義がそのまま実装になります。総当たりなので種の数を増やすと重くなりますが、細胞や結晶の見た目はこれでしか出せません。

#😓 苦労したところ

6 つのアルゴリズムに同じパラメータを効かせる。 密度・複雑さ・スケール・揺らぎ・層・シードの 6 つを共通のつまみにしましたが、意味はアルゴリズムごとに違います。フロー場の「密度」は粒子数、Voronoi の「密度」は種の数。同じ名前で違う実装に効かせる設計にしたので、どのアルゴリズムでも同じ操作感で触れます。

ts
const particleCount = Math.floor(80 + params.density * 520)
const stepCount = Math.floor(80 + params.complexity * 220)

こういう「0〜1 のつまみを実際の値域に写す」変換を各アルゴリズムに持たせる形です。

「たまたま良い絵」を捨てないこと。 生成系は 9 割がハズレで、たまに良いものが出ます。だからこそシードの再現性が要ります。良い絵が出た瞬間にシードが URL に入っている、という状態を最初に作っておいたのが正解でした。

配色を作りすぎない。 色を自由にすると、たいてい濁ります。6 パレットに絞り、各パレットは背景色 + 数色の組で固定しました。選べる範囲を狭めたほうが、平均的な出来が上がります。

#🔭 今後の拡張

  • 反応拡散系(Gray-Scott)の追加 — 有機的な模様の定番
  • アニメーション書き出し(現在は静止画の PNG のみ)
  • 高解像度書き出し(印刷用)
  • 気に入った絵をシード付きでギャラリーに残す

#💡 このサービスから言えること

生成系のサービスでは、再現性が機能そのものです。 「もう一度同じものが出せる」がないと、良い出力に価値が生まれません。シード付き乱数は実装としては地味ですが、体験の土台でした。

そしてもうひとつ、古典的なアルゴリズムは今でも十分に強いということ。Perlin ノイズは 1985 年、L-System は 1968 年、Voronoi にいたっては 1908 年です。数十行で書けて、依存もゼロで、それでも見て面白いものが出ます。

[ ./next_action ]

読んだら、 FluxBloom を実際に動かす。

この開発ログは FluxBloom をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →