Simple 3D Dice Rolling
Demo
User Guide
Initial Interface
Basic Operations
- Click any dice to start rolling
Hidden Features
- Cheat Mode
- Double-click in the lower-left corner area to activate the cheat system
- A dice control panel will appear after activation
- Click "Reset Dice" button to stack dice neatly
- Dice Control Panel
- Allows individual adjustment of each die’s value
- Provides real-time preview of modifications
Dice States
Reset State
After Throw
Core Functionality Implementation
1. Physics Engine Integration
Using @react-three/cannon
for realistic physics effects:
const [ref, api] = useBox(() => ({
mass: 1,
position,
args: [1.5, 1.5, 1.5],
linearDamping: 0.4,
angularDamping: 0.3,
material: {
friction: 0.3,
restitution: 0.6,
}
}))
const [ref, api] = useBox(() => ({
mass: 1,
position,
args: [1.5, 1.5, 1.5],
linearDamping: 0.4,
angularDamping: 0.3,
material: {
friction: 0.3,
restitution: 0.6,
}
}))
mass
: Sets the dice masslinearDamping
: Controls linear motion dampingangularDamping
: Controls rotational dampingfriction
: Friction coefficientrestitution
: Elasticity coefficient
2. State Management
Using Zustand for dice state management:
const useDiceStore = create<DiceState>((set, get) => ({
diceCount: 5,
diceValues: Array(6).fill(1),
diceRefs: new Set(),
throwAllDice: () => {
const { diceRefs } = get()
diceRefs.forEach(api => {
// Random throw force and direction
const maxVelocity = 12 + Math.random() * 6
const height = 15 + Math.random() * 8
// ...
})
}
}))
const useDiceStore = create<DiceState>((set, get) => ({
diceCount: 5,
diceValues: Array(6).fill(1),
diceRefs: new Set(),
throwAllDice: () => {
const { diceRefs } = get()
diceRefs.forEach(api => {
// Random throw force and direction
const maxVelocity = 12 + Math.random() * 6
const height = 15 + Math.random() * 8
// ...
})
}
}))
3. Dice Material Rendering
3.1 Texture Generation
Using Canvas to dynamically generate dice face textures:
export function createDiceTexture(number: number): THREE.Texture {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
// Draw background and dots
const dotPositions = getDotPositions(number)
dotPositions.forEach(([x, y]) => {
context.arc(x, y, dotRadius, 0, Math.PI * 2)
})
return new THREE.CanvasTexture(canvas)
}
export function createDiceTexture(number: number): THREE.Texture {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
// Draw background and dots
const dotPositions = getDotPositions(number)
dotPositions.forEach(([x, y]) => {
context.arc(x, y, dotRadius, 0, Math.PI * 2)
})
return new THREE.CanvasTexture(canvas)
}
3.2 Material Application
const Dice: React.FC<DiceProps> = () => {
const textures = useRef([
createDiceTexture(6),
createDiceTexture(5),
createDiceTexture(2),
createDiceTexture(1),
createDiceTexture(4),
createDiceTexture(3),
])
return (
<Box args={[1.45, 1.45, 1.45]} castShadow>
{textures.current.map((texture, index) => (
<meshPhysicalMaterial
key={index}
attach={`material-${index}`}
map={texture}
metalness={0.3}
roughness={0.4}
clearcoat={0.5}
/>
))}
</Box>
)
}
const Dice: React.FC<DiceProps> = () => {
const textures = useRef([
createDiceTexture(6),
createDiceTexture(5),
createDiceTexture(2),
createDiceTexture(1),
createDiceTexture(4),
createDiceTexture(3),
])
return (
<Box args={[1.45, 1.45, 1.45]} castShadow>
{textures.current.map((texture, index) => (
<meshPhysicalMaterial
key={index}
attach={`material-${index}`}
map={texture}
metalness={0.3}
roughness={0.4}
clearcoat={0.5}
/>
))}
</Box>
)
}
- Using Canvas to dynamically generate textures for each face
- Applying meshPhysicalMaterial for realistic material effects
- Caching textures with useRef to avoid recreation
4. Interaction Feedback
Combining sound and vibration for feedback:
onCollide: (event: any) => {
if (event.body) {
vibrate(VIBRATION_PATTERNS.DICE_HIT);
}
}
onCollide: (event: any) => {
if (event.body) {
vibrate(VIBRATION_PATTERNS.DICE_HIT);
}
}
5. Smooth Animation
Using interpolation for fluid motion:
useFrame(() => {
if (!ref.current) return
const lerpFactor = 0.1
smoothedPosition.current[0] += (currentPosition.x - smoothedPosition.current[0]) * lerpFactor
// ...
})
useFrame(() => {
if (!ref.current) return
const lerpFactor = 0.1
smoothedPosition.current[0] += (currentPosition.x - smoothedPosition.current[0]) * lerpFactor
// ...
})
Key Technical Points
Collision Detection
useEffect(() => {
const currentPosition = [0, 0, 0]
const unsubscribe = api.position.subscribe((v: [number, number, number]) => {
// Detect boundary collisions
const maxBound = 9
if (Math.abs(v[0]) > maxBound || Math.abs(v[2]) > maxBound) {
api.velocity.set(0, 0, 0)
}
})
return () => unsubscribe()
}, [api])
useEffect(() => {
const currentPosition = [0, 0, 0]
const unsubscribe = api.position.subscribe((v: [number, number, number]) => {
// Detect boundary collisions
const maxBound = 9
if (Math.abs(v[0]) > maxBound || Math.abs(v[2]) > maxBound) {
api.velocity.set(0, 0, 0)
}
})
return () => unsubscribe()
}, [api])
Motion State Monitoring
useEffect(() => {
let isMoving = false;
const unsubscribeVelocity = api.velocity.subscribe((v: [number, number, number]) => {
const speed = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
if (speed > 0.1 && !isMoving) {
isMoving = true;
playDiceRollingSound();
} else if (isMoving && speed < 0.1) {
isMoving = false;
stopDiceRollingSound();
}
});
return () => unsubscribeVelocity();
}, [api])
useEffect(() => {
let isMoving = false;
const unsubscribeVelocity = api.velocity.subscribe((v: [number, number, number]) => {
const speed = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
if (speed > 0.1 && !isMoving) {
isMoving = true;
playDiceRollingSound();
} else if (isMoving && speed < 0.1) {
isMoving = false;
stopDiceRollingSound();
}
});
return () => unsubscribeVelocity();
}, [api])