Stumbling Toward 'Awesomeness'

A Technical Art Blog

Monday, November 13, 2017

The Mighty Message Attribute

I recently had a discussion about storing relationships in Maya, and hadn’t realized the role of the message attribute wasn’t this universally cherished thing. In previous posts entitled ‘Don’t use string paths‘, or ‘Why Storing String Refs is Dangerous and Irresponsible‘ I outlined why this is the devil’s work, but in those posts I talked about the API, PyMel and Message Attrs. I didn’t really focus on why message attrs were so important: they serialize node relationships.

For quite some time I have advocated storing relationships with message attrs. At the Maya SIGGRAPH User Event, when they asked me to speak about our modular rigging system, I kind of detailed how we leveraged those at Crytek in CryPed.

msg

I am not quite sure when I started using message attrs to convey relationships, I’m no brainiac, it could have been after seeing this 2003 post from Jason Schleifer on CGTalk:

image

Or maybe I read it in the Maya docs (unlikely):

“Message attributes only exist to formally declare relationships between nodes. By connecting two nodes via message attributes, a relationship between those nodes is expressed.”

So why does Maya use this, and why should I?

As you read in the docs above, when Maya wants to declare a relationship between a camera and image plane, they do so with a message attribute that connects them. This is important because this bond won’t be broken if the plane or it’s parent is renamed. As soon as you store the string path to a node in the DAG, that data is already stale.  It’s no longer valid.  When you query a message attribute, Maya returns the item, it’s DAG path will be valid, regardless of hierarchy or name changes.

Jason’s example above is maybe the most simple, in my image (a decade later) you can see the messages declaring many relationships abstracting the character at three main levels of interface, Character, ChatacterPart and RigPart. I talked about the basic ideas here in a 2013 post about object oriented python in Maya.

Though Rob vigorously disagreed in the comments there, I am still doing this today.  Here’s an example from the facial code we released in EPIC’s ARTv1 rigging tools some time ago. The face is abstracted on two levels, the ‘face’ and the ‘mask’, here I am only displaying the message connecting them:

wiring

By using properties as described in that previous blog post, below I am accessing the system, creating a face instance, walking down the message connection to the mask node, and then asking it for the attach locations. It’s giving me these transforms, by querying the DAG, live:

msg

So, that property looks like this:

    @property
    def attachLocations(self):
        return cmds.listConnections(self.node + '.attachLocations')
    @attachLocations.setter
    def attachLocations(self, locs):
        for loc in locs:
            utils.msgConnect(self.node + '.attachLocations', loc + '.maskNode')

Setting the attach locations through python would look like this, and it would rebuild the message attrs:

face.mask.attachLoactions = ['myLoc1', 'myLoc2']

Working like this, you have to think hard about what a rigger would want to access at what level and expose what’s needed. But in the end, as you see, through python, you have access to everything you need, and none of the data is stale.

How and when to use strings

There are times when the only way you can store a relationship is by using a string in some fashion. Here are some situations and how I have handled them in the past, feel free to leave a comment with your experiences.

  • Maya can’t store a relationship to something that doesn’t exist (has been deleted). It can’t store a relationship when it’s not open. In these situations, instead of storing the name in an attr, I stamp the two nodes with a string attr to store the relationship, then you query the world for another node with a matching stamped attr.
  • Many times you need to feed your class an initial interface node to build/wrap. Instead of feeding it a string name, you can query the world for node type, in the Ryse example above, the rigging and animation tools could query cmds.ls(type=’CryCharacter’), this would return all characters in the scene. This means all rigging and animation tools needed a common ‘working character’ combobox at the top to define the character the tool is operating on. If you don’t have a node type, you can use a special string attr to query for.
  • Sometimes you’re like saving joint names to serialize skinning data or something. You can use message attrs to play it safe here as well. Some pseudocode: For character in characters, if character identifier matches file on disk, for mesh in character.meshes if mesh in file skin it. For joint in character.joints if in file, add them to the skincluster, etc. Here you’re validating all your serialized string data against your class which is traversing the DAG live.
  • Message attrs can get SLOW if you’re tracking thousands of items, you should only be tracking important things you would want later. In CryPed, when we wanted to track all nodes that were created when a module was built, we would stamp them all with a string attr that was the function name that built the module. To track this kind of data HarryZ at Crytek had the pragmatic idea of just doing a global ls of the world when a buildout started and then one at the end and boolean them out, this caught all the intermediate and utility nodes and everything generated by the rigging code.
posted by Chris at 6:10 AM  

Monday, July 28, 2008

Gleaning Data from the 3dsMax ‘Reaction Manager’

