Maya Scene Assembly Wrapper

My ex-classmate Freek Hoekstra was asking about scripting with scene assembly nodes, as it appeared to be lacking documentation and generally didn’t work.

So I felt up to the challenge and with some trial and error was able to create an assemblyDefinition with working representations (the trick is to set all attributes or it will disable the entry).

The difficult part was the assemblyReference. It appeared to import the files rather than referencing them. As I finally found in the assemblyReference.cpp source code this is in fact what should happen.

The assemblyReference imports the file you want to reference, looks up the assemblyDefinition node it just imported and then copies it’s attributes and deletes any new nodes it found. Problem is: it can’t find the assemblyDefinition and doesn’t clean up after itself. So that bit I did manually in python by essentially tracking the difference in ALL scene nodes before and after referencing the file. If there are any new nodes the referencing went wrong and I attempt to do it manually. At the very least my wrapper DOES clean up the file and print some more errors if no assemblyDefinition was found.

This code has no interface to go with it yet, I first want to make up some more features before doing so (such as screenshots!). What the wrapper code does support however is single line exporting of multiple selected groups to each be another representation of one object (imagine a file containing all LODs of an asset). It also supports exporting in different types (maya scene, alembic, gpucache).

Then there’s a single line to save an assetDefinition as a separate file (containing just the one assetDefinition node) which is then ready for referencing; creating an assetReference from a file path is another one-liner.

Please look and try out the examples at the bottom, you could create a sphere and a cube, select them both and run all code at once. This should leave you with a folder & 3 files exported next to the current scene as well as an assetDefinition and assetReference node.

To be complete frank there’s also one thing seriously lacking: changing the definition file of the created assemblyReference node from the attribute editor does not work as it results in errors identical to the ones this wrapper fixes. AssetReferences created by the maya ‘create->scene assembly->assembly reference’ button don’t suffer this problem but I don’t know the code that lies behind it.

#
# Resources used:
# cmds.listAttr('dagAsset1.representations', multi=True)
# C:\Program Files\Autodesk\Maya2014\devkit\plug-ins\sceneAssembly
# C:\Program Files\Autodesk\Maya2014\Python\Lib\site-packages\maya\app\sceneAssembly
# http://docs.autodesk.com/MAYAUL/2013/ENU/Maya-API-Documentation/index.html?url=cpp_ref/hierarchy.html,topicNumber=cpp_ref_hierarchy_html
#

                
import os
import os.path
from maya import cmds
from maya.OpenMaya import *
from maya.OpenMayaMPx import *


cmds.loadPlugin('AbcExport.mll', qt=True)
cmds.loadPlugin('AbcImport.mll', qt=True)
cmds.loadPlugin('sceneAssembly.mll', qt=True)


class Enum():
    '''Bare bones Enum implementation for python 2'''
    def __init__(self, *args):
        
        self.reverse_mapping = {}
        self.__dict = {}
        
        for i in range(len(args)):
            self.reverse_mapping[i] = args[i]
            self.__dict[args[i]] = i
    
    def keys(self):
        return self.__dict.keys()
    
    def __getattr__(self, sAttribname):
        try:
            return self.__dict[sAttribname]
        except:
            raise AttributeError



#Export types, determine what function to use (abcExport, gpuCache, file)
SAExportType = Enum( 'Alembic', 'GpuCache', 'Scene' )

#Reference types, defined by the plugin
SAAssetType = Enum( *cmds.adskRepresentation(q=True, lrt=True) )


