Stumbling Toward 'Awesomeness'

A Technical Art Blog

Monday, February 11, 2013

Object Oriented Python in Maya Pt. 1

I have written many tools at different companies, I taught myself, and don’t have a CS degree. I enjoy shot-sculpting, skinning, and have been known to tweak parameters of on-screen visuals for hours; I don’t consider myself a ‘coder’; still can’t allocate my own memory.  I feel I haven’t really used OOP from an architecture standpoint. So I bought an OOP book, and set out on a journey of self improvement.

‘OOP’ In Maya

In Maya, you often use DG nodes as ‘objects’. At Crytek we have our own modular nodes that create meta-frameworks encapsulating the character pipeline at multiple levels (characters, characterParts, and rigParts). Without knowing it, we were using Object Oriented Analysis when designing our frameworks, and even had some charts that look quite a bit like UML. DG node networks are connected often with message nodes, this is akin to a pointer to the object in memory, whereas with a python ‘object’ I felt it could always easily lose it’s mapping to the scene.

It is possible now with the OpenMaya C++ API to store a pointer to the DG node in memory and just request the full dag path any time you want it, also PyMel objects are Python classes and link to the DG node even when the string name changes.

“John is 47 Years Old and 6 Feet Tall”

Classes always seemed great for times when I had a bunch of data objects, the classic uses are books in a library, or customers: John is 47 years old and likes the color purple. Awesome. However, in Maya, all our data is in nodes already, and those nodes have attributes, those attributes serialize into a Maya file when I save: so I never really felt the need to use classes.

Although, all this ‘getting’, ‘setting’ and ‘listing’ really grows tiresome, even when you have custom methods to do it fairly easily.

It was difficult to find any really useful examples of OOP with classes in Maya. Most of our code is for ‘constructing’: building a rig, building a window, etc. Code that runs in a linear fashion and does ‘stuff’. There’s no huge architecture, the architecture is Maya itself.

Class Warfare

I wanted to package my information in classes and pass that back and forth in a more elegant way –at all times, not just while constructing things. So for classes to be useful to me, I needed them to synchronously exist with DG nodes.

I also didn’t want to have to get and set the information when syncing the classes with DG nodes, that kind of defeats the purpose of Python classes IMO.

Any time I opened a tool I would ‘wrap’ DG nodes in classes that harnessed the power of Python and OOP. To do this meant diving into more of the deep end, but since that was what was useful to me, that’s what I want to talk about here.

To demonstrate, let’s construct this example:

#the setup
loc = cmds.spaceLocator()
cons = [cmds.circle()[0], cmds.circle()[0]]
meshes = [cmds.sphere()[0], cmds.sphere()[0], cmds.sphere()[0]]
cmds.addAttr(loc, sn='controllers', at='message')
cmds.addAttr(cons, sn='rigging', at='message')
for con in cons: cmds.connectAttr(loc[0] + '.controllers', con + '.rigging')
cmds.addAttr(loc, sn='rendermeshes', at='message')
cmds.addAttr(meshes, sn='rendermesh', at='message')
for mesh in meshes: cmds.connectAttr(loc[0] + '.rendermeshes', mesh + '.rendermesh')

So now we have this little node network:

node_network

Now if I wanted to wrap this network in a class. We are going to use @property to give us the functionality of an attribute, but really a method that runs to return us a value (from the DG node) when the ‘attribute’ is queried. I believe using properties is key to harnessing the power of classes in Maya.

class GameThing(object):
	def __init__(self, node):
		self.node = node
 
	#controllers
	@property
	def controllers(self):
		return cmds.listConnections(self.node + ".controllers")

So now we can query the ‘controllers’ attribute/property, and it returns our controllers:

test = GameThing(loc)
print test.controllers
##>>[u'nurbsCircle2', u'nurbsCircle1']

Next up, we add a setter, which runs code when you set a property ‘attribute’:

class GameThing(object):
	def __init__(self, node):
		self.node = node
 
	#controllers
	@property
	def controllers(self):
		return cmds.listConnections(self.node + ".controllers")
 
	@controllers.setter
	def controllers(self, cons):
		#disconnect existing controller connections
		for con in cmds.listConnections(self.node + '.controllers'):
			cmds.disconnectAttr(self.node + '.controllers', con + '.rigging')
 
		for con in cons:
			if cmds.objExists(con):
				if not cmds.attributeQuery('rigging', n=con, ex=1):
					cmds.addAttr(con, longName='rigging', attributeType='message', s=1)
				cmds.connectAttr((self.node + '.controllers'), (con + '.rigging'), f=1)
			else:
				cmds.error(con + ' does not exist!')