This is something we had been discussing over at CGTalk, we couldn’t find a way to figure out Reaction Manager links through maxscript. It just is not exposed. Reaction Manager is like Set Driven in Maya or a Relation Constraint in MotionBuilder. In order to sync rigging components between the packages, you need to be able to query these driven relationships.

I set about doing this by checking dependencies, and it turns out it is possible. It’s a headache, but it is possible!

The problem is that even though slave nodes have controllers with names like “Float_Reactor”, the master nodes have nothing that distinguishes them. I saw that if I got dependents on a master node (it’s controllers, specifically the one that drives the slave), that there was something called ‘ReferenceTarget:Reaction_Master‘:

refs.dependents $.position.controller
#(Controller:Position_Rotation_Scale, ReferenceTarget:Reaction_Master, Controller:Position_Reaction, ReferenceTarget:Reaction_Set, ReferenceTarget:Reaction_Manager, ReferenceTarget:ReferenceTarget, ReferenceTarget:Scene, Controller:Position_Rotation_Scale, $Box:box02 @ [58.426544,76.195091,0.000000], $Box:box01 @ [-42.007244,70.495964,0.000000], ReferenceTarget:NodeSelection, ReferenceTarget:ReferenceTarget, ReferenceTarget:ReferenceTarget)

This is actually a class, as you can see below:

exprForMAXObject (refs.dependents $.position.controller)[2]
"<<Reaction Master instance>>"
 
getclassname (refs.dependents $.position.controller)[2]
"Reaction Master"

So now we get the dependents of this ‘Reaction Master’, and it gives us the node that it is driving:

refs.dependentNodes (refs.dependents $.position.controller)[2]
#($Box:box02 @ [58.426544,76.195091,0.000000])

So here is a fn that gets Master information from a node:

fn getAllReactionMasterRefs obj =
(
	local nodeRef
	local ctrlRef
	for n = 1 to obj.numSubs do
	(
		ctrl = obj[n].controller
		if (ctrl!=undefined) then
		(
			for item in (refs.dependents ctrl) do
			(
				if item as string == "ReferenceTarget:Reaction_Master" then
				(
					nodeRef = (refs.dependentNodes item)
					ctrlRef = ctrl
				)
			)
			getAllReactionMasterRefs obj[n]
		)
	)
	return #(nodeRef, ctrlRef)
)

The node above returns:

getAllReactionMasterRefs $
#(#($Box:box02 @ [58.426544,76.195091,0.000000]), Controller:Position_Rotation_Scale)

The first item is an array of the referenced node, and the second is the controller that is driving *some* aspect of that node.

You now loop through this node looking for ‘Float_Reactor‘, ‘Point3_Reactor‘, etc, and then query them as stated in the manual (‘getReactionInfluence‘, ‘getReactionFalloff‘, etc) to figure out the relationship.

Here is an example function that prints out all reaction data for a slave node:

fn getAllReactionControllers obj =
(
	local list = #()
	for n = 1 to obj.numSubs do
	(
		ctrl = obj[n].controller
		if (ctrl!=undefined) then
		(
			--print (classof ctrl)
			if (classof ctrl) == Float_Reactor \
			or (classof ctrl) == Point3_Reactor \
			or (classof ctrl) == Position_Reactor \
			or (classof ctrl) == Rotation_Reactor \
			or (classof ctrl) == Scale_Reactor then
			(
				reactorDumper obj[n].controller data
			)
		)
		getAllReactionControllers obj[n]
	)
)

Here is the output from ‘getAllReactionControllers $Box2‘:

[Controller:Position_Reaction]
ReactionCount - 2
ReactionName - My Reaction
    ReactionFalloff - 1.0
    ReactionInfluence - 100.0
    ReactionStrength - 1.2
    ReactionState - [51.3844,-17.2801,0]
    ReactionValue - [-40.5492,-20,0]
ReactionName - State02
    ReactionFalloff - 2.0
    ReactionInfluence - 108.665
    ReactionStrength - 1.0
    ReactionState - [65.8385,174.579,0]
    ReactionValue - [-48.2522,167.132,0]

Conclusion
So, once again, no free lunch here. You can loop through the scene looking for Masters, then derive the slave nodes, then dump their info. It shouldn’t be too difficult as you can only have one Master, but if you have multiple reaction controllers in each node effecting the other; it could be a mess. I threw this together in a few minutes just to see if it was possible, not to hand out a polished, working implementation.

posted by Chris at 4:42 PM  

Monday, May 12, 2008

Creating Interactive MotionBuilder User Interface Tools

