ブラウザで遊べる3Dチンチロ、Three.js&Ammo.jsで作ったサイコロゲーム
Webブラウザ上でリアルなチンチロを作りたい…!
ということで作ってみたチンチロがこちらです。
今回は「3Dでサイコロを振るチンチロアプリ」をThree.js × Ammo.jsで作った話を書きます。
この記事では、
- Three.js(3D描画)でサイコロをモデリングする方法
- Ammo.js(物理演算)で物理的に転がす処理
- 出目を判定して役(ゾロ目やワンペアなど)を判定するロジック
- バナー演出・サウンド・UIの追加
までを サンプルコード付き で徹底解説していきます。
UIと効果音周りの実装
まずは画面左上にUIボタンと投げた回数表示を設置します。
<div class="ui-container">
<button id="soundToggleBtn">🔊 サイコロ音 ON</button>
<button id="throwDiceBtn">🎲 サイコロを投げる</button>
<div id="diceCount" class="dice-count">投げた回数: 0</div>
</div>
✅解説
soundToggleBtnで効果音ON/OFF切替throwDiceBtnでサイコロを投げるアクションdiceCountで累計投げ回数を表示
💡ポイント
- CSSで光沢やアニメーションを付けるとUIが映える
- モバイルでも押しやすいサイズに調整
次にJavaScriptで効果音のON/OFF切替を実装します。
let isSoundOn = true;
const soundToggleBtn = document.getElementById('soundToggleBtn');
soundToggleBtn.onclick = () => {
isSoundOn = !isSoundOn;
soundToggleBtn.textContent = isSoundOn ? '🔊 サイコロ音 ON' : '🔇 サイコロ音 OFF';
};
💡ポイント
-
ボタン状態と変数を同期することで、サウンドのON/OFF判定が簡単
さらにサイコロ音は複数のフリー素材を用意し、ランダム再生します。
const diceSounds = [
'mp3/glass01.mp3',
'mp3/glass02.mp3',
'mp3/pottery01.mp3',
'mp3/pottery02.mp3'
];
function playRandomDiceSound() {
if (!isSoundOn) return;
const src = diceSounds[Math.floor(Math.random()*diceSounds.length)];
new Audio(src).play();
}
💡ポイント
- ランダム再生するだけで転がる「リアル感」が大幅アップ
- 複数サイコロ同時再生も可能
Three.jsでシーンを作る
続いて3Dのシーン作り。
カメラやライティング、そしてサイコロを受け止める“お椀”をセットアップしています。
シーンとカメラ
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 3.5, 4.5);
camera.lookAt(0, -bowlRadius/2, 0);
真上からじゃなくて、ちょっと斜めに見下ろす感じにしてるのがポイント。
✅解説
- 背景色を暗くしてサイコロが映えるように
- カメラは真上ではなく斜め上から見下ろすアングル
💡ポイント
-
視点を少し傾けるだけで立体感が増す
光源
scene.add(new THREE.AmbientLight(0x404040, 0.6));
const light1 = new THREE.PointLight(0xFFFFFF, 1.0, 100);
light1.position.set(2,6,6);
light1.castShadow = true;
scene.add(light1);
✅解説
AmbientLightで全体の明るさを調整PointLightで立体感のある影を付与
💡ポイント
-
影を有効にするとサイコロやお椀の立体感が大幅にアップ
お椀(サイコロ受け)
const visualGeometry = new THREE.SphereGeometry(
bowlRadius, 64, 64, 0, Math.PI*2, Math.PI/2, Math.PI/2
);
const visualMaterial = new THREE.MeshStandardMaterial({
color: 0xD2B48C,
roughness: 0.4,
metalness: 0.2,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.95
});
const bowlMesh = new THREE.Mesh(visualGeometry, visualMaterial);
bowlMesh.position.y = -bowlRadius/2;
scene.add(bowlMesh);
✅解説
- 半球形状でサイコロを受け止める
- 半透明にして光の反射を表現
💡ポイント
-
見た目だけでなく、物理演算にも対応させる
Ammo.jsでサイコロの物理演算を実装
サイコロをリアルに転がすには、見た目だけでなく物理演算が必要です。Ammo.jsを使うことで、重力・衝突判定・慣性などを考慮した自然な動きを再現できます。
物理ワールドの構築
let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
let dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
let broadphase = new Ammo.btDbvtBroadphase();
let solver = new Ammo.btSequentialImpulseConstraintSolver();
let physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher,broadphase,solver,collisionConfiguration);
physicsWorld.setGravity(new Ammo.btVector3(0, -9.82, 0));
✅解説
DiscreteDynamicsWorldで剛体シミュレーション- 重力は地球並みの -9.82 m/s²
お椀の物理登録
const ammoMesh = new Ammo.btTriangleMesh();
// three.jsジオメトリの頂点をAmmoに変換してaddTriangle…
const bowlShape = new Ammo.btBvhTriangleMeshShape(ammoMesh, true, true);
const bowlBodyInfo = new Ammo.btRigidBodyConstructionInfo(0, bowlMotionState, bowlShape, new Ammo.btVector3(0,0,0));
const bowlBody = new Ammo.btRigidBody(bowlBodyInfo);
physicsWorld.addRigidBody(bowlBody);
✅解説
- 半球を「静的な三角形メッシュ」として登録
- サイコロがぶつかってもお椀自体は動かない
💡ポイント
- 静的オブジェクトは質量0で登録する
- 衝突判定だけで動かないオブジェクトを作れる
サイコロ(動的オブジェクト)の作成
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(0, 10, 0));
transform.setRotation(new Ammo.btQuaternion(0, 0, 0, 1));
const motionState = new Ammo.btDefaultMotionState(transform);
const colShape = new Ammo.btBoxShape(new Ammo.btVector3(0.5, 0.5, 0.5));
const localInertia = new Ammo.btVector3(0, 0, 0);
colShape.calculateLocalInertia(1, localInertia);
const rbInfo = new Ammo.btRigidBodyConstructionInfo(1, motionState, colShape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);
physicsWorld.addRigidBody(body);
✅解説
btBoxShapeで立方体の衝突判定を作成setOriginで初期位置を指定calculateLocalInertiaで質量に応じた慣性を計算btRigidBodyで物理オブジェクト化
💡ポイント
- 初期位置や回転をランダムに設定すると自然な転がりに
- モバイルでは力の値を調整して安定させる
サイコロの生成
Three.js側
サイコロは立方体として作成し、各面にマテリアルを設定します。
const diceGeometry = new THREE.BoxGeometry(1, 1, 1);
const diceMaterials = [
new THREE.MeshStandardMaterial({ color: 0xffffff }),
new THREE.MeshStandardMaterial({ color: 0xffffff }),
new THREE.MeshStandardMaterial({ color: 0xffffff }),
new THREE.MeshStandardMaterial({ color: 0xffffff }),
new THREE.MeshStandardMaterial({ color: 0xffffff }),
new THREE.MeshStandardMaterial({ color: 0xffffff })
];
const diceMesh = new THREE.Mesh(diceGeometry, diceMaterials);
scene.add(diceMesh);
✅解説
BoxGeometry(1,1,1)で1x1x1の立方体を作成MeshStandardMaterialは光源に反応するマテリアルMeshでジオメトリとマテリアルを結合し、シーンに追加
💡ポイント
- サイコロのサイズとAmmo.jsの衝突形状を合わせること
- マテリアルにテクスチャを貼るとよりリアルな演出が可能
const geometry = new THREE.BoxGeometry(size,size,size);
const material = new THREE.MeshStandardMaterial({color:0xFFFFFF});
const mesh = new THREE.Mesh(geometry, material);
// ドットを追加
const dotGeometry = new THREE.CircleGeometry(dotRadius, 32);
const dotMesh = new THREE.Mesh(dotGeometry, dotMaterial);
dotMesh.position.copy(normal.clone().multiplyScalar(size/2 + 0.001));
mesh.add(dotMesh);
✅解説
- サイコロの目は黒・赤で表現
- 1の面だけ赤にして「雰囲気アップ」
Ammo.js側
const diceShape = new Ammo.btBoxShape(new Ammo.btVector3(size/2,size/2,size/2));
const diceMass = 0.5;
const diceBodyInfo = new Ammo.btRigidBodyConstructionInfo(diceMass, diceMotionState, diceShape, diceLocalInertia);
const diceBody = new Ammo.btRigidBody(diceBodyInfo);
diceBody.setFriction(0.15);
diceBody.setRestitution(0.3);
physicsWorld.addRigidBody(diceBody);
✅解説
- 質量・摩擦・反発係数を設定
- サイコロが自然に転がる
💡ポイント
-
摩擦と反発のバランスで挙動が大きく変わる
サイコロを投げる動作
function throwDice() {
playRandomDiceSound();
createDice(); // 3つ生成
diceAmmoObj.setLinearVelocity(new Ammo.btVector3(Math.cos(angle)*throwSpeed, yVel, Math.sin(angle)*throwSpeed));
diceAmmoObj.setAngularVelocity(new Ammo.btVector3((Math.random()-0.5)*angVel, …));
}
✅解説
- ランダムな速度・回転で投げる
- スマホとPCで速度を調整可能
💡ポイント
-
投げ方次第で転がり方が変わるので、ユーザー体験に直結
出目判定と役演出
出目が揃ったら「ゾロ目」や「ピンゾロ」などの役を判定して、画面中央にド派手なバナーを出しています。
CSSでめちゃくちゃ頑張ってアニメーションを作りこみました。
function checkRole(diceValues) {
const counts = {};
diceValues.forEach(v => counts[v] = (counts[v] || 0) + 1);
if (Object.values(counts).includes(5)) return '五つ子';
if (Object.values(counts).includes(4)) return '四つ子';
if (Object.values(counts).includes(3) && Object.values(counts).includes(2)) return 'フルハウス';
if (Object.values(counts).includes(3)) return '三つ子';
if (Object.values(counts).filter(v=>v===2).length===2) return 'ツーペア';
if (Object.values(counts).includes(2)) return 'ワンペア';
return 'ノーペア';
}
バナー演出(CSS)
特にピンゾロ専用の「伝説級」演出を入れたら、めちゃくちゃ盛り上がるようになりました。
画面シェイク、光のレイズ、コンフェッティまで出るので、ぜひ投げて確認してみてください。
.role-banner.show {
animation: yakuPop 1500ms cubic-bezier(0.2,0.7,0.2,1.2) forwards,
glowPulse 3000ms ease-in-out infinite,
floatEffect 4000ms ease-in-out infinite;
}
.role-banner.legendary {
--glow-color: #ffd54f;
background: linear-gradient(180deg, #fff7e6 0%, #ff9800 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: … legendAura 3200ms ease-in-out infinite;
}
✅解説
- ピンゾロなどのレア役は「伝説級演出」で盛り上げる
- 画面シェイク、光のレイズ、コンフェッティも連動
💡ポイント
- CSSアニメーションで華やかさを演出
- バナーとサウンド・カメラ演出を同期すると臨場感が増す
まとめ
- Three.jsで3D描画、Ammo.jsで物理演算
- Three.jsのジオメトリとAmmo.jsの物理形状を合わせるのが一番難しい
- サイコロ生成・投げる動作・サウンドのランダム化でよりリアルな挙動に
- サイコロの質感や光源の調整で見た目のクオリティが上がる
- UIと効果音で操作感・リアル感をアップ
- 演出部分はCSSアニメーションだけでかなり派手にできる