From 0bcc602eacfa22e7ad25a34ef4bfa1c3c047b454 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Jun 2024 23:02:36 +0800 Subject: [PATCH] add ornatrix family support for maya --- .../plugins/publish/collect_resources_path.py | 3 +- server_addon/maya/client/ayon_maya/api/lib.py | 31 +++++ .../plugins/create/create_ornatrix_cache.py | 22 ++++ .../plugins/create/create_ornatrix_rig.py | 27 ++++ .../plugins/load/load_ornatrix_rig.py | 121 ++++++++++++++++++ .../plugins/publish/collect_yeti_rig.py | 85 +++++++++++- .../plugins/publish/extract_ornatrix_rig.py | 113 ++++++++++++++++ server_addon/maya/server/settings/loaders.py | 21 +++ 8 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_cache.py create mode 100644 server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_rig.py create mode 100644 server_addon/maya/client/ayon_maya/plugins/load/load_ornatrix_rig.py create mode 100644 server_addon/maya/client/ayon_maya/plugins/publish/extract_ornatrix_rig.py diff --git a/client/ayon_core/plugins/publish/collect_resources_path.py b/client/ayon_core/plugins/publish/collect_resources_path.py index 959523918e..d11e2e34f7 100644 --- a/client/ayon_core/plugins/publish/collect_resources_path.py +++ b/client/ayon_core/plugins/publish/collect_resources_path.py @@ -64,7 +64,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "skeletalMesh", "xgen", "yeticacheUE", - "tycache" + "tycache", + "ornatrixRig" ] def process(self, instance): diff --git a/server_addon/maya/client/ayon_maya/api/lib.py b/server_addon/maya/client/ayon_maya/api/lib.py index 2b41ffc06c..871814d1b6 100644 --- a/server_addon/maya/client/ayon_maya/api/lib.py +++ b/server_addon/maya/client/ayon_maya/api/lib.py @@ -769,6 +769,37 @@ def attribute_values(attr_values): else: cmds.setAttr(attr, value) +@contextlib.contextmanager +def attribute_values_from_list(attr_values): + """Remaps node attributes to values for ornatrix during context. + + Arguments: + attr_values (dict): Dictionary with (attr, value) + + """ + for texture_attr in attr_values: + original = [(texture_attr[attr], cmds.getAttr(texture_attr[attr])) + for attr in texture_attr.keys()] + try: + for attr, value in attr_values.items(): + if isinstance(value, string_types): + cmds.setAttr(attr, value, type="string") + else: + cmds.setAttr(attr, value) + yield + finally: + for attr, value in original: + if isinstance(value, string_types): + cmds.setAttr(attr, value, type="string") + elif value is None and cmds.getAttr(attr, type=True) == "string": + # In some cases the maya.cmds.getAttr command returns None + # for string attributes but this value cannot assigned. + # Note: After setting it once to "" it will then return "" + # instead of None. So this would only happen once. + cmds.setAttr(attr, "", type="string") + else: + cmds.setAttr(attr, value) + @contextlib.contextmanager def keytangent_default(in_tangent_type='auto', diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_cache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_cache.py new file mode 100644 index 0000000000..84e2b92d6c --- /dev/null +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_cache.py @@ -0,0 +1,22 @@ +from ayon_maya.api import ( + lib, + plugin +) + + +class CreateOrnatrixCache(plugin.MayaCreator): + """Output for procedural plugin nodes of Yeti """ + + identifier = "io.openpype.creators.maya.ornatrixcache" + label = "Ornatrix Cache" + product_type = "ornatrixCache" + icon = "pagelines" + + def get_instance_attr_defs(self): + + # Add animation data without step and handles + remove = {"step", "handleStart", "handleEnd"} + defs = [attr_def for attr_def in lib.collect_animation_defs() + if attr_def.key not in remove] + + return defs diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_rig.py b/server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_rig.py new file mode 100644 index 0000000000..065872d041 --- /dev/null +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_ornatrix_rig.py @@ -0,0 +1,27 @@ +from maya import cmds + +from ayon_maya.api import ( + lib, + plugin +) + + +class CreateOrnatrixRig(plugin.MayaCreator): + """Output for Ornatrix nodes""" + + identifier = "io.openpype.creators.maya.ornatrixrig" + label = "Ornatrix Rig" + product_type = "ornatrixRig" + icon = "usb" + + def create(self, product_name, instance_data, pre_create_data): + + with lib.undo_chunk(): + instance = super(CreateOrnatrixRig, self).create(product_name, + instance_data, + pre_create_data) + instance_node = instance.get("instance_node") + + self.log.info("Creating Rig instance set up ...") + input_meshes = cmds.sets(name="input_SET", empty=True) + cmds.sets(input_meshes, forceElement=instance_node) diff --git a/server_addon/maya/client/ayon_maya/plugins/load/load_ornatrix_rig.py b/server_addon/maya/client/ayon_maya/plugins/load/load_ornatrix_rig.py new file mode 100644 index 0000000000..350feaf194 --- /dev/null +++ b/server_addon/maya/client/ayon_maya/plugins/load/load_ornatrix_rig.py @@ -0,0 +1,121 @@ +from typing import List + +import os +import json +import maya.cmds as cmds +from ayon_core.pipeline import registered_host +from ayon_core.pipeline.create import CreateContext +from ayon_maya.api import lib, plugin + + +class OrnatrixRigLoader(plugin.ReferenceLoader): + """This loader will load Ornatix rig.""" + + product_types = {"ornatrixRig"} + representations = {"ma"} + + label = "Load Ornatrix Rig" + order = -9 + icon = "code-fork" + color = "orange" + + # From settings + create_cache_instance_on_load = True + + def process_reference( + self, context, name=None, namespace=None, options=None + ): + path = self.filepath_from_context(context) + + attach_to_root = options.get("attach_to_root", True) + group_name = options["group_name"] + + # no group shall be created + if not attach_to_root: + group_name = namespace + + with lib.maintained_selection(): + file_url = self.prepare_root_value( + path, context["project"]["name"] + ) + nodes = cmds.file( + file_url, + namespace=namespace, + reference=True, + returnNewNodes=True, + groupReference=attach_to_root, + groupName=group_name + ) + + color = plugin.get_load_color_for_product_type("ornatrixRig") + if color is not None: + red, green, blue = color + cmds.setAttr(group_name + ".useOutlinerColor", 1) + cmds.setAttr( + group_name + ".outlinerColor", red, green, blue + ) + self.use_resources_textures(namespace, path) + self[:] = nodes + + if self.create_cache_instance_on_load: + self._create_ox_cache_instance(nodes, variant=namespace) + + return nodes + + def _create_ox_cache_instance(self, nodes: List[str], variant: str): + """Create a onratrixcache product type instance to publish the output. + + This is similar to how loading animation rig will automatically create + an animation instance for publishing any loaded character rigs, but + then for Onratrix rigs. + + Args: + nodes (List[str]): Nodes generated on load. + variant (str): Variant for the onratrix cache instance to create. + + """ + + # Check of the nodes connect to the ornatrix-related nodes + ox_nodes = [node for node in nodes if cmds.nodeType(nodes) in + {"HairFromGuidesNode", "GuidesFromMeshNode", + "MeshFromStrandsNode", "SurfaceCombNode"}] + assert not ox_nodes, "No Ornatrix nodes in rig, this is a bug." + + ox_geo_nodes = cmds.ls(nodes, assemblies=True, long=True) + ox_input = next((node for node in nodes if + node.endswith("input_SET")), None) + self.log.info("Creating variant: {}".format(variant)) + + creator_identifier = "io.openpype.creators.maya.ornatrixcache" + + host = registered_host() + create_context = CreateContext(host) + + with lib.maintained_selection(): + cmds.select(ox_geo_nodes + [ox_input], noExpand=True) + create_context.create( + creator_identifier=creator_identifier, + variant=variant, + pre_create_data={"use_selection": True} + ) + + + def use_resources_textures(self, namespace, path): + """Use texture maps from resources directories + + Args: + namespace (str): namespace + path (str): published filepath + """ + _, maya_extension = os.path.splitext(path) + settings_path = path.replace(maya_extension, ".rigsettings") + with open(settings_path, "r") as fp: + image_attributes = json.load(fp) + fp.close() + if image_attributes: + for image_attribute in image_attributes: + texture_attribute = "{}:{}".format( + namespace, image_attribute["texture_attribute"]) + cmds.setAttr(texture_attribute, + image_attribute["destination_file"], + type="string") diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/collect_yeti_rig.py b/server_addon/maya/client/ayon_maya/plugins/publish/collect_yeti_rig.py index dbdc10789f..04a7c29087 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/collect_yeti_rig.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/collect_yeti_rig.py @@ -34,7 +34,7 @@ class CollectYetiRig(plugin.MayaInstancePlugin): yeti_nodes = cmds.ls(instance[:], type="pgYetiMaya", long=True) for node in yeti_nodes: # Get Yeti resources (textures) - resources = self.get_yeti_resources(node) + resources = self.get_texture_resources(node) yeti_resources.extend(resources) instance.data["rigsettings"] = {"inputs": input_connections} @@ -304,3 +304,86 @@ class CollectYetiRig(plugin.MayaInstancePlugin): raise RuntimeError(msg) replaced.append(s) return replaced + + +class CollectOrnatrixRig(CollectYetiRig): + """Collect all information of the Ornatrix Rig""" + + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Ornatrix Rig" + families = ["ornatrixRig"] + + def process(self, instance): + assert "input_SET" in instance.data["setMembers"], ( + "Ornatrix Rig must have an input_SET") + + ornatrix_nodes = cmds.ls(instance[:], long=True) + self.log.debug(f"Getting ornatrix nodes: {ornatrix_nodes}") + # Force frame range for yeti cache export for the rig + # Collect any textures if used + ornatrix_resources = [] + for node in ornatrix_nodes: + # Get Yeti resources (textures) + resources = self.get_texture_resources(node) + ornatrix_resources.extend(resources) + # avoid duplicate dictionary data + instance.data["resources"] = [ + i for n, i in enumerate(ornatrix_resources) + if i not in ornatrix_resources[n + 1:] + ] + self.log.debug("{}".format(instance.data["resources"])) + start = cmds.playbackOptions(query=True, animationStartTime=True) + for key in ["frameStart", "frameEnd", + "frameStartHandle", "frameEndHandle"]: + instance.data[key] = start + + def get_texture_resources(self, node): + # add docstrings + resources = [] + node_shape = cmds.listRelatives(node, shapes=True) + if not node_shape: + return [] + + ox_nodes = [ + ox_node for ox_node in cmds.listConnections(node_shape, destination=True) + if cmds.nodeType(ox_node) in { + "HairFromGuidesNode", "GuidesFromMeshNode", + "MeshFromStrandsNode", "SurfaceCombNode" + } + ] + ox_imageFile = [ + ox_img for ox_img in cmds.listConnections(ox_nodes, destination=False) + if cmds.nodeType(ox_img) == "file" + ] + if not ox_imageFile: + return [] + for img in ox_imageFile: + texture_attr = "{}.fileTextureName".format(img) + texture = cmds.getAttr("{}.fileTextureName".format(img)) + files = [] + if os.path.isabs(texture): + self.log.debug("Texture is absolute path, ignoring " + "image search paths for: %s" % texture) + files = self.search_textures(texture) + else: + root = os.environ["AYON_WORKDIR"] + filepath = os.path.join(root, texture) + files = self.search_textures(filepath) + if files: + # Break out on first match in search paths.. + break + + if not files: + raise KnownPublishError( + "No texture found for: %s " + "(searched: %s)" % (texture)) + + item = { + "files": files, + "source": texture, + "texture_attribute": texture_attr + } + + resources.append(item) + + return resources diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_ornatrix_rig.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_ornatrix_rig.py new file mode 100644 index 0000000000..162f2997d6 --- /dev/null +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_ornatrix_rig.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +"""Extract Ornatrix rig.""" + +import os +import json + +from ayon_maya.api import lib +from ayon_maya.api import plugin +from maya import cmds + + + +class ExtractOrnatrixRig(plugin.MayaExtractorPlugin): + """Extract the Ornatrix rig to a Maya Scene and write the Ornatrix rig data.""" + + label = "Extract Ornatrix Rig" + families = ["ornatrixRig"] + scene_type = "ma" + + def process(self, instance): + """Plugin entry point.""" + maya_settings = instance.context.data["project_settings"]["maya"] + ext_mapping = { + item["name"]: item["value"] + for item in maya_settings["ext_mapping"] + } + if ext_mapping: + self.log.debug("Looking in settings for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.debug( + "Using {} as scene type".format(self.scene_type)) + break + except KeyError: + # no preset found + pass + + # Define extract output file path + dirname = self.staging_dir(instance) + settings_path = os.path.join(dirname, "ornatrix.rigsettings") + image_search_path = instance.data["resourcesDir"] + + # add textures to transfers + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + + resources = instance.data.get("resources", []) + for resource in instance.data.get('resources', []): + for file in resource['files']: + src = file + dst = os.path.join(image_search_path, os.path.basename(file)) + resource["destination_file"] = dst + instance.data['transfers'].append([src, dst]) + + self.log.debug("adding transfer {} -> {}". format(src, dst)) + + self.log.debug("Writing metadata file: {}".format(settings_path)) + with open(settings_path, "w") as fp: + json.dump(resources, fp, ensure_ascii=False) + + # Get input_SET members + input_set = next(i for i in instance if i == "input_SET") + + # Get all items + set_members = cmds.sets(input_set, query=True) or [] + set_members += cmds.listRelatives(set_members, + allDescendents=True, + fullPath=True) or [] + + # Yeti related staging dirs + maya_path = os.path.join(dirname, + "ornatrix_rig.{}".format(self.scene_type)) + nodes = instance.data["setMembers"] + with lib.maintained_selection(): + cmds.select(nodes, noExpand=True) + cmds.file(maya_path, + force=True, + exportSelected=True, + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 + preserveReferences=False, + constructionHistory=True, + shader=False) + + # Ensure files can be stored + # build representations + if "representations" not in instance.data: + instance.data["representations"] = [] + + self.log.debug("rig file: {}".format(maya_path)) + instance.data["representations"].append( + { + 'name': self.scene_type, + 'ext': self.scene_type, + 'files': os.path.basename(maya_path), + 'stagingDir': dirname + } + ) + + self.log.debug("settings file: {}".format(settings_path)) + instance.data["representations"].append( + { + 'name': "rigsettings", + 'ext': "rigsettings", + 'files': os.path.basename(settings_path), + 'stagingDir': dirname + } + ) + + self.log.debug("Extracted {} to {}".format(instance, dirname)) + + cmds.select(clear=True) diff --git a/server_addon/maya/server/settings/loaders.py b/server_addon/maya/server/settings/loaders.py index 2f104d2858..cb6c327610 100644 --- a/server_addon/maya/server/settings/loaders.py +++ b/server_addon/maya/server/settings/loaders.py @@ -39,6 +39,8 @@ class ColorsSetting(BaseSettingsModel): (99, 206, 220, 1.0), title="Yeti Cache:") yetiRig: ColorRGBA_uint8 = SettingsField( (0, 205, 125, 1.0), title="Yeti Rig:") + ornatrixRig: ColorRGBA_uint8 = SettingsField( + (206, 234, 195, 1.0), title="Ornatrix Rig:") # model: ColorRGB_float = SettingsField( # (0.82, 0.52, 0.12), title="Model:" # ) @@ -114,6 +116,17 @@ class YetiRigLoaderModel(LoaderEnabledModel): ) +class OrnatrixRigLoaderModel(LoaderEnabledModel): + create_cache_instance_on_load: bool = SettingsField( + title="Create Ornatrix Cache instance on load", + description=( + "When enabled, upon loading a Ornatrix Rig product a new Ornatrix cache " + "instance is automatically created as preparation to publishing " + "the output directly." + ) + ) + + class LoadersModel(BaseSettingsModel): colors: ColorsSetting = SettingsField( default_factory=ColorsSetting, @@ -210,6 +223,10 @@ class LoadersModel(BaseSettingsModel): default_factory=YetiRigLoaderModel, title="Yeti Rig Loader" ) + OrnatrixRigLoader: OrnatrixRigLoaderModel = SettingsField( + default_factory=OrnatrixRigLoaderModel, + title="Ornatrix Rig Loader" + ) DEFAULT_LOADERS_SETTING = { @@ -281,4 +298,8 @@ DEFAULT_LOADERS_SETTING = { "enabled": True, "create_cache_instance_on_load": True }, + "OrnatrixRigLoader": { + "enabled": True, + "create_cache_instance_on_load": True + }, }