Querying the Scene Graph

This topic presents the basics of scene element organization in Maya.

The Directed Acyclic Graph (DAG)

The scene graph in Maya is commonly referred to as the "Directed Acyclic Graph", or DAG. The DAG is actually a subset of a much larger graph, known as the Dependency Graph, which encompasses a much wider variety of node types including shaders, deformers, constraints, etc. For more information, consult the next section, Dependency Graph Plug-in Basics. For now, we will focus our attention DAG nodes, which can be categorized in one of two ways:

  1. Transform Nodes (MFn.kTransform): This node type defines a local 4x4 transformation matrix which affects all the objects beneath it in the hierarchy. This transformation data is manipulated by the MFnTransform function set. Transform nodes can have other transform nodes as their children to group scene elements together.
  2. Shape Nodes (MFn.kMesh, MFn.kCamera, MFn.kLight, ... ): This node type contains the actual geometric information of a scene element, such as the vertices of a mesh. A shape node always has a transform node as its parent.

The diagram below presents the simplified directed acyclic graph (DAG) of a basic scene. The world node represents the scene's root. The green circles correspond to the transform nodes (kTransform), and allow the shapes to be positioned in the scene. The shape nodes are identified by blue circles. In this DAG, perspShape corresponds to a camera in the scene, while pCubeShape1 and pointLightShape1 respectively represent a cubic mesh and a point light in the scene. Observe that the pointLight1 transform node is a child of the pCube1 transform node, which means that if the pCube1 transform node is moved, the point light will be moved as well.

NOTE:Generally, a shape node cannot have any children under it. The exception to this rule is a special circumstance known as the underworld. The underworld is a DAG subgraph whose root is attached as a child to a shape node. This underworld graph defines the control points and dependencies of a NURBS curve or a NURBS surface.

DAG Paths

The location of a specific scene element within the DAG is identified by a MDagPath object.

The MDagPath can return the MObject to which it corresponds in the DAG using MDagPath.node(). The path itself can be extended to encompass a shape node (MDagPath.extendToShape()), or return the lowest transform node (MDagPath.transform()) to name a few convenient functions. The MDagPath.fullPathName() function returns a string representation of the DAG path to a given node, formatted as a sequence of pipe-separated ("|") node names starting at the nameless root of the DAG. In the diagram above, the string representation of the path from the root world node to pointLightShape1 would be as follows: "|pCube1|pointLight1|pointLightShape1" (note that the root node has no name).

To reduce memory consumption, a complex shape such as a particularly dense mesh can be instanced in multiple locations in the scene graph. In other words, the same shape can appear in different places in the scene without being copied. To achieve this, several transform nodes in the DAG can be the parents of the same shape node. As such, a single shape node can have multiple paths from the root of the DAG. Functions such as MDagPath.isInstanced(), and MDagPath.instanceNumber() can be used to identify instanced shape nodes.

Traversing the Scene Graph

The scene graph can be traversed using an MItDag object, and optionally with an MItDependencyGraph object, since the DAG is a subset of the Dependency Graph.

Classes prefixed with MIt are known as iterators, and allow you to inspect each object in a collection. In our case, MItDag will allow us to iterate over the DAG nodes in the scene. In the following code sample, we create a DAG iterator which will traverse the scene starting at the root, and which will visit each node in a depth-first manner. The OpenMaya.MFn.kInvalid parameter ensures that the MItDag object will not filter any node types.

dagIterator = OpenMaya.MItDag( OpenMaya.MItDag.kDepthFirst, OpenMaya.MFn.kInvalid )

# This reference to the MFnDagNode function set will be needed
# to obtain information about the DAG objects.
dagNodeFn = OpenMaya.MFnDagNode()

The MItDag.isDone() function determines whether or not there are objects remaining to be inspected. The MItDag.currentItem() function returns the iterator's current DAG object whose depth relative to the root can be obtained using MItDag.depth(). Calling MItDag.next() will cause the internal state of the iterator to advance to the next item, but will not return a DAG object; this is only achieved using MItDag.currentItem(). If there are no more items to continue the iteration, the MItDag.isDone() function will return True. We can therefore construct a simple while loop to traverse the scene graph:

