Play 3D Chinchirorin in Your Browser – A Dice Game Built with Three.js & Ammo.js
I wanted to build a realistic Chinchiro in the web browser—so here it is!
This post walks through how I built a 3D Chinchiro dice app using Three.js × Ammo.js.
In this article, I’ll thoroughly explain—with a sample code—the following topics:
- How to model dice with Three.js (3D rendering)
- How to make them physically roll using Ammo.js (physics simulation)
- Logic to read the upward faces and determine the hand (e.g., triples, one pair)
- Adding banner animations, sound effects, and UI
UI and Sound Effects Implementation
First, let’s place the UI buttons and the dice throw counter at the top-left corner of the screen.
<div class="ui-container">
<button id="soundToggleBtn">🔊 サイコロ音 ON</button>
<button id="throwDiceBtn">🎲 サイコロを投げる</button>
<div id="diceCount" class="dice-count">投げた回数: 0</div>
</div>
✅ Explanation
soundToggleBtn→ Toggles sound ON/OFFthrowDiceBtn→ Action to throw dicediceCount→ Displays total number of throws
💡 Point
- Add gloss and animations with CSS to make the UI stand out
- Adjust button sizes so they’re easy to tap on mobile devices
Next, let’s implement sound ON/OFF toggling with JavaScript.
let isSoundOn = true;
const soundToggleBtn = document.getElementById('soundToggleBtn');
soundToggleBtn.onclick = () => {
isSoundOn = !isSoundOn;
soundToggleBtn.textContent = isSoundOn ? '🔊 Dice Sound ON' : '🔇 Dice Sound OFF';
};
💡 Point
By syncing the button state with the variable, it’s simple to determine sound ON/OFF.
Now, prepare multiple free dice sound effects and play one randomly.
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();
}
💡 Point
- Random playback adds a big boost of realism 🎲
- Multiple dice sounds can even be played simultaneously
Creating the Scene with Three.js
Now for the 3D scene setup: camera, lighting, and the “bowl” to catch the dice.
Scene and Camera
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);
Instead of looking directly from above, we set the camera at a slightly tilted angle.
✅ Explanation
- Dark background so dice stand out
- Camera positioned diagonally, not directly above
💡 Point
A slightly angled viewpoint makes the scene feel much more three-dimensional.
Lighting
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);
✅ Explanation
AmbientLightadjusts overall brightnessPointLightcreates shadows for depth
💡 Point
Enabling shadows adds a huge boost of realism to dice and bowl.
Bowl (Dice Catcher)
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);
✅ Explanation
- Hemisphere shape to catch dice
- Semi-transparent to reflect lighting
💡 Point
Not just visuals—this bowl is also used for physics simulation.
Implementing Dice Physics with Ammo.js
To make dice roll realistically, we need physics simulation—gravity, collisions, inertia—using Ammo.js.
Physics World Setup
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));
✅ Explanation
DiscreteDynamicsWorldruns rigid body simulation- Gravity set to Earth’s standard: -9.82 m/s²
Bowl Physics Registration
const ammoMesh = new Ammo.btTriangleMesh();
// convert three.js geometry to Ammo vertices and 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);
✅ Explanation
- Register hemisphere as a static triangle mesh
- Bowl doesn’t move when dice collide
💡 Point
Static objects must be registered with mass = 0.
Dice (Dynamic Object)
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);
✅ Explanation
btBoxShapecreates cube collision detectionsetOriginsets initial positioncalculateLocalInertiaadjusts inertia per massbtRigidBodycreates the physical dice object
💡 Point
Randomize initial position/rotation for natural rolls.
Dice Creation
Three.js side
The dice are created as cubes, with materials assigned to each face.
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);
✅ Explanation
BoxGeometry(1,1,1)creates a cube of size 1×1×1.MeshStandardMaterialis a material that reacts to light sources.- A mesh combines geometry and material, which is then added to the scene.
💡 Point
- The dice size must match the Ammo.js collision shape.
- Adding textures to the materials makes the dice look more realistic.
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);
✅ Explanation
- Dice dots are represented in black or red.
- Only the “1” face is marked in red for added realism.
Ammo.js side
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);
✅ Explanation
- Sets mass, friction, and restitution.
- Ensures dice roll naturally.
💡 Point
-
The balance between friction and restitution greatly affects how the dice behave.
Throwing the Dice
function throwDice() {
playRandomDiceSound();
createDice(); // generate 3 dice
diceAmmoObj.setLinearVelocity(new Ammo.btVector3(Math.cos(angle)*throwSpeed, yVel, Math.sin(angle)*throwSpeed));
diceAmmoObj.setAngularVelocity(new Ammo.btVector3((Math.random()-0.5)*angVel, …));
}
✅ Explanation
- Throws the dice with random speed and rotation.
- Adjust speeds for smartphones vs. PCs.
💡 Point
-
Rolling behavior changes depending on how the dice are thrown, directly affecting the user experience.
Result Judging and Role Presentation
When the dice results match, the app checks for roles such as “three-of-a-kind” or “one pair,” and displays a flashy banner in the center of the screen.
The CSS animations were carefully crafted for dramatic presentation.
function checkRole(diceValues) {
const counts = {};
diceValues.forEach(v => counts[v] = (counts[v] || 0) + 1);
if (Object.values(counts).includes(5)) return 'Five of a kind';
if (Object.values(counts).includes(4)) return 'Four of a kind';
if (Object.values(counts).includes(3) && Object.values(counts).includes(2)) return 'Full House';
if (Object.values(counts).includes(3)) return 'Three of a kind';
if (Object.values(counts).filter(v=>v===2).length===2) return 'Two Pair';
if (Object.values(counts).includes(2)) return 'One Pair';
return 'No Pair';
}
Banner Effects (CSS)
Special effects were added for legendary roles such as “all ones,” making the game far more exciting.
The screen shakes, light rays appear, and confetti bursts—definitely worth checking out.
.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;
}
✅ Explanation
- Rare roles like “all ones” are highlighted with legendary animations.
- Screen shake, light rays, and confetti are synchronized.
💡 Point
- CSS animations bring festive effects.
- Syncing banners with sound and camera effects boosts immersion.
Summary
- Use Three.js for 3D rendering and Ammo.js for physics.
- Matching Three.js geometry with Ammo.js physics shapes is the most challenging part.
- Dice creation, throwing mechanics, and randomized sounds enhance realism.
- Adjusting dice textures and lighting improves visual quality.
- UI and sound effects increase interactivity and immersion.
- CSS animations alone can create highly flashy effects.