ITエンジニア勉強ブログ

自分が学んだことを共有していきます。

森のプロシージャル生成

森をプロシージャル生成するJavaScriptプログラムを作成しました。


See the Pen
Procedural Generation of 2D Forest
by Imai (@imai1)
on CodePen.


細かいところをちょこちょこいじってはいますが、本質的には前に作成した花畑生成のプログラムの画像リソースを差し替えただけです。よければ以下の記事もご覧ください。

imai1.hatenablog.com

imai1.hatenablog.com


画像はいらすとやさんからお借りしました。

花畑のプロシージャル生成

花畑をプロシージャル生成するJavaScriptプログラムを作成しました。


See the Pen
Flower Garden
by Imai (@imai1)
on CodePen.


アルゴリズムの解説

花の配置

自然に見えるよう描画時に位置を崩していますが、データ構造的には単に碁盤状のマス目に花を配置しているだけです。ただし、各マス毎に一様ランダムに花または空白をサンプリングするとホワイトノイズ画像のような配置になってしまうため、同種の花がカタマリを作るよう工夫を加えました。

はじめに各マスにランダムに花を配置して、マルコフ連鎖モンテカルロ法のように状態遷移を繰り返し、良い具合にカタマリが作られた最後の状態を描画します。

状態遷移にはシェリングの分居モデルを利用しました。異人種のエージェントが引越しを繰り返すことでクラスタを作って居住するというエージェントシミュレーションモデルです。詳しい解説は以下の記事をご覧いただけたら嬉しいです。

imai1.hatenablog.com


コード内では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で非同期処理を行います。

配置モデルをシミュレーションして、画像の読み込みも終わったら、drawModelToCanvascanvasに描画します。

// モデルを初期化する。
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);

素材について

いらすとやさんからお借りしました。コード内に個別の画像のリンクがあります。

デフォルメ具合や色合いの揃った素材を集めないとあまり綺麗にならないので頑張って厳選しました。

シェリングの分居モデルのJavaScript実装

シェリングの分居モデルをJavaScriptで実装しました。


ランダムに配置されたエージェント達が初めは移動を繰り返していますが、だんだんと同種のエージェント同士でカタマリを作って定住するようになっていきます。
だいたい数十秒で収束しますので、しばらく見守ってあげてください。


See the Pen
Untitled
by Imai (@imai1)
on CodePen.

このモデルの面白いところは、非常に簡単なルールのエージェントしか実装していないのに、上記のような挙動になることです。初めて知ったという方は以下の解説もご覧いただけたら嬉しいです。

モデルについて

モデルの定義

モデルの定義は非常にシンプルです。

  • 碁盤状のマップを用意して、各マスにランダムにエージェントを配置する。
  • 一定時間ごとに以下を繰り返す。
    • 各マスのエージェントごとに、隣接する8マスの[同種のエージェント数/全エージェント数]を計算し、閾値未満なら隣接する空きマスにランダムに移動させる。

たったこれだけで、冒頭のように分居するエージェント達のシミュレーションが作れてしまいます。

モデルの解説

このモデルは、日本人街や中華街など、異人種が集団で固まって居住する現象を説明する理論として、1970年代に経済学者シェリングが考えたものとのことです。

引越しの閾値は別種のエージェントの不許容度合いを表しています。

もし閾値が1.0だとしたら、どうなるでしょうか?すなわち、隣接する領域に別種のエージェントがいる場合に必ず引っ越す場合にはどうなるでしょうか。

直観的には高速に分拠が進むようにも思えますが、実際には各エージェントはバラバラに移動し続け収束しません。冒頭のCodePenでテキストボックスの閾値に1.0を入力して確かめてみてください。リセットボタンをつけていませんが途中から閾値を変えても結果は大差ありません。

一方、マップのサイズにもよりますが、閾値を下げていって0.3くらいでも十分に分居が生じます。

現実には短時間に連続して引越しできないとか、隣接領域に移動するのではなく良さそうな居住区に飛び地で移動するなどの違いはありますが、分拠は別種のエージェントに対する不許容の結果として生じるのではなく、むしろある程度の許容によって生じる(かもしれない)という可能性を示したのがこのモデルの面白いところです。