This is a copy of a tutorial stored in the Tutorial section of my site.
————————————————————————————

One of the biggest complaints about MotionBuilder, from a Tech Artist/Animator standpoint is it’s Python implementation. Everyone seems to agree: It is very hard to make user-friendly tools in MotionBuilder. Which is why, as you will see, this tutorial is about creating user-friendly tools outside of MotionBuilder.

Why Is Impossible to Create an Interactive UI in MotionBuilder? Is There Another Way?

Because of threading issues, any time you create a window or tool UI in MB, the whole app freezes until you close your window. This makes it very hard to create tools that people interact with and use. I heard from someone at Sony Pictures Imageworks that it is possible to create user-friendly UI, they did it there as a seperate Python app that ‘talks’ to MB through a telnet terminal.

So this is what I did. I first scoured the ‘internets’ and found little or no information on using the telnet ‘Remote Server’ Python implementation in MB, so I decided to create this tutorial.

Python/MotionBuilder Resources

You don’t need to be experienced with Python to follow this tutorial, this is actually the first Python script I have written. You do need two things to follow along: [Python] [wxPython]

Plug: Also, if you are serious about Python Scripting in MotionBuilder, You should grab Jason Parks’ Autodesk Master Class: Python Scripting for MotionBuilder Artists, not only because he’s a friend of mine, but also because it’s an invaluable rescource (the only resource) when just starting out.

Getting Started: Remote Server and Telnetlib

Open up MB and select Window>Python Console Tool, under the ‘Configuration’ tab, you should see the following:

The checkboxes ‘Enable‘ and ‘Allow Network Connections‘ are usually off by default. Turn them both on. Now there is a telnet server running on port 4242 of your PC that can accept Python commands and execute them in MB. If you press ‘Telnet Console‘ a terminal will pop open and connect. Entering commands into this terminal is like entering them into the MB Python Console.

Now open IDLE, the Python GUI/Script editor. Create a new script as follows, my comments in green:

#Import the Python telnet library
import telnetlib
 
#Create a telnet console to your PC (127.0.0.1) and MotionBuilder (4242)
host = telnetlib.Telnet('127.0.0.1', 4242)
 
#Find the prompt, so you can enter a command (seriously)
host.read_until('>>>', 5)
#Create a null called 'locator'
host.write('myNull = FBModelNull("locator")\n')
 
#Close the terminal
host.close()

If you alt+tab back to MB, you should see a null named ‘locator’ in the Schematic View. What we are basically doing is creating an invisible terminal window, like the one we saw when we clicked ‘Telnet Console‘. We then want to send the command myNull = FBModelNull(“locator”) over, but in order to do so, we first need to find the prompt. ‘\n‘ means ‘new line’, so we are essentially entering a command at the prompt and pressing [ENTER].

Communication Issues: Writing a Function to Bridge the Gap

First, you do not want to have to do this stuff every time you need to enter a Python command. Second, you need a way of getting data that is returned as a result of your command/query. This is what a simple function to do that looks like in Python:

def mbPipe(command):
     host.read_until('>>>', .01)
     #write the command
     host.write(command + '\n')
     #read all data returned
     raw = str(host.read_until('>>>', .1))
     #removing garbage i don't want
     raw = raw.replace('\n\r>>>','')
     raw = raw.replace('\r','')
     #make an array, 1 element per line returned
     rawArr = raw.split('\n')
     return rawArr

So now we have a function that makes it easier for us to communicate with MB through telnetlib. Here is an example of it’s use:

mbPipe('selectedModels = FBModelList()')
mbPipe('FBGetSelectedModels(selectedModels,None,True)')
mbPipe('for item in selectedModels: print item.Name')

The code above returned the names of the currently selected items in MB as an array. ([‘Null’, ‘Null 1’, ‘Null 2’, ‘locator’, ‘Test’]) Because this returns an array, it is easy for us to write something a little more complicated like this:

for item in (mbPipe("for item in selectedModels: print item.Name")):
     self.selListBox.Append(item)

This adds all items selected in MB to a listbox. Now that you have an
understanding of how to communicate with MotionBuilder through the
Remote Server and an external Python process via telnetlib, let’s now
build a tool to take advantage of this.

Practical Example of Interactive, External, MotionBuilder UI

I’m not really going to walk you through this one. It’s basically some modified wxPython example code from the internets bent to my bidding. You will see our mbPipe function and some things from above.

I decided to use wxPython because it’s a very simple and efficient UI library for Python that allows you to quickly create decent custom interfaces. You can of course use anything other tools to build a UI.

