dice, orientations and quaternions

Generating random rolls of 6-sided dice is typically an easy task. Under iOS and Swift (4.1+), you can easily generate a random integer from 1 to 6 with the following Swift command:

let roll = Int.random(1...6)  // generate a random integer from 1 to 6

Placing that command in a loop over the number of dice, you can ‘simulate’ a roll. Unfortunately it’s not very engaging or exciting. Instead, iOS proves a full 3D physics engine in SceneKit. Here, instead of generating the results of the 6-sided dice roll, you can simulate the physics of rolling the dice. Given the power of mobile platforms, this results in almost photo realistic simulations of rolling dice. It is much more engaging and interesting to watch. However, you now have a problem of determining the results of the roll. That is, you need to find the results of the roll since it is now the physics that determine the result, not just a random number you generate.

One way to determine the result of a dice roll is to use a simple machine learning (ML) algorithm to find the faces of the dice and then have it report the value of the ‘up face’. iOS has a built-in visual ML framework that is easy and powerful. However, since the result of the roll is based on physics, you could have a problem if one dice lands on top of another dice covering up the face. You could force this not to happen, but then you’re adding artificial physics to the dice roll. Adding artificial physics is almost never a good idea.

A deterministic way to resolve the dice roll is to compare the the final dice orientation to its reference orientation. This is just one spatial rotation.

Let’s first talk about 6-sided dice.

For valid 6-sided dice, you probably know that if you add the opposite faces of the dice you always get a value of seven. However, there are also ‘left-handed’ and ‘right-handed’ dice. Standard dice are ‘right-handed’. If you place the dice at the origin of a 3D coordinate system, then a ‘right-handed’ dice would have the ‘1 face’ perpendicular (or normal) to the positive x-axis, ‘2 face’ is normal to the positive y-axis and ‘3 face’ is normal to the positive z-axis. The other faces are also determined (i.e. ‘4-face’ normal along the negative z-axis, ‘5-face’ normal along negative y-axis and ‘6-face’ normal along the negative x-axis). Such a dice could be described by the notation (1x, 2y, 3z). Of course any cyclic permutation is still a valid ‘right-handed’ dice. For example (1y, 2z, 3x) and (1z, 2x, 3y). ‘Left-handed’ dice are the other permutations (e.g. (1y, 2x, 3z)). We’ll stick with ‘right-handed’ dice.

Physicists are usually taught ‘rotation matrices‘ to rotate objects using Euler angles. Of course there are various ‘conventions’ for Euler angles. The rotational matrices are 3×3 matrices that rotate a 3D object around a principle axis. In 3D there are three principle axis and so you have 3 3×3 matrices or 27 quantities (symmetry of the group reduces this value but you still need to store them). However, the important problem with ‘rotation matrices’ is ‘gimbal lock‘.

An equivalent way of doing 3D rotations is to use unit quaternions. Using unit quaternions reduces the number of quantities required to describe the rotations to 4 and does not suffer from the ‘gimbal lock’ problem. They have been used in computer graphics extensively for years because they are fast and not prone to ‘gimbal lock’.

Quaternions have a long history. Originally described by William Hamilton as a generalization of complex numbers (turns out Carl Gauss found them 24 years earlier but didn’t publish). James Maxwell used them to write down his ‘Maxwell’s equations’. His equations (20 variables and 20 equations) describe all of classical electromagnetism. Of course, Maxwell’s equations also contained the seeds for Einstein’s Special Relativity and Quantum Electrodynamics (QED). Only later, did Oliver Heaviside recast Maxwell’s Equations into the vector form that is widely known today.

Regardless, given a unit quaternion q, if you apply it to a 3D vector, you rotate that vector in 3D. SceneKit can give you the q that describes how to rotate a dice from its reference orientation to its final orientation:

let q = die.presentation.simdOrientation // an unit quaternion from reference to final orientation

where die is a SCNNode representing a 6-sided dice. It is important to use the presentation.simdOrientation of the SCNNode since this is the final dice orientation shown on the device. Now that we have quaternion that describes how to rotate the reference dice orientation to its final orientation, we can rotate normal unit vectors with the same quaternion to find where each face goes. We are then simply looking for the rotated unit vector that points ‘up’. That would be the result of the dice roll.

If we take ‘up’ to be along the positive y-axis (default in SceneKit) and we use a ‘right-handed’ dice corresponding to (1z, 2x, 3y), then given a die, we can determine the result of the roll:

// initial orientation of die:
//   face 1 outward normal along +z axis (therefore face 6 outward normal along -z axis)
//   face 2 outward normal along +x axis
//   face 3 outward normal along +y axis

let up = simd_float3(0, 1, 0) // the +y axis is the 'up' direction (i.e. result of the die)

let q = die.presentation.simdOrientation  // quaternion from reference to final orientation

var value = 6 // default to 6- shouldn't matter
for i in 1...3 {  // loop over three unit vectors
       var n = simd_float3(0, 0, 0)
       n[(i+1) % 3] = 1  // unit vector to rotate
       let a = simd_dot(up, q.act(n))  // rotate with q and project onto 'up' (both are normalized so magnitude = cos of angle between them)
       if a > 0.7071 { value = i; break}  // is face up?
       if a < -0.7071 { value = 7 - i; break} // is face down?
}
print("\(die.name) is \(value)")

This simply takes three (normal) unit vectors (first z-axis, then x-axis and finally y-axis) and looks to see if it’s pointing in the ‘up’ or ‘down’ direction after rotation. If it’s ‘up’, then we know the face value. If it is ‘down’, we know that value too (opposite faces sum to 7). If the unit vector is not pointing ‘up’ or ‘down’, then that face (and it’s opposite) are not the result.

The value of “0.7071” might be confusing. For a dice resting on the xz-plane, the ‘up’ unit vector after the rotation would be +1 (or -1 if it is the ‘down’ unit vector). However, the dice could be sitting on other dice and so it is not resting on the xz-plane. The absolute worst case (which, fortunately is excluded by physics) would be a dice ‘resting’ on an edge or a corner. In this case, there are two or three normal vectors all pointing 45 degrees from the ‘up’ direction. Therefore, we find the first rotated unit normal vector that has an angle less than 45 degrees from the true ‘up’ direction to decide the final result (i.e. angle between ‘up’ and rotated unit vector < 45 degrees means cos(angle) > cos(45) = 1/sqrt(2) ~ 0.7071).

Look for our (free) dice game soon in the App Store! We are mesmerized by it…

Add a Comment

Your email address will not be published. Required fields are marked *