テキストを入力するだけで、自分だけのドミノ倒しが立体的に生成され、爽快に倒れていくWebアプリ「ドミノ倒しメーカー」。一見シンプルなミニゲームに見えますが、その裏側にはWebグラフィックス(Three.js)とリアルタイム物理演算(Cannon.js)、そしてコースを配置するためのアルゴリズムが詰まっています。
- URL: https://domino.work/
1. 物理と描画の分離構成:Three.js × Cannon.js
ドミノ倒しの挙動は、JavaScript用の軽量3D物理演算ライブラリ Cannon.js と、描画ライブラリ Three.js のタッグによって実現されています。
剛体(Body)とメッシュ(Mesh)の同期ループ
計算の基本ロジックは「画面には見えない物理ワールド(CANNON.World)」でドミノの衝突や重力を計算し、その結果を「描画用シーン(THREE.Scene)」のメッシュへ毎フレーム同期する手法です。
// メインアニメーションループ内の同期処理
function animate() {
requestAnimationFrame(animate);
// 1. 物理時間を1/20秒進める
world.step(1 / 20);
// 2. 物理剛体(phyObject)の位置・回転を Three.js のメッシュ(viewMesh)へ転写
for(let i = 0; i < objectCountTotal; i++) {
viewMeshList[i].position.copy(phyObjectList[i].position);
viewMeshList[i].quaternion.copy(phyObjectList[i].quaternion);
}
// 3. 描画
renderer.render(scene, camera);
}
物理エンジン側で剛体(Body)の位置(position)と姿勢クォータニオン(quaternion)を算出し、それをそのままThree.jsのメッシュに代入することで、滑らかなドミノの倒壊を実現しています。
2. コース自動生成の「幾何学(ジオメトリ)ロジック」
「ドミノ倒しメーカー」の最大の特徴は、直線だけでなくカーブ(旋回)、分岐、複数列への展開など多彩なコースを描く点です。これを実現するために、三角関数を用いた座標計算ロジックが組み込まれています。
① 曲線(カーブ)の配置ロジック
ドミノを自然なカーブ状に並べるため、回転半径(turnRadius)と回転角度(turnAngle)から正弦(sin)・余弦(cos)を用いて1個ごとのX/Z座標および回転角を算出しています。
// カーブ(右旋回)配置の計算例
paramSin = Math.PI * paramPiCoef / objectCount;
phyObject.position.x = objectStartPositionX - turnRadius * Math.sin(paramSin);
paramCos = Math.PI * paramPiCoef / objectCount;
phyObject.position.z = objectStartPositionZ - turnRadius - turnRadius * Math.cos(paramCos);
// ドミノ自体の向き(Y軸回転)もカーブの接線方向に合わせる
paramAngle = Math.PI * paramPiCoef / objectCount;
phyObject.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), paramAngle);
② 末広がり展開(マルチ列・ピラミッド分岐)
1本のラインから複数列へドミノが広がるコースでは、レイヤー数(multicolLayer)を動的に判定し、三角数(1, 3, 6, 10...)に基づいてドミノを三角形状に配置する計算が行われています。
3. Webで快適に動かすためのパフォーマンスチューニング
ブラウザ上で大量の3Dオブジェクトと物理計算を同時に走らせると、すぐにメモリやCPUが逼迫します。アプリ内では以下のような徹底した軽量化施策が取られています。
-
初期ロードの段階的オブジェクト追加
コース全体のオブジェクトを一度に物理ワールド(world.add)に追加せず、ドミノが倒れて一定の衝突数(phyObjectCollideCount)に達した段階で後半の剛体を動的に読み込む工夫がされています。 -
倒れたドミノの物理除外(スリープ&リムーブ)
倒れ終わったドミノは一定時間(setTimeout)経過後に物理ワールドから除外されます。描画用メッシュだけを残すことで、剛体同士の不要な衝突計算をカットしています。 -
ジオメトリ・マテリアルのキャッシュ管理
Three.jsのBoxGeometryやMeshLambertMaterialを毎回newするのではなく、サイズごとにキャッシュ(Map)して再利用することで、GPUメモリ(VRAM)の消費を削減しています。
4. 動的テクスチャ生成:文字をドミノにするロジック
入力されたメッセージを3Dドミノやカーペットに貼り付ける仕組みもスマートです。
-
Offscreen Canvasでの文字列描画
画面上には見えない<canvas>要素を利用し、入力文字列から1文字(または複数文字)を取り出してCanvas上に黒文字として描画。 -
Base64テクスチャ化
canvas.toDataURL()を使ってBase64画像化し、Three.jsのTextureLoaderに渡すことで、即座に3Dモデルの表面(マテリアル)にマッピングしています。 -
グリッド分割(カーペット演出)
大きな画像メッセージを作成する際は、画像を縦横のグリッド状(IMAGE_SPLIT_ROW×IMAGE_SPLIT_COL)にカットし、ドミノ1個1個の背面に分割テクスチャとして割り振ることで、ドミノが倒れた時に巨大な絵やメッセージが浮かび上がる演出を実現しています。
5. 臨場感を演出する「ドローンカメラ追従制御」
ただドミノが倒れるのを見るだけでなく、映画のような臨場感を生み出しているのが「ドローンカメラモード」です。
-
動的な焦点(Action Point)の計算
ドミノ同士の衝突衝撃速度(getImpactVelocityAlongNormal)が一定値を超えた地点をリアルタイムにバッファへ蓄積。 -
線形補間(Lerp)によるなめらかな追従
衝突地点の平均座標へ向かって、Vector3.lerp()を用いてカメラの注視点をヌルヌルと滑らかに追従・周回(cameraDroneOrbitSpeed)させています。 -
フィナーレのカメラ引き
コース末尾の特定トリガー(finaleTriggerObjectList)にドミノが到達すると、カメラが全体を俯瞰する視点に切り替わります。
おわりに
誰でも登録不要でサクッと遊べますので、ぜひお好きな言葉や絵文字を入力して試してみてください!
エンジニアの皆さんからの「描画パフォーマンスの改善策」「こういう演出・機能があったらもっと面白い」といったフィードバックやアドバイスをコメントでいただけると大変励みになります。
- ドミノ倒しメーカー: https://domino.work/