So now when we set the ‘controllers’ attribute/property, it runs a method that blows away all current message connections and adds new ones connecting your cons:

test = GameThing(loc)
print test.controllers
##>>[u'nurbsCircle2', u'nurbsCircle1']
test.controllers = [cmds.nurbsSquare()[0]]
print test.controllers
##>>[u'nurbsSquare1']

To me, something like properties makes classes infinitely more useful in Maya. For a short time we tried to engineer a DG node at Crytek that when an attr changed, could eval a string with a similar name on the node. This is essentially what a property can do, and it’s pretty powerful. Take a moment to look through code of some of the real ‘heavy lifters’ in the field, like zooToolBox, and you’ll see @property all over the place.

I hope you found this as useful as I would have.

posted by Chris at 1:03 AM  

5 Comments »

  1. Hi Evans. This article is interesting and useful. I didn’t know that @property has potential to make classes more useful in Maya. And I noticed that zooToolBox is a good material to enhance my python ability.

    By the way, I’m wondering why macaroniKazoo’s Blog isn’t updated for a while. There are a lot of informative information in the Blog, too.

    Thanks again!

    Comment by Ryutaro — 2013/02/11 @ 4:17 AM

  2. Hey Chris, two things:
    1- First, using a property setter for that stuff is a really bad idea. Getters and setters should be very simple (and in fact many discourage properties in python entirely, myself among them). But whether or not properties have their place, they should never have getters or setters that have other observable side effects. Ie, saying myObj.controllers = [] shouldn’t then change the properties of a bunch of other nodes! (And in fact, for collection attributes like your controllers, you’re much better off exposing a read-only attribute that’s a mutable list, mutable properties of mutable types are totally confusing.

    2- The important thing to remember with custom Python classes in Maya is that your python nodes cannot contain state. All the Maya state needs to be in the Maya scene, and your python nodes just forward commands to and from Maya. This is exactly what allows PyMEL to work the way it does, as you observe. So with that in mind, you are free to design your solutions in the way you want, whether your’re using OO or functional designs or whatever. And in fact, based on experience, I find you’re usually much better off designing as few classes as possible and just using PyMEL types. There’s rarely a reason to require the user to learn yet another API for dealing with the data they can already see in the scene! Your class in this instance would be much better off as a function.

    Comment by Rob Galanakis — 2013/02/16 @ 3:04 PM

  3. Thanks for the feedback Rob, we had functions to do all this stuff. But it was a lot of specific functions to do things to DG nodes and it seems more elegant to wrap the dg nodes on load in classes that have metadata and a toolkit with them to suit their needs.

    Instead of a function getCharCons(charPart), it seems much more elegant to be able to say charPart.controllers. Getter and setter functions didnt feel like I was using the power of Python in Maya, it felt like I was still wrapping a shell scripting language.

    Maybe you mean you make your own ‘PyMel types’ to do this, I am not clear on that, we don’t use PyMel here, it was just too slow. :-\

    Comment by admin — 2013/02/20 @ 4:06 PM

  4. I am finding descriptors even more useful then defining properties within the class itself, as you can re-use descriptors in other classes. For instance, I have a Python class that represents an animation clip. Inside that animClip class I have an ‘objects’ property derived from a custom descriptor that simply wraps a PyMel SelectionSet, thus allowing me to go back and forth between maya’s API calls and regular PyMel calls, while also being able to assign either pure text-based object lists, PyNodes, or MObjects, to that property without any additional functionalities. If further flexibility is needed, a descriptor can be extended in a child class. I can now also assign this descriptor to any number of different properties inside the class without the need to rewrite the entire property logic.

    Comment by Dimitry Kachkovski — 2013/03/11 @ 6:23 PM

  5. Chiming in rather belatedly…

    I’ve been doing something like this for Maya GUI using metaclasses to generate the correct set of descriptors that hide the get-set commands from surrounding code (http://techartsurvival.blogspot.com/2014/02/rescuing-maya-gui-from-itself.html). I used to use explicit setters and getters but I’ve found the metaclass version far easier to manage for big sets of things – especially when the differences between properties are usually just changes in command flags).

    If you want to do more just-set-this-values-don’t-make-me-call-f****ing-cmds stuff w/o pymel, I’ve got a simpler descriptor based example at http://techartsurvival.blogspot.com/2014/03/descriptors-and-pythonic-may-properties.html which just manages transform properties.

    I tend to agree with Rob’s point about managing side effects – I try never to let get-set access change the scene (with the exception of setting properties masking a single cmds call, which is just a clearer way of expressing the user’s intent)

    Comment by Steve Theodore — 2014/03/18 @ 12:21 AM

RSS feed for comments on this post. TrackBack URI

Leave a comment

Powered by WordPress