はじめに
(Three.jsは)初投稿です。
古来より伝わる「ライフゲーム」をモダンな言語で実装するチャレンジやっております。
今回はThree.jsを用いて立体視できるよう、かつ色のアニメーションで色彩を豪華にするトライをしてみます。
過去シリーズは以下です。
- ライフゲームをモダンな書き方で・Swift編・探究の章
- ライフゲームをモダンな書き方で・Swift編・発動の章
- ライフゲームをモダンな書き方で・Rust接触編
- ライフゲームをモダンな書き方で・Elixir接触編
- ライフゲームをElixir言語で並列化したかっただけなのに…
ライフゲームとは
概要は先駆者様の解説に委ねます。
コーディングに必要なルールは以下となります。
- 2次元グリッド上で展開され、各セルは「生」または「死」の2つの状態を持つ
- 各世代で、「隣接(上下左右と斜め4方向の計8方向にある)」セルを以下のルールに従って状態が更新される
- 誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代で誕生する
- 生存:生きているセルに隣接する生きたセルが2つまたは3つならば、次の世代でも生存する
- 過疎:生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する
- 過密::生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する
- 端は常に死んでいるとみなす(固定境界)
開発環境
- MacStudio2023(M2 Max)
- macOS Tahoe(26.1)
- Node.js v20.11.0
- Three.js 0.160.0
動作映像
とうろうみたいできれいだね。
コード説明
全体はGitHubに上げています。
特筆部分は以下となります。
セルの構造
一般的なライフゲームは2軸座標の表現のために2次元配列を用いるのが大半ですが、今回は表示のための三次元空間座標に対応する必要があったため、2次元配列では煩雑な処理になることが目に見えていました。
そこで代わりに、キーにライフゲームの座標を表す文字列とした連想配列で表現しています。
// X軸値とY軸値を座標表現で文字列化
const cellKey = (x, y) => `${x},${y}`;
連想配列内の値の型は以下になります。
| 変数名 | 型 | 役割 |
|---|---|---|
| alive | boolean | 生死状態 |
| shape | Three.Mesh | セルのグラフィック |
ライフゲームロジック
生死判定は定石通りですが、座標の始点はセルは左上ですが、三次元グラフィックは中央が視点になるので、座標変換が必要になりました。
// 生存数
const countAliveNeighbors = (cell) => {
let result = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue; // 自分を除外
// 隣接するセルの座標を計算
const neighborX = cell.shape.userData.cellX + dx;
const neighborY = cell.shape.userData.cellY + dy;
// 連想配列キー生成
const key = cellKey(neighborX, neighborY);
// 存在チェック
if (cellsMap.hasOwnProperty(key)) {
const neighborCell = cellsMap[key];
if (neighborCell.alive) {
result += 1;
}
}
}
}
return result;
};
// 状態更新
const updateAliveFlag = (cell) => {
// 生存:生きているセルに隣接する生きたセルが2つかまたは3つあれば、生存維持
// 過疎or過密:上記の条件を満たさない場合は、次の世代で死滅する
// 誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代で誕生する
// 過疎or過密:上記の条件を満たさない場合は、次の世代で死滅する
const aliveCount = countAliveNeighbors(cell);
if (cell.alive && (aliveCount < 2 || aliveCount > 3)) {
return false;
} else if (!cell.alive && aliveCount == 3) {
return true;
} else {
return cell.alive;
}
};
オブジェクトの色の変化
色の変化は色相の変化で行うのが実装しやすいので、色指定はRGB(赤、緑、青)ではなく、HSL(色相、彩度、輝度)で行いました。
色の帯の移動を無限ループさせるイメージです。
// セルのマテリアル色を更新
const updateCellMaterialColor = (material, h, alive) => {
// (色相、彩度、輝度)
material.color.setHSL(h, 0.9, alive ? 0.75 : 0.05);
};
アニメーション動作はThree.jsのアニメーション機能内に仕組んでいます。
// アニメーションループ
const animate = () => {
requestAnimationFrame(animate);
// 前のフレームからの経過時間(秒)を取得
const delta = clock.getDelta();
// タイマー更新
const borderTime = 0.125;
countClockTime += delta;
// ライフゲームの生死状態を保持する連想配列
const aliveMap = {};
if (countClockTime >= borderTime) {
// ライフゲーム世代更新
for (const key in cellsMap) {
if (cellsMap.hasOwnProperty(key)) {
let cell = cellsMap[key];
// 生死状態を確認
const alive = updateAliveFlag(cell, cell.alive);
// 連想配列を登録
aliveMap[key] = alive;
}
}
// タイマーリセット
countClockTime -= borderTime;
}
// セル表示の更新
for (const key in cellsMap) {
if (cellsMap.hasOwnProperty(key)) {
let cell = cellsMap[key];
// 世代反映
if (aliveMap.hasOwnProperty(key)) {
// キーが一致するセルの生死状態を更新
cell.alive = aliveMap[key];
}
// 物体
let shape = cell.shape;
// 色のアニメーション
shape.userData.currentHue += 0.002;
updateCellMaterialColor(shape.material, shape.userData.currentHue, cell.alive);
}
}
// 視点操作の更新
controls.update();
// 描画実行
renderer.render(threeScene, camera);
};
// アニメーション開始
animate();
実際の動作確認なら
以下に公開していますので思う存分楽しんでください。
今後の展開予定
- 他の技術でのチャレンジ
- ライフゲーム以外のロジックのチャレンジ
参考文献
あとがき
初めて本格的にブラウザ上での3Dグラフィックのアプリを作成しました、かつては特定の機器やソフトウェアでなければまともに動作させる事が出来ない表現をここまで手軽になったこと、ただただ感銘いたしました。ライフゲーム以外でも表現できるロジックを探して作成していきたいです。
おわりに
ここまでご閲覧いただきありがとうございます。
今後も思いつくままに記事投稿を続けて行きたい所存であります。
見える、私にも見えるぞ!