なお、シェリングは後にノーベル経済学賞を受賞する方ですが、まだコンピュータが一般に普及していない時代に、手計算でこのシミュレーションを行ったらしいです。

実装について

分居モデルのクラス

全コードはCodePenの方をご覧いただくとして、主要な部分を抜粋しました。

class SegregationModel {
  constructor(width, height) {
    this.width = width;
    this.height = height;
    this.map = new Array(this.height);
    for (let y = 0; y < this.height; ++y) {
      this.map[y] = new Array(this.width);
    }
  }
  initializeMap(probabilityDistribution) {
    // エージェントの分布を初期化する。
  }
  countFraction(x, y) {
    // (x, y)の隣接領域の同種エージェント割合を数える。
  }
  move(x, y) {
    // (x, y)のエージェントを隣接する空き領域にランダムに移動させる。
  }
  update(threshold) {
    // 1世代更新する

    // 充足していないエージェントの探索
    const movers = new Array();
    for (let y = 0; y < this.height; ++y) {
      for (let x = 0; x < this.width; ++x) {
        if (this.map[y][x] == undefined) {
          continue;
        }
        if (this.countFraction(x, y) < threshold) {
          movers.push([x, y]);
        }
      }
    }

    // 引越し
    // for文の順序の早い方に固まる傾向があるため、引っ越し順はシャッフルする
    for (const [x, y] of shuffle(movers)) {
      this.move(x, y);
    }
  }
}

SegregationModel.mapが各マスの状態です。エージェントの種類のIDか、空欄の場合はundefinedです。

update関数が、1世代分の引越しの更新処理です。for文の順に引越しをしていくと特定の方向に移動が固まる(例えばfor (y=0; y < height; y++) for (x=0; x < width; x++)だと、マップの左上にエージェントが集まっていく)傾向が確認できたので、1世代のmove関数の実行をランダムな順にする一手間が加えられていますが、本質的には前述の解説の擬似コードそのものかと思います。

canvasへの描画の関数

SegregationModelのある時点の状態の描画は、以下のdrawModelToCanvas関数にまとめました。

const imgs = new Array();
for (let i = 0; i < 2; i++) {
  imgs.push(new Image());
}

imgs[0].src = "省略";
imgs[1].src = "省略";

function drawModelToCanvas(model, canvasId, cellWidth, cellHeight) {
  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 dx = x * cellWidth;
        const dy = y * cellHeight;
        const [width, height] = 
              (img.naturalHeight < img.naturalWidth) ?
              [cellWidth, img.height * cellWidth / img.width] :
              [img.width * cellHeight / img.height, cellHeight];
        context.drawImage(img, dx, dy, width, height);
      }
    }
  }
}

やっていることは簡単で、SegregationModel.mapの種類IDに従って、エージェントの画像を適切なサイズに縮小しながら碁盤状に貼り付けているだけです。

なお、本来はimgs.src[0] = "..."の部分でImageオブジェクトの初期化完了を待つための非同期処理が必要です。初期化完了前にdrawModelToCanvasを実行しても、何も描画されないこと以外の問題はなかったため、本コードでは気にしないことにしました。

エージェントの画像はいらすとやさんからお借りしました。言及しても危なくない対立が良いかなと思って、体育会系と理系の画像にしました。が、大きめのマップにするとコントラスト的に見難かったので反省です。

アニメーション

一定時間毎のアニメーションは次のように実装しました。

function update() {
  const inputThreshold = document.getElementById(thresholdInputId);
  const threshold = inputThreshold.value;
  model.update(threshold);
  drawModelToCanvas(model, canvasId, cellWidth, cellHeight);
}

setInterval(update, 500);

一定時間毎の処理を一つの関数にまとめて、setIntervalで呼び出すだけです。一定時間毎の処理は、閾値の取得、モデルの更新、再描画、の三つだけです。

JavaScriptやCodePenに画像を埋め込む方法

ちょっとしたコードスニペットやCodePenなどを公開する際に、画像リソースの置き場所に困ることがあるのではないでしょうか。この記事ではJavaScriptファイル内やCodePenに画像を直接埋め込む方法を紹介します。

