Procedural Stairs Tutorial

This tutorial will help you with the basics of Python+Pyside :

This tutorial covers the very basics of Pyside and Python for Maya 2018.
If you are starting to venture into Python and have trouble finding simple examples, hopefully this post will help!

I will go into detailing part by part, with sample codes and screenshots. The full script lives here if you want to download it:
github.com

For this specific example I will not create a module for it, we will just create the script and call it at the end.

Script demo!

So moving forward, we will start by detecting which Maya we are using in order to correctly import the libraries in Maya 2016 or Maya 2018.
The “try” command will do just that, try to do everything inside the block, otherswise skip and do the exception block.

try:
  from PySide2.QtCore import * 
  from PySide2.QtGui import * 
  from PySide2.QtWidgets import *
  from PySide2 import __version__
  from shiboken2 import wrapInstance 
except ImportError:
  from PySide.QtCore import * 
  from PySide.QtGui import * 
  from PySide import __version__
  from shiboken import wrapInstance 

Now we need to import Maya’s commands as well as OpenMayaUI to create our UI with Maya’s window as a parent.

from maya import cmds
from maya import OpenMayaUI as omui 

This next line will get the Maya main window so we can use it as the parent of our widget. We imported it as omui, so thats the name we need to call it

mayaMainWindowPtr = omui.MQtUtil.mainWindow()
mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QWidget) 

Finally we can start by creating our main class. In this case, we will create a class that inherits the properties of QWidget.
QWidget is how we will create our UI. Think of it as a container where we will drop all of our buttons, text box, etc.

class CreateStairsUI(QWidget):  
    def __init__(self, *args, **kwargs):        
        super(CreateStairsUI, self).__init__(*args, **kwargs)   
        self.setParent(mayaMainWindow)
        self.setWindowFlags(Qt.Window)

Notice how we are doing self.setParent(mayaMainWindow), we defined this variable previously.

Next we will define some variables that we will be using through the script. The name of the variables should be relevant to the information they are storing, so that we can read them easily through the code.

We then set some basic info of our UI, like the internal name and the label that will show in the window. We can also set the width here. After that we call self.initUI() which is a procedure that we will define next.

        self.__stepWidth=1
        self.__stepHeight=1
        self.__stepDepth=1
        self.__curvature=0
      
        self.setObjectName('StairUI_uniqueId')        
        self.setWindowTitle('Procedural Stairs')
        self.setMinimumWidth(300)
        
        self.initUI()        

Connects everything from our ui to the procedures.
The ui is comformed by QLabels and QSpinbox, inside a QWidget, which is inside the main QWidget. Something like this:

Widget structure

The main widget is defined as self, since we subclassed it from QWidget(). To access it, just call “self”.
Example: We want to create a QPushButton inside MainWidget. QPushButton can receive a label, and a parent: QPushButton(‘Button’, parent).
Since we want to create the QPushButton inside MainWidget, we can simply: QPushButton(‘Button’, self)

Another way of adding a widget inside another widget is through the use of layout().addWidget()
Each widget has an internal layout to display the contents nicely. To set a layout, we first need to create it:
layout=QHBoxLayout() # Horizontal will display everything in a single row, multiple columns.
or:
layout=QVBoxLayout() # Vertical will display everything in a single column, multiple rows.
And then assign it to a widget:
widget.setLayout(layout)
Then we can add any other widget inside the layout:
layout.addWidget(myOtherWidget)
layout.addWidget(myLabel)
layout.addWidget(mySpinBox)

The contents will be added in order.

Horizontal Widget

There are other more complex layouts, feel free to check some Qt documentation online!

        layout=QVBoxLayout()
        self.setLayout(layout)
        # Steps widget
        stepsWidget=QWidget()
        # Set horizontal layout
        stepsLayout=QHBoxLayout()
        stepsWidget.setLayout(stepsLayout)
        stepsLabel=QLabel('Number of steps')
        self.stepsSpinBox=QSpinBox()
        self.stepsSpinBox.setMinimum(2)
        self.stepsSpinBox.valueChanged.connect(self.createStair)
        stepsLayout.addWidget(stepsLabel)
        stepsLayout.addWidget(self.stepsSpinBox)
        # Width widget
        widthWidget=QWidget()
        widthLayout=QHBoxLayout()
        widthWidget.setLayout(widthLayout)
        widthLabel=QLabel('Step width')
        self.widthSpinBox=QDoubleSpinBox()
        self.widthSpinBox.setMinimum(1)
        self.widthSpinBox.setValue(1)
        self.widthSpinBox.valueChanged.connect(self.updateWidth)
        widthLayout.addWidget(widthLabel)
        widthLayout.addWidget(self.widthSpinBox)
        # Depth widget
        depthWidget=QWidget()
        depthLayout=QHBoxLayout()
        depthWidget.setLayout(depthLayout)
        depthLabel=QLabel('Step depth')
        self.depthSpinBox=QDoubleSpinBox()
        self.depthSpinBox.setMinimum(0.1)
        self.depthSpinBox.setSingleStep(0.1)
        self.depthSpinBox.setValue(1)
        self.depthSpinBox.valueChanged.connect(self.updateDepth)
        depthLayout.addWidget(depthLabel)
        depthLayout.addWidget(self.depthSpinBox)        
        # Height widget
        heightWidget=QWidget()
        heightLayout=QHBoxLayout()
        heightWidget.setLayout(heightLayout)
        heightLabel=QLabel('Step height')
        self.heightSpinBox=QDoubleSpinBox()
        self.heightSpinBox.setMinimum(0.1)
        self.heightSpinBox.setSingleStep(0.1)
        self.heightSpinBox.setValue(1)
        self.heightSpinBox.valueChanged.connect(self.updateHeight)
        heightLayout.addWidget(heightLabel)
        heightLayout.addWidget(self.heightSpinBox)
        # Add all of our widget to the main widget, which has a vertical layout defined way up in line 105
        layout.addWidget(stepsWidget)
        layout.addWidget(depthWidget)
        layout.addWidget(widthWidget)
        layout.addWidget(heightWidget)