class SABase(object):
    '''
    Scene assembly base class, shared functionality
    between reference and definition node wrappers
    '''
    _nodename = None
    _node = None
    
    @property
    def nodename(self):
        return self._nodename
        
    @nodename.setter
    def nodename(self, sNewNodePath):
        if cmds.objExists(sNewNodePath):
            sFullPath = cmds.ls(sNewNodePath, l=True, type=self._wrappedType)[0]
            if not sFullPath:
                cmds.error('Attempting to swap scene assembly node %s with %s, but new node is not of type %s, ignored'%(self._nodename, sNewNodePath, self.__wrappedType))
                return
            self._nodename = sFullPath
            li = MSelectionList()
            MGlobal.getSelectionListByName(self._nodename, li)
            obj = MObject()
            li.getDependNode(0, obj)
            if obj.isNull():
                cmds.error('Attempting to swap scene assembly node %s with %s, but MObject could not be found, ignored'%(self._nodename, sNewNodePath))
                return
            self._node = MFnAssembly( obj )
        else:
            cmds.error('Attempting to swap scene assembly node %s with non existing node %s, ignored'%(self._nodename, sNewNodePath))
            return

    @property
    def activeRepresentationName(self):
        return self._node.getActive()
    
    @activeRepresentationName.setter
    def activeRepresentationName(self, sNewName):
        bValidName = False
        
        #validate name
        iaValidIndices = cmds.getAttr('%s.representations'%self.nodename, multiIndices=True)
        if not iaValidIndices:
            iaValidIndices = []
        for iValidIndex in iaValidIndices:
            if sNewName == cmds.getAttr('%s.representations[%s].repName'%(self.nodename, iValidIndex)):
                bValidName = True
        
        if not bValidName:
            cmds.error('Attempting to activate representation %s on assembly node, but %s has no representation with that name, ignored.'%(sNewName, self._nodename))
            return
        
        #set name
        self._node.activate(sNewName)
    
    
    def __init__(self):
        '''
        ABSTRACT CLASS, do not initialize
        '''
        cmds.error('Initializing SABase, but this is an abstract class. You probably intend to use SAReference or SADefinition.')
        return


class SAReference(SABase):
    '''
    Scene assembly helper class to represent a referenced
    asset in code
    
    NOTE: It should be possible to bind class to existing
    node when working from existing scenes / data so when
    extending take this class implement this funcitonality!
    '''
    
    
    #set the node type for this class, important for error handling
    _wrappedType = 'assemblyReference'
    
    
    def __init__(self, sNodeFullPath=None):
        '''
        @param sNodeFullPath: string, full path name of the
        existing assemblyReference node to bind this object
        instance to.
        '''
        
        #if no argument is given, initialize a blank class
        if sNodeFullPath == None:
            self.nodename = cmds.createNode('assemblyReference')
            return
        
        #else wrap the node given
        if not type(sNodeFullPath) in (unicode, str):
            cmds.error('Trying to initialize SAAsset from %s but argument is not a string'%sNodeFullPath)
            return
        sPath = cmds.ls(sNodeFullPath, type='assemblyReference', l=True)
        if not sPath:
            cmds.error('Trying to initialize SAAsset from %s but argument is not a valid assemblyReference node'%sNodeFullPath)
            return
        self.nodename = sPath[0] 
    
    
    @classmethod
    def CreateFromFile(cls, sFilePath):
        '''
        Given a filepath this creates a reference nodes and connects the path
        It is not capable of reading information of the file beforehand, so just
        like maya's builtin create assembly reference menu it gives errors upon
        importing a file without a reference node and does not obey the LOD saved
        inside the referenced file.
        
        @TODO:
        This function does not work! It just appears to use do regular import...
        '''
        if not os.path.exists(sFilePath):
            cmds.error('Attempting to create scene assembly reference to %s, but file does not exist, ignored.'%sFilePath)
            return
        outInstance = SAReference()
        
        #rename the node
        sFileName = sFilePath.replace('\\','/').rsplit('/',1)[-1].rsplit('.',1)[0]
        outInstance.nodename = cmds.rename(outInstance.nodename, sFileName)
        
        #set the file path
        #POSTLOAD fails and leaves us with a bunch of nodes so
        #let's search for the assemblyDefinition ourselves and
        #keep things clean eh!
        allNodes = cmds.ls(l=True)
        
        #this should work and newNodes should be empty, but it does not work and leaves a mess
        cmds.setAttr('%s.definition'%outInstance.nodename, sFilePath, type='string')
        
        #get file changes
        newNodes = list( set(cmds.ls(l=True))-set(allNodes) )
        if not newNodes:
            cmds.warning('SAReference.CreateFromFile: Reference definition either worked or file was empty. Returning outInstance assuming it is valid.')
            return outInstance
        
        #get assembly definition
        saValidNodes = cmds.ls(newNodes, type='assemblyDefinition', l=True)
        if not saValidNodes or len(saValidNodes) != 1:
            #too many or too few definitions, clean the file
            cmds.delete(newNodes)
            cmds.delete(outInstance)
            cmds.error('Attempting to set assembly reference file to %s but 0 or more than 1 definition nodes were found. File could not be referenced, assemblyReference node removed.'%sFilePath)
            return
        
        iaValidInidices = cmds.getAttr('%s.representations'%saValidNodes[0], multiIndices=True)
        if iaValidInidices:
            #copy all representations
            for i in iaValidInidices:
                #get
                sRepName = cmds.getAttr('%s.representations[%s].repName'%(saValidNodes[0], i))
                sRepLabel = cmds.getAttr('%s.representations[%s].repLabel'%(saValidNodes[0], i))
                sRepType = cmds.getAttr('%s.representations[%s].repType'%(saValidNodes[0], i))
                sRepData = cmds.getAttr('%s.representations[%s].repData'%(saValidNodes[0], i))
                #set
                cmds.setAttr('%s.representations[%s].repName'%(outInstance.nodename, i), sRepName, type='string')
                cmds.setAttr('%s.representations[%s].repLabel'%(outInstance.nodename, i), sRepLabel, type='string')
                cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), sRepType, type='string')
                cmds.setAttr('%s.representations[%s].repData'%(outInstance.nodename, i), sRepData, type='string')
                
            #apply last representation as default
            if len(iaValidIndices) != 0:
                iFurthest = iaValidIndices[len(iaValidIndices)-1]
                sRepName = cmds.getAttr('%s.representations[%s].repName'%(saValidNodes[0], iFurthest))
                outInstance.activeRepresentationName = sRepName

        cmds.delete(newNodes)
        return outInstance


