# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import hou
import sgtk
[docs]HookBaseClass = sgtk.get_hook_baseclass()
# A dict of dicts organized by category, type and output file parm
[docs]_HOUDINI_OUTPUTS = {
# rops
hou.ropNodeTypeCategory(): [
"alembic", # alembic cache
"geometry", # geometry
"ifd", # mantra render node
"arnold", # arnold render node
],
# sops
hou.sopNodeTypeCategory(): [
"rop_alembic", # alembic cache
"rop_geometry", # geometry
],
}
[docs]def _iter_output_nodes():
"""Iterate over all output nodes in the scene.
For a more functional programming approach, you can try::
for category, type_names in _HOUDINI_OUTPUTS.items():
node_types = (hou.nodeType(category, name) for name in type_names)
for node_type in filter(None, node_types): # Strip None node_type
yield category, node_type, node_type.instances() or ()
:return: Node category, node type and all node instances.
:rtype: Generator[hou.NodeTypeCategory, hou.NodeType, tuple[hou.Node]]
"""
for category, type_names in _HOUDINI_OUTPUTS.items():
for name in type_names:
node_type = hou.nodeType(category, name)
if node_type is not None:
yield category, node_type, node_type.instances() or ()
[docs]class HoudiniSessionCollector(HookBaseClass):
"""
Collector that operates on the current houdini session. Should inherit from
the basic collector hook.
"""
[docs] def _get_icon_path(self, icon_name, icons_folders=None):
icons_path = os.path.join(self.disk_location, "icons")
if icons_folders:
icons_folders.append(icons_path)
else:
icons_folders = [icons_path]
return super(HoudiniSessionCollector, self)._get_icon_path(
icon_name, icons_folders=icons_folders
)
@property
[docs] def common_file_info(self):
"""Get a mapping of file types information.
Sets up/stores it as `self._common_file_info` if not initialised.
:return: File types information
:rtype: dict[str, dict[str]]
"""
if not hasattr(self, "_common_file_info"):
# do this once to avoid unnecessary processing
self._common_file_info = {
"Alembic Cache": {
"extensions": ["abc"],
"icon": self._get_icon_path("alembic.png"),
"item_type": "file.alembic",
},
"Geometry Cache": {
"extensions": ["geo", "bgeo.sc", "sc", "vdb"],
"icon": self._get_icon_path("geometry.png"),
"item_type": "file.houdini.geometry",
},
"Rendered Image": {
"extensions": ["exr", "rat"],
"icon": self._get_icon_path("image_sequence.png"),
"item_type": "file.image",
},
"Ass File": {
"extensions": ["ass"],
"icon": self._get_icon_path("ass_file.png"),
"item_type": "file.arnold.ass",
},
"Ifd Cache": {
"extensions": ["ifd"],
"icon": self._get_icon_path("ifd_file.png"),
"item_type": "file.houdini.ifd",
},
"Material X File": {
"extensions": ["mtlx"],
"icon": self._get_icon_path("materialX.png"),
"item_type": "file.arnold.mtlx",
},
}
return self._common_file_info
@property
[docs] def settings(self):
"""
Dictionary defining the settings that this collector expects to receive
through the settings parameter in the process_current_session and
process_file methods.
A dictionary on the following form::
{
"Settings Name": {
"type": "settings_type",
"default": "default_value",
"description": "One line description of the setting"
}
The type string should be one of the data types that toolkit accepts as
part of its environment configuration.
"""
# grab any base class settings
collector_settings = super(HoudiniSessionCollector, self).settings or {}
# settings specific to this collector
houdini_session_settings = {
"Work Template": {
"type": "template",
"default": None,
"description": (
"Template path for artist work files. Should "
"correspond to a template defined in "
"templates.yml. If configured, is made available"
"to publish plugins via the collected item's "
"properties. "
),
},
}
# update the base settings with these settings
collector_settings.update(houdini_session_settings)
return collector_settings
[docs] def process_current_session(self, settings, parent_item):
"""
Analyzes the current Houdini session and parents a subtree of items
under the parent_item passed in.
:param dict settings: Configured settings for this collector
:param parent_item: Root item instance
"""
# create an item representing the current houdini session
item = self.collect_current_houdini_session(settings, parent_item)
# collect other, non-toolkit outputs to present for publishing
self.collect_node_outputs(settings, item)
[docs] def collect_current_houdini_session(self, settings, parent_item):
"""
Creates an item that represents the current houdini session.
:param dict settings: Configured settings for this collector
:param parent_item: Parent Item instance
:returns: Item of type houdini.session
"""
publisher = self.parent
# get the path to the current file
path = hou.hipFile.path()
# determine the display name for the item
if path:
file_info = publisher.util.get_file_path_components(path)
display_name = file_info["filename"]
else:
display_name = "Current Houdini Session"
# create the session item for the publish hierarchy
session_item = parent_item.create_item(
"houdini.session", "Houdini File", display_name
)
# get the icon path to display for this item
icon_path = os.path.join(self.disk_location, "icons", "houdini.png")
session_item.set_icon_from_path(icon_path)
# if a work template is defined, add it to the item properties so that
# it can be used by attached publish plugins
work_template_setting = settings.get("Work Template")
if work_template_setting:
work_template = publisher.engine.get_template_by_name(
work_template_setting.value
)
# store the template on the item for use by publish plugins. we
# can't evaluate the fields here because there's no guarantee the
# current session path won't change once the item has been created.
# the attached publish plugins will need to resolve the fields at
# execution time.
session_item.properties["work_template"] = work_template
self.logger.debug("Work template defined for Houdini collection.")
self.logger.info("Collected current Houdini session")
return session_item
[docs] def collect_node_outputs(self, settings, parent_item):
"""
Creates items for known output nodes
:param dict settings: Configured settings for this collector
:param parent_item: Parent Item instance
"""
engine = self.parent.engine
for _, node_type, nodes in _iter_output_nodes():
type_name = node_type.name()
get_output_paths_and_templates = None
# iterate over each node
for node in nodes:
if get_output_paths_and_templates is None:
get_output_paths_and_templates = getattr(
engine.node_handler(node),
"get_output_paths_and_templates",
None,
)
if get_output_paths_and_templates is None:
# This isn't an export node or missing handler
self.logger.info("%s node: %s", type_name, node.path())
break
# get the evaluated path parm value
self.logger.info("Processing %s node: %s", type_name, node.path())
paths_and_templates = get_output_paths_and_templates(node)
node_item = parent_item.create_item(
"houdini.node.{}".format(type_name), "", node.name()
)
for path_and_templates in paths_and_templates:
path = path_and_templates["path"]
# Check that something was generated
# Path might point to an image number that doesn't exist, so better
# check the sequence paths also as these have been found already
if not (
os.path.exists(path) or path_and_templates.get("sequence_paths")
):
continue
self.logger.info("Processing %s node: %s", node_type, node.path())
is_sequence = "sequence_paths" in path_and_templates
# allow the base class to collect and create the item. it
# should know how to handle the output path
item = super(HoudiniSessionCollector, self)._collect_file(
node_item, path, frame_sequence=is_sequence
)
if (
item.type_spec == "file.image"
and "is_deep" in path_and_templates
):
# Handle deep renders, which can only be exr files annoyingly
sequence = " Sequence" if is_sequence else ""
item.type_spec = "file.image.deep{}".format(
sequence.lower().replace(" ", ".")
)
item.name = "Deep Image{}".format(sequence)
item.properties["publish_type"] = "Deep Image"
item.set_icon_from_path(self._get_icon_path("deep_image.png"))
if is_sequence:
# self._collect_file doesn't fill in
# sequence_paths correctly so we must do it
sequence_paths = path_and_templates["sequence_paths"]
item.properties["sequence_paths"] = sequence_paths
template = path_and_templates["work_template"]
item.properties["work_template"] = template
item.properties["publish_template"] = path_and_templates[
"publish_template"
]
fields = template.get_fields(path)
publish_name_tokens = []
for key in ["name", "location", "variation", "identifier"]:
token = fields.get(key)
if token:
publish_name_tokens.append(token.title())
publish_name = "{} ({})".format(
", ".join(publish_name_tokens), item.type_display
)
item.properties["publish_name"] = publish_name