diff --git a/openpype/hosts/maya/api/alembic.py b/openpype/hosts/maya/api/alembic.py new file mode 100644 index 00000000000..b657262b4db --- /dev/null +++ b/openpype/hosts/maya/api/alembic.py @@ -0,0 +1,306 @@ +import json +import logging +import os + +from maya import cmds # noqa + +from openpype.hosts.maya.api.lib import evaluation + +log = logging.getLogger(__name__) + +# The maya alembic export types +ALEMBIC_ARGS = { + "attr": (list, tuple), + "attrPrefix": (list, tuple), + "autoSubd": bool, + "dataFormat": str, + "dontSkipUnwrittenFrames": bool, + "endFrame": float, + "eulerFilter": bool, + "frameRange": str, # "start end"; overrides startFrame & endFrame + "frameRelativeSample": float, + "melPerFrameCallback": str, + "melPostJobCallback": str, + "noNormals": bool, + "preRoll": bool, + "preRollStartFrame": int, + "pythonPerFrameCallback": str, + "pythonPostJobCallback": str, + "renderableOnly": bool, + "root": (list, tuple), + "selection": bool, + "startFrame": float, + "step": float, + "stripNamespaces": bool, + "userAttr": (list, tuple), + "userAttrPrefix": (list, tuple), + "uvWrite": bool, + "uvsOnly": bool, + "verbose": bool, + "wholeFrameGeo": bool, + "worldSpace": bool, + "writeColorSets": bool, + "writeCreases": bool, # Maya 2015 Ext1+ + "writeFaceSets": bool, + "writeUVSets": bool, # Maya 2017+ + "writeVisibility": bool, +} + + +def extract_alembic( + file, + attr=None, + attrPrefix=None, + dataFormat="ogawa", + endFrame=None, + eulerFilter=True, + frameRange="", + noNormals=False, + preRoll=False, + preRollStartFrame=0, + renderableOnly=False, + root=None, + selection=True, + startFrame=None, + step=1.0, + stripNamespaces=True, + uvWrite=True, + verbose=False, + wholeFrameGeo=False, + worldSpace=False, + writeColorSets=False, + writeCreases=False, + writeNormals=False, + writeFaceSets=False, + writeUVSets=False, + writeVisibility=False +): + """Extract a single Alembic Cache. + + This extracts an Alembic cache using the `-selection` flag to minimize + the extracted content to solely what was Collected into the instance. + + Arguments: + + startFrame (float): Start frame of output. Ignored if `frameRange` + provided. + + endFrame (float): End frame of output. Ignored if `frameRange` + provided. + + frameRange (tuple or str): Two-tuple with start and end frame or a + string formatted as: "startFrame endFrame". This argument + overrides `startFrame` and `endFrame` arguments. + + eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with + an Euler filter. Euler filtering helps resolve irregularities in + rotations especially if X, Y, and Z rotations exceed 360 degrees. + Defaults to True. + + noNormals (bool): When on, normal data from the original polygon + objects is not included in the exported Alembic cache file. + + preRoll (bool): This frame range will not be sampled. + Defaults to False. + + renderableOnly (bool): When on, any non-renderable nodes or hierarchy, + such as hidden objects, are not included in the Alembic file. + Defaults to False. + + selection (bool): Write out all all selected nodes from the + active selection list that are descendents of the roots specified + with -root. Defaults to False. + + uvWrite (bool): When on, UV data from polygon meshes and subdivision + objects are written to the Alembic file. Only the current UV map is + included. + + writeColorSets (bool): Write all color sets on MFnMeshes as + color 3 or color 4 indexed geometry parameters with face varying + scope. Defaults to False. + + writeFaceSets (bool): Write all Face sets on MFnMeshes. + Defaults to False. + + wholeFrameGeo (bool): Data for geometry will only be written + out on whole frames. Defaults to False. + + worldSpace (bool): When on, the top node in the node hierarchy is + stored as world space. By default, these nodes are stored as local + space. Defaults to False. + + writeVisibility (bool): Visibility state will be stored in + the Alembic file. Otherwise everything written out is treated as + visible. Defaults to False. + + writeUVSets (bool): Write all uv sets on MFnMeshes as vector + 2 indexed geometry parameters with face varying scope. Defaults to + False. + + writeCreases (bool): If the mesh has crease edges or crease + vertices, the mesh (OPolyMesh) would now be written out as an OSubD + and crease info will be stored in the Alembic file. Otherwise, + creases info won't be preserved in Alembic file unless a custom + Boolean attribute SubDivisionMesh has been added to mesh node and + its value is true. Defaults to False. + + dataFormat (str): The data format to use for the cache, + defaults to "ogawa" + + step (float): The time interval (expressed in frames) at + which the frame range is sampled. Additional samples around each + frame can be specified with -frs. Defaults to 1.0. + + attr (list of str, optional): A specific geometric attribute to write + out. Defaults to []. + + attrPrefix (list of str, optional): Prefix filter for determining which + geometric attributes to write out. Defaults to ["ABC_"]. + + root (list of str): Maya dag path which will be parented to + the root of the Alembic file. Defaults to [], which means the + entire scene will be written out. + + stripNamespaces (bool): When on, any namespaces associated with the + exported objects are removed from the Alembic file. For example, an + object with the namespace taco:foo:bar appears as bar in the + Alembic file. + + verbose (bool): When on, outputs frame number information to the + Script Editor or output window during extraction. + + preRollStartFrame (float): The frame to start scene + evaluation at. This is used to set the starting frame for time + dependent translations and can be used to evaluate run-up that + isn't actually translated. Defaults to 0. + """ + + # Ensure alembic exporter is loaded + cmds.loadPlugin('AbcExport', quiet=True) + + # Alembic Exporter requires forward slashes + file = file.replace('\\', '/') + + # Ensure list arguments are valid. + attr = attr or [] + attrPrefix = attrPrefix or [] + root = root or [] + + # Pass the start and end frame on as `frameRange` so that it + # never conflicts with that argument + if not frameRange: + # Fallback to maya timeline if no start or end frame provided. + if startFrame is None: + startFrame = cmds.playbackOptions(query=True, + animationStartTime=True) + if endFrame is None: + endFrame = cmds.playbackOptions(query=True, + animationEndTime=True) + + # Ensure valid types are converted to frame range + assert isinstance(startFrame, ALEMBIC_ARGS["startFrame"]) + assert isinstance(endFrame, ALEMBIC_ARGS["endFrame"]) + frameRange = "{0} {1}".format(startFrame, endFrame) + else: + # Allow conversion from tuple for `frameRange` + if isinstance(frameRange, (list, tuple)): + assert len(frameRange) == 2 + frameRange = "{0} {1}".format(frameRange[0], frameRange[1]) + + # Assemble options + options = { + "selection": selection, + "frameRange": frameRange, + "eulerFilter": eulerFilter, + "noNormals": noNormals, + "preRoll": preRoll, + "renderableOnly": renderableOnly, + "uvWrite": uvWrite, + "writeColorSets": writeColorSets, + "writeFaceSets": writeFaceSets, + "wholeFrameGeo": wholeFrameGeo, + "worldSpace": worldSpace, + "writeVisibility": writeVisibility, + "writeUVSets": writeUVSets, + "writeCreases": writeCreases, + "dataFormat": dataFormat, + "step": step, + "attr": attr, + "attrPrefix": attrPrefix, + "stripNamespaces": stripNamespaces, + "verbose": verbose, + "preRollStartFrame": preRollStartFrame + } + + # Validate options + for key, value in options.copy().items(): + + # Discard unknown options + if key not in ALEMBIC_ARGS: + log.warning("extract_alembic() does not support option '%s'. " + "Flag will be ignored..", key) + options.pop(key) + continue + + # Validate value type + valid_types = ALEMBIC_ARGS[key] + if not isinstance(value, valid_types): + raise TypeError("Alembic option unsupported type: " + "{0} (expected {1})".format(value, valid_types)) + + # Ignore empty values, like an empty string, since they mess up how + # job arguments are built + if isinstance(value, (list, tuple)): + value = [x for x in value if x.strip()] + + # Ignore option completely if no values remaining + if not value: + options.pop(key) + continue + + options[key] = value + + # The `writeCreases` argument was changed to `autoSubd` in Maya 2018+ + maya_version = int(cmds.about(version=True)) + if maya_version >= 2018: + options['autoSubd'] = options.pop('writeCreases', False) + + # Format the job string from options + job_args = list() + for key, value in options.items(): + if isinstance(value, (list, tuple)): + for entry in value: + job_args.append("-{} {}".format(key, entry)) + elif isinstance(value, bool): + # Add only when state is set to True + if value: + job_args.append("-{0}".format(key)) + else: + job_args.append("-{0} {1}".format(key, value)) + + job_str = " ".join(job_args) + job_str += ' -file "%s"' % file + + # Ensure output directory exists + parent_dir = os.path.dirname(file) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + if verbose: + log.debug("Preparing Alembic export with options: %s", + json.dumps(options, indent=4)) + log.debug("Extracting Alembic with job arguments: %s", job_str) + + # Perform extraction + print("Alembic Job Arguments : {}".format(job_str)) + + # Disable the parallel evaluation temporarily to ensure no buggy + # exports are made. (PLN-31) + # TODO: Make sure this actually fixes the issues + with evaluation("off"): + cmds.AbcExport(j=job_str, verbose=verbose) + + if verbose: + log.debug("Extracted Alembic to: %s", file) + + return file diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 3a5c6fa10ff..25532504824 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -71,37 +71,6 @@ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] -# The maya alembic export types -_alembic_options = { - "startFrame": float, - "endFrame": float, - "frameRange": str, # "start end"; overrides startFrame & endFrame - "eulerFilter": bool, - "frameRelativeSample": float, - "noNormals": bool, - "renderableOnly": bool, - "step": float, - "stripNamespaces": bool, - "uvWrite": bool, - "wholeFrameGeo": bool, - "worldSpace": bool, - "writeVisibility": bool, - "writeColorSets": bool, - "writeFaceSets": bool, - "writeCreases": bool, # Maya 2015 Ext1+ - "writeUVSets": bool, # Maya 2017+ - "dataFormat": str, - "root": (list, tuple), - "attr": (list, tuple), - "attrPrefix": (list, tuple), - "userAttr": (list, tuple), - "melPerFrameCallback": str, - "melPostJobCallback": str, - "pythonPerFrameCallback": str, - "pythonPostJobCallback": str, - "selection": bool -} - INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000} FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} @@ -1347,178 +1316,6 @@ def is_visible(node, return True - -def extract_alembic(file, - startFrame=None, - endFrame=None, - selection=True, - uvWrite=True, - eulerFilter=True, - dataFormat="ogawa", - verbose=False, - **kwargs): - """Extract a single Alembic Cache. - - This extracts an Alembic cache using the `-selection` flag to minimize - the extracted content to solely what was Collected into the instance. - - Arguments: - - startFrame (float): Start frame of output. Ignored if `frameRange` - provided. - - endFrame (float): End frame of output. Ignored if `frameRange` - provided. - - frameRange (tuple or str): Two-tuple with start and end frame or a - string formatted as: "startFrame endFrame". This argument - overrides `startFrame` and `endFrame` arguments. - - dataFormat (str): The data format to use for the cache, - defaults to "ogawa" - - verbose (bool): When on, outputs frame number information to the - Script Editor or output window during extraction. - - noNormals (bool): When on, normal data from the original polygon - objects is not included in the exported Alembic cache file. - - renderableOnly (bool): When on, any non-renderable nodes or hierarchy, - such as hidden objects, are not included in the Alembic file. - Defaults to False. - - stripNamespaces (bool): When on, any namespaces associated with the - exported objects are removed from the Alembic file. For example, an - object with the namespace taco:foo:bar appears as bar in the - Alembic file. - - uvWrite (bool): When on, UV data from polygon meshes and subdivision - objects are written to the Alembic file. Only the current UV map is - included. - - worldSpace (bool): When on, the top node in the node hierarchy is - stored as world space. By default, these nodes are stored as local - space. Defaults to False. - - eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with - an Euler filter. Euler filtering helps resolve irregularities in - rotations especially if X, Y, and Z rotations exceed 360 degrees. - Defaults to True. - - """ - - # Ensure alembic exporter is loaded - cmds.loadPlugin('AbcExport', quiet=True) - - # Alembic Exporter requires forward slashes - file = file.replace('\\', '/') - - # Pass the start and end frame on as `frameRange` so that it - # never conflicts with that argument - if "frameRange" not in kwargs: - # Fallback to maya timeline if no start or end frame provided. - if startFrame is None: - startFrame = cmds.playbackOptions(query=True, - animationStartTime=True) - if endFrame is None: - endFrame = cmds.playbackOptions(query=True, - animationEndTime=True) - - # Ensure valid types are converted to frame range - assert isinstance(startFrame, _alembic_options["startFrame"]) - assert isinstance(endFrame, _alembic_options["endFrame"]) - kwargs["frameRange"] = "{0} {1}".format(startFrame, endFrame) - else: - # Allow conversion from tuple for `frameRange` - frame_range = kwargs["frameRange"] - if isinstance(frame_range, (list, tuple)): - assert len(frame_range) == 2 - kwargs["frameRange"] = "{0} {1}".format(frame_range[0], - frame_range[1]) - - # Assemble options - options = { - "selection": selection, - "uvWrite": uvWrite, - "eulerFilter": eulerFilter, - "dataFormat": dataFormat - } - options.update(kwargs) - - # Validate options - for key, value in options.copy().items(): - - # Discard unknown options - if key not in _alembic_options: - log.warning("extract_alembic() does not support option '%s'. " - "Flag will be ignored..", key) - options.pop(key) - continue - - # Validate value type - valid_types = _alembic_options[key] - if not isinstance(value, valid_types): - raise TypeError("Alembic option unsupported type: " - "{0} (expected {1})".format(value, valid_types)) - - # Ignore empty values, like an empty string, since they mess up how - # job arguments are built - if isinstance(value, (list, tuple)): - value = [x for x in value if x.strip()] - - # Ignore option completely if no values remaining - if not value: - options.pop(key) - continue - - options[key] = value - - # The `writeCreases` argument was changed to `autoSubd` in Maya 2018+ - maya_version = int(cmds.about(version=True)) - if maya_version >= 2018: - options['autoSubd'] = options.pop('writeCreases', False) - - # Format the job string from options - job_args = list() - for key, value in options.items(): - if isinstance(value, (list, tuple)): - for entry in value: - job_args.append("-{} {}".format(key, entry)) - elif isinstance(value, bool): - # Add only when state is set to True - if value: - job_args.append("-{0}".format(key)) - else: - job_args.append("-{0} {1}".format(key, value)) - - job_str = " ".join(job_args) - job_str += ' -file "%s"' % file - - # Ensure output directory exists - parent_dir = os.path.dirname(file) - if not os.path.exists(parent_dir): - os.makedirs(parent_dir) - - if verbose: - log.debug("Preparing Alembic export with options: %s", - json.dumps(options, indent=4)) - log.debug("Extracting Alembic with job arguments: %s", job_str) - - # Perform extraction - print("Alembic Job Arguments : {}".format(job_str)) - - # Disable the parallel evaluation temporarily to ensure no buggy - # exports are made. (PLN-31) - # TODO: Make sure this actually fixes the issues - with evaluation("off"): - cmds.AbcExport(j=job_str, verbose=verbose) - - if verbose: - log.debug("Extracted Alembic to: %s", file) - - return file - - # region ID def get_id_required_nodes(referenced_nodes=False, nodes=None): """Filter out any node which are locked (reference) or readOnly diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py deleted file mode 100644 index 115c73c0d33..00000000000 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ /dev/null @@ -1,89 +0,0 @@ -from openpype.hosts.maya.api import ( - lib, - plugin -) -from openpype.lib import ( - BoolDef, - TextDef -) - - -class CreateAnimation(plugin.MayaHiddenCreator): - """Animation output for character rigs - - We hide the animation creator from the UI since the creation of it is - automated upon loading a rig. There's an inventory action to recreate it - for loaded rigs if by chance someone deleted the animation instance. - """ - identifier = "io.openpype.creators.maya.animation" - name = "animationDefault" - label = "Animation" - family = "animation" - icon = "male" - - write_color_sets = False - write_face_sets = False - include_parent_hierarchy = False - include_user_defined_attributes = False - - def get_instance_attr_defs(self): - - defs = lib.collect_animation_defs() - - defs.extend([ - BoolDef("writeColorSets", - label="Write vertex colors", - tooltip="Write vertex colors with the geometry", - default=self.write_color_sets), - BoolDef("writeFaceSets", - label="Write face sets", - tooltip="Write face sets with the geometry", - default=self.write_face_sets), - BoolDef("writeNormals", - label="Write normals", - tooltip="Write normals with the deforming geometry", - default=True), - BoolDef("renderableOnly", - label="Renderable Only", - tooltip="Only export renderable visible shapes", - default=False), - BoolDef("visibleOnly", - label="Visible Only", - tooltip="Only export dag objects visible during " - "frame range", - default=False), - BoolDef("includeParentHierarchy", - label="Include Parent Hierarchy", - tooltip="Whether to include parent hierarchy of nodes in " - "the publish instance", - default=self.include_parent_hierarchy), - BoolDef("worldSpace", - label="World-Space Export", - default=True), - BoolDef("includeUserDefinedAttributes", - label="Include User Defined Attributes", - default=self.include_user_defined_attributes), - TextDef("attr", - label="Custom Attributes", - default="", - placeholder="attr1, attr2"), - TextDef("attrPrefix", - label="Custom Attributes Prefix", - placeholder="prefix1, prefix2") - ]) - - # TODO: Implement these on a Deadline plug-in instead? - """ - # Default to not send to farm. - self.data["farm"] = False - self.data["priority"] = 50 - """ - - return defs - - def apply_settings(self, project_settings): - super(CreateAnimation, self).apply_settings(project_settings) - # Hardcoding creator to be enabled due to existing settings would - # disable the creator causing the creator plugin to not be - # discoverable. - self.enabled = True diff --git a/openpype/hosts/maya/plugins/create/create_animation_pointcache.py b/openpype/hosts/maya/plugins/create/create_animation_pointcache.py new file mode 100644 index 00000000000..e44b0c7b27d --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_animation_pointcache.py @@ -0,0 +1,155 @@ +from maya import cmds + +from openpype.hosts.maya.api import lib, plugin + +from openpype.lib import ( + BoolDef, + NumberDef, +) +from openpype.pipeline import CreatedInstance + + +def _get_animation_attr_defs(cls): + """Get Animation generic definitions.""" + defs = lib.collect_animation_defs() + defs.extend( + [ + BoolDef("farm", label="Submit to Farm"), + NumberDef("priority", label="Farm job Priority", default=50), + BoolDef("refresh", label="Refresh viewport during export"), + BoolDef( + "includeParentHierarchy", label="Include Parent Hierarchy" + ), + BoolDef( + "includeUserDefinedAttributes", + label="Include User Defined Attributes" + ), + ] + ) + + return defs + + +def extract_alembic_attributes(node_data, class_name): + """This is a legacy transfer of creator attributes to publish attributes + for ExtractAlembic/ExtractAnimation plugin. + """ + publish_attributes = node_data["publish_attributes"] + + if class_name in publish_attributes: + return node_data + + extract_alembic_flags = [ + "writeColorSets", + "writeFaceSets", + "writeNormals", + "renderableOnly", + "visibleOnly", + "worldSpace", + "renderableOnly" + ] + extract_alembic_attributes = [ + "attr", + "attrPrefix", + "visibleOnly" + ] + attributes = extract_alembic_flags + extract_alembic_attributes + plugin_attributes = {"flags": []} + for attr in attributes: + if attr not in node_data["creator_attributes"].keys(): + continue + value = node_data["creator_attributes"].pop(attr) + + if value and attr in extract_alembic_flags: + plugin_attributes["flags"].append(attr) + + if attr in extract_alembic_attributes: + plugin_attributes[attr] = value + + publish_attributes[class_name] = plugin_attributes + + return node_data + + +class CreateAnimation(plugin.MayaHiddenCreator): + """Animation output for character rigs + + We hide the animation creator from the UI since the creation of it is + automated upon loading a rig. There's an inventory action to recreate it + for loaded rigs if by chance someone deleted the animation instance. + """ + + identifier = "io.openpype.creators.maya.animation" + name = "animationDefault" + label = "Animation" + family = "animation" + icon = "male" + + write_color_sets = False + write_face_sets = False + include_parent_hierarchy = False + include_user_defined_attributes = False + + def collect_instances(self): + try: + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + except KeyError: + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + + for node in cached_subsets.get(self.identifier, []): + node_data = self.read_instance_node(node) + + node_data = extract_alembic_attributes( + node_data, "ExtractAnimation" + ) + + created_instance = CreatedInstance.from_existing(node_data, self) + self._add_instance_to_context(created_instance) + + def get_instance_attr_defs(self): + super(CreateAnimation, self).get_instance_attr_defs() + defs = _get_animation_attr_defs(self) + return defs + + +class CreatePointCache(plugin.MayaCreator): + """Alembic pointcache for animated data""" + + identifier = "io.openpype.creators.maya.pointcache" + label = "Pointcache" + family = "pointcache" + icon = "gears" + write_color_sets = False + write_face_sets = False + include_user_defined_attributes = False + + def collect_instances(self): + try: + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + except KeyError: + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + + for node in cached_subsets.get(self.identifier, []): + node_data = self.read_instance_node(node) + + node_data = extract_alembic_attributes(node_data, "ExtractAlembic") + + created_instance = CreatedInstance.from_existing(node_data, self) + self._add_instance_to_context(created_instance) + + def get_instance_attr_defs(self): + super(CreatePointCache, self).get_instance_attr_defs() + defs = _get_animation_attr_defs(self) + return defs + + def create(self, subset_name, instance_data, pre_create_data): + instance = super(CreatePointCache, self).create( + subset_name, instance_data, pre_create_data + ) + instance_node = instance.get("instance_node") + + # For Arnold standin proxy + proxy_set = cmds.sets(name=instance_node + "_proxy_SET", empty=True) + cmds.sets(proxy_set, forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py deleted file mode 100644 index f4e8cbfc9a2..00000000000 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ /dev/null @@ -1,88 +0,0 @@ -from maya import cmds - -from openpype.hosts.maya.api import ( - lib, - plugin -) -from openpype.lib import ( - BoolDef, - TextDef -) - - -class CreatePointCache(plugin.MayaCreator): - """Alembic pointcache for animated data""" - - identifier = "io.openpype.creators.maya.pointcache" - label = "Pointcache" - family = "pointcache" - icon = "gears" - write_color_sets = False - write_face_sets = False - include_user_defined_attributes = False - - def get_instance_attr_defs(self): - - defs = lib.collect_animation_defs() - - defs.extend([ - BoolDef("writeColorSets", - label="Write vertex colors", - tooltip="Write vertex colors with the geometry", - default=False), - BoolDef("writeFaceSets", - label="Write face sets", - tooltip="Write face sets with the geometry", - default=False), - BoolDef("renderableOnly", - label="Renderable Only", - tooltip="Only export renderable visible shapes", - default=False), - BoolDef("visibleOnly", - label="Visible Only", - tooltip="Only export dag objects visible during " - "frame range", - default=False), - BoolDef("includeParentHierarchy", - label="Include Parent Hierarchy", - tooltip="Whether to include parent hierarchy of nodes in " - "the publish instance", - default=False), - BoolDef("worldSpace", - label="World-Space Export", - default=True), - BoolDef("refresh", - label="Refresh viewport during export", - default=False), - BoolDef("includeUserDefinedAttributes", - label="Include User Defined Attributes", - default=self.include_user_defined_attributes), - TextDef("attr", - label="Custom Attributes", - default="", - placeholder="attr1, attr2"), - TextDef("attrPrefix", - label="Custom Attributes Prefix", - default="", - placeholder="prefix1, prefix2") - ]) - - # TODO: Implement these on a Deadline plug-in instead? - """ - # Default to not send to farm. - self.data["farm"] = False - self.data["priority"] = 50 - """ - - return defs - - def create(self, subset_name, instance_data, pre_create_data): - - instance = super(CreatePointCache, self).create( - subset_name, instance_data, pre_create_data - ) - instance_node = instance.get("instance_node") - - # For Arnold standin proxy - proxy_set = cmds.sets(name=instance_node + "_proxy_SET", empty=True) - cmds.sets(proxy_set, forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index 26a0a01c8bb..8628622bdd2 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -17,7 +17,7 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.4 families = ["animation"] - label = "Collect Animation Output Geometry" + label = "Collect Animation" hosts = ["maya"] ignore_type = ["constraints"] @@ -58,3 +58,7 @@ def process(self, instance): if instance.data.get("farm"): instance.data["families"].append("publish.farm") + # User defined attributes. + instance.data["includeUserDefinedAttributes"] = ( + instance.data["creator_attributes"]["includeUserDefinedAttributes"] + ) diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index 5578a57f314..8b4289ed80c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -45,3 +45,8 @@ def process(self, instance): if proxy_set: instance.remove(proxy_set) instance.data["setMembers"].remove(proxy_set) + + # User defined attributes. + instance.data["includeUserDefinedAttributes"] = ( + instance.data["creator_attributes"]["includeUserDefinedAttributes"] + ) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 0cc802fa7aa..79e07cf7322 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -3,15 +3,24 @@ from maya import cmds from openpype.pipeline import publish +from openpype.hosts.maya.api.alembic import extract_alembic from openpype.hosts.maya.api.lib import ( - extract_alembic, suspended_refresh, maintained_selection, - iter_visible_nodes_in_range + iter_visible_nodes_in_range, ) +from openpype.lib import ( + BoolDef, + TextDef, + NumberDef, + EnumDef, + UISeparatorDef, + UILabelDef, +) +from openpype.pipeline.publish import OpenPypePyblishPluginMixin -class ExtractAlembic(publish.Extractor): +class ExtractAlembic(publish.Extractor, OpenPypePyblishPluginMixin): """Produce an alembic of just point positions and normals. Positions and normals, uvs, creases are preserved, but nothing more, @@ -25,6 +34,19 @@ class ExtractAlembic(publish.Extractor): hosts = ["maya"] families = ["pointcache", "model", "vrayproxy.alembic"] targets = ["local", "remote"] + flags = [] + attr = [] + attrPrefix = [] + dataFormat = "ogawa" + melPerFrameCallback = "" + melPostJobCallback = "" + preRollStartFrame = 0 + pythonPerFrameCallback = "" + pythonPostJobCallback = "" + userAttr = "" + userAttrPrefix = "" + visibleOnly = False + overrides = [] def process(self, instance): if instance.data.get("farm"): @@ -37,43 +59,75 @@ def process(self, instance): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - attrs = instance.data.get("attr", "").split(";") - attrs = [value for value in attrs if value.strip()] + attribute_values = self.get_attr_values_from_data( + instance.data + ) + + attrs = [ + attr.strip() + for attr in attribute_values.get("attr", "").split(";") + if attr.strip() + ] attrs += instance.data.get("userDefinedAttributes", []) attrs += ["cbId"] - attr_prefixes = instance.data.get("attrPrefix", "").split(";") - attr_prefixes = [value for value in attr_prefixes if value.strip()] + attr_prefixes = [ + attr.strip() + for attr in attribute_values.get("attrPrefix", "").split(";") + if attr.strip() + ] - self.log.debug("Extracting pointcache..") + self.log.debug("Extracting pointcache...") dirname = self.staging_dir(instance) parent_dir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, filename) - options = { - "step": instance.data.get("step", 1.0), - "attr": attrs, - "attrPrefix": attr_prefixes, - "writeVisibility": True, - "writeCreases": True, - "writeColorSets": instance.data.get("writeColorSets", False), - "writeFaceSets": instance.data.get("writeFaceSets", False), - "uvWrite": True, - "selection": True, - "worldSpace": instance.data.get("worldSpace", True) - } - + root = None if not instance.data.get("includeParentHierarchy", True): # Set the root nodes if we don't want to include parents # The roots are to be considered the ones that are the actual # direct members of the set - options["root"] = roots + root = roots - if int(cmds.about(version=True)) >= 2017: - # Since Maya 2017 alembic supports multiple uv sets - write them. - options["writeUVSets"] = True + args = { + "file": path, + "attr": attrs, + "attrPrefix": attr_prefixes, + "dataFormat": attribute_values.get("dataFormat", "ogawa"), + "endFrame": end, + "eulerFilter": False, + "noNormals": False, + "preRoll": False, + "preRollStartFrame": attribute_values.get( + "preRollStartFrame", 0 + ), + "renderableOnly": False, + "root": root, + "selection": True, + "startFrame": start, + "step": instance.data.get( + "creator_attributes", {} + ).get("step", 1.0), + "stripNamespaces": False, + "uvWrite": False, + "verbose": False, + "wholeFrameGeo": False, + "worldSpace": False, + "writeColorSets": False, + "writeCreases": False, + "writeFaceSets": False, + "writeUVSets": False, + "writeVisibility": False, + } + + # Export flags are defined as default enabled flags plus publisher + # enabled flags. + non_exposed_flags = list(set(self.flags) - set(self.overrides)) + flags = attribute_values["flags"] + non_exposed_flags + for flag in flags: + args[flag] = True if instance.data.get("visibleOnly", False): # If we only want to include nodes that are visible in the frame @@ -81,20 +135,20 @@ def process(self, instance): # flag does not filter out those that are only hidden on some # frames as it counts "animated" or "connected" visibilities as # if it's always visible. - nodes = list(iter_visible_nodes_in_range(nodes, - start=start, - end=end)) + nodes = list( + iter_visible_nodes_in_range(nodes, start=start, end=end) + ) suspend = not instance.data.get("refresh", False) with suspended_refresh(suspend=suspend): with maintained_selection(): cmds.select(nodes, noExpand=True) - extract_alembic( - file=path, - startFrame=start, - endFrame=end, - **options + self.log.debug( + "Running `extract_alembic` with the arguments: {}".format( + args + ) ) + extract_alembic(**args) if "representations" not in instance.data: instance.data["representations"] = [] @@ -103,7 +157,7 @@ def process(self, instance): "name": "abc", "ext": "abc", "files": filename, - "stagingDir": dirname + "stagingDir": dirname, } instance.data["representations"].append(representation) @@ -118,52 +172,171 @@ def process(self, instance): return path = path.replace(".abc", "_proxy.abc") + args["file"] = path if not instance.data.get("includeParentHierarchy", True): # Set the root nodes if we don't want to include parents # The roots are to be considered the ones that are the actual # direct members of the set - options["root"] = instance.data["proxyRoots"] + args["root"] = instance.data["proxyRoots"] with suspended_refresh(suspend=suspend): with maintained_selection(): cmds.select(instance.data["proxy"]) - extract_alembic( - file=path, - startFrame=start, - endFrame=end, - **options - ) + extract_alembic(**args) representation = { "name": "proxy", "ext": "abc", "files": os.path.basename(path), "stagingDir": dirname, - "outputName": "proxy" + "outputName": "proxy", } instance.data["representations"].append(representation) def get_members_and_roots(self, instance): return instance[:], instance.data.get("setMembers") + @classmethod + def get_attribute_defs(cls): + override_defs = { + "attr": { + "def": TextDef, + "kwargs": { + "label": "Custom Attributes", + "placeholder": "attr1; attr2; ...", + } + }, + "attrPrefix": { + "def": TextDef, + "kwargs": { + "label": "Custom Attributes Prefix", + "placeholder": "prefix1; prefix2; ...", + } + }, + "dataFormat": { + "def": EnumDef, + "kwargs": { + "label": "Data Format", + "items": ["ogawa", "HDF"], + } + }, + "melPerFrameCallback": { + "def": TextDef, + "kwargs": { + "label": "melPerFrameCallback", + } + }, + "melPostJobCallback": { + "def": TextDef, + "kwargs": { + "label": "melPostJobCallback", + } + }, + "preRollStartFrame": { + "def": NumberDef, + "kwargs": { + "label": "Start frame for preroll", + "tooltip": ( + "The frame to start scene evaluation at. This is used" + " to set the starting frame for time dependent " + "translations and can be used to evaluate run-up that" + " isn't actually translated." + ), + } + }, + "pythonPerFrameCallback": { + "def": TextDef, + "kwargs": { + "label": "pythonPerFrameCallback", + } + }, + "pythonPostJobCallback": { + "def": TextDef, + "kwargs": { + "label": "pythonPostJobCallback", + } + }, + "userAttr": { + "def": TextDef, + "kwargs": { + "label": "userAttr", + } + }, + "userAttrPrefix": { + "def": TextDef, + "kwargs": { + "label": "userAttrPrefix", + } + }, + "visibleOnly": { + "def": BoolDef, + "kwargs": { + "label": "Visible Only", + } + } + } + + defs = super(ExtractAlembic, cls).get_attribute_defs() + + defs.extend([ + UISeparatorDef("sep_alembic_options"), + UILabelDef("Alembic Options"), + ]) + + # The Arguments that can be modified by the Publisher + overrides = set(getattr(cls, "overrides", set())) + + # What we have set in the Settings as defaults. + flags = set(getattr(cls, "flags", set())) + + enabled_flags = [x for x in flags if x in overrides] + flags = overrides - set(override_defs.keys()) + defs.append( + EnumDef( + "flags", + flags, + default=enabled_flags, + multiselection=True, + label="Export Flags", + ) + ) + + for key, value in override_defs.items(): + if key not in overrides: + continue + + kwargs = value["kwargs"] + kwargs["default"] = getattr(cls, key, None) + defs.append( + value["def"](key, **value["kwargs"]) + ) + + defs.append( + UISeparatorDef("sep_alembic_options") + ) + + return defs + class ExtractAnimation(ExtractAlembic): - label = "Extract Animation" + label = "Extract Animation (Alembic)" families = ["animation"] def get_members_and_roots(self, instance): - # Collect the out set nodes out_sets = [node for node in instance if node.endswith("out_SET")] if len(out_sets) != 1: - raise RuntimeError("Couldn't find exactly one out_SET: " - "{0}".format(out_sets)) + raise RuntimeError( + "Couldn't find exactly one out_SET: " "{0}".format(out_sets) + ) out_set = out_sets[0] roots = cmds.sets(out_set, query=True) # Include all descendants - nodes = roots + cmds.listRelatives(roots, - allDescendents=True, - fullPath=True) or [] + nodes = ( + roots + + cmds.listRelatives(roots, allDescendents=True, fullPath=True) + or [] + ) return nodes, roots diff --git a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py index d9bec87cfd3..b54c91f05af 100644 --- a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py @@ -3,8 +3,8 @@ from maya import cmds from openpype.pipeline import publish +from openpype.hosts.maya.api.alembic import extract_alembic from openpype.hosts.maya.api.lib import ( - extract_alembic, suspended_refresh, maintained_selection, iter_visible_nodes_in_range diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py index 780ed2377cc..70d39d97098 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py @@ -6,8 +6,8 @@ from maya import cmds # noqa from openpype.pipeline import publish +from openpype.hosts.maya.api.alembic import extract_alembic from openpype.hosts.maya.api.lib import ( - extract_alembic, suspended_refresh, maintained_selection ) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index 4bd01c2df2b..bfcf65c6521 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -5,7 +5,7 @@ from maya import cmds import pyblish.api -from openpype.hosts.maya.api.lib import extract_alembic +from openpype.hosts.maya.api.alembic import extract_alembic from openpype.pipeline import publish from openpype.lib import StringTemplate diff --git a/openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py b/openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py new file mode 100644 index 00000000000..e16196a6d38 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py @@ -0,0 +1,106 @@ +import pyblish.api + +from openpype.pipeline import OptionalPyblishPluginMixin +from openpype.pipeline.publish import RepairAction, PublishValidationError + + +class ValidateAlembicOptionsDefaults( + pyblish.api.InstancePlugin, OptionalPyblishPluginMixin +): + """Validate the attributes on the instance are defaults.""" + + order = pyblish.api.ValidatorOrder + families = ["pointcache", "animation"] + hosts = ["maya"] + label = "Validate Alembic Options Defaults" + actions = [RepairAction] + optional = True + + @classmethod + def _get_plugin_name(self, publish_attributes): + for key in ["ExtractAnimation", "ExtractAlembic"]: + if key in publish_attributes.keys(): + return key + + @classmethod + def _get_settings(self, context): + maya_settings = context.data["project_settings"]["maya"] + settings = maya_settings["publish"]["ExtractAlembic"] + # Flags are a special case since they are a combination of overrides + # and default flags from the settings. + settings["flags"] = [ + x for x in settings["flags"] if x in settings["overrides"] + ] + return settings + + @classmethod + def _get_publish_attributes(self, instance): + attributes = instance.data["publish_attributes"][ + self._get_plugin_name( + instance.data["publish_attributes"] + ) + ] + + settings = self._get_settings(instance.context) + + # Flags are a special case since they are a combination of exposed + # flags and default flags from the settings. So we need to add the + # default flags from the settings and ensure unique items. + non_exposed_flags = [ + x for x in settings["flags"] if x not in settings["overrides"] + ] + attributes["flags"] = attributes["flags"] + non_exposed_flags + + return attributes + + def process(self, instance): + if not self.is_active(instance.data): + return + + settings = self._get_settings(instance.context) + + attributes = self._get_publish_attributes(instance) + + msg = ( + "Alembic Extract setting \"{}\" is not the default value:" + "\nCurrent: {}" + "\nDefault Value: {}\n" + ) + errors = [] + for key, value in attributes.items(): + default_value = settings[key] + + # Lists are best to compared sorted since we cant rely on the order + # of the items. + if isinstance(value, list): + value = sorted(value) + default_value = sorted(default_value) + + if value != default_value: + errors.append(msg.format(key, value, default_value)) + + if errors: + raise PublishValidationError("\n".join(errors)) + + @classmethod + def repair(cls, instance): + # Find create instance twin. + create_context = instance.context.data["create_context"] + create_instance = None + for Instance in create_context.instances: + if Instance.data["instance_id"] == instance.data["instance_id"]: + create_instance = Instance + break + + assert create_instance is not None + + # Set the settings values on the create context then save to workfile. + publish_attributes = instance.data["publish_attributes"] + plugin_name = cls._get_plugin_name(publish_attributes) + attributes = cls._get_publish_attributes(instance) + settings = cls._get_settings(instance.context) + create_publish_attributes = create_instance.data["publish_attributes"] + for key in attributes.keys(): + create_publish_attributes[plugin_name][key] = settings[key] + + create_context.save_changes() diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 615000183dc..7d0af6feb4f 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -557,13 +557,13 @@ "publish_mip_map": true }, "CreateAnimation": { - "write_color_sets": false, - "write_face_sets": false, - "include_parent_hierarchy": false, - "include_user_defined_attributes": false, - "default_variants": [ - "Main" - ] + "default_variants": [], + "step": 1.0, + "includeParentHierarchy": false, + "farm": false, + "priority": 50, + "refresh": false, + "include_user_defined_attributes": false }, "CreateModel": { "enabled": true, @@ -577,12 +577,15 @@ }, "CreatePointCache": { "enabled": true, - "write_color_sets": false, - "write_face_sets": false, - "include_user_defined_attributes": false, "default_variants": [ "Main" - ] + ], + "step": 1.0, + "includeParentHierarchy": false, + "farm": false, + "priority": 50, + "refresh": false, + "include_user_defined_attributes": false }, "CreateProxyAlembic": { "enabled": true, @@ -1107,6 +1110,32 @@ "pointcache", "model", "vrayproxy.alembic" + ], + "flags": [ + "stripNamespaces", + "writeNormals", + "worldSpace" + ], + "attr": "", + "attrPrefix": "", + "dataFormat": "ogawa", + "melPerFrameCallback": "", + "melPostJobCallback": "", + "preRollStartFrame": 0, + "pythonPerFrameCallback": "", + "pythonPostJobCallback": "", + "userAttr": "", + "userAttrPrefix": "", + "visibleOnly": false, + "overrides": [ + "attr", + "attrPrefix", + "worldSpace", + "writeColorSets", + "writeNormals", + "writeFaceSets", + "renderableOnly", + "visibleOnly" ] }, "ExtractObj": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index b56e381c1da..16355eb1a2a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -131,31 +131,37 @@ "type": "label", "label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup." }, + { + "type": "list", + "key": "default_variants", + "label": "Default Variants", + "object_type": "text" + }, { "type": "boolean", - "key": "write_color_sets", - "label": "Write Color Sets" + "key": "includeParentHierarchy", + "label": "Include Parent Hierarchy" }, { "type": "boolean", - "key": "write_face_sets", - "label": "Write Face Sets" + "key": "farm", + "label": "Submit to the Farm" + }, + { + "type": "number", + "key": "priority", + "label": "Farm Job Priority", + "minimum": 0 }, { "type": "boolean", - "key": "include_parent_hierarchy", - "label": "Include Parent Hierarchy" + "key": "refresh", + "label": "Refresh" }, { "type": "boolean", "key": "include_user_defined_attributes", "label": "Include User Defined Attributes" - }, - { - "type": "list", - "key": "default_variants", - "label": "Default Variants", - "object_type": "text" } ] }, @@ -201,26 +207,44 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "list", + "key": "default_variants", + "label": "Default Variants", + "object_type": "text" + }, + { + "type": "number", + "key": "step", + "label": "Step default", + "minimum": 0.0, + "decimal": 4 + }, { "type": "boolean", - "key": "write_color_sets", - "label": "Write Color Sets" + "key": "includeParentHierarchy", + "label": "Include Parent Hierarchy default" }, { "type": "boolean", - "key": "write_face_sets", - "label": "Write Face Sets" + "key": "farm", + "label": "Farm default" + }, + { + "type": "number", + "key": "priority", + "label": "Priority default", + "minimum": 0 }, { "type": "boolean", - "key": "include_user_defined_attributes", - "label": "Include User Defined Attributes" + "key": "refresh", + "label": "Refresh default" }, { - "type": "list", - "key": "default_variants", - "label": "Default Variants", - "object_type": "text" + "type": "boolean", + "key": "include_user_defined_attributes", + "label": "Include User Defined Attributes" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index d2e7c51e249..88f3d476ca3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -750,26 +750,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "ExtractAlembic", - "label": "Extract Alembic", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - } - ] - }, { "type": "dict", "collapsible": true, @@ -1008,6 +988,166 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractAlembic", + "label": "Extract Pointcache/Animation", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Export Defaults" + }, + { + "type": "enum", + "key": "flags", + "multiselection": true, + "label": "Export Flags", + "enum_items": [ + {"autoSubd": "autoSubd"}, + {"dontSkipUnwrittenFrames": "dontSkipUnwrittenFrames"}, + {"eulerFilter": "eulerFilter"}, + {"noNormals": "noNormals"}, + {"preRoll": "preRoll"}, + {"renderableOnly": "renderableOnly"}, + {"stripNamespaces": "stripNamespaces"}, + {"uvWrite": "uvWrite"}, + {"uvsOnly": "uvsOnly"}, + {"verbose": "verbose"}, + {"wholeFrameGeo": "wholeFrameGeo"}, + {"worldSpace": "worldSpace"}, + {"writeColorSets": "writeColorSets"}, + {"writeFaceSets": "writeFaceSets"}, + {"writeNormals": "writeNormals"}, + {"writeUVSets": "writeUVSets"}, + {"writeVisibility": "writeVisibility"} + ] + }, + { + "type": "text", + "key": "attr", + "label": "Custom Attributes" + }, + { + "type": "text", + "key": "attrPrefix", + "label": "Custom Attributes Prefix" + }, + { + "type": "enum", + "key": "dataFormat", + "label": "Data Format", + "enum_items": [ + { + "ogawa": "ogawa" + }, + { + "HDF": "HDF" + } + ] + }, + { + "type": "text", + "key": "melPerFrameCallback", + "label": "melPerFrameCallback" + }, + { + "type": "text", + "key": "melPostJobCallback", + "label": "melPostJobCallback" + }, + { + "type": "number", + "key": "preRollStartFrame", + "label": "Pre Roll Start Frame", + "minimum": 0 + }, + { + "type": "text", + "key": "pythonPerFrameCallback", + "label": "pythonPerFrameCallback" + }, + { + "type": "text", + "key": "pythonPostJobCallback", + "label": "pythonPostJobCallback" + }, + { + "type": "text", + "key": "userAttr", + "label": "userAttr" + }, + { + "type": "text", + "key": "userAttrPrefix", + "label": "userAttrPrefix" + }, + { + "type": "boolean", + "key": "visibleOnly", + "label": "Visible Only" + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "These attributes are exposed to the user when publishing with default values from above." + }, + { + "type": "enum", + "key": "overrides", + "multiselection": true, + "label": "Exposed Overrides", + "enum_items": [ + {"attr": "Custom Attributes"}, + {"attrPrefix": "Custom Attributes Prefix"}, + {"autoSubd": "autoSubd"}, + {"dataFormat": "dataFormat"}, + {"dontSkipUnwrittenFrames": "dontSkipUnwrittenFrames"}, + {"eulerFilter": "eulerFilter"}, + {"melPerFrameCallback": "melPerFrameCallback"}, + {"melPostJobCallback": "melPostJobCallback"}, + {"noNormals": "noNormals"}, + {"preRoll": "preRoll"}, + {"preRollStartFrame": "preRollStartFrame"}, + {"pythonPerFrameCallback": "pythonPerFrameCallback"}, + {"pythonPostJobCallback": "pythonPostJobCallback"}, + {"renderableOnly": "renderableOnly"}, + {"stripNamespaces": "stripNamespaces"}, + {"userAttr": "userAttr"}, + {"userAttrPrefix": "userAttrPrefix"}, + {"uvWrite": "uvWrite"}, + {"uvsOnly": "uvsOnly"}, + {"verbose": "verbose"}, + {"visibleOnly": "visibleOnly"}, + {"wholeFrameGeo": "wholeFrameGeo"}, + {"worldSpace": "worldSpace"}, + {"writeColorSets": "writeColorSets"}, + {"writeCreases": "writeCreases"}, + {"writeFaceSets": "writeFaceSets"}, + {"writeNormals": "writeNormals"}, + {"writeUVSets": "writeUVSets"}, + {"writeVisibility": "writeVisibility"} + ] + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ecccc4e0c8e..2b497707faf 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1485,7 +1485,7 @@ def _input_value_changed(self, value, attr_id): class PublishPluginAttrsWidget(QtWidgets.QWidget): """Widget showing publsish plugin attributes for selected instances. - Attributes are defined on publish plugins. Publihs plugin may define + Attributes are defined on publish plugins. Publish plugin may define attribute definitions but must inherit `OpenPypePyblishPluginMixin` (~/openpype/pipeline/publish). At the moment requires to implement `get_attribute_defs` and `convert_attribute_values` class methods.