17 | Python For Maya: Artist Friendly Programming
18 |
19 |
20 |
21 | This course will teach Python for Maya using an artist friendly approach, by breaking down concepts into small digestible pieces and giving projects with real world use.
22 |
23 | ### About Me
24 |
25 | You can also find more information about me on my website
26 |
27 | [http://www.dgovil.com](http://dgovil.com/)
28 |
29 | ### Projects We'll Be Completing
30 |
31 | During the course, we'll create a few different projects to both showcase how Python is useful in a real world context,and to learn new concepts
32 |
33 | * Create and prop geometry with a simple rig ([LINK](introduction/))
34 | * Rename and organize a scene ([LINK](objectRenamer/))
35 | * Automatically create Gears for modelling with a configurable amount of teeth ([LINK](gearCreator/))
36 | * An Animation Tweener with a simple UI ([LINK](tweener/))
37 | * A Library tool for Rigging Controls with a UI ([LINK](controllerLibrary/))
38 | * A Light Manager ([LINK](lightManager/))
39 | * A command line file tool to manage image sequences ([LINK](commandLine/))
40 |
41 | ## Tools That Will Be Used
42 |
43 | For the course we will use the following
44 |
45 | * **Maya 2017**
46 |
47 | This is currently the latest version of Maya and has some major changes that will be covered.
48 | Feel free to use an older version of Maya (as low as 2011), as I will cover the differences and give you the knowledge to adapt
49 |
50 | You can download an education version of Maya 2017 here: http://www.autodesk.com/education/free-software/maya
51 | You can download a Maya 2017 trial here: http://www.autodesk.com/products/maya/free-trial
52 |
53 | * **Python 2.7**
54 |
55 | Obviously, this course will use Python, but it is important to note we will be using Python 2.7 and not Python 3.x
56 |
57 | If you do not already have Python installed, I recommend downloading Anaconda instead.
58 | It is a packaged version of Python that comes with a lot of great libraries prebuilt for you, and is much easier to get started with than the official Python.
59 | Please remember to download the Python 2.7 version
60 |
61 | https://www.continuum.io/downloads
62 |
63 | Maya 2017 uses Python 2.7 and this is also the agreed upon standard by the VFX Reference Platform www.vfxplatform.com
64 | Maya 2014-2016 also use Python 2.7, whereas Maya 2011-2013 use Python 2.6.
65 |
66 | The latest version of Python is Python 3.5, however Python 3.x has introduced many breaking changes to Python.
67 | These changes are for the better but due to large investment into Python 2 code, Maya will continue to be on Python 2 for a while longer.
68 |
69 | * **PyCharm**
70 |
71 | PyCharm is a very well established IDE with a lot of useful features for a beginner to both learn with and grown into a full fledged developer.
72 | It is my editor of choice today.
73 |
74 | I would recommend downloading PyCharm Edu from here: https://www.jetbrains.com/pycharm-edu/
75 |
76 | PyCharm Edu is a version of PyCharm with a simplified interface (optional) and coursework that will help a user learn Python in their spare time.
77 |
78 | * **Maya DevKit**
79 |
80 | Unfortunately from Maya 2016 onwards, Autodesk stopped shipping the Maya developer kit with Maya.
81 | This isn't super necessary for our course, but it does provide some nice autocomplete features in our editors.
82 |
83 | If you're on **Maya 2016** download the zip file from here: https://github.com/autodesk-adn/Maya-devkit
84 |
85 | If you're on **Maya 2017 or higher** download it from here: https://www.autodesk.com/developer-network/platform-technologies/maya
86 |
87 |
88 | Instructions on how to set up your directories for your specific OS are here: http://help.autodesk.com/view/MAYAUL/2017/ENU//?guid=__files_Setting_up_your_build_environment_htm
89 |
90 | * **Qt.py**
91 |
92 | For the Qt portion of our course, there are several Qt libraries we can use.
93 | If you're using Maya 2017 or above, you can use PySide2 or PyQt5. If you're using Maya 2016 or below, you can use PySide or PyQt4.
94 |
95 | Rather than having to develop for all these options, we can use a library that can make use of whichever one it finds.
96 | This library is called **Qt.py** and you can download it here: https://github.com/mottosso/Qt.py
97 |
98 |
99 | * **Other Editors**
100 |
101 | There are a lot of other editors, and I will personally not be using them for this course.
102 | However, if you have a preference for other editors, I will go over setting up some of the editors with Maya.
103 | The following editors will be covered
104 |
105 | * Sublime Text
106 | * Atom
107 | * Visual Studio Code
108 | * Eclipse
109 |
110 | * **Operating System**
111 | My preferred operating system is **Windows** and it will be what I will be using for the entire course.
112 | That said, I also use **macOS** and **Linux** and where anything should be treated differently, I will make mention of it.
113 |
114 |
115 | ## Libraries That Will Be Covered
116 |
117 | The course will cover the following libraries
118 |
119 | * `maya.cmds`
120 | * `pymel`
121 | * `Qt`
122 | * `PySide` / `PySide2`
123 |
124 |
125 | ## Other Resources
126 |
127 | ### Books
128 |
129 | Just a note that these links are affiliate links that will go to your local Amazon storefront.
130 |
131 | * [Maya Python For Games And Film](http://go.redirectingat.com?id=101037X1556917&xs=1&url=https%3A%2F%2Fwww.amazon.com%2FMaya-Python-Games-Film-Reference%2Fdp%2F0123785782%2Fref%3Dsr_1_1%3Fie%3DUTF8%26qid%3D1479605478%26sr%3D8-1%26keywords%3Dmaya%2Bpython%2Bfor%2Bfilm%2Band%2Bgames)
132 |
133 | This was the book that I learned Python from, and I cannot recommend it enough. It goes a lot more in depth on each topic, as only a book can do, and it's probably the resource I recommend the most.
134 |
135 | * [Practical Maya Programming With Python](http://go.redirectingat.com?id=101037X1556917&xs=1&url=https%3A%2F%2Fwww.amazon.com%2FPractical-Programming-Python-Robert-Galanakis%2Fdp%2F1849694729%2Fref%3Dsr_1_1%3Fie%3DUTF8%26qid%3D1479605681%26sr%3D8-1%26keywords%3Dpractical%2Bpython%2Bmaya)
136 |
137 | Rob Galanakis is a fantastic resource on Python, who runs the [Tech-Artists](http://tech-artists.org/) forum, which is where I often went to get help when I was stuck on issues, or wanted to learn what other people were doing. His book is a great resource as well.
138 |
139 | * [Rapid GUI Programming with Python and Qt: The Definitive Guide to PyQt Programming](http://go.redirectingat.com?id=101037X1556917&xs=1&url=https%3A%2F%2Fwww.amazon.com%2FRapid-GUI-Programming-Python-Definitive-ebook%2Fdp%2FB004YW6LNA%2Fref%3Dsr_1_1%3Fie%3DUTF8%26qid%3D1479605837%26sr%3D8-1%26keywords%3Drapid%2Bpyqt)
140 |
141 | If you're interested in learning more about Qt, this is the best book to have in my opinion. He goes from very basic Qt useage to very advanced concepts. The book is based around PyQt4, but if you've watched my course, it should be easy enough to switch to whichever Qt library you're using
142 |
143 | * [Complete Maya Programming: An Extensive Guide to MEL and C++ API](http://go.redirectingat.com?id=101037X1556917&xs=1&url=https%3A%2F%2Fwww.amazon.com%2FComplete-Maya-Programming-Extensive-Kaufmann%2Fdp%2F1558608354%2Fref%3Dsr_1_1%3Fie%3DUTF8%26qid%3D1479607371%26sr%3D8-1%26keywords%3DMEL%2BC%252B%252B)
144 |
145 | This is for MEL and C++ obviously, and quite an old book, but it's one that is still incredibly useful if you're interested in those languages, and one that many developers have learned from.
146 |
147 |
148 | ### Websites and Blogs
149 |
150 | * [Rigging Dojo] (http://www.riggingdojo.com/)
151 |
152 | Rigging Dojo is **the** online school for rigging and technical skills. They have a ton of great mentored courses on Python, C++, Rigging etc..
153 |
154 | * [Python For Maya: Google Group] (https://groups.google.com/forum/?fromgroups#!forum/python_inside_maya)
155 |
156 | A great google group run by Justin Israel, where people can ask questions about Python and get help from other Python developers.
157 |
158 | * [Learn X in Y Minutes: Python](https://learnxinyminutes.com/docs/python/)
159 |
160 | A website dedicated to giving a really quick introduction to programming languages.
161 |
162 | * [TDsAnonymous] (http://www.tdsanonymous.com/)
163 |
164 | A companion site for an online community I'm part of. The community is invite only, but the site is a place where we can contain resources that we find useful.
165 |
166 | * [Zetcode PyQt4](http://zetcode.com/gui/pyqt4/) and [Zetcode PyQt5](http://zetcode.com/gui/pyqt5/)
167 |
168 | A quick introduction to using PyQt4 and PyQt5. If you're using PySide, just replace the library name.
169 | This is where I learned to use PyQt4 from when I was teaching myself Python, and it's the first place I point people to when they want to learn.
170 |
171 | * [CodeHeadWords](https://codeheadwords.com/)
172 |
173 | This is a blog run by John Hood who is a coworker of mine who's taught me a ton.
174 |
--------------------------------------------------------------------------------
/commandLine/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/commandLine/__init__.py
--------------------------------------------------------------------------------
/commandLine/renamer.py:
--------------------------------------------------------------------------------
1 | """
2 | This is our final project and is an example of how to use python outside Maya to create command line tools
3 |
4 | We'll make a script that can be both reused by other python libraries and from the command line.
5 | It will support standard commandline flags to control its options
6 | """
7 |
8 | # The argparse module is a standard module for creating command line tools
9 | import argparse
10 |
11 | # The re module gives us the power of regular expressions, which is an advanced pattern matching library
12 | import re
13 |
14 | # And of course we need the os module to interact with the operating system
15 | import os
16 |
17 | # Shutil is another utility that we may need to copy a file
18 | import shutil
19 |
20 |
21 | def main():
22 | """
23 | This is the function that gets run by default when this module is executed.
24 | It is common convention to call this first function 'main' but it can be called anything you like
25 | """
26 | # First lets create a new parser to parse the command line arguments
27 | # The arguments we're giving it are ones that will be displayed when a user incorrectly uses your tool or if they ask for help
28 | parser = argparse.ArgumentParser(description="This is a simple batch renaming tool to rename sequences of files",
29 | usage="To replace all files with hello wtih goodbye: python renamer.py hello goodbye")
30 | # We'll add two positional arguments. These must be given
31 | parser.add_argument('inString', help="The word or regex pattern to replace")
32 | parser.add_argument('outString',help="The word or regex pattern to replace it with")
33 | # Then we'll add some keyword arguments. Like in python functions, they default to a value so are optional
34 | # The first one is set to store_true, which means it is False by default but if provided will be set to True
35 | # Therefore you don't provide a value to it
36 | #
37 | # The first argument is the short flag name
38 | # The second is the long version of the same flag
39 | parser.add_argument('-d', '--duplicate', help="Should we duplicate or write over the original files", action='store_true')
40 | parser.add_argument('-r', '--regex', help="Whether the inputs will be using regex or not", action='store_true')
41 |
42 | # This last argument doesn't say store true, which means a value must be given for it, or it will default to None
43 | parser.add_argument('-o', '--out', help="The location to deposit these files. Defaults to this directory")
44 |
45 | # Finally we tell the parser to parse the arguments from the command line
46 | args = parser.parse_args()
47 |
48 | # We use these arguments to provide input to our rename function
49 | rename(args.inString, args.outString, duplicate=args.duplicate,
50 | outDir=args.out, regex=args.regex)
51 |
52 | def rename(inString, outString, duplicate=True, inDir=None, outDir=None, regex=False):
53 | """
54 | A simple function to rename all the given files in a given directory
55 | Args:
56 | inString: the input string to find and replace
57 | outString: the output string to replace it with
58 | duplicate: Whether we should duplicate the renamed files to prevent writing over the originals
59 | inDir: what the directory we should operate in
60 | outDir: the directory we should write to.
61 | regex: Whether we should use regex instead of simple string replace
62 | """
63 | # If no input directory is provided, we'll use the current working directory that the script was called from
64 | if not inDir:
65 | inDir = os.getcwd()
66 |
67 | # If not output directory is provided we'll use the same directory as the current working directory
68 | if not outDir:
69 | outDir = inDir
70 |
71 | # It is possible that the output directory is provided in relative terms ("../../")
72 | # abspath will convert this to a real path
73 | outDir = os.path.abspath(outDir)
74 |
75 | # It is also possible that the output directory does not exist.
76 | # We should error early if it does not exist
77 | if not os.path.exists(outDir):
78 | raise IOError("%s does not exist!" % outDir)
79 | if not os.path.exists(inDir):
80 | raise IOError("%s does not exist!" % inDir)
81 |
82 | # Finally we loop through all the files in the current directory
83 | for f in os.listdir(inDir):
84 | # We will start by skipping over files that start with a dot.
85 | # This is a sign that they are hidden and should not be modified
86 | if f.startswith('.'):
87 | continue
88 |
89 | # If we are told to use regex, then lets use the regex module to replace the string
90 | if regex:
91 | # use regex's substitute function to replace
92 | name = re.sub(inString, outString, f)
93 | else:
94 | # Otherwise lets just use regular string replace
95 | name = f.replace(inString, outString)
96 |
97 | # Finally if the name is identical, then don't bother renaming it because it's wasted time
98 | if name == f:
99 | continue
100 |
101 | # Now lets construct the full paths to copy from since we only currently have the name of the actual file
102 | src = os.path.join(inDir, f)
103 | dest = os.path.join(outDir, name)
104 |
105 | # If we're told to duplicate, we'll use the shutil library and its' copy2 function to copy the file
106 | if duplicate:
107 | shutil.copy2(src, dest)
108 | else:
109 | # Otherwise we'll just use the os module to rename the file
110 | os.rename(src, dest)
111 |
112 |
113 | # We want to run the main() method when this python script is loaded
114 | # But we only want to do it when it's run directly and not when it's imported by something else
115 | # So we use this little check
116 | #
117 | # We check if the namespace (__name__) is __main__
118 | # This means that the code is being run directly instead of being imported into a namespace
119 | #
120 | # It's recommended to design like this so your code can be imported and reused even if you intend to only run it directly
121 | if __name__ == '__main__':
122 | # If this is true, then we run main()
123 | main()
--------------------------------------------------------------------------------
/controllerLibrary/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/controllerLibrary/__init__.py
--------------------------------------------------------------------------------
/controllerLibrary/controllerLibrary.py:
--------------------------------------------------------------------------------
1 | # We are using the Qt library that Marcus made here so that we don't have to worry about Qt4 and Qt5
2 | # If you don't want to use his library you can change it to one of the following
3 | # In Maya 2017 or above we use Qt5 so we can just replace the library name
4 | # from PySide2 import QtWidgets, QtCore, QtGui
5 | # In Maya 2016 and below, you'd have to replace both the library name and QtWidgets is called QtGui
6 | # from PySide import QtGui, QtCore
7 | # But why have that hassle when you can just use this library that means your code will work anywhere?
8 | from Qt import QtWidgets, QtCore, QtGui
9 |
10 | # This is a more complex tool so we'll be importing quite a few more libraries this time
11 | # This is the json library which we'll be using to write out data
12 | import json
13 |
14 | # This is the OS library that lets us deal with our operating system.
15 | # Specifically we will use it to find our files
16 | import os
17 |
18 | # The pprint module is short for pretty print
19 | # It is used to format dictionaries in a nice way
20 | import pprint
21 |
22 | # Finally this is our old faithful maya library
23 | from maya import cmds
24 |
25 |
26 | # We want to create a default directory that we can refer to later
27 | # We use os.path.join because it uses the correct path separator for our operating system
28 | # the userAppDir variable will give us the location where our maya documents are stored by default.
29 | # We'll set out library inside a folder inside this folder
30 | DIRECTORY = os.path.join( cmds.internalVar(userAppDir=True), 'controllerLibrary')
31 |
32 |
33 | # We start by creating our code so that it can work without the UI
34 | # Dictionaries are a good way to store data
35 | # We aren't adding much, so it's easiest to just inherit from a dictionary.
36 | # This lets our library act like its a dictionary while giving us our custom features
37 | class ControllerLibrary(dict):
38 |
39 | # First of all we need a function to create a directory
40 | # We allow the code to set another directory, but we set a default value of our current directory
41 | def createDir(self, directory=DIRECTORY):
42 | # We check if our directory doesn't exist
43 | if not os.path.exists(directory):
44 | # If it doesn't exist, we make the directory
45 | os.mkdir(directory)
46 |
47 | # First lets standardize how we will save our files
48 | # the **info argument will be new to you
49 | # similar to how we used *args in the previous example to capture all arguments
50 | # ** is used to capture all keyword arguments into the variable called info
51 | # Info will be a dictionary
52 | def save(self, name, screenshot=True, directory=DIRECTORY, **info):
53 | """
54 | The save function will save the current scene as a controller
55 | Args:
56 | name: the name to save the controller as
57 | screenshot: Whether or not to save a screenshot
58 | directory: the directory to save to
59 | **info: any extra info we might want to store
60 | """
61 | # We will start by creating the directory just to make sure it exists
62 | self.createDir(directory)
63 |
64 | # We use the same os.path.join to construct the name of our output maya file
65 | path = os.path.join(directory, '%s.ma' % name)
66 |
67 | # Similarly we construct the name of our json file that will store any info
68 | infoFile = os.path.join(directory, '%s.json' % name)
69 |
70 | # If we've been told to save the screenshot, lets run a little more code
71 | if screenshot:
72 | # We call to anotehr method to create the screenshot
73 | # Then we use the return value of that to store in the info dictionary
74 | info['screenshot'] = self.saveScreenshot(name, directory=directory)
75 |
76 | # We store some more information in the info dictionary
77 | info['name'] = name
78 | info['path'] = path
79 |
80 | # Now we rename the file to what we want it to be saved as
81 | cmds.file(rename=path)
82 |
83 | # If something is selected, we only export the selection, otherwise we save the wholefile
84 | if cmds.ls(selection=True):
85 | cmds.file(force=True, exportSelected=True)
86 | else:
87 | cmds.file(save=True, force=True)
88 |
89 | # Since we are a dictionary, we can save data to ourself
90 | self[name] = info
91 |
92 | # Finally we open a file to write to on disk
93 | # The with keyword is used to denote a context wheree the file is called f
94 | # This will open a file for us, run all the logic we give it, then close the file out
95 | with open(infoFile, 'w') as f:
96 | # We use the json library to convert our dictionary to a common data format
97 | # We write it to f
98 | # and we give each line an indentation of 4 spaces to be easy to read
99 | json.dump(info, f, indent=4)
100 |
101 | # Now we have a find function that will be used to find all the controllers in the given directory
102 | def find(self, directory=DIRECTORY):
103 | # First we check if the directory even exists, because why waste our time otherwise?
104 | if not os.path.exists(directory):
105 | return
106 |
107 | # Now we list all the files in that directory
108 | files = os.listdir(directory)
109 |
110 | # We are only interested in finding all the maya ascii files
111 | # We use list comprehension to reduce the files we're looking at
112 | mayaFiles = [f for f in files if f.endswith('.ma')]
113 |
114 | # Now we loop through the maya files we found
115 | for ma in mayaFiles:
116 | # We grab the name and the file extension of the file
117 | name, ext = os.path.splitext(ma)
118 |
119 | # We'll have to construct the name of the screenshot and the json file so that we can find them
120 | infoFile = '%s.json' % name
121 | screenshot = '%s.jpg' % name
122 |
123 | # If the infoFile exists, we'll construct its full path and try to read it in
124 | if infoFile in files:
125 | infoFile = os.path.join(directory, infoFile)
126 |
127 | # Similar to the way we wrote out the file, we'll read it in
128 | with open(infoFile, 'r') as f:
129 | # The JSON module will read our file, and convert it to a python dictionary
130 | data = json.load(f)
131 | else:
132 | # But if the file doesn't exist, we'll just make an empty dictionary
133 | data = {}
134 |
135 | # If we have a screenshot, lets store the info in the dictionary so we know where to find it later
136 | if screenshot in files:
137 | data['screnshot'] = os.path.join(directory, screenshot)
138 |
139 | # Then lets store some basic information
140 | data['name'] = name
141 | data['path'] = os.path.join(directory, ma)
142 |
143 | # Finally since we're a dictionary, we can save data to ourselves like we would to a dictionary
144 | self[name] = data
145 |
146 | # This function will be used to load the controller with the given name
147 | def load(self, name):
148 | path = self[name]['path']
149 | # We tell the file command to import, and tell it to not use any nameSpaces
150 | cmds.file(path, i=True, usingNamespaces=False)
151 |
152 | # This function will save a screenshot to the given directory with the given name
153 | def saveScreenshot(self, name, directory=DIRECTORY):
154 | path = os.path.join(directory, '%s.jpg' % name)
155 |
156 | # We'll fit the view to the objects in our scene or our selection
157 | cmds.viewFit()
158 |
159 | # We'll change our render format to jpg
160 | cmds.setAttr("defaultRenderGlobals.imageFormat", 8) # This is the value for jpeg
161 |
162 | # Finally we'll save out our image using the playblast module
163 | # There are a lot of arguments here so it's good to use the documentation to know what's going on
164 | cmds.playblast(completeFilename=path, forceOverwrite=True, format='image', width=200, height=200,
165 | showOrnaments=False, startTime=1, endTime=1, viewer=False)
166 |
167 | # Return the path of the file we saved
168 | return path
169 |
170 |
171 | # This will be our first Qt UI!
172 | # We'll be creating a dialog, so lets start by inheriting from Qt's QDialog
173 | class ControllerLibraryUI(QtWidgets.QDialog):
174 |
175 | def __init__(self):
176 | # super is an interesting function
177 | # It gets the class that our class is inheriting from
178 | # This is called the superclass
179 | # The reason is that because we redefined __init__ in our class, we no longer call the code in the super's init
180 | # So we need to call our super's init to make sure we are initialized like it wants us to be
181 | super(ControllerLibraryUI, self).__init__()
182 |
183 | # We set our window title
184 | self.setWindowTitle('Controller Library UI')
185 |
186 | # We store our library as a variable that we can access from inside us
187 | self.library = ControllerLibrary()
188 |
189 | # Finally we build our UI
190 | self.buildUI()
191 |
192 | def buildUI(self):
193 | # Just like we made a column layout in the last UI, in Qt we have a vertical box layout
194 | # We tell it that we want to apply the layout to this class (self)
195 | layout = QtWidgets.QVBoxLayout(self)
196 |
197 | # We want to make another widget to store our controls to save the controller
198 | # A widget is what we call a UI element
199 | saveWidget = QtWidgets.QWidget()
200 | # Every widget needs a layout. We want a Horizontal Box Layout for this one, and tell it to apply to our widget
201 | saveLayout = QtWidgets.QHBoxLayout(saveWidget)
202 | # Finally we add this widget to our main widget
203 | layout.addWidget(saveWidget)
204 |
205 | # Our first order of business is to have a text box that we can enter a name
206 | # In Qt this is called a LineEdit
207 | self.saveNameField = QtWidgets.QLineEdit()
208 | # We will then add this to our layout for our save controls
209 | saveLayout.addWidget(self.saveNameField)
210 |
211 | # We add a button to call the save command
212 | saveBtn = QtWidgets.QPushButton('Save')
213 | # When the button is clicked it fires a signal
214 | # A signal can be connected to a function
215 | # So when the button is called, it will call the function that is given.
216 | # In this case, we tell it to call the save method
217 | saveBtn.clicked.connect(self.save)
218 | # and then we add it to our save layout
219 | saveLayout.addWidget(saveBtn)
220 |
221 | # Now we'll set up the list of all our items
222 | # The size is for the size of the icons we will display
223 | size = 64
224 | # First we create a list widget, this will list all the items we give it
225 | self.listWidget = QtWidgets.QListWidget()
226 | # We want the list widget to be in IconMode like a gallery so we set it to a mode
227 | self.listWidget.setViewMode(QtWidgets.QListWidget.IconMode)
228 | # We set the icon size of this list
229 | self.listWidget.setIconSize(QtCore.QSize(size, size))
230 | # then we set it to adjust its position when we resize the window
231 | self.listWidget.setResizeMode(QtWidgets.QListWidget.Adjust)
232 | # Finally we set the grid size to be just a little larger than our icons to store our text label too
233 | self.listWidget.setGridSize(QtCore.QSize(size+12, size+12))
234 | # And finally, finally, we add it to our main layout
235 | layout.addWidget(self.listWidget)
236 |
237 | # Now we need a layout to store our buttons
238 | # So first we create a widget to store this layout
239 | btnWidget = QtWidgets.QWidget()
240 | # We create another horizontal layout and tell it to apply to our btn widdget
241 | btnLayout = QtWidgets.QHBoxLayout(btnWidget)
242 | # And we add this widget to our main UI
243 | layout.addWidget(btnWidget)
244 |
245 | # Similar to above we create three buttons
246 | importBtn = QtWidgets.QPushButton('Import!')
247 | # And we connect it to the relevant functions
248 | importBtn.clicked.connect(self.load)
249 | # And finally we add them to the button layout
250 | btnLayout.addWidget(importBtn)
251 |
252 | refreshBtn = QtWidgets.QPushButton('Refresh')
253 | refreshBtn.clicked.connect(self.populate)
254 | btnLayout.addWidget(refreshBtn)
255 |
256 | closeBtn = QtWidgets.QPushButton('Close')
257 | closeBtn.clicked.connect(self.close)
258 | btnLayout.addWidget(closeBtn)
259 |
260 | # After all that, we'll populate our UI
261 | self.populate()
262 |
263 | def load(self):
264 | # We will ask the listWidget what our currentItem is
265 | currentItem = self.listWidget.currentItem()
266 |
267 | # If we don't have anything selected, it will tell us None is selected, so we can skip this method
268 | if not currentItem:
269 | return
270 |
271 | # We then get the text label of the current item. This will be the name of our control
272 | name = currentItem.text()
273 | # Then we tell our library to load it
274 | self.library.load(name)
275 |
276 | def save(self):
277 | # We start off by getting the name in the text field
278 | name = self.saveNameField.text()
279 |
280 | # If the name is not given, then we will not continue and we'll warn the user
281 | # The strip method will remove empty characters from the string, so that if the user entered spaces, it won't be valid
282 | if not name.strip():
283 | cmds.warning("You must give a name!")
284 | return
285 |
286 | # We use our library to save with the given name
287 | self.library.save(name)
288 | # Then we repopulate our UI with the new data
289 | self.populate()
290 | # And finally, lets remove the text in the name field so that they don't accidentally overwrite the file
291 | self.saveNameField.setText('')
292 |
293 | def populate(self):
294 | # This function will be used to populate the UI. Shocking. I know.
295 |
296 | # First lets clear all the items that are in the list to start fresh
297 | self.listWidget.clear()
298 |
299 | # Then we ask our library to find everything again in case things changed
300 | self.library.find()
301 |
302 | # Now we iterate through the dictionary
303 | # This is why I based our library on a dictionary, because it gives us all the nice tricks a dictionary has
304 | for name, info in self.library.items():
305 | # We create an item for the list widget and tell it to have our controller name as a label
306 | item = QtWidgets.QListWidgetItem(name)
307 |
308 | # We set its tooltip to be the info from the json
309 | # The pprint.pformat will format our dictionary nicely
310 | item.setToolTip(pprint.pformat(info))
311 |
312 | # Finally we check if there's a screenshot available
313 | screenshot = info.get('screenshot')
314 | # If there is, then we will load it
315 | if screenshot:
316 | # So first we make an icon with the path to our screenshot
317 | icon = QtGui.QIcon(screenshot)
318 | # then we set the icon onto our item
319 | item.setIcon(icon)
320 |
321 | # Finally we add our item to the list
322 | self.listWidget.addItem(item)
323 |
324 | # This is a convenience function to display our UI
325 | def showUI():
326 | # Create an instance of our UI
327 | ui = ControllerLibraryUI()
328 | # Show the UI
329 | ui.show()
330 | # Return the ui instance so people using this function can hold on to it
331 | return ui
332 |
--------------------------------------------------------------------------------
/gearCreator/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/gearCreator/__init__.py
--------------------------------------------------------------------------------
/gearCreator/gears1.py:
--------------------------------------------------------------------------------
1 | # The 'as' keyword lets us give a nickname of our choosing to the module we import.
2 | # The name given to an imported module is a namepsace and it helps keep our code tidy
3 | import maya.cmds as cmds
4 |
5 |
6 | # We create a function called createGear
7 | # the 'def' keyword is used to indicate we will create a function
8 | # 'createGear' will be the name of our function
9 | # the function has parameters inside ( ), with two: one for teeth and one for length. Each has a default value
10 | def createGear(teeth=10, length=0.3):
11 | """
12 | This function will create a gear with the given parameters
13 | Args:
14 | teeth: The number of teeth to create
15 | length: The length of the teeth
16 |
17 | Returns:
18 | A tuple of the transform, constructor and extrude node
19 | """
20 | # Above this is the docstring enclosed in """ """ that documents the function
21 | # This helps for people who want to use the function without needing to read the code
22 |
23 | # Teeth are every alternate face, so we double the number of teeth to get the number of spans required
24 | spans = teeth * 2
25 |
26 | # the polyPipe command will create a polygon pipe
27 | # the subdivisionsAxis will say how many divisions we'll have along it's length
28 | # It returns a list of [transform, constructor]
29 | # Instead of getting a list and then extracting it's members, we can directly expand it to variables like here
30 | # The transform is the name of the node created and the constructor is the node that creates the pipe and controls its parameters
31 | transform, constructor = cmds.polyPipe(subdivisionsAxis=spans)
32 |
33 | # We need to select all the faces that will become the teeth.
34 | # We use the range function to start at span times 2 because that's where the side faces start from
35 | # Then we continue until span times 3 because that's where it ends
36 | # The third optional parameter is how big the steps we will take are.
37 | # So we'll be taking 2 steps instead of 1. e.g. 0, 2, 4, 6, 8
38 | # This will return a list of numbers
39 | sideFaces = range(spans * 2, spans * 3, 2)
40 |
41 | # Now we need to clear the selection because we'll be adding each face to it
42 | cmds.select(clear=True)
43 |
44 | # We'll loop through all the faces in the list of sideFaces
45 | for face in sideFaces:
46 | # We'll add to the selection
47 | # The '%s.f[%s]' notation looks odd but it expands to something like pPipe1.f[20]
48 | # Which tells it to select the 20th face of the pPipe1 object
49 | # The %s notation means it is a placeholder for the value of the variables after the %
50 | cmds.select('%s.f[%s]' % (transform, face), add=True)
51 |
52 | # Now we extrude the selected faces by the given length
53 | # This gives us back the value of the extrude node inside a list
54 | extrude = cmds.polyExtrudeFacet(localTranslateZ=length)[0]
55 |
56 | # Finally we return a tuple of (transform, constructor, extrude)
57 | # A tuple is similar to a list but cannot be modified.
58 | # Notice that we don't need to provide the parenthesis that define a tuple, just adding the comma here will do it for us
59 |
60 | # Here the transform is our gear node, the constructor is the node that creates the original pipe and the extrude is the node that extrudes the faces
61 | return transform, constructor, extrude
62 |
63 |
64 | # We now create the changeTeeth function that will modify our constructor and extrude node to change the teeth we get
65 | def changeTeeth(constructor, extrude, teeth=10, length=0.3):
66 | """
67 | Change the number of teeth on a gear with a given number of teeth and a given length for the teeth.
68 | This will create a new extrude node.
69 | Args:
70 | constructor (str): the constructor node
71 | extrude (str): the extrude node
72 | teeth (int): the number of teeth to create
73 | length (float): the length of the teeth to create
74 | """
75 | # Just like before we calculate the number of spans required by duplicating the number of teeth.
76 | spans = teeth * 2
77 |
78 | # We then use the same polyPipe command we used to create the pipe to modify it, this time providing the edit=True parameter
79 | # This edit parameter tells it we want to modify its attributes instead of creating a new one
80 | cmds.polyPipe(constructor, edit=True,
81 | subdivisionsAxis=spans)
82 |
83 | # As we did when creating it we need to get a list of faces to extrude as teeth
84 | sideFaces = range(spans * 2, spans * 3, 2)
85 | faceNames = []
86 |
87 | # We need to get a list in the following format
88 | # [u'f[40]', u'f[42]', u'f[44]', u'f[46]', u'f[48]', u'f[50]', u'f[52]', u'f[54]', u'f[56]', u'f[58]']
89 |
90 | # So we'll loop through all the sidefaces
91 | for face in sideFaces:
92 | # And we'll use the string substitution to create the names
93 | # In this case, %s will be replaced by 'face' which is the number of our face
94 | faceName = 'f[%s]' % (face)
95 |
96 | # We'll add this to our list of faceNames
97 | faceNames.append(faceName)
98 |
99 | # Then we must modify the extrude's parameter for which components it affects.
100 | # This takes a few different arguments
101 |
102 | # The extrude node has an attribute called inputComponents
103 | # To change it we can use a simple setAttr call instead of recreating the extrude which can be expensive
104 | # The arguments to changing a list of components is slightly different than a simple setAttr
105 | # it is:
106 | # cmds.setAttr('extrudeNode.inputComponents', numberOfItems, item1, item2, item3, type='componentList')
107 | cmds.setAttr('%s.inputComponents' % (extrude),
108 | len(faceNames),
109 | *faceNames,
110 | type="componentList")
111 |
112 | # The *faces will be new to you.
113 | # It basically means to expand a list in place for arguments
114 | # so if the list has ['f[1]', 'f[2]'] etc, it will be expanded in the arguments to be like this
115 | # cmds.setAttr('extrudeNode.inputComponents', 2, 'f[1]', 'f[2]', type='componentList'
116 |
117 | cmds.polyExtrudeFacet(extrude, edit=True, ltz=length)
118 |
--------------------------------------------------------------------------------
/gearCreator/gears2.py:
--------------------------------------------------------------------------------
1 | """
2 | In this code sample, we'll convert the functions we created earlier to make a gear, into a class.
3 | A class is a python object that lets you contain functions that relate to a specific object easily
4 |
5 | It will be a little different then the one in the video, because I've added a few more methods, but it should be easy to follow along.
6 | """
7 | import maya.cmds as cmds
8 |
9 | # The class keyword creates a class, ours will be called Gear
10 | # We will base our class on the python 'object'
11 | # Object is the base for everything in python, and by basing off of the python 'object' we get all of its
12 | # attributes for free
13 | class Gear(object):
14 | """
15 | Classes can have docstrings too that will describe how to use it
16 |
17 | In this case you would use it like this
18 |
19 | # This creates an instance of the class.
20 | # Classes descrive an object, instances are the objects that they describe
21 | # For example an Animal class describes an animal, but a dog on the street would be an instance of an Animal
22 | gearA = Gear()
23 |
24 | gearA.create(teeth=20, length=0.2)
25 | gearA.changeTeeth(teeth=10, length=0.5)
26 | """
27 | # The __init__ function is something that classes use a lot
28 | # They get run whenever you initialize a new instance of a class
29 | # For example when you do this gearA = Gear() it will run the __init__
30 | # You can think of them as the entryway to a class that tells it how to set up
31 | #
32 | # Most functions inside a class will take a first parameter called self that tells it to refer to itself.
33 | # Kind of like how you need to know you are yourself, the self parameter tells an instance it is itself
34 | def __init__(self):
35 | # We will just use this __init__ to create placeholder variables on the class
36 | # Variables that start with self are set on the instance and can be accessed outside this function
37 | self.shape = None
38 | self.transform = None
39 | self.constructor = None
40 | self.extrude = None
41 |
42 | # Another thing, functions inside a class are called methods
43 | def create(self, teeth=10, length=0.3):
44 | # The logic here is the same as in the functional version of this
45 | spans = teeth * 2
46 |
47 | # We refer to the createPipe method with self because we want to know to call the method that is inside this class
48 | # Notice we aren't getting a variable back from this method
49 | # Because we will store the variables on the class, we don't have to pass around them from a return (though we can if we choose to)
50 | self.createPipe(spans)
51 |
52 | # Similarly we call self.makeTeeth which is a method inside this class
53 | self.makeTeeth(teeth=teeth, length=length)
54 |
55 | def createPipe(self, spans):
56 | # We set the transform and shape to the class variables
57 | self.transform, self.shape = cmds.polyPipe(subdivisionsAxis=spans)
58 |
59 | # I didn't like having to find the constructor from the extrude node
60 | # Lets just find it now and save it to the class because it won't change
61 | for node in cmds.listConnections('%s.inMesh' % self.transform):
62 | if cmds.objectType(node) == 'polyPipe':
63 | self.constructor = node
64 | break
65 |
66 | def makeTeeth(self, teeth=10, length=0.3):
67 | # The logic here is exactly the same as in the makeTeeth function we created
68 | cmds.select(clear=True)
69 | faces = self.getTeethFaces(teeth)
70 | for face in faces:
71 | cmds.select('%s.%s' % (self.transform, face), add=True)
72 |
73 | # Instead of returning a value, lets just store the extrude node onto the class as a class variable
74 | self.extrude = cmds.polyExtrudeFacet(localTranslateZ=length)[0]
75 | cmds.select(clear=True)
76 |
77 | def changeLength(self, length=0.3):
78 | # Because we stored the extrude node on the class, we can just get it directly
79 | # This way we don't need to be told what extrude node to change
80 | cmds.polyExtrudeFacet(self.extrude, edit=True, ltz=length)
81 |
82 | def changeTeeth(self, teeth=10, length=0.3):
83 | # we know what node the constructor is, so we can refer to it directly
84 | cmds.polyPipe(self.constructor, edit=True, sa=teeth * 2)
85 | # Then we can just call the makeTeeth directly
86 | self.modifyExtrude(teeth=teeth, length=length)
87 |
88 | def getTeethFaces(self, teeth):
89 | spans = teeth * 2
90 | sideFaces = range(spans * 2, spans * 3, 2)
91 |
92 | faces = []
93 | for face in sideFaces:
94 | # Similar to what we did earlier, but using %d instead of %s
95 | # In reality it doesn't matter, but here it means it will only accept a number
96 | faces.append('f[%d]' % face)
97 | return faces
98 |
99 |
100 | def modifyExtrude(self, teeth=10, length=0.3):
101 | faces = self.getTeethFaces(teeth)
102 |
103 | # The extrude node has an attribute called inputComponents
104 | # To change it we can use a simple setAttr call instead of recreating the extrude which can be expensive
105 | # The arguments to changing a list of components is slightly different than a simple setAttr
106 | # it is:
107 | # cmds.setAttr('extrudeNode.inputComponents', numberOfItems, item1, item2, item3, type='componentList')
108 | cmds.setAttr('%s.inputComponents' % self.extrude, len(faces), *faces, type='componentList')
109 |
110 | # The *faces will be new to you.
111 | # It basically means to expand a list in place for arguments
112 | # so if the list has ['f[1]', 'f[2]'] etc, it will be expanded in the arguments to be like this
113 | # cmds.setAttr('extrudeNode.inputComponents', 2, 'f[1]', 'f[2]', type='componentList'
114 |
115 | # Finally we modify the length
116 | self.changeLength(length)
117 |
118 |
--------------------------------------------------------------------------------
/introduction/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/introduction/__init__.py
--------------------------------------------------------------------------------
/introduction/helloCube.py:
--------------------------------------------------------------------------------
1 | # In this excercise we will create a simple cube!
2 | # Just a reminder that anything after a # is just a comment and will not be run
3 |
4 | # We need to import the commands (cmds for short) library from maya to be able to give Maya commands to run
5 | # The import statement lets us bring in other python libraries, called modules from python packages.
6 | # In this case we are bringing in the cmds module from the maya package
7 | from maya import cmds
8 |
9 | # We create a cube by giving maya the polyCube command
10 | # Maya will then give us back the cube's transform and its' shape.
11 | # We will store both in a variable called cube.
12 | # Variables are like nicknames we can give to objects in python, so that we don't need to know how to actually call it.
13 | # Sort of like that guy at work who always forgets my name and calls me Dave.
14 | cube = cmds.polyCube()
15 |
16 | # OKAY I LIED!
17 | # We've created the cube, but that's not exciting is it? Let's go further and make it ready to animate with a control.
18 |
19 | # If we print out the contents of the cube variable we see that it will contain a transform and a shape
20 | # You should see something like [u'pCube1', u'polyCube1']
21 | print cube
22 |
23 | # This is called a list, so we can say the type of cube is a list.
24 | # If you don't believe me, you can run this and it will say: