UPDATED: Project relative paths for external file references

If you work with multiple branches, you might be experiencing issues with Mayas preference for resolving Absolut paths before Relative ones for external file references (textures, audio, other Maya files, etc).

If I create a Maya scene in “Branch_A”, integrate that into “Branch_B” and then open the file, Maya will resolve all external file references to their absolute path if possible. This means that all my external file references will point to files in “Branch_A”, even though I’m opening the file in “Branch_B”. Only if Maya can’t resolve the absolute paths, it will try to resolve relative to the current Maya project/workspace.

I wanted to find an easy way to make the file paths relative without:
• Creating custom tools for the creating of each external file reference type
• Adding callbacks to handle when each external file reference type was created
• Updating the file path’s with the Maya scene loaded (this would cause Maya to reload them)

Doing a text search and replace within the Maya file (.ma), post save/export, seemed like the simplest idea. I tried it out and it turned out to be easy to do and very fast performing. So here is some of the code involved:

Code that replaces path in the last saved scene:

 import re
import paths

def make_file_references_relative(path):
    """
    Make external file references project relative, in the last saved file

    The Maya file has to be an ASCII file and the external file
    references have to be within the current Maya project/workspace.

    D:/P4/My_Project/dev/sprint_tech//Construction/texture.tga -> Construction/texture.tga
    """

    if path:
        # Here we must check that the file is type ascii
        if path.lower().endswith(".ma"):
            try:
                f = open(path, "r")
                text = f.read()
            finally:
                f.close()
            try:
                text = remove_old_project_paths(text)
                text = remove_project_path(text)
            except:
                print "make_file_references_relative: Failed to replace paths in file!"
            try:
                f = open(path, "w")
                f.write(text)
            finally:
                f.close()

def remove_old_project_paths(text):
    """
    Remove all valid project paths in input text. Also change Maya .mb type file references to .ma type
    """
    removeDict = dict()

    # Remove old project paths (not current
    pattern = re.compile('"[^"]*"')
    for m in re.finditer(pattern, text):
        for token in ['/Construction/', '/Assembly/', '"Construction/', '"Assembly/']:
            if re.search(token, m.group()):
                tokenIndex = m.group().find(token)
                replaceString = '"' + m.group()[tokenIndex+1:].replace('.mb', '.ma').replace('.Mb', '.ma').replace('.MB', '.ma')
                if not removeDict.has_key(m.group()):
                    removeDict[str(m.group())] = replaceString
                break

    for key in removeDict.iterkeys():
        text = text.replace(key, removeDict[key])

    return text

def remove_project_path(text):
    """
    Return current project path from input text.
    """

    # Remove current project project
    projectPath = paths.project()
    removeString = '//'.join((projectPath.replace("\\", "/"), ""))
    text = text.replace(removeString, "")

    return text
 

Adding callbacks, for this to happen automatically. I’m using the API, since I could not get scriptNode/Job to happen both on save and export:

 import maya.OpenMaya as OpenMaya
import repath

userCallbacks = []

class _Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Callback:
    __metaclass__ = _Singleton   
    
    def __init__(self):
        self.idList = []
        
    def add(self):
        try:
            if self not in userCallbacks:
                userCallbacks.append(self)
        except:
            pass
        
    def remove(self):
        for _id in self.idList:
            OpenMaya.MMessage.removeCallback(_id)
        try:
            userCallbacks.remove(self)
        except:
            pass
        
class Save_Relative_Callback(Callback):
    
    def __init__(self):
        Callback.__init__(self)
        self.create_callbacks()
        
    def create_callbacks(self):
        try:
            self.idList.append(OpenMaya.MSceneMessage.addCallback(OpenMaya.MSceneMessage.kAfterSave, make_file_references_relative_save))
            self.idList.append(OpenMaya.MSceneMessage.addCallback(OpenMaya.MSceneMessage.kAfterExport, make_file_references_relative_export))
        except:
            print "Failed to create callback"
        
class Last_Save_Callback(Callback):
    lastSave = None
    lastExport = None
    def __init__(self):
        Callback.__init__(self)
        self.create_callbacks()
            
    def create_callbacks(self):
        try:
            self.idList.append(OpenMaya.MSceneMessage.addCallback(OpenMaya.MSceneMessage.kBeforeSave, store_last_save))
            self.idList.append(OpenMaya.MSceneMessage.addCallback(OpenMaya.MSceneMessage.kBeforeExport, store_last_export))
        except:
            print "Failed to create callback"
        
        
def make_file_references_relative_save(*args):
    if Last_Save_Callback.lastSave:
        repath.make_file_references_relative(Last_Save_Callback.lastSave)
    
def make_file_references_relative_export(*args):
    if Last_Save_Callback.lastExport:
        repath.make_file_references_relative(Last_Save_Callback.lastExport)

def store_last_save(*args):
    Last_Save_Callback.lastSave = OpenMaya.MFileIO_beforeSaveFilename()

def store_last_export(*args):
    Last_Save_Callback.lastExport = OpenMaya.MFileIO_beforeExportFilename()

def add(callback):
    callback.add()
 

Adding the callback in a Maya start up script:

 import callback
callback.add(callback.Last_Save_Callback())
callback.add(callback.Save_Relative_Callback())
 

I opted to just store the project relative part of the file path. I tried using a environment variable (E.G. %MY_CURRENT_PROJECT%something/somefile.dds). While this worked great, it does not play nice with 3dsMax or MotionBuilder (who both seem oblivious to the idea of using environment variables in paths), when scenes are FBX’ed across. Using the project relative bit works better.

Of course if your pipeline is setup so that your current project/branch is in a folder that you have mounted to a consistent drive letter (with something like SUBST), you might not have these issues in the first place.

Tags: , ,

Sunday, April 7th, 2013 Main

1 Comment to UPDATED: Project relative paths for external file references

  • sune says:

    The original post was updated!

    I realized that the solution I had posed originally was rather flawed, pretty much right away. But I did not get around to updating it until now..

  • Leave a Reply