花畑のプロシージャル生成
花畑をプロシージャル生成するJavaScriptプログラムを作成しました。
See the Pen
Flower Garden by Imai (@imai1)
on CodePen.
アルゴリズムの解説
花の配置
自然に見えるよう描画時に位置を崩していますが、データ構造的には単に碁盤状のマス目に花を配置しているだけです。ただし、各マス毎に一様ランダムに花または空白をサンプリングするとホワイトノイズ画像のような配置になってしまうため、同種の花がカタマリを作るよう工夫を加えました。
はじめに各マスにランダムに花を配置して、マルコフ連鎖モンテカルロ法のように状態遷移を繰り返し、良い具合にカタマリが作られた最後の状態を描画します。
状態遷移にはシェリングの分居モデルを利用しました。異人種のエージェントが引越しを繰り返すことでクラスタを作って居住するというエージェントシミュレーションモデルです。詳しい解説は以下の記事をご覧いただけたら嬉しいです。
コード内ではSegregationModel
というクラスがこの処理を担当します。このクラスも上記のリンク先で紹介したコードそのままですので、解説は上記の記事をご覧ください。
描画処理
まずは、花の画像用のImage
オブジェクトを用意します。
function loadImages(srcs) { const promises = []; for (const src of srcs) { promises.push(new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.src = src; })); } return promises; } const promiseLoadingImages = loadImages([ "data URLを列挙。長いので省略。" ]);
Image.src
の設定はPromise
で非同期処理を行います。
配置モデルをシミュレーションして、画像の読み込みも終わったら、drawModelToCanvas
でcanvas
に描画します。
// モデルを初期化する。 const model = new SegregationModel(20, 20); model.initializeMap(new ProbabilityDistribution([0.15, 0.15, 0.15])); // 描画前に収束させる。 const threshold = 0.6; for (let i = 0; i < 100; ++i) { model.update(threshold); } // Imageの読み込みが完了したらcanvasに描画する。 Promise.all(promiseLoadingImages).then(imgs => { drawModelToCanvas(model, canvasId, cellWidth, cellHeight, noiseSize, imgs); });
drawModelToCanvas
の実装は以下の通りです。
function drawModelToCanvas(model, canvasId, cellWidth, cellHeight, noizeSize, imgs) { const canvas = document.getElementById(canvasId); const context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < model.height; ++y) { for (let x = 0; x < model.width; ++x) { const person = model.map[y][x]; if (person != undefined) { const img = imgs[person]; const width = cellWidth; const height = img.height * cellWidth / img.width; // 画像の左端が x * cellWidth + ノイズ const dx = x * cellWidth + noise(noiseSize); // 画像の下端が (y+1) * cellHeight + ノイズ const dy = y * cellHeight - (height - cellHeight) + noise(noiseSize); context.drawImage(img, dx, dy, width, height); } } } }
大まかには、SegregationModel.map
を一マスの大きさがcellWidth
×cellHeight
のグリッドに描画しているだけです。
画像は横幅が必ずcellWidth
になるようにアスペクト比を維持して拡大縮小します。
const width = cellWidth; const height = img.height * cellWidth / img.width;
縦幅がcellHeight
と異なる場合は、(ノイズ0において)画像の下端がマスの下端に一致する位置に描画します。これによって、cellHeight
を画像より小さめに設定すると、データ構造状で上下に連続する花の画像が重ねて表示され、奥行き感が生まれます。
最後に、実際の描画位置にnoise
関数でランダムな変位を加えて完成です。
// 画像の左端が x * cellWidth + ノイズ const dx = x * cellWidth + noise(noiseSize); // 画像の下端が (y+1) * cellHeight + ノイズ const dy = y * cellHeight - (height - cellHeight) + noise(noiseSize); context.drawImage(img, dx, dy, width, height);
素材について
いらすとやさんからお借りしました。コード内に個別の画像のリンクがあります。
デフォルメ具合や色合いの揃った素材を集めないとあまり綺麗にならないので頑張って厳選しました。