といっても特別なことは何もなく、フロントエンドに精通した方には明らかだと思いますが、筆者のような初心者向けにやり方を残しておきたいと思います。

imgタグにデータURLを設定

通常、imgタグをHTMLに配置する場合は、src属性に画像のURLを指定するかと思います。

<img src="画像ファイルのURL"></img>

このsrc属性には、通常のURL以外にも、データURLという形式のテキストを設定することができます。データURLとは、画像などのリソースデータを何らかの形式(現行ではbase64のみ?)でエンコードしたデータを、あたかも通常のURLかのように扱えるデータ形式のことです。この機能によって、画像データをbase64エンコードしたテキストをタグに直接埋め込むことができます。

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg ... 長いので省略"></img>

JavaScriptで後から設定してもsrc属性を設定しても同様に機能します。

const img = document.getElementById("img");
img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg ... 長いので省略";

よって、使用したい画像をbase64テキスト形式に変換し、それをソースコードに直接書き込めば、外部の置き場所を確保せずにコードスニペットに画像を埋め込めます。

なお、base64テキストは画像のサイズに応じて長いテキストになりますので、スニペット自体のサイズにはご注意ください。この記事にもデータURLを含むタグを直接埋め込もうとしたのですが、エディタがだいぶ重くなった上、レンダリングされた結果を見ても通常のURLなのかデータURLなのか区別がつかないのでやめました。

データURLへの変換

画像をデータURLないしbase64テキスト(正確には「データURL = ヘッダ + base64テキスト」)に変換する方法はいろいろとありますが、JavaScriptではFileReaderクラスにreadAsDataURLというズバリな機能があります。

<input type="file" onchange="previewFile()"><br>
<img src="" height="200" alt="画像のプレビュー...">
function previewFile() {
  const preview = document.querySelector('img');
  const file = document.querySelector('input[type=file]').files[0];
  const reader = new FileReader();

  reader.addEventListener("load", function () {
    // 画像ファイルを base64 文字列に変換します
    preview.src = reader.result;
  }, false);

  if (file) {
    reader.readAsDataURL(file);
  }
}

developer.mozilla.org

画像を読み込んで、そのデータURLをテキストエリアに表示する以下のようなサンプルも作ってみました。


See the Pen
Untitled
by Imai (@imai1)
on CodePen.

身近な不思議の科学に関するリンク集 主に食べ物の劣化編

身近なのによく考えたら不思議だな・面白いなと筆者が思った物事について、紹介・説明しているWebページのリンク集をまとめました。

注:リンク先のページのテキストや画像の権利はそのページの作成者(場合によってはその更に引用元)に帰属します。リンク先の情報を利用・引用する際にはご留意ください。また、実験を行う場合には安全性に十分に配慮してください。また、食べ物関係は誤りや情報の更新が頻繁であるため、情報の正しさはご自身で判断をお願いします。

食べ物が固くなるのは何故?

冷蔵庫に少し長めに入れていた食べ物が固くなっていて不思議に思いました。

www.takarashuzo.co.jp

www.cotta.jp

tomiz.com

電子レンジに殺菌効果はある?

あるみたいですが、危なそうな食べ物を温める際にはご自身の責任と判断でお願いします。

www.olive-hitomawashi.com

archive.fcg-r.co.jp

www.excite.co.jp

細菌はどのように増える?

時間経過で指数的に増殖するみたいです。

pro.saraya.com

np-schools.com

逆に防腐作用のある環境では指数的に減っていくみたいです。

www.kewpie.co.jp

二日目のカレーが美味しいって本当?

具材の成分がルーに溶け出すみたいです。

www.yomeishu.co.jp

cp.glico.jp

食べ物の〇〇菌は本当に腸に届くの?

同じ名前の〇〇菌でもものによるみたいです。

nyukyou.jp

jp.glico.com

牛乳を飲むとお腹を壊すのは何故?

分解されなかった乳糖によって大腸の細菌が悪さをするからとのことです。分解能力の大小には諸説あるみたいです。

nyukyou.jp

www.j-milk.jp