---
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)."
      }
    }
  ]
}
```
