From ef55dd932d57694d5e872a3f6a9fe4f0cb77370b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 17 Apr 2023 13:00:01 +0200 Subject: [PATCH 01/18] :art: move startup script logic to hook --- .../hosts/max/hooks/force_startup_script.py | 24 +++++++++++++++++++ .../system_settings/applications.json | 4 +--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/hooks/force_startup_script.py diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py new file mode 100644 index 0000000000..4fcf4fef21 --- /dev/null +++ b/openpype/hosts/max/hooks/force_startup_script.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""Pre-launch to force 3ds max startup script.""" +from openpype.lib import PreLaunchHook +import os + + +class ForceStartupScript(PreLaunchHook): + """Inject OpenPype environment to 3ds max. + + Note that this works in combination whit 3dsmax startup script that + is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH + environment. + + Hook `GlobalHostDataHook` must be executed before this hook. + """ + app_groups = ["3dsmax"] + order = 11 + + def execute(self): + startup_args = [ + "-U", + "MAXScript", + f"{os.getenv('OPENPYPE_ROOT')}\\openpype\\hosts\\max\\startup\\startup.ms"] # noqa + self.launch_context.launch_args.append(startup_args) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d25e21a66e..6a0fb45698 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -133,9 +133,7 @@ "linux": [] }, "arguments": { - "windows": [ - "-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms" - ], + "windows": [], "darwin": [], "linux": [] }, From f7026c46948b22128ea43a3bf3a6558fa3215453 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 17 Apr 2023 13:06:07 +0200 Subject: [PATCH 02/18] :recycle: delete ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR --- openpype/settings/defaults/system_settings/applications.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 6a0fb45698..df5b5e07c6 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -119,9 +119,7 @@ "label": "3ds max", "icon": "{}/app_icons/3dsmax.png", "host_name": "max", - "environment": { - "ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR": "{OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup" - }, + "environment": {}, "variants": { "2023": { "use_python_2": false, From b05afaa8371d211d20431e4d4b245807438ef784 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Apr 2023 14:53:15 +0200 Subject: [PATCH 03/18] Global: Optimize anatomy formatting by only formatting used templates instead (#4784) * TemplatesDict can create different type of template * anatomy templates can be formatted on their own * return objected templates on get item * '_rootless_path' is public classmethod 'rootless_path_from_result' * 'AnatomyStringTemplate' expect anatomy templates * remove key getters * fix typo 'create_ojected_templates' -> 'create_objected_templates' * Fix type of argument * Fix long line * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Use format strict + code cosmetics * Get template from the formatted data * Update openpype/plugins/publish/integrate_legacy.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Cosmetics * Move template obj definition for path up + rename to `path_template_obj` * Refactor more cases from `anatomy.format` to template obj `.format_strict` * Refactor more cases from `anatomy.format` to template obj `.format_strict` * Refactor more cases from `anatomy.format` to template obj `.format_strict` --------- Co-authored-by: Jakub Trllo Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../avalon_uri_processor.py | 4 +-- .../publish/extract_workfile_location.py | 5 ++-- .../unreal/hooks/pre_workfile_preparation.py | 4 +-- openpype/lib/usdlib.py | 4 +-- .../deadline/abstract_submit_deadline.py | 4 +-- .../plugins/publish/submit_publish_job.py | 12 ++++---- openpype/pipeline/context_tools.py | 7 +++-- openpype/pipeline/delivery.py | 20 ++++++------- openpype/pipeline/workfile/path_resolving.py | 4 +-- .../plugins/publish/collect_resources_path.py | 12 ++++---- openpype/plugins/publish/integrate.py | 22 ++++++-------- .../plugins/publish/integrate_hero_version.py | 30 +++++++++---------- openpype/plugins/publish/integrate_legacy.py | 13 ++++---- .../plugins/publish/integrate_thumbnail.py | 6 ++-- .../push_to_project/control_integrate.py | 4 +-- openpype/tools/texture_copy/app.py | 4 +-- openpype/tools/workfiles/save_as_dialog.py | 8 ++--- 17 files changed, 79 insertions(+), 84 deletions(-) diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py index d7d1c79d73..48019e0a82 100644 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py @@ -128,14 +128,14 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): if not asset_doc: raise RuntimeError("Invalid asset name: '%s'" % asset) - formatted_anatomy = anatomy.format({ + template_obj = anatomy.templates_obj["publish"]["path"] + path = template_obj.format_strict({ "project": PROJECT, "asset": asset_doc["name"], "subset": subset, "representation": ext, "version": 0 # stub version zero }) - path = formatted_anatomy["publish"]["path"] # Remove the version folder subset_folder = os.path.dirname(os.path.dirname(path)) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py index 18bf0394ae..9ff84e32fb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -27,11 +27,12 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin): rep_name = instance.data.get("representations")[0].get("name") template_data["representation"] = rep_name template_data["ext"] = rep_name - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) filepath = os.path.normpath(template_filled) self.log.info("Using published scene for render {}".format( filepath)) + break if not filepath: self.log.info("Texture batch doesn't contain workfile.") diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 5dae7eef09..efbacc3b16 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -61,10 +61,10 @@ class UnrealPrelaunchHook(PreLaunchHook): project_name=project_doc["name"] ) # Fill templates - filled_anatomy = anatomy.format(workdir_data) + template_obj = anatomy.templates_obj[workfile_template_key]["file"] # Return filename - return filled_anatomy[workfile_template_key]["file"] + return template_obj.format_strict(workdir_data) def exec_plugin_install(self, engine_path: Path, env: dict = None): # set up the QThread and worker with necessary signals diff --git a/openpype/lib/usdlib.py b/openpype/lib/usdlib.py index 20703ee308..5ef1d38f87 100644 --- a/openpype/lib/usdlib.py +++ b/openpype/lib/usdlib.py @@ -327,7 +327,8 @@ def get_usd_master_path(asset, subset, representation): else: asset_doc = get_asset_by_name(project_name, asset, fields=["name"]) - formatted_result = anatomy.format( + template_obj = anatomy.templates_obj["publish"]["path"] + path = template_obj.format_strict( { "project": { "name": project_name, @@ -340,7 +341,6 @@ def get_usd_master_path(asset, subset, representation): } ) - path = formatted_result["publish"]["path"] # Remove the version folder subset_folder = os.path.dirname(os.path.dirname(path)) master_folder = os.path.join(subset_folder, "master") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 648eb77007..558a637e4b 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -534,8 +534,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): template_data["comment"] = None anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) file_path = os.path.normpath(template_filled) self.log.info("Using published scene for render {}".format(file_path)) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 4765772bcf..f80bd40133 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -1202,10 +1202,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): template_data["family"] = "render" template_data["version"] = version - anatomy_filled = anatomy.format(template_data) - - if "folder" in anatomy.templates["render"]: - publish_folder = anatomy_filled["render"]["folder"] + render_templates = anatomy.templates_obj["render"] + if "folder" in render_templates: + publish_folder = render_templates["folder"].format_strict( + template_data + ) else: # solve deprecated situation when `folder` key is not underneath # `publish` anatomy @@ -1215,8 +1216,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): " key underneath `publish` (in global of for project `{}`)." ).format(project_name)) - file_path = anatomy_filled["render"]["path"] - # Directory + file_path = render_templates["path"].format_strict(template_data) publish_folder = os.path.dirname(file_path) return publish_folder diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 6610fd7da7..dede2b8fce 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -463,9 +463,7 @@ def get_workdir_from_session(session=None, template_key=None): session = legacy_io.Session project_name = session["AVALON_PROJECT"] host_name = session["AVALON_APP"] - anatomy = Anatomy(project_name) template_data = get_template_data_from_session(session) - anatomy_filled = anatomy.format(template_data) if not template_key: task_type = template_data["task"]["type"] @@ -474,7 +472,10 @@ def get_workdir_from_session(session=None, template_key=None): host_name, project_name=project_name ) - path = anatomy_filled[template_key]["folder"] + + anatomy = Anatomy(project_name) + template_obj = anatomy.templates_obj[template_key]["folder"] + path = template_obj.format_strict(template_data) if path: path = os.path.normpath(path) return path diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 8cf9a43aac..500f54040a 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -1,5 +1,6 @@ """Functions useful for delivery of published representations.""" import os +import copy import shutil import glob import clique @@ -146,12 +147,11 @@ def deliver_single_file( report_items["Source file was not found"].append(msg) return report_items, 0 - anatomy_filled = anatomy.format(anatomy_data) if format_dict: - template_result = anatomy_filled["delivery"][template_name] - delivery_path = template_result.rootless.format(**format_dict) - else: - delivery_path = anatomy_filled["delivery"][template_name] + anatomy_data = copy.deepcopy(anatomy_data) + anatomy_data["root"] = format_dict["root"] + template_obj = anatomy.templates_obj["delivery"][template_name] + delivery_path = template_obj.format_strict(anatomy_data) # Backwards compatibility when extension contained `.` delivery_path = delivery_path.replace("..", ".") @@ -269,14 +269,12 @@ def deliver_sequence( frame_indicator = "@####@" + anatomy_data = copy.deepcopy(anatomy_data) anatomy_data["frame"] = frame_indicator - anatomy_filled = anatomy.format(anatomy_data) - if format_dict: - template_result = anatomy_filled["delivery"][template_name] - delivery_path = template_result.rootless.format(**format_dict) - else: - delivery_path = anatomy_filled["delivery"][template_name] + anatomy_data["root"] = format_dict["root"] + template_obj = anatomy.templates_obj["delivery"][template_name] + delivery_path = template_obj.format_strict(anatomy_data) delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) delivery_folder = os.path.dirname(delivery_path) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 801cb7223c..15689f4d99 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -132,9 +132,9 @@ def get_workdir_with_workdir_data( project_settings ) - anatomy_filled = anatomy.format(workdir_data) + template_obj = anatomy.templates_obj[template_key]["folder"] # Output is TemplateResult object which contain useful data - output = anatomy_filled[template_key]["folder"] + output = template_obj.format_strict(workdir_data) if output: return output.normalized() return output diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 4a5f9f1cc2..f96dd0ae18 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -83,10 +83,11 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "hierarchy": instance.data["hierarchy"] }) - anatomy_filled = anatomy.format(template_data) - - if "folder" in anatomy.templates["publish"]: - publish_folder = anatomy_filled["publish"]["folder"] + publish_templates = anatomy.templates_obj["publish"] + if "folder" in publish_templates: + publish_folder = publish_templates["folder"].format_strict( + template_data + ) else: # solve deprecated situation when `folder` key is not underneath # `publish` anatomy @@ -95,8 +96,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): " key underneath `publish` (in global of for project `{}`)." ).format(anatomy.project_name)) - file_path = anatomy_filled["publish"]["path"] - # Directory + file_path = publish_templates["path"].format_strict(template_data) publish_folder = os.path.dirname(file_path) publish_folder = os.path.normpath(publish_folder) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 07131ec3ae..65ce30412c 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -665,8 +665,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # - template_data (Dict[str, Any]): source data used to fill template # - to add required data to 'repre_context' not used for # formatting - # - anatomy_filled (Dict[str, Any]): filled anatomy of last file - # - to fill 'publishDir' on instance.data -> not ideal + path_template_obj = anatomy.templates_obj[template_name]["path"] # Treat template with 'orignalBasename' in special way if "{originalBasename}" in template: @@ -700,8 +699,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["originalBasename"], _ = os.path.splitext( src_file_name) - anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled[template_name]["path"] + dst = path_template_obj.format_strict(template_data) src = os.path.join(stagingdir, src_file_name) transfers.append((src, dst)) if repre_context is None: @@ -761,8 +759,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["udim"] = index else: template_data["frame"] = index - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_filled = path_template_obj.format_strict( + template_data + ) dst_filepaths.append(template_filled) if repre_context is None: self.log.debug( @@ -798,8 +797,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if is_udim: template_data["udim"] = repre["udim"][0] # Construct destination filepath from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_filled = path_template_obj.format_strict(template_data) repre_context = template_filled.used_values dst = os.path.normpath(template_filled) @@ -810,11 +808,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # todo: Are we sure the assumption each representation # ends up in the same folder is valid? if not instance.data.get("publishDir"): - instance.data["publishDir"] = ( - anatomy_filled - [template_name] - ["folder"] - ) + template_obj = anatomy.templates_obj[template_name]["folder"] + template_filled = template_obj.format_strict(template_data) + instance.data["publishDir"] = template_filled for key in self.db_representation_context_keys: # Also add these values to the context even if not used by the diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 80141e88fe..b71207c24f 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -291,6 +291,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): )) try: src_to_dst_file_paths = [] + path_template_obj = anatomy.templates_obj[template_key]["path"] for repre_info in published_repres.values(): # Skip if new repre does not have published repre files @@ -303,9 +304,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_data.pop("version", None) # Get filled path to repre context - anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled[template_key]["path"] - + template_filled = path_template_obj.format_strict(anatomy_data) repre_data = { "path": str(template_filled), "template": hero_template @@ -343,8 +342,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # Get head and tail for collection frame_splitter = "_-_FRAME_SPLIT_-_" anatomy_data["frame"] = frame_splitter - _anatomy_filled = anatomy.format(anatomy_data) - _template_filled = _anatomy_filled[template_key]["path"] + _template_filled = path_template_obj.format_strict( + anatomy_data + ) head, tail = _template_filled.split(frame_splitter) padding = int( anatomy.templates[template_key]["frame_padding"] @@ -520,24 +520,24 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): }) if "folder" in anatomy.templates[template_key]: - anatomy_filled = anatomy.format(template_data) - publish_folder = anatomy_filled[template_key]["folder"] + template_obj = anatomy.templates_obj[template_key]["folder"] + publish_folder = template_obj.format_strict(template_data) else: # This is for cases of Deprecated anatomy without `folder` # TODO remove when all clients have solved this issue - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) - anatomy_filled = anatomy.format(template_data) - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy self.log.warning(( "Deprecation warning: Anatomy does not have set `folder`" " key underneath `publish` (in global of for project `{}`)." ).format(anatomy.project_name)) + # solve deprecated situation when `folder` key is not underneath + # `publish` anatomy + template_data.update({ + "frame": "FRAME_TEMP", + "representation": "TEMP" + }) + template_obj = anatomy.templates_obj[template_key]["path"] + file_path = template_obj.format_strict(template_data) - file_path = anatomy_filled[template_key]["path"] # Directory publish_folder = os.path.dirname(file_path) diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 3f1f6ad0c9..c67ce62bf6 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -480,8 +480,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: template_data["udim"] = src_padding_exp % i - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_obj = anatomy.templates_obj[template_name]["path"] + template_filled = template_obj.format_strict(template_data) if repre_context is None: repre_context = template_filled.used_values test_dest_files.append( @@ -587,8 +587,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("udim"): template_data["udim"] = repre["udim"][0] src = os.path.join(stagingdir, fname) - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_obj = anatomy.templates_obj[template_name]["path"] + template_filled = template_obj.format_strict(template_data) repre_context = template_filled.used_values dst = os.path.normpath(template_filled) @@ -600,9 +600,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not instance.data.get("publishDir"): instance.data["publishDir"] = ( - anatomy_filled - [template_name] - ["folder"] + anatomy.templates_obj[template_name]["folder"] + .format_strict(template_data) ) if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 809a1782e0..16cc47d432 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -271,9 +271,9 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): "thumbnail_type": "thumbnail" }) - anatomy_filled = anatomy.format(template_data) - thumbnail_template = anatomy.templates["publish"]["thumbnail"] - template_filled = anatomy_filled["publish"]["thumbnail"] + template_obj = anatomy.templates_obj["publish"]["thumbnail"] + template_filled = template_obj.format_strict(template_data) + thumbnail_template = template_filled.template dst_full_path = os.path.normpath(str(template_filled)) self.log.debug("Copying file .. {} -> {}".format( diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index bb95fdb26f..37a0512d59 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -1050,8 +1050,8 @@ class ProjectPushItemProcess: repre_format_data["ext"] = ext[1:] break - tmp_result = anatomy.format(formatting_data) - folder_path = tmp_result[template_name]["folder"] + template_obj = anatomy.templates_obj[template_name]["folder"] + folder_path = template_obj.format_strict(formatting_data) repre_context = folder_path.used_values folder_path_rootless = folder_path.rootless repre_filepaths = [] diff --git a/openpype/tools/texture_copy/app.py b/openpype/tools/texture_copy/app.py index a695bb8c4d..a5a9f7349a 100644 --- a/openpype/tools/texture_copy/app.py +++ b/openpype/tools/texture_copy/app.py @@ -47,8 +47,8 @@ class TextureCopy: "hierarchy": hierarchy } anatomy = Anatomy(project_name) - anatomy_filled = anatomy.format(template_data) - return anatomy_filled['texture']['path'] + template_obj = anatomy.templates_obj["texture"]["path"] + return template_obj.format_strict(template_data) def _get_version(self, path): versions = [0] diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index aa881e7946..9f1d1060da 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -60,8 +60,8 @@ class CommentMatcher(object): temp_data["version"] = "<>" temp_data["ext"] = "<>" - formatted = anatomy.format(temp_data) - fname_pattern = formatted[template_key]["file"] + template_obj = anatomy.templates_obj[template_key]["file"] + fname_pattern = template_obj.format_strict(temp_data) fname_pattern = re.escape(fname_pattern) # Replace comment and version with something we can match with regex @@ -375,8 +375,8 @@ class SaveAsDialog(QtWidgets.QDialog): data["ext"] = data["ext"].lstrip(".") - anatomy_filled = self.anatomy.format(data) - return anatomy_filled[self.template_key]["file"] + template_obj = self.anatomy.templates_obj[self.template_key]["file"] + return template_obj.format_strict(data) def refresh(self): extensions = list(self._extensions) From 9d8ed55ea5577aa3abbbad768573778f1e3cf03a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 18 Apr 2023 11:07:07 +0100 Subject: [PATCH 04/18] Fix nested model instances. --- .../maya/plugins/publish/collect_review.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index fcb188734f..7ea91afdfc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -36,6 +36,30 @@ class CollectReview(pyblish.api.InstancePlugin): context = instance.context objectset = context.data['objectsets'] + # Convert enum attribute index to string for Display Lights. + index = instance.data.get("displayLights", 0) + display_lights = lib.DISPLAY_LIGHTS_VALUES[index] + if display_lights == "project_settings": + settings = instance.context.data["project_settings"] + settings = settings["maya"]["publish"]["ExtractPlayblast"] + settings = settings["capture_preset"]["Viewport Options"] + display_lights = settings["displayLights"] + + # Collect camera focal length. + burninDataMembers = instance.data.get("burninDataMembers", {}) + if camera is not None: + attr = camera + ".focalLength" + if lib.get_attribute_input(attr): + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + 1 + time_range = range(int(start), int(end)) + focal_length = [cmds.getAttr(attr, time=t) for t in time_range] + else: + focal_length = cmds.getAttr(attr) + + burninDataMembers["focalLength"] = focal_length + + # Account for nested instances like model. reviewable_subsets = list(set(members) & set(objectset)) if reviewable_subsets: if len(reviewable_subsets) > 1: @@ -77,6 +101,8 @@ class CollectReview(pyblish.api.InstancePlugin): data["isolate"] = instance.data["isolate"] data["panZoom"] = instance.data.get("panZoom", False) data["panel"] = instance.data["panel"] + data["displayLights"] = display_lights + data["burninDataMembers"] = burninDataMembers # The review instance must be active cmds.setAttr(str(instance) + '.active', 1) @@ -103,6 +129,8 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ instance.data["frameEndHandle"] + instance.data["displayLights"] = display_lights + instance.data["burninDataMembers"] = burninDataMembers # make ftrack publishable instance.data.setdefault("families", []).append('ftrack') @@ -144,33 +172,3 @@ class CollectReview(pyblish.api.InstancePlugin): audio_data.append(get_audio_node_data(node)) instance.data["audio"] = audio_data - - # Convert enum attribute index to string. - index = instance.data.get("displayLights", 0) - display_lights = lib.DISPLAY_LIGHTS_VALUES[index] - if display_lights == "project_settings": - settings = instance.context.data["project_settings"] - settings = settings["maya"]["publish"]["ExtractPlayblast"] - settings = settings["capture_preset"]["Viewport Options"] - display_lights = settings["displayLights"] - instance.data["displayLights"] = display_lights - - # Collect focal length. - if camera is None: - return - - attr = camera + ".focalLength" - if lib.get_attribute_input(attr): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + 1 - focal_length = [ - cmds.getAttr(attr, time=t) for t in range(int(start), int(end)) - ] - else: - focal_length = cmds.getAttr(attr) - - key = "focalLength" - try: - instance.data["burninDataMembers"][key] = focal_length - except KeyError: - instance.data["burninDataMembers"] = {key: focal_length} From 38cc309d43613e78d94ee7e9bbf53b1044b713be Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 18 Apr 2023 11:34:35 +0100 Subject: [PATCH 05/18] Fix #4851 --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 7ea91afdfc..5c190a4a7b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -92,6 +92,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] data['frameEndHandle'] = instance.data["frameEndHandle"] + data['handleStart'] = instance.data["handleStart"] + data['handleEnd'] = instance.data["handleEnd"] data["frameStart"] = instance.data["frameStart"] data["frameEnd"] = instance.data["frameEnd"] data['step'] = instance.data['step'] From 30eedb646e5a0c784dc5a2e0c485ac834f352f4b Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 18 Apr 2023 12:53:52 +0200 Subject: [PATCH 06/18] Patchelf version locked --- Dockerfile.centos7 | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index 5eb2f478ea..b35bde1589 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -53,6 +53,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n # we need to build our own patchelf WORKDIR /temp-patchelf RUN git clone https://github.com/NixOS/patchelf.git . \ + && git checkout 0.17.0 \ && source scl_source enable devtoolset-7 \ && ./bootstrap.sh \ && ./configure \ From b8e69a5b0171a807e5523b94f2f685d654714641 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 15:21:08 +0200 Subject: [PATCH 07/18] Support .abc files directly for Arnold standin look assignment --- .../maya/tools/mayalookassigner/alembic.py | 97 +++++++++++++++++++ .../tools/mayalookassigner/arnold_standin.py | 6 ++ .../tools/mayalookassigner/vray_proxies.py | 90 +---------------- 3 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 openpype/hosts/maya/tools/mayalookassigner/alembic.py diff --git a/openpype/hosts/maya/tools/mayalookassigner/alembic.py b/openpype/hosts/maya/tools/mayalookassigner/alembic.py new file mode 100644 index 0000000000..6885e923d3 --- /dev/null +++ b/openpype/hosts/maya/tools/mayalookassigner/alembic.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Tools for loading looks to vray proxies.""" +import os +from collections import defaultdict +import logging + +import six + +import alembic.Abc + + +log = logging.getLogger(__name__) + + +def get_alembic_paths_by_property(filename, attr, verbose=False): + # type: (str, str, bool) -> dict + """Return attribute value per objects in the Alembic file. + + Reads an Alembic archive hierarchy and retrieves the + value from the `attr` properties on the objects. + + Args: + filename (str): Full path to Alembic archive to read. + attr (str): Id attribute. + verbose (bool): Whether to verbosely log missing attributes. + + Returns: + dict: Mapping of node full path with its id + + """ + # Normalize alembic path + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + filename = str(filename) # path must be string + + try: + archive = alembic.Abc.IArchive(filename) + except RuntimeError: + # invalid alembic file - probably vrmesh + log.warning("{} is not an alembic file".format(filename)) + return {} + root = archive.getTop() + + iterator = list(root.children) + obj_ids = {} + + for obj in iterator: + name = obj.getFullName() + + # include children for coming iterations + iterator.extend(obj.children) + + props = obj.getProperties() + if props.getNumProperties() == 0: + # Skip those without properties, e.g. '/materials' in a gpuCache + continue + + # THe custom attribute is under the properties' first container under + # the ".arbGeomParams" + prop = props.getProperty(0) # get base property + + _property = None + try: + geo_params = prop.getProperty('.arbGeomParams') + _property = geo_params.getProperty(attr) + except KeyError: + if verbose: + log.debug("Missing attr on: {0}".format(name)) + continue + + if not _property.isConstant(): + log.warning("Id not constant on: {0}".format(name)) + + # Get first value sample + value = _property.getValue()[0] + + obj_ids[name] = value + + return obj_ids + + +def get_alembic_ids_cache(path): + # type: (str) -> dict + """Build a id to node mapping in Alembic file. + + Nodes without IDs are ignored. + + Returns: + dict: Mapping of id to nodes in the Alembic. + + """ + node_ids = get_alembic_paths_by_property(path, attr="cbId") + id_nodes = defaultdict(list) + for node, _id in six.iteritems(node_ids): + id_nodes[_id].append(node) + + return dict(six.iteritems(id_nodes)) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 7eeeb72553..0ce2b21dcd 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -9,6 +9,7 @@ from openpype.pipeline import legacy_io from openpype.client import get_last_version_by_subset_name from openpype.hosts.maya import api from . import lib +from .alembic import get_alembic_ids_cache log = logging.getLogger(__name__) @@ -68,6 +69,11 @@ def get_nodes_by_id(standin): (dict): Dictionary with node full name/path and id. """ path = cmds.getAttr(standin + ".dso") + + if path.endswith(".abc"): + # Support alembic files directly + return get_alembic_ids_cache(path) + json_path = None for f in os.listdir(os.path.dirname(path)): if f.endswith(".json"): diff --git a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py index 1d2ec5fd87..c875fec7f0 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -1,108 +1,20 @@ # -*- coding: utf-8 -*- """Tools for loading looks to vray proxies.""" -import os from collections import defaultdict import logging -import six - -import alembic.Abc from maya import cmds from openpype.client import get_last_version_by_subset_name from openpype.pipeline import legacy_io import openpype.hosts.maya.lib as maya_lib from . import lib +from .alembic import get_alembic_ids_cache log = logging.getLogger(__name__) -def get_alembic_paths_by_property(filename, attr, verbose=False): - # type: (str, str, bool) -> dict - """Return attribute value per objects in the Alembic file. - - Reads an Alembic archive hierarchy and retrieves the - value from the `attr` properties on the objects. - - Args: - filename (str): Full path to Alembic archive to read. - attr (str): Id attribute. - verbose (bool): Whether to verbosely log missing attributes. - - Returns: - dict: Mapping of node full path with its id - - """ - # Normalize alembic path - filename = os.path.normpath(filename) - filename = filename.replace("\\", "/") - filename = str(filename) # path must be string - - try: - archive = alembic.Abc.IArchive(filename) - except RuntimeError: - # invalid alembic file - probably vrmesh - log.warning("{} is not an alembic file".format(filename)) - return {} - root = archive.getTop() - - iterator = list(root.children) - obj_ids = {} - - for obj in iterator: - name = obj.getFullName() - - # include children for coming iterations - iterator.extend(obj.children) - - props = obj.getProperties() - if props.getNumProperties() == 0: - # Skip those without properties, e.g. '/materials' in a gpuCache - continue - - # THe custom attribute is under the properties' first container under - # the ".arbGeomParams" - prop = props.getProperty(0) # get base property - - _property = None - try: - geo_params = prop.getProperty('.arbGeomParams') - _property = geo_params.getProperty(attr) - except KeyError: - if verbose: - log.debug("Missing attr on: {0}".format(name)) - continue - - if not _property.isConstant(): - log.warning("Id not constant on: {0}".format(name)) - - # Get first value sample - value = _property.getValue()[0] - - obj_ids[name] = value - - return obj_ids - - -def get_alembic_ids_cache(path): - # type: (str) -> dict - """Build a id to node mapping in Alembic file. - - Nodes without IDs are ignored. - - Returns: - dict: Mapping of id to nodes in the Alembic. - - """ - node_ids = get_alembic_paths_by_property(path, attr="cbId") - id_nodes = defaultdict(list) - for node, _id in six.iteritems(node_ids): - id_nodes[_id].append(node) - - return dict(six.iteritems(id_nodes)) - - def assign_vrayproxy_shaders(vrayproxy, assignments): # type: (str, dict) -> None """Assign shaders to content of Vray Proxy. From f082b85fced9a98fdfdc529f43d792afa680ed50 Mon Sep 17 00:00:00 2001 From: 64qam Date: Tue, 18 Apr 2023 15:21:42 +0200 Subject: [PATCH 08/18] Update Dockerfile.centos7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- Dockerfile.centos7 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index b35bde1589..ce1a624a4f 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -52,8 +52,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n # we need to build our own patchelf WORKDIR /temp-patchelf -RUN git clone https://github.com/NixOS/patchelf.git . \ - && git checkout 0.17.0 \ +RUN git clone -b 0.17.0 --single-branch https://github.com/NixOS/patchelf.git . \ && source scl_source enable devtoolset-7 \ && ./bootstrap.sh \ && ./configure \ From dec2521c05b6998540a9dfeeb7f2cfa2afdb206d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:29:04 +0200 Subject: [PATCH 09/18] Do not change time slider ranges in `get_frame_range` function --- openpype/hosts/maya/api/lib.py | 90 ++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 61ea3d59df..a78ac184c2 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2153,17 +2153,23 @@ def set_scene_resolution(width, height, pixelAspect): cmds.setAttr("%s.pixelAspect" % control_node, pixelAspect) -def get_frame_range(): - """Get the current assets frame range and handles.""" +def get_frame_range(include_animation_range=False): + """Get the current assets frame range and handles. + + Args: + include_animation_range (bool, optional): Whether to include + `animationStart` and `animationEnd` keys to define the outer + range of the timeline. It is excluded by default. + + Returns: + dict: Asset's expected frame range values. + + """ # Set frame start/end project_name = get_current_project_name() - task_name = get_current_task_name() asset_name = get_current_asset_name() asset = get_asset_by_name(project_name, asset_name) - settings = get_project_settings(project_name) - include_handles_settings = settings["maya"]["include_handles"] - current_task = asset.get("data").get("tasks").get(task_name) frame_start = asset["data"].get("frameStart") frame_end = asset["data"].get("frameEnd") @@ -2175,32 +2181,39 @@ def get_frame_range(): handle_start = asset["data"].get("handleStart") or 0 handle_end = asset["data"].get("handleEnd") or 0 - animation_start = frame_start - animation_end = frame_end - - include_handles = include_handles_settings["include_handles_default"] - for item in include_handles_settings["per_task_type"]: - if current_task["type"] in item["task_type"]: - include_handles = item["include_handles"] - break - if include_handles: - animation_start -= int(handle_start) - animation_end += int(handle_end) - - cmds.playbackOptions( - minTime=frame_start, - maxTime=frame_end, - animationStartTime=animation_start, - animationEndTime=animation_end - ) - cmds.currentTime(frame_start) - - return { + frame_range = { "frameStart": frame_start, "frameEnd": frame_end, "handleStart": handle_start, - "handleEnd": handle_end + "handleEnd": handle_end, } + if include_animation_range: + # The animation range values are only included to define whether + # the Maya time slider should include the handles or not. + # Some usages of this function use the full dictionary to define + # instance attributes for which we want to exclude the animation + # keys. That is why these are excluded by default. + task_name = get_current_task_name() + settings = get_project_settings(project_name) + include_handles_settings = settings["maya"]["include_handles"] + current_task = asset.get("data").get("tasks").get(task_name) + + animation_start = frame_start + animation_end = frame_end + + include_handles = include_handles_settings["include_handles_default"] + for item in include_handles_settings["per_task_type"]: + if current_task["type"] in item["task_type"]: + include_handles = item["include_handles"] + break + if include_handles: + animation_start -= int(handle_start) + animation_end += int(handle_end) + + frame_range["animationStart"] = animation_start + frame_range["animationEnd"] = animation_end + + return frame_range def reset_frame_range(playback=True, render=True, fps=True): @@ -2219,18 +2232,19 @@ def reset_frame_range(playback=True, render=True, fps=True): ) set_scene_fps(fps) - frame_range = get_frame_range() - - frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) - frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + frame_range = get_frame_range(include_animation_range=True) + frame_start = frame_range["frameStart"] + frame_end = frame_range["frameEnd"] + animation_start = frame_range["animationStart"] + animation_end = frame_range["animationEnd"] if playback: - cmds.playbackOptions(minTime=frame_start) - cmds.playbackOptions(maxTime=frame_end) - cmds.playbackOptions(animationStartTime=frame_start) - cmds.playbackOptions(animationEndTime=frame_end) - cmds.playbackOptions(minTime=frame_start) - cmds.playbackOptions(maxTime=frame_end) + cmds.playbackOptions( + minTime=frame_start, + maxTime=frame_end, + animationStartTime=animation_start, + animationEndTime=animation_end + ) cmds.currentTime(frame_start) if render: From 8c1abf2b5b96dd3ed875b717f240402b14f71711 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:30:02 +0200 Subject: [PATCH 10/18] Allow potential case that frame range might not be defined on an asset. - Warning will still be printed from `get_frame_range` function --- openpype/hosts/maya/api/lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index a78ac184c2..e78da3d801 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2233,6 +2233,10 @@ def reset_frame_range(playback=True, render=True, fps=True): set_scene_fps(fps) frame_range = get_frame_range(include_animation_range=True) + if not frame_range: + # No frame range data found for asset + return + frame_start = frame_range["frameStart"] frame_end = frame_range["frameEnd"] animation_start = frame_range["animationStart"] From 0ad5442cd4280d6ee63deaf9cde9b55c37d3fc35 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 10:52:04 +0200 Subject: [PATCH 11/18] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index e78da3d801..c3de2c327f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2185,7 +2185,7 @@ def get_frame_range(include_animation_range=False): "frameStart": frame_start, "frameEnd": frame_end, "handleStart": handle_start, - "handleEnd": handle_end, + "handleEnd": handle_end } if include_animation_range: # The animation range values are only included to define whether From 839377696b970410b131bcd2129f5458dda2e56d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 15:29:20 +0200 Subject: [PATCH 12/18] Implement `switch` method on loaders --- openpype/hosts/houdini/plugins/load/load_alembic.py | 3 +++ openpype/hosts/houdini/plugins/load/load_alembic_archive.py | 3 +++ openpype/hosts/houdini/plugins/load/load_bgeo.py | 3 +++ openpype/hosts/houdini/plugins/load/load_camera.py | 3 +++ openpype/hosts/houdini/plugins/load/load_image.py | 3 +++ openpype/hosts/houdini/plugins/load/load_usd_layer.py | 3 +++ openpype/hosts/houdini/plugins/load/load_usd_reference.py | 3 +++ openpype/hosts/houdini/plugins/load/load_vdb.py | 3 +++ 8 files changed, 24 insertions(+) diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py index 96e666b255..c6f0ebf2f9 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic.py @@ -104,3 +104,6 @@ class AbcLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py index b960073e12..47d2e1b896 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py @@ -73,3 +73,6 @@ class AbcArchiveLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py index b298d423bc..86e8675c02 100644 --- a/openpype/hosts/houdini/plugins/load/load_bgeo.py +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -106,3 +106,6 @@ class BgeoLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 059ad11a76..6365508f4e 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -192,3 +192,6 @@ class CameraLoader(load.LoaderPlugin): new_node.moveToGoodPosition() return new_node + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index c78798e58a..26bc569c53 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -125,3 +125,6 @@ class ImageLoader(load.LoaderPlugin): prefix, padding, suffix = first_fname.rsplit(".", 2) fname = ".".join([prefix, "$F{}".format(len(padding)), suffix]) return os.path.join(root, fname).replace("\\", "/") + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_layer.py b/openpype/hosts/houdini/plugins/load/load_usd_layer.py index 2e5079925b..1f0ec25128 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_layer.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_layer.py @@ -79,3 +79,6 @@ class USDSublayerLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_reference.py b/openpype/hosts/houdini/plugins/load/load_usd_reference.py index c4371db39b..f66d05395e 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_reference.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_reference.py @@ -79,3 +79,6 @@ class USDReferenceLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py index c558a7a0e7..87900502c5 100644 --- a/openpype/hosts/houdini/plugins/load/load_vdb.py +++ b/openpype/hosts/houdini/plugins/load/load_vdb.py @@ -102,3 +102,6 @@ class VdbLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) From 1a10e0fc74e8d79bada3d1a2ab595250b6d43a92 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 18:01:43 +0200 Subject: [PATCH 13/18] Hide animation instance in creator + add inventory action to recreate animation publish instance for loaded rigs --- openpype/hosts/maya/api/lib.py | 52 +++++++++++++++++++ .../maya/plugins/create/create_animation.py | 6 +++ .../rig_recreate_animation_instance.py | 37 +++++++++++++ .../hosts/maya/plugins/load/load_reference.py | 44 ++-------------- .../defaults/project_settings/maya.json | 2 +- 5 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 61ea3d59df..db8195ac40 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -32,6 +32,10 @@ from openpype.pipeline import ( load_container, registered_host, ) +from openpype.pipeline.create import ( + legacy_create, + get_legacy_creator_by_name, +) from openpype.pipeline.context_tools import ( get_current_asset_name, get_current_project_asset, @@ -3913,3 +3917,51 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): capture_preset = plugin_settings["capture_preset"] return capture_preset or {} + + +def create_rig_animation_instance(nodes, context, namespace, log=None): + """Create an animation publish instance for loaded rigs. + + See the RecreateRigAnimationInstance inventory action on how to use this + for loaded rig containers. + + Arguments: + nodes (list): Member nodes of the rig instance. + context (dict): Representation context of the rig container + namespace (str): Namespace of the rig container + log (logging.Logger, optional): Logger to log to if provided + + Returns: + None + + """ + output = next((node for node in nodes if + node.endswith("out_SET")), None) + controls = next((node for node in nodes if + node.endswith("controls_SET")), None) + + assert output, "No out_SET in rig, this is a bug." + assert controls, "No controls_SET in rig, this is a bug." + + # Find the roots amongst the loaded nodes + roots = cmds.ls(nodes, assemblies=True, long=True) or \ + get_highest_in_hierarchy(nodes) + assert roots, "No root nodes in rig, this is a bug." + + asset = legacy_io.Session["AVALON_ASSET"] + dependency = str(context["representation"]["_id"]) + + if log: + log.info("Creating subset: {}".format(namespace)) + + # Create the animation instance + creator_plugin = get_legacy_creator_by_name("CreateAnimation") + with maintained_selection(): + cmds.select([output, controls] + roots, noExpand=True) + legacy_create( + creator_plugin, + name=namespace, + asset=asset, + options={"useSelection": True}, + data={"dependencies": dependency} + ) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index f992ff2c1a..095cbcdd64 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -7,6 +7,12 @@ from openpype.hosts.maya.api import ( class CreateAnimation(plugin.Creator): """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. + # Note: This setting is actually applied from project settings + enabled = False + name = "animationDefault" label = "Animation" family = "animation" diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py new file mode 100644 index 0000000000..fe4a123dfe --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -0,0 +1,37 @@ +from openpype.pipeline import ( + InventoryAction, + get_representation_context +) +from openpype.hosts.maya.api.lib import ( + create_rig_animation_instance, + get_container_members, +) + + +class RecreateRigAnimationInstance(InventoryAction): + """Recreate animation publish instance for loaded rigs""" + + label = "Recreate rig animation instance" + icon = "industry" + color = "#55DDAA" + + @staticmethod + def is_compatible(container): + return ( + container.get("loader") == "ReferenceLoader" + and container.get("name", "").startswith("rig") + ) + + def process(self, containers): + + for container in containers: + # todo: delete an existing entry if it exist or skip creation + + namespace = container["namespace"] + representation_id = container["representation"] + context = get_representation_context(representation_id) + nodes = get_container_members(container) + + create_rig_animation_instance(nodes, context, namespace) + + diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c2b321b789..0dbdb03bb7 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -4,16 +4,12 @@ import contextlib from maya import cmds from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io -from openpype.pipeline.create import ( - legacy_create, - get_legacy_creator_by_name, -) import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, - parent_nodes + parent_nodes, + create_rig_animation_instance ) @@ -114,9 +110,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - # Name of creator class that will be used to create animation instance - animation_creator_name = "CreateAnimation" - def process_reference(self, context, name, namespace, options): import maya.cmds as cmds @@ -220,37 +213,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self._lock_camera_transforms(members) def _post_process_rig(self, name, namespace, context, options): - - output = next((node for node in self if - node.endswith("out_SET")), None) - controls = next((node for node in self if - node.endswith("controls_SET")), None) - - assert output, "No out_SET in rig, this is a bug." - assert controls, "No controls_SET in rig, this is a bug." - - # Find the roots amongst the loaded nodes - roots = cmds.ls(self[:], assemblies=True, long=True) - assert roots, "No root nodes in rig, this is a bug." - - asset = legacy_io.Session["AVALON_ASSET"] - dependency = str(context["representation"]["_id"]) - - self.log.info("Creating subset: {}".format(namespace)) - - # Create the animation instance - creator_plugin = get_legacy_creator_by_name( - self.animation_creator_name + nodes = self[:] + create_rig_animation_instance( + nodes, context, namespace, log=self.log ) - with maintained_selection(): - cmds.select([output, controls] + roots, noExpand=True) - legacy_create( - creator_plugin, - name=namespace, - asset=asset, - options={"useSelection": True}, - data={"dependencies": dependency} - ) def _lock_camera_transforms(self, nodes): cameras = cmds.ls(nodes, type="camera") diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5960547d46..91712e6672 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -554,7 +554,7 @@ "publish_mip_map": true }, "CreateAnimation": { - "enabled": true, + "enabled": false, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, From fbc0430bb21f0c6af39e907986873cfac83d625f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 18:06:25 +0200 Subject: [PATCH 14/18] Tweak color + icon --- .../maya/plugins/inventory/rig_recreate_animation_instance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py index fe4a123dfe..90b4d3eab8 100644 --- a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -12,8 +12,8 @@ class RecreateRigAnimationInstance(InventoryAction): """Recreate animation publish instance for loaded rigs""" label = "Recreate rig animation instance" - icon = "industry" - color = "#55DDAA" + icon = "wrench" + color = "#888888" @staticmethod def is_compatible(container): From 5b7d419e18087354b576c1f73452df2c74a170f1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 18:07:08 +0200 Subject: [PATCH 15/18] Cosmetics --- openpype/hosts/maya/api/lib.py | 6 ++++-- .../plugins/inventory/rig_recreate_animation_instance.py | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index db8195ac40..f3c079506b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3944,8 +3944,10 @@ def create_rig_animation_instance(nodes, context, namespace, log=None): assert controls, "No controls_SET in rig, this is a bug." # Find the roots amongst the loaded nodes - roots = cmds.ls(nodes, assemblies=True, long=True) or \ - get_highest_in_hierarchy(nodes) + roots = ( + cmds.ls(nodes, assemblies=True, long=True) or + get_highest_in_hierarchy(nodes) + ) assert roots, "No root nodes in rig, this is a bug." asset = legacy_io.Session["AVALON_ASSET"] diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py index 90b4d3eab8..39bc59fbbf 100644 --- a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -33,5 +33,3 @@ class RecreateRigAnimationInstance(InventoryAction): nodes = get_container_members(container) create_rig_animation_instance(nodes, context, namespace) - - From b82279f9d7e47e18c9b7f7e8e9755b8f658a4dac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 21:58:16 +0200 Subject: [PATCH 16/18] Fix default so namespace behaves like before #4511 --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5960547d46..a535f8d4c9 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1459,7 +1459,7 @@ ] }, "reference_loader": { - "namespace": "{asset_name}_{subset}_##", + "namespace": "{asset_name}_{subset}_##_", "group_name": "_GRP" } }, From 1ab4243d58fc24e932edb10dd932719561d033fc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 14:03:29 +0200 Subject: [PATCH 17/18] Tweak rig publish + load documentation, add documentation for Recreate rig animation instance action --- website/docs/artist_hosts_maya.md | 70 +++++++++++++----- ...ory_action_recreate_animation_instance.png | Bin 0 -> 46819 bytes 2 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 website/docs/assets/maya-inventory_action_recreate_animation_instance.png diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 0a551f0213..6b2abcb58b 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -238,12 +238,12 @@ For resolution and frame range, use **OpenPype → Set Frame Range** and Creating and publishing rigs with OpenPype follows similar workflow as with other data types. Create your rig and mark parts of your hierarchy in sets to -help OpenPype validators and extractors to check it and publish it. +help OpenPype validators and extractors to check and publish it. ### Preparing rig for publish When creating rigs, it is recommended (and it is in fact enforced by validators) -to separate bones or driving objects, their controllers and geometry so they are +to separate bone or driven objects, their controllers and geometry so they are easily managed. Currently OpenPype doesn't allow to publish model at the same time as its rig so for demonstration purposes, I'll first create simple model for robotic arm, just made out of simple boxes and I'll publish it. @@ -252,41 +252,48 @@ arm, just made out of simple boxes and I'll publish it. For more information about publishing models, see [Publishing models](artist_hosts_maya.md#publishing-models). -Now lets start with empty scene. Load your model - **OpenPype → Load...**, right +Now let's start with empty scene. Load your model - **OpenPype → Load...**, right click on it and select **Reference (abc)**. -I've created few bones and their controllers in two separate -groups - `rig_GRP` and `controls_GRP`. Naming is not important - just adhere to -your naming conventions. +I've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and +placed the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to +your naming conventions. Then I parented everything into a single top group named `arm_rig`. -Then I've put everything into `arm_rig` group. - -When you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype. -Select your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**. -Set is created in your scene to mark rig parts for export. Notice that it has -two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` +With the prepared hierarchy it is time to create a *Rig instance* in OpenPype. +Select the top group of your rig and go to **OpenPype → Create...**. Select **Rig**. +A publish set for your rig is created in your scene to mark rig parts for export. +Notice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` and geometry to `out_SET`. You should end up with something like this: ![Maya - Rig Hierarchy Example](assets/maya-rig_hierarchy_example.jpg) +:::note controls_SET and out_SET contents +It is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to +the individual meshes - it's even **recommended**. However, the `controls_SET` +requires the individual controls in it that the artist is supposed to animate +and manipulate so the publish validators can accurately check the rig's +controls. +::: + ### Publishing rigs -Publishing rig is done in same way as publishing everything else. Save your scene -and go **OpenPype → Publish**. When you run validation you'll mostly run at first into -few issues. Although number of them will seem to be intimidating at first, you'll -find out they are mostly minor things easily fixed. +Publishing rigs is done in a same way as publishing everything else. Save your scene +and go **OpenPype → Publish**. When you run validation you'll most likely run into +a few issues at first. Although a number of them will seem to be intimidating you +will find out they are mostly minor things, easily fixed and are there to optimize +your rig for consistency and safe usage by the artist. -* **Non Duplicate Instance Members (ID)** - This will most likely fail because when +- **Non Duplicate Instance Members (ID)** - This will most likely fail because when creating rigs, we usually duplicate few parts of it to reuse them. But duplication will duplicate also ID of original object and OpenPype needs every object to have unique ID. This is easily fixed by **Repair** action next to validator name. click on little up arrow on right side of validator name and select **Repair** form menu. -* **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as +- **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as animator usually doesn't need to see them and they clutter his viewports. So well behaving rig should have them hidden. **Repair** action will help here also. -* **Rig Controllers** will check if there are no transforms on unlocked attributes +- **Rig Controllers** will check if there are no transforms on unlocked attributes of controllers. This is needed because animator should have ease way to reset rig to it's default position. It also check that those attributes doesn't have any incoming connections from other parts of scene to ensure that published rig doesn't @@ -297,6 +304,19 @@ have any missing dependencies. You can load rig with [Loader](artist_tools_loader). Go **OpenPype → Load...**, select your rig, right click on it and **Reference** it. +### Animation instances + +Whenever you load a rig an animation publish instance is automatically created +for it. This means that if you load a rig you don't need to create a pointcache +instance yourself to publish the geometry. This is all cleanly prepared for you +when loading a published rig. + +:::tip Missing animation instance for your loaded rig? +Did you accidentally delete the animation instance for a loaded rig? You can +recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) +inventory action. +::: + ## Point caches OpenPype is using Alembic format for point caches. Workflow is very similar as other data types. @@ -646,3 +666,15 @@ Select 1 container of type `animation` or `pointcache`, then 1+ container of any The action searches the selected containers for 1 animation container of type `animation` or `pointcache`. This animation container will be connected to the rest of the selected containers. Matching geometries between containers is done by comparing the attribute `cbId`. The connection between geometries is done with a live blendshape. + +### Recreate rig animation instance + +This action can regenerate an animation instance for a loaded rig, for example +for when it was accidentally deleted by the user. + +![Maya - Inventory Action Recreate Rig Animation Instance](assets/maya-inventory_action_recreate_animation_instance.png) + +#### Usage + +Select 1 or more container of type `rig` for which you want to recreate the +animation instance. \ No newline at end of file diff --git a/website/docs/assets/maya-inventory_action_recreate_animation_instance.png b/website/docs/assets/maya-inventory_action_recreate_animation_instance.png new file mode 100644 index 0000000000000000000000000000000000000000..42a6f269648df6f9ef0420e96e3ab137c7321a0a GIT binary patch literal 46819 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�il6%M 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 HVbuQsK%Is3 literal 0 HcmV?d00001 From a8abe2dda8f46ea3d9bb73cd63655c0ed8b7fadd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 14:04:40 +0200 Subject: [PATCH 18/18] Fix typo --- website/docs/artist_hosts_maya.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 6b2abcb58b..e36ccb77d2 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -243,7 +243,7 @@ help OpenPype validators and extractors to check and publish it. ### Preparing rig for publish When creating rigs, it is recommended (and it is in fact enforced by validators) -to separate bone or driven objects, their controllers and geometry so they are +to separate bones or driven objects, their controllers and geometry so they are easily managed. Currently OpenPype doesn't allow to publish model at the same time as its rig so for demonstration purposes, I'll first create simple model for robotic arm, just made out of simple boxes and I'll publish it. @@ -257,11 +257,11 @@ click on it and select **Reference (abc)**. I've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and placed the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to -your naming conventions. Then I parented everything into a single top group named `arm_rig`. +your naming conventions. Then I parented everything into a single top group named `arm_rig`. With the prepared hierarchy it is time to create a *Rig instance* in OpenPype. Select the top group of your rig and go to **OpenPype → Create...**. Select **Rig**. -A publish set for your rig is created in your scene to mark rig parts for export. +A publish set for your rig is created in your scene to mark rig parts for export. Notice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` and geometry to `out_SET`. You should end up with something like this: @@ -269,19 +269,19 @@ and geometry to `out_SET`. You should end up with something like this: :::note controls_SET and out_SET contents It is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to -the individual meshes - it's even **recommended**. However, the `controls_SET` +the individual meshes - it's even **recommended**. However, the `controls_SET` requires the individual controls in it that the artist is supposed to animate -and manipulate so the publish validators can accurately check the rig's +and manipulate so the publish validators can accurately check the rig's controls. ::: ### Publishing rigs Publishing rigs is done in a same way as publishing everything else. Save your scene -and go **OpenPype → Publish**. When you run validation you'll most likely run into -a few issues at first. Although a number of them will seem to be intimidating you +and go **OpenPype → Publish**. When you run validation you'll most likely run into +a few issues at first. Although a number of them will seem to be intimidating you will find out they are mostly minor things, easily fixed and are there to optimize -your rig for consistency and safe usage by the artist. +your rig for consistency and safe usage by the artist. - **Non Duplicate Instance Members (ID)** - This will most likely fail because when creating rigs, we usually duplicate few parts of it to reuse them. But duplication @@ -312,8 +312,8 @@ instance yourself to publish the geometry. This is all cleanly prepared for you when loading a published rig. :::tip Missing animation instance for your loaded rig? -Did you accidentally delete the animation instance for a loaded rig? You can -recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) +Did you accidentally delete the animation instance for a loaded rig? You can +recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) inventory action. ::: @@ -677,4 +677,4 @@ for when it was accidentally deleted by the user. #### Usage Select 1 or more container of type `rig` for which you want to recreate the -animation instance. \ No newline at end of file +animation instance.