Design Dices with Any Number of Faces in Maya

The Goal: Any-N-Faced Dice, Fair and Beautiful

dices

Dice are everywhere in games and probability, but traditional geometry only allows a handful of Platonic solids — regular polyhedra like the cube (d6), dodecahedron (d12), and icosahedron (d20). What if we want a d17, a d32, or even a d91? How do we make a die with any number of faces that is:

  • Visually balanced
  • Approximately fair (equal probability for each face)
  • Controllable and artistically tunable
  • Procedurally generated in Maya

This post follows my journey through geometry, mesh hacking, and custom algorithms in Maya Python to create these “impossible” dice.


What is Regular Polyhedra & the Platonic Limit

At first, I explored regular polyhedra — Platonic and Archimedean solids. These are geometrically perfect, with identical faces and angles. Unfortunately, there’s a strict limit:

  • Only 5 Platonic solids exist
  • Only a few more semi-regular solids offer symmetric face layouts
  • Only specific face counts are possible (like 4, 6, 8, 12, 20, 32)

So unless your dice are one of those lucky numbers, this approach doesn’t scale.

1
mesh = cmds.polyPlatonicSolid(r=2, st=0)[0]

Here you can choose the limited solid type available: dodecahedron, icosahedron, octahedron or tertrahedron.

solid


Method 1: Face Subdivision and Reduction

Next, I tried a workaround using a combination of Maya’s Subdivide and Reduce tool. The idea:

  1. Start with a low-poly geometry
  2. Apply polySubdivideFacet until roughly N faces available

or.

  1. Start with a high-poly geometry
  2. Apply polyReduce and tweak until roughly N faces remain

It kind of works, but control is limited. Face counts are not exact, and distribution can be messy. Also, most faces become triangles, which limits aesthetics and fairness perception.

Subdivide

1
2
3
4
face_count = cmds.polyEvaluate(mesh, face=True)
while face_count < 25:
cmds.polySubdivideFacet(mesh + ".f[*]", dv=1, ch=False)
face_count = cmds.polyEvaluate(mesh, face=True)

The problem as you can see is that extra faces are generated by dividing existing flat faces, which is not actually creating the dice faces which can be rolled on.

subdivide

Face Reduction

1
2
3
4
5
6
7
8
cmds.polyReduce(mesh,
version=1, # modern polyReduce
termination=2, # 0=ratio, 1=vertex, 2=face target
tct=face_count,
keepBorder=1,
keepHardEdge=1,
cachingReduce=0,
symmetryPlaneW=0) # no symmetry enforcement

Note that the face count here is actually the triangle count. There’s also other options such as limiting the percentage or the vertex.

reduce


Method 2: Triangulation with Scattered Points

Trying to control geometry directly, I generated N evenly scattered points on a sphere, and connected them using Delaunay-like triangulation. The result is a clean triangle mesh.

Problems:

  • The number of points ≠ number of faces
  • Every face is a triangle — no variation
  • Hard to get patch-like regions, like you’d see on a real ball

for generating points on surface, see: this section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import maya.cmds as cmds
from scipy.spatial import ConvexHull

def create_polyhedron(n_faces=32, scale=5.0):
# Step 1: Generate points
points = fibonacci_sphere(n_faces)

# Step 2: Compute convex hull
hull = ConvexHull(points)

# Step 3: Create mesh from hull
all_verts = []
for p in points:
all_verts.append([coord * scale for coord in p])

face_verts = []
for simplex in hull.simplices:
face_verts.append([all_verts[i] for i in simplex])

# Flatten vertices and create mesh
mesh_verts = []
mesh_faces = []

vert_index_map = {}
index_counter = 0

for face in face_verts:
face_indices = []
for vertex in face:
v_key = tuple(vertex)
if v_key not in vert_index_map:
vert_index_map[v_key] = index_counter
mesh_verts.append(vertex)
index_counter += 1
face_indices.append(vert_index_map[v_key])
mesh_faces.append(face_indices)

# Create Maya mesh
mesh_name = "d{}_hull".format(n_faces)
mesh = cmds.polyCreateFacet(p=mesh_verts, n=mesh_name)[0]

# Delete default faces and rebuild custom ones
cmds.delete(mesh, ch=True)
cmds.delete(mesh, constructionHistory=True)
for i in range(cmds.polyEvaluate(mesh, face=True)):
cmds.delete(f"{mesh}.f[{i}]", ch=True)
for face in mesh_faces:
face_points = [mesh_verts[i] for i in face]
cmds.polyCreateFacet(p=face_points)

return mesh

triangulate

