Implementing a Mid‑Autumn Festival Dice Game with Three.js and cannon‑es
This tutorial explains how to create an interactive Mid‑Autumn Festival dice‑game using Three.js for 3D rendering, cannon‑es for physics simulation, glTF/glb model loading, heightfield collision handling, and JavaScript logic to determine game results.
The article introduces the traditional Mid‑Autumn dice game and demonstrates how to recreate it as a web‑based 3D interactive experience using Three.js and the cannon‑es physics engine.
Implementation Steps
Load dice and basin 3D models.
Set up a physics engine scene.
Bind Three.js objects to cannon‑es bodies.
Implement the game rules.
Model Loading
Models are obtained from Sketchfab in glTF or glb format; glb is preferred for its smaller binary size.
Three.js Model Loader
/**
* Load a GLTF model
*/
let loadGLTF = async (url): Promise
=> {
return new Promise(resolve => {
const loader = new GLTFLoader();
loader.load(url, object => {
resolve(object as THREE.Group);
});
});
};
/**
* Initialize models
*/
let loadModel = async () => {
let basinModel = await loadGLTF("/basin2.glb");
let diceModel = await loadGLTF("/dice.glb");
// dice setup
dice = diceModel.scene.children[0];
dice.scale.set(0.1, 0.1, 0.1);
dice.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.material.metalness = 1;
child.material.emissive = child.material.color;
child.material.emissiveMap = child.material.map;
}
});
// basin setup
basin = basinModel.scene.children[0];
basin.scale.set(18, 18, 18);
basin.position.y = 0.7;
basin.position.x = 0;
scene.add(basin);
};Physics Engine Simulation
The cannon‑es library provides gravity, collision, and sleep handling.
let world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.allowSleep = true;Dice bodies are created with a Box shape:
/**
* Create a dice physics body
*/
const generateDiceBody = () => {
const size = 0.1;
const halfExtents = new CANNON.Vec3(size, size, size);
const dice = new CANNON.Body({
mass: 0.1,
material: new CANNON.Material({
friction: 0.1,
restitution: 0.7
}),
shape: new CANNON.Box(halfExtents)
});
dice.sleepSpeedLimit = 1.0;
world.addBody(dice);
return dice;
};Because a simple box cannot represent the basin, a Heightfield shape is generated from a grid of heights that form a concave bowl.
const initBasin = () => {
const numRows = 60;
const numCols = 60;
let heights = [];
for (let i = 0; i < numRows; i++) {
const row = [];
for (let j = 0; j < numCols; j++) {
const x = (j / (numCols - 1) - 0.5) * 20;
const z = (i / (numRows - 1) - 0.5) * 20;
const radius = 4.2;
const height = Math.sqrt(x * x + z * z) <= radius ? -1.7 : 0;
row.push(height);
}
heights.push(row);
}
const heightfieldShape = new CANNON.Heightfield(heights, { elementSize: 0.2 });
const heightfieldBody = new CANNON.Body({
mass: 0,
material: new CANNON.Material({ friction: 0.1, restitution: 0.7 })
});
heightfieldBody.addShape(heightfieldShape);
heightfieldBody.position.set(-5.9, 1.3, 5.9);
heightfieldBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(heightfieldBody);
};Binding Three.js Objects to Physics Bodies
let dices = [];
/** Initialize dice positions */
let initDice = () => {
for (let i = 1; i <= 6; i++) {
let cDice = generateDiceBody();
cDice.quaternion.setFromEuler(Math.PI / 2, 0, 0);
let tDice = dice.clone();
scene.add(tDice);
dices.push({ tDice, cDice });
}
};
// Sync positions each frame
dices.forEach(({ cDice, tDice }) => {
if (cDice && tDice) {
tDice.position.copy(cDice.position);
tDice.quaternion.copy(cDice.quaternion);
}
});Game Rules
Dice values are derived from their rotation angles, and the result is determined by counting occurrences of each face according to traditional rules.
let getDicePoints = () => {
let points = [];
dices.forEach(({ tDice }) => {
let xAngle = Math.round((tDice.rotation.x / Math.PI) * 180);
let zAngle = Math.round((tDice.rotation.z / Math.PI) * 180);
let point;
if (xAngle == -90) point = 1;
else if (xAngle == 90) point = 5;
else if (xAngle + zAngle == -90 || xAngle + zAngle == 270) point = 4;
else if (xAngle + zAngle == 0 || xAngle + zAngle == 360) point = 3;
else if (xAngle + zAngle == 180 || xAngle + zAngle == -180) point = 6;
else point = 2;
points.push(point);
});
return points;
};
const getName = () => {
let obj = {};
result.value.forEach(index => {
obj[index] = (obj[index] || 0) + 1;
});
if (obj[4] == 1) {
return Object.keys(obj).length === 6 ? "对堂" : "一秀";
} else if (obj[4] == 2) return "二举";
else if (obj[4] == 3) return "三红";
else if (obj[4] == 4) {
return obj[2] == 2 ? "状元插金花" : "状元";
} else if (obj[4] == 5) return "五王";
else if (obj[4] == 6) return "六捧红";
else if (Object.keys(obj).some(k => obj[k] == 4)) return "四进";
else if (obj[6] == 5) return "五子登科";
else if (obj[6] == 6) return "手捧黑";
return "再接再厉";
};Conclusion
The tutorial covers model acquisition and format differences, physics engine usage for both simple and complex shapes, and the specific rules of the Mid‑Autumn dice game, providing a solid foundation for building similar WebGL games.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.