""" Fair Warning: This will be the most complex example in the course using more advanced maya features alongside more advanced python features than previous examples. """ # First of all let me grab the Qt module because it has somethings I want that I don't need to use often import json import os import Qt # I will use the following modules more often, so let me import them directly import time from Qt import QtWidgets, QtCore, QtGui # This is the logging module # It is a much better way of logging output instead of using print statements import logging # We'll do a basic configuration of the loggers logging.basicConfig() # We want a logger specifically for this tool, so lets grab one so that we can control it on its own logger = logging.getLogger('LightingManager') # Loggers have different levels we can log to. # We can configure the current level to make it disable certain logs when we don't want it. logger.setLevel(logging.DEBUG) # Okay, so this is kind of messy but necessary at the moment. # While Qt.py lets us abstract the actual Qt library, there are a few things it cannot do yet and a few support libraries we need that we have to import ourtselves # So I need to check the correct binding we're using under Qt.py # If you're specifically using a Qt binding, then just use the import that makes sense for you. I'll elaborate below if Qt.__binding__.startswith('PyQt'): # If we're using PyQt4 or PyQt5 we need to import sip logger.debug('Using sip') # So we import wrapInstance from sip and alias it to wrapInstance so that it's the same as the others from sip import wrapinstance as wrapInstance # Also PyQt uses pyqtSignal instead of Signal so we will import it and alias it to Signal from Qt.QtCore import pyqtSignal as Signal elif Qt.__binding__ == 'PySide': # If we're using PySide (Maya 2016 and earlier), we'll use shiboken instead logger.debug('Using shiboken') # Shiboken already uses the correct names for both wrapInstance and Signal so we just need to import them without aliasing them from shiboken import wrapInstance from Qt.QtCore import Signal else: # Finally, the only option left is PySide2(Maya 2017 and higher) which uses shiboken2 logger.debug('Using shiboken2') # Again, this uses the correct naming so we just import without aliasing from shiboken2 import wrapInstance from Qt.QtCore import Signal # For the import statemnets above, if you feel like simplifying the process, then just use the part that is relevant to the Maya version you're using # This is the Maya API library for dealing with UIs # This is the extent of the internal Maya API that we will be using directly for this course. from maya import OpenMayaUI as omui # Then we plan to use pyMel instead of maya.cmds for this project # PyMel is like a layer above maya.cmds and the Maya API that bridges them together to make a more python like API # This is nicer than using cmds which was originally made for MEL and the API which was designed for C++ # That said, it has its shortcomings that I will cover in a video which is why I haven't covered it till now import pymel.core as pm # Finally from the functional tools library we import partial that will be useful for craeting temporary functions from functools import partial class LightWidget(QtWidgets.QWidget): """ Now on to the good stuff This is our Basic controller for controlling lights to display it, give it the name of a light like so ui = LightWidget('directionalLight1') ui.show() """ # This is our solo signal # We are creating our own signal for other Qt objects to connect to # Qt demands that we make the signal here so it knows what the class looks like onSolo = Signal(bool) def __init__(self, light): # Our init function takes the name of a light # We then call the init from QWidget to make sure that our object is initialized properly super(LightWidget, self).__init__() # If the light is a string, we want to convert it to a PyMel object to deal with it easier # The isInstance checks if it is of type basestring (which includes all the various string types) if isinstance(light, basestring): logger.debug('Converting node to a PyNode') light = pm.PyNode(light) # We might also get passed the transform instead of the light shape, either as a PyNode or a name. # So we'll check if it's a transform node and then get the shape if isinstance(light, pm.nodetypes.Transform): light = light.getShape() # Then we store the pyMel node on this class self.light = light # Finally we call the buildUI method self.buildUI() def buildUI(self): # We create a GridLayout # GridLayouts are very flexible and allow us to quickly position widgets in a grid layout = QtWidgets.QGridLayout(self) # We make a checkbox with the label of our Light node's transform # Here you can see why PyMel is useful. Rather than passing our light's name to other cmds functions to get its parent # we can simply just call a method of the light object itself. self.name = name = QtWidgets.QCheckBox(str(self.light.getTransform())) # Lets make sure its value is the same as the lights visibility # Again, instead of doing cmds.getAttr('%s.visibility' % self.light), this simplifies the code a lot name.setChecked(self.light.visibility.get()) # We connect the toggled signal from the checkbox to a lambda. It will be called anytime the checkbox value changes # A lambda is another name for an unnamed function that will be called later # It is the same as this piece of code # # def setLightVisibility(self, val): # self.light.visibility.set(val) # # I like using lambdas when the logic is very simple. If your logic is more complex, use a real function or method name.toggled.connect(lambda val: self.light.visibility.set(val)) # Finally we add it to the layout in position 0, 0 (row 0, column 0) layout.addWidget(name, 0, 0) # Now we need a button to solo the light solo = QtWidgets.QPushButton('Solo') # Buttons can also be checkable, in that when you click them they will stay pressed till you unpress them solo.setCheckable(True) # Finally we connect the toggled value of the button to another lambda # This lambda will in turn tell our custom onSolo signal to emit with the same value it receive # This is the same as this piece of code # # def emitSoloSignal(self, value): # self.onSolo.emit(value) # # Again, for a simple one line function that we never use again, a lambda is a good fit solo.toggled.connect(lambda val: self.onSolo.emit(val)) # Then we add it to the grid layout in position (row 0, column 1) layout.addWidget(solo, 0, 1) # This will be our button to delete the light delete = QtWidgets.QPushButton('X') # The delete Light function is a little more complex so we will make it a real method and connect to it delete.clicked.connect(self.deleteLight) # We set the maximum width to 10, so that it's not super wide delete.setMaximumWidth(10) # Finally we add it to the same row, but the next column over layout.addWidget(delete, 0, 2) # We want a slider that can control the intensity of the light # We tell it that we want it to be horizontal by passing it the Qt value for Horizontal intensity = QtWidgets.QSlider(QtCore.Qt.Horizontal) # We set the minimum and maximum value of the slider intensity.setMinimum(1) intensity.setMaximum(1000) # Then we set its current value based of the intensity of the light itself intensity.setValue(self.light.intensity.get()) # We then connect its value changed signal to another lambda that sets the lights intensity intensity.valueChanged.connect(lambda val: self.light.intensity.set(val)) # Finally we add it to the grid, on the next row down. # If you notice this takes two extra variables, which tell it how many rows and columns to occupy # So we are adding it to row 1, column 2 and telling it to take 1 row and 2 columns of space # If you don't provide the last two arguments, they default to 1 each layout.addWidget(intensity, 1, 0, 1, 2) # This will be our button to display the color of the light self.colorBtn = QtWidgets.QPushButton() # We set the width and height of the button to our liking self.colorBtn.setMaximumWidth(20) self.colorBtn.setMaximumHeight(20) # Finally we call a method to sat the buttons color based on the lights current color self.setButtonColor() # We then connect it to our setColor method, again something too complex to be a lambda self.colorBtn.clicked.connect(self.setColor) # Finally we add it to the grid again, at row 1, column 2 with the default sizing layout.addWidget(self.colorBtn, 1, 2) # Now this is a weird Qt thing where we tell it the kind of sizing we want it respect # We are saying that the widget should never be larger than the maximum space it needs self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) def disableLight(self, val): # This function takes a value, converts it to bool and then sets our checkbox to that value self.name.setChecked(not bool(val)) def deleteLight(self): # When we delete the light, we need to also delete our widget # So lets set our parent to Nothing. This will remove it from the manager UI and tells Qt to stop holding onto it self.setParent(None) # There is a period of time before Qt deletes it after we tell it to remove it # So lets mark its visibility to False self.setVisible(False) # Then we tell instruct it to delete it later just in case it hasn't gotten the hint yet self.deleteLater() # We only delete the light itself after the widget is deleted so that in the event of an error, we don't do any damage to the scene # We use the light's transform to make sure we are deleting at the transform level and not just the shape under it pm.delete(self.light.getTransform()) def setColor(self): # First of all we get the color values from the light. This will be a list of 3 floats lightColor = self.light.color.get() # Then we provide this to the maya's color editor which gives us back the color the user specified color = pm.colorEditor(rgbValue=lightColor) # Annoyingly, it gives us back a string instead of a list of numbers. # So we split the string, and then convert it to floats r, g, b, a = [float(c) for c in color.split()] # We then use the r,g,b to set the colors on the light and the button color = (r, g, b) self.light.color.set(color) self.setButtonColor(color) def setButtonColor(self, color=None): # This function sets the color on the color picker button # If no color is provided, we get the color from the light if not color: # We use pymels methods to query the value color = self.light.color.get() # We make sure that any provided color is a list of 3 items # Assert is a one liner that is similar to this piece of code: # # if not len(color) == 3: # raise Exception("You must provide a list of 3 colors") # # It is generally useful for validating inputs with simple checks assert len(color) == 3, "You must provide a list of 3 colors" # Finally everything gives us the r,g,b in normalized floats from 0 to 1 # Qt expects it in integer values from 0 to 255 # So we multiply the members of color by 255 to get the correct number r, g, b = [c * 255 for c in color] # Qt lets us style objects using CSS similar to in websites # So we give it a CSS string with the correct r,g,b values and a full alpha self.colorBtn.setStyleSheet('background-color: rgba(%s, %s, %s, 1.0);' % (r, g, b)) class LightingManager(QtWidgets.QWidget): """ This is the main lighting manager. To call it we just do LightingManager(dock=True) and it will display docked, otherwise dock=False will display it as a window """ # This is a dictionary of Light types to use for the Manager. # The Key is the name that will be displayed in the UI # The Value is the function that will be called lightTypes = { "Point Light": pm.pointLight, "Spot Light": pm.spotLight, # This is our first exposure to partial # Partial is like a lambda, and in most cases are identical. # The difference is lambdas get their values when they run, partials get their values when you create it # In this case, we are saying make a partial function to call pm.shadingNode and everything else will be arguments to it # This is the same as # # def createAreaLight(self): # pm.shadingNode('areaLight', asLight=True) # # But it can be convenient to just use a partial rather than making functions for everything "Area Light": partial(pm.shadingNode, 'areaLight', asLight=True), "Directional Light": pm.directionalLight, "Volume Light": partial(pm.shadingNode, 'volumeLight', asLight=True) } def __init__(self, dock=False): # So first we check if we want this to be able to dock if dock: # If we should be able to dock, then we'll use this function to get the dock parent = getDock() else: # Otherwise, lets remove all instances of the dock incase it's already docked deleteDock() # Then if we have a UI called lightingManager, we'll delete it so that we can only have one instance of this # A try except is a very important part of programming when we don't want an error to stop our code # We first try to do something and if we fail, then we do something else. try: pm.deleteUI('lightingManager') except: logger.debug('No previous UI exists') # Then we create a new dialog and give it the main maya window as its parent # we also store it as the parent for our current UI to be put inside parent = QtWidgets.QDialog(parent=getMayaMainWindow()) # We set its name so that we can find and delete it later parent.setObjectName('lightingManager') # Then we set the title parent.setWindowTitle('Lighting Manager') # Finally we give it a layout dlgLayout = QtWidgets.QVBoxLayout(parent) # Now we are on to our actual widget # We've figured out our parent, so lets send that to the QWidgets initialization method super(LightingManager, self).__init__(parent=parent) # We call our buildUI method to construct our UI self.buildUI() # Now we can tell it to populate with widgets for every light self.populate() # We then add ourself to our parents layout self.parent().layout().addWidget(self) # Finally if we're not docked, then we show our parent if not dock: parent.show() def buildUI(self): # Like in the LightWidget we show our layout = QtWidgets.QGridLayout(self) # We create a combobox # Comboboxes are essentially dropdown selectionwidgets self.lightTypeCB = QtWidgets.QComboBox() # We populate it with the items in our lightTypes dictionary # I like to have my items alphabetically so I sort it to begin with for lightType in sorted(self.lightTypes): # We add the option to the combobox self.lightTypeCB.addItem(lightType) # Finally we add it to the layout in row 0, column 0 # We tell it take 1 row and two columns worth of space layout.addWidget(self.lightTypeCB, 0, 0, 1, 2) # We create a button to create the chosen lights createBtn = QtWidgets.QPushButton('Create') # We connect the button so it calls the createLight method when its clicked createBtn.clicked.connect(self.createLight) # We add it to the layout in row 0, column 2 layout.addWidget(createBtn, 0, 2) # We want to put all the LightWidgets inside a scrolling container # We first need a container widget scrollWidget = QtWidgets.QWidget() # We want to make sure this widget only tries to be the maximum size of its contents scrollWidget.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) # Then we give it a vertical layout because we want everything arranged vertically self.scrollLayout = QtWidgets.QVBoxLayout(scrollWidget) # Finally we create a scrollArea that will be in charge of scrolling its contents scrollArea = QtWidgets.QScrollArea() # Make sure it's resizable so it resizes as the UI grows or shrinks scrollArea.setWidgetResizable(True) # Then we set it to use our container widget to scroll scrollArea.setWidget(scrollWidget) # Then we add this scrollArea to the main layout, at row 1, column 0 # We tell it to take 1 row and 3 columns of space layout.addWidget(scrollArea, 1, 0, 1, 3) # We add the save button to save our lights saveBtn = QtWidgets.QPushButton('Save') # When clicked it will call the saveLights method saveBtn.clicked.connect(self.saveLights) # We add it to row 2, column 0 layout.addWidget(saveBtn, 2, 0) # We also add an import button to import our lights importBtn = QtWidgets.QPushButton('Import') # When clicked it will call the importLights method importBtn.clicked.connect(self.importLights) # We add it to row 2, column 1 layout.addWidget(importBtn, 2, 1) # We need a refresh button to manually force the UI to refresh on changes refreshBtn = QtWidgets.QPushButton('Refresh') # We'll connect this to the refresh method refreshBtn.clicked.connect(self.refresh) # Finally we add it to the layout at row 2, column 2 layout.addWidget(refreshBtn, 2, 2) def refresh(self): # This is one of the rare times I use a while loop # It could be done in a for loop, but I want to show you how a while loop would look # We say that while the scrollLayout.count() gives us any Truth-y value we will run the logic # count() tells us how many children it has while self.scrollLayout.count(): # We take the first child of the layout, and ask for the associated widget # Taking the child, means that it is no longer under the care of its parent widget = self.scrollLayout.takeAt(0).widget() # Some objects don't have widgets, so we'll only run this for objects with a widget if widget: # We set the visibility to False because there is a period where it will still be alive widget.setVisible(False) # Then we tell it to kill the widget when it can widget.deleteLater() # Finally we tell it to populate again self.populate() def populate(self): # We list all the existing lights in the scene by type of the lights for light in pm.ls(type=["areaLight", "spotLight", "pointLight", "directionalLight", "volumeLight"]): # PyMel gives us back a PyNode for each object it lists # We will pass this to the addLight method that will create the widget for it self.addLight(light) def saveLights(self): # We'll now save the lights down to a JSON file that can be shared as a preset # The properties dictionary will hold all the light properties to save down properties = {} # First lets get all the light widgets that exist in our manager for lightWidget in self.findChildren(LightWidget): # For each widget we can get its' light object light = lightWidget.light # Then we need to get its transform node transform = light.getTransform() # Finally we add it to the dictionary. # The key will be the name of the transform which we get by converting the node to a string # Then we simply query the attributes of the light that we want to save down properties[str(transform)] = { 'translate': list(transform.translate.get()), 'rotation': list(transform.rotate.get()), 'lightType': pm.objectType(light), 'intensity': light.intensity.get(), 'color': light.color.get() } # We fetch the light manager directory to save in directory = self.getDirectory() # We then construct the name of the lightFile to save # We'll be using time.strftime to construct a name using the current time # %m%d will give 0701 for July 1st (month and day) # so we'd end up with a name like lightFile_0701.json stored in our directory lightFile = os.path.join(directory, 'lightFile_%s.json' % time.strftime('%m%d')) # Next we open the file to write with open(lightFile, 'w') as f: # Then we use json to write out our file to this location json.dump(properties, f, indent=4) # A helpful logger call tells us where the file was saved to. logger.info('Saving file to %s' % lightFile) def getDirectory(self): # The getDirectory method will give us back the name of our library directory and create it if it doesn't exist directory = os.path.join(pm.internalVar(userAppDir=True), 'lightManager') if not os.path.exists(directory): os.mkdir(directory) return directory def importLights(self): # This function goes over importing the lights back in. # We first find the directory directory = self.getDirectory() # Then we use the QFileDialog to open a file browser so we can select the json file to import # We give it self as the part, a name for the browser and tell it which directory to open to fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Light Browser", directory) # Next we open the fileName in read mode with open(fileName[0], 'r') as f: # Then we use json to load the file into a dictionary properties = json.load(f) # We loop through the keys and values of this dictionary using properties.items() for light, info in properties.items(): # We find the light type from the info lightType = info.get('lightType') # Then for each of the light types we support, we check if they match the light type for lt in self.lightTypes: # But the light type of a Point Light is pointLight, so we convert Point Light to pointLight and then compare if ('%sLight' % lt.split()[0].lower()) == lightType: # If we found a match, then we break out break else: # For Loops also have an else statement. This only runs when the loop has not been broken out of # We assume if we haven't broken out of the loop, then we haven't found the light type # If that's the case, we just notify the user and continue on to the next light logger.info('Cannot find a corresponding light type for %s (%s)' % (light, lightType)) continue # we can reuse variable from the loop, in this case lt was the light type. # We use this to create a light light = self.createLight(lightType=lt) # then we set the parameters on the light itself light.intensity.set(info.get('intensity')) light.color.set(info.get('color')) # Then we get the transform of the light to set its parameters transform = light.getTransform() transform.translate.set(info.get('translate')) transform.rotate.set(info.get('rotation')) # After that's done, we call the populate method to refresh our interface self.populate() def createLight(self, lightType=None, add=True): # This function creates lights. Duh. # First we get the text of the combobox if we haven;t been given a light if not lightType: lightType = self.lightTypeCB.currentText() # Then we look up the lightTypes dictionary to find the function to call func = self.lightTypes[lightType] # All our functions are pymel functions so they'll return a pymel object light = func() # We wil pass this to the addLight method if the method has been told to add it if add: self.addLight(light) return light def addLight(self, light): # This will create a LightWidget for the given light and add it to the UI # First we create the LightWidget widget = LightWidget(light) # Then we connect the onSolo signal from the widget to our isolate method widget.onSolo.connect(self.isolate) # Finally we add it to the scrollLayout self.scrollLayout.addWidget(widget) def isolate(self, val): # This function will isolate a single light # First we find all our children who are LightWidgets lightWidgets = self.findChildren(LightWidget) # We'll loop through the list to perform our logic for widget in lightWidgets: # Every signal lets us know who sent it that we can query with sender() # So for every widget we check if its the sender if widget != self.sender(): # If it's not the widget, we'll disable it widget.disableLight(val) def getMayaMainWindow(): """ Since Maya is Qt, we can parent our UIs to it. This means that we don't have to manage our UI and can leave it to Maya. Returns: QtWidgets.QMainWindow: The Maya MainWindow """ # We use the OpenMayaUI API to get a reference to Maya's MainWindow win = omui.MQtUtil_mainWindow() # Then we can use the wrapInstance method to convert it to something python can understand # In this case, we're converting it to a QMainWindow ptr = wrapInstance(long(win), QtWidgets.QMainWindow) # Finally we return this to whoever wants it return ptr def getDock(name='LightingManagerDock'): """ This function creates a dock with the given name. It's an example of how we can mix Maya's UI elements with Qt elements Args: name: The name of the dock to create Returns: QtWidget.QWidget: The dock's widget """ # First lets delete any conflicting docks deleteDock(name) # Then we create a workspaceControl dock using Maya's UI tools # This gives us back the name of the dock created ctrl = pm.workspaceControl(name, dockToMainWindow=('right', 1), label="Lighting Manager") # We can use the OpenMayaUI API to get the actual Qt widget associated with the name qtCtrl = omui.MQtUtil_findControl(ctrl) # Finally we use wrapInstance to convert it to something Python can understand, in this case a QWidget ptr = wrapInstance(long(qtCtrl), QtWidgets.QWidget) # And we return that QWidget back to whoever wants it. return ptr def deleteDock(name='LightingManagerDock'): """ A simple function to delete the given dock Args: name: the name of the dock """ # We use the workspaceControl to see if the dock exists if pm.workspaceControl(name, query=True, exists=True): # If it does we delete it pm.deleteUI(name)