alembic example - shotgunsoftware/tk-multi-publish GitHub Wiki

Before You Begin

To publish Alembic Cache files from Maya, make sure that the AbcExport plug-in is loaded in the plug-in manager. You`ll also need a Shot scene with some grouped meshes ready to be published.

Step 1 - Prepare the environment

First, install the tk-multi-publish app for the Shot_Step environment:

LINKBOX_APP:shotgunsoftware/tk-multi-publish:See the Multi Publish installation instructions for more information.

As well as adding the app to the environment, this will also download it from the app store and ensure you are running the latest version.

Once installed, open the Shot_Step environment file, typically found at <pipeline_configuration_root>/config/env/Shot_Step.yml and locate the entry for the tk-multi-publish app under the tk-maya engine that you want to modify:

engines:
  tk-maya:
    apps:
      tk-multi-publish:
        ...

Tip: If you want to experiment with a new configuration for an app you can duplicate the entry in the environment and call it something different. Many apps also support a separate display name to help differentiate them in the main Menu.

For this example, modify the app name and display name to be tk-multi-publish-alembic-example and Publish Alembic Example respectively. The initial configuration should now look like:

tk-multi-publish-alembic-example:
  allow_taskless_publishes: true
  display_name: Publish Alembic Example
  expand_single_items: false
  hook_copy_file: default
  hook_post_publish: default
  hook_primary_pre_publish: default
  hook_primary_publish: default
  hook_scan_scene: default
  hook_secondary_pre_publish: default
  hook_secondary_publish: default
  hook_thumbnail: default
  location: {name: tk-multi-publish, type: app_store, version: v0.2.7}
  primary_description: Publish and version up the current work file
  primary_display_name: Current Work File
  primary_icon: ''
  primary_publish_template: maya_shot_publish
  primary_scene_item_type: work_file
  primary_tank_type: Maya Scene
  secondary_outputs: []
  template_work: maya_shot_work

You'll be changing some of these settings but for the full reference take a look at the Multi Publish documentation.

Save the config and then reload the engines and apps in Maya. You'll now have a new Publish Alembic Example entry in the main menu!

Step 2 - Scan the Scene

The first thing the app does is scan the scene for things to publish. To do this, it uses the hook specified by the hook_scan_scene setting. The default implementation just looks for the scene itself, so using this as a starting point, you are going to extend it to additionally find grouped geometry from the scene.

LINKBOX_DOC:11:Read this doc for full instructions on how to work with hooks.

To quickly summarize, you need to do the following to take the default hook as a starting point for this implementation:

  • Locate the default implementation - this can be found in <code_root>/install/apps/app_store/tk-multi-publish/<version>/hooks/scan_scene_tk-maya.py.
  • Copy this hook to the hooks directory in your current pipeline configuration, e.g. <pc_root>/config/hooks
  • Rename it to something unique, e.g. alembic_example_scan_scene.py
  • Update the hook_scan_scene setting to use this hook: hook_scan_scene: alembic_example_scan_scene

The app will now use your version of the scan scene hook so go ahead and open it up.

The hook returns a list of items that it finds from the scene - the default implementation just finds the scene itself so let's extend it so that it also finds all top level groups that contain one or more meshes. To do this, add the following code at the end of the execute method:

...

# look for root level groups that have meshes as children:
for grp in cmds.ls(assemblies=True, long=True):
    if cmds.ls(grp, dag=True, type="mesh"):
        # include this group as a 'mesh_group' type
        items.append({"type":"mesh_group", "name":grp})

...

You'll notice that when it adds an item to the list, it specifies the type: mesh_group:

items.append({"type":"mesh_group", "name":grp})

You'll see in a minute how this type is used by the app to associate outputs with each item - remember this as it's quite important!

---FOLD--- This is the complete version of alembic_example_scan_scene.py

import os
import maya.cmds as cmds

import tank
from tank import Hook
from tank import TankError

class ScanSceneHook(Hook):
    """
    Hook to scan scene for items to publish
    """
    
    def execute(self, **kwargs):
        """
        Main hook entry point
        :returns:       A list of any items that were found to be published.  
                        Each item in the list should be a dictionary containing 
                        the following keys:
                        {
                            type:   String
                                    This should match a scene_item_type defined in
                                    one of the outputs in the configuration and is 
                                    used to determine the outputs that should be 
                                    published for the item
                                    
                            name:   String
                                    Name to use for the item in the UI
                            
                            description:    String
                                            Description of the item to use in the UI
                                            
                            selected:       Bool
                                            Initial selected state of item in the UI.  
                                            Items are selected by default.
                                            
                            required:       Bool
                                            Required state of item in the UI.  If True then
                                            item will not be deselectable.  Items are not
                                            required by default.
                                            
                            other_params:   Dictionary
                                            Optional dictionary that will be passed to the
                                            pre-publish and publish hooks
                        }
        """   
                
        items = []
        
        # get the main scene:
        scene_name = cmds.file(query=True, sn=True)
        if not scene_name:
            raise TankError("Please Save your file before Publishing")
        
        scene_path = os.path.abspath(scene_name)
        name = os.path.basename(scene_path)

        # create the primary item - this will match the primary output 'scene_item_type':            
        items.append({"type": "work_file", "name": name})

        # look for root level groups that have meshes as children:
        for grp in cmds.ls(assemblies=True, long=True):
            if cmds.ls(grp, dag=True, type="mesh"):
                # include this group as a 'mesh_group' type
                items.append({"type":"mesh_group", "name":grp})

        return items

---FOLD---

Now you've told the app how to find mesh groups in the scene, the next step is to tell it what outputs are available for these groups...

Step 3 - Define the Outputs

The Multi Publish app has two types of output:

  • Primary - Typically the Primary output is used to represent your scene file - there can only be a single Primary output.
  • Secondary - Any additional things to be published are represented as Secondary outputs. You can define as many of these as you need.

These are represented in different areas of the main interface:

This example defines a new Alembic Cache secondary output for all top-level mesh groups within the scene. To do this, edit the secondary_outputs setting in your tk-multi-publish-alembic configuration.

Lets fill in some details and then we'll walk through the important bits:

...
secondary_outputs:
- description: 'Publish Alembic Cache data for the model'
  display_group: Caches
  display_name: Alembic Cache
  icon: 'icons/alembic_output.png'
  name: alembic_cache
  publish_template: maya_shot_mesh_alembic_cache
  required: false
  scene_item_type: mesh_group
  selected: false
  tank_type: 'Alembic Cache'
...

Most of these are self explanatory but for full details please see the Multi Publish reference documentation.

The settings you need to care about for this example are:

name: alembic_cache

The name should be unique across all outputs and can't be primary as this is reserved for the primary output. You'll use this name used later when you are doing the actual publishing...

scene_item_type: mesh_group

Remember the type returned for each item in the scan scene hook back in Step 2? Setting the scene_item_type in the output to the same type tells the app that for all items of type mesh_group you want to publish an Alembic Cache output for it.

Tip: This mechanism allows the association of multiple different outputs with a single item type so if you want to add another output later, for example OBJ export, you can do this without having to modify the scan hook again.

publish_template: maya_shot_mesh_alembic_cache

This setting allows you to specify a template to be used for the published Alembic Cache files. You'll have access to this when you do the publish.

LINKBOX_DOC:4:Take a look at this doc for lots of useful information on how to configure templates.

For this example, the following template has been used:

# excerpt from templates.yml
keys:
    ...
    grp_name:
        type: str
        
paths:        
    ...
    maya_shot_mesh_alembic_cache: '@shot_root/publish/maya/{name}[_{grp_name}].v{version}.abc'

If you are using the default config you can add this to your templates.yml file. Otherwise you may have to tweak the template and/or hooks to work correctly.

tank_type: Alembic Cache

This is the unique publish type that will be used when registering your Alembic Cache files in Shotgun. Again, you'll have access to this when you do the publish.

icon: 'icons/alembic_output.png'

This is the icon the app will use to represent outputs of this new type. Save this icon:

to your configuration's 'icons' directory, e.g. <pc_root>/config/icons/alembic_output.png

Lets see what this looks like. Save the config, reload the app in Maya, open up your test Shot scene and run the Publish Alembic Example command from the menu. If everything has gone to plan, you will now find some Secondary Publishes listed in the UI!

Don't click Publish just yet as you still need to add the code that exports the Alembic Cache files and registers them with Shotgun.

Step 4 - Pre-Publish Validation

Publishing is split into three stages:

  • Pre-Publish
  • Publish
  • Post-Publish.

The first of these, Pre-Publish, allows you to do any validation of the items to be published and report any issues back to the user.

As with scanning the scene, Pre-Publish is implemented via a hook which is defined in the app configuration by the hook_secondary_pre_publish setting.

Follow the instructions in Step 2 to copy the default hook <code_root>/install/apps/app_store/tk-multi-publish/<version>/hooks/secondary_pre_publish_tk-maya.py to your configuration hooks directory and rename it to alembic_example_secondary_pre_publish.py.

Then edit the setting in the configuration to:

hook_secondary_pre_publish: alembic_example_secondary_pre_publish

The app will now use your version of the Pre-Publish hook so go ahead and open it up.

The hook takes a list of tasks as it's main input:

def execute(self, tasks, work_template, progress_cb, **kwargs):
    ...

This list is based on the users selection from the UI so it only contains things they have selected to be published:

Each task is a dictionary containing an item and an output.

  • The item is the one returned from the scan scene hook back in Step 2.
  • The output contains the name, publish_template and tank_type you configured for the Secondary output in Step 3.

The default implementation for this hook just provides a scaffold to iterate over all the tasks selected by the user and reports an error if it doesn't know how to handle an output.

The user might have deleted or modified the group returned by the scan scene hook so this implementation needs to be extended to make sure they haven't and that everything is ready to be published.

To do this, first replace:

# pre-publish item here, e.g.
#if output["name"] == "foo":
#    ...
#else:
# don't know how to publish this output types!
errors.append("Don't know how to publish this item!")

with

# pre-publish alembic_cache output
if output["name"] == "alembic_cache":
    errors.extend(self._validate_item_for_alembic_cache_publish(item))
else:
	# don't know how to publish this output types!
	errors.append("Don't know how to publish this item!")

Then add a new validation method to the PrePublishHook class:

def _validate_item_for_alembic_cache_publish(self, item):
    """
    Validate that the item is valid to be exported 
    to an alembic cache
    """
    errors = []
    # check that the group still exists:
    if not cmds.objExists(item["name"]):
        errors.append("This group couldn't be found in the scene!")
        
    # and that it still contains meshes:
    elif not cmds.ls(item["name"], dag=True, type="mesh"):
        errors.append("This group doesn't appear to contain any meshes!")
        
    # finally return any errors
    return errors

Any problems it finds are returned so that the user is given an opportunity to fix them before publishing. You can of course extend this to do much more comprehensive validation of the items.

---FOLD--- This is the complete version of alembic_example_secondary_pre_publish.py

import os
import maya.cmds as cmds

import tank
from tank import Hook
from tank import TankError

class PrePublishHook(Hook):
    """
    Single hook that implements pre-publish functionality
    """
    def execute(self, tasks, work_template, progress_cb, **kwargs):
        """
        Main hook entry point
        :tasks:         List of tasks to be pre-published.  Each task is be a 
                        dictionary containing the following keys:
                        {   
                            item:   Dictionary
                                    This is the item returned by the scan hook 
                                    {   
                                        name:           String
                                        description:    String
                                        type:           String
                                        other_params:   Dictionary
                                    }
                                   
                            output: Dictionary
                                    This is the output as defined in the configuration - the 
                                    primary output will always be named 'primary' 
                                    {
                                        name:             String
                                        publish_template: template
                                        tank_type:        String
                                    }
                        }
                        
        :work_template: template
                        This is the template defined in the config that
                        represents the current work file
               
        :progress_cb:   Function
                        A progress callback to log progress during pre-publish.  Call:
                        
                            progress_cb(percentage, msg)
                             
                        to report progress to the UI
                        
        :returns:       A list of any tasks that were found which have problems that
                        need to be reported in the UI.  Each item in the list should
                        be a dictionary containing the following keys:
                        {
                            task:   Dictionary
                                    This is the task that was passed into the hook and
                                    should not be modified
                                    {
                                        item:...
                                        output:...
                                    }
                                    
                            errors: List
                                    A list of error messages (strings) to report    
                        }
        """       
        results = []
        
        # validate tasks:
        for task in tasks:
            item = task["item"]
            output = task["output"]
            errors = []
        
            # report progress:
            progress_cb(0, "Validating", task)
        
             # pre-publish alembic_cache output
            if output["name"] == "alembic_cache":
                errors.extend(self._validate_item_for_alembic_cache_publish(item))
            else:
                # don't know how to publish this output types!
                errors.append("Don't know how to publish this item!")       

            # if there is anything to report then add to result
            if len(errors) > 0:
                # add result:
                results.append({"task":task, "errors":errors})
                
            progress_cb(100)
            
        return results

    def _validate_item_for_alembic_cache_publish(self, item):
        """
        Validate that the item is valid to be exported 
        to an alembic cache
        """
        errors = []
        # check that the group still exists:
        if not cmds.objExists(item["name"]):
            errors.append("This group couldn't be found in the scene!")
            
        # and that it still contains meshes:
        elif not cmds.ls(item["name"], dag=True, type="mesh"):
            errors.append("This group doesn't appear to contain any meshes!")
            
        # finally return any errors
        return errors

---FOLD---

Step 5 - Publish

The Publish stage is where you are going to export an Alembic Cache file to the publish path for each item and then register them with Shotgun. For this example, this is also the final thing you need to do.

This stage is also implemented via a hook which is defined in the app configuration by the hook_secondary_publish setting.

Again, follow the previous instructions to copy the default hook <code_root>/install/apps/app_store/tk-multi-publish/<version>/hooks/secondary_publish_tk-maya.py to your configuration hooks directory and rename it to alembic_example_secondary_publish.py. Then edit the setting in the configuration to:

hook_secondary_publish: alembic_example_secondary_publish

The app will now use your version of the Publish hook so go ahead and open it up.

The Publish hook is also provided a list of tasks together with a few more inputs:

def execute(self, tasks, work_template, comment, thumbnail_path, 
            sg_task, primary_publish_path, progress_cb, **kwargs):
    ...
  • The comment, thumbnail_path and sg_task represent the information provided by the user through the UI.
  • The primary_publish_path is the path of the primary publish, in this case the scene file itself.

As with the Pre-Publish hook, the default implementation is just a scaffold for iterating over the tasks so the first thing to do is add some code so that it can handle the new alembic_cache output.

To do this, replace:

# publish item here, e.g.
#if output["name"] == "foo":
#    ...
#else:
# don't know how to publish this output types!
errors.append("Don't know how to publish this item!")

with:

# publish alembic_cache output
if output["name"] == "alembic_cache":
    try:
       self._publish_alembic_cache_for_item(item, output, work_template, primary_publish_path, 
                                            sg_task, comment, thumbnail_path, progress_cb)
    except Exception, e:
	   errors.append("Publish failed - %s" % e)
else:
	# don't know how to publish this output types!
	errors.append("Don't know how to publish this item!")

Next, add a new publish method to the PublishHook class:

def _publish_alembic_cache_for_item(self, item, output, work_template, primary_publish_path, 
                                    sg_task, comment, thumbnail_path, progress_cb)
    ...

Lets look at what this new method does.

First, it uses the work_template to extract a dictionary of fields from the current scene path:

    scene_path = os.path.abspath(cmds.file(query=True, sn=True))
    fields = work_template.get_fields(scene_path)
    publish_version = fields["version"]

Next, it adds the group name to the fields and then builds the publish path to use for the Alembic Cache file:

    fields["grp_name"] = group_name
    publish_path = publish_template.apply_fields(fields)

Using this path, it then uses the Alembic Cache export command AbcExport to export the group to the publish path:

    frame_start = int(cmds.playbackOptions(q=True, min=True))
    frame_end = int(cmds.playbackOptions(q=True, max=True))
    abc_publish_path = publish_path.replace("\\", "/")
    abc_export_cmd = ("AbcExport -j \"-fr %d %d -root %s -file %s\"" 
                        % (frame_start, frame_end, item["name"], abc_publish_path))
    try:
        self.parent.log_debug("Executing command: %s" % abc_export_cmd)
        mel.eval(abc_export_cmd)
    except Exception, e:
        raise TankError("Failed to export Alembic Cache: %s" % e)

Finally, it registers the published Alembic Cache file with Shotgun:

    self._register_publish(publish_path, ...

And that's it!

---FOLD--- This is the complete alembic_example_secondary_publish.py

import os
import shutil
import maya.cmds as cmds
import maya.mel as mel

import tank
from tank import Hook
from tank import TankError

class PublishHook(Hook):
    """
    Single hook that implements publish functionality for secondary tasks
    """    
    def execute(self, tasks, work_template, comment, thumbnail_path, sg_task, primary_publish_path, progress_cb, **kwargs):
        """
        Main hook entry point
        :tasks:         List of secondary tasks to be published.  Each task is a 
                        dictionary containing the following keys:
                        {
                            item:   Dictionary
                                    This is the item returned by the scan hook 
                                    {   
                                        name:           String
                                        description:    String
                                        type:           String
                                        other_params:   Dictionary
                                    }
                                   
                            output: Dictionary
                                    This is the output as defined in the configuration - the 
                                    primary output will always be named 'primary' 
                                    {
                                        name:             String
                                        publish_template: template
                                        tank_type:        String
                                    }
                        }
                        
        :work_template: template
                        This is the template defined in the config that
                        represents the current work file
               
        :comment:       String
                        The comment provided for the publish
                        
        :thumbnail:     Path string
                        The default thumbnail provided for the publish
                        
        :sg_task:       Dictionary (shotgun entity description)
                        The shotgun task to use for the publish    
                        
        :primary_publish_path: Path string
                        This is the path of the primary published file as returned
                        by the primary publish hook
                        
        :progress_cb:   Function
                        A progress callback to log progress during pre-publish.  Call:
                        
                            progress_cb(percentage, msg)
                             
                        to report progress to the UI
        
        :returns:       A list of any tasks that had problems that need to be reported 
                        in the UI.  Each item in the list should be a dictionary containing 
                        the following keys:
                        {
                            task:   Dictionary
                                    This is the task that was passed into the hook and
                                    should not be modified
                                    {
                                        item:...
                                        output:...
                                    }
                                    
                            errors: List
                                    A list of error messages (strings) to report    
                        }
        """
        results = []
        
        # publish all tasks:
        for task in tasks:
            item = task["item"]
            output = task["output"]
            errors = []
        
            # report progress:
            progress_cb(0, "Publishing", task)

            # publish alembic_cache output
            if output["name"] == "alembic_cache":
                try:
                    self._publish_alembic_cache_for_item(item, output, work_template, primary_publish_path, sg_task, comment, thumbnail_path, progress_cb)
                except Exception, e:
                    errors.append("Publish failed - %s" % e)
            else:
                # don't know how to publish this output types!
                errors.append("Don't know how to publish this item!")
                
            # if there is anything to report then add to result
            if len(errors) > 0:
                # add result:
                results.append({"task":task, "errors":errors})
             
            progress_cb(100)
             
        return results

    def _publish_alembic_cache_for_item(self, item, output, work_template, primary_publish_path, sg_task, comment, thumbnail_path, progress_cb):
        """
        Export an Alembic cache for the specified item and publish it
        to Shotgun.
        """
        group_name = item["name"].strip("|")
        tank_type = output["tank_type"]
        publish_template = output["publish_template"]        
        
        # get the current scene path and extract fields from it
        # using the work template:
        scene_path = os.path.abspath(cmds.file(query=True, sn=True))
        fields = work_template.get_fields(scene_path)
        publish_version = fields["version"]
        
        # update fields with the group name:
        fields["grp_name"] = group_name

        # create the publish path by applying the fields 
        # with the publish template:
        publish_path = publish_template.apply_fields(fields)
        
        # build and execute the Alembic export command for this item:
        frame_start = int(cmds.playbackOptions(q=True, min=True))
        frame_end = int(cmds.playbackOptions(q=True, max=True))
        # The AbcExport command expects forward slashes!
        abc_publish_path = publish_path.replace("\\", "/")
        abc_export_cmd = "AbcExport -j \"-fr %d %d -root %s -file %s\"" % (frame_start, frame_end, item["name"], abc_publish_path)
        try:
            self.parent.log_debug("Executing command: %s" % abc_export_cmd)
            mel.eval(abc_export_cmd)
        except Exception, e:
            raise TankError("Failed to export Alembic Cache: %s" % e)
        
        # Finally, register this publish with Shotgun
        self._register_publish(publish_path, 
                               group_name, 
                               sg_task, 
                               publish_version, 
                               tank_type,
                               comment,
                               thumbnail_path, 
                               [primary_publish_path])
        
    def _register_publish(self, path, name, sg_task, publish_version, tank_type, comment, thumbnail_path, dependency_paths=None):
        """
        Helper method to register publish using the 
        specified publish info.
        """
        # construct args:
        args = {
            "tk": self.parent.tank,
            "context": self.parent.context,
            "comment": comment,
            "path": path,
            "name": name,
            "version_number": publish_version,
            "thumbnail_path": thumbnail_path,
            "task": sg_task,
            "dependency_paths": dependency_paths,
            "published_file_type":tank_type,
        }
        
        # register publish;
        sg_data = tank.util.register_publish(**args)
        
        return sg_data

---FOLD---

---FOLD--- This is the complete tk-multi-publish-alembic-example configuration in the Shot_Step environment.

tk-multi-publish-alembic-example:
  allow_taskless_publishes: true
  display_name: Publish Alembic Example
  expand_single_items: false
  hook_copy_file: default
  hook_post_publish: default
  hook_primary_pre_publish: default
  hook_primary_publish: default
  hook_scan_scene: alembic_example_scan_scene
  hook_secondary_pre_publish: alembic_example_secondary_pre_publish
  hook_secondary_publish: alembic_example_secondary_publish
  hook_thumbnail: default
  location: {name: tk-multi-publish, type: app_store, version: v0.2.7}
  primary_description: Publish and version up the current work file
  primary_display_name: Current Work File
  primary_icon: ''
  primary_publish_template: maya_shot_publish
  primary_scene_item_type: work_file
  primary_tank_type: Maya Scene
  secondary_outputs:
  - description: 'Publish Alembic Cache data for the model'
    display_group: Caches
    display_name: Alembic Cache
    icon: 'icons/alembic_output.png'
    name: alembic_cache
    publish_template: maya_shot_mesh_alembic_cache
    required: false
    scene_item_type: mesh_group
    selected: false
    tank_type: 'Alembic Cache'
  template_work: maya_shot_work

---FOLD---

Everything is now setup to publish Alembic Caches for referenced Assets in a Shot scene!

Save the config, reload the app in Maya, open up your test Shot scene and run the Publish Alembic Example command from the menu.

Make sure the Alembic Cache items found by your custom scan hook are selected in the UI, add a comment and hit Publish. The app will first validate that the references are valid using your new pre publish hook. If everything is ok it will then export Alembic Caches and register them as Shotgun published files using your new publish hook!

⚠️ **GitHub.com Fallback** ⚠️