To the left here is a little tool. When you press ‘Get Selected Items‘, the listbox will fill with the ‘models’ currently selected in MB. As you click on items in the listbox, the position/rotation text at the bottom will change reflecting the position and rotation of the ‘models’ in MB. Pressing “Delete This Model‘ will delete the object in MB.

import wx
import telnetlib
host = telnetlib.Telnet("127.0.0.1", 4242)
 
def mbPipe(command):
     host.read_until('>>>', .01)
     #write the command
     host.write(command + '\n')
     #read all data returned
     raw = str(host.read_until('>>>', .1))
     #removing garbage i don't want
     raw = raw.replace('\n\r>>>','')
     raw = raw.replace('\r','')
     rawArr = raw.split('\n')
     #cleanArr = [item.replace('\r', '') for item in rawArr]
     return rawArr
 
class MyFrame(wx.Frame):
 
     def __init__(self):
          # create a frame, no parent, default to wxID_ANY
     wx.Frame.__init__(self, None, wx.ID_ANY, 'mbUI test',pos=(200, 150),size=(175,280))
 
          #create listbox
          self.selListBox = wx.ListBox(self, -1, choices=[],pos=(8,38),size=(150, 145))
          self.selListBox.Bind(wx.EVT_LISTBOX, self.selListBoxChange)
 
          #create model metadata
          self.translationLabel = wx.StaticText(self,-1,'Pos:',pos=(8,185))
          self.rotationLabel = wx.StaticText(self,-1,'Rot:',pos=(8,200))
 
          #make buttons
          self.selItems = wx.Button(self, id=-1,label='Get Selected Items',pos=(8, 8),size=(150, 28))
     self.selItems.Bind(wx.EVT_BUTTON, self.selItemsClick)
          self.selItems.SetToolTip(wx.ToolTip("Gets all 'models' selected in Motion Builder"))
 
          self.delItems = wx.Button(self, id=-1,label='Delete This Model',pos=(8, 217),size=(150, 28))
          self.delItems.Bind(wx.EVT_BUTTON, self.delItemsClick)
          self.delItems.SetToolTip(wx.ToolTip("Will delete selected models"))
 
          # show the frame
          self.Show(True)
 
     def selItemsClick(self,event):
          mbPipe("selectedModels = FBModelList()")
          mbPipe("FBGetSelectedModels(selectedModels,None,True)")
          self.selListBox.Set([])
          for item in (mbPipe("for item in selectedModels: print item.Name")):
               self.selListBox.Append(item)
 
     def delItemsClick(self,event):
          mbPipe('FBFindModelByName("' + str(self.selListBox.GetStringSelection()) + '").FBDelete()')
          mbPipe("selectedModels = FBModelList()")
          mbPipe("FBGetSelectedModels(selectedModels,None,True)")
          self.selListBox.Set([])
          for item in (mbPipe("for item in selectedModels: print item.Name")):
               self.selListBox.Append(item)
 
     def selListBoxChange(self,event):
          trans = mbPipe('FBFindModelByName("' + str(self.selListBox.GetStringSelection()) + '").Translation')
          rot = mbPipe('FBFindModelByName("' + str(self.selListBox.GetStringSelection()) + '").Rotation')
          self.translationLabel.SetLabel(('Pos: ' + trans[0]))
          self.rotationLabel.SetLabel(('Rot: ' + rot[0]))
 
application = wx.PySimpleApp()
window = MyFrame()
application.MainLoop()

Conclusion: This Sucks, but it Makes You A Better Person

Sure, this is a pretty unconventional way to have to create tools, but suffering this will make you a better Technical Artist/Animator. Not to mention you can now create great, interactive tools, without resorting to C++ and the Open Reality SDK.

Sync Issues?: It seems that sometimes MB just closes the connection, or perhaps telnetlib does. Either way, if your tool becomes unresponsive, the trick I found is to close the tool, then uncheck/recheck ‘Allow Network Connections‘ in the ‘Python Console Tool‘ Configuration tab shown above. Then re-run your script.

PyWin32: There is another Python library called Python Win32 Extentions, that allows you to get your hands dirty with COM and things. You can use some of these to make it so that your tool shows up as a child window of the MB app itself, this way users don’t even have to press alt+tab to use your app and it looks like it belongs in MBuilder. Of course there is no mention of this on teh internets, but I found an example for 3DSMax/Python in Adam Pletcher’s GDC talk: Python for Technical Artists, and a Maya/Python example on cgTalk. This is somewhat more difficult; using the above sources I have written a MotionBuilder example of this [here].

posted by Chris at 1:03 AM  

Powered by WordPress