How to construct a node graph
So there are a bunch of pieces to how the ephemeral rig graph works, but I figure it’s best to start by talking about how the graph itself behaves. It’s actually really similar to how Maya’s graph works in DG mode, but unlike Maya’s graph it only needs to exist for nodes that are in the “critical path” of the user’s interactions. Currently the graph only supports manipulating one node at a time, so the critical path is whatever nodes depend on the one the user has selected.
The naive way to solve this would be to just start with the node being manipulated and work your way down, but this would break down fairly quickly. For one thing, you can have nodes in the critical path that depend on nodes that aren’t in the critical path. This is, for instance, true when manipulating the hand when the rig is in “suspend” mode. The elbow depends on the hand, but it also depends on the shoulder, which is not in the critical path of the hand.
Here’s another case: a character grasping it’s head with it’s hand. I’ve paired the head and hand so they move together as one. Rotating from the torso, however, effects the elbow from two different directions--from the hand via the head, and from the shoulder (note the HUD telling you what rig interaction mode is currently active).
Trying to evaluate these examples forward through the graph would end up getting you old data, or nonexistant data. Basically, you need a way to know what has or hasn’t already been evaluated, and base your order of evaluation off of that. In other words, just like Maya’s graph, nodes need to be able to be “dirty” or “clean” so you can know if they are safe to pull data from to calculate other nodes. And the simplest way to arrange evaluation around that question is to work backward, starting at the end of the “tree” of nodes and working backwards through the graph.
Let’s take a look at the code for the ephemeral rig node class:
A bunch of this code is about finding connections through the graph and building constraints, but for the moment what we care about is just the eval() method of the node.
Note that it tells its drivers to evaluate before it itself does. And, because all nodes in the graph have an eval method, the nodes on which it depends will tell their drivers to do the same. So first the evaluation requests will cascade up the graph until they hit something that doesn’t require any drivers, ie the node the user is controlling or a “dummy node” that isn’t in the critical path (like the shoulder in our arm example). Then the evaluation itself cascades back down the graph, marking the nodes clean as it goes. When there are multiple branches to the graph, the evaluation request cascade will stop when it encounters a clean node and use that node’s results without evaluating, preventing nodes from being needlessly evaluated twice. To figure out what’s in the critical path in the first place, the graph builds itself by looking at message connections I’ve placed in the scene.
These connections simply tell the ephemeral rig system what types of graph could be built--they do nothing else. When a node is selected, the ephemeral rig system looks at these connections and, depending on the current settings each node has for what it’s relationships to other nodes should be, builds the graph anew. Here’s the code for doing so.
Basically what’s happening here is that the function starts with the node the user has currently selected, and recursively walks down the connections to find all the nodes that this node could effect. At each step, those possible connections are filtered to get only the ones relevant to the current interaction.
For instance, each node also has it’s own current constraint type, which is stored as a string attribute on the transform node. When I switch to a different interaction mode, this string is set for the nodes in the limb I have selected. For instance, holding down ‘Z’ to switch into FK mode while manipulating an arm would set all it’s ephConstraintType attributes to “forward.” Then when the graph is rebuilt it will filter the connections used to find the critical path to just “forward” connections.
This doesn’t build the graph itself though--it just finds the nodes in the critical path. To actually build the graph, each node looks back up the connections to find its drivers. These drivers may or may not themselves be in the critical path, and if they aren’t the node creates a “dummy” node for them. This node has all the methods of an ephNode so that the whole group of nodes can be called together safely, but they don’t do anything. I already know this node isn’t in the critical path, so it will not be affected by the user and is always clean.
I also have it filter the drivers to detect and prevent circularities. The “pair” constraint type has connections that are always two-way, so it’s necessary to choose a single path through the pair and prevent the graph from doubling back on itself. Since the graph is rebuilt every time you select a different control, this still produces seemingly circular behavior--ie you can manipulate the pair from either side--as it will find a different path through the pair each time.
(Yes, I know that isNotDrivenByNode() uses an if/else when I could perfectly well use a single not to so the same thing in one line. Formulating it this way is easier for my brain to reason about. I’m not sorry.)
Note that I’m being very careful when choosing variable names to create an obvious division between strings on the one hand, and actual nodes (which may be MObjects, EphNodes, or occasionally PyNodes) on the other. Every time I’m using a string to refer to a Maya node (as you would with the commands module) I use a variable like “nodeName,” never “node.” When mixing cmds and Open Maya 2 in the same code base this distinction is very important!
Just remember, the map is not the territory, and the tap is not meritorious! Little Shaper humor for you there, sundog!