# Traverse the scene.
while( not dagIterator.isDone() ):

    # Obtain the current item.
    dagObject = dagIterator.currentItem()

    # Extract the depth of the DAG object.
    depth = dagIterator.depth()
            
    # Make our MFnDagNode function set operate on the current DAG object.
    dagNodeFn.setObject( dagObject )
                       
    # Extract the DAG object's name.
    name = dagNodeFn.name()
            
    print name + ' (' + dagObject.apiTypeStr() + ') depth: ' + str( depth )
    
    # Iterate to the next item.
    dagIterator.next()
NOTE:We elaborate on the use of function sets (ex: MFnDagNode) in Creating and Manipulating Objects.

Inspecting Selected Scene Elements

When objects are selected from Maya's user interface (or through scripts), they are added to a global active selection list, accessible via MGlobal.getActiveSelectionList(). The MGlobal static class provides a variety of functions pertaining to the Maya application, logging, object selection, command execution, 3D views (including the scene's up-axis), and model manipulation.

The code below illustrates how to obtain the active selection list by populating a MSelectionList object.

Python API 2.0:

Python API 1.0:

We use an instance of MItSelectionList to iterate over the selection list. The constructor of MItSelectionList allows us to specify a filter to iterate over objects of a specific type. In the following example, our iterator filters for objects compatible with the MFnDagNode function set by specifying the MFn.kDagNode parameter:

iterator = OpenMaya.MItSelectionList( selectionList, OpenMaya.MFn.kDagNode )

With the Python API 2.0, you can use a selection list to iterate over the entire scene graph by selecting all nodes in the scene first, and calling getDagPath() on the iterator for each item. For example:

dagNodeFn = OpenMaya.MFnDagNode()
 
cmds.select(all=True)
selectionList = OpenMaya.MGlobal.getActiveSelectionList()
if sList.length()>0:
    iterator = OpenMaya.MItSelectionList(selectionList, OpenMaya.MFn.kDagNode)
    while not iterator.isDone():  
        print iterator.getDagPath()
        iterator.next()

Example Command Plug-in: Printing DAG Paths

Filename: printPaths.py

Sample Script Editor Output:

Program Summary: The plug-in code below creates the printPaths() command. The behavior of this command depends on whether or not the active selection list contains DAG objects. If one or more DAG objects have been selected, their respective names, types, DAG paths and compatible function set types are printed to the Script Editor output. Otherwise, the scene graph is printed using each DAG node's name and type.

Python API 2.0:
# pyPrintPaths.py

import sys
import maya.cmds as cmds
import maya.api.OpenMaya as OpenMaya

def maya_useNewAPI():
	"""
	The presence of this function tells Maya that the plugin produces, and
	expects to be passed, objects created using the Maya Python API 2.0.
	"""
	pass
	
kPluginCmdName = 'pyPrintPaths'

