What is mango?
Mango is a development project to create a “pipeline in a box” like product, to most this will sound like a very bold claim, and let me assure you that it is. To attempt to live up to this claim i have structure the development road map of mango into different phases. Through out the different phases of mango development road map, we will be tackling the different aspects of the big picture Studio pipeline, mango is currently on phase1 and phase 2 should be released early 2015, with more phases already planned.
What to expect from the Mango Phase 1 Deployment
Phase 1 is the entry point to the essentials building blocks that are need to be setup up and expandable pipeline, While at the same time using a common case scenario for small studios that it hopes to improve at the time of adoption. The generalist pipeline is a common case scenario that can greatly be improved by automating and tracking their most basic needs.
this I identify as…
- Ingesting plates
- Making work files
- Making 3d elements for compositors
- Helping Compositors Build and keep comps up to date
- Publishing different outputs from comp (for editorial or DI)
- Dailies pipeline, reviewing work and having a running history (even when people don’t want , or can’t use shotgun)
To cover those basic goals, there was core infrastructure that had to be covered like, robust directory structure, and a fast locally hosted database driven versioning system, but i won’t bore you with those details. Instead let’s take a look at some of the more visible components of mango phase 1.
Application Agnostic Pipeline
One of my main initiatives is to write mango apps so that they can run in multiple applications with out much effort. This is done by using an API that translate generic command like file open, close, etc to native command the apps hosting the application can understand, By doing this the Pipeline logic, and UI code of the apps can be share across multiple applications.
This allows me the following…
- I can easily integrate new applications with mango as they start trending as long as they have python pyside support
- I can handle all future software integrations my self
- I can continue to update and enhance the applications with out the need to maintain duplicate code.
- Reduces testing and bug fixing freeing me up to work on other aspects of the pipeline
This is all done in the hopes that you the client, doesn’t have to worry about Mango or the big picture of Pipeline.
The current list of application that have been integrated are
With all that said let’s take a look at some of the applications you will be inheriting and the role the play in the Mango Phase 1 the “generalist” pipeline.
- Configures all artist software before launching the app for the artist and allowing to begin work
- Serves as the primary system to distribute tools, and plugins based on simple text based profiles
- Sets a repository (for tools and plugins) standard for all applications (3ds Max, Nuke, Maya, Hiero) so that a single person can maintain all packages
- It’s fully integrated with deadline and the render pipeline so that slaves will all ways have he same set of tools that were used when a job was submitted
- Minimizes the need for IT to manage and push plugin installations by automating the process via deadline and user interaction with the software launcher
- New plugins and tools can be deployed sooner and with out affecting other shows by separating this new additions into new profiles
Plate ingestion and proxy creation
- Quickly ingest image sequences to be tracked in the pipeline via stand alone tool
- Batch create project, sequence, shot and plate ingestion via csv ingestion tool
- I’m currently working on writing a Hiero exporter so that plates can be ingested directly from Hiero(More on this at the end)
- Automatically create jpg proxy versions of the ingested files via (ffmpg)
- Automate Shotgun project creation and and tasks generation for shots
Version Viewer(Stand alone and software integrated)
- Quickly find any and all resources made in the mango pipeline
- Quickly jump to the directory where a selected resource version is stored
- view resource dependencies (what went into making the currently selected version)
- Flip current version (Currently using Open source flipbook application Jefecheck, but can be expanded to use other flipbook apps)
- Submit version for review in shotgun (think of this as submit to dailies)
- Setup or edit any new or exiting projects sequences or shots
- Access the plate ingestion tools
Work Flow Tools
Workflow tools are the apps integrated in the artist application that dictate how we do things (Mostly create resources, do hand offs, and receive new and updated work).
here’s a list of the workflow tools we will be deploying with phase 1
Workspace manger (3ds Max,Nuke,Maya,Hiero)
- Quick tool for setting your work shot context
- Create New work files
- Open any previously create work files,
- Checkout work files created by other artist
- Find out if someone made a new version of your file in your absence
Publish Work File (3ds Max,Nuke,Maya,Hiero)
- Just like it sounds published the currently opened work file to be shared with the rest of the studio
Snapshot Work File (3ds Max,Nuke,Maya,Hiero)
- This, like the Publish Work File, allows the artist to iterate his current file but without publishing the work file. Think of it a private iteration (stays in the user work area)
Render Pass Manger(3ds Max)
- Proprietary Render Pass manger for 3ds max
- Fully tested and developed in production
- Focus on smaller learning curve (vs RPManager), and optimized for larger scenes
- Fully integrated into the Mango Versioning System
- Batch Render and Publish your passes as 3delements
- Automatically sets Exr optimized settings
- Fully integrated with deadline (render passes can be batched rendered and published on the farm)
- Automatically handles dependency tracking
- This is a shot resource catalog application built for compositors
- Find, review and import any resources available for comp
- Find out what “Read” nodes are out of date and update them
- Rollback “Read” nodes to any previous version at any time
Element Publish (Nuke, and Maya)
- Render and publish your nuke outputs as any of the following resource types (comp, precomp, paint, roto, plate)
- Render and publish your Maya Render Passes as 3delements
- Fully integrated with mango versioning system,
- Fully integrated with deadline(render passes can be batched rendered and published on the farm)
- Elements can also be published locally
- Automatically handles dependency tracking
All of this tools will be available too you from day one and will be deeply covered in the training time we will have together. Please also keep in mind that as subscription customer, i will be extending Mango, and maintain all the tools and workflows above. Which means that you won’t really have to maintain any of this tools and workflows in house, and the workflows will continue to expand as other areas of the pipeline are tackled.
Support and Mango Tickets
All support for mango goes through the http://mango1.zendesk.com, here you can make an account and submit problems, bugs, request
as well as track the status of any outstanding tickets (you will receive emails as well).
Shotgun Toolkit vs Mango
Mango is meant to replace the need for Shotgun Tool Kit altogether, and while Mango and STK seem similar in fashion, it is important to know that they operate on different concepts.
When i worked with the Shotgun Tool Kit (at Pixo) I was not pleased with the over all pipeline vision that shotgun had for the tool kit, which was basically none. I can only guess this is why it is a marketed as “tool kit” and not a “pipeline”.
Being that the bundled apps, “empty shells” with “hooks”, are written in a generic way so that the user can implement their own vision. I saw no real value in them, at least not in the context of my vision which revolves mostly about user experience. The overcomplicated system that comes along with using the toolkit purely as a deployment tool and directory structure generation tool, was so complicated and cover-some, That’s why I decided to create my on kit. A simpler system that allowed me to develop quickly from scratch and that did not impose road blocks by making me funnel my vision through tools that were created with no practical cases in mind. What you get from Mango is not a tool kit, it is an end to end pipeline and set of standards and practices that will be developed in several separate pre-planed phases. To me the vision of the product is more important the the code it’s self. This is the main reason why i felt Mango could fill a real need in the industry, “Pipeline in a box”. Mango those optionally interface with shotgun, but limits it to project management (Planning, Bidding, task tracking, work review, notes, etc), and keeps all the resource tracking to it’s self. Ideally Shotgun in mango is just a module, meaning that if at some point something better, and more affordable comes along, we can switch without changing any other aspect of the pipeline, or you can chose to not use it all.
Based on the customer feed back that I’ve received, Hiero still has some serious issues that will keep it from fully replacing smoke and flames in the near future as a finishing tool. On the other hand it seems to be a great tool for creating project structure and kicking out plates and other elements received from clients via EDL, early in the post production stage.
Because of this, the first Hiero Mango integration project I want to tackle is a Mango Exporter for Hiero.
the idea being that if we get assets (movies or imagess sequences) and an EDL from the client, we can quickly create the entire mango project structure and ingest plates (and create secondary resolutions) for the entire project via Hiero.
I would like to spend some time with you guys and see in what other ways you are using Heiro in the production pipeline, to see how else we can integrate or workflow and maximize the Hiero Mango integration.
but the Mango Exporter for Hiero is something that i hope to start developing this week.
If you guys are interested you can check an outline of the Mango development road map, and the currently scheduled phases.
Mango Development Road Map
- Phase 1 (Finished)
- work files
- Phase 2 (Hope to finish early next year)
- Look Dev
- Finalizing Stereo (Nuke, Maya)
- Phase 3
- Asset Library
- Show Archival
- Phase 4
- Inter studio remote location work sharing
- Phase 5
- Flame, Smoke
- Phase 7
- Color pipeline
- show,seq, shot LUTs
- openColorIO (Hardware profiles)
- lens Distortion Pipeline
Quick example on how to use vector math (mostly) to get the most out of locator based rigging. The following example makes a joint chain and a pole vector locator using 3 positions (Shoulder, elbow, and wrist). All the joint orientations and pole vector position are worked out automatically by the use of some simple subtraction addition and cross multiplication of vectors. In the future, I’m hoping to write a more in-depth explanation of my process. But, in the mean while I hope it helps those diving into auto rigging who might not be so fresh on vector math
The example is done in Maya, since Maya joints can have much different orientations than those visually displayed in the view-port.
Being that this snippet is in python, we must make use of the mayaAPIs vector and matrix classes which is a departure to the way in which we used to do it, in mel.
Here’s the snippet!
hope you find it useful
from maya import cmds import maya.api.OpenMaya as om def makeIkPlaneSetup(helpers, poleVectorDistance=5.0): if len(helpers) != 3: raise Exception('makeIkPlaneSetup input error, you need objects to pull positions from there were %s inputs\n' % len(helpers)) shld = om.MVector(cmds.xform(helpers,q=True,ws=True,t=True)) elbow = om.MVector(cmds.xform(helpers,q=True,ws=True,t=True)) wrist = om.MVector(cmds.xform(helpers,q=True,ws=True,t=True)) #figure out the upNode (plane direction) planeX = wrist - shld planeXL = planeX.length() armDis = (elbow - shld).length() foreArmDis = (wrist-elbow).length() fraction = armDis/(foreArmDis+armDis) planeP = shld + (planeX.normalize() * (planeXL*fraction)) upNode = (elbow-planeP).normalize() pvPos = shld + (upNode * poleVectorDistance) #shoulder orintation matrix shdXAxis = (elbow-shld).normalize() shdYAxis = (upNode ^ shdXAxis).normalize() #cross product a noramalize.... shdZAxis = (shdXAxis ^ shdYAxis).normalize() #cross product a noramalize.... shldM = om.MMatrix([[shdXAxis.x,shdXAxis.y,shdXAxis.z,0], [shdYAxis.x,shdYAxis.y,shdYAxis.z,0], [shdZAxis.x,shdZAxis.y,shdZAxis.z,0], [shld.x,shld.y,shld.z,1]]) elbowXAxis = (wrist-elbow).normalize() elbowYAxis = shdYAxis elbowZAxis = (elbowXAxis ^ elbowYAxis).normalize() elbowM = om.MMatrix([[elbowXAxis.x,elbowXAxis.y,elbowXAxis.z,0], [elbowYAxis.x,elbowYAxis.y,elbowYAxis.z,0], [elbowZAxis.x,elbowZAxis.y,elbowZAxis.z,0], [elbow.x,elbow.y,elbow.z,1]]) wristM = om.MMatrix([[elbowXAxis.x,elbowXAxis.y,elbowXAxis.z,0], [elbowYAxis.x,elbowYAxis.y,elbowYAxis.z,0], [elbowZAxis.x,elbowZAxis.y,elbowZAxis.z,0], [wrist.x,wrist.y,wrist.z,1]]) pvM = om.MMatrix([[shdXAxis.x,shdXAxis.y,shdXAxis.z,0], [shdYAxis.x,shdYAxis.y,shdYAxis.z,0], [shdZAxis.x,shdZAxis.y,shdZAxis.z,0], [pvPos.x,pvPos.y,pvPos.z,1]]) #convert matrix values to list for xform input shldML = [v for v in shldM] elbowML = [v for v in elbowM] wristML = [v for v in wristM] pvML = [v for v in pvM] #make pole vector point pv = cmds.spaceLocator() cmds.select(clear=True) #we'll keep joints parented to avoid rotation offset which maya creates when parenting joints post creation shldJ = cmds.joint() elbowJ = cmds.joint() wristJ = cmds.joint() cmds.xform(shldJ,ws=True,m=shldML) cmds.xform(elbowJ,ws=True,m=elbowML) cmds.xform(wristJ,ws=True,m=wristML) cmds.xform(pv,ws=True,m=pvML) makeIkPlaneSetup(cmds.ls(sl=True))
If you’re reading this right now, you’re probably trying to figure out why the F*&%$@ the built in method for resetting a skin-wrap in max, meshDeformOps.reset(), is doing absolutely nothing for you…
Turns out this method is broken in max.
Here’s a little hack that will allow press the reset button it’s self on any skinwrap via maxscript allowing you to by pass this roadblock.
fn resetSkinWrap md = ( deselect $* select (refs.dependentNodes md) max modify mode modpanel.setcurrentobject md maxHWND = windows.getMaxHWND() for c in (windows.getChildrenHWND maxHWND) do ( if c == "CustButton" and c == "Reset" then ( UIAccessor.PressButton c return True ) ) false ) --usage for m in (getClassInstances skin_wrap) do resetSkinWrap m
I had a chance to break down the blurdev “Dialog” class today. This class is a modified QDialog class that automatically handles parenting the dialog to the max window. The way in which the Blur Developers achieve this is by compiling a Qt4 module (QtWinMigrate) that does not ship with either PySide or PyQt4 that can convert the windows handle id of the main max window into a QObject that we can use as a parent. the process is very simple, here’s a snippet…
from blurdev.gui.winwidget import WinWidget parent = WinWidget.newInstance(blurdev.core.hwnd())
Here’s an example built directly into a class that inherits a QMainWindow…
import blurdev from blurdev.gui.winwidget import WinWidget from PyQt4 import QtGui,QtCore, uic class test(QtGui.QMainWindow): def __init__(self,parent=None): if not parent: parent = WinWidget.newInstance(blurdev.core.hwnd()) super(test,self).__init__(parent) self.setAttribute(QtCore.Qt.WA_DeleteOnClose ) self.setMouseTracking( True ) self.checkScreenGeo = True self.aboutToClearPathsEnabled = True gui = test() gui.show()
last but not least here’s the example from my previous uic post using this code to parent the window to Max.
from PyQt4 import QtGui, QtCore, uic base,form = uic.loadUiType(r'c:\uifiles\mainUI.ui') class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self) #---- self.populateList() self.connectWidgets() #populate a list box... def populateList(self): self.scnObjects = mxs.objects for o in self.scnObjects:self.listWidget.addItem(o.name) #-- connect event handlers def connectWidgets(self): self.listWidget.itemSelectionChanged.connect(self.selectObjects) self.pushButton.clicked.connect(self.pressButton) #--list box events def selectObjects(self): sel =  mxs.deselect(mxs.objects) for i in self.listWidget.selectedItems(): obj = self.scnObjects[(self.listWidget.indexFromItem(i).row())] sel.append(obj) mxs.select(sel) mxs.ForceCompleteRedraw() #-- button event def pressButton(self): sel = self.listWidget.selectedItems() QtGui.QMessageBox.about(self,'QtMessage',"%i objects selected" % len(mxs.selection)) def open(): from blurdev.gui.winwidget import WinWidget app = mainApp(parent=WinWidget.newInstance(blurdev.core.hwnd())) app.show() return app test = open()
Give it a go, i hope it works for you. Until next time, happy coding!
Sometimes being able to use a simple dictionary (vs crazy nested arrays) in max script can greatly simplify your code and can allow you to layout your logic in more manageable way.
Unfortunately maxscript doesn’t offer a native dictionary value type, and the .net equivalent “hashtable” is a pain to use in max. If you are not to keen on the use of .net hashtables you might want to try a custom struct like this one…
This dictionary struct uses “sorted keys” and “bsearch” to speed up the look up of keys and their values vs the “findItem” method that can be exponentially slower as as the number of keys grows in size.
take a look…
struct losDic ( private table = #(), fn binSort a b = ( if a > b then 1 else if a < b then -1 else 0 ), fn formatDic dic level:0 spaces:4 = ( strm = "\n" as stringStream padding = "" if level != 0 then ( for a=1 to level do for b=1 to spaces do padding += " " ) for k in (dic.keys()) do ( val = dic.getK k case (classof val) of ( (losDic):format "%%:\n%" padding k (formatDic val level:(level+1) spaces:spaces) to:strm default:format "%%:%\n" padding k val to:strm ) ) (strm as string) ), public fn count = keys.count, fn getK k = ( val = bsearch #(k) table binSort if val == undefined then return val val ), fn setK k v = ( val = bsearch #(k) table binSort if val == undefined then ( append table #(k,v) qsort table binSort return this ) val = v this ), fn hasK k = ( t = case (bsearch #(k) table binSort) of ( (undefined):false default:true ) t ), fn delK k = ( indx --findItem keysL (k as name) for i=1 to table.count where table[i] == k do ( indx = i exit ) if indx == 0 then throw ("there is no key "+k+" in dictionary") deleteItem table indx this ), fn keys = ( out = for k in table collect k out ), fn pprint = ( print (formatDic this) ok ) )
Here’s an example of how it’s basic usage…
--make the dictionary dic = losDic() --set keys dic.setK "pappa" 9999 --embeded dictonaries dic.setK "powers" (losDic()) dic.setK "object" (sphere()) --set key in the embeded dictionary (dic.getK "powers").setK "lazerEyes" true (dic.getK "powers").setK "bulletProof" true --query the available keys print "------keys------" print (dic.keys()) print "----------------" --loop throug a dictionary print "print keys and values" for k in dic.keys() do format "%:%\n" k (dic.getK k) print "preaty print" --prety print the dictionary dic.pprint()
till next time!!
Before I start this post, I would like to mention that most of the stuff covered in this post also applies to developing in PySide and MaxPlus. I’m currently using 3ds Max 2012 at work, which is why I chose to use PyQt and Blur Python, instead of MaxPlus and PySide. The 3 development concepts (Qt Designer, uic module, and packages) I’m going to cover in this post are general standards to developing in Python and Qt altogether, and should be applied when developing for other software such as Maya and Nuke as well.
One of the most exciting aspects of having Python and Qt running in 3ds Max is the ability to write guis using the Qt4 framework. While maxscript and its strange but convenient implementation of .net have proven to be an unstoppable combination. Python and the Qt4 framework bring an ease to the development process that will leave the most hardcore maxscript gurus wondering how they ever lived without it.
With that said, I have noticed that many of my fellow max scripters using Python and Qt for the first time don’t have a good enough entry point to some of the more common practices that make development with these tools super fast and incredibly fun. I hope the following code along with this write-up will help the readers get the most out of Qt and Python when developing for 3ds Max and possibly other software like Maya, Nuke, etc. In this write-up I will be covering 3 simple concepts that I use in my daily tool development. This write-up assumes that the reader has a strong knowledge of maxscript and a basic knowledge of the python syntax. No knowledge of Qt is required. The code samples do make small use of inheritance, an object oriented programing concept that might be foreign to TDs who’ve only work in maxscript where this method does not exist. The code is simple enough to implement without the need for a deep understanding of how inheritance works.
Let’s get started!
Qt Designer (Stop writing gui code)
One of the coolest aspects of Qt is being able to use the “what you see is what you get” Qt Designer application (ships with Qt). This application allows the user to quickly create complex guis that can range from full application windows, dialogs or even other widgets, to extend Qt Designer and Qt itself. Qt Designer achieves this without the need to write a single line of code. Some max TDs might find it reminiscent to the “maxscript visual editor” tool inside of 3ds Max, except for one important factor…
It doesn’t suck.
In all seriousness Qt Designer is a fantastic little app that allows you to visualize and create your own gui by dragging widgets from a repository right into your dialog. It also allows you to use Qt’s simple and intuitive layout widgets, which developers can use to make gui that resize beautifully without all the extra code usually involved in writing resizable max roll-outs and dialogs. The layout system in Qt is enough to make most veteran maxscript developers never want to use native UI functions in maxscript ever again.
So while yes, you could just write Qt gui’s in Python programmatically, which can be a pain to maintain as tools grow and change, the need to do so becomes obsolete for static UI’s. Freeing the developer to work on more important things, like logic.
UIC module (Using .ui files in python)
So now that you are all excited about using Qt Designer to start making some next level guis inside 3ds Max, you might be stuck figuring out how to get your Designer gui (a “.ui” file) into Python. A common practice is to use a uic.exe that ships with PyQt to translate a “*.ui” file (xml format) to a “*.py” file (python code) that can be imported or pasted into your python scripts.
I personally do not like this method for the following reasons:
- Anytime a gui change is needed the developer has to do an extra step of converting the “*.ui” file to a “*.py”
- This practice encourages developers to do patches or edits in the newly created “*.py” file. A practice that can be destructive when changes require going back and forth between Qt Designer and Python.
- Due to reasons 1 and 2 developers might limit the use Qt Designer to creating the first version of the “*.ui” file and make all future changes outside of Qt Designer directly in converted “*.py” files. This destroys the “Feng shui” of having a separation between the gui code and the code that makes up the logic of the gui.
So, what is the easier more elegant way of integrating the use of your “*.ui” files straight into Python, without the need for messy conversions, you ask?? The answer is simple…
The uic module.
from PyQt4 import uic
When used properly uic can provide a fast and easy way to convert “*.ui” files to Python classes you can start using at run-time (and in a very sexy object oriented way).
Here’s a snippet showing my preferred usage of the uic module in Python for creating fancy guis:
from PyQt4 import QtGui, QtCore, uic base,form = uic.loadUiType(r'c:\uifiles\mainUI.ui')) class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self)
In this snippet our dialog will be its own class of name “mainApp”. To display the interface all we have to do is the usual:
gui = mainApp() gui.show()
At this point our gui is done, leaving us free to just focus on the logic of our tool and connecting the widgets to the logic. Making a very clean separation between the gui and the main logic of our code. Isn’t uic fun??!!
Note:PySide uic module does not have a “loadUiType” method, but it’s easy enough to make your own here’s an example.
Python Packages (Keep your code organized and portable)
For many maxscripters that don’t have alot of experience using Python, Python packages will be somewhat of a new concept. But it’s a pretty simple one that will allow you to keep your tools very clean and organized. You won’t have to worry about them clashing with other people’s tools and libraries.
Note:For a very nerdy (by which I mean technical and in depth) description to Python packages you can checkout the following link.
To understand packages we need to understand how the Python file sourcing mechanism (“import”) works and how it relates to the maxscript’s equivalent (“filein”).
In maxscript to source code from a secondary location one can simply do a …
In Python it’s not that simple. Python files have to live in very specific locations for them to be “fileined” via the “import” command. These locations are loaded via the pythonpath environment variable. If the folders that your auxiliary files are stored in are NOT in the list of paths declared by the pythonpath variable, Python will fail to find them when you attempt to import them. By making packages we avoid the potential nightmare of having to add the path of every folder containing auxiliary files to the pythonpath before importing them and instead we can register our root folder and any sub folders as packages. This lets Python know to automatically search this folder for any auxiliary modules our tools are attempting to load.
To start using packages the easy way, we can simply start developing from a folder that is already in the default pythonpath such as “C:\Python27\Lib\site-packages”. But, we can also make folders outside of the Python root directory and develop from there as long as we let Python know to look for packages or modules in these external folders.
In my example I’ve chosen the second options and will be using the following folder as my main development area…
To start the example tool we will create a folder called sampleQtMaxGui (then name of our tool). We will then place a “__init__.py” file inside this folder effectively making our tool package.
This registers the folder “C:\dev\workspace\losDev\sampleQtMaxGui” as a package (assuming we have added “C:\dev\workspace\losDev” to the pythonpath environment variable). At this point, Python will be able to source any files inside of this newly created package. Furthermore, we can now import our new package into any Python session via the following line…
If we wanted to get very fancy and organize our tools in more detail, we can always make more packages inside of our initial package. For example, the following structure would make a package called “3dsMax” that would host all our Python tools that only run in 3ds Max,each tool would be hosted in it’s own package.
Importing the “sampleQtMaxGui” tool in to a Python session would then look like this…
I could then shorten the name space into a smaller variable by using the “as” expression to import the module into a shorter name space.
import 3dsmax.sampleQtMaxGui as sampleQtMaxGui
The big thing to take away from this, is that in Python it is not possible to import from sub-folders unless they contain a “__init__.py” file. These “__init__.py” files don’t have to contain any code, they only need to exist. But once they do, you’ll be able to source them easily via the import method.
Putting it all together
Ok, so now that we’ve talked about Qt Designer, the uic module, and python packages, it is time that we put it all together and see how this all works in practice. What better way to do this than to make a simple example tool. (download the finished sample code here)
Before we start our tool, here are a few things that we need to know for this example:
- The folder containing all my tools will be “C:\dev\workspace\losDev”.
- We will be creating a single tool called sampleQtMaxGui.
- This tool will be deployed as a package.
So, let’s make folder called “sampleQtMaxGui” and place a “__init__.py” file inside of it (ex. C:\dev\workspace\losDev\sampleQtMaxGui\__init__.py). Next, let’s make a simple “*.ui” file in Qt Designer that will be made up of a listWidget (list box) and a pushButton (button).
Here are the steps…
- Find designer.exe in your computer and launch it.
- Select the “main window” as your initial template.
- Drag and drop a list-widget and a push-button for the widget repository into your dialog (don’t worry about placement).
- Right click on the dialog and set the layout to vertical (this should automatically position your tools)
- After adding the widgets save the gui as “mainUI.ui” inside our tool’s package under a folder called “views” (ex. C:\dev\workspace\losDev\sampleQtMaxGui\views\mainUI.ui)
Once we’ve saved the “*.ui” file inside of the package it is time to start doing the Python work. In order to keep this example simple, all of our Python scripting will happen inside the “__init__.py”. The first lines of code will be used to import all the necessary Qt modules, the Blur python maxscript commands, and the “os” module.
from PyQt4 import QtCore, QtGui, uic from Py3dsMax import mxs import os
Next, we will be loading our “.ui” file via the “loadUiType” method in the uic module, effectively converting the content of the file into a class we can use to inherit from.
base,form = uic.loadUiType(os.path.join(os.path.dirname(__file__),'views','mainUI.ui'))
Note the use of “os.path.dirname(__file__)“. “__file__” is a global variable that python registers, which contains the absolute path to the file we’re currently working on. By using the “dirname” method of the “os” module, we can parse this path for the name of our tool’s root directory. We then use the “join” method to concatenate this folder, our subfolder containing our “*.ui” files (“views”), and the name of our “*.ui” file. This will effectively reproduce the absolute path of our “mainUI.ui” file regardless of where the package lives.
Now, it’s time to setup our gui class and have it inherit the values generated by the “loadUiType” method…
class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self)
The “__init__” function is a Python method built into the class mechanism that gets executed whenever our class is instantiated. This means that as soon as we run the following line this method will be ran as well…
x = mainApp()
The line “super(mainApp,self).__init__(parent)” makes sure that our parent class is also initiated. The “super(mainApp,self)” method is similar to the “delegate” pointer used when writing scripted plugins in maxscript and can be used to access method and properties stored in the parent class. After initializing our delegate or parent class, the “setupUi” method becomes automatically available to our custom class. By executing this method next, all the widgets we created in Qt Designer are then added to our class as well, leaving our class now capable to render an exact clone of the gui saved in the “.ui” file.
The “__init__” method of our custom class is also a perfect place to add other functions that would populate and connect our widgets. We exemplify this by adding the following methods to our class “populateList” and “connectWidgets”.
class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self) #populate a list box... def populateList(self): self.scnObjects = mxs.objects for o in self.scnObjects:self.listWidget.addItem(o.name) #-- connect event handlers def connectWidgets(self): pass
After adding the new methods to our class, we can execute them inside the “__init__” method.
class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self) #---- self.populateList() self.connectWidgets() #populate a list box... def populateList(self): self.scnObjects = mxs.objects for o in self.scnObjects:self.listWidget.addItem(o.name) #-- connect event handlers def connectWidgets(self): pass
Next, we add the event handler methods that are to be connected in our currently empty “connectWidgets” method. For this example we will create two new methods “selectObjects” that will be fired when the user changes the selection on the list box, and a “pressButton” method that we’ll be fired when the user press our push button…
class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self) #---- self.populateList() self.connectWidgets() #populate a list box... def populateList(self): self.scnObjects = mxs.objects for o in self.scnObjects:self.listWidget.addItem(o.name) #-- connect event handlers def connectWidgets(self): pass #--list box events def selectObjects(self): sel =  mxs.deselect(mxs.objects) for i in self.listWidget.selectedItems(): obj = self.scnObjects[(self.listWidget.indexFromItem(i).row())] sel.append(obj) mxs.select(sel) mxs.ForceCompleteRedraw() #-- button event def pressButton(self): sel = self.listWidget.selectedItems() QtGui.QMessageBox.about(self,'QtMessage',"%i objects selected" % len(mxs.selection))
The last thing we do to finish our class is connect the callbacks from our widgets to the methods we want them to execute. In order to do this we add the following lines to the “connectWidgets” method. Note: callbacks in Qt are referred to as “Signals” and are built directly onto the widgets themselves, keep this in mind when looking at documentation for widgets.
In the snippet above “listWidget” is the object name of the list box we created in Qt Designer and “pushButton” refers to the object name of the button that will also created in Qt Designer.
Our final code should look something like this…
class mainApp(base,form): def __init__(self,parent=None): super(mainApp,self).__init__(parent) self.setupUi(self) #---- self.populateList() self.connectWidgets() #populate a list box... def populateList(self): self.scnObjects = mxs.objects for o in self.scnObjects:self.listWidget.addItem(o.name) #-- connect event handlers def connectWidgets(self): self.listWidget.itemSelectionChanged.connect(self.selectObjects) self.pushButton.clicked.connect(self.pressButton) #--list box events def selectObjects(self): sel =  mxs.deselect(mxs.objects) for i in self.listWidget.selectedItems(): obj = self.scnObjects[(self.listWidget.indexFromItem(i).row())] sel.append(obj) mxs.select(sel) mxs.ForceCompleteRedraw() #-- button event def pressButton(self): sel = self.listWidget.selectedItems() QtGui.QMessageBox.about(self,'QtMessage',"%i objects selected" % len(mxs.selection))
We’re now completely done with our gui class, but there’s still one last thing we want to do. To make it simple to open our gui from any where, we will add one more method outside of our class called “open”. Here’s what that looks like…
def open(): app = mainApp() app.show() return app
This function will allow us to open the gui right after importing our tool with the following lines…
import sampleQtMaxGui ui = sampleQtMaxGui.open()
Running your tool from maxscript
To have a maxscript execute your tool, we make the following snippet in maxscript…
st = "import sys losDevPath = r'C:\dev\workspace\losDev' sys.path.insert(0,losDevPath) import sampleQtMaxGui gui = sampleQtMaxGui.open() " python.exec st
It’s important to know that the following lines are only necessary if the path to development folder has not already been added to the pythonpath. These lines allow you to add the path to your development folder at run-time vs having to mess with your machines environment variables. But, keep in mind this is a ghetto way of doing things. If you have no idea how to add your development folder to the pythonpath and you’re not working in a folder that is visible by Python, these lines might get you by for now. I strongly suggest you use this method only for testing or experimenting with the sample code in this post.
import sys losDevPath = r'C:\dev\workspace\losDev' sys.path.insert(0,losDevPath)
If the path to your development folder is already in the pythonpath you can use this instead.
st = "import sampleQtMaxGui gui = sampleQtMaxGui.open() " python.exec st
One thing to keep in mind is that once you import a Python script into 3ds Max (or any other Python session), the code gets compiled and later changes to the code will not be refreshed unless your restart max, or force reload the library. If you want to load the latest code every time you import your tool you can add the “reload” method.
st = "import sampleQtMaxGui reload(sampleQtMaxGui)" gui = sampleQtMaxGui.open() python.exec st
Anyway that’s it for now, good luck coding!!
One of the ways in which Mango facilitates the understanding of how people work and collaborate with each other, is by introducing the concept of “resources” and “resource types”.
resources are the different files that are made by the pipeline(or artist in the pipeline) and handed off between different pipeline steps. One of the key designs ideas of mango is to categorize and label the different kind of resources that are created in production to better understand the following questions…
- What is the main purpose of the resource?
- What step of the pipeline is best suited to create this resource?
- How is this resource used by artist and producers (The Pipeline)?
- How do we ingest this resource into different steps of the pipeline efficiently?
- How do we keep every instance of this resource used through out the pipeline up to date?
this is illustrated in the following graph. (right click on the image and choose “view image” to see a larger version)
In this graph, we illustrate the relationship between different “resource types” in the lighting to composting pipeline. in this snippet of the pipeline the following 4 types of resources are used
- 3d Elements
in this scenario, work-files mostly deal with application specific files (.max, .ma, .mb, .nk). it is important to understand that in the mango pipeline “work files” in their entirety are not designed to be handed of to the next pipeline step (dough they certainly can be). instead the goal is to have a modular system, where the artist can build “work file” with everything that they need to get the job done, but only hand off the smaller chunk of work that his responsible for via the publish process. the next person in the pipe then collects all the available resources, built by the people before him/her, and and assembles a complete file that is then used to generate new or updated resources.
In this example the lighter makes a “work file” where he/she will load or create all the resources that he need to create the “3dElements” (image sequences) that will be assembled by the comp artist.
the roto paint artist will make a “work file” where he/she will paint out markers from a plate and render this clean “paint” elements so they can be used by the compositor
the compositor will make a work file in which he will composite all the 3dElements, Paint, and PreComp images sequences and render a final comp.
in this workflow, no artist really cares or needs an other artist’s “work file”. They only need to get the resources created by “work files”.
in the case of the other 3 resource types used (3dElements, preComp, paint, comp) they all share one thing in common, they are all images sequences. But just having a resource type called “image sequence” doesn’t tells enough about how this resource is to be used in our pipeline. By breaking it down into the the following different types, it becomes more clear as what the purpose of each image sequence is meant to do, and how it is supposed to be used by the next step in the pipeline.
Why not have different resource types for each application?the reason why all this different software formats are grouped together as one resource type is because they have a very particular purpose, and that is to create other resources. no other resources in our pipeline has the ability to create new resources other than work files. so we give them they own special category.
it it possible what so ever, that this file types will be used by future resource types, in wich this files will be used to hand over data needed for some step of the pipeline, but in this case the files would never be opened and worked on.
it’s also important to note, that by making the work-file resource type generic. we can more rapidly add new software package to the pipeline… seance of all the tools dealing with work-files won’t have to be updated to account for a new resource type.
with that said Mango does track what application made the work file. and as to stream line the process of finding workfiles that belong to a particular software.
Unique Storage location per resource typeone of the benefits of splitting resources into this “purpose descriptive” types, Is that it allows us to split the storage across multiple drives or servers, An example of this can be storing all image sequences of type 3dElements on drive x:, while storing all images sequence of type comp in Y:. A studio has the option of using as many different storage server as there are resource types.
One of the main benefits of building a pipeline with the concept of resource and resource types, centered around the idea of “Work files” that out put axillary “resources”, is that growing the pipeline to cover new techniques and technology becomes much easier, by simply introducing new resource types, and rules that govern how this new resource is going to be handed off.
this means additions can be made to the pipeline with our affecting any all-ready existing methods and practices. or having to re-engineered major parts of code to account for new on though of practices.
I’m currently working on a small python script to create slates for frame sequences. i figure it would be a nice if the tool could make slates with custom data without the use of 3d party software such as nuke (which is common practice) to build and populate the information…
it then occurred to me, that i could create slate templates via html and css. Giving my clients total control over the design of the slate. My tool would then simply run a search and replace on the html file to populate the key fields that are unique to the slate and then render the html page, as an image to be loaded as the first frame of the sequence….
the over all approach is simple, the only tricky part was figuring out how to use the QWebKit module of PySide to load a local html page and then render it as an image.
Here’s a simple snippet of what i came up with…
from PySide import QtCore,QtGui,QtWebKit import sys app = QtGui.QApplication(sys.argv) view = QtWebKit.QWebPage() loop = QtCore.QEventLoop() view.mainFrame().loadFinished.connect(loop.quit) view.mainFrame().load('http://www.losart3d.com') loop.exec_() print 'done loading page proceed to rendering' size = view.mainFrame().contentsSize() view.setViewportSize(size)#.viewportSize()) image = QtGui.QImage(size,QtGui.QImage.Format_ARGB32_Premultiplied) image.fill(QtCore.Qt.transparent) p = QtGui.QPainter(image) p.setRenderHint(QtGui.QPainter.Antialiasing,True) p.setRenderHint(QtGui.QPainter.TextAntialiasing,True) p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform,True) view.mainFrame().render(p) p.end() image.save(r'c:\temp\renderedPage.jpg') print 'done!'
I’ve recently have been diving more in depth into qthread and how it can help make GUIs more interactive, specially when dealing with large data sets, where the programer might want to offload the data collection and processing methods to secondary threads as not to lock up the ui.
I’m still kind of new to the subject, so it’s taken me a while to get a clean working example, and how to set this up. I’ve used q thread before to achieve the effect, but it’s always been hard to know if my over understanding and approach to the method is the most optimized. after lot’s of online digging i found this approach to dealing with threading to work pretty well and be relatively easy to setup.
''' testing thread ui by Carlos Anguiano ''' from PySide import QtCore,QtGui import sys, random #inherit from Qthread and setup our own thread class class upateThread(QtCore.QThread): progress = QtCore.Signal(str) #create a custom sygnal we can subscribe to to emit update commands def __init__(self,parent=None): super(upateThread,self).__init__(parent) self.exiting = False def run(self): while True: self.msleep(10) self.progress.emit(str(random.randint(0,100))) class myDialog(QtGui.QDialog): def __init__(self,parent=None): super(myDialog,self).__init__(parent) self.resize(200,0) self.qlabel = QtGui.QLabel(self) self.qlabel.setText('Processor:') self.qlabelSt = QtGui.QLabel(self) self.btn = QtGui.QToolButton(self) l = QtGui.QVBoxLayout(self) l.addWidget(self.qlabel) l.addWidget(self.qlabelSt) l.addWidget(self.btn) self.btn.pressed.connect(lambda :self.qlabelSt.setText(str(random.randint(0,100)))) self.setupUpdateThread() def updateText(self,text): self.qlabel.setText('random number: '+text) def setupUpdateThread(self): self.updateThread = upateThread() #connect our update functoin to the progress signal of the update thread self.updateThread.progress.connect(self.updateText,QtCore.Qt.QueuedConnection) if not self.updateThread.isRunning():#if the thread has not been started let's kick it off self.updateThread.start() if __name__ == '__main__': app = QtGui.QApplication(sys.argv) win = myDialog() win.show() sys.exit(app.exec_())