HOME LINKEDIN PORTFOLIO RESUME

Joseph Danaher  |  Technical Artist


Here you can find some recent code examples for the tools I wrote during our last project at Z2, Project Wilcox. The design called for a complex sets of assets, from skinned player gear and cosmetics, to a variety of ability animations.

At Z2, I was the product owner of Zen Art Tools for many years, which was a framework written to provide a our artists with workflow enhancements in Maya, and to increase the pipeline efficiency from Maya to engine. During Project Wilcox, I re-wrote the export framework entirely to support Unity and features particular to our game.

Asset Class

Asset Class

  • Provides a common interface for exporting all types of assets
  • Parent class for common subtypes such as Animations, Meshes, and Skeletons
  • Automates the export process to reduce human error and eliminate the need for file hunting
  • Includes methods for pathing, logging and Perforce maintenance
  • The Asset class was designed to avoid rewriting common functions for exporting various types of assets. Apart from the common asset types, our game also needed specific rules for things like gear and cosmetic assets. A factory determines and initializes subtypes, then calls the export functions.

    Show Code
    
    
    class Asset(object):
        """An Asset defines some data we want to export from Maya. It also handles a
        variety of tasks common to all Asset exports
            * Perforce add/checkout
            * Logging
            * Export filepath manipulation and structuring
            * Export filename prefixing
    
        Subclasses should define specific asset types, like Mesh or Animation,
        and typically override prepare_export_scene as needed to properly set
        up the FBX scene.
    
        :param str asset_name: The base name of the asset
        :param str export_basepath: The base path to which we will export
    
        """
        def __init__(self, asset_name, export_basepath):
            self.asset_name = asset_name
            self.export_basepath = export_basepath
            self.source_filepath = pm.sceneName()
            self._prefix = None
            self._export_log = None
    
        @property
        def subpath(self):
            """Certain assets need a subpath beyond the reflected path.
            This property is for subclasses to override
            :return: The export subpath
            :rtype str:
            """
            return ""
    
        @property
        def additional_tags(self):
            """Override method for subclasses to add tags to the filename
            :return: List of tags
            :rtype list:
            """
            return []
    
        @property
        def prefix(self):
            """Gets the exported filename's prefix by analyzing the source path
            :rtype str:
            """
            # Getting the prefix is non-trivial, so let's only do it once
            if self._prefix == None:
                self._prefix = pf.generate_filename_prefix(self.source_filepath, self.additional_tags)
            return self._prefix
    
        @property
        def filename(self):
            """Creates the export filepath, which is the asset name, with the
            generated prefix and file extension (.fbx)
            :return: The export filename
            :rtype str:
            """
            return self.prefix + self.asset_name + ".fbx"
    
        @property
        def export_filepath(self):
            """Creates the final export filepath, which combines the export_basepath,
            subpath, and filename.
            :return: The export filepath
            :rtype str:
            """
            return os.path.join(os.path.sep, self.export_basepath, self.subpath, self.filename)
    
        def _checkout_files(self):
            '''Handles the perforce checkout step.
            :rtype NoneType:
            '''
            meta_filepath = self.export_filepath + ".meta"
            workspace = cfg.perforce_workspace
            p4.checkout_files([workspace, self.export_filepath, meta_filepath])
    
        def _ensure_dirs(self, filePath):
            """Ensures that a path exists before we start placing files there.
            This will creates the directory structure if it does not exist.
            :rtype NoneType:
            """
            p, f = os.path.split( filePath )
            if not os.path.isdir( p ):
                os.makedirs( p )
    
        @property
        def export_log(self):
            return self._export_log
    
        @export_log.setter
        def export_log(self, messages):
            self._export_log = messages
    
        def print_export_log(self):
            if self.export_log:
                print ExportLog(self.export_log)
    
        def prepare_export_scene(self):
            """This method should be overridden if the scene needs preparation
            for the export. The FBX exporter is selection-based,
            so this is typically for parenting the skeleton to the asset group
            and making the right selection before export.
            :rtype NoneType:
            """
            pass
    
        def export(self):
            """Perfoms all the steps necessary for exporting the asset.
                * Ensures the directories exist
                * Checks out or adds files to perforce
                * Creates the selection for the FBX scene
                * Provides some logging
                * Executes the FBX exporter
            :rtype NoneType:
            """
            # Ensure the dir path exists
            self._ensure_dirs(self.export_filepath)
    
            # Perforce checkout/add
            self._checkout_files()
    
            # Prepare the export object
            self.prepare_export_scene()
    
            # Export logging
            self.print_export_log()
    
            # Perform Export
            fbx.export_asset(self.export_filepath)
    
              

    Mesh Subclass

    Mesh Subclass

  • Handles both bound and static meshes
  • Sets up a hierarchically consistant stucture for animation and mesh stitching
  • Inherits Asset, inherited by meshes with special rules
  • This class handles all basic mesh types. In our pipeline, the rig was referenced into the scene and the mesh was skinned to it. At export time, the skeleton is parented to the asset group, ensuring that the hierarchy was the same across all types of assets.

    Show Code
    
    class Mesh(Asset):
        """A Mesh defines an Asset that is made of one or many submeshes.
        These meshes can be either static or bound, and this object will determine
        what to do in either case.
        """
        def __init__(self, asset_group, export_basepath):
            Asset.__init__(self, asset_group.name, export_basepath)
            self.asset_group = asset_group
            self.objects = pm.listRelatives(self.asset_group.grp, allDescendents=True, type = "transform")
            self._skeleton = None
            self.export_log = self._log
    
        @property
        def subpath(self):
            """Meshes go into the "meshes" subpath
            :rtype str:
            """
            return "meshes"
    
        @property
        def _is_bound_mesh(self):
            """Determines if the meshes are bound or not
            :rtype bool:
            """
            retval = False
            for transform in self.objects:
                if any(x for x in pm.listConnections(transform.getShape()) if pm.nodeType(x) == "skinCluster"):
                    retval = True
            return retval
    
        @property
        def skeleton(self):
            """If the mesh has a skeleton, ensure it is exported alongside the meshes. 
            We only supprt one skeleton per FBX scene.
            :raises ValueError: If the meshes do not all reference the same skeleton
            :return: None or The skeleton's root node
            :rtype NoneType or PyNode:
            """
            retval = None
            if self._skeleton == None:
                if self._is_bound_mesh:
                    root_joint = set()
                    for transform in self.objects:
                        root_joint.add(get_root_joint_for_mesh_transform(transform))
                    root_joint.discard(None)
    
                    if len(root_joint) > 1:
                        error_msg = "There can be only one skeleton per FBX scene. "
                        error_msg += "Consider arranging these assets into different groups."
                        raise ValueError(error_msg)
                    self._skeleton = root_joint.pop()
            return self._skeleton
    
        @property
        def _log(self):
            """Creates information we want to send to the export log.
            :return: The list of messages to send to the logger
            :rtype list:
            """
            content = [
                "Exporting Asset: {0}".format(self.asset_name),
                "Filename:",
                "  {0}".format(self.filename),
                "Source Filepath:",
                "  {0}".format(self.source_filepath),
                "Export Filepath:",
                "  {0}".format(self.export_filepath)
            ]
            if self.skeleton:
                content.insert(1,"Skeleton:")
                content.insert(2,"  {0}".format(self.skeleton.name().replace(ROOT_JOINT,"")))
            return content
    
        def prepare_export_scene(self):
            """Prepares the scene for export.
            :rtype NoneType:
            """
            if self._is_bound_mesh:
                sk = Skeleton(self.skeleton)
                sk.validate()
                sk.create_export_skeleton()
                pm.parent(self.skeleton, self.asset_group.grp)
            pm.select(self.asset_group.grp, r = True)
              
    File Name Prefixing System

    Automated File Name Prefixing System

  • Automates the construction of tags or attributes needed by the game to identify the asset
  • Generates the prefix for the filename to be used during export path determination
  • Have you ever worked on a game where you needed to reference a spreadsheet just to decrypt what the file name needs to be? Our project depended on these attributes to identify various pieces of gear and environment assets. This system provides a way to contextually derive the prefix from the file path, or by attributes set by the artist in custom AETemplates.

    Show Code
    
    class AttributeMap(object):
        """Creates a mapping of directories to attributes that we can use
        to generate the prefix for a file name.
        """
        def __init__(self):
            """Deserializes the json into an object we can use.
            Errors raised here should be caught by the exporter.
            :raises IOError: If the json file can't be found
            :raises ValueError: If the json is malformed in some way
            """
            try:
                file_obj = open(ATTR_CONFIG_FILEPATH)
                json_dict = json.load(file_obj)
            except ValueError as er:
                error_msg = "The json file is malformed: {0}".format(ATTR_CONFIG_FILEPATH)
                raise ValueError(error_msg, er)
            except IOError as er:
                error_msg = "Could not find the attribute mapping file, try syncing perforce."
                raise IOError(error_msg, er)
    
            self.attribute_map = json_dict["attribute_mapping"]
    
        def __call__(self):
            """Returns the attribute map. This is an order-dependent list of
            key value pairs.
            :return: The attribute map
            :rtype: list
            """
            return self.attribute_map
    
        def __str__(self):
            """Presents the attribute mapping in a readable format.
            """
            retval = ""
            for category in self.attribute_map:
                for k, v in category.iteritems():
                    if k == "__category":
                        retval += "\n\n{0}".format(v)
                    else:
                        retval += "\n\t{0} : {1}".format(k,v)
    
            return retval
    
    
    class FilenamePrefix(object):
        """Generates a filename prefix of attributes for the purpose of
        enforcing naming conventions without needing the user to look them
        up and set them manually.
    
        For example, a path ".../pc/male/gear/armor/wizard/robe.ma" would get
        the prefix "pc_m_arm_wiz__", which is used by the game to identify asset
        types
    
        :param str path: The path to the source file
        :param list additional_attrs: (Optional) Additional tags set by the exporter
        """
        def __init__(self, path, additional_attrs=None):
            self.path = path
            self.additional_attrs = additional_attrs
            self.attribute_map = AttributeMap()
            self.filepath_attributes = self._get_filepath_attributes()
    
        def _get_filepath_attributes(self):
            """Splits the file path into a list and trims it down to just the
            directories between the project and the source file.
            These directories are the potential attributes of a file.
            :return: The list of potential attributes
            :rtype: list
            :raises ValueError: If PROJECT is not in the file path
            """
            dirs = self.path.split(os.path.sep)
            if not PROJECT in dirs:
                error_msg = "{0} was not found in the list of directories. ".format(PROJECT)
                error_msg += "Are you sure this file is in the project path?"
                raise ValueError(error_msg1, error_msg2)
            # We want the last occurance of PROJECT, in case there is a
            # local folder with the same name.
            idx = len(dirs) - dirs[::-1].index(PROJECT) - 1
            dirs = dirs[idx:]
            dirs = [x.lower() for x in dirs]
            return dirs
    
        def create(self):
            """Generates a prefix with the appropriate attributes using the
            settings in the config file.
            :return: The file name prefix
            :rtype: str
            :raises ValueError: If no prefix was generated
            """
            prefix = []
            for category in self.attribute_map():
                for attribute, fragment in category.items():
                    if attribute in self.filepath_attributes:
                        prefix.append(fragment)
    
            # This is used for attributes not present in the filepath.
            if self.additional_attrs:
                prefix.append(additional_attrs)
    
            if prefix:
                # Prefixes are followed by double underscores--add one to the list
                prefix.append("_")
                # Assemble the final prefix string
                prefix = "_".join(prefix)
            else:
                error_msg = "No prefix was generated. Check that the filepath is correct"
                raise ValueError(error_msg)
    
            return prefix
    
    
              
    Logging System

    Logging System

  • Logs a list of messages to the console in an easily readable format
  • This was made with the intent of simplifying tools logs and making them easy to identify. The class accepts a list of strings, then formats them in such a way that they can be contained in a box with a header section.

    Show Code
    
    class LogBox(object):
        """LogBox provides some formatting for a list of messages
        We assume the first element is the title of the log.
    
        :param list content: The list of messages to log
        :param bool headless_log: The log should include a header
        :raises TypeError: If we get type other than list
        """
        def __init__(self, messages):
            if not type(messages) == list:
                raise TypeError("LogBox: List argument expected, got {0}.".format(type(messages)))
            self.messages = messages
            self.messages[1:] = [self._fill_space(x, messages[1:]) for x in messages[1:]]
            self.max_msg_length = len(max(self.messages[1:], key=len))
    
        def __repr__(self):
            """Formats the messages and puts them in two sections, body and header
            :return: The logging string
            :rtype str:
            """
            head = "\n| {0} |".format(self.messages[0])
            head_enclosure = "-" * (len(head) - 1)
            header = "\n{0}{1}".format(head_enclosure,head)
    
            body_enclosure = "\n" + "-" * (self.max_msg_length - 1)
            body = self._top_enclosure(head_enclosure, body_enclosure)
            body += "".join(map(str,self.messages[1:]))
            body += body_enclosure
    
            return header + body
    
        def _fill_space(self, line, lines):
            """Makes space for a box to appear around the body messages
            :return: The body messages string
            :rtype str:
            """
            front = "\n| "
            whitespace = (len(max(lines, key=len)) - len(line)) * " "
            end = " |"
            return front + line + whitespace + end
    
        def _top_enclosure(self, head_enclosure, body_enclosure):
            """Finds the longest border between the body and head enclosures
            :return: The top enclosure for the body
            :rtype str:
            """
            return max([head_enclosure, body_enclosure], key=len)