##########################################################
# Plug-in 
##########################################################
class printPathsCmd(OpenMaya.MPxCommand):
    
    def __init__(self):
        ''' Constructor. '''
        OpenMaya.MPxCommand.__init__(self)
        
    def doIt(self, args):
        ''' 
        Print the DAG paths of the selected objects.
        If no DAG objects are selected, print the entire
        scene graph.
        '''
        
        # Populate the MSelectionList with the currently selected
        # objects using the static function MGlobal.getActiveSelectionList().
        
        #selectionList = OpenMaya.MSelectionList()
        selectionList = OpenMaya.MGlobal.getActiveSelectionList()
        
        # This selection list can contain more than just scene elements (DAG nodes),
        # so we must create an iterator over this selection list (MItSelectionList), 
        # and filter for objects compatible with the MFnDagNode function set (MFn.kDagNode).
        iterator = OpenMaya.MItSelectionList( selectionList, OpenMaya.MFn.kDagNode )
        
        if iterator.isDone():
            # Print the whole scene if there are no DAG nodes selected.
            print '====================='
            print ' SCENE GRAPH (DAG):  '
            print '====================='
            self.printScene()
        else:
            # Print the paths of the selected DAG objects. 
            print '======================='
            print ' SELECTED DAG OBJECTS: '
            print '======================='
            self.printSelectedDAGPaths( iterator )
    
    def printSelectedDAGPaths(self, pSelectionListIterator):
        ''' Print the DAG path(s) of the selected object(s). '''
        
        # Create an MDagPath object which will be populated on each iteration.
        dagPath = OpenMaya.MDagPath()
        
        # Obtain a reference to MFnDag function set to print the name of the DAG object
        dagFn = OpenMaya.MFnDagNode()
        
        
        
        # Perform each iteration.
        while( not pSelectionListIterator.isDone() ):
            
            # Populate our MDagPath object. This will likely provide
            # us with a Transform node.
            dagPath = pSelectionListIterator.getDagPath()
            try:
                # Attempt to extend the path to the shape node.
                dagPath.extendToShape()
            except Exception as e:
                # Do nothing if this operation fails.
                pass
            
            # Obtain the name of the object.
            dagObject = dagPath.node()
            dagFn.setObject( dagObject )
            name = dagFn.name()
            
            # Obtain the compatible function sets for this DAG object.
            # These values refer to the enumeration values of MFn
            fntypes = []
            fntypes = OpenMaya.MGlobal.getFunctionSetList( dagObject )
            
            # Print the DAG object information.
            print name + ' (' + dagObject.apiTypeStr + ')'
            print '\tDAG path: [' + str( dagPath.fullPathName() ) + ']'
            print '\tCompatible function sets: ' + str( fntypes )
            
            # Advance to the next item
            pSelectionListIterator.next()
        
        print '====================='         
            
    def printScene(self):
        ''' Traverse and print the elements in the scene graph (DAG)  '''
        
        # Create a function set which we will re-use throughout our scene graph traversal.
        dagNodeFn = OpenMaya.MFnDagNode()
        
        # Create an iterator to traverse the scene graph starting at the world node
        # (the scene's origin). We use a depth-first traversal, and we do not filter for
        # any scene elements, as indicated by the 'OpenMaya.MFn.kInvalid' parameter.
        dagIterator = OpenMaya.MItDag( OpenMaya.MItDag.kDepthFirst,
                                       OpenMaya.MFn.kInvalid )
       
        # Traverse the scene.
        while( not dagIterator.isDone() ):
            
            # Obtain the current item.
            dagObject = dagIterator.currentItem()
            depth = dagIterator.depth()
            
            # Make our MFnDagNode function set operate on the current DAG object.
            dagNodeFn.setObject( dagObject )
                       
            # Extract the DAG object's name.
            name = dagNodeFn.name()
            
            # Generate our output by first incrementing the tabs based on the depth
            # of the current object. This formats our output nicely.
            output = ''
            for i in range( 0, depth ):
                output += '\t'
                
            output += name + ' (' + dagObject.apiTypeStr + ')'
            print output
            
            # Increment to the next item.
            dagIterator.next()
        
        print '====================='

    


##########################################################
# Plug-in initialization.
##########################################################       
def cmdCreator():
    ''' Creates an instance of our command class. '''
    return printPathsCmd() 
    
def initializePlugin(mobject):
    ''' Initializes the plug-in.'''
    mplugin = OpenMaya.MFnPlugin( mobject )
    try:
        mplugin.registerCommand( kPluginCmdName, cmdCreator )
    except:
        sys.stderr.write( "Failed to register command: %s\n" % kPluginCmdName )

def uninitializePlugin(mobject):
    ''' Uninitializes the plug-in '''
    mplugin = OpenMaya.MFnPlugin( mobject )
    try:
        mplugin.deregisterCommand( kPluginCmdName )
    except:
        sys.stderr.write( "Failed to unregister command: %s\n" % kPluginCmdName )

##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:

import maya.cmds as cmds
cmds.loadPlugin( 'pyPrintPaths.py' )
cmds.pyPrintPaths()
 
