For decades, digital dice rolls and card shuffles in video games were governed by simple Pseudo-Random Number Generators (PRNGs). A player clicked a button, the CPU generated a random float between 0 and 1, and the screen flashed a pre-rendered result. While functional, this method lacked tactile feedback. The roll didn't feel real because it wasn't physical.
Modern browser gaming has shifted this paradigm. With the performance leap in WebGL and hardware-accelerated JavaScript, we can now simulate high-fidelity 3D physics directly in the browser. In games like Neon Dice Destiny, luck is not just a variable—it is a physical simulation driven by rigid-body mechanics, friction, and angular velocity.
1. Modeling Rigid-Body Dynamics in JavaScript
To simulate physical dice, we require two distinct engines working in tandem:
- The Visual Engine (Three.js): Renders the 3D meshes, textures, lighting, and camera perspectives on an HTML5 canvas.
- The Physics Engine (Cannon.js or custom physics): Simulates the mathematical properties of the objects in a virtual coordinate system, calculating velocity, collision vectors, mass, and drag.
Every frame, the physics engine updates the coordinates of the dice. The visual engine then copies those coordinates and applies them to the rendered 3D models. The dice are modeled as bounding boxes (cubes) with defined mass (e.g., 100g), coefficient of restitution (bounciness), and friction coefficients.
2. The Equations of Motion
When the player clicks to roll, we apply an initial linear force vector and an angular impulse (torque) to the dice. Once thrown, gravity accelerates the dice downward:
F_gravity = mass * gravity_vector
v(t + dt) = v(t) + (F_net / mass) * dt
x(t + dt) = x(t) + v(t + dt) * dt
Meanwhile, the angular velocity causes the dice to rotate around their axes. Without torque, the dice would slide rather than roll. The rotation is computed using quaternions to prevent gimbal lock, ensuring smooth 3D rotations on all axes.
3. Managing Chaotic Collisions
The true magic happens when the dice strike the boundaries of the container or each other. We use narrow-phase collision detection to identify exactly when the corners of a die intersect the flat planes of the floor or walls.
Upon collision, we calculate the normal reaction force and the friction. The coefficient of restitution determines how much energy is conserved. To simulate a premium resin dice feel, we keep the restitution moderately low (around 0.3) so the dice bounce satisfyingly but settle quickly, rather than bouncing endlessly like rubber balls.
4. Solving the Sorting and Result Pipeline
One of the hardest parts of using physical simulations for gameplay is determining which face is pointing upward once the dice settle. We solve this by finding the world space normal vectors of all six faces of each die. By calculating the dot product of each face normal against the global up-vector (0, 1, 0), the face with a dot product closest to 1.0 is declared the top face.
To prevent value swapping bugs when results are displayed, we run a sorting algorithm. Once the physics engine marks the dice as "sleeping" (velocity near zero), we sort the results based on their final horizontal positions (X-coordinates) from left to right. This ensures that the HUD badges align perfectly with the physical dice on screen.
5. Performance Tuning for Mobile
Running a continuous 3D physics loop along with 3D rendering can quickly drain mobile batteries. To keep the game running at 60 FPS, we implement several optimizations:
- Physics Sleep: Once a die's linear velocity drops below 0.1 and angular velocity below 0.1 for more than 500ms, we put it to "sleep"—deactivating physics calculations for that body until the next roll.
- Simplified Colliders: Instead of calculating complex mesh-to-mesh collisions, we treat the game board walls as simple infinite planes, reducing collision math to basic linear algebra.