As you can see, the dice seems fair with its face approximately the same size, but it doesn’t look nice, with only triangle faces, it just looks like an oddly-shape polygon.


Method 3: Soccer Ball Inspiration – The Slicing Trick

Eventually, inspiration struck from something unexpected: soccer balls.

Soccer balls use a combination of hexagons and pentagons — not regular polyhedra, but still visually even and fair enough. This led to a breakthrough: What if we scattered points on a sphere, and used them to slice the sphere into patches?

The Slicing Method:

  1. Scatter N points on a sphere surface
  2. For each point, slice a patch using a spherical Voronoi or convex cut
  3. The result: a mesh with N face-like patches, not limited to triangles!

This allows for any number of sides — even prime numbers like 17 or 19 — and visually balanced results that look and feel like real dice.

for generating points on surface, see: this section

1
2
3
4
5
6
7
8
9
10
11
12
13
def generate(faces, iteration, depth):
pts = generate_even_sphere_points(n_points=faces, iterations=iteration)
locators = create_locators(pts)
sphere = cmds.polySphere(r=5, name='targetSphere')[0]

for i, pos in enumerate(pts):
# 2. Create a cutter plane for each point
cutter = cmds.polyCube(w=10, h=depth, d=10, name=f'cutter_{i}')[0]
cmds.xform(cutter, translation=[pos[0], pos[1], pos[2]], worldSpace=True)

# 3. Rotate the cutter to face the center (0,0,0)
cmds.aimConstraint(sphere, cutter, aimVector=(0,-1,0), upVector=(0,1,0), worldUpType="scene")
sphere = cmds.polyCBoolOp(sphere, cutter, op=2, name='cutResult')

slice

  • You can adjust the depth of the slice: more depth creates narrower edges between dice faces
  • Iteration controls the repulsion based algorithm to generate evenly distributed splice points, note that the larger the number of faces in combination with iteration will significantly increase the generation time

Bonus: How to Scatter Points Evenly on a Sphere

Getting evenly distributed points on a sphere is a problem on its own. Two methods I tested:

Fibonacci Sphere

  • Fast and elegant
  • Uses golden angle to place points around the sphere
  • Good for approximate distributions

Drawback: Not perfectly uniform — more clustering near poles

1
2
3
4
5
6
7
8
9
10
11
def fibonacci_sphere(n_points):
points = []
golden_angle = math.pi * (3 - math.sqrt(5))
for i in range(n_points):
y = 1 - (i / float(n_points - 1)) * 2
radius = math.sqrt(1 - y * y)
theta = golden_angle * i
x = math.cos(theta) * radius
z = math.sin(theta) * radius
points.append([x, y, z])
return points

Repulsion-Based Relaxation

  • Start with random points
  • Simulate repelling forces between them
  • After several iterations, points naturally spread out evenly
  • Much fairer for small N (like 7, 17, etc.)

In practice: For 10–100 points, repulsion-based methods yield the best results visually and statistically.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import maya.cmds as cmds
import math
import random


def normalize(v):
mag = math.sqrt(sum(x**2 for x in v))
return [x / mag for x in v]

def subtract(a, b):
return [a[i] - b[i] for i in range(3)]

def add(a, b):
return [a[i] + b[i] for i in range(3)]

def scale(v, s):
return [x * s for x in v]

def distance(a, b):
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))

def repulsion_step(points, strength=0.01):
new_points = []
for i, pi in enumerate(points):
force = [0, 0, 0]
for j, pj in enumerate(points):
if i == j:
continue
diff = subtract(pi, pj)
dist = max(distance(pi, pj), 0.001)
f = scale(normalize(diff), strength / (dist ** 2))
force = add(force, f)
new_pi = normalize(add(pi, force)) # project back to sphere
new_points.append(new_pi)
return new_points

def generate_even_sphere_points(n_points=17, iterations=500, radius=5):
# Start with random points on the sphere
points = []
for _ in range(n_points):
theta = random.uniform(0, 2 * math.pi)
phi = math.acos(random.uniform(-1, 1))
x = math.sin(phi) * math.cos(theta)
y = math.sin(phi) * math.sin(theta)
z = math.cos(phi)
points.append([x, y, z])

# Relax points using repulsion
for _ in range(iterations):
points = repulsion_step(points)

# Scale to radius and return
return [scale(p, radius) for p in points]

def create_locators(points):
locators = list()
for i, p in enumerate(points):
loc=cmds.spaceLocator(p=p, name="slicePoint_%d" % i)
center = cmds.objectCenter(loc, gl = True)
cmds.xform(loc, pivots = center)
locators.append(loc)
return locators