├── .gitignore ├── LICENSE ├── README.md ├── commandLine ├── __init__.py └── renamer.py ├── controllerLibrary ├── __init__.py └── controllerLibrary.py ├── gearCreator ├── __init__.py ├── gears1.py └── gears2.py ├── introduction ├── __init__.py ├── helloCube.py └── helloWorld.py ├── lightManager ├── __init__.py ├── lightManager.py └── lightManager2016Below.py ├── objectRenamer ├── __init__.py ├── renamer1.py └── renamer2.py ├── py3notes.md └── tweener ├── __init__.py ├── reusableUI.py └── tweener.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python For Maya: Artist Friendly Programming 2 | 3 | ## Table of Contents 4 | 5 | * [Course Outline](#course-outline) 6 | * [Software Being Used](#tools-that-will-be-used) 7 | * [Resources](#other-resources) 8 | * [Books](#books) 9 | * [Websites](#websites-and-blogs) 10 | 11 | ## Course Outline 12 | 13 | Code samples for people who take part in my Python for Maya course 14 | 15 | The course is available here 16 | 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: 25 | print type(cube) 26 | 27 | # Lists are , well, a list of objects. They can contain anything, even other lists. 28 | # In this case, the list contains the names of the transform and the shape of the cube. 29 | # We need to get just the transform, which is the first member of the list 30 | transform = cube[0] 31 | 32 | # While humans count lists from 1, computers count lists from 0. 33 | # It takes a while to get used to it, but you'll learn it quickly enough 34 | # So we've taken the first object in the list, with index of 0. 35 | # If you want the creation node instead, you can get the second item in the list, at index 1 36 | # As you can see the [] notation is used to get the item at that index 37 | creator = cube[1] 38 | 39 | # Now lets create a nurbs circle controller to parent the cube under 40 | # But I don't remember the command to create a circle! 41 | # If you create a nurbs circle manually, it will show you the circle command in the script editor 42 | # So we can deduce it will be the following 43 | circle = cmds.circle() 44 | 45 | # Similar to the cube, we've created a circle and got back a list of the circle transform and its' shape 46 | # We can see this by printing out the contents of circle 47 | print circle 48 | 49 | # We only need the transform so lets take just the transform like we did above 50 | # As you can see here, you can always repurpose a variable and it will now refer to the new thing we point it to. 51 | # Just like my coworker calls a few different people Dave, I can call a few different things circle 52 | circle = circle[0] 53 | 54 | # Okay so we have the circle transform (circle) and the cube's transform (transform) 55 | # Let's parent the cube under the circle 56 | # What you see here is that we can give commands more details on how to run. 57 | # In this case, we're telling the parent command to take the transform we found earlier from the cube 58 | # and then put it under the circle transform we found aboove 59 | # These are called positional arguments as their order is important 60 | cmds.parent(transform, circle) 61 | 62 | # Now that we can controle the cube with the circle, lets lock the cube's controls 63 | # We're going to set the attributes of the cube to locked 64 | # Maya uses the period symbol to show that attributes belong to an object. e.g. pCube1.transform 65 | # transform is a string and we can use the plus symbol to add another string to it 66 | # As you can see, strings can use either single or double quotes as long as you end them with the same 67 | # lock is an example of a keyword argument. Its' order is not important as you refer to the argument name directly. 68 | cmds.setAttr(transform+'.translate', lock=True) 69 | cmds.setAttr(transform+".rotate", lock=True) 70 | cmds.setAttr(transform+'.scale', lock=True) 71 | 72 | # Finally lets select the circle 73 | cmds.select(circle) 74 | 75 | # And there you have it, you've quickly learned how to prop things! 76 | -------------------------------------------------------------------------------- /introduction/helloWorld.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a very simple example of a python script. 3 | The print statement will display any thing after the word print. 4 | In this case, we are creating a "string" that says "Hello, World!" 5 | 6 | A string is a alphabets, numbers and/or symbols. 7 | 8 | This is a simple but important test, because it's one of the simplest ways to see if something is working. 9 | 10 | P.S. If you're wondering, everything inside these triple quotes will not be run, and is just my commentary 11 | """ 12 | # P.P.S Anything after a # is also a comment and will not be run 13 | 14 | print "Hello, World!" -------------------------------------------------------------------------------- /lightManager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/lightManager/__init__.py -------------------------------------------------------------------------------- /lightManager/lightManager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fair Warning: This will be the most complex example in the course using more advanced maya features alongside 3 | more advanced python features than previous examples. 4 | """ 5 | 6 | # First of all let me grab the Qt module because it has somethings I want that I don't need to use often 7 | import json 8 | import os 9 | 10 | import Qt 11 | 12 | # I will use the following modules more often, so let me import them directly 13 | import time 14 | from Qt import QtWidgets, QtCore, QtGui 15 | 16 | # This is the logging module 17 | # It is a much better way of logging output instead of using print statements 18 | import logging 19 | 20 | # We'll do a basic configuration of the loggers 21 | logging.basicConfig() 22 | 23 | # We want a logger specifically for this tool, so lets grab one so that we can control it on its own 24 | logger = logging.getLogger('LightingManager') 25 | 26 | # Loggers have different levels we can log to. 27 | # We can configure the current level to make it disable certain logs when we don't want it. 28 | logger.setLevel(logging.DEBUG) 29 | 30 | # Okay, so this is kind of messy but necessary at the moment. 31 | # 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 32 | # So I need to check the correct binding we're using under Qt.py 33 | # If you're specifically using a Qt binding, then just use the import that makes sense for you. I'll elaborate below 34 | if Qt.__binding__.startswith('PyQt'): 35 | # If we're using PyQt4 or PyQt5 we need to import sip 36 | logger.debug('Using sip') 37 | # So we import wrapInstance from sip and alias it to wrapInstance so that it's the same as the others 38 | from sip import wrapinstance as wrapInstance 39 | # Also PyQt uses pyqtSignal instead of Signal so we will import it and alias it to Signal 40 | from Qt.QtCore import pyqtSignal as Signal 41 | elif Qt.__binding__ == 'PySide': 42 | # If we're using PySide (Maya 2016 and earlier), we'll use shiboken instead 43 | logger.debug('Using shiboken') 44 | # Shiboken already uses the correct names for both wrapInstance and Signal so we just need to import them without aliasing them 45 | from shiboken import wrapInstance 46 | from Qt.QtCore import Signal 47 | else: 48 | # Finally, the only option left is PySide2(Maya 2017 and higher) which uses shiboken2 49 | logger.debug('Using shiboken2') 50 | # Again, this uses the correct naming so we just import without aliasing 51 | from shiboken2 import wrapInstance 52 | from Qt.QtCore import Signal 53 | 54 | # 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 55 | 56 | # This is the Maya API library for dealing with UIs 57 | # This is the extent of the internal Maya API that we will be using directly for this course. 58 | from maya import OpenMayaUI as omui 59 | 60 | # Then we plan to use pyMel instead of maya.cmds for this project 61 | # PyMel is like a layer above maya.cmds and the Maya API that bridges them together to make a more python like API 62 | # This is nicer than using cmds which was originally made for MEL and the API which was designed for C++ 63 | # That said, it has its shortcomings that I will cover in a video which is why I haven't covered it till now 64 | import pymel.core as pm 65 | 66 | # Finally from the functional tools library we import partial that will be useful for craeting temporary functions 67 | from functools import partial 68 | 69 | 70 | class LightWidget(QtWidgets.QWidget): 71 | """ 72 | Now on to the good stuff 73 | This is our Basic controller for controlling lights 74 | 75 | to display it, give it the name of a light like so 76 | 77 | ui = LightWidget('directionalLight1') 78 | ui.show() 79 | """ 80 | 81 | # This is our solo signal 82 | # We are creating our own signal for other Qt objects to connect to 83 | # Qt demands that we make the signal here so it knows what the class looks like 84 | onSolo = Signal(bool) 85 | 86 | def __init__(self, light): 87 | # Our init function takes the name of a light 88 | 89 | # We then call the init from QWidget to make sure that our object is initialized properly 90 | super(LightWidget, self).__init__() 91 | 92 | # If the light is a string, we want to convert it to a PyMel object to deal with it easier 93 | # The isInstance checks if it is of type basestring (which includes all the various string types) 94 | if isinstance(light, basestring): 95 | logger.debug('Converting node to a PyNode') 96 | light = pm.PyNode(light) 97 | 98 | # We might also get passed the transform instead of the light shape, either as a PyNode or a name. 99 | # So we'll check if it's a transform node and then get the shape 100 | if isinstance(light, pm.nodetypes.Transform): 101 | light = light.getShape() 102 | 103 | # Then we store the pyMel node on this class 104 | self.light = light 105 | 106 | # Finally we call the buildUI method 107 | self.buildUI() 108 | 109 | def buildUI(self): 110 | # We create a GridLayout 111 | # GridLayouts are very flexible and allow us to quickly position widgets in a grid 112 | layout = QtWidgets.QGridLayout(self) 113 | 114 | # We make a checkbox with the label of our Light node's transform 115 | # Here you can see why PyMel is useful. Rather than passing our light's name to other cmds functions to get its parent 116 | # we can simply just call a method of the light object itself. 117 | self.name = name = QtWidgets.QCheckBox(str(self.light.getTransform())) 118 | # Lets make sure its value is the same as the lights visibility 119 | # Again, instead of doing cmds.getAttr('%s.visibility' % self.light), this simplifies the code a lot 120 | name.setChecked(self.light.visibility.get()) 121 | # We connect the toggled signal from the checkbox to a lambda. It will be called anytime the checkbox value changes 122 | # A lambda is another name for an unnamed function that will be called later 123 | # It is the same as this piece of code 124 | # 125 | # def setLightVisibility(self, val): 126 | # self.light.visibility.set(val) 127 | # 128 | # I like using lambdas when the logic is very simple. If your logic is more complex, use a real function or method 129 | name.toggled.connect(lambda val: self.light.visibility.set(val)) 130 | # Finally we add it to the layout in position 0, 0 (row 0, column 0) 131 | layout.addWidget(name, 0, 0) 132 | 133 | # Now we need a button to solo the light 134 | solo = QtWidgets.QPushButton('Solo') 135 | # Buttons can also be checkable, in that when you click them they will stay pressed till you unpress them 136 | solo.setCheckable(True) 137 | # Finally we connect the toggled value of the button to another lambda 138 | # This lambda will in turn tell our custom onSolo signal to emit with the same value it receive 139 | # This is the same as this piece of code 140 | # 141 | # def emitSoloSignal(self, value): 142 | # self.onSolo.emit(value) 143 | # 144 | # Again, for a simple one line function that we never use again, a lambda is a good fit 145 | solo.toggled.connect(lambda val: self.onSolo.emit(val)) 146 | # Then we add it to the grid layout in position (row 0, column 1) 147 | layout.addWidget(solo, 0, 1) 148 | 149 | # This will be our button to delete the light 150 | delete = QtWidgets.QPushButton('X') 151 | # The delete Light function is a little more complex so we will make it a real method and connect to it 152 | delete.clicked.connect(self.deleteLight) 153 | # We set the maximum width to 10, so that it's not super wide 154 | delete.setMaximumWidth(10) 155 | # Finally we add it to the same row, but the next column over 156 | layout.addWidget(delete, 0, 2) 157 | 158 | # We want a slider that can control the intensity of the light 159 | # We tell it that we want it to be horizontal by passing it the Qt value for Horizontal 160 | intensity = QtWidgets.QSlider(QtCore.Qt.Horizontal) 161 | # We set the minimum and maximum value of the slider 162 | intensity.setMinimum(1) 163 | intensity.setMaximum(1000) 164 | # Then we set its current value based of the intensity of the light itself 165 | intensity.setValue(self.light.intensity.get()) 166 | # We then connect its value changed signal to another lambda that sets the lights intensity 167 | intensity.valueChanged.connect(lambda val: self.light.intensity.set(val)) 168 | # Finally we add it to the grid, on the next row down. 169 | # If you notice this takes two extra variables, which tell it how many rows and columns to occupy 170 | # So we are adding it to row 1, column 2 and telling it to take 1 row and 2 columns of space 171 | # If you don't provide the last two arguments, they default to 1 each 172 | layout.addWidget(intensity, 1, 0, 1, 2) 173 | 174 | # This will be our button to display the color of the light 175 | self.colorBtn = QtWidgets.QPushButton() 176 | # We set the width and height of the button to our liking 177 | self.colorBtn.setMaximumWidth(20) 178 | self.colorBtn.setMaximumHeight(20) 179 | # Finally we call a method to sat the buttons color based on the lights current color 180 | self.setButtonColor() 181 | # We then connect it to our setColor method, again something too complex to be a lambda 182 | self.colorBtn.clicked.connect(self.setColor) 183 | # Finally we add it to the grid again, at row 1, column 2 with the default sizing 184 | layout.addWidget(self.colorBtn, 1, 2) 185 | 186 | # Now this is a weird Qt thing where we tell it the kind of sizing we want it respect 187 | # We are saying that the widget should never be larger than the maximum space it needs 188 | self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 189 | 190 | def disableLight(self, val): 191 | # This function takes a value, converts it to bool and then sets our checkbox to that value 192 | self.name.setChecked(not bool(val)) 193 | 194 | def deleteLight(self): 195 | # When we delete the light, we need to also delete our widget 196 | # So lets set our parent to Nothing. This will remove it from the manager UI and tells Qt to stop holding onto it 197 | self.setParent(None) 198 | # There is a period of time before Qt deletes it after we tell it to remove it 199 | # So lets mark its visibility to False 200 | self.setVisible(False) 201 | # Then we tell instruct it to delete it later just in case it hasn't gotten the hint yet 202 | self.deleteLater() 203 | 204 | # 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 205 | # We use the light's transform to make sure we are deleting at the transform level and not just the shape under it 206 | pm.delete(self.light.getTransform()) 207 | 208 | def setColor(self): 209 | # First of all we get the color values from the light. This will be a list of 3 floats 210 | lightColor = self.light.color.get() 211 | # Then we provide this to the maya's color editor which gives us back the color the user specified 212 | color = pm.colorEditor(rgbValue=lightColor) 213 | 214 | # Annoyingly, it gives us back a string instead of a list of numbers. 215 | # So we split the string, and then convert it to floats 216 | r, g, b, a = [float(c) for c in color.split()] 217 | 218 | # We then use the r,g,b to set the colors on the light and the button 219 | color = (r, g, b) 220 | self.light.color.set(color) 221 | self.setButtonColor(color) 222 | 223 | def setButtonColor(self, color=None): 224 | # This function sets the color on the color picker button 225 | # If no color is provided, we get the color from the light 226 | if not color: 227 | # We use pymels methods to query the value 228 | color = self.light.color.get() 229 | 230 | # We make sure that any provided color is a list of 3 items 231 | # Assert is a one liner that is similar to this piece of code: 232 | # 233 | # if not len(color) == 3: 234 | # raise Exception("You must provide a list of 3 colors") 235 | # 236 | # It is generally useful for validating inputs with simple checks 237 | assert len(color) == 3, "You must provide a list of 3 colors" 238 | 239 | # Finally everything gives us the r,g,b in normalized floats from 0 to 1 240 | # Qt expects it in integer values from 0 to 255 241 | # So we multiply the members of color by 255 to get the correct number 242 | r, g, b = [c * 255 for c in color] 243 | 244 | # Qt lets us style objects using CSS similar to in websites 245 | # So we give it a CSS string with the correct r,g,b values and a full alpha 246 | self.colorBtn.setStyleSheet('background-color: rgba(%s, %s, %s, 1.0);' % (r, g, b)) 247 | 248 | 249 | class LightingManager(QtWidgets.QWidget): 250 | """ 251 | This is the main lighting manager. 252 | To call it we just do 253 | 254 | LightingManager(dock=True) and it will display docked, otherwise dock=False will display it as a window 255 | 256 | """ 257 | 258 | # This is a dictionary of Light types to use for the Manager. 259 | # The Key is the name that will be displayed in the UI 260 | # The Value is the function that will be called 261 | lightTypes = { 262 | "Point Light": pm.pointLight, 263 | "Spot Light": pm.spotLight, 264 | # This is our first exposure to partial 265 | # Partial is like a lambda, and in most cases are identical. 266 | # The difference is lambdas get their values when they run, partials get their values when you create it 267 | # In this case, we are saying make a partial function to call pm.shadingNode and everything else will be arguments to it 268 | # This is the same as 269 | # 270 | # def createAreaLight(self): 271 | # pm.shadingNode('areaLight', asLight=True) 272 | # 273 | # But it can be convenient to just use a partial rather than making functions for everything 274 | "Area Light": partial(pm.shadingNode, 'areaLight', asLight=True), 275 | "Directional Light": pm.directionalLight, 276 | "Volume Light": partial(pm.shadingNode, 'volumeLight', asLight=True) 277 | } 278 | 279 | def __init__(self, dock=False): 280 | # So first we check if we want this to be able to dock 281 | if dock: 282 | # If we should be able to dock, then we'll use this function to get the dock 283 | parent = getDock() 284 | else: 285 | # Otherwise, lets remove all instances of the dock incase it's already docked 286 | deleteDock() 287 | # Then if we have a UI called lightingManager, we'll delete it so that we can only have one instance of this 288 | # A try except is a very important part of programming when we don't want an error to stop our code 289 | # We first try to do something and if we fail, then we do something else. 290 | try: 291 | pm.deleteUI('lightingManager') 292 | except: 293 | logger.debug('No previous UI exists') 294 | 295 | # Then we create a new dialog and give it the main maya window as its parent 296 | # we also store it as the parent for our current UI to be put inside 297 | parent = QtWidgets.QDialog(parent=getMayaMainWindow()) 298 | # We set its name so that we can find and delete it later 299 | parent.setObjectName('lightingManager') 300 | # Then we set the title 301 | parent.setWindowTitle('Lighting Manager') 302 | 303 | # Finally we give it a layout 304 | dlgLayout = QtWidgets.QVBoxLayout(parent) 305 | 306 | # Now we are on to our actual widget 307 | # We've figured out our parent, so lets send that to the QWidgets initialization method 308 | super(LightingManager, self).__init__(parent=parent) 309 | 310 | # We call our buildUI method to construct our UI 311 | self.buildUI() 312 | 313 | # Now we can tell it to populate with widgets for every light 314 | self.populate() 315 | 316 | # We then add ourself to our parents layout 317 | self.parent().layout().addWidget(self) 318 | 319 | # Finally if we're not docked, then we show our parent 320 | if not dock: 321 | parent.show() 322 | 323 | def buildUI(self): 324 | # Like in the LightWidget we show our 325 | layout = QtWidgets.QGridLayout(self) 326 | 327 | # We create a combobox 328 | # Comboboxes are essentially dropdown selectionwidgets 329 | self.lightTypeCB = QtWidgets.QComboBox() 330 | # We populate it with the items in our lightTypes dictionary 331 | # I like to have my items alphabetically so I sort it to begin with 332 | for lightType in sorted(self.lightTypes): 333 | # We add the option to the combobox 334 | self.lightTypeCB.addItem(lightType) 335 | # Finally we add it to the layout in row 0, column 0 336 | # We tell it take 1 row and two columns worth of space 337 | layout.addWidget(self.lightTypeCB, 0, 0, 1, 2) 338 | 339 | # We create a button to create the chosen lights 340 | createBtn = QtWidgets.QPushButton('Create') 341 | # We connect the button so it calls the createLight method when its clicked 342 | createBtn.clicked.connect(self.createLight) 343 | # We add it to the layout in row 0, column 2 344 | layout.addWidget(createBtn, 0, 2) 345 | 346 | # We want to put all the LightWidgets inside a scrolling container 347 | # We first need a container widget 348 | scrollWidget = QtWidgets.QWidget() 349 | # We want to make sure this widget only tries to be the maximum size of its contents 350 | scrollWidget.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 351 | # Then we give it a vertical layout because we want everything arranged vertically 352 | self.scrollLayout = QtWidgets.QVBoxLayout(scrollWidget) 353 | 354 | # Finally we create a scrollArea that will be in charge of scrolling its contents 355 | scrollArea = QtWidgets.QScrollArea() 356 | # Make sure it's resizable so it resizes as the UI grows or shrinks 357 | scrollArea.setWidgetResizable(True) 358 | # Then we set it to use our container widget to scroll 359 | scrollArea.setWidget(scrollWidget) 360 | # Then we add this scrollArea to the main layout, at row 1, column 0 361 | # We tell it to take 1 row and 3 columns of space 362 | layout.addWidget(scrollArea, 1, 0, 1, 3) 363 | 364 | # We add the save button to save our lights 365 | saveBtn = QtWidgets.QPushButton('Save') 366 | # When clicked it will call the saveLights method 367 | saveBtn.clicked.connect(self.saveLights) 368 | # We add it to row 2, column 0 369 | layout.addWidget(saveBtn, 2, 0) 370 | 371 | # We also add an import button to import our lights 372 | importBtn = QtWidgets.QPushButton('Import') 373 | # When clicked it will call the importLights method 374 | importBtn.clicked.connect(self.importLights) 375 | # We add it to row 2, column 1 376 | layout.addWidget(importBtn, 2, 1) 377 | 378 | # We need a refresh button to manually force the UI to refresh on changes 379 | refreshBtn = QtWidgets.QPushButton('Refresh') 380 | # We'll connect this to the refresh method 381 | refreshBtn.clicked.connect(self.refresh) 382 | # Finally we add it to the layout at row 2, column 2 383 | layout.addWidget(refreshBtn, 2, 2) 384 | 385 | def refresh(self): 386 | # This is one of the rare times I use a while loop 387 | # It could be done in a for loop, but I want to show you how a while loop would look 388 | 389 | # We say that while the scrollLayout.count() gives us any Truth-y value we will run the logic 390 | # count() tells us how many children it has 391 | while self.scrollLayout.count(): 392 | # We take the first child of the layout, and ask for the associated widget 393 | # Taking the child, means that it is no longer under the care of its parent 394 | widget = self.scrollLayout.takeAt(0).widget() 395 | # Some objects don't have widgets, so we'll only run this for objects with a widget 396 | if widget: 397 | # We set the visibility to False because there is a period where it will still be alive 398 | widget.setVisible(False) 399 | # Then we tell it to kill the widget when it can 400 | widget.deleteLater() 401 | 402 | # Finally we tell it to populate again 403 | self.populate() 404 | 405 | def populate(self): 406 | # We list all the existing lights in the scene by type of the lights 407 | for light in pm.ls(type=["areaLight", "spotLight", "pointLight", "directionalLight", "volumeLight"]): 408 | # PyMel gives us back a PyNode for each object it lists 409 | # We will pass this to the addLight method that will create the widget for it 410 | self.addLight(light) 411 | 412 | def saveLights(self): 413 | # We'll now save the lights down to a JSON file that can be shared as a preset 414 | 415 | # The properties dictionary will hold all the light properties to save down 416 | properties = {} 417 | 418 | # First lets get all the light widgets that exist in our manager 419 | for lightWidget in self.findChildren(LightWidget): 420 | # For each widget we can get its' light object 421 | light = lightWidget.light 422 | 423 | # Then we need to get its transform node 424 | transform = light.getTransform() 425 | 426 | # Finally we add it to the dictionary. 427 | # The key will be the name of the transform which we get by converting the node to a string 428 | # Then we simply query the attributes of the light that we want to save down 429 | properties[str(transform)] = { 430 | 'translate': list(transform.translate.get()), 431 | 'rotation': list(transform.rotate.get()), 432 | 'lightType': pm.objectType(light), 433 | 'intensity': light.intensity.get(), 434 | 'color': light.color.get() 435 | } 436 | 437 | # We fetch the light manager directory to save in 438 | directory = self.getDirectory() 439 | 440 | # We then construct the name of the lightFile to save 441 | # We'll be using time.strftime to construct a name using the current time 442 | # %m%d will give 0701 for July 1st (month and day) 443 | # so we'd end up with a name like lightFile_0701.json stored in our directory 444 | lightFile = os.path.join(directory, 'lightFile_%s.json' % time.strftime('%m%d')) 445 | 446 | # Next we open the file to write 447 | with open(lightFile, 'w') as f: 448 | # Then we use json to write out our file to this location 449 | json.dump(properties, f, indent=4) 450 | 451 | # A helpful logger call tells us where the file was saved to. 452 | logger.info('Saving file to %s' % lightFile) 453 | 454 | def getDirectory(self): 455 | # The getDirectory method will give us back the name of our library directory and create it if it doesn't exist 456 | directory = os.path.join(pm.internalVar(userAppDir=True), 'lightManager') 457 | if not os.path.exists(directory): 458 | os.mkdir(directory) 459 | return directory 460 | 461 | def importLights(self): 462 | # This function goes over importing the lights back in. 463 | # We first find the directory 464 | directory = self.getDirectory() 465 | 466 | # Then we use the QFileDialog to open a file browser so we can select the json file to import 467 | # We give it self as the part, a name for the browser and tell it which directory to open to 468 | fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Light Browser", directory) 469 | 470 | # Next we open the fileName in read mode 471 | with open(fileName[0], 'r') as f: 472 | # Then we use json to load the file into a dictionary 473 | properties = json.load(f) 474 | 475 | # We loop through the keys and values of this dictionary using properties.items() 476 | for light, info in properties.items(): 477 | 478 | # We find the light type from the info 479 | lightType = info.get('lightType') 480 | 481 | # Then for each of the light types we support, we check if they match the light type 482 | for lt in self.lightTypes: 483 | # But the light type of a Point Light is pointLight, so we convert Point Light to pointLight and then compare 484 | if ('%sLight' % lt.split()[0].lower()) == lightType: 485 | # If we found a match, then we break out 486 | break 487 | else: 488 | # For Loops also have an else statement. This only runs when the loop has not been broken out of 489 | # We assume if we haven't broken out of the loop, then we haven't found the light type 490 | # If that's the case, we just notify the user and continue on to the next light 491 | logger.info('Cannot find a corresponding light type for %s (%s)' % (light, lightType)) 492 | continue 493 | 494 | # we can reuse variable from the loop, in this case lt was the light type. 495 | # We use this to create a light 496 | light = self.createLight(lightType=lt) 497 | 498 | # then we set the parameters on the light itself 499 | light.intensity.set(info.get('intensity')) 500 | light.color.set(info.get('color')) 501 | 502 | # Then we get the transform of the light to set its parameters 503 | transform = light.getTransform() 504 | transform.translate.set(info.get('translate')) 505 | transform.rotate.set(info.get('rotation')) 506 | 507 | # After that's done, we call the populate method to refresh our interface 508 | self.populate() 509 | 510 | def createLight(self, lightType=None, add=True): 511 | # This function creates lights. Duh. 512 | # First we get the text of the combobox if we haven;t been given a light 513 | if not lightType: 514 | lightType = self.lightTypeCB.currentText() 515 | 516 | # Then we look up the lightTypes dictionary to find the function to call 517 | func = self.lightTypes[lightType] 518 | 519 | # All our functions are pymel functions so they'll return a pymel object 520 | light = func() 521 | # We wil pass this to the addLight method if the method has been told to add it 522 | if add: 523 | self.addLight(light) 524 | 525 | return light 526 | 527 | def addLight(self, light): 528 | # This will create a LightWidget for the given light and add it to the UI 529 | # First we create the LightWidget 530 | widget = LightWidget(light) 531 | 532 | # Then we connect the onSolo signal from the widget to our isolate method 533 | widget.onSolo.connect(self.isolate) 534 | # Finally we add it to the scrollLayout 535 | self.scrollLayout.addWidget(widget) 536 | 537 | def isolate(self, val): 538 | # This function will isolate a single light 539 | # First we find all our children who are LightWidgets 540 | lightWidgets = self.findChildren(LightWidget) 541 | # We'll loop through the list to perform our logic 542 | for widget in lightWidgets: 543 | # Every signal lets us know who sent it that we can query with sender() 544 | # So for every widget we check if its the sender 545 | if widget != self.sender(): 546 | # If it's not the widget, we'll disable it 547 | widget.disableLight(val) 548 | 549 | 550 | def getMayaMainWindow(): 551 | """ 552 | Since Maya is Qt, we can parent our UIs to it. 553 | This means that we don't have to manage our UI and can leave it to Maya. 554 | 555 | Returns: 556 | QtWidgets.QMainWindow: The Maya MainWindow 557 | """ 558 | # We use the OpenMayaUI API to get a reference to Maya's MainWindow 559 | win = omui.MQtUtil_mainWindow() 560 | # Then we can use the wrapInstance method to convert it to something python can understand 561 | # In this case, we're converting it to a QMainWindow 562 | ptr = wrapInstance(long(win), QtWidgets.QMainWindow) 563 | # Finally we return this to whoever wants it 564 | return ptr 565 | 566 | 567 | def getDock(name='LightingManagerDock'): 568 | """ 569 | This function creates a dock with the given name. 570 | It's an example of how we can mix Maya's UI elements with Qt elements 571 | Args: 572 | name: The name of the dock to create 573 | 574 | Returns: 575 | QtWidget.QWidget: The dock's widget 576 | """ 577 | # First lets delete any conflicting docks 578 | deleteDock(name) 579 | # Then we create a workspaceControl dock using Maya's UI tools 580 | # This gives us back the name of the dock created 581 | ctrl = pm.workspaceControl(name, dockToMainWindow=('right', 1), label="Lighting Manager") 582 | 583 | # We can use the OpenMayaUI API to get the actual Qt widget associated with the name 584 | qtCtrl = omui.MQtUtil_findControl(ctrl) 585 | 586 | # Finally we use wrapInstance to convert it to something Python can understand, in this case a QWidget 587 | ptr = wrapInstance(long(qtCtrl), QtWidgets.QWidget) 588 | 589 | # And we return that QWidget back to whoever wants it. 590 | return ptr 591 | 592 | 593 | def deleteDock(name='LightingManagerDock'): 594 | """ 595 | A simple function to delete the given dock 596 | Args: 597 | name: the name of the dock 598 | """ 599 | # We use the workspaceControl to see if the dock exists 600 | if pm.workspaceControl(name, query=True, exists=True): 601 | # If it does we delete it 602 | pm.deleteUI(name) 603 | -------------------------------------------------------------------------------- /lightManager/lightManager2016Below.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the same as the lightManager code but for Maya 2016 and Below which use the dockControl instead of the workspaceControl. 3 | The main changes are in the __init__ method of the LightManager class, the getDock function and the deleteDock function 4 | 5 | Fair Warning: This will be the most complex example in the course using more advanced maya features alongside 6 | more advanced python features than previous examples. 7 | """ 8 | 9 | # First of all let me grab the Qt module because it has somethings I want that I don't need to use often 10 | import json 11 | import os 12 | 13 | import Qt 14 | 15 | # I will use the following modules more often, so let me import them directly 16 | import time 17 | from Qt import QtWidgets, QtCore, QtGui 18 | 19 | # This is the logging module 20 | # It is a much better way of logging output instead of using print statements 21 | import logging 22 | 23 | # We'll do a basic configuration of the loggers 24 | logging.basicConfig() 25 | 26 | # We want a logger specifically for this tool, so lets grab one so that we can control it on its own 27 | logger = logging.getLogger('LightingManager') 28 | 29 | # Loggers have different levels we can log to. 30 | # We can configure the current level to make it disable certain logs when we don't want it. 31 | logger.setLevel(logging.DEBUG) 32 | 33 | # Okay, so this is kind of messy but necessary at the moment. 34 | # 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 35 | # So I need to check the correct binding we're using under Qt.py 36 | # If you're specifically using a Qt binding, then just use the import that makes sense for you. I'll elaborate below 37 | if Qt.__binding__.startswith('PyQt'): 38 | # If we're using PyQt4 or PyQt5 we need to import sip 39 | logger.debug('Using sip') 40 | # So we import wrapInstance from sip and alias it to wrapInstance so that it's the same as the others 41 | from sip import wrapinstance as wrapInstance 42 | # Also PyQt uses pyqtSignal instead of Signal so we will import it and alias it to Signal 43 | from Qt.QtCore import pyqtSignal as Signal 44 | elif Qt.__binding__ == 'PySide': 45 | # If we're using PySide (Maya 2016 and earlier), we'll use shiboken instead 46 | logger.debug('Using shiboken') 47 | # Shiboken already uses the correct names for both wrapInstance and Signal so we just need to import them without aliasing them 48 | from shiboken import wrapInstance 49 | from Qt.QtCore import Signal 50 | else: 51 | # Finally, the only option left is PySide2(Maya 2017 and higher) which uses shiboken2 52 | logger.debug('Using shiboken2') 53 | # Again, this uses the correct naming so we just import without aliasing 54 | from shiboken2 import wrapInstance 55 | from Qt.QtCore import Signal 56 | 57 | # 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 58 | 59 | # This is the Maya API library for dealing with UIs 60 | # This is the extent of the internal Maya API that we will be using directly for this course. 61 | from maya import OpenMayaUI as omui 62 | 63 | # Then we plan to use pyMel instead of maya.cmds for this project 64 | # PyMel is like a layer above maya.cmds and the Maya API that bridges them together to make a more python like API 65 | # This is nicer than using cmds which was originally made for MEL and the API which was designed for C++ 66 | # That said, it has its shortcomings that I will cover in a video which is why I haven't covered it till now 67 | import pymel.core as pm 68 | 69 | # Finally from the functional tools library we import partial that will be useful for craeting temporary functions 70 | from functools import partial 71 | 72 | 73 | class LightWidget(QtWidgets.QWidget): 74 | """ 75 | Now on to the good stuff 76 | This is our Basic controller for controlling lights 77 | 78 | to display it, give it the name of a light like so 79 | 80 | ui = LightWidget('directionalLight1') 81 | ui.show() 82 | """ 83 | 84 | # This is our solo signal 85 | # We are creating our own signal for other Qt objects to connect to 86 | # Qt demands that we make the signal here so it knows what the class looks like 87 | onSolo = Signal(bool) 88 | 89 | def __init__(self, light): 90 | # Our init function takes the name of a light 91 | 92 | # We then call the init from QWidget to make sure that our object is initialized properly 93 | super(LightWidget, self).__init__() 94 | 95 | # If the light is a string, we want to convert it to a PyMel object to deal with it easier 96 | # The isInstance checks if it is of type basestring (which includes all the various string types) 97 | if isinstance(light, basestring): 98 | logger.debug('Converting node to a PyNode') 99 | light = pm.PyNode(light) 100 | 101 | # We might also get passed the transform instead of the light shape, either as a PyNode or a name. 102 | # So we'll check if it's a transform node and then get the shape 103 | if isinstance(light, pm.nodetypes.Transform): 104 | light = light.getShape() 105 | 106 | # Then we store the pyMel node on this class 107 | self.light = light 108 | 109 | # Finally we call the buildUI method 110 | self.buildUI() 111 | 112 | def buildUI(self): 113 | # We create a GridLayout 114 | # GridLayouts are very flexible and allow us to quickly position widgets in a grid 115 | layout = QtWidgets.QGridLayout(self) 116 | 117 | # We make a checkbox with the label of our Light node's transform 118 | # Here you can see why PyMel is useful. Rather than passing our light's name to other cmds functions to get its parent 119 | # we can simply just call a method of the light object itself. 120 | self.name = name = QtWidgets.QCheckBox(str(self.light.getTransform())) 121 | # Lets make sure its value is the same as the lights visibility 122 | # Again, instead of doing cmds.getAttr('%s.visibility' % self.light), this simplifies the code a lot 123 | name.setChecked(self.light.visibility.get()) 124 | # We connect the toggled signal from the checkbox to a lambda. It will be called anytime the checkbox value changes 125 | # A lambda is another name for an unnamed function that will be called later 126 | # It is the same as this piece of code 127 | # 128 | # def setLightVisibility(self, val): 129 | # self.light.visibility.set(val) 130 | # 131 | # I like using lambdas when the logic is very simple. If your logic is more complex, use a real function or method 132 | name.toggled.connect(lambda val: self.light.visibility.set(val)) 133 | # Finally we add it to the layout in position 0, 0 (row 0, column 0) 134 | layout.addWidget(name, 0, 0) 135 | 136 | # Now we need a button to solo the light 137 | solo = QtWidgets.QPushButton('Solo') 138 | # Buttons can also be checkable, in that when you click them they will stay pressed till you unpress them 139 | solo.setCheckable(True) 140 | # Finally we connect the toggled value of the button to another lambda 141 | # This lambda will in turn tell our custom onSolo signal to emit with the same value it receive 142 | # This is the same as this piece of code 143 | # 144 | # def emitSoloSignal(self, value): 145 | # self.onSolo.emit(value) 146 | # 147 | # Again, for a simple one line function that we never use again, a lambda is a good fit 148 | solo.toggled.connect(lambda val: self.onSolo.emit(val)) 149 | # Then we add it to the grid layout in position (row 0, column 1) 150 | layout.addWidget(solo, 0, 1) 151 | 152 | # This will be our button to delete the light 153 | delete = QtWidgets.QPushButton('X') 154 | # The delete Light function is a little more complex so we will make it a real method and connect to it 155 | delete.clicked.connect(self.deleteLight) 156 | # We set the maximum width to 10, so that it's not super wide 157 | delete.setMaximumWidth(10) 158 | # Finally we add it to the same row, but the next column over 159 | layout.addWidget(delete, 0, 2) 160 | 161 | # We want a slider that can control the intensity of the light 162 | # We tell it that we want it to be horizontal by passing it the Qt value for Horizontal 163 | intensity = QtWidgets.QSlider(QtCore.Qt.Horizontal) 164 | # We set the minimum and maximum value of the slider 165 | intensity.setMinimum(1) 166 | intensity.setMaximum(1000) 167 | # Then we set its current value based of the intensity of the light itself 168 | intensity.setValue(self.light.intensity.get()) 169 | # We then connect its value changed signal to another lambda that sets the lights intensity 170 | intensity.valueChanged.connect(lambda val: self.light.intensity.set(val)) 171 | # Finally we add it to the grid, on the next row down. 172 | # If you notice this takes two extra variables, which tell it how many rows and columns to occupy 173 | # So we are adding it to row 1, column 2 and telling it to take 1 row and 2 columns of space 174 | # If you don't provide the last two arguments, they default to 1 each 175 | layout.addWidget(intensity, 1, 0, 1, 2) 176 | 177 | # This will be our button to display the color of the light 178 | self.colorBtn = QtWidgets.QPushButton() 179 | # We set the width and height of the button to our liking 180 | self.colorBtn.setMaximumWidth(20) 181 | self.colorBtn.setMaximumHeight(20) 182 | # Finally we call a method to sat the buttons color based on the lights current color 183 | self.setButtonColor() 184 | # We then connect it to our setColor method, again something too complex to be a lambda 185 | self.colorBtn.clicked.connect(self.setColor) 186 | # Finally we add it to the grid again, at row 1, column 2 with the default sizing 187 | layout.addWidget(self.colorBtn, 1, 2) 188 | 189 | # Now this is a weird Qt thing where we tell it the kind of sizing we want it respect 190 | # We are saying that the widget should never be larger than the maximum space it needs 191 | self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 192 | 193 | def disableLight(self, val): 194 | # This function takes a value, converts it to bool and then sets our checkbox to that value 195 | self.name.setChecked(not bool(val)) 196 | 197 | def deleteLight(self): 198 | # When we delete the light, we need to also delete our widget 199 | # So lets set our parent to Nothing. This will remove it from the manager UI and tells Qt to stop holding onto it 200 | self.setParent(None) 201 | # There is a period of time before Qt deletes it after we tell it to remove it 202 | # So lets mark its visibility to False 203 | self.setVisible(False) 204 | # Then we tell instruct it to delete it later just in case it hasn't gotten the hint yet 205 | self.deleteLater() 206 | 207 | # 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 208 | # We use the light's transform to make sure we are deleting at the transform level and not just the shape under it 209 | pm.delete(self.light.getTransform()) 210 | 211 | def setColor(self): 212 | # First of all we get the color values from the light. This will be a list of 3 floats 213 | lightColor = self.light.color.get() 214 | # Then we provide this to the maya's color editor which gives us back the color the user specified 215 | color = pm.colorEditor(rgbValue=lightColor) 216 | 217 | # Annoyingly, it gives us back a string instead of a list of numbers. 218 | # So we split the string, and then convert it to floats 219 | r, g, b, a = [float(c) for c in color.split()] 220 | 221 | # We then use the r,g,b to set the colors on the light and the button 222 | color = (r, g, b) 223 | self.light.color.set(color) 224 | self.setButtonColor(color) 225 | 226 | def setButtonColor(self, color=None): 227 | # This function sets the color on the color picker button 228 | # If no color is provided, we get the color from the light 229 | if not color: 230 | # We use pymels methods to query the value 231 | color = self.light.color.get() 232 | 233 | # We make sure that any provided color is a list of 3 items 234 | # Assert is a one liner that is similar to this piece of code: 235 | # 236 | # if not len(color) == 3: 237 | # raise Exception("You must provide a list of 3 colors") 238 | # 239 | # It is generally useful for validating inputs with simple checks 240 | assert len(color) == 3, "You must provide a list of 3 colors" 241 | 242 | # Finally everything gives us the r,g,b in normalized floats from 0 to 1 243 | # Qt expects it in integer values from 0 to 255 244 | # So we multiply the members of color by 255 to get the correct number 245 | r, g, b = [c * 255 for c in color] 246 | 247 | # Qt lets us style objects using CSS similar to in websites 248 | # So we give it a CSS string with the correct r,g,b values and a full alpha 249 | self.colorBtn.setStyleSheet('background-color: rgba(%s, %s, %s, 1.0);' % (r, g, b)) 250 | 251 | 252 | class LightingManager(QtWidgets.QWidget): 253 | """ 254 | This is the main lighting manager. 255 | To call it we just do 256 | 257 | LightingManager(dock=True) and it will display docked, otherwise dock=False will display it as a window 258 | 259 | """ 260 | 261 | # This is a dictionary of Light types to use for the Manager. 262 | # The Key is the name that will be displayed in the UI 263 | # The Value is the function that will be called 264 | lightTypes = { 265 | "Point Light": pm.pointLight, 266 | "Spot Light": pm.spotLight, 267 | # This is our first exposure to partial 268 | # Partial is like a lambda, and in most cases are identical. 269 | # The difference is lambdas get their values when they run, partials get their values when you create it 270 | # In this case, we are saying make a partial function to call pm.shadingNode and everything else will be arguments to it 271 | # This is the same as 272 | # 273 | # def createAreaLight(self): 274 | # pm.shadingNode('areaLight', asLight=True) 275 | # 276 | # But it can be convenient to just use a partial rather than making functions for everything 277 | "Area Light": partial(pm.shadingNode, 'areaLight', asLight=True), 278 | "Directional Light": pm.directionalLight, 279 | "Volume Light": partial(pm.shadingNode, 'volumeLight', asLight=True) 280 | } 281 | 282 | def __init__(self, dock=False): 283 | # First lets delete a dock if we have one so that we aren't creating more than we neec 284 | deleteDock() 285 | # Then if we have a UI called lightingManager, we'll delete it so that we can only have one instance of this 286 | # A try except is a very important part of programming when we don't want an error to stop our code 287 | # We first try to do something and if we fail, then we do something else. 288 | try: 289 | pm.deleteUI('lightingManager') 290 | except: 291 | logger.debug('No previous UI exists') 292 | # <=Maya2016: For Maya 2016 and below we always put it inside a QDialog and only dock at the end of this __init__ 293 | # Then we create a new dialog and give it the main maya window as its parent 294 | # we also store it as the parent for our current UI to be put inside 295 | parent = QtWidgets.QDialog(parent=getMayaMainWindow()) 296 | # We set its name so that we can find and delete it later 297 | # <=Maya2016: This also lets us attach the light manager to our dock control 298 | parent.setObjectName('lightingManager') 299 | # Then we set the title 300 | parent.setWindowTitle('Lighting Manager') 301 | 302 | # Finally we give it a layout 303 | dlgLayout = QtWidgets.QVBoxLayout(parent) 304 | 305 | # Now we are on to our actual widget 306 | # We've figured out our parent, so lets send that to the QWidgets initialization method 307 | super(LightingManager, self).__init__(parent=parent) 308 | 309 | # We call our buildUI method to construct our UI 310 | self.buildUI() 311 | 312 | # Now we can tell it to populate with widgets for every light 313 | self.populate() 314 | 315 | # We then add ourself to our parents layout 316 | self.parent().layout().addWidget(self) 317 | # Finally if we're not docked, then we show our parent 318 | parent.show() 319 | 320 | # <=Maya2016: For Maya 2016 and below we need to create the dock after we create our widget's parent window 321 | if dock: 322 | getDock() 323 | 324 | def buildUI(self): 325 | # Like in the LightWidget we show our 326 | layout = QtWidgets.QGridLayout(self) 327 | 328 | # We create a combobox 329 | # Comboboxes are essentially dropdown selectionwidgets 330 | self.lightTypeCB = QtWidgets.QComboBox() 331 | # We populate it with the items in our lightTypes dictionary 332 | # I like to have my items alphabetically so I sort it to begin with 333 | for lightType in sorted(self.lightTypes): 334 | # We add the option to the combobox 335 | self.lightTypeCB.addItem(lightType) 336 | # Finally we add it to the layout in row 0, column 0 337 | # We tell it take 1 row and two columns worth of space 338 | layout.addWidget(self.lightTypeCB, 0, 0, 1, 2) 339 | 340 | # We create a button to create the chosen lights 341 | createBtn = QtWidgets.QPushButton('Create') 342 | # We connect the button so it calls the createLight method when its clicked 343 | createBtn.clicked.connect(self.createLight) 344 | # We add it to the layout in row 0, column 2 345 | layout.addWidget(createBtn, 0, 2) 346 | 347 | # We want to put all the LightWidgets inside a scrolling container 348 | # We first need a container widget 349 | scrollWidget = QtWidgets.QWidget() 350 | # We want to make sure this widget only tries to be the maximum size of its contents 351 | scrollWidget.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 352 | # Then we give it a vertical layout because we want everything arranged vertically 353 | self.scrollLayout = QtWidgets.QVBoxLayout(scrollWidget) 354 | 355 | # Finally we create a scrollArea that will be in charge of scrolling its contents 356 | scrollArea = QtWidgets.QScrollArea() 357 | # Make sure it's resizable so it resizes as the UI grows or shrinks 358 | scrollArea.setWidgetResizable(True) 359 | # Then we set it to use our container widget to scroll 360 | scrollArea.setWidget(scrollWidget) 361 | # Then we add this scrollArea to the main layout, at row 1, column 0 362 | # We tell it to take 1 row and 3 columns of space 363 | layout.addWidget(scrollArea, 1, 0, 1, 3) 364 | 365 | # We add the save button to save our lights 366 | saveBtn = QtWidgets.QPushButton('Save') 367 | # When clicked it will call the saveLights method 368 | saveBtn.clicked.connect(self.saveLights) 369 | # We add it to row 2, column 0 370 | layout.addWidget(saveBtn, 2, 0) 371 | 372 | # We also add an import button to import our lights 373 | importBtn = QtWidgets.QPushButton('Import') 374 | # When clicked it will call the importLights method 375 | importBtn.clicked.connect(self.importLights) 376 | # We add it to row 2, column 1 377 | layout.addWidget(importBtn, 2, 1) 378 | 379 | # We need a refresh button to manually force the UI to refresh on changes 380 | refreshBtn = QtWidgets.QPushButton('Refresh') 381 | # We'll connect this to the refresh method 382 | refreshBtn.clicked.connect(self.refresh) 383 | # Finally we add it to the layout at row 2, column 2 384 | layout.addWidget(refreshBtn, 2, 2) 385 | 386 | def refresh(self): 387 | # This is one of the rare times I use a while loop 388 | # It could be done in a for loop, but I want to show you how a while loop would look 389 | 390 | # We say that while the scrollLayout.count() gives us any Truth-y value we will run the logic 391 | # count() tells us how many children it has 392 | while self.scrollLayout.count(): 393 | # We take the first child of the layout, and ask for the associated widget 394 | # Taking the child, means that it is no longer under the care of its parent 395 | widget = self.scrollLayout.takeAt(0).widget() 396 | # Some objects don't have widgets, so we'll only run this for objects with a widget 397 | if widget: 398 | # We set the visibility to False because there is a period where it will still be alive 399 | widget.setVisible(False) 400 | # Then we tell it to kill the widget when it can 401 | widget.deleteLater() 402 | 403 | # Finally we tell it to populate again 404 | self.populate() 405 | 406 | def populate(self): 407 | # We list all the existing lights in the scene by type of the lights 408 | for light in pm.ls(type=["areaLight", "spotLight", "pointLight", "directionalLight", "volumeLight"]): 409 | # PyMel gives us back a PyNode for each object it lists 410 | # We will pass this to the addLight method that will create the widget for it 411 | self.addLight(light) 412 | 413 | def saveLights(self): 414 | # We'll now save the lights down to a JSON file that can be shared as a preset 415 | 416 | # The properties dictionary will hold all the light properties to save down 417 | properties = {} 418 | 419 | # First lets get all the light widgets that exist in our manager 420 | for lightWidget in self.findChildren(LightWidget): 421 | # For each widget we can get its' light object 422 | light = lightWidget.light 423 | 424 | # Then we need to get its transform node 425 | transform = light.getTransform() 426 | 427 | # Finally we add it to the dictionary. 428 | # The key will be the name of the transform which we get by converting the node to a string 429 | # Then we simply query the attributes of the light that we want to save down 430 | properties[str(transform)] = { 431 | 'translate': list(transform.translate.get()), 432 | 'rotation': list(transform.rotate.get()), 433 | 'lightType': pm.objectType(light), 434 | 'intensity': light.intensity.get(), 435 | 'color': light.color.get() 436 | } 437 | 438 | # We fetch the light manager directory to save in 439 | directory = self.getDirectory() 440 | 441 | # We then construct the name of the lightFile to save 442 | # We'll be using time.strftime to construct a name using the current time 443 | # %m%d will give 0701 for July 1st (month and day) 444 | # so we'd end up with a name like lightFile_0701.json stored in our directory 445 | lightFile = os.path.join(directory, 'lightFile_%s.json' % time.strftime('%m%d')) 446 | 447 | # Next we open the file to write 448 | with open(lightFile, 'w') as f: 449 | # Then we use json to write out our file to this location 450 | json.dump(properties, f, indent=4) 451 | 452 | # A helpful logger call tells us where the file was saved to. 453 | logger.info('Saving file to %s' % lightFile) 454 | 455 | def getDirectory(self): 456 | # The getDirectory method will give us back the name of our library directory and create it if it doesn't exist 457 | directory = os.path.join(pm.internalVar(userAppDir=True), 'lightManager') 458 | if not os.path.exists(directory): 459 | os.mkdir(directory) 460 | return directory 461 | 462 | def importLights(self): 463 | # This function goes over importing the lights back in. 464 | # We first find the directory 465 | directory = self.getDirectory() 466 | 467 | # Then we use the QFileDialog to open a file browser so we can select the json file to import 468 | # We give it self as the part, a name for the browser and tell it which directory to open to 469 | fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Light Browser", directory) 470 | 471 | # Next we open the fileName in read mode 472 | with open(fileName[0], 'r') as f: 473 | # Then we use json to load the file into a dictionary 474 | properties = json.load(f) 475 | 476 | # We loop through the keys and values of this dictionary using properties.items() 477 | for light, info in properties.items(): 478 | 479 | # We find the light type from the info 480 | lightType = info.get('lightType') 481 | 482 | # Then for each of the light types we support, we check if they match the light type 483 | for lt in self.lightTypes: 484 | # But the light type of a Point Light is pointLight, so we convert Point Light to pointLight and then compare 485 | if ('%sLight' % lt.split()[0].lower()) == lightType: 486 | # If we found a match, then we break out 487 | break 488 | else: 489 | # For Loops also have an else statement. This only runs when the loop has not been broken out of 490 | # We assume if we haven't broken out of the loop, then we haven't found the light type 491 | # If that's the case, we just notify the user and continue on to the next light 492 | logger.info('Cannot find a corresponding light type for %s (%s)' % (light, lightType)) 493 | continue 494 | 495 | # we can reuse variable from the loop, in this case lt was the light type. 496 | # We use this to create a light 497 | light = self.createLight(lightType=lt) 498 | 499 | # then we set the parameters on the light itself 500 | light.intensity.set(info.get('intensity')) 501 | light.color.set(info.get('color')) 502 | 503 | # Then we get the transform of the light to set its parameters 504 | transform = light.getTransform() 505 | transform.translate.set(info.get('translate')) 506 | transform.rotate.set(info.get('rotation')) 507 | 508 | # After that's done, we call the populate method to refresh our interface 509 | self.populate() 510 | 511 | def createLight(self, lightType=None, add=True): 512 | # This function creates lights. Duh. 513 | # First we get the text of the combobox if we haven;t been given a light 514 | if not lightType: 515 | lightType = self.lightTypeCB.currentText() 516 | 517 | # Then we look up the lightTypes dictionary to find the function to call 518 | func = self.lightTypes[lightType] 519 | 520 | # All our functions are pymel functions so they'll return a pymel object 521 | light = func() 522 | # We wil pass this to the addLight method if the method has been told to add it 523 | if add: 524 | self.addLight(light) 525 | 526 | def addLight(self, light): 527 | # This will create a LightWidget for the given light and add it to the UI 528 | # First we create the LightWidget 529 | widget = LightWidget(light) 530 | 531 | # Then we connect the onSolo signal from the widget to our isolate method 532 | widget.onSolo.connect(self.isolate) 533 | # Finally we add it to the scrollLayout 534 | self.scrollLayout.addWidget(widget) 535 | 536 | def isolate(self, val): 537 | # This function will isolate a single light 538 | # First we find all our children who are LightWidgets 539 | lightWidgets = self.findChildren(LightWidget) 540 | # We'll loop through the list to perform our logic 541 | for widget in lightWidgets: 542 | # Every signal lets us know who sent it that we can query with sender() 543 | # So for every widget we check if its the sender 544 | if widget != self.sender(): 545 | # If it's not the widget, we'll disable it 546 | widget.disableLight(val) 547 | 548 | 549 | def getMayaMainWindow(): 550 | """ 551 | Since Maya is Qt, we can parent our UIs to it. 552 | This means that we don't have to manage our UI and can leave it to Maya. 553 | 554 | Returns: 555 | QtWidgets.QMainWindow: The Maya MainWindow 556 | """ 557 | # We use the OpenMayaUI API to get a reference to Maya's MainWindow 558 | win = omui.MQtUtil_mainWindow() 559 | # Then we can use the wrapInstance method to convert it to something python can understand 560 | # In this case, we're converting it to a QMainWindow 561 | ptr = wrapInstance(long(win), QtWidgets.QMainWindow) 562 | # Finally we return this to whoever wants it 563 | return ptr 564 | 565 | 566 | def getDock(name='LightingManagerDock'): 567 | """ 568 | This function creates a dock with the given name. 569 | It's an example of how we can mix Maya's UI elements with Qt elements 570 | Args: 571 | name: The name of the dock to create 572 | 573 | Returns: 574 | QtWidget.QWidget: The dock's widget 575 | """ 576 | # First lets delete any conflicting docks 577 | deleteDock(name) 578 | # Then we create a dockControl dock using Maya's UI tools 579 | # This gives us back the name of the dock created 580 | 581 | # <=Maya2016: In Maya 2016 and below, we just give our Light Managers object name to the dockControl. 582 | # You can see this name when we do self.setObjectName in the LightManagers __init__ method 583 | ctrl = pm.dockControl(name, area='right', content='lightingManager', allowedArea='all', label="Lighting Manager") 584 | 585 | # And then we return the control name 586 | return ctrl 587 | 588 | 589 | def deleteDock(name='LightingManagerDock'): 590 | """ 591 | A simple function to delete the given dock 592 | Args: 593 | name: the name of the dock 594 | """ 595 | # We use the dockControl to see if the dock exists 596 | if pm.dockControl(name, query=True, exists=True): 597 | # If it does we delete it 598 | pm.deleteUI(name) 599 | -------------------------------------------------------------------------------- /objectRenamer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/objectRenamer/__init__.py -------------------------------------------------------------------------------- /objectRenamer/renamer1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a utility to add suffixes to objects based on the object type. 3 | This helps keep our scene tidy and organized. 4 | """ 5 | 6 | # First we import the commands library from Maya so we can issue commands to Maya 7 | from maya import cmds 8 | 9 | # First we want to check if we have something selected 10 | # We will ask maya to list (ls) everything that is selected (selection=True) 11 | # We should also get the full path to the object instead of just the name of the object 12 | selection = cmds.ls(selection=True, long=True) 13 | 14 | # This will give us back a list of the paths of all the objects we have selected 15 | print selection 16 | 17 | # Now we need to check if we might not have something selected 18 | # We will check the length of the selection list to see if it is 0 (empty) in which case we will just list everything 19 | 20 | # This is an If statement, where we are saying: "If it is true that the length is zero, then do the part after that" 21 | # Unlike other programming languages, python uses indentation to show when something is inside a block of code 22 | # For example, the indented line after the if statement is inside the if statement 23 | if len(selection) == 0: 24 | # This is inside the if statement 25 | selection = cmds.ls(long=True, dag=True) 26 | # This is outside it. 27 | 28 | # We can run into an issue where we'll rename a parent before a child, causing the path to the child to change. 29 | # To work around that we'll sort the list by length 30 | 31 | # So this can be a little confusing 32 | # Just like Maya has attributes on objects with the period, so does python. 33 | # The list object has a sort method on it, that will sort itself. 34 | # Usually this will sort it alphabetically, but we want to sort by length and to reverse it to put the longest first 35 | selection.sort(key=len, reverse=True) 36 | 37 | 38 | # Now we need to loop through the objects and rename them 39 | # We'll use a for loop for this 40 | # We step through all the items one by one in the selection object, assign it to a variable (obj) and run the logic below it 41 | for obj in selection: 42 | # For each object in the selection list, run the following logic 43 | 44 | # The name will be something like grandparent|parent|child 45 | # We just want the child part of the name, so we split using the | character which gives us a list of ['grandparent', 'parent', 'child'] 46 | # We need to get the last item in the list, so we use [-1]. This means we backwards through the list and pick the next item, which would therefore be the last item 47 | shortName = obj.split('|')[-1] 48 | 49 | print "Before rename: ", shortName 50 | 51 | # If the object is a transform, then we should check if it has a shape below it 52 | children = cmds.listRelatives(obj, children=True) or [] 53 | 54 | # We will only do this if there is one child 55 | if len(children) == 1: 56 | # We will take the first child 57 | child = children[0] 58 | objType = cmds.objectType(child) 59 | else: 60 | # Now we get the object type of the current object 61 | objType = cmds.objectType(obj) 62 | 63 | # We use a bunch of if statements to find the suffix we want to add 64 | # An if statement can have three parts. the if, elif and else 65 | if objType == "mesh": 66 | suffix = 'geo' 67 | elif objType == "joint": 68 | suffix = 'jnt' 69 | elif objType == 'camera': 70 | # In the case of the camera, we will say to continue. 71 | # Continue means that we will continue on to the next item in the list and skip the rest of the logic for this one 72 | print "Skipping camera" 73 | continue 74 | else: 75 | suffix = 'grp' 76 | 77 | # Now we need to construct the new name 78 | newName = shortName+"_"+suffix 79 | 80 | # Now tell it to rename the obj to the new name with the suffix 81 | cmds.rename(obj, newName) 82 | 83 | print "After rename: ", newName -------------------------------------------------------------------------------- /objectRenamer/renamer2.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this code sample we learn about defining functions. 3 | We'll restructure the code from the first renamer example into a function we can call 4 | """ 5 | 6 | from maya import cmds 7 | 8 | # We define a dictionary here using the {} notation 9 | # Dictionaries are an association of a key and a value. 10 | # In a real dictionary you'd have the word (key) and the definition (value) 11 | # In this case you have the key as the objectType and the value as a suffix. 12 | # For the camera we will set None as we want to use it to indicate Camera's should have no suffix 13 | SUFFIXES = { 14 | "mesh": "geo", 15 | "joint": "jnt", 16 | "camera": None, 17 | } 18 | 19 | # And if something doesn't have an appropriate suffix, lets default to group. 20 | DEFAULT = "grp" 21 | 22 | # Here we create our first function. 23 | # The word 'def' is used to define (def-ine) a function 24 | # Following the def, is the name of the function 25 | # In the parenthesis () we provide an argument, and give it an optional default value 26 | # This means that when someone uses our function they can provide some input, but the default value means they don't have to. 27 | # 28 | # Following the function is some text in triple quotes. This is called a docstring. 29 | # Docstrings are documentation on how to use the function and are a good practice to write so that people 30 | # can understand how to use your function without reading the code 31 | def rename(selection=False): 32 | """ 33 | Renames objects by adding suffixes based on the object type 34 | Args: 35 | selection (bool): Whether we should use the selection or not. Defaults to False 36 | 37 | Raises: 38 | RuntimeError: If nothing is selected 39 | 40 | Returns: 41 | list: A list of all the objects renamed 42 | """ 43 | 44 | # Our function has an input called selection. 45 | # This is used to let it know if we should use the selection or not 46 | 47 | # The ls function also takes an input called selection, and we can just pass that through 48 | objects = cmds.ls(selection=selection, dag=True) 49 | 50 | # Now if we are trying to use the selection and nothing is selected, lets give an error 51 | if selection and not objects: 52 | # We raise an exception, just like you would raise a complaint against someone 53 | # We give a RuntimeError because the error is to do with how we run this 54 | # And we give a message with some details 55 | # This will end our function and display a detailed error message (traceback) to our users 56 | raise RuntimeError("You don't have anything selected") 57 | 58 | # Now we need to sort our items from longest to shortest again so that we don't rename parents before children 59 | objects.sort(key=len, reverse=True) 60 | 61 | # Now we loop through all the objects we have 62 | for obj in objects: 63 | 64 | # We get the shortname again by splitting at the last | 65 | shortName = obj.split('|')[-1] 66 | 67 | # Now we see if there are children and if there are we get their type. 68 | # This is in case we receive a transform and not its shape 69 | children = cmds.listRelatives(obj, children=True) or [] 70 | if len(children) == 1: 71 | child = children[0] 72 | objType = cmds.objectType(child) 73 | else: 74 | objType = cmds.objectType(obj) 75 | 76 | # Now we look at the dictionary and ask to get the value associated with the key 77 | # In this case, if objType is mesh, we will get geo 78 | # If the dictionary doesn't hold the item, it will return the default value instead that we ask it for 79 | suffix = SUFFIXES.get(objType, DEFAULT) 80 | 81 | # If we can't get a suffix, we will continue 82 | # Continue means that we will continue on to the next item and skip the logic for this one 83 | if not suffix: 84 | continue 85 | 86 | # To prevent adding the suffix twice, we'll check if it already has the suffix and skip it if it does 87 | if shortName.endswith('_'+suffix): 88 | continue 89 | 90 | # Now we'll make the new name using string formatting 91 | # Instead of using the + symbol, we can use the %s symbol to insert strings 92 | newName = '%s_%s' % (shortName, suffix) 93 | cmds.rename(shortName, newName) 94 | 95 | # Now we find where in the list of objects our current object is 96 | index = objects.index(obj) 97 | 98 | # Then we replace it in the list with the name of the new object 99 | objects[index] = obj.replace(shortName, newName) 100 | 101 | # Finally we return the list back to the user of our function 102 | # Returning is how a function can let things outside it know what the result of it is 103 | return objects -------------------------------------------------------------------------------- /py3notes.md: -------------------------------------------------------------------------------- 1 | # Changes to the course if using Python 3 and Maya 2022 and later 2 | 3 | ## Table of Contents 4 | 5 | * [Editor](#editor) 6 | * [Syntax Changes](#python-3-syntax-changes) 7 | * [Maya Changes](#changes-in-newer-versions-of-maya) 8 | 9 | ## Editor 10 | We recommend using VSCode as the new code editor when following the course. 11 | 12 | Starting with Maya2022, the DevKit no longer ships with the "other" folder that contained the completion file. Use the MayaCode and MayaPy extensions for VSCode for easy autocomplete setup. Be sure to create the userSetup.py script to configure the port forwarding to Maya. 13 | 14 | Refer to the guide compiled on the process here 15 | 16 | The refactoring feature used in lesson 69 is also supported in VSCode. 17 | 18 | ## Python 3 Syntax Changes 19 | * **Print** has now been changed to a function, so all calls to print will need to have the parameters surrounded by parentheses - i.e. print("hello world") or print(help(...)) 20 | 21 | * **Reload** is now located in the importlib library. To use it, include the line 22 |
23 | import importlib.reload as reload 24 |
25 | in the Maya python editor before using the reload(...) when testing the course scripts. 26 | * To elevate your dev experience we recommend including this line in the **userSetup.py** script referred to earlier. 27 | 28 | **Lighting Manager** - 29 | In lesson 62, basestring is no longer supported in python3. Instead replace it with str. 30 | Similary, there are longer two forms integers (int and long) in python3. So in lesson 66, when casting the reference for the main window, instead of long(win) use int(win). 31 | 32 | For more changes between python2 and python3, check out this reference page. 33 | 34 | ## Changes in newer versions of Maya 35 | Aside from icon changes, there is one main update to Maya that is noteworthy for this course. The **default renderer** has been changed from **Maya Software** to **Arnold**. For the **Controller Library** project, make sure to _change the default renderer to Maya Software_ like it is in the tutorials. 36 | 37 | The setting is the second dropdown in the Render Settings window. 38 | -------------------------------------------------------------------------------- /tweener/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgovil/PythonForMayaSamples/7afeebbb9635202f7610485d7b682a3435386076/tweener/__init__.py -------------------------------------------------------------------------------- /tweener/reusableUI.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | import tweener 3 | from gearCreator import gears2 as gear 4 | 5 | 6 | # We put a lot of work into making our window for the tweener. 7 | # But what if we want to make another window? Do we have to write everything again? 8 | 9 | # Fortunately Python classes support inheritance 10 | # Just like your inherit your good looks from your parents, Python classes can inherit methods and attributes from their parents 11 | 12 | # So we first make a Base Window that we can reuse. 13 | # This will have just the bare functionality to make a window 14 | class BaseWindow(object): 15 | windowName = "BaseWindow" 16 | 17 | def show(self): 18 | if cmds.window(self.windowName, query=True, exists=True): 19 | self.close() 20 | 21 | cmds.window(self.windowName) 22 | self.buildUI() 23 | cmds.showWindow() 24 | 25 | def buildUI(self): 26 | # This is a placeholder method 27 | pass 28 | 29 | # Again, the *args variable means that we don't know how many arguments we will receive so lets put them all inside this argument called args 30 | def reset(self, *args): 31 | # This is a placeholder method 32 | pass 33 | 34 | def close(self, *args): 35 | cmds.deleteUI(self.windowName) 36 | 37 | 38 | # For our tweener UI, we inherit from our BaseWindow 39 | # Just like our BaseWindow inherits from the Python object, our TweenerWindow inherits from BaseWindow 40 | # This means that it will get all the attributes and methods that the Base Window has 41 | class TweenerWindow(BaseWindow): 42 | # The Base Window has a windowName attribute 43 | # By defining it here, we are overriding it. 44 | # Just like you may have different taste in music than your parents golden oldies, a child class can have 45 | # different attributes than its parent 46 | windowName = "TweenerWindow" 47 | 48 | # Similarly we redefine the buildUI method. 49 | # When buildUI is called in any methods from BaseWindow, it will know to refer to our overriden variable here 50 | def buildUI(self): 51 | column = cmds.columnLayout() 52 | cmds.text(label="Use this slider to set the tween amount") 53 | 54 | cmds.rowLayout(numberOfColumns=2) 55 | self.slider = cmds.floatSlider(min=0, max=100, value=50, step=1, changeCommand=tweener.tween) 56 | cmds.button(label="Reset", command=self.reset) 57 | 58 | cmds.setParent(column) 59 | cmds.button(label="Close", command=self.close) 60 | 61 | # And again, we just need to override the reset method 62 | # We don't need to define the close, or show methods because it gets those from BaseWindow 63 | def reset(self, *arg): 64 | cmds.floatSlider(self.slider, edit=True, value=50) 65 | 66 | 67 | # Now that we have our tweener working, we can work on our Gear Creator Window 68 | # Just like the Tweener we inherit from BaseWindow which gives us all its attributes and methods 69 | class GearWindow(BaseWindow): 70 | # We redefine the window name 71 | windowName = "GearWindow" 72 | 73 | # Our old friend init, which is called whenever we create a new window 74 | def __init__(self): 75 | # we just need to store our current gear inside a variable 76 | self.gear = None 77 | 78 | # Just like the Tweener, we just redefine the buildUI to customize our UI 79 | # It gets called from the show method that we inherited 80 | def buildUI(self): 81 | column = cmds.columnLayout() 82 | cmds.text(label="Use the slider to modify the number of teeth the gear will have") 83 | 84 | cmds.rowLayout(numberOfColumns=4) 85 | 86 | # This label will show the number of teeth we've set 87 | self.label = cmds.text(label="10") 88 | # Unlike the tweener, we use an integer slider and we set it to run the modifyGear method as it is dragged 89 | self.slider = cmds.intSlider(min=5, max=30, value=10, step=1, dragCommand=self.modifyGear) 90 | cmds.button(label="Make Gear", command=self.makeGear) 91 | cmds.button(label="Reset", command=self.reset) 92 | 93 | cmds.setParent(column) 94 | cmds.button(label="Close", command=self.close) 95 | 96 | def makeGear(self, *args): 97 | # We first need to see what the slider is set to, to find how many teeth we need to make 98 | teeth = cmds.intSlider(self.slider, query=True, value=True) 99 | 100 | # We make a near gear class instance 101 | self.gear = gear.Gear() 102 | 103 | # Now we create the gear with the given number of teeth 104 | self.gear.create(teeth=teeth) 105 | 106 | def modifyGear(self, teeth): 107 | # When the slider is changes, this method will be called. 108 | # First we will update the label that displays the number of teeth 109 | # the str() function converts the number into a string 110 | cmds.text(self.label, edit=True, label=str(teeth)) 111 | 112 | # If there is a gear already made, then we will set the slider to edit it 113 | if self.gear: 114 | self.gear.changeTeeth(teeth=teeth) 115 | 116 | def reset(self, *args): 117 | # When we reset, we will intentionally say we're done with this gear, move on to the next one 118 | # So moving the slider now will not adjust an existing gear 119 | self.gear = None 120 | 121 | # We will reset the slider value 122 | cmds.intSlider(self.slider, edit=True, value=10) 123 | 124 | # And finally reset the label value 125 | # the str() function converts the number into a string 126 | cmds.text(self.label, edit=True, label=str(10)) 127 | -------------------------------------------------------------------------------- /tweener/tweener.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | # It is important when programming, to split out our logic from our UI 4 | # This lets us call our tween code from other places without calling our UI, and also lets us test if its working properly 5 | def tween(percentage, obj=None, attrs=None, selection=True): 6 | """ 7 | This function will tween the keyed attributes on an object. 8 | Args: 9 | percentage (float): This is a mandatory argument (since it has no default) and will be the percentage to tween 10 | obj (str): the name of the object to use. This is optional since it has a default value 11 | attrs (list): A list of the attributes to tween. This is optional since it has a default value 12 | selection (bool): Whether to use the selection or not. Again, optional because it has a default 13 | """ 14 | # We need to error early if we aren't given an object AND we are told not to use the selection 15 | if not obj and not selection: 16 | # We raise a ValueError saying we got nothing to work with 17 | raise ValueError("No object given to tween") 18 | 19 | # If there is no object given, but selection must be true by now, we will get the current selection 20 | if not obj: 21 | # We list the selection. sl is shorthand for selection. [0] is beacause we only care about the first item 22 | obj = cmds.ls(sl=1)[0] 23 | 24 | # If we don't have a list of attrs to work with, we'll query which attributes are keyable 25 | if not attrs: 26 | attrs = cmds.listAttr(obj, keyable=True) 27 | 28 | # Now we need to get the current frame from Maya 29 | currentTime = cmds.currentTime(query=True) 30 | 31 | # Now we have everything to start, lets loop through all the attributes 32 | for attr in attrs: 33 | # It's common to need the object and attribute together like pCube1.translateX so we prepare the full name before hand 34 | attrFull = '%s.%s' % (obj, attr) 35 | 36 | # We query what keyframes exist for that attribute 37 | keyframes = cmds.keyframe(attrFull, query=True) 38 | 39 | # If there are no keyframes, then it isn't keyed so we skip it 40 | if not keyframes: 41 | # We continue on to the next item in the loop 42 | continue 43 | 44 | # We create an empty list to hold the values of our previous keyframes 45 | previousKeyframes = [] 46 | # We loop through all the keyframes 47 | for k in keyframes: 48 | # If they are less than our current frame we will add them to the list 49 | if k < currentTime: 50 | # We append the frame value to the list 51 | previousKeyframes.append(k) 52 | 53 | # This is the same as above but it uses a concept called list comprehension 54 | # It lets us flatten all the logic we just used into a single line 55 | # It says add every frame from keyframes if the frame is greater than the current time 56 | laterKeyframes = [frame for frame in keyframes if frame > currentTime] 57 | 58 | # If we have neither previous or later frames, then skip ahead 59 | if not previousKeyframes and not laterKeyframes: 60 | continue 61 | 62 | # If we do have previous keyframes, we need to find the nearest one to us 63 | if previousKeyframes: 64 | # This will be the highest of the previous keyframes, so it can be found using max 65 | previousFrame = max(previousKeyframes) 66 | else: 67 | # But if there are no previous keyframes, we'll set previousFrame to None to say we didn't find one 68 | previousFrame = None 69 | 70 | # Instead of doing a multiline if statement like above, we can do it in one line like here 71 | # If there are laterKeyframes, we'll find the minimum value to get the closest 72 | # Otherwise we'll set it to None to indicate we couldn't find it 73 | nextFrame = min(laterKeyframes) if laterKeyframes else None 74 | 75 | # If we didn't find it, we'll set previousFrame to be the same as the nextFrame. 76 | # This helps simplify our logic later 77 | if previousFrame is None: 78 | previousFrame = nextFrame 79 | 80 | # SImilar to the above if statement, we can condense it to a single line 81 | nextFrame = previousFrame if nextFrame is None else nextFrame 82 | 83 | # Now we query the values on the respective frames for this attribute 84 | # Because we prepared the attrFull variable above, we can reuse it here 85 | previousValue = cmds.getAttr(attrFull, time=previousFrame) 86 | nextValue = cmds.getAttr(attrFull, time=nextFrame) 87 | 88 | if nextFrame is None: 89 | # If there is no nextFrame, then set our currentValue to the previousValue 90 | currentValue = previousValue 91 | elif previousFrame is None: 92 | # if there is no previousFrame, set our currentValue to the nextValue 93 | currentValue = nextValue 94 | elif previousValue == nextValue: 95 | # If they are both equal, then just pick one of them 96 | currentValue = previousValue 97 | else: 98 | # If they are different, we calculate the tween value 99 | difference = nextValue - previousValue 100 | biasedDifference = (difference * percentage) / 100.0 101 | currentValue = previousValue + biasedDifference 102 | 103 | # Finally we set this value 104 | # Sometimes Maya doesn't update the viewport when just setting a key so also do a setAttr 105 | cmds.setAttr(attrFull, currentValue) 106 | # Then finally set the key 107 | cmds.setKeyframe(attrFull, time=currentTime, value=currentValue) 108 | 109 | 110 | # Again we create a class to contain functions(also called methods) that relate to each other 111 | # In this case they relate because they are part of the window 112 | # This class is called TweenerWindow, and it is based of (inherits) the python 'object' 113 | class TweenerWindow(object): 114 | # This is a class level variable 115 | # We use these for variables that don't really change 116 | windowName = "TweenerWindow" 117 | 118 | # Here we create a method called show that will be used to show the window 119 | # Its first argument is 'self' so that it has a reference to itself 120 | def show(self): 121 | # First we check if a window of this name already exists. 122 | # This prevents us having many tweener windows when we just want one 123 | if cmds.window(self.windowName, query=True, exists=True): 124 | # If another window of the same name exists, we close it by deleting it 125 | cmds.deleteUI(self.windowName) 126 | 127 | # Now we create a window using our name 128 | cmds.window(self.windowName) 129 | 130 | # Now we call our buildUI method to build out the insides of the UI 131 | self.buildUI() 132 | 133 | # Finally we must actually show the window 134 | cmds.showWindow() 135 | 136 | def buildUI(self): 137 | # To start with we create a layout to hold our UI objects 138 | # A layout is a UI object that lays out its children, in this case in a column 139 | column = cmds.columnLayout() 140 | 141 | # Now we create a text label to tell a user how to use our UI 142 | cmds.text(label="Use this slider to set the tween amount") 143 | 144 | # We want to put our slider and a button side by side. This is not possible in a columnLayout, so we use a row 145 | row = cmds.rowLayout(numberOfColumns=2) 146 | 147 | # We create a slider, set its minimum, maximum and default value. 148 | # The changeCommand needs to be given a function to call, so we give it our tween function 149 | # We need to hold on to our slider's name so we can edit it later, so we hold it in a variable 150 | self.slider = cmds.floatSlider(min=0, max=100, value=50, step=1, changeCommand=tween) 151 | 152 | # Now we make a button to reset our UI, and it calls our reset method 153 | cmds.button(label="Reset", command=self.reset) 154 | 155 | # Finally we don't want to add anymore to our row layout but want to add it to our column again 156 | # So we must change the active parent layout 157 | cmds.setParent(column) 158 | 159 | # We add a button to close our UI 160 | cmds.button(label="Close", command=self.close) 161 | 162 | # *args will be a new concept for you 163 | # It basically means I do not know how many arguments I will get, so please put them all inside this one list (tuple) called args 164 | def reset(self, *args): 165 | # This resets the slider to its default value 166 | cmds.floatSlider(self.slider, edit=True, value=50) 167 | 168 | def close(self, *args): 169 | # This will delete our UI, thereby closing it 170 | cmds.deleteUI(self.windowName) 171 | --------------------------------------------------------------------------------