--- title: "Building a 3D Polyhedral Dice Roller in Three.js & Cannon.js: Rigid Body Physics & Fair RPG Randomness" description: "How to build a 3D polyhedral dice simulator (D4-D20) using Three.js, Cannon.js rigid body physics, quaternions, and 3D face vector detection." tags: ["threejs", "javascript", "webgl", "gamedev"] canonical_url: "https://entscheidomat.com/ratgeber/zufallsgenerator-richtig-nutzen" target_keywords: ["würfel online", "würfel online werfen", "wuerfel generator", "d20 würfel online", "threejs dice physics"] --- # Building a 3D Polyhedral Dice Roller in Three.js & Cannon.js: Rigid Body Physics & Fair RPG Randomness Rolling physical dice is an iconic part of tabletop role-playing games (TTRPGs) like Dungeons & Dragons, Pathfinder, and board games. Whether you need a standard 6-sided cube or a 20-sided icosahedron (D20), players expect a digital dice roller to feel tactile, behave according to realistic Newtonian physics, and deliver statistically fair outcomes. For web developers building RPG tools or decision suites like a digital [Würfel Online](https://entscheidomat.com/wuerfel-online), rendering 2D numbers or pseudo-random text overlays often feels flat and unconvincing. In this article, we will build a production-ready **3D Polyhedral Dice Roller in TypeScript** using **Three.js** for WebGL rendering and **Cannon-es** for 3D rigid body physics simulation. We will cover geometry construction, initial impulse vectors, quaternion face orientation detection, and crypto-random seeding. --- ## 1. The Physics of 3D Rigid Body Dice Tossing Simulating a rolling die requires solving rigid body dynamics in a 3D space: 1. **Linear Velocity ($\vec{v}$):** Translates the die through 3D space. 2. **Angular Velocity ($\vec{\omega}$):** Rotates the die around its center of mass. 3. **Gravity ($\vec{g} = -9.81 \text{ m/s}^2$):** Accelerates the die downward toward the floor collision plane. 4. **Restitution ($e$) & Friction ($\mu$):** Models bounce elasticity and floor surface grip. ```text Angular Impulse (Torque τ) ↺ ┌─────────┐ │ 🎲 D20 │ ──► Linear Velocity (v) └────┬────┘ │ ▼ Gravity (g = -9.81 m/s²) ═════════════════════════════════════════ Floor Plane (Restitution e = 0.3) ``` --- ## 2. Setting Up Three.js & Cannon-es Physics World First, we set up a synchronized 3D rendering scene (Three.js) and physics simulation world (Cannon-es): ```typescript import * as THREE from "three"; import * as CANNON from "cannon-es"; export class PhysicsDiceScene { private scene: THREE.Scene; private camera: THREE.PerspectiveCamera; private renderer: THREE.WebGLRenderer; private world: CANNON.World; private diceMesh?: THREE.Mesh; private diceBody?: CANNON.Body; constructor(container: HTMLElement) { // 1. Initialize Three.js Scene this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x101114); this.camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 100); this.camera.position.set(0, 12, 12); this.camera.lookAt(0, 0, 0); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(container.clientWidth, container.clientHeight); this.renderer.shadowMap.enabled = true; container.appendChild(this.renderer.domElement); // 2. Lighting Setup const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); this.scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 1.2); dirLight.position.set(5, 15, 5); dirLight.castShadow = true; this.scene.add(dirLight); // 3. Initialize Cannon-es Physics World this.world = new CANNON.World(); this.world.gravity.set(0, -19.6, 0); // 2x Earth gravity for punchy dice rolls // Floor Contact Material const floorMaterial = new CANNON.Material("floor"); const diceMaterial = new CANNON.Material("dice"); const contactMaterial = new CANNON.ContactMaterial(floorMaterial, diceMaterial, { friction: 0.4, restitution: 0.3 // Bounciness }); this.world.addContactMaterial(contactMaterial); // Add Floor Rigid Body const floorBody = new CANNON.Body({ type: CANNON.Body.STATIC, shape: new CANNON.Plane(), material: floorMaterial }); floorBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // Rotate horizontal this.world.addBody(floorBody); } } ``` --- ## 3. Creating a Polyhedral D6 Mesh & Physics Body Next, we create a standard 6-sided cube die (D6) with rounded edges and mapped UV texture coordinates. ```typescript export function createD6Die(scene: THREE.Scene, world: CANNON.World): { mesh: THREE.Mesh; body: CANNON.Body } { const size = 1.5; const halfSize = size / 2; // 1. Three.js Box Geometry const geometry = new THREE.BoxGeometry(size, size, size); const material = new THREE.MeshStandardMaterial({ color: 0x3b5bdb, roughness: 0.2, metalness: 0.1 }); const mesh = new THREE.Mesh(geometry, material); mesh.castShadow = true; scene.add(mesh); // 2. Cannon.js Physics Box Shape const shape = new CANNON.Box(new CANNON.Vec3(halfSize, halfSize, halfSize)); const body = new CANNON.Body({ mass: 1.0, // 1 kg shape: shape, position: new CANNON.Vec3(0, 5, 0) }); world.addBody(body); return { mesh, body }; } ``` --- ## 4. Crypto-Random Impulse Injection & Rolling Mechanics To start a toss, we apply a randomized upward vector velocity and a strong angular torque vector generated using `crypto.getRandomValues()` to eliminate predictable trajectory patterns. ```typescript export function rollDice(body: CANNON.Body): void { // Reset Position to top body.position.set(0, 5, 0); body.velocity.set(0, 0, 0); body.angularVelocity.set(0, 0, 0); // Generate Cryptographic Random Velocity & Torque const buffer = new Uint32Array(4); crypto.getRandomValues(buffer); // Random Linear Impulse (X and Z spread, Y upward toss) const impulseX = ((buffer[0] / 0xFFFFFFFF) - 0.5) * 8; const impulseY = 4 + (buffer[1] / 0xFFFFFFFF) * 4; const impulseZ = ((buffer[2] / 0xFFFFFFFF) - 0.5) * 8; body.velocity.set(impulseX, impulseY, impulseZ); // Random Angular Spin (Torque) const spinX = ((buffer[3] / 0xFFFFFFFF) - 0.5) * 40; const spinY = ((buffer[0] / 0xFFFFFFFF) - 0.5) * 40; const spinZ = ((buffer[1] / 0xFFFFFFFF) - 0.5) * 40; body.angularVelocity.set(spinX, spinY, spinZ); } ``` --- ## 5. Detecting the Top Face Using Quaternion Vector Transformation Once the die comes to rest on the floor plane (linear and angular velocity drop near zero), how do we mathematically identify which face is pointing strictly upward toward the sky (+Y axis)? Each of the 6 faces of a cube has a local normal vector in local space: * Face 1 (`+Z`): `(0, 0, 1)` * Face 6 (`-Z`): `(0, 0, -1)` * Face 2 (`+X`): `(1, 0, 0)` * Face 5 (`-X`): `(-1, 0, 0)` * Face 3 (`+Y`): `(0, 1, 0)` * Face 4 (`-Y`): `(0, -1, 0)` We transform each local normal vector into world space using the die's final **Quaternion Rotation Matrix** and calculate the dot product with the world Up vector `(0, 1, 0)`. The face whose world vector has the **highest dot product (closest to +1.0)** is the winning top face! ```typescript export interface FaceNormal { value: number; localVector: THREE.Vector3; } const D6_FACES: FaceNormal[] = [ { value: 1, localVector: new THREE.Vector3(0, 0, 1) }, { value: 6, localVector: new THREE.Vector3(0, 0, -1) }, { value: 2, localVector: new THREE.Vector3(1, 0, 0) }, { value: 5, localVector: new THREE.Vector3(-1, 0, 0) }, { value: 3, localVector: new THREE.Vector3(0, 1, 0) }, { value: 4, localVector: new THREE.Vector3(0, -1, 0) } ]; /** * Calculates the top face value of a landed die using Quaternion vector alignment. */ export function getLandedFaceValue(mesh: THREE.Mesh): number { const worldUp = new THREE.Vector3(0, 1, 0); let maxDot = -Infinity; let winningValue = 1; D6_FACES.forEach(face => { // Clone local vector and transform by Mesh Quaternion orientation const worldVector = face.localVector.clone().applyQuaternion(mesh.quaternion); // Calculate dot product with World Up (0, 1, 0) const dot = worldVector.dot(worldUp); if (dot > maxDot) { maxDot = dot; winningValue = face.value; } }); return winningValue; } ``` --- ## 6. The 60 FPS Render Loop Finally, we sync Cannon.js physics steps with Three.js rendering frames using `requestAnimationFrame`: ```typescript export function startAnimationLoop( scene: THREE.Scene, camera: THREE.Camera, renderer: THREE.WebGLRenderer, world: CANNON.World, mesh: THREE.Mesh, body: CANNON.Body, onSettle?: (value: number) => void ): void { const timeStep = 1 / 60; // 60 FPS let isSettledReported = false; function animate() { requestAnimationFrame(animate); // 1. Step Physics World world.step(timeStep); // 2. Synchronize Three.js Mesh with Cannon.js Body mesh.position.copy(body.position as any); mesh.quaternion.copy(body.quaternion as any); // 3. Check for Rest State (Velocity near zero) const isStationary = body.velocity.lengthSquared() < 0.001 && body.angularVelocity.lengthSquared() < 0.001; if (isStationary && !isSettledReported && body.position.y < 1.0) { isSettledReported = true; const result = getLandedFaceValue(mesh); if (onSettle) onSettle(result); } // 4. Render 3D Scene renderer.render(scene, camera); } animate(); } ``` --- ## Summary & Performance Best Practices | Parameter | 2D CSS Spinner / Text | 3D WebGL (Three.js + Cannon.js) | | :--- | :--- | :--- | | **Tactile Realism** | Low | **High (True Newtonian Gravity & Collisions)** | | **Polyhedral Support** | D6 only | **D4, D6, D8, D10, D12, D20, D100** | | **Face Determination** | Hardcoded | **Quaternion World Vector Dot Product** | | **Framerate** | Varies | **Locked 60 FPS on WebGL GPU** | Test a live 3D dice generator online at [Entscheidomat Würfel Online](https://entscheidomat.com/wuerfel-online). --- ## FAQ (Schema Structured Data) ```json { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "How do you calculate which face of a 3D die landed facing up?", "acceptedAnswer": { "@type": "Answer", "text": "By transforming the local normal vectors of each die face by the 3D mesh's final quaternion rotation matrix and taking the dot product with the world Up vector (0, 1, 0). The face with the highest dot product is the landed value." } }, { "@type": "Question", "name": "Is 3D WebGL physics fair for online dice rolling?", "acceptedAnswer": { "@type": "Answer", "text": "Yes, provided the initial linear velocity, angular spin torque, and initial spawn orientation vectors are seeded using Web Crypto API (crypto.getRandomValues)." } } ] } ```