'''
Python API 1.0:
# printPaths.py

import sys
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya

kPluginCmdName = 'printPaths'

##########################################################
# Plug-in 
##########################################################
class printPathsCmd(OpenMayaMPx.MPxCommand):
    
    def __init__(self):
        ''' Constructor. '''
        OpenMayaMPx.MPxCommand.__init__(self)
        
    def doIt(self, args):
        ''' 
        Print the DAG paths of the selected objects.
        If no DAG objects are selected, print the entire
        scene graph.
        '''
        
        # Populate the MSelectionList with the currently selected
        # objects using the static function MGlobal.getActiveSelectionList().
        selectionList = OpenMaya.MSelectionList()
        OpenMaya.MGlobal.getActiveSelectionList( selectionList )
        
        # This selection list can contain more than just scene elements (DAG nodes),
        # so we must create an iterator over this selection list (MItSelectionList), 
        # and filter for objects compatible with the MFnDagNode function set (MFn.kDagNode).
        iterator = OpenMaya.MItSelectionList( selectionList, OpenMaya.MFn.kDagNode )
        
        if iterator.isDone():
            # Print the whole scene if there are no DAG nodes selected.
            self.printScene()
        else:
            # Print the paths of the selected DAG objects. 
            self.printSelectedDAGPaths( iterator )
    
    def printSelectedDAGPaths(self, pSelectionListIterator):
        ''' Print the DAG path(s) of the selected object(s). '''
        
        # Create an MDagPath object which will be populated on each iteration.
        dagPath = OpenMaya.MDagPath()
        
        # Obtain a reference to MFnDag function set to print the name of the DAG object
        dagFn = OpenMaya.MFnDagNode()
        
        print '======================='
        print ' SELECTED DAG OBJECTS: '
        print '======================='
        
        # Perform each iteration.
        while( not pSelectionListIterator.isDone() ):
            
            # Populate our MDagPath object. This will likely provide
            # us with a Transform node.
            pSelectionListIterator.getDagPath( dagPath )
            try:
                # Attempt to extend the path to the shape node.
                dagPath.extendToShape()
            except Exception as e:
                # Do nothing if this operation fails.
                pass
            
            # Obtain the name of the object.
            dagObject = dagPath.node()
            dagFn.setObject( dagObject )
            name = dagFn.name()
            
            # Obtain the compatible function sets for this DAG object.
            # These values refer to the enumeration values of MFn
            fntypes = []
            OpenMaya.MGlobal.getFunctionSetList( dagObject, fntypes )
            
            # Print the DAG object information.
            print name + ' (' + dagObject.apiTypeStr() + ')'
            print '\tDAG path: [' + str( dagPath.fullPathName() ) + ']'
            print '\tCompatible function sets: ' + str( fntypes )
            
            # Advance to the next item
            pSelectionListIterator.next()
        
        print '====================='
            
            
    def printScene(self):
        ''' Traverse and print the elements in the scene graph (DAG)  '''
        
        # Create a function set which we will re-use throughout our scene graph traversal.
        dagNodeFn = OpenMaya.MFnDagNode()
        
        # Create an iterator to traverse the scene graph starting at the world node
        # (the scene's origin). We use a depth-first traversal, and we do not filter for
        # any scene elements, as indicated by the 'OpenMaya.MFn.kInvalid' parameter.
        dagIterator = OpenMaya.MItDag( OpenMaya.MItDag.kDepthFirst,
                                       OpenMaya.MFn.kInvalid )

        print '====================='
        print ' SCENE GRAPH (DAG):  '
        print '====================='
        
        # Traverse the scene.
        while( not dagIterator.isDone() ):
            
            # Obtain the current item.
            dagObject = dagIterator.currentItem()
            depth = dagIterator.depth()
            
            # Make our MFnDagNode function set operate on the current DAG object.
            dagNodeFn.setObject( dagObject )
                       
            # Extract the DAG object's name.
            name = dagNodeFn.name()
            
            # Generate our output by first incrementing the tabs based on the depth
            # of the current object. This formats our output nicely.
            output = ''
            for i in range( 0, depth ):
                output += '\t'
                
            output += name + ' (' + dagObject.apiTypeStr() + ')'
            print output
            
            # Increment to the next item.
            dagIterator.next()
        
        print '====================='


##########################################################
# Plug-in initialization.
##########################################################       
def cmdCreator():
    ''' Creates an instance of our command class. '''
    return OpenMayaMPx.asMPxPtr( printPathsCmd() )
    
def initializePlugin(mobject):
    ''' Initializes the plug-in.'''
    mplugin = OpenMayaMPx.MFnPlugin( mobject )
    try:
        mplugin.registerCommand( kPluginCmdName, cmdCreator )
    except:
        sys.stderr.write( "Failed to register command: %s\n" % kPluginCmdName )

def uninitializePlugin(mobject):
    ''' Uninitializes the plug-in '''
    mplugin = OpenMayaMPx.MFnPlugin( mobject )
    try:
        mplugin.deregisterCommand( kPluginCmdName )
    except:
        sys.stderr.write( "Failed to unregister command: %s\n" % kPluginCmdName )

##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:

import maya.cmds as cmds
cmds.loadPlugin( 'printPaths.py' )
cmds.printPaths()
 
'''