The next procedures update the width, height and depth, without calling the main creation of the cubes.
It basically iterates through the children of StairsGroup, gets the inputs of each cubes and modifies the value and subdivision level.
Also, it calculates the position of the cubes if the height, width or depth changed.
Removed the curvature in purpose, so you can have a challenge on applying it back.

    def getChildrenInputs(self):
        '''
        Returns the inputs for every cube, so we can modify width, height, depth and subdivision level
        '''
        inputs=[]
        if cmds.objExists('StairGroup'):
            getStepGroups=self.getChildren()
            for stepGroup in getStepGroups:
                inputs.append(cmds.listConnections(cmds.listRelatives(cmds.listRelatives(stepGroup)))[1])
        return inputs   

    def getChildren(self):
        '''
        Returns the bunch of groups inside the StairGroup
        '''
        if cmds.objExists('StairGroup'):
            return cmds.listRelatives('StairGroup')

    def updateWidth(self, value):
        '''
        Iterates through the StairGroup children and modifies the width accordingly to the new value of the ui.
        We only get the inputs, since we will not modify the children groups of StairGroup directly.
        '''
        self.stepWidth=value
        getInputs=self.getChildrenInputs()
        for polyInput in getInputs:
            cmds.setAttr('%s.width' % polyInput, self.stepWidth)
            if self.stepWidth>1:
                cmds.setAttr('%s.subdivisionsWidth' % polyInput, self.stepWidth)

    def updateDepth(self, value):
        '''
        Iterates through the StairGroup children and modifies the depth accordingly to the new value of the ui.
        Gets both inputs and children, since we need the children groups of StairGroup to modify its translation Z.
        '''
        self.stepDepth=value
        count=1
        getInputs=self.getChildrenInputs()
        getChildren=self.getChildren()
        for polyInput in getInputs:
            cmds.setAttr('%s.depth' % polyInput, self.stepDepth)
            if self.stepDepth>1:
                cmds.setAttr('%s.subdivisionsDepth' % polyInput, self.stepDepth)
            cmds.setAttr('%s.tz' % getChildren[count-1], self.stepDepth*count)
            count+=1

    def updateHeight(self, value):
        '''
        Iterates through the StairGroup children and modifies the height accordingly to the new value of the ui.
        Gets both inputs and children, since we need the children groups of StairGroup to modify its translation Y.
        '''
        self.stepHeight=value
        count=1
        getInputs=self.getChildrenInputs()
        getChildren=self.getChildren()
        for polyInput in getInputs:
            cmds.setAttr('%s.height' % polyInput, self.stepHeight)
            if self.stepHeight>1:
                cmds.setAttr('%s.subdivisionsHeight' % polyInput, self.stepHeight)
            cmds.setAttr('%s.ty' % getChildren[count-1], self.stepHeight*count)
            count+=1

    # Getters and setters
    @property
    def curvature(self):
        return self.__curvature

    @curvature.setter
    def curvature(self, curvature):
        self.__curvature=curvature

    @property
    def stepWidth(self):
        return self.__stepWidth

    @stepWidth.setter
    def stepWidth(self, stepWidth):
        self.__stepWidth=stepWidth

    @property
    def stepDepth(self):
        return self.__stepDepth

    @stepDepth.setter
    def stepDepth(self, stepDepth):
        self.__stepDepth=stepDepth

    @property
    def stepHeight(self):
        return self.__stepHeight

    @stepHeight.setter
    def stepHeight(self, stepHeight):
        self.__stepHeight=stepHeight

    def createStair(self, value):
        '''
        Main function that procedurally creates a bunch of cubes and sets them according to the values of the ui.
        '''
        if not cmds.objExists('StairGroup'):
            stairGroup=cmds.group(n='StairGroup', em=True)
        else:
            cmds.delete(cmds.listRelatives('StairGroup'))
            stairGroup='StairGroup'
        
        for step in range(1,value+1):
            stepGroup=cmds.group(n='StepGroup_%s' % (str(step)), em=True)
            stepCube=cmds.polyCube(n='Step_%s' % (str(step)), w=1, h=1, d=1, sx=1, sy=1, sz=1, ax=(0,1,0), cuv=4, ch=1)
            cmds.parent(stepCube, stepGroup)
            cmds.setAttr('%s.ty' % stepGroup, self.__stepHeight*step)
            cmds.setAttr('%s.tz' % stepGroup, self.__stepDepth*step)
            cmds.parent(stepGroup, stairGroup)
        self.updateHeight(self.__stepHeight)
        self.updateWidth(self.__stepWidth)
        self.updateDepth(self.__stepDepth)

# Create a new instance of our main class and show it though the QtGui.QWidget.show() definition. 
CreateStairsUI().show()

Leave a Comment

Scroll to Top