class SAAsset(SABase):
    '''
    Scene assembly helper class to create and represent an
    asset in code
    
    NOTE: It should be possible to bind class to existing
    node when working from existing scenes / data so when
    extending take this class implement this funcitonality!
    '''
    
    
    #set the node type for this class, important for error handling
    _wrappedType = 'assemblyDefinition'
    
    
    def __init__(self, sNodeFullPath=None):
        '''
        @param sNodeFullPath: string, full path name of the
        existing assemblyDefinition node to bind this object
        instance to.
        '''
        
        #if no argument is given, initialize a blank class
        if sNodeFullPath == None:
            self.nodename = cmds.createNode('assemblyDefinition')
            return
        
        #else wrap the noe given
        if not type(sNodeFullPath) in (unicode, str):
            cmds.error('Trying to initialize SAAsset from %s but argument is not a string'%sNodeFullPath)
            return
        sPath = cmds.ls(sNodeFullPath, type='assemblyDefinition', l=True)
        if not sPath:
            cmds.error('Trying to initialize SAAsset from %s but argument is not a valid assemblyDefinition node'%sNodeFullPath)
            return
        self.nodename = sPath[0]
    
    def SaveAsAssembly(self):
        '''
        Exports this asset to a file using currentSceneName_alembic_assembly
        
        @returns: string, the new file path

        @TODO:
        support suffixing (don't assume alembic),
        support multiple assets exported from one file (so not based on scene name)
        '''
        sCurrentFile = cmds.file(q=True, sn=True)
        if not sCurrentFile:
            cmds.error('Scene needs to be saved first, subfolder and LOD files will be created next to it')
            return
        sSceneType = cmds.file(q=True, type=True)[0]
        sCurrentDirectory, sCurrentFileName = sCurrentFile.replace('\\','/').rsplit('/', 1)
        sCurrentFileName, sCurrentExtension = sCurrentFileName.rsplit('.',1)
        
        cmds.select(self.nodename)
        sAssemblyFilePath = '%s/%s_alembic_assembly.%s'%(sCurrentDirectory, sCurrentFileName, sCurrentExtension)
        cmds.file(sAssemblyFilePath, force=True, type=sSceneType, pr=True, es=True);
        
        return sAssemblyFilePath
    
    
    @classmethod
    def CreateFromGroups(cls, saLodGroups, iExportType):
        '''
        @param saLodGroups: string array, full path names of
        each group starting from most detailed to least detailed
        
        @param exportType: SAExportType, defines the export function to use
        
        This function exports each group to a separete file and
        creates a sceneassembly node pointing to each file as
        next lod level
        '''
        #get selection
        sSelection = cmds.ls(sl=True, l=True)
        
        #grab info from current scene
        sCurrentFile = cmds.file(q=True, sn=True)
        if not sCurrentFile:
            cmds.error('Scene needs to be saved first, subfolder and LOD files will be created next to it')
            return
        sSceneType = cmds.file(q=True, type=True)[0]
        sCurrentDirectory, sCurrentFileName = sCurrentFile.replace('\\','/').rsplit('/', 1)
        sCurrentFileName = sCurrentFileName.rsplit('.',1)[0]
        
        #generate directory to store lods ins
        sLodDir = '%s/%s_LODs'%(sCurrentDirectory, sCurrentFileName)
        if not os.path.exists(sLodDir):
            os.makedirs(sLodDir)
        
        #create dag asset to put lods into
        outInstance = SAAsset()

        if True: #try:
            #export lods
            for i in range(len(saLodGroups)):
                #get file name
                sOutFileName = '%s_lod%s'%(sCurrentFileName, i)
                #get file full path
                sLodFilePath = '%s/%s'%(sLodDir, sOutFileName)
                
                if SAExportType.GpuCache:
                    iCurrentFrame = cmds.currentTime(q=True)
                    sLodFilePath = '%s.abc'%sLodFilePath
                    sDir, sName = sLodFilePath.replace('\\','/').rsplit('/',1)
                    sGpuCacheFile = cmds.gpuCache(saLodGroups[i], startTime=iCurrentFrame, endTime=iCurrentFrame, directory=sDir, fileName=sName)
                    
                    #append extension to show the file type in the representation name
                    sOutFileName = '%s.abc'%sOutFileName
                    
                    #set node attributes
                    cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), 'Cache', type='string')
                    
                elif SAExportType.Alembic:
                    iCurrentFrame = cmds.currentTime(q=True)
                    sLodFilePath = '%s.abc'%sLodFilePath
                    cmds.AbcExport(j='-frameRange %s %s -root %s -file %s'%(iCurrentFrame, iCurrentFrame, saLodGroups[i], sLodFilePath))
                    
                    #append extension to show the file type in the representation name
                    sOutFileName = '%s.abc'%sOutFileName
                    
                    #set node attributes
                    cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), 'Cache', type='string')
                    
                else:
                    cmds.select(saLodGroups[i])
                    cmds.file(sLodFilePath, force=True, type=sSceneType, pr=True, es=True);
                    
                    #append extension
                    sOutFileName = '%s.%s'%(sOutFileName, sCurrentFile.rsplit('.',1)[-1])
                    sLodFilePath = '%s.%s'%(sLodFilePath, sCurrentFile.rsplit('.',1)[-1])
                    
                    #set node attributes
                    cmds.setAttr('%s.representations[%s].repType'%(outInstance.nodename, i), 'Scene', type='string')
                    
                #set node attributes
                cmds.setAttr('%s.representations[%s].repName'%(outInstance.nodename, i), sOutFileName, type='string')
                cmds.setAttr('%s.representations[%s].repLabel'%(outInstance.nodename, i), sOutFileName, type='string')
                cmds.setAttr('%s.representations[%s].repData'%(outInstance.nodename, i), sLodFilePath, type='string')
                
                #default to furthest lod
                if i == len(saLodGroups)-1:
                    outInstance._node.activate(sOutFileName)
        else: #except:
            cmds.delete(outInstance)
            cmds.select(sSelection)
            cmds.error('LOD exporting and linking failed, no scene assembly definition created.')
            return
        
        #restore selection, make redo easier & avoid confusion
        if sSelection:
            cmds.select(sSelection)
        
        return outInstance

'''
#Usage examples

#Create an assemblyDefinition and for each selected transform: export and add as representation
dagAsset1 = SAAsset.CreateFromGroups( cmds.ls(sl=True, l=True, type='transform'), SAExportType.GpuCache)

#Wrap an existing assemblyDefinition
dagAsset1 = SAAsset( 'dagAsset1' )

#Set the currently visible definition (default is last)
dagAsset1.activeRepresentationName = 'crystal_pylon_lod0.abc'

#Export the wrapped assemblyDefinition to a separate file for referencing
sExportedPath = dagAsset1.SaveAsAssembly()

#Reference a filePath, assuming it contains exactly one assemblyDefinition node (other nodes are discarded)
reference1 = SAReference.CreateFromFile( sExportedPath )

#Set the currently visible definition in the reference (default is last)
reference1.activeRepresentationName = 'crystal_pylon_lod2.abc'
'''

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>