Tech Art Adventures - Rubik Cube in Maya using MASH
How I glued together a Rubik’s Cube in PyMel
Introduction
I love puzzles. They serve as effective mental exercise, enhancing focus and problem-solving skills. By requiring sustained attention and cognitive effort, they help improve mental agility. They’re a straightforward way to engage your mind, making them more than just a simple pastime.
As an admirer of cubers, I decided to create my own digital Rubik’s Cube in Maya using Pymel. The objective is to have a tool that can generate n sized Rubik and provide handles to interact and solve it.
Creating the Rubik’s Cube
First, let’s start by creating a Python class called Builder
that will hold all the necessary methods and properties to construct our Rubik’s Cube in Maya. The Builder
class will contain a constructor and various methods for building, coloring, and controlling the Rubik’s Cube.
Building the Cube
The build
method will be responsible for generating the initial grid of pieces for the Rubik’s Cube. It will create a MASH network in Maya, which allows for the procedural generation of objects like our Rubik’s Cube. The method sets the arrangement, grid size, and margin distance between instances, and then bakes the instances to geometry.
def build(self, _size):
"""
Drives the order of Rubik's Cube
construction.
"""
self.size = _size
radiusDiv = 1.25
self.controlRadius = self.size / radiusDiv
## Create MASH network to generate initial grid of pieces
instancer = 'instancer'
cube = cmds.polyCube(w = 1, h = 1, name = f'{self.name}')
mashNetwork = mapi.Network()
mashNetwork.createNetwork(name = self.name, geometry = instancer)
# Use primitive cube as distribution source
cmds.connectAttr(
str(cube[0]) + '.outMesh',
mashNetwork.distribute + '.inputMesh')
# Arrange grid based on Rubik's Cube size
cmds.setAttr(mashNetwork.distribute + '.arrangement', 6)
cmds.setAttr(mashNetwork.distribute + '.gridx', _size)
cmds.setAttr(mashNetwork.distribute + '.gridy', _size)
cmds.setAttr(mashNetwork.distribute + '.gridz', _size)
# Set margin distance between instances and center overall distribution
amplitude = _size - 1
cmds.setAttr(mashNetwork.distribute + '.gridAmplitudeX', amplitude)
cmds.setAttr(mashNetwork.distribute + '.gridAmplitudeY', amplitude)
cmds.setAttr(mashNetwork.distribute + '.gridAmplitudeZ', amplitude)
cmds.setAttr(mashNetwork.distribute + '.centerLinearDistribution', 1)
# Bake MASH instances to Geometry
mash = cmds.ls(type = instancer)
cmds.select(mash)
cmds.BakeInstancerToGeometry()
mbake.MASHbakeInstancer(False)
cmds.CloseFrontWindow()
# Cleanup
cmds.delete(mash)
cmds.delete(cube)
# Create a list of node references
index = 1
query = f'{self.name}*'
nodes = cmds.ls(query, dag=True, transforms=True)
for node in nodes:
if (not self.rootParent and self.isParent(node)):
newName = f'{self.name}_parent'
cmds.rename(node, newName)
self.rootParent = newName
continue
newName = f'{self.name}_{index}'
cmds.rename(node, newName)
self.nodes.append(newName)
index += 1
# Visuals
# Add edge smoothness on every piece
self.bevelAll()
# Color accoridng to face
self.color(Faces.FRONT, [0,1,0])
self.color(Faces.BACK, [0,0,1])
self.color(Faces.UP, [1,1,1])
self.color(Faces.DOWN, [1,1,0])
self.color(Faces.LEFT, [1,0.5,0.1])
self.color(Faces.RIGHT, [1,0,0])
# Center World Rotate Pivot to allow coordinated rotation
for node in self.nodes:
cmds.xform(node, worldSpace=True, rotatePivot=[0,0,0])
#Check if reference display layer already exists
layerName = 'rubik_reference_layer'
dispLayers = cmds.ls(type = 'displayLayer')
layerExists = False
if dispLayers:
for layer in dispLayers:
if layer == layerName:
layerExists = True
break
if not layerExists:
layer = cmds.createDisplayLayer(name = layerName)
cmds.setAttr("{}.displayType".format(layer), 2)
# Add nodes the reference layer to prevent user interaction
cmds.editDisplayLayerMembers(layerName, self.nodes, noRecurse=True)
# Add Controls
maxLenght = 0.5 + (self.size - 2) * 0.5
step = -maxLenght
for i in range(0, self.size):
self.createControls([step,0,0], 0)
self.createControls([0,step,0], 1)
self.createControls([0,0,step], 2)
step += 1
cmds.select(clear=True)
# Begin tracking input selection
self.initSelectionJob()
Beveling the Cube Pieces
To add a smooth edge to each piece, we will use the bevelAll
method. This method applies a simple bevel on all the pieces in the Rubik’s Cube, with customizable segment and offset values.
def bevelAll(self):
"""
Apply simple bevel on all pieces.
[Segment] and [offset] values are exposed
for tweaking
"""
for node in self.nodes:
cmds.select(node)
cmds.polyBevel3(
segments=self.bevelSeg,
offset=self.bevelOffset,
chamfer=True,
depth=1,
smoothingAngle=0)
Coloring the Cube
The Rubik’s Cube is known for its colorful faces, and our virtual cube is no exception. The color
method applies vertex-based paint to each face, with a specified color for each of the six faces (Front, Back, Up, Down, Left, and Right).
def color(self, _face, color):
"""
Vertex based paint.
[notUndoable] flag improves performance for
large numbers of object. This will make
the command not undoable regardless of
whether undo has been enabled or not.
"""
faces = self.selectFacesByOrientation(_face)
vertices = self.selectVerticesFromFace(faces)
cmds.select(clear=True)
for vertex in vertices:
cmds.polyColorPerVertex(
vertex,
rgb=(
color[0],
color[1],
color[2]),
notUndoable=True,
colorDisplayOption=True,
clamped=True)
for node in self.nodes:
cmds.setAttr(f'{node}.displayColors', 1)
cmds.refresh()
Interacting with the Cube
Our Rubik’s Cube should not only be visually appealing but also interactive. To achieve this, we’ll create control handles that allow the user to select and rotate the Rubik’s Cube pieces.
Creating Control Handles
The createControls
method generates circle controls and aligns them with the corresponding faces of the Rubik’s Cube. It also tags and renames the control handles, appending them to a list of controls for future reference.
def createControls(self, position, axis):
"""
Create circle controls and align
with the coresponding faces
"""
vector = [0,0,0]
vector[axis] = 1
control = cmds.circle(
object=True,
normal=vector,
center=position,
radius=self.controlRadius)
cmds.parent(control[0], self.rootParent)
cmds.xform(control[0], worldSpace=True, centerPivots=True)
rotation = [0,0,0]
tag = f'Control_{len(self.controls)}'
cmds.rename(control[0],tag)
self.controls.append([tag,position,rotation,axis])
Selecting and Rotating Control Handles
The selectControlHandle
and validateControlHandle
methods enable users to select and rotate the control handles in the scene, while ensuring that the rotation matches a 90-degree snap in both directions.
def initSelectionJob(self):
"""
Initialises tracking job for when a control handle is selected
in the scene.
! As of now, user is required to deselect gizmos on empty space
before selecting next one, otherwise the selection is not recognized
by Maya.
"""
if cmds.scriptJob(exists=self.selectionJob):
cmds.scriptJob(kill=self.selectionJob, force=True)
job = cmds.scriptJob(
killWithScene=True,
ct=("SomethingSelected",
self.selectControlHandle),
protected=True)
self.selectionJob = job
def selectControlHandle(self):
# Prevent interaction when pieces aren't set.
if not self.interactable: return
# Confirm that only single control handle is selected,
# otherwise clear selection
selectedControl = cmds.ls('Control*', selection=True, flatten=True)
if len(selectedControl) <= 0: return
if len(selectedControl) > 1:
cmds.select(all=True, clear=True)
return
if selectedControl[0] == self.activeControl[0]:
return
self.activeControl[0] = selectedControl[0]
matchedNodes = []
# Find selected control and collect matching pieces
for control in self.controls:
if control[0] == self.activeControl[0]:
self.activeControl[1] = control[3]
matchedNodes = self.matchNodes(control[1], control[3])
break
# Unparent old selection
if self.previousControl:
oldSelection = cmds.listRelatives(
self.previousControl,
children = True,
type='transform')
for match in oldSelection:
cmds.parent(match, self.rootParent)
# Parent new selection
for match in matchedNodes:
cmds.parent(match, selectedControl[0], s=True)
cmds.reorder(match, front = True )
# Highlight control and show context handle
cmds.select(selectedControl[0], noExpand=True)
cmds.setToolTo('RotateSuperContext')
self.previousControl = self.activeControl[0]
# Begin validating Rubik's Cube position
self.initControlValidation()
def validateControlHandle(self):
"""
Checks if rotation matches 90 degree snap
in both directions. If not, hides all
control handles except current.
Limits global interaction.
"""
rotAttribute = f'{self.activeControl[0]}.r{self.axis[self.activeControl[1]]}'
angle = cmds.getAttr(rotAttribute)
if angle == 0 or abs(angle) % 90 == 0:
self.interactable = True
print('Selected Control is Valid and Set')
else:
self.interactable = False
print ('Selected Control is Invalid ' + str(angle))
if not self.interactable and self.visibleControls:
self.visibleControls = False
for control in self.controls:
if control[0] == self.activeControl[0]: continue
if cmds.getAttr(f'{control[0]}.visibility') == 1:
cmds.hide(control[0])
elif self.interactable:
self.visibleControls = True
for control in self.controls:
cmds.showHidden(control[0])
Saving and Loading Cube State
Finally, to enhance the usability of our Rubik’s Cube, we’ll provide the functionality to save and load its state. The getState
method saves the relevant variables, including the size, controls, and transforms, into a JSON file.
def getState(self):
"""
Saves relevant variables
into a .json file
"""
transforms = {
'node': [],
'pos': [],
'ori': []
}
for node in self.nodes:
position = cmds.xform(
node, query = True,
translation = True,
worldSpace = True)
orientation = cmds.xform(
node, query = True,
translation = True,
worldSpace = True)
transforms['node'].append(node)
transforms['pos'].append(position)
transforms['ori'].append(orientation)
state = {
'size': self.size,
'controls': self.controls,
'transforms': transforms
}
return state
Conclusion
In this blog post, we’ve explored how to implement a Rubik’s Cube run in Maya using pymel. With this implementation, users can create, control, and interact with a virtual Rubik’s Cube in a 3D environment. This project demonstrates the versatility and power of scripting
Feel free to download the full project from my repository.