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アニメーションだけでかなり派手にできる
ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、ゲームの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。