From 2fdd13738066f320359a2cf7d84407bcd93e8fbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 28 Nov 2022 09:40:42 +0000 Subject: [PATCH 001/139] Optional control of display lights on playblast. --- openpype/hosts/maya/plugins/create/create_review.py | 2 ++ openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index ba51ffa009..65aeb2d76a 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -25,6 +25,7 @@ class CreateReview(plugin.Creator): "depth peeling", "alpha cut" ] + displayLights = ["default", "all", "selected", "active", "none"] def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -41,5 +42,6 @@ class CreateReview(plugin.Creator): data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency + data["displayLights"] = self.displayLights self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index b19d24fad7..cbf99eccaa 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -97,6 +97,10 @@ class ExtractPlayblast(publish.Extractor): refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True)) pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + + # Show lighting mode. + index = instance.data.get("displayLights", 0) + preset["viewport_options"]["displayLights"] = self.displayLights[index] # Override transparency if requested. transparency = instance.data.get("transparency", 0) From e214062047f09e6b0879cb015e6445b889b0d33a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Dec 2022 12:19:57 +0000 Subject: [PATCH 002/139] Missing class data. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index cbf99eccaa..c6bbe44efc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -23,6 +23,7 @@ class ExtractPlayblast(publish.Extractor): families = ["review"] optional = True capture_preset = {} + displayLights = ["default", "all", "selected", "active", "none"] def process(self, instance): self.log.info("Extracting capture..") @@ -97,7 +98,7 @@ class ExtractPlayblast(publish.Extractor): refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True)) pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) - + # Show lighting mode. index = instance.data.get("displayLights", 0) preset["viewport_options"]["displayLights"] = self.displayLights[index] From 6bc8748b99e58894479071e14b04780a3da9cd15 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Dec 2022 09:18:45 +0000 Subject: [PATCH 003/139] Collect display lights list in lib. --- openpype/hosts/maya/api/lib.py | 2 ++ openpype/hosts/maya/plugins/create/create_review.py | 3 +-- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2530021eba..617e4e3d3a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -113,6 +113,8 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] +DISPLAY_LIGHTS = ["default", "all", "selected", "active", "none"] + def get_main_window(): """Acquire Maya's main window""" diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 65aeb2d76a..1935d18deb 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -25,7 +25,6 @@ class CreateReview(plugin.Creator): "depth peeling", "alpha cut" ] - displayLights = ["default", "all", "selected", "active", "none"] def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -42,6 +41,6 @@ class CreateReview(plugin.Creator): data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency - data["displayLights"] = self.displayLights + data["displayLights"] = lib.DISPLAY_LIGHTS self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index c6bbe44efc..d8e1184335 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -23,7 +23,6 @@ class ExtractPlayblast(publish.Extractor): families = ["review"] optional = True capture_preset = {} - displayLights = ["default", "all", "selected", "active", "none"] def process(self, instance): self.log.info("Extracting capture..") @@ -101,7 +100,7 @@ class ExtractPlayblast(publish.Extractor): # Show lighting mode. index = instance.data.get("displayLights", 0) - preset["viewport_options"]["displayLights"] = self.displayLights[index] + preset["viewport_options"]["displayLights"] = lib.DISPLAY_LIGHTS[index] # Override transparency if requested. transparency = instance.data.get("transparency", 0) From c921bc14c56ec3780981e6a432a26bc32fd84235 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 15 Dec 2022 08:57:22 +0000 Subject: [PATCH 004/139] Convert enum to string in collector --- openpype/hosts/maya/plugins/publish/collect_review.py | 5 +++++ openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index eb872c2935..995bd23687 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -5,6 +5,7 @@ import pyblish.api from openpype.client import get_subset_by_name from openpype.pipeline import legacy_io +from openpype.hosts.maya.api import lib class CollectReview(pyblish.api.InstancePlugin): @@ -139,3 +140,7 @@ class CollectReview(pyblish.api.InstancePlugin): "filename": node.filename.get() } ) + + # Convert enum attribute to string. + index = instance.data.get("displayLights", 0) + instance.data["displayLights"] = lib.DISPLAY_LIGHTS[index] diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index d8e1184335..08eb754c6d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -99,8 +99,8 @@ class ExtractPlayblast(publish.Extractor): pm.currentTime(refreshFrameInt, edit=True) # Show lighting mode. - index = instance.data.get("displayLights", 0) - preset["viewport_options"]["displayLights"] = lib.DISPLAY_LIGHTS[index] + display_lights = instance.data["displayLights"] + preset["viewport_options"]["displayLights"] = display_lights # Override transparency if requested. transparency = instance.data.get("transparency", 0) From 9284e986d1f3d3131bb4b4cce1a2808cad141bb0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 15 Dec 2022 08:57:36 +0000 Subject: [PATCH 005/139] Use display lights in thumbnail --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 712159c2be..bb9cef2c5c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -105,6 +105,10 @@ class ExtractThumbnail(publish.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + # Show lighting mode. + display_lights = instance.data["displayLights"] + preset["viewport_options"]["displayLights"] = display_lights + # Isolate view is requested by having objects in the set besides a # camera. if preset.pop("isolate_view", False) and instance.data.get("isolate"): From 67b95c218b51e5c87132e5270d0a7c47760ed7e5 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 15 Dec 2022 13:13:48 +0000 Subject: [PATCH 006/139] Update openpype/hosts/maya/plugins/publish/collect_review.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 995bd23687..d15eb7a12b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -141,6 +141,6 @@ class CollectReview(pyblish.api.InstancePlugin): } ) - # Convert enum attribute to string. + # Convert enum attribute index to string. index = instance.data.get("displayLights", 0) instance.data["displayLights"] = lib.DISPLAY_LIGHTS[index] From ae496b9712bafc77a0d8350b92b0e84505eee512 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Jan 2023 07:30:29 +0000 Subject: [PATCH 007/139] Use project settings by default. --- openpype/hosts/maya/api/lib.py | 4 +++- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4b8b6b1949..9aa2325e25 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -113,7 +113,9 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] -DISPLAY_LIGHTS = ["default", "all", "selected", "active", "none"] +DISPLAY_LIGHTS = [ + "project_settings", "default", "all", "selected", "active", "none" +] def get_main_window(): diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index a1e6b2d503..7542785152 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -100,7 +100,8 @@ class ExtractPlayblast(publish.Extractor): # Show lighting mode. display_lights = instance.data["displayLights"] - preset["viewport_options"]["displayLights"] = display_lights + if display_lights != "project_settings": + preset["viewport_options"]["displayLights"] = display_lights # Override transparency if requested. transparency = instance.data.get("transparency", 0) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 80e94303a6..de6bc3895e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -107,7 +107,8 @@ class ExtractThumbnail(publish.Extractor): # Show lighting mode. display_lights = instance.data["displayLights"] - preset["viewport_options"]["displayLights"] = display_lights + if display_lights != "project_settings": + preset["viewport_options"]["displayLights"] = display_lights # Override transparency if requested. transparency = instance.data.get("transparency", 0) From f9f95b84e68da86ce53f9881ee59b98acb6d9aef Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 25 Jan 2023 11:09:19 +0000 Subject: [PATCH 008/139] Basic implementation of the new Creator --- openpype/hosts/unreal/api/__init__.py | 6 +- openpype/hosts/unreal/api/pipeline.py | 53 ++++++- openpype/hosts/unreal/api/plugin.py | 209 +++++++++++++++++++++++++- 3 files changed, 262 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index ca9db259e6..2618a7677c 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Unreal Editor OpenPype host API.""" -from .plugin import Loader +from .plugin import ( + UnrealActorCreator, + UnrealAssetCreator, + Loader +) from .pipeline import ( install, diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 2081c8fd13..7a21effcbc 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import os +import json import logging from typing import List from contextlib import contextmanager @@ -16,13 +17,14 @@ from openpype.pipeline import ( ) from openpype.tools.utils import host_tools import openpype.hosts.unreal -from openpype.host import HostBase, ILoadHost +from openpype.host import HostBase, ILoadHost, IPublishHost import unreal # noqa - logger = logging.getLogger("openpype.hosts.unreal") + OPENPYPE_CONTAINERS = "OpenPypeContainers" +CONTEXT_CONTAINER = "OpenPype/context.json" UNREAL_VERSION = semver.VersionInfo( *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") ) @@ -35,7 +37,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") -class UnrealHost(HostBase, ILoadHost): +class UnrealHost(HostBase, ILoadHost, IPublishHost): """Unreal host implementation. For some time this class will re-use functions from module based @@ -60,6 +62,26 @@ class UnrealHost(HostBase, ILoadHost): show_tools_dialog() + def update_context_data(self, data, changes): + unreal.log_warning("update_context_data") + unreal.log_warning(data) + content_path = unreal.Paths.project_content_dir() + op_ctx = content_path + CONTEXT_CONTAINER + with open(op_ctx, "w+") as f: + json.dump(data, f) + with open(op_ctx, "r") as fp: + test = eval(json.load(fp)) + unreal.log_warning(test) + + def get_context_data(self): + content_path = unreal.Paths.project_content_dir() + op_ctx = content_path + CONTEXT_CONTAINER + if not os.path.isfile(op_ctx): + return {} + with open(op_ctx, "r") as fp: + data = eval(json.load(fp)) + return data + def install(): """Install Unreal configuration for OpenPype.""" @@ -133,6 +155,31 @@ def ls(): yield data +def lsinst(): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + # UE 5.1 changed how class name is specified + class_name = [ + "/Script/OpenPype", + "OpenPypePublishInstance" + ] if ( + UNREAL_VERSION.major == 5 + and UNREAL_VERSION.minor > 0 + ) else "OpenPypePublishInstance" # noqa + instances = ar.get_assets_by_class(class_name, True) + + # get_asset_by_class returns AssetData. To get all metadata we need to + # load asset. get_tag_values() work only on metadata registered in + # Asset Registry Project settings (and there is no way to set it with + # python short of editing ini configuration file). + for asset_data in instances: + asset = asset_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset_data.asset_name + data = cast_map_to_str_dict(data) + + yield data + + def parse_container(container): """To get data from container, AssetContainer must be loaded. diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 6fc00cb71c..f89ff153b1 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,7 +1,212 @@ # -*- coding: utf-8 -*- -from abc import ABC +import sys +import six +from abc import ( + ABC, + ABCMeta, + abstractmethod +) -from openpype.pipeline import LoaderPlugin +import unreal + +from .pipeline import ( + create_publish_instance, + imprint, + lsinst +) +from openpype.lib import BoolDef +from openpype.pipeline import ( + Creator, + LoaderPlugin, + CreatorError, + CreatedInstance +) + + +class OpenPypeCreatorError(CreatorError): + pass + + +@six.add_metaclass(ABCMeta) +class UnrealBaseCreator(Creator): + """Base class for Unreal creator plugins.""" + root = "/Game/OpenPype/PublishInstances" + suffix = "_INS" + + @staticmethod + def cache_subsets(shared_data): + """Cache instances for Creators to shared data. + + Create `unreal_cached_subsets` key when needed in shared data and + fill it with all collected instances from the scene under its + respective creator identifiers. + + If legacy instances are detected in the scene, create + `unreal_cached_legacy_subsets` there and fill it with + all legacy subsets under family as a key. + + Args: + Dict[str, Any]: Shared data. + + Return: + Dict[str, Any]: Shared data dictionary. + + """ + if shared_data.get("unreal_cached_subsets") is None: + shared_data["unreal_cached_subsets"] = {} + if shared_data.get("unreal_cached_legacy_subsets") is None: + shared_data["unreal_cached_legacy_subsets"] = {} + cached_instances = lsinst() + for i in cached_instances: + if not i.get("creator_identifier"): + # we have legacy instance + family = i.get("family") + if (family not in + shared_data["unreal_cached_legacy_subsets"]): + shared_data[ + "unreal_cached_legacy_subsets"][family] = [i] + else: + shared_data[ + "unreal_cached_legacy_subsets"][family].append(i) + continue + + creator_id = i.get("creator_identifier") + if creator_id not in shared_data["unreal_cached_subsets"]: + shared_data["unreal_cached_subsets"][creator_id] = [i] + else: + shared_data["unreal_cached_subsets"][creator_id].append(i) + return shared_data + + @abstractmethod + def create(self, subset_name, instance_data, pre_create_data): + pass + + def collect_instances(self): + # cache instances if missing + self.cache_subsets(self.collection_shared_data) + for instance in self.collection_shared_data[ + "unreal_cached_subsets"].get(self.identifier, []): + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + unreal.log_warning(f"Update instances: {update_list}") + for created_inst, _changes in update_list: + instance_node = created_inst.get("instance_path", "") + + if not instance_node: + unreal.log_warning( + f"Instance node not found for {created_inst}") + + new_values = { + key: new_value + for key, (_old_value, new_value) in _changes.items() + } + imprint( + instance_node, + new_values + ) + + def remove_instances(self, instances): + for instance in instances: + instance_node = instance.data.get("instance_path", "") + if instance_node: + unreal.EditorAssetLibrary.delete_asset(instance_node) + + self._remove_instance_from_context(instance) + + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection") + ] + + +@six.add_metaclass(ABCMeta) +class UnrealAssetCreator(UnrealBaseCreator): + """Base class for Unreal creator plugins based on assets.""" + + def create(self, subset_name, instance_data, pre_create_data): + """Create instance of the asset. + + Args: + subset_name (str): Name of the subset. + instance_data (dict): Data for the instance. + pre_create_data (dict): Data for the instance. + + Returns: + CreatedInstance: Created instance. + """ + try: + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + instance_data["members"] = selection + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) + + +@six.add_metaclass(ABCMeta) +class UnrealActorCreator(UnrealBaseCreator): + """Base class for Unreal creator plugins based on actors.""" + + def create(self, subset_name, instance_data, pre_create_data): + """Create instance of the asset. + + Args: + subset_name (str): Name of the subset. + instance_data (dict): Data for the instance. + pre_create_data (dict): Data for the instance. + + Returns: + CreatedInstance: Created instance. + """ + try: + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + selection = [a.get_path_name() for a in sel_objects] + + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + instance_data["members"] = selection + instance_data[ + "level"] = unreal.EditorLevelLibrary.get_editor_world() + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) class Loader(LoaderPlugin, ABC): From fc09f0b532cf3a1ee496a9f74ae22d55753e7841 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Jan 2023 17:35:11 +0000 Subject: [PATCH 009/139] Improved basic creator --- openpype/hosts/unreal/api/plugin.py | 95 ++++++++++++++++++----------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index f89ff153b1..6a561420fa 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -4,7 +4,6 @@ import six from abc import ( ABC, ABCMeta, - abstractmethod ) import unreal @@ -12,7 +11,8 @@ import unreal from .pipeline import ( create_publish_instance, imprint, - lsinst + lsinst, + UNREAL_VERSION ) from openpype.lib import BoolDef from openpype.pipeline import ( @@ -77,9 +77,28 @@ class UnrealBaseCreator(Creator): shared_data["unreal_cached_subsets"][creator_id].append(i) return shared_data - @abstractmethod def create(self, subset_name, instance_data, pre_create_data): - pass + try: + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) def collect_instances(self): # cache instances if missing @@ -117,7 +136,7 @@ class UnrealBaseCreator(Creator): def get_pre_create_attr_defs(self): return [ - BoolDef("use_selection", label="Use selection") + BoolDef("use_selection", label="Use selection", default=True) ] @@ -137,25 +156,21 @@ class UnrealAssetCreator(UnrealBaseCreator): CreatedInstance: Created instance. """ try: - selection = [] + # Check if instance data has members, filled by the plugin. + # If not, use selection. + if not instance_data.get("members"): + selection = [] - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] - instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) - instance_data["members"] = selection - instance_data["subset"] = subset_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - instance = CreatedInstance( - self.family, + instance_data["members"] = selection + + super(UnrealAssetCreator, self).create( subset_name, instance_data, - self) - self._add_instance_to_context(instance) - - imprint(f"{self.root}/{instance_name}", instance_data) + pre_create_data) except Exception as er: six.reraise( @@ -180,27 +195,33 @@ class UnrealActorCreator(UnrealBaseCreator): CreatedInstance: Created instance. """ try: - selection = [] + if UNREAL_VERSION.major == 5: + world = unreal.UnrealEditorSubsystem().get_editor_world() + else: + world = unreal.EditorLevelLibrary.get_editor_world() - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() - selection = [a.get_path_name() for a in sel_objects] + # Check if the level is saved + if world.get_path_name().startswith("/Temp/"): + raise OpenPypeCreatorError( + "Level must be saved before creating instances.") - instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) - instance_data["members"] = selection - instance_data[ - "level"] = unreal.EditorLevelLibrary.get_editor_world() - instance_data["subset"] = subset_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - instance = CreatedInstance( - self.family, + # Check if instance data has members, filled by the plugin. + # If not, use selection. + if not instance_data.get("members"): + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + selection = [a.get_path_name() for a in sel_objects] + + instance_data["members"] = selection + + instance_data["level"] = world.get_path_name() + + super(UnrealActorCreator, self).create( subset_name, instance_data, - self) - self._add_instance_to_context(instance) - - imprint(f"{self.root}/{instance_name}", instance_data) + pre_create_data) except Exception as er: six.reraise( From f57a6775cc0c0a88ec85002432fbbcaa394cf8ca Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Jan 2023 17:35:42 +0000 Subject: [PATCH 010/139] Updated creators to be compatible with new publisher --- .../unreal/plugins/create/create_camera.py | 44 +++---------- .../unreal/plugins/create/create_layout.py | 39 ++--------- .../unreal/plugins/create/create_look.py | 64 +++++++++---------- .../plugins/create/create_staticmeshfbx.py | 34 ++-------- .../unreal/plugins/create/create_uasset.py | 44 ++++--------- 5 files changed, 65 insertions(+), 160 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index bf1489d688..239dc87db5 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -1,41 +1,13 @@ -import unreal -from unreal import EditorAssetLibrary as eal -from unreal import EditorLevelLibrary as ell - -from openpype.hosts.unreal.api.pipeline import instantiate -from openpype.pipeline import LegacyCreator +# -*- coding: utf-8 -*- +from openpype.hosts.unreal.api.plugin import ( + UnrealActorCreator, +) -class CreateCamera(LegacyCreator): - """Layout output for character rigs""" +class CreateCamera(UnrealActorCreator): + """Create Camera.""" - name = "layoutMain" + identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" - icon = "cubes" - - root = "/Game/OpenPype/Instances" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateCamera, self).__init__(*args, **kwargs) - - def process(self): - data = self.data - - name = data["subset"] - - data["level"] = ell.get_editor_world().get_path_name() - - if not eal.does_directory_exist(self.root): - eal.make_directory(self.root) - - factory = unreal.LevelSequenceFactoryNew() - tools = unreal.AssetToolsHelpers().get_asset_tools() - tools.create_asset(name, f"{self.root}/{name}", None, factory) - - asset_name = f"{self.root}/{name}/{name}.{name}" - - data["members"] = [asset_name] - - instantiate(f"{self.root}", name, data, None, self.suffix) + icon = "camera" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index c1067b00d9..1d2e800a13 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -1,42 +1,13 @@ # -*- coding: utf-8 -*- -from unreal import EditorLevelLibrary - -from openpype.pipeline import LegacyCreator -from openpype.hosts.unreal.api.pipeline import instantiate +from openpype.hosts.unreal.api.plugin import ( + UnrealActorCreator, +) -class CreateLayout(LegacyCreator): +class CreateLayout(UnrealActorCreator): """Layout output for character rigs.""" - name = "layoutMain" + identifier = "io.openpype.creators.unreal.layout" label = "Layout" family = "layout" icon = "cubes" - - root = "/Game" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateLayout, self).__init__(*args, **kwargs) - - def process(self): - data = self.data - - name = data["subset"] - - selection = [] - # if (self.options or {}).get("useSelection"): - # sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - # selection = [a.get_path_name() for a in sel_objects] - - data["level"] = EditorLevelLibrary.get_editor_world().get_path_name() - - data["members"] = [] - - if (self.options or {}).get("useSelection"): - # Set as members the selected actors - for actor in EditorLevelLibrary.get_selected_level_actors(): - data["members"].append("{}.{}".format( - actor.get_outer().get_name(), actor.get_name())) - - instantiate(self.root, name, data, selection, self.suffix) diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 4abf3f6095..08d61ab9f8 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,56 +1,53 @@ # -*- coding: utf-8 -*- -"""Create look in Unreal.""" -import unreal # noqa -from openpype.hosts.unreal.api import pipeline, plugin -from openpype.pipeline import LegacyCreator +import unreal + +from openpype.hosts.unreal.api.pipeline import ( + create_folder +) +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator +) -class CreateLook(LegacyCreator): +class CreateLook(UnrealAssetCreator): """Shader connections defining shape look.""" - name = "unrealLook" - label = "Unreal - Look" + identifier = "io.openpype.creators.unreal.look" + label = "Look" family = "look" icon = "paint-brush" - root = "/Game/Avalon/Assets" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateLook, self).__init__(*args, **kwargs) - - def process(self): - name = self.data["subset"] - + def create(self, subset_name, instance_data, pre_create_data): selection = [] - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] + if len(selection) != 1: + raise RuntimeError("Please select only one asset.") + + selected_asset = selection[0] + + look_directory = "/Game/OpenPype/Looks" + # Create the folder - path = f"{self.root}/{self.data['asset']}" - new_name = pipeline.create_folder(path, name) - full_path = f"{path}/{new_name}" + folder_name = create_folder(look_directory, subset_name) + path = f"{look_directory}/{folder_name}" # Create a new cube static mesh ar = unreal.AssetRegistryHelpers.get_asset_registry() cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") - # Create the avalon publish instance object - container_name = f"{name}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=full_path) - # Get the mesh of the selected object - original_mesh = ar.get_asset_by_object_path(selection[0]).get_asset() - materials = original_mesh.get_editor_property('materials') + original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() + materials = original_mesh.get_editor_property('static_materials') - self.data["members"] = [] + instance_data["members"] = [] # Add the materials to the cube for material in materials: - name = material.get_editor_property('material_slot_name') - object_path = f"{full_path}/{name}.{name}" + mat_name = material.get_editor_property('material_slot_name') + object_path = f"{path}/{mat_name}.{mat_name}" unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset( cube.get_asset(), object_path ) @@ -61,8 +58,11 @@ class CreateLook(LegacyCreator): unreal_object.add_material( material.get_editor_property('material_interface')) - self.data["members"].append(object_path) + instance_data["members"].append(object_path) unreal.EditorAssetLibrary.save_asset(object_path) - pipeline.imprint(f"{full_path}/{container_name}", self.data) + super(CreateLook, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 45d517d27d..1acf7084d1 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -1,35 +1,13 @@ # -*- coding: utf-8 -*- -"""Create Static Meshes as FBX geometry.""" -import unreal # noqa -from openpype.hosts.unreal.api.pipeline import ( - instantiate, +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, ) -from openpype.pipeline import LegacyCreator -class CreateStaticMeshFBX(LegacyCreator): - """Static FBX geometry.""" +class CreateStaticMeshFBX(UnrealAssetCreator): + """Create Static Meshes as FBX geometry.""" - name = "unrealStaticMeshMain" - label = "Unreal - Static Mesh" + identifier = "io.openpype.creators.unreal.staticmeshfbx" + label = "Static Mesh (FBX)" family = "unrealStaticMesh" icon = "cube" - asset_types = ["StaticMesh"] - - root = "/Game" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateStaticMeshFBX, self).__init__(*args, **kwargs) - - def process(self): - - name = self.data["subset"] - - selection = [] - if (self.options or {}).get("useSelection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - unreal.log("selection: {}".format(selection)) - instantiate(self.root, name, self.data, selection, self.suffix) diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index ee584ac00c..2d6fcc1d59 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -1,36 +1,25 @@ -"""Create UAsset.""" +# -*- coding: utf-8 -*- from pathlib import Path import unreal -from openpype.hosts.unreal.api import pipeline -from openpype.pipeline import LegacyCreator +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) -class CreateUAsset(LegacyCreator): - """UAsset.""" +class CreateUAsset(UnrealAssetCreator): + """Create UAsset.""" - name = "UAsset" + identifier = "io.openpype.creators.unreal.uasset" label = "UAsset" family = "uasset" icon = "cube" - root = "/Game/OpenPype" - suffix = "_INS" + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() - def __init__(self, *args, **kwargs): - super(CreateUAsset, self).__init__(*args, **kwargs) - - def process(self): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - subset = self.data["subset"] - path = f"{self.root}/PublishInstances/" - - unreal.EditorAssetLibrary.make_directory(path) - - selection = [] - if (self.options or {}).get("useSelection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] @@ -50,12 +39,7 @@ class CreateUAsset(LegacyCreator): if Path(sys_path).suffix != ".uasset": raise RuntimeError(f"{Path(sys_path).name} is not a UAsset.") - unreal.log("selection: {}".format(selection)) - container_name = f"{subset}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=path) - - data = self.data.copy() - data["members"] = selection - - pipeline.imprint(f"{path}/{container_name}", data) + super(CreateUAsset, self).create( + subset_name, + instance_data, + pre_create_data) From e411e197379e487a5dd5342e867bba2501ad8442 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 27 Jan 2023 16:53:39 +0000 Subject: [PATCH 011/139] Updated render creator --- .../unreal/plugins/create/create_render.py | 174 ++++++++++-------- 1 file changed, 94 insertions(+), 80 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a85d17421b..de3efdad74 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,117 +1,131 @@ +# -*- coding: utf-8 -*- import unreal -from openpype.hosts.unreal.api import pipeline -from openpype.pipeline import LegacyCreator +from openpype.hosts.unreal.api.pipeline import ( + get_subsequences +) +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) -class CreateRender(LegacyCreator): +class CreateRender(UnrealAssetCreator): """Create instance for sequence for rendering""" - name = "unrealRender" - label = "Unreal - Render" + identifier = "io.openpype.creators.unreal.render" + label = "Render" family = "render" - icon = "cube" - asset_types = ["LevelSequence"] - - root = "/Game/OpenPype/PublishInstances" - suffix = "_INS" - - def process(self): - subset = self.data["subset"] + icon = "eye" + def create(self, subset_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() - # The asset name is the the third element of the path which contains - # the map. - # The index of the split path is 3 because the first element is an - # empty string, as the path begins with "/Content". - a = unreal.EditorUtilityLibrary.get_selected_assets()[0] - asset_name = a.get_path_name().split("/")[3] - - # Get the master sequence and the master level. - # There should be only one sequence and one level in the directory. - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - sequences = ar.get_assets(filter) - ms = sequences[0].get_editor_property('object_path') - filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - levels = ar.get_assets(filter) - ml = levels[0].get_editor_property('object_path') - - selection = [] - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [ a.get_path_name() for a in sel_objects - if a.get_class().get_name() in self.asset_types] + if a.get_class().get_name() == "LevelSequence"] else: - selection.append(self.data['sequence']) + selection = [instance_data['sequence']] - unreal.log(f"selection: {selection}") + seq_data = None - path = f"{self.root}" - unreal.EditorAssetLibrary.make_directory(path) + for sel in selection: + selected_asset = ar.get_asset_by_object_path(sel).get_asset() + selected_asset_path = selected_asset.get_path_name() - ar = unreal.AssetRegistryHelpers.get_asset_registry() + # Check if the selected asset is a level sequence asset. + if selected_asset.get_class().get_name() != "LevelSequence": + unreal.log_warning( + f"Skipping {selected_asset.get_name()}. It isn't a Level " + "Sequence.") - for a in selection: - ms_obj = ar.get_asset_by_object_path(ms).get_asset() + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] - seq_data = None + # Get the master sequence and the master level. + # There should be only one sequence and one level in the directory. + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() - if a == ms: - seq_data = { - "sequence": ms_obj, - "output": f"{ms_obj.get_name()}", - "frame_range": ( - ms_obj.get_playback_start(), ms_obj.get_playback_end()) - } + # If the selected asset is the master sequence, we get its data + # and then we create the instance for the master sequence. + # Otherwise, we cycle from the master sequence to find the selected + # sequence and we get its data. This data will be used to create + # the instance for the selected sequence. In particular, + # we get the frame range of the selected sequence and its final + # output path. + master_seq_data = { + "sequence": master_seq_obj, + "output": f"{master_seq_obj.get_name()}", + "frame_range": ( + master_seq_obj.get_playback_start(), + master_seq_obj.get_playback_end())} + + if selected_asset_path == master_seq: + seq_data = master_seq_data else: - seq_data_list = [{ - "sequence": ms_obj, - "output": f"{ms_obj.get_name()}", - "frame_range": ( - ms_obj.get_playback_start(), ms_obj.get_playback_end()) - }] + seq_data_list = [master_seq_data] - for s in seq_data_list: - subscenes = pipeline.get_subsequences(s.get('sequence')) + for seq in seq_data_list: + subscenes = get_subsequences(seq.get('sequence')) - for ss in subscenes: + for sub_seq in subscenes: + sub_seq_obj = sub_seq.get_sequence() curr_data = { - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), + "sequence": sub_seq_obj, + "output": (f"{seq.get('output')}/" + f"{sub_seq_obj.get_name()}"), "frame_range": ( - ss.get_start_frame(), ss.get_end_frame() - 1) - } + sub_seq.get_start_frame(), + sub_seq.get_end_frame() - 1)} - if ss.get_sequence().get_path_name() == a: + # If the selected asset is the current sub-sequence, + # we get its data and we break the loop. + # Otherwise, we add the current sub-sequence data to + # the list of sequences to check. + if sub_seq_obj.get_path_name() == selected_asset_path: seq_data = curr_data break + seq_data_list.append(curr_data) + # If we found the selected asset, we break the loop. if seq_data is not None: break + # If we didn't find the selected asset, we don't create the + # instance. if not seq_data: + unreal.log_warning( + f"Skipping {selected_asset.get_name()}. It isn't a " + "sub-sequence of the master sequence.") continue - d = self.data.copy() - d["members"] = [a] - d["sequence"] = a - d["master_sequence"] = ms - d["master_level"] = ml - d["output"] = seq_data.get('output') - d["frameStart"] = seq_data.get('frame_range')[0] - d["frameEnd"] = seq_data.get('frame_range')[1] + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] - container_name = f"{subset}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=path) - pipeline.imprint(f"{path}/{container_name}", d) + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) From 575eb50c03e02227e2c9dedf8fc7c2a32f558c85 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 30 Jan 2023 11:17:21 +0000 Subject: [PATCH 012/139] Hound fixes --- openpype/hosts/unreal/api/plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 6a561420fa..71ce0c18a7 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -104,7 +104,7 @@ class UnrealBaseCreator(Creator): # cache instances if missing self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ - "unreal_cached_subsets"].get(self.identifier, []): + "unreal_cached_subsets"].get(self.identifier, []): created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) @@ -162,7 +162,8 @@ class UnrealAssetCreator(UnrealBaseCreator): selection = [] if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + utility_lib = unreal.EditorUtilityLibrary + sel_objects = utility_lib.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] instance_data["members"] = selection @@ -211,7 +212,8 @@ class UnrealActorCreator(UnrealBaseCreator): selection = [] if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + utility_lib = unreal.EditorUtilityLibrary + sel_objects = utility_lib.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] instance_data["members"] = selection From af2737a99f608ef6598d54ae8d098a3509a6223b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Jan 2023 16:05:01 +0000 Subject: [PATCH 013/139] Collect instances is no longer needed with the new publisher --- .../plugins/publish/collect_instances.py | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/publish/collect_instances.py diff --git a/openpype/hosts/unreal/plugins/publish/collect_instances.py b/openpype/hosts/unreal/plugins/publish/collect_instances.py deleted file mode 100644 index 27b711cad6..0000000000 --- a/openpype/hosts/unreal/plugins/publish/collect_instances.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect publishable instances in Unreal.""" -import ast -import unreal # noqa -import pyblish.api -from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION -from openpype.pipeline.publish import KnownPublishError - - -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by OpenPypePublishInstance class - - This collector finds all paths containing `OpenPypePublishInstance` class - asset - - Identifier: - id (str): "pyblish.avalon.instance" - - """ - - label = "Collect Instances" - order = pyblish.api.CollectorOrder - 0.1 - hosts = ["unreal"] - - def process(self, context): - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - class_name = [ - "/Script/OpenPype", - "OpenPypePublishInstance" - ] if ( - UNREAL_VERSION.major == 5 - and UNREAL_VERSION.minor > 0 - ) else "OpenPypePublishInstance" # noqa - instance_containers = ar.get_assets_by_class(class_name, True) - - for container_data in instance_containers: - asset = container_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = container_data.asset_name - # convert to strings - data = {str(key): str(value) for (key, value) in data.items()} - if not data.get("family"): - raise KnownPublishError("instance has no family") - - # content of container - members = ast.literal_eval(data.get("members")) - self.log.debug(members) - self.log.debug(asset.get_path_name()) - # remove instance container - self.log.info("Creating instance for {}".format(asset.get_name())) - - instance = context.create_instance(asset.get_name()) - instance[:] = members - - # Store the exact members of the object set - instance.data["setMembers"] = members - instance.data["families"] = [data.get("family")] - instance.data["level"] = data.get("level") - instance.data["parent"] = data.get("parent") - - label = "{0} ({1})".format(asset.get_name()[:-4], - data["asset"]) - - instance.data["label"] = label - - instance.data.update(data) From c93fc9aad0743d4252d6bb58c33ac21365b7eac7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 11:22:36 +0000 Subject: [PATCH 014/139] Use External Data in the Unreal Publish Instance to store members Not possible with all the families. Some families require to store actors in a scenes, and we cannot store them in the External Data. --- openpype/hosts/unreal/api/plugin.py | 24 ++++++--- .../unreal/plugins/create/create_look.py | 6 ++- .../publish/collect_instance_members.py | 49 +++++++++++++++++++ .../unreal/plugins/publish/extract_look.py | 4 +- .../unreal/plugins/publish/extract_uasset.py | 8 ++- 5 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/publish/collect_instance_members.py diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 71ce0c18a7..da571af9be 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -80,7 +80,7 @@ class UnrealBaseCreator(Creator): def create(self, subset_name, instance_data, pre_create_data): try: instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) + pub_instance = create_publish_instance(instance_name, self.root) instance_data["subset"] = subset_name instance_data["instance_path"] = f"{self.root}/{instance_name}" @@ -92,6 +92,15 @@ class UnrealBaseCreator(Creator): self) self._add_instance_to_context(instance) + pub_instance.set_editor_property('add_external_assets', True) + assets = pub_instance.get_editor_property('asset_data_external') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for member in pre_create_data.get("members", []): + obj = ar.get_asset_by_object_path(member).get_asset() + assets.add(obj) + imprint(f"{self.root}/{instance_name}", instance_data) except Exception as er: @@ -158,15 +167,14 @@ class UnrealAssetCreator(UnrealBaseCreator): try: # Check if instance data has members, filled by the plugin. # If not, use selection. - if not instance_data.get("members"): - selection = [] + if not pre_create_data.get("members"): + pre_create_data["members"] = [] if pre_create_data.get("use_selection"): - utility_lib = unreal.EditorUtilityLibrary - sel_objects = utility_lib.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - instance_data["members"] = selection + utilib = unreal.EditorUtilityLibrary + sel_objects = utilib.get_selected_assets() + pre_create_data["members"] = [ + a.get_path_name() for a in sel_objects] super(UnrealAssetCreator, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 08d61ab9f8..047764ef2a 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -34,6 +34,8 @@ class CreateLook(UnrealAssetCreator): folder_name = create_folder(look_directory, subset_name) path = f"{look_directory}/{folder_name}" + instance_data["look"] = path + # Create a new cube static mesh ar = unreal.AssetRegistryHelpers.get_asset_registry() cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") @@ -42,7 +44,7 @@ class CreateLook(UnrealAssetCreator): original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() materials = original_mesh.get_editor_property('static_materials') - instance_data["members"] = [] + pre_create_data["members"] = [] # Add the materials to the cube for material in materials: @@ -58,7 +60,7 @@ class CreateLook(UnrealAssetCreator): unreal_object.add_material( material.get_editor_property('material_interface')) - instance_data["members"].append(object_path) + pre_create_data["members"].append(object_path) unreal.EditorAssetLibrary.save_asset(object_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py new file mode 100644 index 0000000000..74969f5033 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -0,0 +1,49 @@ +import unreal + +import pyblish.api + + +class CollectInstanceMembers(pyblish.api.InstancePlugin): + """ + Collect members of instance. + + This collector will collect the assets for the families that support to + have them included as External Data, and will add them to the instance + as members. + """ + + order = pyblish.api.CollectorOrder + 0.1 + hosts = ["unreal"] + families = ["look", "unrealStaticMesh", "uasset"] + label = "Collect Instance Members" + + def process(self, instance): + """Collect members of instance.""" + self.log.info("Collecting instance members") + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + inst_path = instance.data.get('instance_path') + inst_name = instance.data.get('objectName') + + pub_instance = ar.get_asset_by_object_path( + f"{inst_path}.{inst_name}").get_asset() + + if not pub_instance: + self.log.error(f"{inst_path}.{inst_name}") + raise RuntimeError(f"Instance {instance} not found.") + + if not pub_instance.get_editor_property("add_external_assets"): + # No external assets in the instance + return + + assets = pub_instance.get_editor_property('asset_data_external') + + members = [] + + for asset in assets: + members.append(asset.get_path_name()) + + self.log.debug(f"Members: {members}") + + instance.data["members"] = members diff --git a/openpype/hosts/unreal/plugins/publish/extract_look.py b/openpype/hosts/unreal/plugins/publish/extract_look.py index f999ad8651..4b32b4eb95 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_look.py +++ b/openpype/hosts/unreal/plugins/publish/extract_look.py @@ -29,13 +29,13 @@ class ExtractLook(publish.Extractor): for member in instance: asset = ar.get_asset_by_object_path(member) - object = asset.get_asset() + obj = asset.get_asset() name = asset.get_editor_property('asset_name') json_element = {'material': str(name)} - material_obj = object.get_editor_property('static_materials')[0] + material_obj = obj.get_editor_property('static_materials')[0] material = material_obj.material_interface base_color = mat_lib.get_material_property_input_node( diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index 89d779d368..f719df2a82 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -22,7 +22,13 @@ class ExtractUAsset(publish.Extractor): staging_dir = self.staging_dir(instance) filename = "{}.uasset".format(instance.name) - obj = instance[0] + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] asset = ar.get_asset_by_object_path(obj).get_asset() sys_path = unreal.SystemLibrary.get_system_path(asset) From 20227c686d5339968b3f1e3c4fc8119b0dd8a8df Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 12:18:03 +0000 Subject: [PATCH 015/139] Improved attributes for the creators --- openpype/hosts/unreal/api/plugin.py | 20 +++++++++++++------ .../unreal/plugins/create/create_render.py | 6 ++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index da571af9be..7121aea20b 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -14,7 +14,10 @@ from .pipeline import ( lsinst, UNREAL_VERSION ) -from openpype.lib import BoolDef +from openpype.lib import ( + BoolDef, + UILabelDef +) from openpype.pipeline import ( Creator, LoaderPlugin, @@ -143,11 +146,6 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) - def get_pre_create_attr_defs(self): - return [ - BoolDef("use_selection", label="Use selection", default=True) - ] - @six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): @@ -187,6 +185,11 @@ class UnrealAssetCreator(UnrealBaseCreator): OpenPypeCreatorError(f"Creator error: {er}"), sys.exc_info()[2]) + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection", default=True) + ] + @six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): @@ -239,6 +242,11 @@ class UnrealActorCreator(UnrealBaseCreator): OpenPypeCreatorError(f"Creator error: {er}"), sys.exc_info()[2]) + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select actors to create instance from them.") + ] + class Loader(LoaderPlugin, ABC): """This serves as skeleton for future OpenPype specific functionality""" diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index de3efdad74..8100a5016c 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -7,6 +7,7 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) +from openpype.lib import UILabelDef class CreateRender(UnrealAssetCreator): @@ -129,3 +130,8 @@ class CreateRender(UnrealAssetCreator): subset_name, instance_data, pre_create_data) + + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select the sequence to render.") + ] From 65e08973fe423c5f456a5a9654fc59d711e06adb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 16:15:03 +0000 Subject: [PATCH 016/139] Fix render creator problem with selection --- .../hosts/unreal/plugins/create/create_render.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 8100a5016c..a1e3e43a78 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -6,6 +6,7 @@ from openpype.hosts.unreal.api.pipeline import ( ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, + OpenPypeCreatorError ) from openpype.lib import UILabelDef @@ -21,13 +22,13 @@ class CreateRender(UnrealAssetCreator): def create(self, subset_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [ - a.get_path_name() for a in sel_objects - if a.get_class().get_name() == "LevelSequence"] - else: - selection = [instance_data['sequence']] + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [ + a.get_path_name() for a in sel_objects + if a.get_class().get_name() == "LevelSequence"] + + if len(selection) == 0: + raise RuntimeError("Please select at least one Level Sequence.") seq_data = None From 106f9ca2bb750ebed02016264e3f46b199aa494f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 16:17:23 +0000 Subject: [PATCH 017/139] Hound fixes --- openpype/hosts/unreal/plugins/create/create_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a1e3e43a78..c957e50e29 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -5,8 +5,7 @@ from openpype.hosts.unreal.api.pipeline import ( get_subsequences ) from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, - OpenPypeCreatorError + UnrealAssetCreator ) from openpype.lib import UILabelDef From 8e30e565fdefb3a567567cd9651182eb0da2f68d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 11:41:57 +0000 Subject: [PATCH 018/139] Implemented suggestions from review --- openpype/hosts/unreal/api/pipeline.py | 3 - openpype/hosts/unreal/api/plugin.py | 69 +++++++------------ .../unreal/plugins/create/create_camera.py | 2 +- .../unreal/plugins/create/create_look.py | 14 ++-- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 7a21effcbc..0fe8c02ec5 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -69,9 +69,6 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): op_ctx = content_path + CONTEXT_CONTAINER with open(op_ctx, "w+") as f: json.dump(data, f) - with open(op_ctx, "r") as fp: - test = eval(json.load(fp)) - unreal.log_warning(test) def get_context_data(self): content_path = unreal.Paths.project_content_dir() diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 7121aea20b..fc724105b6 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- +import collections import sys import six -from abc import ( - ABC, - ABCMeta, -) +from abc import ABC import unreal @@ -26,11 +24,6 @@ from openpype.pipeline import ( ) -class OpenPypeCreatorError(CreatorError): - pass - - -@six.add_metaclass(ABCMeta) class UnrealBaseCreator(Creator): """Base class for Unreal creator plugins.""" root = "/Game/OpenPype/PublishInstances" @@ -56,28 +49,20 @@ class UnrealBaseCreator(Creator): """ if shared_data.get("unreal_cached_subsets") is None: - shared_data["unreal_cached_subsets"] = {} - if shared_data.get("unreal_cached_legacy_subsets") is None: - shared_data["unreal_cached_legacy_subsets"] = {} - cached_instances = lsinst() - for i in cached_instances: - if not i.get("creator_identifier"): - # we have legacy instance - family = i.get("family") - if (family not in - shared_data["unreal_cached_legacy_subsets"]): - shared_data[ - "unreal_cached_legacy_subsets"][family] = [i] - else: - shared_data[ - "unreal_cached_legacy_subsets"][family].append(i) - continue - - creator_id = i.get("creator_identifier") - if creator_id not in shared_data["unreal_cached_subsets"]: - shared_data["unreal_cached_subsets"][creator_id] = [i] + unreal_cached_subsets = collections.defaultdict(list) + unreal_cached_legacy_subsets = collections.defaultdict(list) + for instance in lsinst(): + creator_id = instance.get("creator_identifier") + if creator_id: + unreal_cached_subsets[creator_id].append(instance) else: - shared_data["unreal_cached_subsets"][creator_id].append(i) + family = instance.get("family") + unreal_cached_legacy_subsets[family].append(instance) + + shared_data["unreal_cached_subsets"] = unreal_cached_subsets + shared_data["unreal_cached_legacy_subsets"] = ( + unreal_cached_legacy_subsets + ) return shared_data def create(self, subset_name, instance_data, pre_create_data): @@ -108,8 +93,8 @@ class UnrealBaseCreator(Creator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def collect_instances(self): @@ -121,17 +106,17 @@ class UnrealBaseCreator(Creator): self._add_instance_to_context(created_instance) def update_instances(self, update_list): - unreal.log_warning(f"Update instances: {update_list}") - for created_inst, _changes in update_list: + for created_inst, changes in update_list: instance_node = created_inst.get("instance_path", "") if not instance_node: unreal.log_warning( f"Instance node not found for {created_inst}") + continue new_values = { - key: new_value - for key, (_old_value, new_value) in _changes.items() + key: changes[key].new_value + for key in changes.changed_keys } imprint( instance_node, @@ -147,7 +132,6 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) -@six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on assets.""" @@ -181,8 +165,8 @@ class UnrealAssetCreator(UnrealBaseCreator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def get_pre_create_attr_defs(self): @@ -191,7 +175,6 @@ class UnrealAssetCreator(UnrealBaseCreator): ] -@six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on actors.""" @@ -214,7 +197,7 @@ class UnrealActorCreator(UnrealBaseCreator): # Check if the level is saved if world.get_path_name().startswith("/Temp/"): - raise OpenPypeCreatorError( + raise CreatorError( "Level must be saved before creating instances.") # Check if instance data has members, filled by the plugin. @@ -238,8 +221,8 @@ class UnrealActorCreator(UnrealBaseCreator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def get_pre_create_attr_defs(self): diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 239dc87db5..00815e1ed4 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -10,4 +10,4 @@ class CreateCamera(UnrealActorCreator): identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" - icon = "camera" + icon = "fa.camera" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 047764ef2a..cecb88bca3 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -7,6 +7,7 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) +from openpype.lib import UILabelDef class CreateLook(UnrealAssetCreator): @@ -18,10 +19,10 @@ class CreateLook(UnrealAssetCreator): icon = "paint-brush" def create(self, subset_name, instance_data, pre_create_data): - selection = [] - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + # We need to set this to True for the parent class to work + pre_create_data["use_selection"] = True + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] if len(selection) != 1: raise RuntimeError("Please select only one asset.") @@ -68,3 +69,8 @@ class CreateLook(UnrealAssetCreator): subset_name, instance_data, pre_create_data) + + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select the asset from which to create the look.") + ] From fa3a7419409598ba0b3b2c9cb42d1c42be20822b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 11:45:30 +0000 Subject: [PATCH 019/139] Fixed problem with the instance metadata --- openpype/hosts/unreal/api/pipeline.py | 2 +- openpype/hosts/unreal/api/plugin.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0fe8c02ec5..0810ec7c07 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -76,7 +76,7 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): if not os.path.isfile(op_ctx): return {} with open(op_ctx, "r") as fp: - data = eval(json.load(fp)) + data = json.load(fp) return data diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index fc724105b6..a852ed9bb1 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import ast import collections import sys import six @@ -89,7 +90,9 @@ class UnrealBaseCreator(Creator): obj = ar.get_asset_by_object_path(member).get_asset() assets.add(obj) - imprint(f"{self.root}/{instance_name}", instance_data) + imprint(f"{self.root}/{instance_name}", instance.data_to_store()) + + return instance except Exception as er: six.reraise( @@ -102,6 +105,11 @@ class UnrealBaseCreator(Creator): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ "unreal_cached_subsets"].get(self.identifier, []): + # Unreal saves metadata as string, so we need to convert it back + instance['creator_attributes'] = ast.literal_eval( + instance.get('creator_attributes', '{}')) + instance['publish_attributes'] = ast.literal_eval( + instance.get('publish_attributes', '{}')) created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) From 614bcb320c3a6bde5e717000065b5c17088ccdc6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 16:56:21 +0000 Subject: [PATCH 020/139] Creator allows to create a new level sequence with render instance --- .../unreal/plugins/create/create_render.py | 126 +++++++++++++++--- 1 file changed, 111 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index c957e50e29..bc39b43802 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -2,12 +2,17 @@ import unreal from openpype.hosts.unreal.api.pipeline import ( - get_subsequences + UNREAL_VERSION, + create_folder, + get_subsequences, ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) -from openpype.lib import UILabelDef +from openpype.lib import ( + BoolDef, + UILabelDef +) class CreateRender(UnrealAssetCreator): @@ -18,7 +23,88 @@ class CreateRender(UnrealAssetCreator): family = "render" icon = "eye" - def create(self, subset_name, instance_data, pre_create_data): + def create_instance( + self, instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data + ): + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] + + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) + + def create_with_new_sequence( + self, subset_name, instance_data, pre_create_data + ): + # If the option to create a new level sequence is selected, + # create a new level sequence and a master level. + + root = f"/Game/OpenPype/Sequences" + + # Create a new folder for the sequence in root + sequence_dir_name = create_folder(root, subset_name) + sequence_dir = f"{root}/{sequence_dir_name}" + + unreal.log_warning(f"sequence_dir: {sequence_dir}") + + # Create the level sequence + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + seq = asset_tools.create_asset( + asset_name=subset_name, + package_path=sequence_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew()) + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) + + # Create the master level + prev_level = None + if UNREAL_VERSION.major >= 5: + curr_level = unreal.LevelEditorSubsystem().get_current_level() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + levels = unreal.EditorLevelUtils.get_levels(world) + curr_level = levels[0] if len(levels) else None + if not curr_level: + raise RuntimeError("No level loaded.") + curr_level_path = curr_level.get_outer().get_path_name() + + # If the level path does not start with "/Game/", the current + # level is a temporary, unsaved level. + if curr_level_path.startswith("/Game/"): + prev_level = curr_level_path + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().save_current_level() + else: + unreal.EditorLevelLibrary.save_current_level() + + ml_path = f"{sequence_dir}/{subset_name}_MasterLevel" + + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().new_level(ml_path) + else: + unreal.EditorLevelLibrary.new_level(ml_path) + + seq_data = { + "sequence": seq, + "output": f"{seq.get_name()}", + "frame_range": ( + seq.get_playback_start(), + seq.get_playback_end())} + + self.create_instance( + instance_data, subset_name, pre_create_data, + seq.get_path_name(), seq.get_path_name(), ml_path, seq_data) + + def create_from_existing_sequence( + self, subset_name, instance_data, pre_create_data + ): ar = unreal.AssetRegistryHelpers.get_asset_registry() sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() @@ -118,20 +204,30 @@ class CreateRender(UnrealAssetCreator): "sub-sequence of the master sequence.") continue - instance_data["members"] = [selected_asset_path] - instance_data["sequence"] = selected_asset_path - instance_data["master_sequence"] = master_seq - instance_data["master_level"] = master_lvl - instance_data["output"] = seq_data.get('output') - instance_data["frameStart"] = seq_data.get('frame_range')[0] - instance_data["frameEnd"] = seq_data.get('frame_range')[1] + self.create_instance( + instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data) - super(CreateRender, self).create( - subset_name, - instance_data, - pre_create_data) + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("create_seq"): + self.create_with_new_sequence( + subset_name, instance_data, pre_create_data) + else: + self.create_from_existing_sequence( + subset_name, instance_data, pre_create_data) def get_pre_create_attr_defs(self): return [ - UILabelDef("Select the sequence to render.") + UILabelDef( + "Select a Level Sequence to render or create a new one." + ), + BoolDef( + "create_seq", + label="Create a new Level Sequence", + default=False + ), + UILabelDef( + "WARNING: If you create a new Level Sequence, the current " + "level will be saved and a new Master Level will be created." + ) ] From f94cae429e0cb7b056153211adec7fa7813b28f8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Feb 2023 11:46:10 +0000 Subject: [PATCH 021/139] Allow the user to set frame range of new sequence --- .../unreal/plugins/create/create_render.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index bc39b43802..b999f9ae20 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -11,7 +11,7 @@ from openpype.hosts.unreal.api.plugin import ( ) from openpype.lib import ( BoolDef, - UILabelDef + NumberDef ) @@ -61,6 +61,10 @@ class CreateRender(UnrealAssetCreator): package_path=sequence_dir, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew()) + + seq.set_playback_start(pre_create_data.get("start_frame")) + seq.set_playback_end(pre_create_data.get("end_frame")) + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) # Create the master level @@ -229,5 +233,19 @@ class CreateRender(UnrealAssetCreator): UILabelDef( "WARNING: If you create a new Level Sequence, the current " "level will be saved and a new Master Level will be created." - ) + ), + NumberDef( + "start_frame", + label="Start Frame", + default=0, + minimum=-999999, + maximum=999999 + ), + NumberDef( + "end_frame", + label="Start Frame", + default=150, + minimum=-999999, + maximum=999999 + ), ] From e2ea7fad1a7f4d0aaec178dd18e76ffa18e3f3af Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Feb 2023 11:47:26 +0000 Subject: [PATCH 022/139] Added option to not include hierarchy when creating a render instance --- .../unreal/plugins/create/create_render.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index b999f9ae20..6f2049693f 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from pathlib import Path + import unreal from openpype.hosts.unreal.api.pipeline import ( @@ -10,6 +12,8 @@ from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) from openpype.lib import ( + UILabelDef, + UISeparatorDef, BoolDef, NumberDef ) @@ -68,7 +72,6 @@ class CreateRender(UnrealAssetCreator): unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) # Create the master level - prev_level = None if UNREAL_VERSION.major >= 5: curr_level = unreal.LevelEditorSubsystem().get_current_level() else: @@ -82,7 +85,6 @@ class CreateRender(UnrealAssetCreator): # If the level path does not start with "/Game/", the current # level is a temporary, unsaved level. if curr_level_path.startswith("/Game/"): - prev_level = curr_level_path if UNREAL_VERSION.major >= 5: unreal.LevelEditorSubsystem().save_current_level() else: @@ -131,25 +133,31 @@ class CreateRender(UnrealAssetCreator): f"Skipping {selected_asset.get_name()}. It isn't a Level " "Sequence.") - # The asset name is the the third element of the path which - # contains the map. - # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". - sel_path = selected_asset_path - asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] + if pre_create_data.get("use_hierarchy"): + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace( + "/Game/OpenPype/", "").split("/")[0] + + search_path = f"/Game/OpenPype/{asset_name}" + else: + search_path = Path(selected_asset_path).parent.as_posix() # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. ar_filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[search_path], recursive_paths=False) sequences = ar.get_assets(ar_filter) master_seq = sequences[0].get_asset().get_path_name() master_seq_obj = sequences[0].get_asset() ar_filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[search_path], recursive_paths=False) levels = ar.get_assets(ar_filter) master_lvl = levels[0].get_asset().get_path_name() @@ -168,7 +176,8 @@ class CreateRender(UnrealAssetCreator): master_seq_obj.get_playback_start(), master_seq_obj.get_playback_end())} - if selected_asset_path == master_seq: + if (selected_asset_path == master_seq or + pre_create_data.get("use_hierarchy")): seq_data = master_seq_data else: seq_data_list = [master_seq_data] @@ -231,7 +240,7 @@ class CreateRender(UnrealAssetCreator): default=False ), UILabelDef( - "WARNING: If you create a new Level Sequence, the current " + "WARNING: If you create a new Level Sequence, the current\n" "level will be saved and a new Master Level will be created." ), NumberDef( @@ -248,4 +257,14 @@ class CreateRender(UnrealAssetCreator): minimum=-999999, maximum=999999 ), + UISeparatorDef(), + UILabelDef( + "The following settings are valid only if you are not\n" + "creating a new sequence." + ), + BoolDef( + "use_hierarchy", + label="Use Hierarchy", + default=False + ), ] From d2403bcbdace79d8e645b6dbd68e439dbb144e03 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Feb 2023 12:00:48 +0000 Subject: [PATCH 023/139] Hanldes IndexError when looking for hierarchy for selected sequence --- .../unreal/plugins/create/create_render.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 6f2049693f..b2a246d3a8 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -148,19 +148,23 @@ class CreateRender(UnrealAssetCreator): # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[search_path], - recursive_paths=False) - sequences = ar.get_assets(ar_filter) - master_seq = sequences[0].get_asset().get_path_name() - master_seq_obj = sequences[0].get_asset() - ar_filter = unreal.ARFilter( - class_names=["World"], - package_paths=[search_path], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() + try: + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[search_path], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[search_path], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() + except IndexError: + raise RuntimeError( + f"Could not find the hierarchy for the selected sequence.") # If the selected asset is the master sequence, we get its data # and then we create the instance for the master sequence. From 6064fa2d45ca59269cf101b6f19edcf557996f24 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Mar 2023 11:09:49 +0000 Subject: [PATCH 024/139] Added settings for rendering --- .../settings/defaults/project_settings/unreal.json | 2 ++ .../schemas/projects_schema/schema_project_unreal.json | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 75cee11bd9..ff290ef254 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -11,6 +11,8 @@ }, "level_sequences_for_layouts": false, "delete_unmatched_assets": false, + "render_config_path": "", + "preroll_frames": 0, "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 8988dd2ff0..40bbb40ccc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -32,6 +32,16 @@ "key": "delete_unmatched_assets", "label": "Delete assets that are not matched" }, + { + "type": "text", + "key": "render_config_path", + "label": "Render Config Path" + }, + { + "type": "number", + "key": "preroll_frames", + "label": "Pre-roll frames" + }, { "type": "dict", "collapsible": true, From 095c792ad229d23e0b0d2b5f4fa44eb0ae229862 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Mar 2023 11:10:42 +0000 Subject: [PATCH 025/139] Uses settings for rendering --- openpype/hosts/unreal/api/rendering.py | 54 +++++++++++++++++--------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 29e4747f6e..5ef4792000 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -2,6 +2,7 @@ import os import unreal +from openpype.settings import get_project_settings from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline @@ -66,6 +67,13 @@ def start_rendering(): ar = unreal.AssetRegistryHelpers.get_asset_registry() + data = get_project_settings(project) + config = None + config_path = str(data.get("unreal").get("render_config_path")) + if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path): + unreal.log("Found saved render configuration") + config = ar.get_asset_by_object_path(config_path).get_asset() + for i in inst_data: sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() @@ -81,47 +89,50 @@ def start_rendering(): # Get all the sequences to render. If there are subsequences, # add them and their frame ranges to the render list. We also # use the names for the output paths. - for s in sequences: - subscenes = pipeline.get_subsequences(s.get('sequence')) + for seq in sequences: + subscenes = pipeline.get_subsequences(seq.get('sequence')) if subscenes: - for ss in subscenes: + for sub_seq in subscenes: sequences.append({ - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), + "sequence": sub_seq.get_sequence(), + "output": (f"{seq.get('output')}/" + f"{sub_seq.get_sequence().get_name()}"), "frame_range": ( - ss.get_start_frame(), ss.get_end_frame()) + sub_seq.get_start_frame(), sub_seq.get_end_frame()) }) else: # Avoid rendering camera sequences - if "_camera" not in s.get('sequence').get_name(): - render_list.append(s) + if "_camera" not in seq.get('sequence').get_name(): + render_list.append(seq) # Create the rendering jobs and add them to the queue. - for r in render_list: + for render_setting in render_list: job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) job.sequence = unreal.SoftObjectPath(i["master_sequence"]) job.map = unreal.SoftObjectPath(i["master_level"]) job.author = "OpenPype" + # If we have a saved configuration, copy it to the job. + if config: + job.get_configuration().copy_from(config) + # User data could be used to pass data to the job, that can be # read in the job's OnJobFinished callback. We could, # for instance, pass the AvalonPublishInstance's path to the job. # job.user_data = "" + output_dir = render_setting.get('output') + shot_name = render_setting.get('sequence').get_name() + settings = job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineOutputSetting) settings.output_resolution = unreal.IntPoint(1920, 1080) - settings.custom_start_frame = r.get("frame_range")[0] - settings.custom_end_frame = r.get("frame_range")[1] + settings.custom_start_frame = render_setting.get("frame_range")[0] + settings.custom_end_frame = render_setting.get("frame_range")[1] settings.use_custom_playback_range = True - settings.file_name_format = "{sequence_name}.{frame_number}" - settings.output_directory.path = f"{render_dir}/{r.get('output')}" - - renderPass = job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineDeferredPassBase) - renderPass.disable_multisample_effects = True + settings.file_name_format = f"{shot_name}" + ".{frame_number}" + settings.output_directory.path = f"{render_dir}/{output_dir}" job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineImageSequenceOutput_PNG) @@ -130,6 +141,13 @@ def start_rendering(): if queue.get_jobs(): global executor executor = unreal.MoviePipelinePIEExecutor() + + preroll_frames = data.get("unreal").get("preroll_frames", 0) + + settings = unreal.MoviePipelinePIEExecutorSettings() + settings.set_editor_property( + "initial_delay_frame_count", preroll_frames) + executor.on_executor_finished_delegate.add_callable_unique( _queue_finish_callback) executor.on_individual_job_finished_delegate.add_callable_unique( From 839d5834ca611c20f042c3036bcf422ce5ee32ce Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 8 Mar 2023 11:12:24 +0100 Subject: [PATCH 026/139] Fix merge problem --- openpype/hosts/unreal/plugins/create/create_camera.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 33a0662d7d..642924e2d6 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -7,8 +7,6 @@ from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) -class CreateCamera(UnrealActorCreator): - """Create Camera.""" class CreateCamera(UnrealAssetCreator): """Create Camera.""" From 700927c1645fc9183a739abfd4529f4a94e027d2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Mar 2023 15:21:35 +0000 Subject: [PATCH 027/139] Restored lost changes --- .../unreal/plugins/create/create_render.py | 212 ++++++++++++++---- 1 file changed, 174 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 5834d2e7a7..b2a246d3a8 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,14 +1,22 @@ # -*- coding: utf-8 -*- +from pathlib import Path + import unreal -from openpype.pipeline import CreatorError from openpype.hosts.unreal.api.pipeline import ( - get_subsequences + UNREAL_VERSION, + create_folder, + get_subsequences, ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) -from openpype.lib import UILabelDef +from openpype.lib import ( + UILabelDef, + UISeparatorDef, + BoolDef, + NumberDef +) class CreateRender(UnrealAssetCreator): @@ -19,7 +27,90 @@ class CreateRender(UnrealAssetCreator): family = "render" icon = "eye" - def create(self, subset_name, instance_data, pre_create_data): + def create_instance( + self, instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data + ): + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] + + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) + + def create_with_new_sequence( + self, subset_name, instance_data, pre_create_data + ): + # If the option to create a new level sequence is selected, + # create a new level sequence and a master level. + + root = f"/Game/OpenPype/Sequences" + + # Create a new folder for the sequence in root + sequence_dir_name = create_folder(root, subset_name) + sequence_dir = f"{root}/{sequence_dir_name}" + + unreal.log_warning(f"sequence_dir: {sequence_dir}") + + # Create the level sequence + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + seq = asset_tools.create_asset( + asset_name=subset_name, + package_path=sequence_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew()) + + seq.set_playback_start(pre_create_data.get("start_frame")) + seq.set_playback_end(pre_create_data.get("end_frame")) + + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) + + # Create the master level + if UNREAL_VERSION.major >= 5: + curr_level = unreal.LevelEditorSubsystem().get_current_level() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + levels = unreal.EditorLevelUtils.get_levels(world) + curr_level = levels[0] if len(levels) else None + if not curr_level: + raise RuntimeError("No level loaded.") + curr_level_path = curr_level.get_outer().get_path_name() + + # If the level path does not start with "/Game/", the current + # level is a temporary, unsaved level. + if curr_level_path.startswith("/Game/"): + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().save_current_level() + else: + unreal.EditorLevelLibrary.save_current_level() + + ml_path = f"{sequence_dir}/{subset_name}_MasterLevel" + + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().new_level(ml_path) + else: + unreal.EditorLevelLibrary.new_level(ml_path) + + seq_data = { + "sequence": seq, + "output": f"{seq.get_name()}", + "frame_range": ( + seq.get_playback_start(), + seq.get_playback_end())} + + self.create_instance( + instance_data, subset_name, pre_create_data, + seq.get_path_name(), seq.get_path_name(), ml_path, seq_data) + + def create_from_existing_sequence( + self, subset_name, instance_data, pre_create_data + ): ar = unreal.AssetRegistryHelpers.get_asset_registry() sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() @@ -27,8 +118,8 @@ class CreateRender(UnrealAssetCreator): a.get_path_name() for a in sel_objects if a.get_class().get_name() == "LevelSequence"] - if not selection: - raise CreatorError("Please select at least one Level Sequence.") + if len(selection) == 0: + raise RuntimeError("Please select at least one Level Sequence.") seq_data = None @@ -42,28 +133,38 @@ class CreateRender(UnrealAssetCreator): f"Skipping {selected_asset.get_name()}. It isn't a Level " "Sequence.") - # The asset name is the third element of the path which - # contains the map. - # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". - sel_path = selected_asset_path - asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] + if pre_create_data.get("use_hierarchy"): + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace( + "/Game/OpenPype/", "").split("/")[0] + + search_path = f"/Game/OpenPype/{asset_name}" + else: + search_path = Path(selected_asset_path).parent.as_posix() # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - sequences = ar.get_assets(ar_filter) - master_seq = sequences[0].get_asset().get_path_name() - master_seq_obj = sequences[0].get_asset() - ar_filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() + try: + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[search_path], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[search_path], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() + except IndexError: + raise RuntimeError( + f"Could not find the hierarchy for the selected sequence.") # If the selected asset is the master sequence, we get its data # and then we create the instance for the master sequence. @@ -79,7 +180,8 @@ class CreateRender(UnrealAssetCreator): master_seq_obj.get_playback_start(), master_seq_obj.get_playback_end())} - if selected_asset_path == master_seq: + if (selected_asset_path == master_seq or + pre_create_data.get("use_hierarchy")): seq_data = master_seq_data else: seq_data_list = [master_seq_data] @@ -119,20 +221,54 @@ class CreateRender(UnrealAssetCreator): "sub-sequence of the master sequence.") continue - instance_data["members"] = [selected_asset_path] - instance_data["sequence"] = selected_asset_path - instance_data["master_sequence"] = master_seq - instance_data["master_level"] = master_lvl - instance_data["output"] = seq_data.get('output') - instance_data["frameStart"] = seq_data.get('frame_range')[0] - instance_data["frameEnd"] = seq_data.get('frame_range')[1] + self.create_instance( + instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data) - super(CreateRender, self).create( - subset_name, - instance_data, - pre_create_data) + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("create_seq"): + self.create_with_new_sequence( + subset_name, instance_data, pre_create_data) + else: + self.create_from_existing_sequence( + subset_name, instance_data, pre_create_data) def get_pre_create_attr_defs(self): return [ - UILabelDef("Select the sequence to render.") + UILabelDef( + "Select a Level Sequence to render or create a new one." + ), + BoolDef( + "create_seq", + label="Create a new Level Sequence", + default=False + ), + UILabelDef( + "WARNING: If you create a new Level Sequence, the current\n" + "level will be saved and a new Master Level will be created." + ), + NumberDef( + "start_frame", + label="Start Frame", + default=0, + minimum=-999999, + maximum=999999 + ), + NumberDef( + "end_frame", + label="Start Frame", + default=150, + minimum=-999999, + maximum=999999 + ), + UISeparatorDef(), + UILabelDef( + "The following settings are valid only if you are not\n" + "creating a new sequence." + ), + BoolDef( + "use_hierarchy", + label="Use Hierarchy", + default=False + ), ] From 423cbf9e5465ee146523460376efde0595e44374 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Mar 2023 17:06:36 +0000 Subject: [PATCH 028/139] Fix level sequence not being added to instance --- openpype/hosts/unreal/plugins/create/create_render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index b2a246d3a8..b9c443c456 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -69,6 +69,8 @@ class CreateRender(UnrealAssetCreator): seq.set_playback_start(pre_create_data.get("start_frame")) seq.set_playback_end(pre_create_data.get("end_frame")) + pre_create_data["members"] = [seq.get_path_name()] + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) # Create the master level From 7d1e376761f8c4532af04f649355f9aead58e61f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 22 Mar 2023 11:35:20 +0000 Subject: [PATCH 029/139] Added warning if no assets selected when starting rendering --- openpype/hosts/unreal/api/rendering.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 29e4747f6e..25faa2ac2c 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -4,6 +4,7 @@ import unreal from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline +from openpype.widgets.message_window import Window queue = None @@ -37,6 +38,15 @@ def start_rendering(): # Get selected sequences assets = unreal.EditorUtilityLibrary.get_selected_assets() + if not assets: + Window( + parent=None, + title="No assets selected", + message="No assets selected. Select a render instance.", + level="warning") + raise RuntimeError( + "No assets selected. You need to select a render instance.") + # instances = pipeline.ls_inst() instances = [ a for a in assets From bc50139ec8ef7d93636f7ccfc861bd98dbdc2ba0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 15:20:01 +0000 Subject: [PATCH 030/139] Only parent to world on extraction if nested. --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 0cc842b4ec..fb097ca84a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -65,9 +65,10 @@ class ExtractXgen(publish.Extractor): ) cmds.delete(set(children) - set(shapes)) - duplicate_transform = cmds.parent( - duplicate_transform, world=True - )[0] + if cmds.listRelatives(duplicate_transform, parent=True): + duplicate_transform = cmds.parent( + duplicate_transform, world=True + )[0] duplicate_nodes.append(duplicate_transform) From 399541602898ce342f3f8639a1969a144c9824c7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 15:27:33 +0000 Subject: [PATCH 031/139] Validation for required namespace. --- .../hosts/maya/plugins/publish/validate_xgen.py | 13 +++++++++++++ website/docs/artist_hosts_maya_xgen.md | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 2870909974..47b24e218c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -57,3 +57,16 @@ class ValidateXgen(pyblish.api.InstancePlugin): json.dumps(inactive_modifiers, indent=4, sort_keys=True) ) ) + + # We need a namespace else there will be a naming conflict when + # extracting because of stripping namespaces and parenting to world. + node_names = [instance.data["xgmPalette"]] + for _, connections in instance.data["xgenConnections"].items(): + node_names.append(connections["transform"].split(".")[0]) + + non_namespaced_nodes = [n for n in node_names if ":" not in n] + if non_namespaced_nodes: + raise PublishValidationError( + "Could not find namespace on {}. Namespace is required for" + " xgen publishing.".format(non_namespaced_nodes) + ) diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index ec5f2ed921..db7bbd0557 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -43,6 +43,10 @@ Create an Xgen instance to publish. This needs to contain only **one Xgen collec You can create multiple Xgen instances if you have multiple collections to publish. +:::note +The Xgen publishing requires a namespace on the Xgen collection (palette) and the geometry used. +::: + ### Publish The publishing process will grab geometry used for Xgen along with any external files used in the collection's descriptions. This creates an isolated Maya file with just the Xgen collection's dependencies, so you can use any nested geometry when creating the Xgen description. An Xgen version will consist of: From 7dc59ece7b245e3bf47daf5bb7cbbd76cf49cf33 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:46:35 +0100 Subject: [PATCH 032/139] Define settings --- .../projects_schema/schema_project_maya.json | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 47dfb37024..80e2d43411 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -10,6 +10,41 @@ "key": "open_workfile_post_initialization", "label": "Open Workfile Post Initialization" }, + { + "type": "dict", + "key": "explicit_plugins_loading", + "label": "Explicit Plugins Loading", + "collapsible": true, + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "plugins_to_load", + "label": "Plugins To Load", + "object_type": { + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "name", + "label": "Name" + } + ] + } + } + ] + }, { "key": "imageio", "type": "dict", From 5349579f748bc1522d0ade1ef24da3c07337ad7c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:46:46 +0100 Subject: [PATCH 033/139] Define setting defaults --- .../defaults/project_settings/maya.json | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2aa95fd1be..cc3a76c599 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,5 +1,414 @@ { "open_workfile_post_initialization": false, + "explicit_plugins_loading": { + "enabled": false, + "plugins_to_load": [ + { + "enabled": false, + "name": "AbcBullet" + }, + { + "enabled": true, + "name": "AbcExport" + }, + { + "enabled": true, + "name": "AbcImport" + }, + { + "enabled": false, + "name": "animImportExport" + }, + { + "enabled": false, + "name": "ArubaTessellator" + }, + { + "enabled": false, + "name": "ATFPlugin" + }, + { + "enabled": false, + "name": "atomImportExport" + }, + { + "enabled": false, + "name": "AutodeskPacketFile" + }, + { + "enabled": false, + "name": "autoLoader" + }, + { + "enabled": false, + "name": "bifmeshio" + }, + { + "enabled": false, + "name": "bifrostGraph" + }, + { + "enabled": false, + "name": "bifrostshellnode" + }, + { + "enabled": false, + "name": "bifrostvisplugin" + }, + { + "enabled": false, + "name": "blast2Cmd" + }, + { + "enabled": false, + "name": "bluePencil" + }, + { + "enabled": false, + "name": "Boss" + }, + { + "enabled": false, + "name": "bullet" + }, + { + "enabled": true, + "name": "cacheEvaluator" + }, + { + "enabled": false, + "name": "cgfxShader" + }, + { + "enabled": false, + "name": "cleanPerFaceAssignment" + }, + { + "enabled": false, + "name": "clearcoat" + }, + { + "enabled": false, + "name": "convertToComponentTags" + }, + { + "enabled": false, + "name": "curveWarp" + }, + { + "enabled": false, + "name": "ddsFloatReader" + }, + { + "enabled": true, + "name": "deformerEvaluator" + }, + { + "enabled": false, + "name": "dgProfiler" + }, + { + "enabled": false, + "name": "drawUfe" + }, + { + "enabled": false, + "name": "dx11Shader" + }, + { + "enabled": false, + "name": "fbxmaya" + }, + { + "enabled": false, + "name": "fltTranslator" + }, + { + "enabled": false, + "name": "freeze" + }, + { + "enabled": false, + "name": "Fur" + }, + { + "enabled": false, + "name": "gameFbxExporter" + }, + { + "enabled": false, + "name": "gameInputDevice" + }, + { + "enabled": false, + "name": "GamePipeline" + }, + { + "enabled": false, + "name": "gameVertexCount" + }, + { + "enabled": false, + "name": "geometryReport" + }, + { + "enabled": false, + "name": "geometryTools" + }, + { + "enabled": false, + "name": "glslShader" + }, + { + "enabled": true, + "name": "GPUBuiltInDeformer" + }, + { + "enabled": false, + "name": "gpuCache" + }, + { + "enabled": false, + "name": "hairPhysicalShader" + }, + { + "enabled": false, + "name": "ik2Bsolver" + }, + { + "enabled": false, + "name": "ikSpringSolver" + }, + { + "enabled": false, + "name": "invertShape" + }, + { + "enabled": false, + "name": "lges" + }, + { + "enabled": false, + "name": "lookdevKit" + }, + { + "enabled": false, + "name": "MASH" + }, + { + "enabled": false, + "name": "matrixNodes" + }, + { + "enabled": false, + "name": "mayaCharacterization" + }, + { + "enabled": false, + "name": "mayaHIK" + }, + { + "enabled": false, + "name": "MayaMuscle" + }, + { + "enabled": false, + "name": "mayaUsdPlugin" + }, + { + "enabled": false, + "name": "mayaVnnPlugin" + }, + { + "enabled": false, + "name": "melProfiler" + }, + { + "enabled": false, + "name": "meshReorder" + }, + { + "enabled": false, + "name": "modelingToolkit" + }, + { + "enabled": false, + "name": "mtoa" + }, + { + "enabled": false, + "name": "mtoh" + }, + { + "enabled": false, + "name": "nearestPointOnMesh" + }, + { + "enabled": true, + "name": "objExport" + }, + { + "enabled": false, + "name": "OneClick" + }, + { + "enabled": false, + "name": "OpenEXRLoader" + }, + { + "enabled": false, + "name": "pgYetiMaya" + }, + { + "enabled": false, + "name": "pgyetiVrayMaya" + }, + { + "enabled": false, + "name": "polyBoolean" + }, + { + "enabled": false, + "name": "poseInterpolator" + }, + { + "enabled": false, + "name": "quatNodes" + }, + { + "enabled": false, + "name": "randomizerDevice" + }, + { + "enabled": false, + "name": "redshift4maya" + }, + { + "enabled": true, + "name": "renderSetup" + }, + { + "enabled": false, + "name": "retargeterNodes" + }, + { + "enabled": false, + "name": "RokokoMotionLibrary" + }, + { + "enabled": false, + "name": "rotateHelper" + }, + { + "enabled": false, + "name": "sceneAssembly" + }, + { + "enabled": false, + "name": "shaderFXPlugin" + }, + { + "enabled": false, + "name": "shotCamera" + }, + { + "enabled": false, + "name": "snapTransform" + }, + { + "enabled": false, + "name": "stage" + }, + { + "enabled": true, + "name": "stereoCamera" + }, + { + "enabled": false, + "name": "stlTranslator" + }, + { + "enabled": false, + "name": "studioImport" + }, + { + "enabled": false, + "name": "Substance" + }, + { + "enabled": false, + "name": "substancelink" + }, + { + "enabled": false, + "name": "substancemaya" + }, + { + "enabled": false, + "name": "substanceworkflow" + }, + { + "enabled": false, + "name": "svgFileTranslator" + }, + { + "enabled": false, + "name": "sweep" + }, + { + "enabled": false, + "name": "testify" + }, + { + "enabled": false, + "name": "tiffFloatReader" + }, + { + "enabled": false, + "name": "timeSliderBookmark" + }, + { + "enabled": false, + "name": "Turtle" + }, + { + "enabled": false, + "name": "Type" + }, + { + "enabled": false, + "name": "udpDevice" + }, + { + "enabled": false, + "name": "ufeSupport" + }, + { + "enabled": false, + "name": "Unfold3D" + }, + { + "enabled": false, + "name": "VectorRender" + }, + { + "enabled": false, + "name": "vrayformaya" + }, + { + "enabled": false, + "name": "vrayvolumegrid" + }, + { + "enabled": false, + "name": "xgenToolkit" + }, + { + "enabled": false, + "name": "xgenVray" + } + ] + }, "imageio": { "ocio_config": { "enabled": false, From f99c968df3dca4949e2e12a24ee908d5bf1ca997 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:48:13 +0100 Subject: [PATCH 034/139] Add launch arguments and env --- openpype/hooks/pre_add_last_workfile_arg.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 2558daef30..3d5f59cc67 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -44,10 +44,20 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): # Determine whether to open workfile post initialization. if self.host_name == "maya": - key = "open_workfile_post_initialization" - if self.data["project_settings"]["maya"][key]: + maya_settings = self.data["project_settings"]["maya"] + + if maya_settings["explicit_plugins_loading"]["enabled"]: + self.log.debug("Explicit plugins loading.") + self.launch_context.launch_args.append("-noAutoloadPlugins") + + keys = [ + "open_workfile_post_initialization", "explicit_plugins_loading" + ] + values = [maya_settings[k] for k in keys] + if any(values): self.log.debug("Opening workfile post initialization.") - self.data["env"]["OPENPYPE_" + key.upper()] = "1" + key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" + self.data["env"][key] = "1" return # Add path to workfile to arguments From ae4468bd209144fa406ba17b15a5c4d54c147516 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:48:25 +0100 Subject: [PATCH 035/139] Load plugins explicitly --- openpype/hosts/maya/startup/userSetup.py | 34 +++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index c77ecb829e..4932bf14c0 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,5 +1,4 @@ import os -from functools import partial from openpype.settings import get_project_settings from openpype.pipeline import install_host @@ -13,23 +12,40 @@ install_host(host) print("Starting OpenPype usersetup...") +settings = get_project_settings(os.environ['AVALON_PROJECT']) + +# Loading plugins explicitly. +if settings["maya"]["explicit_plugins_loading"]["enabled"]: + def _explicit_load_plugins(): + project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) + maya_settings = project_settings["maya"] + explicit_plugins_loading = maya_settings["explicit_plugins_loading"] + if explicit_plugins_loading["enabled"]: + for plugin in explicit_plugins_loading["plugins_to_load"]: + if plugin["enabled"]: + print("Loading " + plugin["name"]) + try: + cmds.loadPlugin(plugin["name"], quiet=True) + except RuntimeError as e: + print(e) + + cmds.evalDeferred( + _explicit_load_plugins, + lowestPriority=True + ) # Open Workfile Post Initialization. key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" if bool(int(os.environ.get(key, "0"))): + def _log_and_open(): + print("Opening \"{}\"".format(os.environ["AVALON_LAST_WORKFILE"])) + cmds.file(os.environ["AVALON_LAST_WORKFILE"], open=True, force=True) cmds.evalDeferred( - partial( - cmds.file, - os.environ["AVALON_LAST_WORKFILE"], - open=True, - force=True - ), + _log_and_open, lowestPriority=True ) - # Build a shelf. -settings = get_project_settings(os.environ['AVALON_PROJECT']) shelf_preset = settings['maya'].get('project_shelf') if shelf_preset: From 53361fe9dfae15bd4a1f318561dd4ea8393ce353 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:53:41 +0100 Subject: [PATCH 036/139] Modeling Toolkit is default loaded. --- 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 cc3a76c599..9b71b97d75 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -228,7 +228,7 @@ "name": "meshReorder" }, { - "enabled": false, + "enabled": true, "name": "modelingToolkit" }, { From 4bfb4aa75779cdd75d380cb0a976b5cb1757cbbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 10:03:53 +0100 Subject: [PATCH 037/139] Docs --- website/docs/admin_hosts_maya.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 23cacb4193..edbfa8da36 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -172,3 +172,12 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile ![maya build template](assets/maya-build_workfile_from_template.png) + +## Explicit Plugins Loading +You can define which plugins to load on launch of Maya here; `project_settings/maya/explicit_plugins_loading`. This can help improve Maya's launch speed, if you know which plugins are needed. + +By default only the required plugins are enabled. You can also add any plugin to the list to enable on launch. + +:::note technical +When enabling this feature, the workfile will be launched post initialization no matter the setting on `project_settings/maya/open_workfile_post_initialization`. This is to avoid any issues with references needing plugins. +::: From 4dd58e15d89383a870890562e9f084ee3fb189bf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 11:13:04 +0100 Subject: [PATCH 038/139] Fixed error on rendering --- openpype/hosts/unreal/api/rendering.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 5ef4792000..e197f9075d 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -134,6 +134,9 @@ def start_rendering(): settings.file_name_format = f"{shot_name}" + ".{frame_number}" settings.output_directory.path = f"{render_dir}/{output_dir}" + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineDeferredPassBase) + job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineImageSequenceOutput_PNG) From 45ea981efb5af84deaae232a8737e0aae6abab21 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 11:15:20 +0100 Subject: [PATCH 039/139] Added setting for rendering format --- openpype/hosts/unreal/api/rendering.py | 18 +++++++++++++++--- .../defaults/project_settings/unreal.json | 1 + .../projects_schema/schema_project_unreal.json | 12 ++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index e197f9075d..a2be041c18 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -33,7 +33,7 @@ def start_rendering(): """ Start the rendering process. """ - print("Starting rendering...") + unreal.log("Starting rendering...") # Get selected sequences assets = unreal.EditorUtilityLibrary.get_selected_assets() @@ -137,8 +137,20 @@ def start_rendering(): job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineDeferredPassBase) - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_PNG) + render_format = data.get("unreal").get("render_format", "png") + + if render_format == "png": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_PNG) + elif render_format == "exr": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_EXR) + elif render_format == "jpg": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_JPG) + elif render_format == "bmp": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_BMP) # If there are jobs in the queue, start the rendering process. if queue.get_jobs(): diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index ff290ef254..737a17d289 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -13,6 +13,7 @@ "delete_unmatched_assets": false, "render_config_path": "", "preroll_frames": 0, + "render_format": "png", "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 40bbb40ccc..35eb0b24f1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -42,6 +42,18 @@ "key": "preroll_frames", "label": "Pre-roll frames" }, + { + "key": "render_format", + "label": "Render format", + "type": "enum", + "multiselection": false, + "enum_items": [ + {"png": "PNG"}, + {"exr": "EXR"}, + {"jpg": "JPG"}, + {"bmp": "BMP"} + ] + }, { "type": "dict", "collapsible": true, From a579dfc860b7e22d344c617afefef37899dae994 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 12:31:02 +0100 Subject: [PATCH 040/139] Get the correct frame range data --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 87f1338ee8..e6584e130f 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -20,6 +20,7 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): def process(self, instance): representations = instance.data.get("representations") for repr in representations: + data = instance.data.get("assetEntity", {}).get("data", {}) patterns = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble( repr["files"], minimum_items=1, patterns=patterns) @@ -30,8 +31,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = list(collection.indexes) current_range = (frames[0], frames[-1]) - required_range = (instance.data["frameStart"], - instance.data["frameEnd"]) + required_range = (data["frameStart"], + data["frameEnd"]) if current_range != required_range: raise ValueError(f"Invalid frame range: {current_range} - " From 6d2a45e9516ef55b84a181d0b30a8abe5405afbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 16:42:35 +0100 Subject: [PATCH 041/139] Move -noAutoLoadPlugins flag to separate hook. --- openpype/hooks/pre_add_last_workfile_arg.py | 7 +------ .../hosts/maya/hooks/pre_auto_load_plugins.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 openpype/hosts/maya/hooks/pre_auto_load_plugins.py diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 3d5f59cc67..df4aa5cc5d 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -44,15 +44,10 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): # Determine whether to open workfile post initialization. if self.host_name == "maya": - maya_settings = self.data["project_settings"]["maya"] - - if maya_settings["explicit_plugins_loading"]["enabled"]: - self.log.debug("Explicit plugins loading.") - self.launch_context.launch_args.append("-noAutoloadPlugins") - keys = [ "open_workfile_post_initialization", "explicit_plugins_loading" ] + maya_settings = self.data["project_settings"]["maya"] values = [maya_settings[k] for k in keys] if any(values): self.log.debug("Opening workfile post initialization.") diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py new file mode 100644 index 0000000000..3c3ddbe4dc --- /dev/null +++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py @@ -0,0 +1,15 @@ +from openpype.lib import PreLaunchHook + + +class PreAutoLoadPlugins(PreLaunchHook): + """Define -noAutoloadPlugins command flag.""" + + # Execute before workfile argument. + order = 0 + app_groups = ["maya"] + + def execute(self): + maya_settings = self.data["project_settings"]["maya"] + if maya_settings["explicit_plugins_loading"]["enabled"]: + self.log.debug("Explicit plugins loading.") + self.launch_context.launch_args.append("-noAutoloadPlugins") From 72af67fc657fd7e52eca302d86f887c9af041212 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 16:42:45 +0100 Subject: [PATCH 042/139] Warn about render farm support. --- website/docs/admin_hosts_maya.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index edbfa8da36..5211760632 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -180,4 +180,6 @@ By default only the required plugins are enabled. You can also add any plugin to :::note technical When enabling this feature, the workfile will be launched post initialization no matter the setting on `project_settings/maya/open_workfile_post_initialization`. This is to avoid any issues with references needing plugins. + +Renderfarm integration is not supported for this feature. ::: From 32743b7a855e1a9df980f923f35da0620adbb237 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Mar 2023 12:48:00 +0100 Subject: [PATCH 043/139] Setup settings. --- .../defaults/project_settings/maya.json | 245 ++-- .../schemas/schema_maya_capture.json | 1258 +++++++++-------- 2 files changed, 770 insertions(+), 733 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e914eb29f9..4044bdf5df 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -788,126 +788,133 @@ "validate_shapes": true }, "ExtractPlayblast": { - "capture_preset": { - "Codec": { - "compression": "png", - "format": "image", - "quality": 95 - }, - "Display Options": { - "background": [ - 125, - 125, - 125, - 255 - ], - "backgroundBottom": [ - 125, - 125, - 125, - 255 - ], - "backgroundTop": [ - 125, - 125, - 125, - 255 - ], - "override_display": true - }, - "Generic": { - "isolate_view": true, - "off_screen": true, - "pan_zoom": false - }, - "Renderer": { - "rendererName": "vp2Renderer" - }, - "Resolution": { - "width": 1920, - "height": 1080 - }, - "Viewport Options": { - "override_viewport_options": true, - "displayLights": "default", - "displayTextures": true, - "textureMaxResolution": 1024, - "renderDepthOfField": true, - "shadows": true, - "twoSidedLighting": true, - "lineAAEnable": true, - "multiSample": 8, - "useDefaultMaterial": false, - "wireframeOnShaded": false, - "xray": false, - "jointXray": false, - "backfaceCulling": false, - "ssaoEnable": false, - "ssaoAmount": 1, - "ssaoRadius": 16, - "ssaoFilterRadius": 16, - "ssaoSamples": 16, - "fogging": false, - "hwFogFalloff": "0", - "hwFogDensity": 0.0, - "hwFogStart": 0, - "hwFogEnd": 100, - "hwFogAlpha": 0, - "hwFogColorR": 1.0, - "hwFogColorG": 1.0, - "hwFogColorB": 1.0, - "motionBlurEnable": false, - "motionBlurSampleCount": 8, - "motionBlurShutterOpenFraction": 0.2, - "cameras": false, - "clipGhosts": false, - "deformers": false, - "dimensions": false, - "dynamicConstraints": false, - "dynamics": false, - "fluids": false, - "follicles": false, - "gpuCacheDisplayFilter": false, - "greasePencils": false, - "grid": false, - "hairSystems": true, - "handles": false, - "headsUpDisplay": false, - "ikHandles": false, - "imagePlane": true, - "joints": false, - "lights": false, - "locators": false, - "manipulators": false, - "motionTrails": false, - "nCloths": false, - "nParticles": false, - "nRigids": false, - "controlVertices": false, - "nurbsCurves": false, - "hulls": false, - "nurbsSurfaces": false, - "particleInstancers": false, - "pivots": false, - "planes": false, - "pluginShapes": false, - "polymeshes": true, - "strokes": false, - "subdivSurfaces": false, - "textures": false - }, - "Camera Options": { - "displayGateMask": false, - "displayResolution": false, - "displayFilmGate": false, - "displayFieldChart": false, - "displaySafeAction": false, - "displaySafeTitle": false, - "displayFilmPivot": false, - "displayFilmOrigin": false, - "overscan": 1.0 + "profiles": [ + { + "task_types": [], + "task_names": [], + "subsets": [], + "capture_preset": { + "Codec": { + "compression": "png", + "format": "image", + "quality": 95 + }, + "Display Options": { + "background": [ + 125, + 125, + 125, + 255 + ], + "backgroundBottom": [ + 125, + 125, + 125, + 255 + ], + "backgroundTop": [ + 125, + 125, + 125, + 255 + ], + "override_display": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true, + "pan_zoom": false + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1920, + "height": 1080 + }, + "Viewport Options": { + "override_viewport_options": true, + "displayLights": "default", + "displayTextures": true, + "textureMaxResolution": 1024, + "renderDepthOfField": true, + "shadows": true, + "twoSidedLighting": true, + "lineAAEnable": true, + "multiSample": 8, + "useDefaultMaterial": false, + "wireframeOnShaded": false, + "xray": false, + "jointXray": false, + "backfaceCulling": false, + "ssaoEnable": false, + "ssaoAmount": 1, + "ssaoRadius": 16, + "ssaoFilterRadius": 16, + "ssaoSamples": 16, + "fogging": false, + "hwFogFalloff": "0", + "hwFogDensity": 0.0, + "hwFogStart": 0, + "hwFogEnd": 100, + "hwFogAlpha": 0, + "hwFogColorR": 1.0, + "hwFogColorG": 1.0, + "hwFogColorB": 1.0, + "motionBlurEnable": false, + "motionBlurSampleCount": 0, + "motionBlurShutterOpenFraction": 0.2, + "cameras": false, + "clipGhosts": false, + "deformers": false, + "dimensions": false, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "gpuCacheDisplayFilter": false, + "greasePencils": false, + "grid": false, + "hairSystems": true, + "handles": false, + "headsUpDisplay": false, + "ikHandles": false, + "imagePlane": true, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "controlVertices": false, + "nurbsCurves": false, + "hulls": false, + "nurbsSurfaces": false, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": false, + "strokes": false, + "subdivSurfaces": false, + "textures": false + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } + } } - } + ] }, "ExtractMayaSceneRaw": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 416e530db2..1d0f94e5b8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -5,622 +5,652 @@ "label": "Extract Playblast settings", "children": [ { - "type": "dict", - "key": "capture_preset", - "children": [ - { - "type": "dict", - "key": "Codec", - "children": [ - { - "type": "label", - "label": "Codec" - }, - { - "type": "text", - "key": "compression", - "label": "Encoding" - }, - { - "type": "text", - "key": "format", - "label": "Format" - }, - { - "type": "number", - "key": "quality", - "label": "Quality", - "decimal": 0, - "minimum": 0, - "maximum": 100 - }, + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "capture_preset", + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Encoding" + }, + { + "type": "text", + "key": "format", + "label": "Format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, - { - "type": "splitter" - } - ] - }, - { - "type": "dict", - "key": "Display Options", - "children": [ - { - "type": "label", - "label": "Display Options" - }, + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, - { - "type": "color", - "key": "background", - "label": "Background Color: " - }, - { - "type": "color", - "key": "backgroundBottom", - "label": "Background Bottom: " - }, - { - "type": "color", - "key": "backgroundTop", - "label": "Background Top: " - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Generic", - "children": [ - { - "type": "label", - "label": "Generic" - }, - { - "type": "boolean", - "key": "isolate_view", - "label": " Isolate view" - }, - { - "type": "boolean", - "key": "off_screen", - "label": " Off Screen" - }, - { - "type": "boolean", - "key": "pan_zoom", - "label": " 2D Pan/Zoom" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Renderer", - "children": [ - { - "type": "label", - "label": "Renderer" - }, - { - "type": "enum", - "key": "rendererName", - "label": "Renderer name", - "enum_items": [ - { "vp2Renderer": "Viewport 2.0" } - ] - } - ] - }, - { - "type": "dict", - "key": "Resolution", - "children": [ - { - "type": "splitter" - }, - { - "type": "label", - "label": "Resolution" - }, - { - "type": "number", - "key": "width", - "label": " Width", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "height", - "label": "Height", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "collapsible": true, - "key": "Viewport Options", - "label": "Viewport Options", - "children": [ - { - "type": "boolean", - "key": "override_viewport_options", - "label": "Override Viewport Options" - }, - { - "type": "enum", - "key": "displayLights", - "label": "Display Lights", - "enum_items": [ - { "default": "Default Lighting"}, - { "all": "All Lights"}, - { "selected": "Selected Lights"}, - { "flat": "Flat Lighting"}, - { "nolights": "No Lights"} - ] - }, - { - "type": "boolean", - "key": "displayTextures", - "label": "Display Textures" - }, - { - "type": "number", - "key": "textureMaxResolution", - "label": "Texture Clamp Resolution", - "decimal": 0 - }, - { - "type": "splitter" - }, - { - "type": "label", - "label": "Display" - }, - { - "type":"boolean", - "key": "renderDepthOfField", - "label": "Depth of Field" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "shadows", - "label": "Display Shadows" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "Two Sided Lighting" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "lineAAEnable", - "label": "Enable Anti-Aliasing" - }, - { - "type": "number", - "key": "multiSample", - "label": "Anti Aliasing Samples", - "decimal": 0, - "minimum": 0, - "maximum": 32 - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "useDefaultMaterial", - "label": "Use Default Material" - }, - { - "type": "boolean", - "key": "wireframeOnShaded", - "label": "Wireframe On Shaded" - }, - { - "type": "boolean", - "key": "xray", - "label": "X-Ray" - }, - { - "type": "boolean", - "key": "jointXray", - "label": "X-Ray Joints" - }, - { - "type": "boolean", - "key": "backfaceCulling", - "label": "Backface Culling" - }, - { - "type": "boolean", - "key": "ssaoEnable", - "label": "Screen Space Ambient Occlusion" - }, - { - "type": "number", - "key": "ssaoAmount", - "label": "SSAO Amount" - }, - { - "type": "number", - "key": "ssaoRadius", - "label": "SSAO Radius" - }, - { - "type": "number", - "key": "ssaoFilterRadius", - "label": "SSAO Filter Radius", - "decimal": 0, - "minimum": 1, - "maximum": 32 - }, - { - "type": "number", - "key": "ssaoSamples", - "label": "SSAO Samples", - "decimal": 0, - "minimum": 8, - "maximum": 32 - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "fogging", - "label": "Enable Hardware Fog" - }, - { - "type": "enum", - "key": "hwFogFalloff", - "label": "Hardware Falloff", - "enum_items": [ - { "0": "Linear"}, - { "1": "Exponential"}, - { "2": "Exponential Squared"} - ] - }, - { - "type": "number", - "key": "hwFogDensity", - "label": "Fog Density", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "number", - "key": "hwFogStart", - "label": "Fog Start" - }, - { - "type": "number", - "key": "hwFogEnd", - "label": "Fog End" - }, - { - "type": "number", - "key": "hwFogAlpha", - "label": "Fog Alpha" - }, - { - "type": "number", - "key": "hwFogColorR", - "label": "Fog Color R", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "number", - "key": "hwFogColorG", - "label": "Fog Color G", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "number", - "key": "hwFogColorB", - "label": "Fog Color B", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "motionBlurEnable", - "label": "Enable Motion Blur" - }, - { - "type": "number", - "key": "motionBlurSampleCount", - "label": "Motion Blur Sample Count", - "decimal": 0, - "minimum": 8, - "maximum": 32 - }, - { - "type": "number", - "key": "motionBlurShutterOpenFraction", - "label": "Shutter Open Fraction", - "decimal": 3, - "minimum": 0.01, - "maximum": 32 - }, - { - "type": "splitter" - }, - { - "type": "label", - "label": "Show" - }, - { - "type": "boolean", - "key": "cameras", - "label": "Cameras" - }, - { - "type": "boolean", - "key": "clipGhosts", - "label": "Clip Ghosts" - }, - { - "type": "boolean", - "key": "deformers", - "label": "Deformers" - }, - { - "type": "boolean", - "key": "dimensions", - "label": "Dimensions" - }, - { - "type": "boolean", - "key": "dynamicConstraints", - "label": "Dynamic Constraints" - }, - { - "type": "boolean", - "key": "dynamics", - "label": "Dynamics" - }, - { - "type": "boolean", - "key": "fluids", - "label": "Fluids" - }, - { - "type": "boolean", - "key": "follicles", - "label": "Follicles" - }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "GPU Cache" - }, - { - "type": "boolean", - "key": "greasePencils", - "label": "Grease Pencil" - }, - { - "type": "boolean", - "key": "grid", - "label": "Grid" - }, - { - "type": "boolean", - "key": "hairSystems", - "label": "Hair Systems" - }, - { - "type": "boolean", - "key": "handles", - "label": "Handles" - }, - { - "type": "boolean", - "key": "headsUpDisplay", - "label": "HUD" - }, - { - "type": "boolean", - "key": "ikHandles", - "label": "IK Handles" - }, - { - "type": "boolean", - "key": "imagePlane", - "label": "Image Planes" - }, - { - "type": "boolean", - "key": "joints", - "label": "Joints" - }, - { - "type": "boolean", - "key": "lights", - "label": "Lights" - }, - { - "type": "boolean", - "key": "locators", - "label": "Locators" - }, - { - "type": "boolean", - "key": "manipulators", - "label": "Manipulators" - }, - { - "type": "boolean", - "key": "motionTrails", - "label": "Motion Trails" - }, - { - "type": "boolean", - "key": "nCloths", - "label": "nCloths" - }, - { - "type": "boolean", - "key": "nParticles", - "label": "nParticles" - }, - { - "type": "boolean", - "key": "nRigids", - "label": "nRigids" - }, - { - "type": "boolean", - "key": "controlVertices", - "label": "NURBS CVs" - }, - { - "type": "boolean", - "key": "nurbsCurves", - "label": "NURBS Curves" - }, - { - "type": "boolean", - "key": "hulls", - "label": "NURBS Hulls" - }, - { - "type": "boolean", - "key": "nurbsSurfaces", - "label": "NURBS Surfaces" - }, - { - "type": "boolean", - "key": "particleInstancers", - "label": "Particle Instancers" - }, - { - "type": "boolean", - "key": "pivots", - "label": "Pivots" - }, - { - "type": "boolean", - "key": "planes", - "label": "Planes" - }, - { - "type": "boolean", - "key": "pluginShapes", - "label": "Plugin Shapes" - }, - { - "type": "boolean", - "key": "polymeshes", - "label": "Polygons" - }, - { - "type": "boolean", - "key": "strokes", - "label": "Strokes" - }, - { - "type": "boolean", - "key": "subdivSurfaces", - "label": "Subdiv Surfaces" - }, - { - "type": "boolean", - "key": "textures", - "label": "Texture Placements" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Camera Options", - "label": "Camera Options", - "children": [ - { - "type": "boolean", - "key": "displayGateMask", - "label": "Display Gate Mask" - }, - { - "type": "boolean", - "key": "displayResolution", - "label": "Display Resolution" - }, - { - "type": "boolean", - "key": "displayFilmGate", - "label": "Display Film Gate" - }, - { - "type": "boolean", - "key": "displayFieldChart", - "label": "Display Field Chart" - }, - { - "type": "boolean", - "key": "displaySafeAction", - "label": "Display Safe Action" - }, - { - "type": "boolean", - "key": "displaySafeTitle", - "label": "Display Safe Title" - }, - { - "type": "boolean", - "key": "displayFilmPivot", - "label": "Display Film Pivot" - }, - { - "type": "boolean", - "key": "displayFilmOrigin", - "label": "Display Film Origin" - }, - { - "type": "number", - "key": "overscan", - "label": "Overscan", - "decimal": 1, - "minimum": 0, - "maximum": 10 - } - ] - } - ] + { + "type": "color", + "key": "background", + "label": "Background Color: " + }, + { + "type": "color", + "key": "backgroundBottom", + "label": "Background Bottom: " + }, + { + "type": "color", + "key": "backgroundTop", + "label": "Background Top: " + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + }, + { + "type": "boolean", + "key": "pan_zoom", + "label": " 2D Pan/Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "enum", + "key": "rendererName", + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "Override Viewport Options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "boolean", + "key": "displayTextures", + "label": "Display Textures" + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Display" + }, + { + "type":"boolean", + "key": "renderDepthOfField", + "label": "Depth of Field" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "lineAAEnable", + "label": "Enable Anti-Aliasing" + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "useDefaultMaterial", + "label": "Use Default Material" + }, + { + "type": "boolean", + "key": "wireframeOnShaded", + "label": "Wireframe On Shaded" + }, + { + "type": "boolean", + "key": "xray", + "label": "X-Ray" + }, + { + "type": "boolean", + "key": "jointXray", + "label": "X-Ray Joints" + }, + { + "type": "boolean", + "key": "backfaceCulling", + "label": "Backface Culling" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "number", + "key": "ssaoAmount", + "label": "SSAO Amount" + }, + { + "type": "number", + "key": "ssaoRadius", + "label": "SSAO Radius" + }, + { + "type": "number", + "key": "ssaoFilterRadius", + "label": "SSAO Filter Radius", + "decimal": 0, + "minimum": 1, + "maximum": 32 + }, + { + "type": "number", + "key": "ssaoSamples", + "label": "SSAO Samples", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "fogging", + "label": "Enable Hardware Fog" + }, + { + "type": "enum", + "key": "hwFogFalloff", + "label": "Hardware Falloff", + "enum_items": [ + { "0": "Linear"}, + { "1": "Exponential"}, + { "2": "Exponential Squared"} + ] + }, + { + "type": "number", + "key": "hwFogDensity", + "label": "Fog Density", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogStart", + "label": "Fog Start" + }, + { + "type": "number", + "key": "hwFogEnd", + "label": "Fog End" + }, + { + "type": "number", + "key": "hwFogAlpha", + "label": "Fog Alpha" + }, + { + "type": "number", + "key": "hwFogColorR", + "label": "Fog Color R", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorG", + "label": "Fog Color G", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorB", + "label": "Fog Color B", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "motionBlurEnable", + "label": "Enable Motion Blur" + }, + { + "type": "number", + "key": "motionBlurSampleCount", + "label": "Motion Blur Sample Count", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "number", + "key": "motionBlurShutterOpenFraction", + "label": "Shutter Open Fraction", + "decimal": 3, + "minimum": 0.01, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Show" + }, + { + "type": "boolean", + "key": "cameras", + "label": "Cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "Clip Ghosts" + }, + { + "type": "boolean", + "key": "deformers", + "label": "Deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "Dimensions" + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "Dynamic Constraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "Dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "Fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "Follicles" + }, + { + "type": "boolean", + "key": "gpuCacheDisplayFilter", + "label": "GPU Cache" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "Grease Pencil" + }, + { + "type": "boolean", + "key": "grid", + "label": "Grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "Hair Systems" + }, + { + "type": "boolean", + "key": "handles", + "label": "Handles" + }, + { + "type": "boolean", + "key": "headsUpDisplay", + "label": "HUD" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "IK Handles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "Image Planes" + }, + { + "type": "boolean", + "key": "joints", + "label": "Joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "Lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "Locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "Manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "Motion Trails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "NURBS CVs" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "NURBS Curves" + }, + { + "type": "boolean", + "key": "hulls", + "label": "NURBS Hulls" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "NURBS Surfaces" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "Particle Instancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "Pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "Planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "Plugin Shapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "Polygons" + }, + { + "type": "boolean", + "key": "strokes", + "label": "Strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "Subdiv Surfaces" + }, + { + "type": "boolean", + "key": "textures", + "label": "Texture Placements" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "Display Gate Mask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "Display Resolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "Display Film Gate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "Display Field Chart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "Display Safe Action" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "Display Safe Title" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "Display Film Pivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "Display Film Origin" + }, + { + "type": "number", + "key": "overscan", + "label": "Overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + } + ] + } } ] } From 2278478598dbda1b465f2d9108ce27106d807e21 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Sun, 2 Apr 2023 08:25:06 +0100 Subject: [PATCH 044/139] Update openpype/hosts/maya/plugins/publish/collect_review.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/collect_review.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index d15eb7a12b..a184865602 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -143,4 +143,8 @@ class CollectReview(pyblish.api.InstancePlugin): # Convert enum attribute index to string. index = instance.data.get("displayLights", 0) - instance.data["displayLights"] = lib.DISPLAY_LIGHTS[index] + display_lights = lib.DISPLAY_LIGHTS[index] + if display_lights == "project_settings": + # project_settings/maya/publish/ExtractPlayblast/capture_preset/Viewport Options/displayLights + display_lights = instance.context.data["project_settings"]["maya"]["publish"]["ExtractPlayblast"]["capture_preset"]["Viewport Options"]["displayLights"] # noqa + instance.data["displayLights"] = display_lights From b9e9750377d44836c94013f2222422bca736c644 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Sun, 2 Apr 2023 08:25:25 +0100 Subject: [PATCH 045/139] Update openpype/hosts/maya/plugins/publish/extract_playblast.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 801f05a770..2167f2c5b3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -95,10 +95,8 @@ class ExtractPlayblast(publish.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) - # Show lighting mode. - display_lights = instance.data["displayLights"] - if display_lights != "project_settings": - preset["viewport_options"]["displayLights"] = display_lights + # Use displayLights setting from instance + preset["viewport_options"]["displayLights"] = instance.data["displayLights"] # Override transparency if requested. transparency = instance.data.get("transparency", 0) From 3ebac0b326e4f06aaa5a396554e4fff16df5efeb Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Sun, 2 Apr 2023 08:25:34 +0100 Subject: [PATCH 046/139] Update openpype/hosts/maya/plugins/publish/extract_thumbnail.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 79c768228f..92d0141f01 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -105,11 +105,8 @@ class ExtractThumbnail(publish.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) - # Show lighting mode. - display_lights = instance.data["displayLights"] - if display_lights != "project_settings": - preset["viewport_options"]["displayLights"] = display_lights - + # Use displayLights setting from instance + preset["viewport_options"]["displayLights"] = instance.data["displayLights"] # Override transparency if requested. transparency = instance.data.get("transparency", 0) if transparency != 0: From 2e234a84dc791bf3452fb9e0610a23bda3cec233 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 2 Apr 2023 08:34:26 +0100 Subject: [PATCH 047/139] Hound --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0381a8adc1..f790d08ae3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -117,7 +117,8 @@ class ExtractPlayblast(publish.Extractor): pm.currentTime(refreshFrameInt, edit=True) # Use displayLights setting from instance - preset["viewport_options"]["displayLights"] = instance.data["displayLights"] + key = "displayLights" + preset["viewport_options"][key] = instance.data[key] # Override transparency if requested. transparency = instance.data.get("transparency", 0) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 430322c911..d66f65ce88 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -106,7 +106,9 @@ class ExtractThumbnail(publish.Extractor): pm.currentTime(refreshFrameInt, edit=True) # Use displayLights setting from instance - preset["viewport_options"]["displayLights"] = instance.data["displayLights"] + key = "displayLights" + preset["viewport_options"][key] = instance.data[key] + # Override transparency if requested. transparency = instance.data.get("transparency", 0) if transparency != 0: From 87dc14fe9e91c202d4eefa82f85093a4a2814c76 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 3 Apr 2023 11:59:16 +0100 Subject: [PATCH 048/139] Default values for profiles. --- .../defaults/project_settings/maya.json | 6 +- openpype/settings/entities/color_entity.py | 6 +- openpype/settings/entities/input_entities.py | 4 +- .../schemas/schema_maya_capture.json | 267 ++++++++++++------ 4 files changed, 188 insertions(+), 95 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4044bdf5df..f6162182e8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -829,8 +829,8 @@ "rendererName": "vp2Renderer" }, "Resolution": { - "width": 1920, - "height": 1080 + "width": 0, + "height": 0 }, "Viewport Options": { "override_viewport_options": true, @@ -896,7 +896,7 @@ "pivots": false, "planes": false, "pluginShapes": false, - "polymeshes": false, + "polymeshes": true, "strokes": false, "subdivSurfaces": false, "textures": false diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index bdaab6f583..a542f2fa38 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -11,7 +11,9 @@ class ColorEntity(InputEntity): def _item_initialization(self): self.valid_value_types = (list, ) - self.value_on_not_set = [0, 0, 0, 255] + self.value_on_not_set = self.convert_to_valid_type( + self.schema_data.get("default", [0, 0, 0, 255]) + ) self.use_alpha = self.schema_data.get("use_alpha", True) def set_override_state(self, *args, **kwargs): @@ -64,6 +66,6 @@ class ColorEntity(InputEntity): new_value.append(item) # Make sure - if not self.use_alpha: + if hasattr(self, "use_alpha") and not self.use_alpha: new_value[3] = 255 return new_value diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 89f12afd9b..842117ad48 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -442,7 +442,9 @@ class TextEntity(InputEntity): def _item_initialization(self): self.valid_value_types = (STRING_TYPE, ) - self.value_on_not_set = "" + self.value_on_not_set = self.convert_to_valid_type( + self.schema_data.get("default", "") + ) # GUI attributes self.multiline = self.schema_data.get("multiline", False) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1d0f94e5b8..beaa7c442d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -46,12 +46,14 @@ { "type": "text", "key": "compression", - "label": "Encoding" + "label": "Encoding", + "default": "png" }, { "type": "text", "key": "format", - "label": "Format" + "label": "Format", + "default": "image" }, { "type": "number", @@ -59,7 +61,8 @@ "label": "Quality", "decimal": 0, "minimum": 0, - "maximum": 100 + "maximum": 100, + "default": 95 }, { @@ -79,22 +82,26 @@ { "type": "color", "key": "background", - "label": "Background Color: " + "label": "Background Color: ", + "default": [125, 125, 125, 255] }, { "type": "color", "key": "backgroundBottom", - "label": "Background Bottom: " + "label": "Background Bottom: ", + "default": [125, 125, 125, 255] }, { "type": "color", "key": "backgroundTop", - "label": "Background Top: " + "label": "Background Top: ", + "default": [125, 125, 125, 255] }, { "type": "boolean", "key": "override_display", - "label": "Override display options" + "label": "Override display options", + "default": true } ] }, @@ -112,17 +119,20 @@ { "type": "boolean", "key": "isolate_view", - "label": " Isolate view" + "label": " Isolate view", + "default": true }, { "type": "boolean", "key": "off_screen", - "label": " Off Screen" + "label": " Off Screen", + "default": true }, { "type": "boolean", "key": "pan_zoom", - "label": " 2D Pan/Zoom" + "label": " 2D Pan/Zoom", + "default": false } ] }, @@ -143,7 +153,8 @@ "label": "Renderer name", "enum_items": [ { "vp2Renderer": "Viewport 2.0" } - ] + ], + "default": "vp2Renderer" } ] }, @@ -164,7 +175,8 @@ "label": " Width", "decimal": 0, "minimum": 0, - "maximum": 99999 + "maximum": 99999, + "default": 0 }, { "type": "number", @@ -172,7 +184,8 @@ "label": "Height", "decimal": 0, "minimum": 0, - "maximum": 99999 + "maximum": 99999, + "default": 0 } ] }, @@ -188,7 +201,8 @@ { "type": "boolean", "key": "override_viewport_options", - "label": "Override Viewport Options" + "label": "Override Viewport Options", + "default": true }, { "type": "enum", @@ -200,18 +214,21 @@ { "selected": "Selected Lights"}, { "flat": "Flat Lighting"}, { "nolights": "No Lights"} - ] + ], + "default": "default" }, { "type": "boolean", "key": "displayTextures", - "label": "Display Textures" + "label": "Display Textures", + "default": true }, { "type": "number", "key": "textureMaxResolution", "label": "Texture Clamp Resolution", - "decimal": 0 + "decimal": 0, + "default": 1024 }, { "type": "splitter" @@ -223,7 +240,8 @@ { "type":"boolean", "key": "renderDepthOfField", - "label": "Depth of Field" + "label": "Depth of Field", + "default": true }, { "type": "splitter" @@ -231,12 +249,14 @@ { "type": "boolean", "key": "shadows", - "label": "Display Shadows" + "label": "Display Shadows", + "default": true }, { "type": "boolean", "key": "twoSidedLighting", - "label": "Two Sided Lighting" + "label": "Two Sided Lighting", + "default": true }, { "type": "splitter" @@ -244,7 +264,8 @@ { "type": "boolean", "key": "lineAAEnable", - "label": "Enable Anti-Aliasing" + "label": "Enable Anti-Aliasing", + "default": true }, { "type": "number", @@ -252,7 +273,8 @@ "label": "Anti Aliasing Samples", "decimal": 0, "minimum": 0, - "maximum": 32 + "maximum": 32, + "default": 8 }, { "type": "splitter" @@ -260,42 +282,50 @@ { "type": "boolean", "key": "useDefaultMaterial", - "label": "Use Default Material" + "label": "Use Default Material", + "default": false }, { "type": "boolean", "key": "wireframeOnShaded", - "label": "Wireframe On Shaded" + "label": "Wireframe On Shaded", + "default": false }, { "type": "boolean", "key": "xray", - "label": "X-Ray" + "label": "X-Ray", + "default": false }, { "type": "boolean", "key": "jointXray", - "label": "X-Ray Joints" + "label": "X-Ray Joints", + "default": false }, { "type": "boolean", "key": "backfaceCulling", - "label": "Backface Culling" + "label": "Backface Culling", + "default": false }, { "type": "boolean", "key": "ssaoEnable", - "label": "Screen Space Ambient Occlusion" + "label": "Screen Space Ambient Occlusion", + "default": false }, { "type": "number", "key": "ssaoAmount", - "label": "SSAO Amount" + "label": "SSAO Amount", + "default": 1 }, { "type": "number", "key": "ssaoRadius", - "label": "SSAO Radius" + "label": "SSAO Radius", + "default": 16 }, { "type": "number", @@ -303,7 +333,8 @@ "label": "SSAO Filter Radius", "decimal": 0, "minimum": 1, - "maximum": 32 + "maximum": 32, + "default": 16 }, { "type": "number", @@ -311,7 +342,8 @@ "label": "SSAO Samples", "decimal": 0, "minimum": 8, - "maximum": 32 + "maximum": 32, + "default": 16 }, { "type": "splitter" @@ -319,7 +351,8 @@ { "type": "boolean", "key": "fogging", - "label": "Enable Hardware Fog" + "label": "Enable Hardware Fog", + "default": false }, { "type": "enum", @@ -329,7 +362,8 @@ { "0": "Linear"}, { "1": "Exponential"}, { "2": "Exponential Squared"} - ] + ], + "default": "0" }, { "type": "number", @@ -337,22 +371,26 @@ "label": "Fog Density", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 0 }, { "type": "number", "key": "hwFogStart", - "label": "Fog Start" + "label": "Fog Start", + "default": 0 }, { "type": "number", "key": "hwFogEnd", - "label": "Fog End" + "label": "Fog End", + "default": 100 }, { "type": "number", "key": "hwFogAlpha", - "label": "Fog Alpha" + "label": "Fog Alpha", + "default": 0 }, { "type": "number", @@ -360,7 +398,8 @@ "label": "Fog Color R", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 1 }, { "type": "number", @@ -368,7 +407,8 @@ "label": "Fog Color G", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 1 }, { "type": "number", @@ -376,7 +416,8 @@ "label": "Fog Color B", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 1 }, { "type": "splitter" @@ -384,7 +425,8 @@ { "type": "boolean", "key": "motionBlurEnable", - "label": "Enable Motion Blur" + "label": "Enable Motion Blur", + "default": false }, { "type": "number", @@ -392,7 +434,8 @@ "label": "Motion Blur Sample Count", "decimal": 0, "minimum": 8, - "maximum": 32 + "maximum": 32, + "default": 8 }, { "type": "number", @@ -400,7 +443,8 @@ "label": "Shutter Open Fraction", "decimal": 3, "minimum": 0.01, - "maximum": 32 + "maximum": 32, + "default": 0.2 }, { "type": "splitter" @@ -412,182 +456,218 @@ { "type": "boolean", "key": "cameras", - "label": "Cameras" + "label": "Cameras", + "default": false }, { "type": "boolean", "key": "clipGhosts", - "label": "Clip Ghosts" + "label": "Clip Ghosts", + "default": false }, { "type": "boolean", "key": "deformers", - "label": "Deformers" + "label": "Deformers", + "default": false }, { "type": "boolean", "key": "dimensions", - "label": "Dimensions" + "label": "Dimensions", + "default": false }, { "type": "boolean", "key": "dynamicConstraints", - "label": "Dynamic Constraints" + "label": "Dynamic Constraints", + "default": false }, { "type": "boolean", "key": "dynamics", - "label": "Dynamics" + "label": "Dynamics", + "default": false }, { "type": "boolean", "key": "fluids", - "label": "Fluids" + "label": "Fluids", + "default": false }, { "type": "boolean", "key": "follicles", - "label": "Follicles" + "label": "Follicles", + "default": false }, { "type": "boolean", "key": "gpuCacheDisplayFilter", - "label": "GPU Cache" + "label": "GPU Cache", + "default": false }, { "type": "boolean", "key": "greasePencils", - "label": "Grease Pencil" + "label": "Grease Pencil", + "default": false }, { "type": "boolean", "key": "grid", - "label": "Grid" + "label": "Grid", + "default": false }, { "type": "boolean", "key": "hairSystems", - "label": "Hair Systems" + "label": "Hair Systems", + "default": true }, { "type": "boolean", "key": "handles", - "label": "Handles" + "label": "Handles", + "default": false }, { "type": "boolean", "key": "headsUpDisplay", - "label": "HUD" + "label": "HUD", + "default": false }, { "type": "boolean", "key": "ikHandles", - "label": "IK Handles" + "label": "IK Handles", + "default": false }, { "type": "boolean", "key": "imagePlane", - "label": "Image Planes" + "label": "Image Planes", + "default": true }, { "type": "boolean", "key": "joints", - "label": "Joints" + "label": "Joints", + "default": false }, { "type": "boolean", "key": "lights", - "label": "Lights" + "label": "Lights", + "default": false }, { "type": "boolean", "key": "locators", - "label": "Locators" + "label": "Locators", + "default": false }, { "type": "boolean", "key": "manipulators", - "label": "Manipulators" + "label": "Manipulators", + "default": false }, { "type": "boolean", "key": "motionTrails", - "label": "Motion Trails" + "label": "Motion Trails", + "default": false }, { "type": "boolean", "key": "nCloths", - "label": "nCloths" + "label": "nCloths", + "default": false }, { "type": "boolean", "key": "nParticles", - "label": "nParticles" + "label": "nParticles", + "default": false }, { "type": "boolean", "key": "nRigids", - "label": "nRigids" + "label": "nRigids", + "default": false }, { "type": "boolean", "key": "controlVertices", - "label": "NURBS CVs" + "label": "NURBS CVs", + "default": false }, { "type": "boolean", "key": "nurbsCurves", - "label": "NURBS Curves" + "label": "NURBS Curves", + "default": false }, { "type": "boolean", "key": "hulls", - "label": "NURBS Hulls" + "label": "NURBS Hulls", + "default": false }, { "type": "boolean", "key": "nurbsSurfaces", - "label": "NURBS Surfaces" + "label": "NURBS Surfaces", + "default": false }, { "type": "boolean", "key": "particleInstancers", - "label": "Particle Instancers" + "label": "Particle Instancers", + "default": false }, { "type": "boolean", "key": "pivots", - "label": "Pivots" + "label": "Pivots", + "default": false }, { "type": "boolean", "key": "planes", - "label": "Planes" + "label": "Planes", + "default": false }, { "type": "boolean", "key": "pluginShapes", - "label": "Plugin Shapes" + "label": "Plugin Shapes", + "default": false }, { "type": "boolean", "key": "polymeshes", - "label": "Polygons" + "label": "Polygons", + "default": true }, { "type": "boolean", "key": "strokes", - "label": "Strokes" + "label": "Strokes", + "default": false }, { "type": "boolean", "key": "subdivSurfaces", - "label": "Subdiv Surfaces" + "label": "Subdiv Surfaces", + "default": false }, { "type": "boolean", "key": "textures", - "label": "Texture Placements" + "label": "Texture Placements", + "default": false } ] }, @@ -600,42 +680,50 @@ { "type": "boolean", "key": "displayGateMask", - "label": "Display Gate Mask" + "label": "Display Gate Mask", + "default": false }, { "type": "boolean", "key": "displayResolution", - "label": "Display Resolution" + "label": "Display Resolution", + "default": false }, { "type": "boolean", "key": "displayFilmGate", - "label": "Display Film Gate" + "label": "Display Film Gate", + "default": false }, { "type": "boolean", "key": "displayFieldChart", - "label": "Display Field Chart" + "label": "Display Field Chart", + "default": false }, { "type": "boolean", "key": "displaySafeAction", - "label": "Display Safe Action" + "label": "Display Safe Action", + "default": false }, { "type": "boolean", "key": "displaySafeTitle", - "label": "Display Safe Title" + "label": "Display Safe Title", + "default": false }, { "type": "boolean", "key": "displayFilmPivot", - "label": "Display Film Pivot" + "label": "Display Film Pivot", + "default": false }, { "type": "boolean", "key": "displayFilmOrigin", - "label": "Display Film Origin" + "label": "Display Film Origin", + "default": false }, { "type": "number", @@ -643,7 +731,8 @@ "label": "Overscan", "decimal": 1, "minimum": 0, - "maximum": 10 + "maximum": 10, + "default": 1 } ] } From 655ae7e7f879cac7127fc754bd472426d09ce9b1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 3 Apr 2023 12:09:26 +0100 Subject: [PATCH 049/139] create review for profiles --- .../maya/plugins/create/create_review.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index e709239ae7..5a1afe9790 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -1,8 +1,14 @@ from collections import OrderedDict +import json + from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io +from openpype.lib.profiles_filtering import filter_profiles +from openpype.client import get_asset_by_name class CreateReview(plugin.Creator): @@ -32,6 +38,30 @@ class CreateReview(plugin.Creator): super(CreateReview, self).__init__(*args, **kwargs) data = OrderedDict(**self.data) + project_name = legacy_io.Session["AVALON_PROJECT"] + profiles = get_project_settings( + project_name + )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + + preset = None + if not profiles: + self.log.warning("No profiles present for extract playblast.") + else: + asset_doc = get_asset_by_name(project_name, data["asset"]) + task_name = legacy_io.Session["AVALON_TASK"] + task_type = asset_doc["data"]["tasks"][task_name]["type"] + + filtering_criteria = { + "hosts": "maya", + "families": "review", + "task_names": task_name, + "task_types": task_type, + "subset": data["subset"] + } + preset = filter_profiles( + profiles, filtering_criteria, logger=self.log + )["capture_preset"] + # Option for using Maya or asset frame range in settings. frame_range = lib.get_frame_range() if self.useMayaTimeline: @@ -40,6 +70,7 @@ class CreateReview(plugin.Creator): data[key] = value data["fps"] = lib.collect_animation_data(fps=True)["fps"] + data["review_width"] = self.Width data["review_height"] = self.Height data["isolate"] = self.isolate @@ -48,4 +79,16 @@ class CreateReview(plugin.Creator): data["transparency"] = self.transparency data["panZoom"] = self.panZoom + if preset: + self.log.info( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) + ) + data["review_width"] = preset["Resolution"]["width"] + data["review_height"] = preset["Resolution"]["height"] + data["isolate"] = preset["Generic"]["isolate_view"] + data["imagePlane"] = preset["Viewport Options"]["imagePlane"] + data["panZoom"] = preset["Generic"]["pan_zoom"] + self.data = data From 0ff0b6b645e1f7293347a24a638bb2afb80556e9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 07:39:53 +0100 Subject: [PATCH 050/139] Move launch logic to host module. --- openpype/hooks/pre_add_last_workfile_arg.py | 13 ---------- .../hosts/maya/hooks/pre_auto_load_plugins.py | 22 +++++++++++++--- .../pre_open_workfile_post_initialization.py | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index df4aa5cc5d..2a35db869a 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -42,18 +42,5 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): self.log.info("Current context does not have any workfile yet.") return - # Determine whether to open workfile post initialization. - if self.host_name == "maya": - keys = [ - "open_workfile_post_initialization", "explicit_plugins_loading" - ] - maya_settings = self.data["project_settings"]["maya"] - values = [maya_settings[k] for k in keys] - if any(values): - self.log.debug("Opening workfile post initialization.") - key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" - self.data["env"][key] = "1" - return - # Add path to workfile to arguments self.launch_context.launch_args.append(last_workfile) diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py index 3c3ddbe4dc..689d7adb4f 100644 --- a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py +++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py @@ -1,15 +1,29 @@ from openpype.lib import PreLaunchHook -class PreAutoLoadPlugins(PreLaunchHook): +class MayaPreAutoLoadPlugins(PreLaunchHook): """Define -noAutoloadPlugins command flag.""" - # Execute before workfile argument. - order = 0 + # Before AddLastWorkfileToLaunchArgs + order = 9 app_groups = ["maya"] def execute(self): + + # Ignore if there's no last workfile to start. + if not self.data.get("start_last_workfile"): + return + maya_settings = self.data["project_settings"]["maya"] - if maya_settings["explicit_plugins_loading"]["enabled"]: + enabled = maya_settings["explicit_plugins_loading"]["enabled"] + if enabled: + # Force disable the `AddLastWorkfileToLaunchArgs`. + self.data.pop("start_last_workfile") + + # Force post initialization so our dedicated plug-in load can run + # prior to Maya opening a scene file. + key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" + self.launch_context.env[key] = "1" + self.log.debug("Explicit plugins loading.") self.launch_context.launch_args.append("-noAutoloadPlugins") diff --git a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py new file mode 100644 index 0000000000..7582ce0591 --- /dev/null +++ b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py @@ -0,0 +1,25 @@ +from openpype.lib import PreLaunchHook + + +class MayaPreOpenWorkfilePostInitialization(PreLaunchHook): + """Define whether open last workfile should run post initialize.""" + + # Before AddLastWorkfileToLaunchArgs. + order = 9 + app_groups = ["maya"] + + def execute(self): + + # Ignore if there's no last workfile to start. + if not self.data.get("start_last_workfile"): + return + + maya_settings = self.data["project_settings"]["maya"] + enabled = maya_settings["open_workfile_post_initialization"] + if enabled: + # Force disable the `AddLastWorkfileToLaunchArgs`. + self.data.pop("start_last_workfile") + + self.log.debug("Opening workfile post initialization.") + key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" + self.launch_context.env[key] = "1" From 7444e33a941498bf040bacd6b3710a34f9f59e92 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 07:53:50 +0100 Subject: [PATCH 051/139] Move review camera validation to validator. --- .../maya/plugins/publish/collect_review.py | 11 +++----- .../maya/plugins/publish/validate_review.py | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_review.py diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 00565c5819..ab730db66e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -31,14 +31,11 @@ class CollectReview(pyblish.api.InstancePlugin): # get cameras members = instance.data['setMembers'] - cameras = cmds.ls(members, long=True, - dag=True, cameras=True) self.log.debug('members: {}'.format(members)) - - # validate required settings - assert len(cameras) == 1, "Not a single camera found in extraction" - camera = cameras[0] - self.log.debug('camera: {}'.format(camera)) + cameras = cmds.ls(members, long=True, dag=True, cameras=True) + camera = None + if cameras: + camera = cameras[0] objectset = instance.context.data['objectsets'] diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py new file mode 100644 index 0000000000..fd11b2147b --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -0,0 +1,25 @@ +from maya import cmds + +import pyblish.api + +from openpype.pipeline.publish import ( + ValidateContentsOrder, PublishValidationError +) + + +class ValidateReview(pyblish.api.InstancePlugin): + """Validate review.""" + + order = ValidateContentsOrder + label = "Validate Review" + families = ["review"] + + def process(self, instance): + cameras = cmds.ls( + instance.data["setMembers"], long=True, dag=True, cameras=True + ) + + if len(cameras) != 1: + raise PublishValidationError( + "Not a single camera found in instance." + ) From c4b887597a3ad9318367553f0151675063ab9560 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 07:54:31 +0100 Subject: [PATCH 052/139] Support review profiles in extraction --- .../maya/plugins/publish/extract_playblast.py | 51 +++++++++++--- .../maya/plugins/publish/extract_thumbnail.py | 67 ++++++++++++++----- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 72b1489522..0556fd9eea 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -7,6 +7,7 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib +from openpype.lib.profiles_filtering import filter_profiles from maya import cmds import pymel.core as pm @@ -34,7 +35,7 @@ class ExtractPlayblast(publish.Extractor): hosts = ["maya"] families = ["review"] optional = True - capture_preset = {} + profiles = None def _capture(self, preset): self.log.info( @@ -47,6 +48,10 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("playblast path {}".format(path)) def process(self, instance): + if not self.profiles: + self.log.warning("No profiles present for Extract Playblast") + return + self.log.info("Extracting capture..") # get scene fps @@ -66,12 +71,35 @@ class ExtractPlayblast(publish.Extractor): # get cameras camera = instance.data["review_camera"] - preset = lib.load_capture_preset(data=self.capture_preset) - # Grab capture presets from the project settings - capture_presets = self.capture_preset + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + capture_preset = filter_profiles( + self.profiles, filtering_criteria, logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset( + data=capture_preset + ) + + # "isolate_view" will already have been applied at creation, so we'll + # ignore it here. + preset.pop("isolate_view") + # Set resolution variables from capture presets - width_preset = capture_presets["Resolution"]["width"] - height_preset = capture_presets["Resolution"]["height"] + width_preset = capture_preset["Resolution"]["width"] + height_preset = capture_preset["Resolution"]["height"] + # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") @@ -122,8 +150,9 @@ class ExtractPlayblast(publish.Extractor): preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Isolate view is requested by having objects in the set besides a - # camera. - if preset.pop("isolate_view", False) and instance.data.get("isolate"): + # camera. If there is only 1 member it'll be the camera because we + # validate to have 1 camera only. + if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: preset["isolate"] = instance.data["setMembers"] # Show/Hide image planes on request. @@ -158,7 +187,7 @@ class ExtractPlayblast(publish.Extractor): ) override_viewport_options = ( - capture_presets["Viewport Options"]["override_viewport_options"] + capture_preset["Viewport Options"]["override_viewport_options"] ) # Force viewer to False in call to capture because we have our own @@ -234,8 +263,8 @@ class ExtractPlayblast(publish.Extractor): collected_files = collected_files[0] representation = { - "name": self.capture_preset["Codec"]["compression"], - "ext": self.capture_preset["Codec"]["compression"], + "name": capture_preset["Codec"]["compression"], + "ext": capture_preset["Codec"]["compression"], "files": collected_files, "stagingDir": stagingdir, "frameStart": start, diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index f2d084b828..4672940254 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -1,11 +1,14 @@ import os import glob import tempfile +import json import capture -from openpype.pipeline import publish +from openpype.pipeline import publish, legacy_io from openpype.hosts.maya.api import lib +from openpype.lib.profiles_filtering import filter_profiles +from openpype.settings import get_project_settings from maya import cmds import pymel.core as pm @@ -24,26 +27,48 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): + project_name = legacy_io.Session["AVALON_PROJECT"] + profiles = get_project_settings( + project_name + )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + + if not profiles: + self.log.warning("No profiles present for Extract Playblast") + return + self.log.info("Extracting capture..") camera = instance.data["review_camera"] - maya_setting = instance.context.data["project_settings"]["maya"] - plugin_setting = maya_setting["publish"]["ExtractPlayblast"] - capture_preset = plugin_setting["capture_preset"] + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + capture_preset = filter_profiles( + profiles, filtering_criteria, logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset( + data=capture_preset + ) + + # "isolate_view" will already have been applied at creation, so we'll + # ignore it here. + preset.pop("isolate_view") + override_viewport_options = ( capture_preset["Viewport Options"]["override_viewport_options"] ) - try: - preset = lib.load_capture_preset(data=capture_preset) - except KeyError as ke: - self.log.error("Error loading capture presets: {}".format(str(ke))) - preset = {} - self.log.info("Using viewport preset: {}".format(preset)) - - # preset["off_screen"] = False - preset["camera"] = camera preset["start_frame"] = instance.data["frameStart"] preset["end_frame"] = instance.data["frameStart"] @@ -59,10 +84,9 @@ class ExtractThumbnail(publish.Extractor): "overscan": 1.0, "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), } - capture_presets = capture_preset # Set resolution variables from capture presets - width_preset = capture_presets["Resolution"]["width"] - height_preset = capture_presets["Resolution"]["height"] + width_preset = capture_preset["Resolution"]["width"] + height_preset = capture_preset["Resolution"]["height"] # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") @@ -111,8 +135,9 @@ class ExtractThumbnail(publish.Extractor): preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Isolate view is requested by having objects in the set besides a - # camera. - if preset.pop("isolate_view", False) and instance.data.get("isolate"): + # camera. If there is only 1 member it'll be the camera because we + # validate to have 1 camera only. + if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: preset["isolate"] = instance.data["setMembers"] # Show or Hide Image Plane @@ -140,6 +165,12 @@ class ExtractThumbnail(publish.Extractor): preset.update(panel_preset) cmds.setFocus(panel) + self.log.info( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) + ) + path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From b960b653300bff616918f14cb1b6f3a65d519056 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 08:13:35 +0100 Subject: [PATCH 053/139] Order display options better. --- .../schemas/schema_maya_capture.json | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index a8961b48dd..3fc92a1b05 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -77,13 +77,24 @@ "type": "label", "label": "Display Options" }, - + { + "type": "boolean", + "key": "override_display", + "label": "Override display options", + "default": true + }, { "type": "color", "key": "background", "label": "Background Color: ", "default": [125, 125, 125, 255] }, + { + "type": "boolean", + "key": "displayGradient", + "label": "Display background gradient", + "default": true + }, { "type": "color", "key": "backgroundBottom", @@ -95,18 +106,6 @@ "key": "backgroundTop", "label": "Background Top: ", "default": [125, 125, 125, 255] - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options", - "default": true - }, - { - "type": "boolean", - "key": "displayGradient", - "label": "Display background gradient", - "default": true } ] }, From bc004453edd0cc42648ce228e1249c9eb05a2700 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 08:33:18 +0100 Subject: [PATCH 054/139] Update openpype/hosts/maya/startup/userSetup.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/startup/userSetup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 4932bf14c0..b28d89e7bd 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -38,8 +38,9 @@ if settings["maya"]["explicit_plugins_loading"]["enabled"]: key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" if bool(int(os.environ.get(key, "0"))): def _log_and_open(): - print("Opening \"{}\"".format(os.environ["AVALON_LAST_WORKFILE"])) - cmds.file(os.environ["AVALON_LAST_WORKFILE"], open=True, force=True) + path = os.environ["AVALON_LAST_WORKFILE"] + print("Opening \"{}\"".format(path)) + cmds.file(path, open=True, force=True) cmds.evalDeferred( _log_and_open, lowestPriority=True From b5e80e565b5de71625531beb7818d34d9b7da1df Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 08:34:42 +0100 Subject: [PATCH 055/139] Update openpype/hosts/maya/startup/userSetup.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/startup/userSetup.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b28d89e7bd..4a00c3dce7 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -20,14 +20,13 @@ if settings["maya"]["explicit_plugins_loading"]["enabled"]: project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) maya_settings = project_settings["maya"] explicit_plugins_loading = maya_settings["explicit_plugins_loading"] - if explicit_plugins_loading["enabled"]: - for plugin in explicit_plugins_loading["plugins_to_load"]: - if plugin["enabled"]: - print("Loading " + plugin["name"]) - try: - cmds.loadPlugin(plugin["name"], quiet=True) - except RuntimeError as e: - print(e) + for plugin in explicit_plugins_loading["plugins_to_load"]: + if plugin["enabled"]: + print("Loading plug-in: " + plugin["name"]) + try: + cmds.loadPlugin(plugin["name"], quiet=True) + except RuntimeError as e: + print(e) cmds.evalDeferred( _explicit_load_plugins, From 0c626f54c5aa69730692f50a6de3123a555d3419 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 08:52:53 +0100 Subject: [PATCH 056/139] Refactor settings variables. --- openpype/hosts/maya/startup/userSetup.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 4a00c3dce7..b58ebb0f7f 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -12,14 +12,12 @@ install_host(host) print("Starting OpenPype usersetup...") -settings = get_project_settings(os.environ['AVALON_PROJECT']) +project_settings = get_project_settings(os.environ['AVALON_PROJECT']) # Loading plugins explicitly. -if settings["maya"]["explicit_plugins_loading"]["enabled"]: +explicit_plugins_loading = project_settings["maya"]["explicit_plugins_loading"] +if explicit_plugins_loading["enabled"]: def _explicit_load_plugins(): - project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) - maya_settings = project_settings["maya"] - explicit_plugins_loading = maya_settings["explicit_plugins_loading"] for plugin in explicit_plugins_loading["plugins_to_load"]: if plugin["enabled"]: print("Loading plug-in: " + plugin["name"]) @@ -46,7 +44,7 @@ if bool(int(os.environ.get(key, "0"))): ) # Build a shelf. -shelf_preset = settings['maya'].get('project_shelf') +shelf_preset = project_settings['maya'].get('project_shelf') if shelf_preset: project = os.environ["AVALON_PROJECT"] From 4b3f96af5e41ccc29461fd5e0fec9306a062edd1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 08:53:08 +0100 Subject: [PATCH 057/139] Comment deferred evaluation --- openpype/hosts/maya/startup/userSetup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b58ebb0f7f..ae6a999d98 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -26,6 +26,8 @@ if explicit_plugins_loading["enabled"]: except RuntimeError as e: print(e) + # We need to load plugins deferred as loading them directly does not work + # correctly due to Maya's initialization. cmds.evalDeferred( _explicit_load_plugins, lowestPriority=True From 2d6d1ba88200fbf0cd9813dbe8b56553a86c6c55 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 10:11:20 +0100 Subject: [PATCH 058/139] Fix OP-5542 # Traceback (most recent call last): # File "C:\Users\florianbehr\AppData\Local\pypeclub\openpype\3.15\openpype-v3.15.4-thescope230404\openpype\hosts\maya\tools\mayalookassigner\app.py", line 272, in on_process_selected # nodes = list(set(item["nodes"]).difference(arnold_standins)) # UnboundLocalError: local variable 'arnold_standins' referenced before assignment --- openpype/hosts/maya/tools/mayalookassigner/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index 2a8775fff6..4619c80913 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -263,14 +263,14 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) + + nodes = list(set(item["nodes"]).difference(arnold_standins)) else: self.echo( "Could not assign to aiStandIn because mtoa plugin is not " "loaded." ) - nodes = list(set(item["nodes"]).difference(arnold_standins)) - # Assign look if nodes: assign_look_by_version(nodes, version_id=version["_id"]) From 94cd27fbc27d1c11a1b2fd0edb193257fc2717e9 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 10:25:35 +0100 Subject: [PATCH 059/139] Update openpype/hosts/maya/tools/mayalookassigner/app.py --- openpype/hosts/maya/tools/mayalookassigner/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index 4619c80913..a8d0f243e9 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -263,7 +263,6 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) - nodes = list(set(item["nodes"]).difference(arnold_standins)) else: self.echo( From 912f757390a93df678bb964c6beb3665bc5fb08d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:31:27 +0100 Subject: [PATCH 060/139] Update openpype/hosts/maya/plugins/create/create_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 5a1afe9790..eeccc5b21e 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -38,7 +38,7 @@ class CreateReview(plugin.Creator): super(CreateReview, self).__init__(*args, **kwargs) data = OrderedDict(**self.data) - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = get_current_project_name() profiles = get_project_settings( project_name )["maya"]["publish"]["ExtractPlayblast"]["profiles"] From 462d3247e82725c9bfaf083b63c240441de5ef40 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:42:40 +0100 Subject: [PATCH 061/139] Update openpype/hosts/maya/plugins/create/create_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index eeccc5b21e..1eb8e421a1 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -48,7 +48,7 @@ class CreateReview(plugin.Creator): self.log.warning("No profiles present for extract playblast.") else: asset_doc = get_asset_by_name(project_name, data["asset"]) - task_name = legacy_io.Session["AVALON_TASK"] + task_name = get_current_task_name() task_type = asset_doc["data"]["tasks"][task_name]["type"] filtering_criteria = { From 54a135a1284e72330dcf57e7c70d45fef3de5ce8 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:43:01 +0100 Subject: [PATCH 062/139] Update openpype/hosts/maya/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 2daea7f3eb..ca08016fab 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -26,7 +26,7 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = instance.context.data["projectName"] profiles = get_project_settings( project_name )["maya"]["publish"]["ExtractPlayblast"]["profiles"] From c969120d15a9dab03548f538c2ba662ced596166 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:44:11 +0100 Subject: [PATCH 063/139] Update openpype/hosts/maya/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index ca08016fab..038a3c0c7f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -27,7 +27,8 @@ class ExtractThumbnail(publish.Extractor): def process(self, instance): project_name = instance.context.data["projectName"] - profiles = get_project_settings( + project_settings = instance.context.data["project_settings"] + profiles = project_settings["maya"]["publish"]["ExtractPlayblast"]["profiles"] project_name )["maya"]["publish"]["ExtractPlayblast"]["profiles"] From a3d358a661b9c32dea6cce3c4b6b0e4dcd010bbb Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:44:35 +0100 Subject: [PATCH 064/139] Update openpype/settings/entities/color_entity.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/entities/color_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index a542f2fa38..e9a2136754 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -11,10 +11,10 @@ class ColorEntity(InputEntity): def _item_initialization(self): self.valid_value_types = (list, ) + self.use_alpha = self.schema_data.get("use_alpha", True) self.value_on_not_set = self.convert_to_valid_type( self.schema_data.get("default", [0, 0, 0, 255]) ) - self.use_alpha = self.schema_data.get("use_alpha", True) def set_override_state(self, *args, **kwargs): super(ColorEntity, self).set_override_state(*args, **kwargs) From c290422fcde6077f744e7d14593076412c79f991 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:44:48 +0100 Subject: [PATCH 065/139] Update openpype/settings/entities/color_entity.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/entities/color_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index e9a2136754..f838a6b0ad 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -66,6 +66,6 @@ class ColorEntity(InputEntity): new_value.append(item) # Make sure - if hasattr(self, "use_alpha") and not self.use_alpha: + if not self.use_alpha: new_value[3] = 255 return new_value From d8d2a317ac24c3618d0a99ee5235b3b71e1718d7 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:45:20 +0100 Subject: [PATCH 066/139] Update openpype/settings/entities/color_entity.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> From 890d88908c20e97b384e02bb88d605c22804934f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:46:45 +0100 Subject: [PATCH 067/139] BigRoy feedback --- .../hosts/maya/plugins/publish/collect_review.py | 9 ++++----- .../hosts/maya/plugins/publish/validate_review.py | 7 ++----- .../settings/defaults/project_settings/maya.json | 6 ++++-- .../projects_schema/schemas/schema_maya_capture.json | 12 ++++++------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 858ee24026..0b3799ac13 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -31,9 +31,6 @@ class CollectReview(pyblish.api.InstancePlugin): members = instance.data['setMembers'] self.log.debug('members: {}'.format(members)) cameras = cmds.ls(members, long=True, dag=True, cameras=True) - camera = None - if cameras: - camera = cameras[0] context = instance.context objectset = context.data['objectsets'] @@ -64,7 +61,8 @@ class CollectReview(pyblish.api.InstancePlugin): else: data['families'] = ['review'] - data['review_camera'] = camera + data["cameras"] = cameras + data['review_camera'] = cameras[0] if cameras else None data['frameStartFtrack'] = instance.data["frameStartHandle"] data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] @@ -98,7 +96,8 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug("Existing subsets found, keep legacy name.") instance.data['subset'] = legacy_subset_name - instance.data['review_camera'] = camera + instance.data["cameras"] = cameras + instance.data['review_camera'] = cameras[0] if cameras else None instance.data['frameStartFtrack'] = \ instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py index 7e9b86c64f..68e8c4a74a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review.py +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -15,9 +15,7 @@ class ValidateReview(pyblish.api.InstancePlugin): families = ["review"] def process(self, instance): - cameras = cmds.ls( - instance.data["setMembers"], long=True, dag=True, cameras=True - ) + cameras = instance.data["cameras"] # validate required settings if len(cameras) == 0: @@ -31,5 +29,4 @@ class ValidateReview(pyblish.api.InstancePlugin): "Cameras found: {}".format(instance, ", ".join(cameras)) ) - camera = cameras[0] - self.log.debug('camera: {}'.format(camera)) + self.log.debug('camera: {}'.format(instance.data["review_camera"])) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a54c869939..24d55de1fd 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -874,7 +874,6 @@ "dynamics": false, "fluids": false, "follicles": false, - "gpuCacheDisplayFilter": false, "greasePencils": false, "grid": false, "hairSystems": true, @@ -901,7 +900,10 @@ "polymeshes": true, "strokes": false, "subdivSurfaces": false, - "textures": false + "textures": false, + "pluginObjects": { + "gpuCacheDisplayFilter": false + } }, "Camera Options": { "displayGateMask": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 3fc92a1b05..1909a20cf5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -505,12 +505,6 @@ "label": "Follicles", "default": false }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "GPU Cache", - "default": false - }, { "type": "boolean", "key": "greasePencils", @@ -672,6 +666,12 @@ "key": "textures", "label": "Texture Placements", "default": false + }, + { + "type": "dict-modifiable", + "key": "pluginObjects", + "label": "Plugin Objects", + "object_type": "boolean" } ] }, From a5918bc3f8116309c2a5bd2d790686eacb2e63bb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:50:05 +0100 Subject: [PATCH 068/139] Move preset debug log behind OPENPYPE_DEBUG --- openpype/hosts/maya/plugins/create/create_review.py | 10 ++++++---- .../hosts/maya/plugins/publish/extract_playblast.py | 9 +++++---- .../hosts/maya/plugins/publish/extract_thumbnail.py | 9 +++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 5a1afe9790..786c795a1a 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -1,3 +1,4 @@ +import os from collections import OrderedDict import json @@ -80,11 +81,12 @@ class CreateReview(plugin.Creator): data["panZoom"] = self.panZoom if preset: - self.log.info( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - ) data["review_width"] = preset["Resolution"]["width"] data["review_height"] = preset["Resolution"]["height"] data["isolate"] = preset["Generic"]["isolate_view"] diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 7787c1df7f..81007520a8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -37,11 +37,12 @@ class ExtractPlayblast(publish.Extractor): profiles = None def _capture(self, preset): - self.log.info( - "Using preset:\n{}".format( - json.dumps(preset, sort_keys=True, indent=4) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - ) path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 2daea7f3eb..ee64c11ca4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -164,11 +164,12 @@ class ExtractThumbnail(publish.Extractor): preset.update(panel_preset) cmds.setFocus(panel) - self.log.info( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - ) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 70c9c534f017fe3281547f9248a9bb41c1bcb765 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:57:06 +0100 Subject: [PATCH 069/139] Hound --- openpype/hosts/maya/plugins/create/create_review.py | 2 +- .../hosts/maya/plugins/publish/extract_thumbnail.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 75a1a5bf08..594faa7978 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -7,7 +7,7 @@ from openpype.hosts.maya.api import ( plugin ) from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name, get_current_task_name from openpype.lib.profiles_filtering import filter_profiles from openpype.client import get_asset_by_name diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 67d085e2f5..cf0f80fa15 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -5,10 +5,9 @@ import json import capture -from openpype.pipeline import publish, legacy_io +from openpype.pipeline import publish from openpype.hosts.maya.api import lib from openpype.lib.profiles_filtering import filter_profiles -from openpype.settings import get_project_settings from maya import cmds @@ -26,11 +25,8 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - project_name = instance.context.data["projectName"] - project_settings = instance.context.data["project_settings"] - profiles = project_settings["maya"]["publish"]["ExtractPlayblast"]["profiles"] - project_name - )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + maya_settings = instance.context.data["project_settings"]["maya"] + profiles = maya_settings["publish"]["ExtractPlayblast"]["profiles"] if not profiles: self.log.warning("No profiles present for Extract Playblast") From 56fc69a9c98639be06a18c2a6a6e74ee05386744 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:58:40 +0100 Subject: [PATCH 070/139] Hound --- openpype/hosts/maya/plugins/publish/validate_review.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py index 68e8c4a74a..346fb54ac4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review.py +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -1,5 +1,3 @@ -from maya import cmds - import pyblish.api from openpype.pipeline.publish import ( From 09f5e3ecc1eb067b60524c66618cbce0e6514e86 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 31 Mar 2023 14:43:10 +0200 Subject: [PATCH 071/139] remove placeholder parent to root at cleanup --- .../maya/api/workfile_template_builder.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 4bee0664ef..d65e4c74d2 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -234,26 +234,10 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): return self.get_load_plugin_options(options) def cleanup_placeholder(self, placeholder, failed): - """Hide placeholder, parent them to root - add them to placeholder set and register placeholder's parent - to keep placeholder info available for future use + """Hide placeholder, add them to placeholder set """ - node = placeholder._scene_identifier - node_parent = placeholder.data["parent"] - if node_parent: - cmds.setAttr(node + ".parent", node_parent, type="string") - if cmds.getAttr(node + ".index") < 0: - cmds.setAttr(node + ".index", placeholder.data["index"]) - - holding_sets = cmds.listSets(object=node) - if holding_sets: - for set in holding_sets: - cmds.sets(node, remove=set) - - if cmds.listRelatives(node, p=True): - node = cmds.parent(node, world=True)[0] cmds.sets(node, addElement=PLACEHOLDER_SET) cmds.hide(node) cmds.setAttr(node + ".hiddenInOutliner", True) @@ -286,8 +270,6 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): elif not cmds.sets(root, q=True): return - if placeholder.data["parent"]: - cmds.parent(nodes_to_parent, placeholder.data["parent"]) # Move loaded nodes to correct index in outliner hierarchy placeholder_form = cmds.xform( placeholder.scene_identifier, From 9aa8aa469fc3a81e714207809af786b520043bf6 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 31 Mar 2023 14:44:18 +0200 Subject: [PATCH 072/139] fix missing var standard --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 7054658c64..376157d210 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1216,7 +1216,7 @@ def get_representations( version_ids=version_ids, context_filters=context_filters, names_by_version_ids=names_by_version_ids, - standard=True, + standard=standard, archived=archived, fields=fields ) From 9be576c2147e712db4d257f95c187e951eca40ec Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 31 Mar 2023 14:45:49 +0200 Subject: [PATCH 073/139] fix linked asset import --- .../workfile/workfile_template_builder.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 0ce59de8ad..a3d7340367 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -158,7 +158,7 @@ class AbstractTemplateBuilder(object): def linked_asset_docs(self): if self._linked_asset_docs is None: self._linked_asset_docs = get_linked_assets( - self.current_asset_doc + self.project_name, self.current_asset_doc ) return self._linked_asset_docs @@ -1151,13 +1151,10 @@ class PlaceholderItem(object): return self._log def __repr__(self): - name = None - if hasattr("name", self): - name = self.name - if hasattr("_scene_identifier ", self): - name = self._scene_identifier - - return "< {} {} >".format(self.__class__.__name__, name) + return "< {} {} >".format( + self.__class__.__name__, + self._scene_identifier + ) @property def order(self): @@ -1419,16 +1416,7 @@ class PlaceholderLoadMixin(object): "family": [placeholder.data["family"]] } - elif builder_type != "linked_asset": - context_filters = { - "asset": [re.compile(placeholder.data["asset"])], - "subset": [re.compile(placeholder.data["subset"])], - "hierarchy": [re.compile(placeholder.data["hierarchy"])], - "representation": [placeholder.data["representation"]], - "family": [placeholder.data["family"]] - } - - else: + elif builder_type == "linked_asset": asset_regex = re.compile(placeholder.data["asset"]) linked_asset_names = [] for asset_doc in linked_asset_docs: @@ -1444,6 +1432,15 @@ class PlaceholderLoadMixin(object): "family": [placeholder.data["family"]], } + else: + context_filters = { + "asset": [re.compile(placeholder.data["asset"])], + "subset": [re.compile(placeholder.data["subset"])], + "hierarchy": [re.compile(placeholder.data["hierarchy"])], + "representation": [placeholder.data["representation"]], + "family": [placeholder.data["family"]] + } + return list(get_representations( project_name, context_filters=context_filters From 6f8f61fb4a47e26c67180bf84a169baff45df4f2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 15:10:34 +0100 Subject: [PATCH 074/139] Reinstate settings backwards compatibility. --- .../defaults/project_settings/maya.json | 123 ++++ .../schemas/schema_maya_capture.json | 625 ++++++++++++++++++ 2 files changed, 748 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 24d55de1fd..234a02c6d4 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -789,6 +789,129 @@ "validate_shapes": true }, "ExtractPlayblast": { + "capture_preset": { + "Codec": { + "compression": "png", + "format": "image", + "quality": 95 + }, + "Display Options": { + "override_display": true, + "background": [ + 125, + 125, + 125, + 255 + ], + "backgroundBottom": [ + 125, + 125, + 125, + 255 + ], + "backgroundTop": [ + 125, + 125, + 125, + 255 + ], + "displayGradient": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true, + "pan_zoom": false + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1920, + "height": 1080 + }, + "Viewport Options": { + "override_viewport_options": true, + "displayLights": "default", + "displayTextures": true, + "textureMaxResolution": 1024, + "renderDepthOfField": true, + "shadows": true, + "twoSidedLighting": true, + "lineAAEnable": true, + "multiSample": 8, + "useDefaultMaterial": false, + "wireframeOnShaded": false, + "xray": false, + "jointXray": false, + "backfaceCulling": false, + "ssaoEnable": false, + "ssaoAmount": 1, + "ssaoRadius": 16, + "ssaoFilterRadius": 16, + "ssaoSamples": 16, + "fogging": false, + "hwFogFalloff": "0", + "hwFogDensity": 0.0, + "hwFogStart": 0, + "hwFogEnd": 100, + "hwFogAlpha": 0, + "hwFogColorR": 1.0, + "hwFogColorG": 1.0, + "hwFogColorB": 1.0, + "motionBlurEnable": false, + "motionBlurSampleCount": 8, + "motionBlurShutterOpenFraction": 0.2, + "cameras": false, + "clipGhosts": false, + "deformers": false, + "dimensions": false, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "greasePencils": false, + "grid": false, + "hairSystems": true, + "handles": false, + "headsUpDisplay": false, + "ikHandles": false, + "imagePlane": true, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "controlVertices": false, + "nurbsCurves": false, + "hulls": false, + "nurbsSurfaces": false, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": true, + "strokes": false, + "subdivSurfaces": false, + "textures": false, + "pluginObjects": { + "gpuCacheDisplayFilter": false + } + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } + }, "profiles": [ { "task_types": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1909a20cf5..19c169df9c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -4,6 +4,631 @@ "key": "ExtractPlayblast", "label": "Extract Playblast settings", "children": [ + { + "type": "dict", + "key": "capture_preset", + "label": "DEPRECATED! Please use \"Profiles\" below.", + "collapsed": false, + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Encoding" + }, + { + "type": "text", + "key": "format", + "label": "Format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, + + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + }, + { + "type": "color", + "key": "background", + "label": "Background Color: " + }, + { + "type": "color", + "key": "backgroundBottom", + "label": "Background Bottom: " + }, + { + "type": "color", + "key": "backgroundTop", + "label": "Background Top: " + }, + { + "type": "boolean", + "key": "displayGradient", + "label": "Display background gradient" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + }, + { + "type": "boolean", + "key": "pan_zoom", + "label": " 2D Pan/Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "enum", + "key": "rendererName", + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "Override Viewport Options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "boolean", + "key": "displayTextures", + "label": "Display Textures" + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Display" + }, + { + "type":"boolean", + "key": "renderDepthOfField", + "label": "Depth of Field" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "lineAAEnable", + "label": "Enable Anti-Aliasing" + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "useDefaultMaterial", + "label": "Use Default Material" + }, + { + "type": "boolean", + "key": "wireframeOnShaded", + "label": "Wireframe On Shaded" + }, + { + "type": "boolean", + "key": "xray", + "label": "X-Ray" + }, + { + "type": "boolean", + "key": "jointXray", + "label": "X-Ray Joints" + }, + { + "type": "boolean", + "key": "backfaceCulling", + "label": "Backface Culling" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "number", + "key": "ssaoAmount", + "label": "SSAO Amount" + }, + { + "type": "number", + "key": "ssaoRadius", + "label": "SSAO Radius" + }, + { + "type": "number", + "key": "ssaoFilterRadius", + "label": "SSAO Filter Radius", + "decimal": 0, + "minimum": 1, + "maximum": 32 + }, + { + "type": "number", + "key": "ssaoSamples", + "label": "SSAO Samples", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "fogging", + "label": "Enable Hardware Fog" + }, + { + "type": "enum", + "key": "hwFogFalloff", + "label": "Hardware Falloff", + "enum_items": [ + { "0": "Linear"}, + { "1": "Exponential"}, + { "2": "Exponential Squared"} + ] + }, + { + "type": "number", + "key": "hwFogDensity", + "label": "Fog Density", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogStart", + "label": "Fog Start" + }, + { + "type": "number", + "key": "hwFogEnd", + "label": "Fog End" + }, + { + "type": "number", + "key": "hwFogAlpha", + "label": "Fog Alpha" + }, + { + "type": "number", + "key": "hwFogColorR", + "label": "Fog Color R", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorG", + "label": "Fog Color G", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorB", + "label": "Fog Color B", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "motionBlurEnable", + "label": "Enable Motion Blur" + }, + { + "type": "number", + "key": "motionBlurSampleCount", + "label": "Motion Blur Sample Count", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "number", + "key": "motionBlurShutterOpenFraction", + "label": "Shutter Open Fraction", + "decimal": 3, + "minimum": 0.01, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Show" + }, + { + "type": "boolean", + "key": "cameras", + "label": "Cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "Clip Ghosts" + }, + { + "type": "boolean", + "key": "deformers", + "label": "Deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "Dimensions" + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "Dynamic Constraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "Dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "Fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "Follicles" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "Grease Pencil" + }, + { + "type": "boolean", + "key": "grid", + "label": "Grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "Hair Systems" + }, + { + "type": "boolean", + "key": "handles", + "label": "Handles" + }, + { + "type": "boolean", + "key": "headsUpDisplay", + "label": "HUD" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "IK Handles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "Image Planes" + }, + { + "type": "boolean", + "key": "joints", + "label": "Joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "Lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "Locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "Manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "Motion Trails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "NURBS CVs" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "NURBS Curves" + }, + { + "type": "boolean", + "key": "hulls", + "label": "NURBS Hulls" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "NURBS Surfaces" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "Particle Instancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "Pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "Planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "Plugin Shapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "Polygons" + }, + { + "type": "boolean", + "key": "strokes", + "label": "Strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "Subdiv Surfaces" + }, + { + "type": "boolean", + "key": "textures", + "label": "Texture Placements" + }, + { + "type": "dict-modifiable", + "key": "pluginObjects", + "label": "Plugin Objects", + "object_type": "boolean" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "Display Gate Mask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "Display Resolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "Display Film Gate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "Display Field Chart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "Display Safe Action" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "Display Safe Title" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "Display Film Pivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "Display Film Origin" + }, + { + "type": "number", + "key": "overscan", + "label": "Overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + }, { "type": "list", "key": "profiles", From 9ed7e00254f42544a51702bcc3fe86e4169f05ff Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 16:59:55 +0100 Subject: [PATCH 075/139] Fix missing camera variable. --- openpype/hosts/maya/plugins/publish/collect_review.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 0b3799ac13..3652c0aa40 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -31,6 +31,7 @@ class CollectReview(pyblish.api.InstancePlugin): members = instance.data['setMembers'] self.log.debug('members: {}'.format(members)) cameras = cmds.ls(members, long=True, dag=True, cameras=True) + camera = cameras[0] if cameras else None context = instance.context objectset = context.data['objectsets'] @@ -62,7 +63,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['families'] = ['review'] data["cameras"] = cameras - data['review_camera'] = cameras[0] if cameras else None + data['review_camera'] = camera data['frameStartFtrack'] = instance.data["frameStartHandle"] data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] @@ -97,7 +98,7 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['subset'] = legacy_subset_name instance.data["cameras"] = cameras - instance.data['review_camera'] = cameras[0] if cameras else None + instance.data['review_camera'] = camera instance.data['frameStartFtrack'] = \ instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ @@ -145,6 +146,9 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["audio"] = audio_data # Collect focal length. + if camera is None: + return + attr = camera + ".focalLength" if get_attribute_input(attr): start = instance.data["frameStart"] From abea98091aa22f1e6949b70832fd654f953375d5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 17:00:14 +0100 Subject: [PATCH 076/139] Code cosmetics --- openpype/hosts/maya/plugins/create/create_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 594faa7978..156f1e3461 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -45,9 +45,7 @@ class CreateReview(plugin.Creator): )["maya"]["publish"]["ExtractPlayblast"]["profiles"] preset = None - if not profiles: - self.log.warning("No profiles present for extract playblast.") - else: + if profiles: asset_doc = get_asset_by_name(project_name, data["asset"]) task_name = get_current_task_name() task_type = asset_doc["data"]["tasks"][task_name]["type"] @@ -62,6 +60,8 @@ class CreateReview(plugin.Creator): preset = filter_profiles( profiles, filtering_criteria, logger=self.log )["capture_preset"] + else: + self.log.warning("No profiles present for extract playblast.") # Option for using Maya or asset frame range in settings. frame_range = lib.get_frame_range() From f2f42fad308eebdcad7d9ee3267e26a1772bfb33 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 17:03:57 +0100 Subject: [PATCH 077/139] Reinstate backwards compatibility for publishing. --- .../maya/plugins/publish/extract_playblast.py | 20 +++++------ .../maya/plugins/publish/extract_thumbnail.py | 33 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 81007520a8..a9f5062c48 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -34,6 +34,7 @@ class ExtractPlayblast(publish.Extractor): hosts = ["maya"] families = ["review"] optional = True + capture_preset = {} profiles = None def _capture(self, preset): @@ -48,10 +49,6 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("playblast path {}".format(path)) def process(self, instance): - if not self.profiles: - self.log.warning("No profiles present for Extract Playblast") - return - self.log.info("Extracting capture..") # get scene fps @@ -85,12 +82,15 @@ class ExtractPlayblast(publish.Extractor): "task_types": task_type, "subset": subset } - capture_preset = filter_profiles( - self.profiles, filtering_criteria, logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset( - data=capture_preset - ) + capture_preset = self.capture_preset + preset = lib.load_capture_preset(data=self.capture_preset) + if self.profiles: + capture_preset = filter_profiles( + self.profiles, filtering_criteria, logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset(data=capture_preset) + else: + self.log.warning("No profiles present for Extract Playblast") # "isolate_view" will already have been applied at creation, so we'll # ignore it here. diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index cf0f80fa15..8d635d0df2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -25,13 +25,6 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - maya_settings = instance.context.data["project_settings"]["maya"] - profiles = maya_settings["publish"]["ExtractPlayblast"]["profiles"] - - if not profiles: - self.log.warning("No profiles present for Extract Playblast") - return - self.log.info("Extracting capture..") camera = instance.data["review_camera"] @@ -50,12 +43,26 @@ class ExtractThumbnail(publish.Extractor): "task_types": task_type, "subset": subset } - capture_preset = filter_profiles( - profiles, filtering_criteria, logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset( - data=capture_preset - ) + + maya_settings = instance.context.data["project_settings"]["maya"] + plugin_settings = maya_settings["publish"]["ExtractPlayblast"] + + capture_preset = plugin_settings["capture_preset"] + preset = {} + try: + preset = lib.load_capture_preset(data=capture_preset) + except KeyError as ke: + self.log.error("Error loading capture presets: {}".format(str(ke))) + + if plugin_settings["profiles"]: + capture_preset = filter_profiles( + plugin_settings["profiles"], + filtering_criteria, + logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset(data=capture_preset) + else: + self.log.warning("No profiles present for Extract Playblast") # "isolate_view" will already have been applied at creation, so we'll # ignore it here. From 0b3802d9f27c1def5a8dd07553ca205f88d02a85 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 17:08:19 +0100 Subject: [PATCH 078/139] Remove default profile. --- .../defaults/project_settings/maya.json | 131 +----------------- 1 file changed, 1 insertion(+), 130 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 234a02c6d4..8c817b5ba0 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -912,136 +912,7 @@ "overscan": 1.0 } }, - "profiles": [ - { - "task_types": [], - "task_names": [], - "subsets": [], - "capture_preset": { - "Codec": { - "compression": "png", - "format": "image", - "quality": 95 - }, - "Display Options": { - "background": [ - 125, - 125, - 125, - 255 - ], - "backgroundBottom": [ - 125, - 125, - 125, - 255 - ], - "backgroundTop": [ - 125, - 125, - 125, - 255 - ], - "override_display": true, - "displayGradient": true - }, - "Generic": { - "isolate_view": true, - "off_screen": true, - "pan_zoom": false - }, - "Renderer": { - "rendererName": "vp2Renderer" - }, - "Resolution": { - "width": 0, - "height": 0 - }, - "Viewport Options": { - "override_viewport_options": true, - "displayLights": "default", - "displayTextures": true, - "textureMaxResolution": 1024, - "renderDepthOfField": true, - "shadows": true, - "twoSidedLighting": true, - "lineAAEnable": true, - "multiSample": 8, - "useDefaultMaterial": false, - "wireframeOnShaded": false, - "xray": false, - "jointXray": false, - "backfaceCulling": false, - "ssaoEnable": false, - "ssaoAmount": 1, - "ssaoRadius": 16, - "ssaoFilterRadius": 16, - "ssaoSamples": 16, - "fogging": false, - "hwFogFalloff": "0", - "hwFogDensity": 0.0, - "hwFogStart": 0, - "hwFogEnd": 100, - "hwFogAlpha": 0, - "hwFogColorR": 1.0, - "hwFogColorG": 1.0, - "hwFogColorB": 1.0, - "motionBlurEnable": false, - "motionBlurSampleCount": 0, - "motionBlurShutterOpenFraction": 0.2, - "cameras": false, - "clipGhosts": false, - "deformers": false, - "dimensions": false, - "dynamicConstraints": false, - "dynamics": false, - "fluids": false, - "follicles": false, - "greasePencils": false, - "grid": false, - "hairSystems": true, - "handles": false, - "headsUpDisplay": false, - "ikHandles": false, - "imagePlane": true, - "joints": false, - "lights": false, - "locators": false, - "manipulators": false, - "motionTrails": false, - "nCloths": false, - "nParticles": false, - "nRigids": false, - "controlVertices": false, - "nurbsCurves": false, - "hulls": false, - "nurbsSurfaces": false, - "particleInstancers": false, - "pivots": false, - "planes": false, - "pluginShapes": false, - "polymeshes": true, - "strokes": false, - "subdivSurfaces": false, - "textures": false, - "pluginObjects": { - "gpuCacheDisplayFilter": false - } - }, - "Camera Options": { - "displayGateMask": false, - "displayResolution": false, - "displayFilmGate": false, - "displayFieldChart": false, - "displaySafeAction": false, - "displaySafeTitle": false, - "displayFilmPivot": false, - "displayFilmOrigin": false, - "overscan": 1.0 - } - } - } - ] + "profiles": [] }, "ExtractMayaSceneRaw": { "enabled": true, From 9c9a1c08399184d863f8f5be4c6688bf183e488d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 6 Apr 2023 16:47:46 +0100 Subject: [PATCH 079/139] Discard vray proxies and aistandin from same variable. --- openpype/hosts/maya/tools/mayalookassigner/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index a8d0f243e9..fe7f460588 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -250,7 +250,7 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): if vp in nodes: vrayproxy_assign_look(vp, subset_name) - nodes = list(set(item["nodes"]).difference(vray_proxies)) + nodes = list(set(nodes).difference(vray_proxies)) else: self.echo( "Could not assign to VRayProxy because vrayformaya plugin " @@ -260,10 +260,12 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): # Assign Arnold Standin look. if cmds.pluginInfo("mtoa", query=True, loaded=True): arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) + for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) - nodes = list(set(item["nodes"]).difference(arnold_standins)) + + nodes = list(set(nodes).difference(arnold_standins)) else: self.echo( "Could not assign to aiStandIn because mtoa plugin is not " From 0cb647c0b20732ced4c1229429be4d256a98c792 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 6 Apr 2023 16:48:40 +0100 Subject: [PATCH 080/139] Hound --- openpype/hosts/maya/tools/mayalookassigner/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index fe7f460588..13da999c2d 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -260,11 +260,11 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): # Assign Arnold Standin look. if cmds.pluginInfo("mtoa", query=True, loaded=True): arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) - + for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) - + nodes = list(set(nodes).difference(arnold_standins)) else: self.echo( From 33a52f1a5282ba308aeb2861a79f8184642ef362 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 7 Apr 2023 10:43:30 +0100 Subject: [PATCH 081/139] Fix No Lights in project settings. --- .../schemas/projects_schema/schemas/schema_maya_capture.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index a4a986bad8..d468f098e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -176,7 +176,7 @@ { "all": "All Lights"}, { "selected": "Selected Lights"}, { "flat": "Flat Lighting"}, - { "nolights": "No Lights"} + { "none": "No Lights"} ] }, { From 273d87f8b8dec02b80e07da9eea038ac840f319b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 7 Apr 2023 10:44:08 +0100 Subject: [PATCH 082/139] Fix flat lighting and sync labels with project settings. --- openpype/hosts/maya/api/lib.py | 12 ++++++++++-- openpype/hosts/maya/plugins/create/create_review.py | 2 +- .../hosts/maya/plugins/publish/collect_review.py | 4 +--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 931c0f9e5b..f94b32d917 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -112,8 +112,16 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] -DISPLAY_LIGHTS = [ - "project_settings", "default", "all", "selected", "active", "none" +DISPLAY_LIGHTS_VALUES = [ + "project_settings", "default", "all", "selected", "flat", "none" +] +DISPLAY_LIGHTS_LABELS = [ + "Use Project Settings", + "Default Lighting", + "All Lights", + "Selected Lights", + "Flat Lighting", + "No Lights" ] diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index de92bbb6b5..094c9ebf8c 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -47,6 +47,6 @@ class CreateReview(plugin.Creator): data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency data["panZoom"] = self.panZoom - data["displayLights"] = lib.DISPLAY_LIGHTS + data["displayLights"] = lib.DISPLAY_LIGHTS_LABELS self.data = data diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 516a83de64..3ca45deb3a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -150,10 +150,8 @@ class CollectReview(pyblish.api.InstancePlugin): # Convert enum attribute index to string. index = instance.data.get("displayLights", 0) - display_lights = lib.DISPLAY_LIGHTS[index] + display_lights = lib.DISPLAY_LIGHTS_VALUES[index] if display_lights == "project_settings": - # project_settings/maya/publish/ExtractPlayblast/capture_preset - # /Viewport Options/displayLights settings = instance.context.data["project_settings"] settings = settings["maya"]["publish"]["ExtractPlayblast"] settings = settings["capture_preset"]["Viewport Options"] From eeaa7fdc55bf2adbd13ef01808559f21c63179b1 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 11 Apr 2023 10:22:22 +0100 Subject: [PATCH 083/139] Update openpype/hosts/maya/plugins/publish/extract_playblast.py --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index a9f5062c48..b2deb71d0b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -85,12 +85,13 @@ class ExtractPlayblast(publish.Extractor): capture_preset = self.capture_preset preset = lib.load_capture_preset(data=self.capture_preset) if self.profiles: - capture_preset = filter_profiles( + profile = filter_profiles( self.profiles, filtering_criteria, logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset(data=capture_preset) + ) + capture_preset = profile.get("capture_preset) or capture_preset else: self.log.warning("No profiles present for Extract Playblast") + preset = lib.load_capture_preset(data=capture_preset) # "isolate_view" will already have been applied at creation, so we'll # ignore it here. From 97d1829b0faba2ba862bb1beaa997368a75d5b60 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 11 Apr 2023 10:24:18 +0100 Subject: [PATCH 084/139] Update openpype/hosts/maya/plugins/publish/extract_playblast.py --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index b2deb71d0b..13e6fb2f0d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -88,7 +88,7 @@ class ExtractPlayblast(publish.Extractor): profile = filter_profiles( self.profiles, filtering_criteria, logger=self.log ) - capture_preset = profile.get("capture_preset) or capture_preset + capture_preset = profile.get("capture_preset") or capture_preset else: self.log.warning("No profiles present for Extract Playblast") preset = lib.load_capture_preset(data=capture_preset) From 06a94f09370b4e359f909f14a1be862364cbafc4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 11 Apr 2023 21:24:57 +0100 Subject: [PATCH 085/139] BigRoy feedback --- .../maya/plugins/create/create_review.py | 7 +++-- .../maya/plugins/publish/extract_playblast.py | 11 +++++-- .../maya/plugins/publish/extract_thumbnail.py | 31 +++++++++---------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 156f1e3461..972b3a0160 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -42,7 +42,7 @@ class CreateReview(plugin.Creator): project_name = get_current_project_name() profiles = get_project_settings( project_name - )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + )["maya"]["publish"]["ExtractPlayblast"].get("profiles") preset = None if profiles: @@ -57,9 +57,10 @@ class CreateReview(plugin.Creator): "task_types": task_type, "subset": data["subset"] } - preset = filter_profiles( + profile = filter_profiles( profiles, filtering_criteria, logger=self.log - )["capture_preset"] + ) + preset = profile["capture_preset"] if profile else None else: self.log.warning("No profiles present for extract playblast.") diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 13e6fb2f0d..0ce5aa883e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -82,15 +82,20 @@ class ExtractPlayblast(publish.Extractor): "task_types": task_type, "subset": subset } - capture_preset = self.capture_preset - preset = lib.load_capture_preset(data=self.capture_preset) + if self.profiles: profile = filter_profiles( self.profiles, filtering_criteria, logger=self.log ) - capture_preset = profile.get("capture_preset") or capture_preset + capture_preset = profile.get("capture_preset") else: self.log.warning("No profiles present for Extract Playblast") + + # Backward compatibility for deprecated Extract Playblast settings + # without profiles. + if capture_preset is None: + capture_preset = self.capture_preset + preset = lib.load_capture_preset(data=capture_preset) # "isolate_view" will already have been applied at creation, so we'll diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 8d635d0df2..cd4e4694ba 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -44,26 +44,23 @@ class ExtractThumbnail(publish.Extractor): "subset": subset } - maya_settings = instance.context.data["project_settings"]["maya"] - plugin_settings = maya_settings["publish"]["ExtractPlayblast"] - - capture_preset = plugin_settings["capture_preset"] - preset = {} - try: - preset = lib.load_capture_preset(data=capture_preset) - except KeyError as ke: - self.log.error("Error loading capture presets: {}".format(str(ke))) - - if plugin_settings["profiles"]: - capture_preset = filter_profiles( - plugin_settings["profiles"], - filtering_criteria, - logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset(data=capture_preset) + if self.profiles: + profile = filter_profiles( + self.profiles, filtering_criteria, logger=self.log + ) + capture_preset = profile.get("capture_preset") else: self.log.warning("No profiles present for Extract Playblast") + # Backward compatibility for deprecated Extract Playblast settings + # without profiles. + if capture_preset is None: + maya_settings = instance.context.data["project_settings"]["maya"] + plugin_settings = maya_settings["publish"]["ExtractPlayblast"] + capture_preset = plugin_settings["capture_preset"] + + preset = lib.load_capture_preset(data=capture_preset) + # "isolate_view" will already have been applied at creation, so we'll # ignore it here. preset.pop("isolate_view") From ef5658ce09bbc6d96bb45525063915cdff63bc7a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 12 Apr 2023 08:45:53 +0100 Subject: [PATCH 086/139] Refactor fetching capture preset to lib. --- openpype/hosts/maya/api/lib.py | 45 ++++++++++++++ .../maya/plugins/create/create_review.py | 60 ++++++------------- .../maya/plugins/publish/extract_playblast.py | 34 +++-------- .../maya/plugins/publish/extract_thumbnail.py | 36 +++-------- 4 files changed, 78 insertions(+), 97 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 22803a2e3a..a79e7ade0c 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -33,6 +33,7 @@ from openpype.pipeline import ( registered_host, ) from openpype.pipeline.context_tools import get_current_project_asset +from openpype.lib.profiles_filtering import filter_profiles self = sys.modules[__name__] @@ -3812,3 +3813,47 @@ def get_all_children(nodes): iterator.next() # noqa: B305 return list(traversed) + + +def get_capture_preset(task_name, task_type, subset, project_settings, log): + """Get capture preset for playblasting. + + Logic for transitioning from old style capture preset to new capture preset + profiles. + + Args: + task_name (str): Task name. + take_type (str): Task type. + subset (str): Subset name. + project_settings (dict): Project settings. + log (object): Logging object. + """ + filtering_criteria = { + "hosts": "maya", + "families": "review", + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + + plugin_settings = project_settings["maya"]["publish"]["ExtractPlayblast"] + if plugin_settings["profiles"]: + profile = filter_profiles( + plugin_settings["profiles"], + filtering_criteria, + logger=log + ) + capture_preset = profile.get("capture_preset") + else: + log.warning("No profiles present for Extract Playblast") + + # Backward compatibility for deprecated Extract Playblast settings + # without profiles. + if capture_preset is None: + log.debug( + "Falling back to deprecated Extract Playblast capture preset " + "because no new style playblast profiles are defined." + ) + capture_preset = plugin_settings["capture_preset"] + + return capture_preset diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 972b3a0160..eb68bbb257 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -8,7 +8,6 @@ from openpype.hosts.maya.api import ( ) from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name, get_current_task_name -from openpype.lib.profiles_filtering import filter_profiles from openpype.client import get_asset_by_name @@ -40,29 +39,21 @@ class CreateReview(plugin.Creator): data = OrderedDict(**self.data) project_name = get_current_project_name() - profiles = get_project_settings( - project_name - )["maya"]["publish"]["ExtractPlayblast"].get("profiles") - - preset = None - if profiles: - asset_doc = get_asset_by_name(project_name, data["asset"]) - task_name = get_current_task_name() - task_type = asset_doc["data"]["tasks"][task_name]["type"] - - filtering_criteria = { - "hosts": "maya", - "families": "review", - "task_names": task_name, - "task_types": task_type, - "subset": data["subset"] - } - profile = filter_profiles( - profiles, filtering_criteria, logger=self.log + asset_doc = get_asset_by_name(project_name, data["asset"]) + task_name = get_current_task_name() + preset = lib.get_capture_preset( + task_name, + asset_doc["data"]["tasks"][task_name]["type"], + data["subset"], + get_project_settings(project_name), + self.log + ) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - preset = profile["capture_preset"] if profile else None - else: - self.log.warning("No profiles present for extract playblast.") # Option for using Maya or asset frame range in settings. frame_range = lib.get_frame_range() @@ -73,25 +64,12 @@ class CreateReview(plugin.Creator): data["fps"] = lib.collect_animation_data(fps=True)["fps"] - data["review_width"] = self.Width - data["review_height"] = self.Height - data["isolate"] = self.isolate data["keepImages"] = self.keepImages - data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency - data["panZoom"] = self.panZoom - - if preset: - if os.environ.get("OPENPYPE_DEBUG") == "1": - self.log.debug( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) - ) - ) - data["review_width"] = preset["Resolution"]["width"] - data["review_height"] = preset["Resolution"]["height"] - data["isolate"] = preset["Generic"]["isolate_view"] - data["imagePlane"] = preset["Viewport Options"]["imagePlane"] - data["panZoom"] = preset["Generic"]["pan_zoom"] + data["review_width"] = preset["Resolution"]["width"] + data["review_height"] = preset["Resolution"]["height"] + data["isolate"] = preset["Generic"]["isolate_view"] + data["imagePlane"] = preset["Viewport Options"]["imagePlane"] + data["panZoom"] = preset["Generic"]["pan_zoom"] self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0ce5aa883e..78a8106444 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -7,7 +7,6 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from openpype.lib.profiles_filtering import filter_profiles from maya import cmds @@ -68,33 +67,14 @@ class ExtractPlayblast(publish.Extractor): # get cameras camera = instance.data["review_camera"] - host_name = instance.context.data["hostName"] - family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) - task_name = task_data.get("name") - task_type = task_data.get("type") - subset = instance.data["subset"] - - filtering_criteria = { - "hosts": host_name, - "families": family, - "task_names": task_name, - "task_types": task_type, - "subset": subset - } - - if self.profiles: - profile = filter_profiles( - self.profiles, filtering_criteria, logger=self.log - ) - capture_preset = profile.get("capture_preset") - else: - self.log.warning("No profiles present for Extract Playblast") - - # Backward compatibility for deprecated Extract Playblast settings - # without profiles. - if capture_preset is None: - capture_preset = self.capture_preset + capture_preset = lib.get_capture_preset( + task_data.get("name"), + task_data.get("type"), + instance.data["subset"], + instance.context.data["project_settings"], + self.log + ) preset = lib.load_capture_preset(data=capture_preset) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index cd4e4694ba..e2125e7c44 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -7,7 +7,6 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from openpype.lib.profiles_filtering import filter_profiles from maya import cmds @@ -29,35 +28,14 @@ class ExtractThumbnail(publish.Extractor): camera = instance.data["review_camera"] - host_name = instance.context.data["hostName"] - family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) - task_name = task_data.get("name") - task_type = task_data.get("type") - subset = instance.data["subset"] - - filtering_criteria = { - "hosts": host_name, - "families": family, - "task_names": task_name, - "task_types": task_type, - "subset": subset - } - - if self.profiles: - profile = filter_profiles( - self.profiles, filtering_criteria, logger=self.log - ) - capture_preset = profile.get("capture_preset") - else: - self.log.warning("No profiles present for Extract Playblast") - - # Backward compatibility for deprecated Extract Playblast settings - # without profiles. - if capture_preset is None: - maya_settings = instance.context.data["project_settings"]["maya"] - plugin_settings = maya_settings["publish"]["ExtractPlayblast"] - capture_preset = plugin_settings["capture_preset"] + capture_preset = lib.get_capture_preset( + task_data.get("name"), + task_data.get("type"), + instance.data["subset"], + instance.context.data["project_settings"], + self.log + ) preset = lib.load_capture_preset(data=capture_preset) From 1663a309a67e4abc78435d9b7189cc70bcff6160 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 12 Apr 2023 12:06:54 +0100 Subject: [PATCH 087/139] Fix tile rendering --- .../plugins/publish/submit_maya_deadline.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 062732c059..5542435387 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -327,6 +327,11 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info = copy.deepcopy(payload_job_info) plugin_info = copy.deepcopy(payload_plugin_info) + # Force plugin reload for vray cause the region does not get flushed + # between tile renders. + if plugin_info["Renderer"] == "vray": + job_info.ForceReloadPlugin = True + # if we have sequence of files, we need to create tile job for # every frame job_info.TileJob = True @@ -436,6 +441,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): assembly_payloads = [] output_dir = self.job_info.OutputDirectory[0] + config_files = [] for file in assembly_files: frame = re.search(R_FRAME_NUMBER, file).group("frame") @@ -461,6 +467,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): datetime.now().strftime("%Y_%m_%d_%H_%M_%S") ) ) + config_files.append(config_file) try: if not os.path.isdir(output_dir): os.makedirs(output_dir) @@ -469,8 +476,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): self.log.warning("Path is unreachable: " "`{}`".format(output_dir)) - assembly_plugin_info["ConfigFile"] = config_file - with open(config_file, "w") as cf: print("TileCount={}".format(tiles_count), file=cf) print("ImageFileName={}".format(file), file=cf) @@ -479,6 +484,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): print("ImageHeight={}".format( instance.data.get("resolutionHeight")), file=cf) + reversed_y = False + if plugin_info["Renderer"] == "arnold": + reversed_y = True + with open(config_file, "a") as cf: # Need to reverse the order of the y tiles, because image # coordinates are calculated from bottom left corner. @@ -489,7 +498,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance.data.get("resolutionWidth"), instance.data.get("resolutionHeight"), payload_plugin_info["OutputFilePrefix"], - reversed_y=True + reversed_y=reversed_y )[1] for k, v in sorted(tiles.items()): print("{}={}".format(k, v), file=cf) @@ -518,6 +527,11 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance.data["assemblySubmissionJobs"] = assembly_job_ids + # Remove config files to avoid confusion about where data is coming + # from in Deadline. + for config_file in config_files: + os.remove(config_file) + def _get_maya_payload(self, data): job_info = copy.deepcopy(self.job_info) @@ -878,8 +892,6 @@ def _format_tiles( out["PluginInfo"]["RegionRight{}".format(tile)] = right # Tile config - cfg["Tile{}".format(tile)] = new_filename - cfg["Tile{}Tile".format(tile)] = new_filename cfg["Tile{}FileName".format(tile)] = new_filename cfg["Tile{}X".format(tile)] = left cfg["Tile{}Y".format(tile)] = top From 55ccf73ebb7a82ca9e8d8293225f6357426d9707 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Apr 2023 13:31:16 +0200 Subject: [PATCH 088/139] Remove single assembly validation for animation instances --- openpype/hosts/maya/plugins/publish/validate_single_assembly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_single_assembly.py b/openpype/hosts/maya/plugins/publish/validate_single_assembly.py index 8771ca58d1..b768c9c4e8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_single_assembly.py +++ b/openpype/hosts/maya/plugins/publish/validate_single_assembly.py @@ -19,7 +19,7 @@ class ValidateSingleAssembly(pyblish.api.InstancePlugin): order = ValidateContentsOrder hosts = ['maya'] - families = ['rig', 'animation'] + families = ['rig'] label = 'Single Assembly' def process(self, instance): From 9a96a6b2e0c74aadb1561733a1aec857cd63d2cd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Apr 2023 16:47:19 +0100 Subject: [PATCH 089/139] KnownPublishError > PublishValidationError --- openpype/hosts/maya/plugins/publish/validate_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py index 346fb54ac4..12a2e7f86f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review.py +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -1,7 +1,7 @@ import pyblish.api from openpype.pipeline.publish import ( - ValidateContentsOrder, KnownPublishError + ValidateContentsOrder, PublishValidationError ) @@ -17,11 +17,11 @@ class ValidateReview(pyblish.api.InstancePlugin): # validate required settings if len(cameras) == 0: - raise KnownPublishError( + raise PublishValidationError( "No camera found in review instance: {}".format(instance) ) elif len(cameras) > 2: - raise KnownPublishError( + raise PublishValidationError( "Only a single camera is allowed for a review instance but " "more than one camera found in review instance: {}. " "Cameras found: {}".format(instance, ", ".join(cameras)) From 9a5f86ea1b065a7cf847836b8148e2bc4cb79c59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Apr 2023 18:42:50 +0200 Subject: [PATCH 090/139] add review tag to output of extract sequence if instance is marked for review (#4843) --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 1a21715aa2..8a610cf388 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -144,7 +144,7 @@ class ExtractSequence(pyblish.api.Extractor): # Fill tags and new families from project settings tags = [] - if family_lowered == "review": + if "review" in instance.data["families"]: tags.append("review") # Sequence of one frame From a061c897794a5617e746d2c3387bfb8f81d55569 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 14 Apr 2023 08:38:18 +0100 Subject: [PATCH 091/139] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2ac9f06fcd..46f423023f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3871,6 +3871,8 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): project_settings (dict): Project settings. log (object): Logging object. """ + capture_preset = {} + filtering_criteria = { "hosts": "maya", "families": "review", From 119df6d24e7b9ba6865cb854a0089c71fe3e58e4 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 14 Apr 2023 08:39:42 +0100 Subject: [PATCH 092/139] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 46f423023f..8ca6ade2ec 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3872,7 +3872,6 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): log (object): Logging object. """ capture_preset = {} - filtering_criteria = { "hosts": "maya", "families": "review", From 2ee9c1727045d0c234f9051b9847cd82f58f7ce3 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 14 Apr 2023 12:28:26 +0100 Subject: [PATCH 093/139] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8ca6ade2ec..39db06f70f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3871,7 +3871,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): project_settings (dict): Project settings. log (object): Logging object. """ - capture_preset = {} + capture_preset = None filtering_criteria = { "hosts": "maya", "families": "review", @@ -3900,4 +3900,4 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): ) capture_preset = plugin_settings["capture_preset"] - return capture_preset + return capture_preset or {} From bcdaf5c129c0c7a9dabe2d4a29d1c4768d8daf4c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Apr 2023 17:06:16 +0200 Subject: [PATCH 094/139] fixing passing CU secrets in release workflow --- .github/workflows/miletone_release_trigger.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/miletone_release_trigger.yml b/.github/workflows/miletone_release_trigger.yml index 26a2d5833d..4a031be7f9 100644 --- a/.github/workflows/miletone_release_trigger.yml +++ b/.github/workflows/miletone_release_trigger.yml @@ -45,3 +45,6 @@ jobs: token: ${{ secrets.YNPUT_BOT_TOKEN }} user_email: ${{ secrets.CI_EMAIL }} user_name: ${{ secrets.CI_USER }} + cu_api_key: ${{ secrets.CLICKUP_API_KEY }} + cu_team_id: ${{ secrets.CLICKUP_TEAM_ID }} + cu_field_id: ${{ secrets.CLICKUP_RELEASE_FIELD_ID }} From a14f9196bb74ad39d40a07c3db38c666a38a830a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 14 Apr 2023 15:15:13 +0000 Subject: [PATCH 095/139] [Automated] Release --- CHANGELOG.md | 943 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 945 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e22b783c4..5aeb546c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,948 @@ # Changelog + +## [3.15.4](https://github.com/ynput/OpenPype/tree/3.15.4) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.3...3.15.4) + +### **🆕 New features** + + +
+Maya: Cant assign shaders to the ass file - OP-4859 #4460 + +Support AiStandIn nodes for look assignment. + +Using operators we assign shaders and attribute/parameters to nodes within standins. Initially there is only support for a limited mount of attributes but we can add support as needed; +``` +primaryVisibility +castsShadows +receiveShadows +aiSelfShadows +aiOpaque +aiMatte +aiVisibleInDiffuseTransmission +aiVisibleInSpecularTransmission +aiVisibleInVolume +aiVisibleInDiffuseReflection +aiVisibleInSpecularReflection +aiSubdivUvSmoothing +aiDispHeight +aiDispPadding +aiDispZeroValue +aiStepSize +aiVolumePadding +aiSubdivType +aiSubdivIterations +``` + + +___ + +
+ + +
+Maya: GPU cache representation #4649 + +Implement GPU cache for model, animation and pointcache. + + +___ + +
+ + +
+Houdini: Implement review family with opengl node #3839 + +Implements a first pass for Reviews publishing in Houdini. Resolves #2720 + +Uses the `opengl` ROP node to produce PNG images. + + +___ + +
+ + +
+Maya: Camera focal length visible in review - OP-3278 #4531 + +Camera focal length visible in review. + +Support camera focal length in review; static and dynamic.Resolves #3220 + + +___ + +
+ + +
+Maya: Defining plugins to load on Maya start - OP-4994 #4714 + +Feature to define plugins to load on Maya launch. + + +___ + +
+ + +
+Nuke, DL: Returning Suspended Publishing attribute #4715 + +Old Nuke Publisher's feature for suspended publishing job on render farm was added back to the current Publisher. + + +___ + +
+ + +
+Settings UI: Allow setting a size hint for text fields #4821 + +Text entity have `minimum_lines_count` which allows to change minimum size hint of UI input. + + +___ + +
+ + +
+TrayPublisher: Move 'BatchMovieCreator' settings to 'create' subcategory #4827 + +Moved settings for `BatchMoviewCreator` into subcategory `create` in settings. Changes are made to match other hosts settings chema and structure. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya looks: support for native Redshift texture format #2971 + +Add support for native Redshift textures handling. Closes #2599 + +Uses Redshift's Texture Processor executable to convert textures being used in renders to the Redshift ".rstexbin" format. + + +___ + +
+ + +
+Maya: custom namespace for references #4511 + +Adding an option in Project Settings > Maya > Loader plugins to set custom namespace. If no namespace is set, the default one is used. + + +___ + +
+ + +
+Maya: Set correct framerange with handles on file opening #4664 + +Set the range of playback from the asset data, counting handles, to get the correct data when calling the "collect_animation_data" function. + + +___ + +
+ + +
+Maya: Fix camera update #4751 + +Fix resetting any modelPanel to a different camera when loading a camera and updating. + + +___ + +
+ + +
+Maya: Remove single assembly validation for animation instances #4840 + +Rig groups may now be parented to others groups when `includeParentHierarchy` attribute on the instance is "off". + + +___ + +
+ + +
+Maya: Optional control of display lights on playblast. #4145 + +Optional control of display lights on playblast. + +Giving control to what display lights are on the playblasts. + + +___ + +
+ + +
+Kitsu: note family requirements #4551 + +Allowing to add family requirements to `IntegrateKitsuNote` task status change. + +Adds a `Family requirements` setting to `Integrate Kitsu Note`, so you can add requirements to determine if kitsu task status should be changed based on which families are published or not. For instance you could have the status change only if another subset than workfile is published (but workfile can still be included) by adding an item set to `Not equal` and `workfile`. + + +___ + +
+ + +
+Deactivate closed Kitsu projects on OP #4619 + +Deactivate project on OP when the project is closed on Kitsu. + + +___ + +
+ + +
+Maya: Suggestion to change capture labels. #4691 + +Change capture labels. + + +___ + +
+ + +
+Houdini: Change node type for OpenPypeContext `null` -> `subnet` #4745 + +Change the node type for OpenPype's hidden context node in Houdini from `null` to `subnet`. This fixes #4734 + + +___ + +
+ + +
+General: Extract burnin hosts filters #4749 + +Removed hosts filter from ExtractBurnin plugin. Instance without representations won't cause crash but just skip the instance. We've discovered because Blender already has review but did not create burnins. + + +___ + +
+ + +
+Global: Improve speed of Collect Custom Staging Directory #4768 + +Improve speed of Collect Custom Staging Directory. + + +___ + +
+ + +
+General: Anatomy templates formatting #4773 + +Added option to format only single template from anatomy instead of formatting all of them all the time. Formatting of all templates is causing slowdowns e.g. during publishing of hundreds of instances. + + +___ + +
+ + +
+Harmony: Handle zip files with deeper structure #4782 + +External Harmony zip files might contain one additional level with scene name. + + +___ + +
+ + +
+Unreal: Use common logic to configure executable #4788 + +Unreal Editor location and version was autodetected. This easied configuration in some cases but was not flexible enought. This PR is changing the way Unreal Editor location is set, unifying it with the logic other hosts are using. + + +___ + +
+ + +
+Github: Grammar tweaks + uppercase issue title #4813 + +Tweak some of the grammar in the issue form templates. + + +___ + +
+ + +
+Houdini: Allow creation of publish instances via Houdini TAB menu #4831 + +Register the available Creator's as houdini tools so an artist can add publish instances via the Houdini TAB node search menu from within the network editor. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Fix Collect Render for V-Ray, Redshift and Renderman for missing colorspace #4650 + +Fix Collect Render not working for Redshift, V-Ray and Renderman due to missing `colorspace` argument to `RenderProduct` dataclass. + + +___ + +
+ + +
+Maya: Xgen fixes #4707 + +Fix for Xgen extraction of world parented nodes and validation for required namespace. + + +___ + +
+ + +
+Maya: Fix extract review and thumbnail for Maya 2020 #4744 + +Fix playblasting in Maya 2020 with override viewport options enabled. Fixes #4730. + + +___ + +
+ + +
+Maya: local variable 'arnold_standins' referenced before assignment - OP-5542 #4778 + +MayaLookAssigner erroring when MTOA is not loaded: +``` +# Traceback (most recent call last): +# File "\openpype\hosts\maya\tools\mayalookassigner\app.py", line 272, in on_process_selected +# nodes = list(set(item["nodes"]).difference(arnold_standins)) +# UnboundLocalError: local variable 'arnold_standins' referenced before assignment +``` + + +___ + +
+ + +
+Maya: Fix getting view and display in Maya 2020 - OP-5035 #4795 + +The `view_transform` returns a different format in Maya 2020. Fixes #4540 (hopefully). + + +___ + +
+ + +
+Maya: Fix Look Maya 2020 Py2 support for Extract Look #4808 + +Fix Extract Look supporting python 2.7 for Maya 2020. + + +___ + +
+ + +
+Maya: Fix Validate Mesh Overlapping UVs plugin #4816 + +Fix typo in the code where a maya command returns a `list` instead of `str`. + + +___ + +
+ + +
+Maya: Fix tile rendering with Vray - OP-5566 #4832 + +Fixes tile rendering with Vray. + + +___ + +
+ + +
+Deadline: checking existing frames fails when there is number in file name #4698 + +Previous implementation of validator failed on files with any other number in rendered file names.Used regular expression pattern now handles numbers in the file names (eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr", "Main_beauty.1001.1001.exr") but not numbers behind frames (eg. "Main_beauty.1001.v001.exr") + + +___ + +
+ + +
+Maya: Validate Render Settings. #4735 + +Fixes error message when using attribute validation. + + +___ + +
+ + +
+General: Hero version sites recalculation #4737 + +Sites recalculation in integrate hero version did expect that it is integrated exactly same amount of files as in previous integration. This is not the case in many cases, so the sites recalculation happens in a different way, first are prepared all sites from previous representation files, and all of them are added to each file in new representation. + + +___ + +
+ + +
+Houdini: Fix collect current file #4739 + +Fixes the Workfile publishing getting added into every instance being published from Houdini + + +___ + +
+ + +
+Global: Fix Extract Burnin + Colorspace functions for conflicting python environments with PYTHONHOME #4740 + +This fixes the running of openpype processes from e.g. a host with conflicting python versions that had `PYTHONHOME` said additionally to `PYTHONPATH`, like e.g. Houdini Py3.7 together with OpenPype Py3.9 when using Extract Burnin for a review in #3839This fix applies to Extract Burnin and some of the colorspace functions that use `run_openpype_process` + + +___ + +
+ + +
+Harmony: render what is in timeline in Harmony locally #4741 + +Previously it wasn't possible to render according to what was set in Timeline in scene start/end, just by what it was set in whole timeline.This allows artist to override what is in DB with what they require (with disabled `Validate Scene Settings`). Now artist can extend scene by additional frames, that shouldn't be rendered, but which might be desired.Removed explicit set scene settings (eg. applying frames and resolution directly to the scene after launch), added separate menu item to allow artist to do it themselves. + + +___ + +
+ + +
+Maya: Extract Review settings add Use Background Gradient #4747 + +Add Display Gradient Background toggle in settings to fix support for setting flat background color for reviews. + + +___ + +
+ + +
+Nuke: publisher is offering review on write families on demand #4755 + +Original idea where reviewable toggle will be offered in publisher on demand is fixed and now `review` attribute can be disabled in settings. + + +___ + +
+ + +
+Workfiles: keep Browse always enabled #4766 + +Browse might make sense even if there are no workfiles present, actually in that case it makes the most sense (eg. I want to locate workfile from outside - from Desktop for example). + + +___ + +
+ + +
+Global: label key in instance data is optional #4779 + +Collect OTIO review plugin is not crashing if `label` key is missing in instance data. + + +___ + +
+ + +
+Loader: Fix missing variable #4781 + +There is missing variable `handles` in loader tool after https://github.com/ynput/OpenPype/pull/4746. The variable was renamed to `handles_label` and is initialized to `None` if handles are not available. + + +___ + +
+ + +
+Nuke: Workfile Template builder fixes #4783 + +Popup window after Nuke start is not showing. Knobs with X/Y coordination on nodes where were converted from placeholders are not added if `keepPlaceholders` is witched off. + + +___ + +
+ + +
+Maya: Add family filter 'review' to burnin profile with focal length #4791 + +Avoid profile burnin with `focalLength` key for renders, but use only for playblast reviews. + + +___ + +
+ + +
+add farm instance to the render collector in 3dsMax #4794 + +bug fix for the failure of submitting publish job in 3dsmax + + +___ + +
+ + +
+Publisher: Plugin active attribute is respected #4798 + +Publisher consider plugin's `active` attribute, so the plugin is not processed when `active` is set to `False`. But we use the attribute in `OptionalPyblishPluginMixin` for different purposes, so I've added hack bypass of the active state validation when plugin inherit from the mixin. This is temporary solution which cannot be changed until all hosts use Publisher otherwise global plugins would be broken. Also plugins which have `enabled` set to `False` are filtered out -> this happened only when automated settings were applied and the settings contained `"enabled"` key se to `False`. + + +___ + +
+ + +
+Nuke: settings and optional attribute in publisher for some validators #4811 + +New publisher is supporting optional switch for plugins which is offered in Publisher in Right panel. Some plugins were missing this switch and also settings which would offer the optionality. + + +___ + +
+ + +
+Settings: Version settings popup fix #4822 + +Version completer popup have issues on some platforms, this should fix those edge cases. Also fixed issue when completer stayed shown fater reset (save). + + +___ + +
+ + +
+Hiero/Nuke: adding monitorOut key to settings #4826 + +New versions of Hiero were introduced with new colorspace property for Monitor Out. It have been added into project settings. Also added new config names into settings enumerator option. + + +___ + +
+ + +
+Nuke: removed default workfile template builder preset #4835 + +Default for workfile template builder should have been empty. + + +___ + +
+ + +
+TVPaint: Review can be made from any instance #4843 + +Add `"review"` tag to output of extract sequence if instance is marked for review. At this moment only instances with family `"review"` were able to define input for `ExtractReview` plugin which is not right. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Deadline: Remove unused FramesPerTask job info submission #4657 + +Remove unused `FramesPerTask` job info submission to Deadline. + + +___ + +
+ + +
+Maya: Remove pymel dependency #4724 + +Refactors code written using `pymel` to use standard maya python libraries instead like `maya.cmds` or `maya.api.OpenMaya` + + +___ + +
+ + +
+Remove "preview" data from representation #4759 + +Remove "preview" data from representation + + +___ + +
+ + +
+Maya: Collect Review cleanup code for attached subsets #4720 + +Refactor some code for Maya: Collect Review for attached subsets. + + +___ + +
+ + +
+Refactor: Remove `handles`, `edit_in` and `edit_out` backwards compatibility #4746 + +Removes backward compatibiliy fallback for data called `handles`, `edit_in` and `edit_out`. + + +___ + +
+ +### **📃 Documentation** + + +
+Bump webpack from 5.69.1 to 5.76.1 in /website #4624 + +Bumps [webpack](https://github.com/webpack/webpack) from 5.69.1 to 5.76.1. +
+Release notes +

Sourced from webpack's releases.

+
+

v5.76.1

+

Fixed

+
    +
  • Added assert/strict built-in to NodeTargetPlugin
  • +
+

Revert

+ +

v5.76.0

+

Bugfixes

+ +

Features

+ +

Security

+ +

Repo Changes

+ +

New Contributors

+ +

Full Changelog: https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0

+

v5.75.0

+

Bugfixes

+
    +
  • experiments.* normalize to false when opt-out
  • +
  • avoid NaN%
  • +
  • show the correct error when using a conflicting chunk name in code
  • +
  • HMR code tests existance of window before trying to access it
  • +
  • fix eval-nosources-* actually exclude sources
  • +
  • fix race condition where no module is returned from processing module
  • +
  • fix position of standalong semicolon in runtime code
  • +
+

Features

+
    +
  • add support for @import to extenal CSS when using experimental CSS in node
  • +
+ +
+

... (truncated)

+
+
+Commits +
    +
  • 21be52b Merge pull request #16804 from webpack/chore-patch-release
  • +
  • 1cce945 chore(release): 5.76.1
  • +
  • e76ad9e Merge pull request #16803 from ryanwilsonperkin/revert-16759-real-content-has...
  • +
  • 52b1b0e Revert "Improve performance of hashRegExp lookup"
  • +
  • c989143 Merge pull request #16766 from piranna/patch-1
  • +
  • 710eaf4 Merge pull request #16789 from dmichon-msft/contenthash-hashsalt
  • +
  • 5d64468 Merge pull request #16792 from webpack/update-version
  • +
  • 67af5ec chore(release): 5.76.0
  • +
  • 97b1718 Merge pull request #16781 from askoufis/loader-context-target-type
  • +
  • b84efe6 Merge pull request #16759 from ryanwilsonperkin/real-content-hash-regex-perf
  • +
  • Additional commits viewable in compare view
  • +
+
+
+Maintainer changes +

This version was pushed to npm by evilebottnawi, a new releaser for webpack since your current version.

+
+
+ + +[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.69.1&new-version=5.76.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) + +Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. + +[//]: # (dependabot-automerge-start) +[//]: # (dependabot-automerge-end) + +--- + +
+Dependabot commands and options +
+ +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it +- `@dependabot merge` will merge this PR after your CI passes on it +- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it +- `@dependabot cancel merge` will cancel a previously requested merge and block automerging +- `@dependabot reopen` will reopen this PR if it is closed +- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually +- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) +- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language +- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language +- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language +- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language + +You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts). + +
+___ + +
+ + +
+Documentation: Add Extract Burnin documentation #4765 + +Add documentation for Extract Burnin global plugin settings. + + +___ + +
+ + +
+Documentation: Move publisher related tips to publisher area #4772 + +Move publisher related tips for After Effects artist documentation to the correct position. + + +___ + +
+ + +
+Documentation: Add extra terminology to the key concepts glossary #4838 + +Tweak some of the key concepts in the documentation. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya: Refactor Extract Look with dedicated processors for maketx #4711 + +Refactor Maya extract look to fix some issues: +- [x] Allow Extraction with maketx with OCIO Color Management enabled in Maya. +- [x] Fix file hashing so it includes arguments to maketx, so that when arguments change it correctly generates a new hash +- [x] Fix maketx destination colorspace when OCIO is enabled +- [x] Use pre-collected colorspaces of the resources instead of trying to retrieve again in Extract Look +- [x] Fix colorspace attributes being reinterpreted by maya on export (fix remapping) - goal is to resolve #2337 +- [x] Fix support for checking config path of maya default OCIO config (due to using `lib.get_color_management_preferences` which remaps that path) +- [x] Merged in #2971 to refactor MakeTX into TextureProcessor and also support generating Redshift `.rstexbin` files. - goal is to resolve #2599 +- [x] Allow custom arguments to `maketx` from OpenPype Settings like mentioned here by @fabiaserra for arguments like: `--monochrome-detect`, `--opaque-detect`, `--checknan`. +- [x] Actually fix the code and make it work. :) (I'll try to keep below checkboxes in sync with my code changes) +- [x] Publishing without texture processor should work (no maketx + no rstexbin) +- [x] Publishing with maketx should work +- [x] Publishing with rstexbin should work +- [x] Test it. (This is just me doing some test-runs, please still test the PR!) + + +___ + +
+ + +
+Maya template builder load all assets linked to the shot #4761 + +Problem +All the assets of the ftrack project are loaded and not those linked to the shot + +How get error +Open maya in the context of shot, then build a new scene with the "Build Workfile from template" button in "OpenPype" menu. +![image](https://user-images.githubusercontent.com/7068597/229124652-573a23d7-a2b2-4d50-81bf-7592c00d24dc.png) + + +___ + +
+ + +
+Global: Do not force instance data with frame ranges of the asset #4383 + +This aims to resolve #4317 + + +___ + +
+ + +
+Cosmetics: Fix some grammar in docstrings and messages (and some code) #4752 + +Tweak some grammar in codebase + + +___ + +
+ + +
+Deadline: Submit publish job fails due root work hardcode - OP-5528 #4775 + +Generating config templates was hardcoded to `root[work]`. This PR fixes that. + + +___ + +
+ + +
+CreateContext: Added option to remove Unknown attributes #4776 + +Added option to remove attributes with UnkownAttrDef on instances. Pop of key will also remove the attribute definition from attribute values, so they're not recreated again. + + +___ + +
+ + + ## [3.15.3](https://github.com/ynput/OpenPype/tree/3.15.3) diff --git a/openpype/version.py b/openpype/version.py index d9e29d691e..1d41f1aa5d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.4-nightly.3" +__version__ = "3.15.4" diff --git a/pyproject.toml b/pyproject.toml index 42ce5aa32c..b97ad8923c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.3" # OpenPype +version = "3.15.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From fcffb919486dfdef7474ccf5aefd264f34c7a8f2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Fri, 14 Apr 2023 17:39:53 +0200 Subject: [PATCH 096/139] After Effects: fix handles KeyError (#4727) * get handles from context if not in asset * fix linting errors * get frameStart, frameEnd, handleStart and handleEnd from context --------- Co-authored-by: clement hector Co-authored-by: Thomas Fricard --- .../aftereffects/plugins/publish/collect_workfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 3c5013b3bd..c21c3623c3 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -53,10 +53,10 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "active": True, "asset": asset_entity["name"], "task": task, - "frameStart": asset_entity["data"]["frameStart"], - "frameEnd": asset_entity["data"]["frameEnd"], - "handleStart": asset_entity["data"]["handleStart"], - "handleEnd": asset_entity["data"]["handleEnd"], + "frameStart": context.data['frameStart'], + "frameEnd": context.data['frameEnd'], + "handleStart": context.data['handleStart'], + "handleEnd": context.data['handleEnd'], "fps": asset_entity["data"]["fps"], "resolutionWidth": asset_entity["data"].get( "resolutionWidth", From ef55dd932d57694d5e872a3f6a9fe4f0cb77370b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 17 Apr 2023 13:00:01 +0200 Subject: [PATCH 097/139] :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 098/139] :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 099/139] 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 100/139] 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 101/139] 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 102/139] 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 103/139] 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 104/139] 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 105/139] 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 106/139] 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 107/139] 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 108/139] 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 109/139] 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 110/139] 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 111/139] 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 112/139] 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 a8e5a0c5fc37a1236f70dc5c890e3160f8b5f6f3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 20 Apr 2023 12:10:55 +0200 Subject: [PATCH 113/139] :art: calculate hash for tx texture --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 520951a5e6..3cc95a0b2e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -280,7 +280,7 @@ class MakeTX(TextureProcessor): # Do nothing if the source file is already a .tx file. return TextureResult( path=source, - file_hash=None, # todo: unknown texture hash? + file_hash=source_hash(source), colorspace=colorspace, transfer_mode=COPY ) From 1ab4243d58fc24e932edb10dd932719561d033fc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 14:03:29 +0200 Subject: [PATCH 114/139] 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 115/139] 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. From f05f7510b4256964de741b6fd982327da9e4e1aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 21:39:49 +0200 Subject: [PATCH 116/139] adding slate condition to plugin --- openpype/plugins/publish/validate_sequence_frames.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/plugins/publish/validate_sequence_frames.py b/openpype/plugins/publish/validate_sequence_frames.py index 0dba99b07c..239008ee21 100644 --- a/openpype/plugins/publish/validate_sequence_frames.py +++ b/openpype/plugins/publish/validate_sequence_frames.py @@ -49,7 +49,12 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): collection = collections[0] frames = list(collection.indexes) + if instance.data.get("slate"): + # Slate is not part of the frame range + frames = frames[1:] + current_range = (frames[0], frames[-1]) + required_range = (instance.data["frameStart"], instance.data["frameEnd"]) From aa2d683dd9402268d355d74df452ce72e8c09e6a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 21:49:58 +0200 Subject: [PATCH 117/139] adding test routine for the slate condition --- .../publish/test_validate_sequence_frames.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py b/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py index 58d9de011d..17e47c9f64 100644 --- a/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py +++ b/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py @@ -180,5 +180,23 @@ class TestValidateSequenceFrames(BaseTest): plugin.process(instance) assert ("Missing frames: [1002]" in str(excinfo.value)) + def test_validate_sequence_frames_slate(self, instance, plugin): + representations = [ + { + "ext": "exr", + "files": [ + "Main_beauty.1000.exr", + "Main_beauty.1001.exr", + "Main_beauty.1002.exr", + "Main_beauty.1003.exr" + ] + } + ] + instance.data["slate"] = True + instance.data["representations"] = representations + instance.data["frameEnd"] = 1003 + + plugin.process(instance) + test_case = TestValidateSequenceFrames() From 843fd5f1b920e4a22cf94595eeb2c4c945ad0cbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Apr 2023 11:31:52 +0200 Subject: [PATCH 118/139] Nuke: Legacy convertor skips deprecation warnings (#4846) * convert legacy checks for AVALON_TAB to avoid deprecation warnings * simplify 'get_avalon_knob_data' --- openpype/hosts/nuke/api/lib.py | 13 ++++++------- .../hosts/nuke/plugins/create/convert_legacy.py | 7 +++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index fe3a2d2bd1..64fa32a383 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -495,17 +495,17 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): data (dict) """ + data = {} + if AVALON_TAB not in node.knobs(): + return data + # check if lists if not isinstance(prefix, list): - prefix = list([prefix]) - - data = dict() + prefix = [prefix] # loop prefix for p in prefix: # check if the node is avalon tracked - if AVALON_TAB not in node.knobs(): - continue try: # check if data available on the node test = node[AVALON_DATA_GROUP].value() @@ -516,8 +516,7 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): if create: node = set_avalon_knob_data(node) return get_avalon_knob_data(node) - else: - return {} + return {} # get data from filtered knobs data.update({k.replace(p, ''): node[k].value() diff --git a/openpype/hosts/nuke/plugins/create/convert_legacy.py b/openpype/hosts/nuke/plugins/create/convert_legacy.py index c143e4cb27..377e9f78f6 100644 --- a/openpype/hosts/nuke/plugins/create/convert_legacy.py +++ b/openpype/hosts/nuke/plugins/create/convert_legacy.py @@ -2,7 +2,8 @@ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.nuke.api.lib import ( INSTANCE_DATA_KNOB, get_node_data, - get_avalon_knob_data + get_avalon_knob_data, + AVALON_TAB, ) from openpype.hosts.nuke.api.plugin import convert_to_valid_instaces @@ -17,13 +18,15 @@ class LegacyConverted(SubsetConvertorPlugin): legacy_found = False # search for first available legacy item for node in nuke.allNodes(recurseGroups=True): - if node.Class() in ["Viewer", "Dot"]: continue if get_node_data(node, INSTANCE_DATA_KNOB): continue + if AVALON_TAB not in node.knobs(): + continue + # get data from avalon knob avalon_knob_data = get_avalon_knob_data( node, ["avalon:", "ak:"], create=False) From a2f79419bcb51731546e5422292e51cbd66bd52f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 11:59:41 +0200 Subject: [PATCH 119/139] Clear publisher comment on successful publish or on window close (#4885) --- openpype/tools/publisher/window.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 8826e0f849..0615157e1b 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -284,6 +284,9 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "publish.has_validated.changed", self._on_publish_validated_change ) + controller.event_system.add_callback( + "publish.finished.changed", self._on_publish_finished_change + ) controller.event_system.add_callback( "publish.process.stopped", self._on_publish_stop ) @@ -400,6 +403,7 @@ class PublisherWindow(QtWidgets.QDialog): # TODO capture changes and ask user if wants to save changes on close if not self._controller.host_context_has_changed: self._save_changes(False) + self._comment_input.setText("") # clear comment self._reset_on_show = True self._controller.clear_thumbnail_temp_dir_path() super(PublisherWindow, self).closeEvent(event) @@ -777,6 +781,11 @@ class PublisherWindow(QtWidgets.QDialog): if event["value"]: self._validate_btn.setEnabled(False) + def _on_publish_finished_change(self, event): + if event["value"]: + # Successful publish, remove comment from UI + self._comment_input.setText("") + def _on_publish_stop(self): self._set_publish_overlay_visibility(False) self._reset_btn.setEnabled(True) From 5b1854e9022ed7e6fc994b08ed160543572851c2 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Fri, 21 Apr 2023 18:17:01 +0800 Subject: [PATCH 120/139] Add fps as instance.data in collect review in Houdini. (#4888) * add fps as instance data in collect review data * Trllo's feedback --- openpype/hosts/houdini/plugins/publish/collect_review_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index e321dcb2fa..8118e6d558 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -17,6 +17,7 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): # which isn't the actual frame range that this instance renders. instance.data["handleStart"] = 0 instance.data["handleEnd"] = 0 + instance.data["fps"] = instance.context.data["fps"] # Get the camera from the rop node to collect the focal length ropnode_path = instance.data["instance_node"] From cac990cd3cb707fa3528b2f302fb5791a783b678 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 12:20:10 +0200 Subject: [PATCH 121/139] Code: Tweak docstrings and return type hints (#4875) * Tweak docstrings and return type hints * Remove test import of `typing` * Fix indentations * Fix indentations * Fix typos * Update openpype/client/entities.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * `fields` as `Optional` iterable of strings. --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/client/entities.py | 229 +++++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 94 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 376157d210..8004dc3019 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -69,6 +69,19 @@ def convert_ids(in_ids): def get_projects(active=True, inactive=False, fields=None): + """Yield all project entity documents. + + Args: + active (Optional[bool]): Include active projects. Defaults to True. + inactive (Optional[bool]): Include inactive projects. + Defaults to False. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Yields: + dict: Project entity data which can be reduced to specified 'fields'. + None is returned if project with specified filters was not found. + """ mongodb = get_project_database() for project_name in mongodb.collection_names(): if project_name in ("system.indexes",): @@ -81,6 +94,20 @@ def get_projects(active=True, inactive=False, fields=None): def get_project(project_name, active=True, inactive=True, fields=None): + """Return project entity document by project name. + + Args: + project_name (str): Name of project. + active (Optional[bool]): Allow active project. Defaults to True. + inactive (Optional[bool]): Allow inactive project. Defaults to True. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Project entity data which can be reduced to + specified 'fields'. None is returned if project with specified + filters was not found. + """ # Skip if both are disabled if not active and not inactive: return None @@ -124,17 +151,18 @@ def get_whole_project(project_name): def get_asset_by_id(project_name, asset_id, fields=None): - """Receive asset data by it's id. + """Receive asset data by its id. Args: project_name (str): Name of project where to look for queried entities. asset_id (Union[str, ObjectId]): Asset's id. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - dict: Asset entity data. - None: Asset was not found by id. + Union[Dict, None]: Asset entity data which can be reduced to + specified 'fields'. None is returned if asset with specified + filters was not found. """ asset_id = convert_id(asset_id) @@ -147,17 +175,18 @@ def get_asset_by_id(project_name, asset_id, fields=None): def get_asset_by_name(project_name, asset_name, fields=None): - """Receive asset data by it's name. + """Receive asset data by its name. Args: project_name (str): Name of project where to look for queried entities. asset_name (str): Asset's name. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - dict: Asset entity data. - None: Asset was not found by name. + Union[Dict, None]: Asset entity data which can be reduced to + specified 'fields'. None is returned if asset with specified + filters was not found. """ if not asset_name: @@ -195,8 +224,8 @@ def _get_assets( parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. standard (bool): Query standard assets (type 'asset'). archived (bool): Query archived assets (type 'archived_asset'). - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Query cursor as iterable which returns asset documents matching @@ -261,8 +290,8 @@ def get_assets( asset_names (Iterable[str]): Name assets that should be found. parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. archived (bool): Add also archived assets. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Query cursor as iterable which returns asset documents matching @@ -300,8 +329,8 @@ def get_archived_assets( be found. asset_names (Iterable[str]): Name assets that should be found. parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Query cursor as iterable which returns asset documents matching @@ -356,17 +385,18 @@ def get_asset_ids_with_subsets(project_name, asset_ids=None): def get_subset_by_id(project_name, subset_id, fields=None): - """Single subset entity data by it's id. + """Single subset entity data by its id. Args: project_name (str): Name of project where to look for queried entities. subset_id (Union[str, ObjectId]): Id of subset which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If subset with specified filters was not found. - Dict: Subset document which can be reduced to specified 'fields'. + Union[Dict, None]: Subset entity data which can be reduced to + specified 'fields'. None is returned if subset with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -379,20 +409,19 @@ def get_subset_by_id(project_name, subset_id, fields=None): def get_subset_by_name(project_name, subset_name, asset_id, fields=None): - """Single subset entity data by it's name and it's version id. + """Single subset entity data by its name and its version id. Args: project_name (str): Name of project where to look for queried entities. subset_name (str): Name of subset. asset_id (Union[str, ObjectId]): Id of parent asset. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - Union[None, Dict[str, Any]]: None if subset with specified filters was - not found or dict subset document which can be reduced to - specified 'fields'. - + Union[Dict, None]: Subset entity data which can be reduced to + specified 'fields'. None is returned if subset with specified + filters was not found. """ if not subset_name: return None @@ -434,8 +463,8 @@ def get_subsets( names_by_asset_ids (dict[ObjectId, List[str]]): Complex filtering using asset ids and list of subset names under the asset. archived (bool): Look for archived subsets too. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching subsets. @@ -520,17 +549,18 @@ def get_subset_families(project_name, subset_ids=None): def get_version_by_id(project_name, version_id, fields=None): - """Single version entity data by it's id. + """Single version entity data by its id. Args: project_name (str): Name of project where to look for queried entities. version_id (Union[str, ObjectId]): Id of version which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ version_id = convert_id(version_id) @@ -546,18 +576,19 @@ def get_version_by_id(project_name, version_id, fields=None): def get_version_by_name(project_name, version, subset_id, fields=None): - """Single version entity data by it's name and subset id. + """Single version entity data by its name and subset id. Args: project_name (str): Name of project where to look for queried entities. - version (int): name of version entity (it's version). + version (int): name of version entity (its version). subset_id (Union[str, ObjectId]): Id of version which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -574,7 +605,7 @@ def get_version_by_name(project_name, version, subset_id, fields=None): def version_is_latest(project_name, version_id): - """Is version the latest from it's subset. + """Is version the latest from its subset. Note: Hero versions are considered as latest. @@ -680,8 +711,8 @@ def get_versions( versions (Iterable[int]): Version names (as integers). Filter ignored if 'None' is passed. hero (bool): Look also for hero versions. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching versions. @@ -705,12 +736,13 @@ def get_hero_version_by_subset_id(project_name, subset_id, fields=None): project_name (str): Name of project where to look for queried entities. subset_id (Union[str, ObjectId]): Subset id under which is hero version. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If hero version for passed subset id does not exists. - Dict: Hero version entity data. + Union[Dict, None]: Hero version entity data which can be reduced to + specified 'fields'. None is returned if hero version with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -730,17 +762,18 @@ def get_hero_version_by_subset_id(project_name, subset_id, fields=None): def get_hero_version_by_id(project_name, version_id, fields=None): - """Hero version by it's id. + """Hero version by its id. Args: project_name (str): Name of project where to look for queried entities. version_id (Union[str, ObjectId]): Hero version id. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If hero version with passed id was not found. - Dict: Hero version entity data. + Union[Dict, None]: Hero version entity data which can be reduced to + specified 'fields'. None is returned if hero version with specified + filters was not found. """ version_id = convert_id(version_id) @@ -773,8 +806,8 @@ def get_hero_versions( should look for hero versions. Filter ignored if 'None' is passed. version_ids (Iterable[Union[str, ObjectId]]): Hero version ids. Filter ignored if 'None' is passed. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor|list: Iterable yielding hero versions matching passed filters. @@ -801,8 +834,8 @@ def get_output_link_versions(project_name, version_id, fields=None): project_name (str): Name of project where to look for queried entities. version_id (Union[str, ObjectId]): Version id which can be used as input link for other versions. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Iterable: Iterable cursor yielding versions that are used as input @@ -828,8 +861,8 @@ def get_last_versions(project_name, subset_ids, fields=None): Args: project_name (str): Name of project where to look for queried entities. subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: dict[ObjectId, int]: Key is subset id and value is last version name. @@ -913,12 +946,13 @@ def get_last_version_by_subset_id(project_name, subset_id, fields=None): Args: project_name (str): Name of project where to look for queried entities. subset_id (Union[str, ObjectId]): Id of version which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -945,12 +979,13 @@ def get_last_version_by_subset_name( asset_id (Union[str, ObjectId]): Asset id which is parent of passed subset name. asset_name (str): Asset name which is parent of passed subset name. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ if not asset_id and not asset_name: @@ -972,18 +1007,18 @@ def get_last_version_by_subset_name( def get_representation_by_id(project_name, representation_id, fields=None): - """Representation entity data by it's id. + """Representation entity data by its id. Args: project_name (str): Name of project where to look for queried entities. representation_id (Union[str, ObjectId]): Representation id. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If representation with specified filters was not found. - Dict: Representation entity data which can be reduced - to specified 'fields'. + Union[Dict, None]: Representation entity data which can be reduced to + specified 'fields'. None is returned if representation with + specified filters was not found. """ if not representation_id: @@ -1004,19 +1039,19 @@ def get_representation_by_id(project_name, representation_id, fields=None): def get_representation_by_name( project_name, representation_name, version_id, fields=None ): - """Representation entity data by it's name and it's version id. + """Representation entity data by its name and its version id. Args: project_name (str): Name of project where to look for queried entities. representation_name (str): Representation name. version_id (Union[str, ObjectId]): Id of parent version entity. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If representation with specified filters was not found. - Dict: Representation entity data which can be reduced - to specified 'fields'. + Union[dict[str, Any], None]: Representation entity data which can be + reduced to specified 'fields'. None is returned if representation + with specified filters was not found. """ version_id = convert_id(version_id) @@ -1202,8 +1237,8 @@ def get_representations( names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering using version ids and list of names under the version. archived (bool): Output will also contain archived representations. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching representations. @@ -1247,8 +1282,8 @@ def get_archived_representations( representation context fields. names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching representations. @@ -1377,8 +1412,8 @@ def get_thumbnail_id_from_source(project_name, src_type, src_id): src_id (Union[str, ObjectId]): Id of source entity. Returns: - ObjectId: Thumbnail id assigned to entity. - None: If Source entity does not have any thumbnail id assigned. + Union[ObjectId, None]: Thumbnail id assigned to entity. If Source + entity does not have any thumbnail id assigned. """ if not src_type or not src_id: @@ -1397,14 +1432,14 @@ def get_thumbnails(project_name, thumbnail_ids, fields=None): """Receive thumbnails entity data. Thumbnail entity can be used to receive binary content of thumbnail based - on it's content and ThumbnailResolvers. + on its content and ThumbnailResolvers. Args: project_name (str): Name of project where to look for queried entities. thumbnail_ids (Iterable[Union[str, ObjectId]]): Ids of thumbnail entities. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: cursor: Cursor of queried documents. @@ -1429,12 +1464,13 @@ def get_thumbnail(project_name, thumbnail_id, fields=None): Args: project_name (str): Name of project where to look for queried entities. thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If thumbnail with specified id was not found. - Dict: Thumbnail entity data which can be reduced to specified 'fields'. + Union[Dict, None]: Thumbnail entity data which can be reduced to + specified 'fields'.None is returned if thumbnail with specified + filters was not found. """ if not thumbnail_id: @@ -1458,8 +1494,13 @@ def get_workfile_info( project_name (str): Name of project where to look for queried entities. asset_id (Union[str, ObjectId]): Id of asset entity. task_name (str): Task name on asset. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Workfile entity data which can be reduced to + specified 'fields'.None is returned if workfile with specified + filters was not found. """ if not asset_id or not task_name or not filename: From b751c539c3d3f0d2aa9ed6846bac01ce1ad91eb5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 12:22:11 +0200 Subject: [PATCH 122/139] Publisher: Make sure to reset asset widget when hidden and reshown (#4886) * Make sure to reset asset widget when hidden and reshown * change '_soft_reset_enabled' only on controller reset --------- Co-authored-by: Jakub Trllo --- openpype/tools/publisher/widgets/assets_widget.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 3c559af259..a750d8d540 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -211,6 +211,10 @@ class AssetsDialog(QtWidgets.QDialog): layout.addWidget(asset_view, 1) layout.addLayout(btns_layout, 0) + controller.event_system.add_callback( + "controller.reset.finished", self._on_controller_reset + ) + asset_view.double_clicked.connect(self._on_ok_clicked) filter_input.textChanged.connect(self._on_filter_change) ok_btn.clicked.connect(self._on_ok_clicked) @@ -245,6 +249,10 @@ class AssetsDialog(QtWidgets.QDialog): new_pos.setY(new_pos.y() - int(self.height() / 2)) self.move(new_pos) + def _on_controller_reset(self): + # Change reset enabled so model is reset on show event + self._soft_reset_enabled = True + def showEvent(self, event): """Refresh asset model on show.""" super(AssetsDialog, self).showEvent(event) From d5ccdcbaab3b7946ad62730d968498ab0e19f612 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 13:21:46 +0200 Subject: [PATCH 123/139] fixing nightly workflow --- .github/workflows/nightly_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml index f1850762d9..3f8c75dce3 100644 --- a/.github/workflows/nightly_merge.yml +++ b/.github/workflows/nightly_merge.yml @@ -25,5 +25,5 @@ jobs: - name: Invoke pre-release workflow uses: benc-uk/workflow-dispatch@v1 with: - workflow: Nightly Prerelease + workflow: prerelease.yml token: ${{ secrets.YNPUT_BOT_TOKEN }} From edccc0f9e915d05843dbd0e1b1dc1513cc464aa3 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 21 Apr 2023 11:23:24 +0000 Subject: [PATCH 124/139] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 1d41f1aa5d..b9090cd8a1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.4" +__version__ = "3.15.5-nightly.1" From d03200238bbb1a0e57f14e88fe39902daed6c98f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 13:27:10 +0200 Subject: [PATCH 125/139] prerelease step with workflow dispatch for update bug. --- .github/workflows/prerelease.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index e8c619c6eb..8c5c733c08 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -65,3 +65,9 @@ jobs: source_ref: 'main' target_branch: 'develop' commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' + + - name: Invoke Update bug report workflow + uses: benc-uk/workflow-dispatch@v1 + with: + workflow: update_bug_report.yml + token: ${{ secrets.YNPUT_BOT_TOKEN }} \ No newline at end of file From 34b1ad105b76e7d69094741f668927b96d406f4d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:18:41 +0200 Subject: [PATCH 126/139] implemented collector for review instances to fix extract review issues (#4891) --- .../plugins/publish/collect_review_frames.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py b/openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py new file mode 100644 index 0000000000..6b41c0dd21 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import pyblish.api + + +class CollectReviewInfo(pyblish.api.InstancePlugin): + """Collect data required for review instances. + + ExtractReview plugin requires frame start/end, fps on instance data which + are missing on instances from TrayPublishes. + + Warning: + This is temporary solution to "make it work". Contains removed changes + from https://github.com/ynput/OpenPype/pull/4383 reduced only for + review instances. + """ + + label = "Collect Review Info" + order = pyblish.api.CollectorOrder + 0.491 + families = ["review"] + hosts = ["traypublisher"] + + def process(self, instance): + asset_entity = instance.data.get("assetEntity") + if instance.data.get("frameStart") is not None or not asset_entity: + self.log.debug("Missing required data on instance") + return + + asset_data = asset_entity["data"] + # Store collected data for logging + collected_data = {} + for key in ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + ): + if key in instance.data or key not in asset_data: + continue + value = asset_data[key] + collected_data[key] = value + instance.data[key] = value + self.log.debug("Collected data: {}".format(str(collected_data))) From cf7e704964d1db85476e0d2eacc7e5c53485a6ef Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 18:43:01 +0200 Subject: [PATCH 127/139] Collect `currentFile` context data separate from workfile instance (#4883) --- .../plugins/publish/collect_current_file.py | 32 +++-------------- .../plugins/publish/collect_workfile.py | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_workfile.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_current_file.py b/openpype/hosts/houdini/plugins/publish/collect_current_file.py index caf679f98b..7b55778803 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/collect_current_file.py @@ -4,15 +4,14 @@ import hou import pyblish.api -class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): +class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.01 + order = pyblish.api.CollectorOrder - 0.1 label = "Houdini Current File" hosts = ["houdini"] - families = ["workfile"] - def process(self, instance): + def process(self, context): """Inject the current working file""" current_file = hou.hipFile.path() @@ -34,26 +33,5 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): "saved correctly." ) - instance.context.data["currentFile"] = current_file - - folder, file = os.path.split(current_file) - filename, ext = os.path.splitext(file) - - instance.data.update({ - "setMembers": [current_file], - "frameStart": instance.context.data['frameStart'], - "frameEnd": instance.context.data['frameEnd'], - "handleStart": instance.context.data['handleStart'], - "handleEnd": instance.context.data['handleEnd'] - }) - - instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': file, - "stagingDir": folder, - }] - - self.log.info('Collected instance: {}'.format(file)) - self.log.info('Scene path: {}'.format(current_file)) - self.log.info('staging Dir: {}'.format(folder)) + context.data["currentFile"] = current_file + self.log.info('Current workfile path: {}'.format(current_file)) diff --git a/openpype/hosts/houdini/plugins/publish/collect_workfile.py b/openpype/hosts/houdini/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..a6e94ec29e --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_workfile.py @@ -0,0 +1,36 @@ +import os + +import pyblish.api + + +class CollectWorkfile(pyblish.api.InstancePlugin): + """Inject workfile representation into instance""" + + order = pyblish.api.CollectorOrder - 0.01 + label = "Houdini Workfile Data" + hosts = ["houdini"] + families = ["workfile"] + + def process(self, instance): + + current_file = instance.context.data["currentFile"] + folder, file = os.path.split(current_file) + filename, ext = os.path.splitext(file) + + instance.data.update({ + "setMembers": [current_file], + "frameStart": instance.context.data['frameStart'], + "frameEnd": instance.context.data['frameEnd'], + "handleStart": instance.context.data['handleStart'], + "handleEnd": instance.context.data['handleEnd'] + }) + + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] + + self.log.info('Collected instance: {}'.format(file)) + self.log.info('staging Dir: {}'.format(folder)) From f4ee2a7537ad393ed6991ea835e5cdb95d77f8c6 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 22 Apr 2023 03:25:51 +0000 Subject: [PATCH 128/139] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index b9090cd8a1..b43cc436bb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.5-nightly.1" +__version__ = "3.15.5-nightly.2" From 60d386b127badba113199c94111bd76de1dee041 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:53:17 +0200 Subject: [PATCH 129/139] :bug: fix missing review flag on instance with pre-render --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 536a0698f3..6697a1e59a 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -190,7 +190,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, # make sure rendered sequence on farm will # be used for extract review - if not instance.data["review"]: + if not instance.data.get("review"): instance.data["useSequenceForReview"] = False self.log.debug("instance.data: {}".format(pformat(instance.data))) From ed1fd82ff21877eb517c14865ce48da7149637e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Apr 2023 13:16:38 +0200 Subject: [PATCH 130/139] Scene inventory: Model refresh fix with cherry picking (#4895) * fix bug in model refresh * fix signal callbacks * rename '_refresh_callback' to '_on_refresh_request' --- openpype/tools/sceneinventory/model.py | 169 +++++++++++++----------- openpype/tools/sceneinventory/window.py | 9 +- 2 files changed, 98 insertions(+), 80 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 63d2945145..5cc849bb9e 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -199,90 +199,103 @@ class InventoryModel(TreeModel): """Refresh the model""" host = registered_host() - if not items: # for debugging or testing, injecting items from outside + # for debugging or testing, injecting items from outside + if items is None: if isinstance(host, ILoadHost): items = host.get_containers() - else: + elif hasattr(host, "ls"): items = host.ls() + else: + items = [] self.clear() - - if self._hierarchy_view and selected: - if not hasattr(host.pipeline, "update_hierarchy"): - # If host doesn't support hierarchical containers, then - # cherry-pick only. - self.add_items((item for item in items - if item["objectName"] in selected)) - return - - # Update hierarchy info for all containers - items_by_name = {item["objectName"]: item - for item in host.pipeline.update_hierarchy(items)} - - selected_items = set() - - def walk_children(names): - """Select containers and extend to chlid containers""" - for name in [n for n in names if n not in selected_items]: - selected_items.add(name) - item = items_by_name[name] - yield item - - for child in walk_children(item["children"]): - yield child - - items = list(walk_children(selected)) # Cherry-picked and extended - - # Cut unselected upstream containers - for item in items: - if not item.get("parent") in selected_items: - # Parent not in selection, this is root item. - item["parent"] = None - - parents = [self._root_item] - - # The length of `items` array is the maximum depth that a - # hierarchy could be. - # Take this as an easiest way to prevent looping forever. - maximum_loop = len(items) - count = 0 - while items: - if count > maximum_loop: - self.log.warning("Maximum loop count reached, possible " - "missing parent node.") - break - - _parents = list() - for parent in parents: - _unparented = list() - - def _children(): - """Child item provider""" - for item in items: - if item.get("parent") == parent.get("objectName"): - # (NOTE) - # Since `self._root_node` has no "objectName" - # entry, it will be paired with root item if - # the value of key "parent" is None, or not - # having the key. - yield item - else: - # Not current parent's child, try next - _unparented.append(item) - - self.add_items(_children(), parent) - - items[:] = _unparented - - # Parents of next level - for group_node in parent.children(): - _parents += group_node.children() - - parents[:] = _parents - count += 1 - - else: + if not selected or not self._hierarchy_view: self.add_items(items) + return + + if ( + not hasattr(host, "pipeline") + or not hasattr(host.pipeline, "update_hierarchy") + ): + # If host doesn't support hierarchical containers, then + # cherry-pick only. + self.add_items(( + item + for item in items + if item["objectName"] in selected + )) + return + + # TODO find out what this part does. Function 'update_hierarchy' is + # available only in 'blender' at this moment. + + # Update hierarchy info for all containers + items_by_name = { + item["objectName"]: item + for item in host.pipeline.update_hierarchy(items) + } + + selected_items = set() + + def walk_children(names): + """Select containers and extend to chlid containers""" + for name in [n for n in names if n not in selected_items]: + selected_items.add(name) + item = items_by_name[name] + yield item + + for child in walk_children(item["children"]): + yield child + + items = list(walk_children(selected)) # Cherry-picked and extended + + # Cut unselected upstream containers + for item in items: + if not item.get("parent") in selected_items: + # Parent not in selection, this is root item. + item["parent"] = None + + parents = [self._root_item] + + # The length of `items` array is the maximum depth that a + # hierarchy could be. + # Take this as an easiest way to prevent looping forever. + maximum_loop = len(items) + count = 0 + while items: + if count > maximum_loop: + self.log.warning("Maximum loop count reached, possible " + "missing parent node.") + break + + _parents = list() + for parent in parents: + _unparented = list() + + def _children(): + """Child item provider""" + for item in items: + if item.get("parent") == parent.get("objectName"): + # (NOTE) + # Since `self._root_node` has no "objectName" + # entry, it will be paired with root item if + # the value of key "parent" is None, or not + # having the key. + yield item + else: + # Not current parent's child, try next + _unparented.append(item) + + self.add_items(_children(), parent) + + items[:] = _unparented + + # Parents of next level + for group_node in parent.children(): + _parents += group_node.children() + + parents[:] = _parents + count += 1 def add_items(self, items, parent=None): """Add the items to the model. diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 89424fd746..6ee1c0d38e 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -107,8 +107,8 @@ class SceneInventoryWindow(QtWidgets.QDialog): view.hierarchy_view_changed.connect( self._on_hierarchy_view_change ) - view.data_changed.connect(self.refresh) - refresh_button.clicked.connect(self.refresh) + view.data_changed.connect(self._on_refresh_request) + refresh_button.clicked.connect(self._on_refresh_request) update_all_button.clicked.connect(self._on_update_all) self._update_all_button = update_all_button @@ -139,6 +139,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): """ + def _on_refresh_request(self): + """Signal callback to trigger 'refresh' without any arguments.""" + + self.refresh() + def refresh(self, items=None): with preserve_expanded_rows( tree_view=self._view, From ebcd48d13875f472a4c5d1eddc9e4a834b37133d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Apr 2023 17:36:26 +0200 Subject: [PATCH 131/139] Publisher: Keep track about current context and fix context selection widget (#4892) * keep track about last context so it can be updated on context change * don't use '_asset_name' attribute for validation of selected asset * use current context after publisher window close --- .../tools/publisher/widgets/create_widget.py | 39 ++++++++++++++++++- openpype/tools/publisher/window.py | 3 ++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index ef9c5b98fe..db20b21ed7 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -282,6 +282,9 @@ class CreateWidget(QtWidgets.QWidget): thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) + controller.event_system.add_callback( + "main.window.closed", self._on_main_window_close + ) controller.event_system.add_callback( "plugins.refresh.finished", self._on_plugins_refresh ) @@ -316,6 +319,10 @@ class CreateWidget(QtWidgets.QWidget): self._first_show = True self._last_thumbnail_path = None + self._last_current_context_asset = None + self._last_current_context_task = None + self._use_current_context = True + @property def current_asset_name(self): return self._controller.current_asset_name @@ -356,12 +363,39 @@ class CreateWidget(QtWidgets.QWidget): if check_prereq: self._invalidate_prereq() + def _on_main_window_close(self): + """Publisher window was closed.""" + + # Use current context on next refresh + self._use_current_context = True + def refresh(self): + current_asset_name = self._controller.current_asset_name + current_task_name = self._controller.current_task_name + # Get context before refresh to keep selection of asset and # task widgets asset_name = self._get_asset_name() task_name = self._get_task_name() + # Replace by current context if last loaded context was + # 'current context' before reset + if ( + self._use_current_context + or ( + self._last_current_context_asset + and asset_name == self._last_current_context_asset + and task_name == self._last_current_context_task + ) + ): + asset_name = current_asset_name + task_name = current_task_name + + # Store values for future refresh + self._last_current_context_asset = current_asset_name + self._last_current_context_task = current_task_name + self._use_current_context = False + self._prereq_available = False # Disable context widget so refresh of asset will use context asset @@ -398,7 +432,10 @@ class CreateWidget(QtWidgets.QWidget): prereq_available = False creator_btn_tooltips.append("Creator is not selected") - if self._context_change_is_enabled() and self._asset_name is None: + if ( + self._context_change_is_enabled() + and self._get_asset_name() is None + ): # QUESTION how to handle invalid asset? prereq_available = False creator_btn_tooltips.append("Context is not selected") diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 0615157e1b..e94979142a 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -406,6 +406,9 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input.setText("") # clear comment self._reset_on_show = True self._controller.clear_thumbnail_temp_dir_path() + # Trigger custom event that should be captured only in UI + # - backend (controller) must not be dependent on this event topic!!! + self._controller.event_system.emit("main.window.closed", {}, "window") super(PublisherWindow, self).closeEvent(event) def leaveEvent(self, event): From afa3f563e43be117af30bd2896983b7bd7027d9f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 24 Apr 2023 15:41:57 +0000 Subject: [PATCH 132/139] [Automated] Release --- CHANGELOG.md | 303 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 305 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aeb546c14..16deaaa4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,309 @@ # Changelog +## [3.15.5](https://github.com/ynput/OpenPype/tree/3.15.5) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.4...3.15.5) + +### **🚀 Enhancements** + + +
+Maya: Playblast profiles #4777 + +Support playblast profiles.This enables studios to customize what playblast settings should be on a per task and/or subset basis. For example `modeling` should have `Wireframe On Shaded` enabled, while all other tasks should have it disabled. + + +___ + +
+ + +
+Maya: Support .abc files directly for Arnold standin look assignment #4856 + +If `.abc` file is loaded into arnold standin support look assignment through the `cbId` attributes in the alembic file. + + +___ + +
+ + +
+Maya: Hide animation instance in creator #4872 + +- Hide animation instance in creator +- Add inventory action to recreate animation publish instance for loaded rigs + + +___ + +
+ + +
+Unreal: Render Creator enhancements #4477 + +Improvements to the creator for render family + +This PR introduces some enhancements to the creator for the render family in Unreal Engine: +- Added the option to create a new, empty sequence for the render. +- Added the option to not include the whole hierarchy for the selected sequence. +- Improvements of the error messages. + + +___ + +
+ + +
+Unreal: Added settings for rendering #4575 + +Added settings for rendering in Unreal Engine. + +Two settings has been added: +- Pre roll frames, to set how many frames are used to load the scene before starting the actual rendering. +- Configuration path, to allow to save a preset of settings from Unreal, and use it for rendering. + + +___ + +
+ + +
+Global: Optimize anatomy formatting by only formatting used templates instead #4784 + +Optimization to not format full anatomy when only a single template is used. Instead format only the single template instead. + + +___ + +
+ + +
+Patchelf version locked #4853 + +For Centos dockerfile it is necessary to lock the patchelf version to the older, otherwise the build process fails. + +___ + +
+ + +
+Houdini: Implement `switch` method on loaders #4866 + +Implement `switch` method on loaders + + +___ + +
+ + +
+Code: Tweak docstrings and return type hints #4875 + +Tweak docstrings and return type hints for functions in `openpype.client.entities`. + + +___ + +
+ + +
+Publisher: Clear comment on successful publish and on window close #4885 + +Clear comment text field on successful publish and on window close. + + +___ + +
+ + +
+Publisher: Make sure to reset asset widget when hidden and reshown #4886 + +Make sure to reset asset widget when hidden and reshown. Without this the asset list would never refresh in the set asset widget when changing context on an existing instance and thus would not show new assets from after the first time launching that widget. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Fix nested model instances. #4852 + +Fix nested model instance under review instance, where data collection was not including "Display Lights" and "Focal Length". + + +___ + +
+ + +
+Maya: Make default namespace naming backwards compatible #4873 + +Namespaces of loaded references are now _by default_ back to what they were before #4511 + + +___ + +
+ + +
+Nuke: Legacy convertor skips deprecation warnings #4846 + +Nuke legacy convertor was triggering deprecated function which is causing a lot of logs which slows down whole process. Changed the convertor to skip all nodes without `AVALON_TAB` to avoid the warnings. + + +___ + +
+ + +
+3dsmax: move startup script logic to hook #4849 + +Startup script for OpenPype was interfering with Open Last Workfile feature. Moving this loggic from simple command line argument in the Settings to pre-launch hook is solving the order of command line arguments and making both features work. + + +___ + +
+ + +
+Maya: Don't change time slider ranges in `get_frame_range` #4858 + +Don't change time slider ranges in `get_frame_range` + + +___ + +
+ + +
+Maya: Looks - calculate hash for tx texture #4878 + +Texture hash is calculated for textures used in published look and it is used as key in dictionary. In recent changes, this hash is not calculated for TX files, resulting in `None` value as key in dictionary, crashing publishing. This PR is adding texture hash for TX files to solve that issue. + + +___ + +
+ + +
+Houdini: Collect `currentFile` context data separate from workfile instance #4883 + +Fix publishing without an active workfile instance due to missing `currentFile` data.Now collect `currentFile` into context in houdini through context plugin no matter the active instances. + + +___ + +
+ + +
+Nuke: fixed broken slate workflow once published on deadline #4887 + +Slate workflow is now working as expected and Validate Sequence Frames is not raising the once slate frame is included. + + +___ + +
+ + +
+Add fps as instance.data in collect review in Houdini. #4888 + +fix the bug of failing to publish extract review in HoudiniOriginal error: +```python + File "OpenPype\build\exe.win-amd64-3.9\openpype\plugins\publish\extract_review.py", line 516, in prepare_temp_data + "fps": float(instance.data["fps"]), +KeyError: 'fps' +``` + + +___ + +
+ + +
+TrayPublisher: Fill missing data for instances with review #4891 + +Fill required data to instance in traypublisher if instance has review family. The data are required by ExtractReview and it would be complicated to do proper fix at this moment! The collector does for review instances what did https://github.com/ynput/OpenPype/pull/4383 + + +___ + +
+ + +
+Publisher: Keep track about current context and fix context selection widget #4892 + +Change selected context to current context on reset. Fix bug when context widget is re-enabled. + + +___ + +
+ + +
+Scene inventory: Model refresh fix with cherry picking #4895 + +Fix cherry pick issue in scene inventory. + + +___ + +
+ + +
+Nuke: Pre-render and missing review flag on instance causing crash #4897 + +If instance created in nuke was missing `review` flag, collector crashed. + + +___ + +
+ +### **Merged pull requests** + + +
+After Effects: fix handles KeyError #4727 + +Sometimes when publishing with AE (we only saw this error on AE 2023), we got a KeyError for the handles in the "Collect Workfile" step. So I did get the handles from the context if ther's no handles in the asset entity. + + +___ + +
+ + + + ## [3.15.4](https://github.com/ynput/OpenPype/tree/3.15.4) diff --git a/openpype/version.py b/openpype/version.py index b43cc436bb..02537af762 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.5-nightly.2" +__version__ = "3.15.5" diff --git a/pyproject.toml b/pyproject.toml index b97ad8923c..2f40d58f56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.4" # OpenPype +version = "3.15.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 55957621645e3ddb6e313916509cbcad275a76e8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 17:47:33 +0200 Subject: [PATCH 133/139] Fusion: Simplify creator icons code (#4899) * Simplify setting creator icons * Use font-awesome 5 explicitly --- openpype/hosts/fusion/plugins/create/create_saver.py | 6 +----- openpype/hosts/fusion/plugins/create/create_workfile.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 56085b0a06..cedc4029fa 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,7 +1,5 @@ import os -import qtawesome - from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, @@ -28,6 +26,7 @@ class CreateSaver(Creator): family = "render" default_variants = ["Main", "Mask"] description = "Fusion Saver to generate image sequence" + icon = "fa5.eye" instance_attributes = ["reviewable"] @@ -89,9 +88,6 @@ class CreateSaver(Creator): self._add_instance_to_context(created_instance) - def get_icon(self): - return qtawesome.icon("fa.eye", color="white") - def update_instances(self, update_list): for created_inst, _changes in update_list: new_data = created_inst.data_to_store() diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 0bb3a0d3d4..40721ea88a 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -1,5 +1,3 @@ -import qtawesome - from openpype.hosts.fusion.api import ( get_current_comp ) @@ -15,6 +13,7 @@ class FusionWorkfileCreator(AutoCreator): identifier = "workfile" family = "workfile" label = "Workfile" + icon = "fa5.file" default_variant = "Main" @@ -104,6 +103,3 @@ class FusionWorkfileCreator(AutoCreator): existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name - - def get_icon(self): - return qtawesome.icon("fa.file-o", color="white") From 0ef59fcb39a033a11b94e0d3884b1b48029e75eb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Apr 2023 08:18:46 +0200 Subject: [PATCH 134/139] adding ci user and email --- .github/workflows/update_bug_report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update_bug_report.yml b/.github/workflows/update_bug_report.yml index 9f44d7c7a6..7a1bfb7bfd 100644 --- a/.github/workflows/update_bug_report.yml +++ b/.github/workflows/update_bug_report.yml @@ -18,6 +18,8 @@ jobs: uses: ynput/gha-populate-form-version@main with: github_token: ${{ secrets.YNPUT_BOT_TOKEN }} + github_user: ${{ secrets.CI_USER }} + github_email: ${{ secrets.CI_EMAIL }} registry: github dropdown: _version limit_to: 100 From 0567701ddb827f9644e9f9631f56d4b3c73d01c5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 25 Apr 2023 06:32:26 +0000 Subject: [PATCH 135/139] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c4073ed1af..fe86a8400b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,10 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.5 + - 3.15.5-nightly.2 + - 3.15.5-nightly.1 + - 3.15.4 - 3.15.4-nightly.3 - 3.15.4-nightly.2 - 3.15.4-nightly.1 @@ -131,10 +135,6 @@ body: - 3.13.1-nightly.2 - 3.13.1-nightly.1 - 3.13.0 - - 3.13.0-nightly.1 - - 3.12.3-nightly.3 - - 3.12.3-nightly.2 - - 3.12.3-nightly.1 validations: required: true - type: dropdown @@ -166,8 +166,8 @@ body: label: Are there any labels you wish to add? description: Please search labels and identify those related to your bug. options: - - label: I have added the relevant labels to the bug report. - required: true + - label: I have added the relevant labels to the bug report. + required: true - type: textarea id: logs attributes: From 4ed1c1f65d6f99ece0f35c404e6ca40c3ee2c5fd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Apr 2023 10:29:12 +0200 Subject: [PATCH 136/139] Enhancement: Fix PySide 6.5 support for loader (#4900) * Reverse inheritance order to avoid PySide6.5 bug `PYSIDE-2294` & `PYSIDE-2304` * Fix PySide6 support --- openpype/tools/loader/model.py | 2 +- openpype/tools/publisher/widgets/list_view_widgets.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 14671e341f..e5d8400031 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -123,7 +123,7 @@ class BaseRepresentationModel(object): self.remote_provider = remote_provider -class SubsetsModel(TreeModel, BaseRepresentationModel): +class SubsetsModel(BaseRepresentationModel, TreeModel): doc_fetched = QtCore.Signal() refreshed = QtCore.Signal(bool) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 227ae7bda9..cb5a203130 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -1039,7 +1039,8 @@ class InstanceListView(AbstractInstanceView): proxy_index = proxy_model.mapFromSource(select_indexes[0]) selection_model.setCurrentIndex( proxy_index, - selection_model.ClearAndSelect | selection_model.Rows + QtCore.QItemSelectionModel.ClearAndSelect + | QtCore.QItemSelectionModel.Rows ) return From 38347ece5a7e60f23d643568e1268e3900f8fa21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:37:49 +0200 Subject: [PATCH 137/139] Publisher: Small style changes (#4894) * border hover has color without alpha * changed border radius to 0.2em * removed border from scroll area * variant hint button has 0.5em width * inputs in attribute definitions have smaller padding * label is shown only to value inputs and added tooltips * change spacing for attribute befinitions * align labels to right * implemented 'ComboBox' which ignores wheel events and has styled delegate * PixmalLabel has minimum sizeHint * cards are smaller * renamed 'Options' to 'Context' * implemented active state changes in card view * set object name of main window to "PublishWindow" * plugin don't have to pass 'title' to an error * fix PySide6 support for custom keysequences * check for exact match for all bindings * added validation of exact match for save shortcut --- openpype/pipeline/publish/publish_plugins.py | 2 +- openpype/style/data.json | 2 +- openpype/style/style.css | 29 ++++++-- openpype/tools/attribute_defs/widgets.py | 10 ++- openpype/tools/publisher/constants.py | 5 +- openpype/tools/publisher/control.py | 11 ++- .../publisher/widgets/card_view_widgets.py | 72 ++++++++++++++++--- .../tools/publisher/widgets/create_widget.py | 4 ++ .../publisher/widgets/list_view_widgets.py | 5 +- .../publisher/widgets/precreate_widget.py | 14 +++- openpype/tools/publisher/widgets/widgets.py | 33 +++++++-- openpype/tools/publisher/window.py | 19 +++-- openpype/tools/utils/__init__.py | 2 + openpype/tools/utils/widgets.py | 34 ++++++++- 14 files changed, 208 insertions(+), 34 deletions(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 331235fadc..a38896ec8e 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -45,7 +45,7 @@ class PublishValidationError(Exception): def __init__(self, message, title=None, description=None, detail=None): self.message = message - self.title = title or "< Missing title >" + self.title = title self.description = description or message self.detail = detail super(PublishValidationError, self).__init__(message) diff --git a/openpype/style/data.json b/openpype/style/data.json index 404ca6944c..bea2a3d407 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -48,7 +48,7 @@ "bg-view-selection-hover": "rgba(92, 173, 214, .8)", "border": "#373D48", - "border-hover": "rgba(168, 175, 189, .3)", + "border-hover": "rgb(92, 99, 111)", "border-focus": "rgb(92, 173, 214)", "restart-btn-bg": "#458056", diff --git a/openpype/style/style.css b/openpype/style/style.css index da477eeefa..29abb1d351 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -35,6 +35,11 @@ QWidget:disabled { color: {color:font-disabled}; } +/* Some DCCs have set borders to solid color */ +QScrollArea { + border: none; +} + QLabel { background: transparent; } @@ -42,7 +47,7 @@ QLabel { /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; - border-radius: 0.3em; + border-radius: 0.2em; background: {color:bg-inputs}; padding: 0.1em; } @@ -226,7 +231,7 @@ QMenu::separator { /* Combobox */ QComboBox { border: 1px solid {color:border}; - border-radius: 3px; + border-radius: 0.2em; padding: 1px 3px 1px 3px; background: {color:bg-inputs}; } @@ -474,7 +479,6 @@ QAbstractItemView:disabled{ } QAbstractItemView::item:hover { - /* color: {color:bg-view-hover}; */ background: {color:bg-view-hover}; } @@ -743,7 +747,7 @@ OverlayMessageWidget QWidget { #TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { background: transparent; - border-radius: 0.3em; + border-radius: 0.2em; } #TypeEditor:focus, #ToolEditor:focus, #NameEditor:focus, #NumberEditor:focus { @@ -860,7 +864,13 @@ OverlayMessageWidget QWidget { background: {color:bg-view-hover}; } -/* New Create/Publish UI */ +/* Publisher UI (Create/Publish) */ +#PublishWindow QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { + padding: 1px; +} +#PublishWindow QComboBox { + padding: 1px 1px 1px 0.2em; +} PublisherTabsWidget { background: {color:publisher:tab-bg}; } @@ -944,6 +954,7 @@ PixmapButton:disabled { border-top-left-radius: 0px; padding-top: 0.5em; padding-bottom: 0.5em; + width: 0.5em; } #VariantInput[state="new"], #VariantInput[state="new"]:focus, #VariantInput[state="new"]:hover { border-color: {color:publisher:success}; @@ -1072,7 +1083,7 @@ ValidationArtistMessage QLabel { #AssetNameInputWidget { background: {color:bg-inputs}; border: 1px solid {color:border}; - border-radius: 0.3em; + border-radius: 0.2em; } #AssetNameInputWidget QWidget { @@ -1465,6 +1476,12 @@ CreateNextPageOverlay { } /* Attribute Definition widgets */ +AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { + padding: 1px; +} +AttributeDefinitionsWidget QComboBox { + padding: 1px 1px 1px 0.2em; +} InViewButton, InViewButton:disabled { background: transparent; } diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 0d4e1e88a9..d46c238da1 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -1,4 +1,3 @@ -import uuid import copy from qtpy import QtWidgets, QtCore @@ -126,7 +125,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): row = 0 for attr_def in attr_defs: - if not isinstance(attr_def, UIDef): + if attr_def.is_value_def: if attr_def.key in self._current_keys: raise KeyError( "Duplicated key \"{}\"".format(attr_def.key)) @@ -144,11 +143,16 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols - if attr_def.label: + if attr_def.is_value_def and attr_def.label: label_widget = QtWidgets.QLabel(attr_def.label, self) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) layout.addWidget( label_widget, row, 0, 1, expand_cols ) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 5d23886aa8..660fccecf1 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -2,7 +2,7 @@ from qtpy import QtCore, QtGui # ID of context item in instance view CONTEXT_ID = "context" -CONTEXT_LABEL = "Options" +CONTEXT_LABEL = "Context" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" @@ -15,6 +15,9 @@ VARIANT_TOOLTIP = ( "\nnumerical characters (0-9) dot (\".\") or underscore (\"_\")." ) +INPUTS_LAYOUT_HSPACING = 4 +INPUTS_LAYOUT_VSPACING = 2 + # Roles for instance views INSTANCE_ID_ROLE = QtCore.Qt.UserRole + 1 SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2 diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7754e4aa02..4b083d4bc8 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -163,7 +163,7 @@ class AssetDocsCache: return copy.deepcopy(self._full_asset_docs_by_name[asset_name]) -class PublishReport: +class PublishReportMaker: """Report for single publishing process. Report keeps current state of publishing and currently processed plugin. @@ -784,6 +784,13 @@ class PublishValidationErrors: # Make sure the cached report is cleared plugin_id = self._plugins_proxy.get_plugin_id(plugin) + if not error.title: + if hasattr(plugin, "label") and plugin.label: + plugin_label = plugin.label + else: + plugin_label = plugin.__name__ + error.title = plugin_label + self._error_items.append( ValidationErrorItem.from_result(plugin_id, error, instance) ) @@ -1674,7 +1681,7 @@ class PublisherController(BasePublisherController): # pyblish.api.Context self._publish_context = None # Pyblish report - self._publish_report = PublishReport(self) + self._publish_report = PublishReportMaker(self) # Store exceptions of validation error self._publish_validation_errors = PublishValidationErrors() diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 0734e1bc27..13715bc73c 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -9,7 +9,7 @@ Only one item can be selected at a time. ``` : Icon. Can have Warning icon when context is not right ┌──────────────────────┐ -│ Options │ +│ Context │ │ ────────── │ │ [x]│ │ [x]│ @@ -202,7 +202,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget): class InstanceGroupWidget(BaseGroupWidget): """Widget wrapping instances under group.""" - active_changed = QtCore.Signal() + active_changed = QtCore.Signal(str, str, bool) def __init__(self, group_icons, *args, **kwargs): super(InstanceGroupWidget, self).__init__(*args, **kwargs) @@ -253,13 +253,16 @@ class InstanceGroupWidget(BaseGroupWidget): instance, group_icon, self ) widget.selected.connect(self._on_widget_selection) - widget.active_changed.connect(self.active_changed) + widget.active_changed.connect(self._on_active_changed) self._widgets_by_id[instance.id] = widget self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 self._update_ordered_item_ids() + def _on_active_changed(self, instance_id, value): + self.active_changed.emit(self.group_name, instance_id, value) + class CardWidget(BaseClickableFrame): """Clickable card used as bigger button.""" @@ -332,7 +335,7 @@ class ContextCardWidget(CardWidget): icon_layout.addWidget(icon_widget) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 10, 5) + layout.setContentsMargins(0, 2, 10, 2) layout.addLayout(icon_layout, 0) layout.addWidget(label_widget, 1) @@ -363,7 +366,7 @@ class ConvertorItemCardWidget(CardWidget): icon_layout.addWidget(icon_widget) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 10, 5) + layout.setContentsMargins(0, 2, 10, 2) layout.addLayout(icon_layout, 0) layout.addWidget(label_widget, 1) @@ -377,7 +380,7 @@ class ConvertorItemCardWidget(CardWidget): class InstanceCardWidget(CardWidget): """Card widget representing instance.""" - active_changed = QtCore.Signal() + active_changed = QtCore.Signal(str, bool) def __init__(self, instance, group_icon, parent): super(InstanceCardWidget, self).__init__(parent) @@ -424,7 +427,7 @@ class InstanceCardWidget(CardWidget): top_layout.addWidget(expand_btn, 0) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 10, 5) + layout.setContentsMargins(0, 2, 10, 2) layout.addLayout(top_layout) layout.addWidget(detail_widget) @@ -445,6 +448,10 @@ class InstanceCardWidget(CardWidget): def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) + @property + def is_active(self): + return self._active_checkbox.isChecked() + def set_active(self, new_value): """Set instance as active.""" checkbox_value = self._active_checkbox.isChecked() @@ -515,7 +522,7 @@ class InstanceCardWidget(CardWidget): return self.instance["active"] = new_value - self.active_changed.emit() + self.active_changed.emit(self._id, new_value) def _on_expend_clicked(self): self._set_expanded() @@ -584,6 +591,45 @@ class InstanceCardView(AbstractInstanceView): result.setWidth(width) return result + def _toggle_instances(self, value): + if not self._active_toggle_enabled: + return + + widgets = self._get_selected_widgets() + changed = False + for widget in widgets: + if not isinstance(widget, InstanceCardWidget): + continue + + is_active = widget.is_active + if value == -1: + widget.set_active(not is_active) + changed = True + continue + + _value = bool(value) + if is_active is not _value: + widget.set_active(_value) + changed = True + + if changed: + self.active_changed.emit() + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Space: + self._toggle_instances(-1) + return True + + elif event.key() == QtCore.Qt.Key_Backspace: + self._toggle_instances(0) + return True + + elif event.key() == QtCore.Qt.Key_Return: + self._toggle_instances(1) + return True + + return super(InstanceCardView, self).keyPressEvent(event) + def _get_selected_widgets(self): output = [] if ( @@ -742,7 +788,15 @@ class InstanceCardView(AbstractInstanceView): for widget in self._widgets_by_group.values(): widget.update_instance_values() - def _on_active_changed(self): + def _on_active_changed(self, group_name, instance_id, value): + group_widget = self._widgets_by_group[group_name] + instance_widget = group_widget.get_widget_by_item_id(instance_id) + if instance_widget.is_selected: + for widget in self._get_selected_widgets(): + if isinstance(widget, InstanceCardWidget): + widget.set_active(value) + else: + self._select_item_clear(instance_id, group_name, instance_widget) self.active_changed.emit() def _on_widget_selection(self, instance_id, group_name, selection_type): diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index db20b21ed7..30980af03d 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -22,6 +22,8 @@ from ..constants import ( CREATOR_IDENTIFIER_ROLE, CREATOR_THUMBNAIL_ENABLED_ROLE, CREATOR_SORT_ROLE, + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, ) SEPARATORS = ("---separator---", "---") @@ -198,6 +200,8 @@ class CreateWidget(QtWidgets.QWidget): variant_subset_layout = QtWidgets.QFormLayout(variant_subset_widget) variant_subset_layout.setContentsMargins(0, 0, 0, 0) + variant_subset_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + variant_subset_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) variant_subset_layout.addRow("Variant", variant_widget) variant_subset_layout.addRow("Subset", subset_name_input) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index cb5a203130..557e6559c8 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -11,7 +11,7 @@ selection can be enabled disabled using checkbox or keyboard key presses: - Backspace - disable selection ``` -|- Options +|- Context |- [x] | |- [x] | |- [x] @@ -486,6 +486,9 @@ class InstanceListView(AbstractInstanceView): group_widget.set_expanded(expanded) def _on_toggle_request(self, toggle): + if not self._active_toggle_enabled: + return + selected_instance_ids = self._instance_view.get_selected_instance_ids() if toggle == -1: active = None diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py index 3037a0e12d..3bf0bc3657 100644 --- a/openpype/tools/publisher/widgets/precreate_widget.py +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -2,6 +2,8 @@ from qtpy import QtWidgets, QtCore from openpype.tools.attribute_defs import create_widget_for_attr_def +from ..constants import INPUTS_LAYOUT_HSPACING, INPUTS_LAYOUT_VSPACING + class PreCreateWidget(QtWidgets.QWidget): def __init__(self, parent): @@ -81,6 +83,8 @@ class AttributesWidget(QtWidgets.QWidget): layout = QtWidgets.QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) self._layout = layout @@ -117,8 +121,16 @@ class AttributesWidget(QtWidgets.QWidget): col_num = 2 - expand_cols - if attr_def.label: + if attr_def.is_value_def and attr_def.label: label_widget = QtWidgets.QLabel(attr_def.label, self) + tooltip = attr_def.tooltip + if tooltip: + label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) self._layout.addWidget( label_widget, row, 0, 1, expand_cols ) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index d2ce1fbcb2..cd1f1f5a96 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -9,7 +9,7 @@ import collections from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from openpype.lib.attribute_definitions import UnknownDef, UIDef +from openpype.lib.attribute_definitions import UnknownDef from openpype.tools.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -36,6 +36,8 @@ from .icons import ( from ..constants import ( VARIANT_TOOLTIP, ResetKeySequence, + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, ) @@ -1098,6 +1100,8 @@ class GlobalAttrsWidget(QtWidgets.QWidget): btns_layout.addWidget(cancel_btn) main_layout = QtWidgets.QFormLayout(self) + main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) main_layout.addRow("Variant", variant_input) main_layout.addRow("Asset", asset_value_widget) main_layout.addRow("Task", task_value_widget) @@ -1346,6 +1350,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.setColumnStretch(0, 0) content_layout.setColumnStretch(1, 1) content_layout.setAlignment(QtCore.Qt.AlignTop) + content_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) row = 0 for attr_def, attr_instances, values in result: @@ -1371,9 +1377,19 @@ class CreatorAttrsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols - label = attr_def.label or attr_def.key + label = None + if attr_def.is_value_def: + label = attr_def.label or attr_def.key if label: label_widget = QtWidgets.QLabel(label, self) + tooltip = attr_def.tooltip + if tooltip: + label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) content_layout.addWidget( label_widget, row, 0, 1, expand_cols ) @@ -1474,6 +1490,8 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): attr_def_layout = QtWidgets.QGridLayout(attr_def_widget) attr_def_layout.setColumnStretch(0, 0) attr_def_layout.setColumnStretch(1, 1) + attr_def_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + attr_def_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.addWidget(attr_def_widget, 0) @@ -1501,12 +1519,19 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): expand_cols = 1 col_num = 2 - expand_cols - label = attr_def.label or attr_def.key + label = None + if attr_def.is_value_def: + label = attr_def.label or attr_def.key if label: label_widget = QtWidgets.QLabel(label, content_widget) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) attr_def_layout.addWidget( label_widget, row, 0, 1, expand_cols ) @@ -1517,7 +1542,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): ) row += 1 - if isinstance(attr_def, UIDef): + if not attr_def.is_value_def: continue widget.value_changed.connect(self._input_value_changed) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index e94979142a..b3471163ae 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -46,6 +46,8 @@ class PublisherWindow(QtWidgets.QDialog): def __init__(self, parent=None, controller=None, reset_on_show=None): super(PublisherWindow, self).__init__(parent) + self.setObjectName("PublishWindow") + self.setWindowTitle("OpenPype publisher") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) @@ -440,15 +442,24 @@ class PublisherWindow(QtWidgets.QDialog): event.accept() return - if event.matches(QtGui.QKeySequence.Save): + save_match = event.matches(QtGui.QKeySequence.Save) + if save_match == QtGui.QKeySequence.ExactMatch: if not self._controller.publish_has_started: self._save_changes(True) event.accept() return - if ResetKeySequence.matches( - QtGui.QKeySequence(event.key() | event.modifiers()) - ): + # PySide6 Support + if hasattr(event, "keyCombination"): + reset_match_result = ResetKeySequence.matches( + QtGui.QKeySequence(event.keyCombination()) + ) + else: + reset_match_result = ResetKeySequence.matches( + QtGui.QKeySequence(event.modifiers() | event.key()) + ) + + if reset_match_result == QtGui.QKeySequence.ExactMatch: if not self.controller.publish_is_running: self.reset() event.accept() diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 4292e2d726..4149763f80 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -1,6 +1,7 @@ from .widgets import ( FocusSpinBox, FocusDoubleSpinBox, + ComboBox, CustomTextComboBox, PlaceholderLineEdit, BaseClickableFrame, @@ -38,6 +39,7 @@ from .overlay_messages import ( __all__ = ( "FocusSpinBox", "FocusDoubleSpinBox", + "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", "BaseClickableFrame", diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index b416c56797..bae89aeb09 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -41,7 +41,28 @@ class FocusDoubleSpinBox(QtWidgets.QDoubleSpinBox): super(FocusDoubleSpinBox, self).wheelEvent(event) -class CustomTextComboBox(QtWidgets.QComboBox): +class ComboBox(QtWidgets.QComboBox): + """Base of combobox with pre-implement changes used in tools. + + Combobox is using styled delegate by default so stylesheets are propagated. + + Items are not changed on scroll until the combobox is in focus. + """ + + def __init__(self, *args, **kwargs): + super(ComboBox, self).__init__(*args, **kwargs) + delegate = QtWidgets.QStyledItemDelegate() + self.setItemDelegate(delegate) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self._delegate = delegate + + def wheelEvent(self, event): + if self.hasFocus(): + return super(ComboBox, self).wheelEvent(event) + + +class CustomTextComboBox(ComboBox): """Combobox which can have different text showed.""" def __init__(self, *args, **kwargs): @@ -253,6 +274,9 @@ class PixmapLabel(QtWidgets.QLabel): self._empty_pixmap = QtGui.QPixmap(0, 0) self._source_pixmap = pixmap + self._last_width = 0 + self._last_height = 0 + def set_source_pixmap(self, pixmap): """Change source image.""" self._source_pixmap = pixmap @@ -263,6 +287,12 @@ class PixmapLabel(QtWidgets.QLabel): size += size % 2 return size, size + def minimumSizeHint(self): + width, height = self._get_pix_size() + if width != self._last_width or height != self._last_height: + self._set_resized_pix() + return QtCore.QSize(width, height) + def _set_resized_pix(self): if self._source_pixmap is None: self.setPixmap(self._empty_pixmap) @@ -276,6 +306,8 @@ class PixmapLabel(QtWidgets.QLabel): QtCore.Qt.SmoothTransformation ) ) + self._last_width = width + self._last_height = height def resizeEvent(self, event): self._set_resized_pix() From a724bd1c77ca9ded191967648e96c2adda8619ea Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 26 Apr 2023 03:25:35 +0000 Subject: [PATCH 138/139] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 02537af762..080fd6eece 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.5" +__version__ = "3.15.6-nightly.1" From 4107874eb999a6a5dfb7bf00c209b365b71bd796 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:41:00 +0200 Subject: [PATCH 139/139] Project packager: Backup and restore can store only database (#4879) * added helper functions to client mongo api * pack and unpack project functions can work without project files * added flag argument to pack project command to zip only project files * unpack project has also only project argument * Fix extractions --- openpype/cli.py | 15 +- openpype/client/mongo.py | 223 +++++++++++++++++++++++++- openpype/lib/project_backpack.py | 267 +++++++++++++++++++------------ openpype/pype_commands.py | 8 +- 4 files changed, 394 insertions(+), 119 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index a650a9fdcc..54af42920d 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -415,11 +415,12 @@ def repack_version(directory): @main.command() @click.option("--project", help="Project name") @click.option( - "--dirpath", help="Directory where package is stored", default=None -) -def pack_project(project, dirpath): + "--dirpath", help="Directory where package is stored", default=None) +@click.option( + "--dbonly", help="Store only Database data", default=False, is_flag=True) +def pack_project(project, dirpath, dbonly): """Create a package of project with all files and database dump.""" - PypeCommands().pack_project(project, dirpath) + PypeCommands().pack_project(project, dirpath, dbonly) @main.command() @@ -427,9 +428,11 @@ def pack_project(project, dirpath): @click.option( "--root", help="Replace root which was stored in project", default=None ) -def unpack_project(zipfile, root): +@click.option( + "--dbonly", help="Store only Database data", default=False, is_flag=True) +def unpack_project(zipfile, root, dbonly): """Create a package of project with all files and database dump.""" - PypeCommands().unpack_project(zipfile, root) + PypeCommands().unpack_project(zipfile, root, dbonly) @main.command() diff --git a/openpype/client/mongo.py b/openpype/client/mongo.py index 72acbc5476..251041c028 100644 --- a/openpype/client/mongo.py +++ b/openpype/client/mongo.py @@ -5,6 +5,12 @@ import logging import pymongo import certifi +from bson.json_util import ( + loads, + dumps, + CANONICAL_JSON_OPTIONS +) + if sys.version_info[0] == 2: from urlparse import urlparse, parse_qs else: @@ -15,6 +21,49 @@ class MongoEnvNotSet(Exception): pass +def documents_to_json(docs): + """Convert documents to json string. + + Args: + Union[list[dict[str, Any]], dict[str, Any]]: Document/s to convert to + json string. + + Returns: + str: Json string with mongo documents. + """ + + return dumps(docs, json_options=CANONICAL_JSON_OPTIONS) + + +def load_json_file(filepath): + """Load mongo documents from a json file. + + Args: + filepath (str): Path to a json file. + + Returns: + Union[dict[str, Any], list[dict[str, Any]]]: Loaded content from a + json file. + """ + + if not os.path.exists(filepath): + raise ValueError("Path {} was not found".format(filepath)) + + with open(filepath, "r") as stream: + content = stream.read() + return loads("".join(content)) + + +def get_project_database_name(): + """Name of database name where projects are available. + + Returns: + str: Name of database name where projects are. + """ + + return os.environ.get("AVALON_DB") or "avalon" + + def _decompose_url(url): """Decompose mongo url to basic components. @@ -210,12 +259,102 @@ class OpenPypeMongoConnection: return mongo_client -def get_project_database(): - db_name = os.environ.get("AVALON_DB") or "avalon" - return OpenPypeMongoConnection.get_mongo_client()[db_name] +# ------ Helper Mongo functions ------ +# Functions can be helpful with custom tools to backup/restore mongo state. +# Not meant as API functionality that should be used in production codebase! +def get_collection_documents(database_name, collection_name, as_json=False): + """Query all documents from a collection. + + Args: + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection where to look for collection. + as_json (Optional[bool]): Output should be a json string. + Default: 'False' + + Returns: + Union[list[dict[str, Any]], str]: Queried documents. + """ + + client = OpenPypeMongoConnection.get_mongo_client() + output = list(client[database_name][collection_name].find({})) + if as_json: + output = documents_to_json(output) + return output -def get_project_connection(project_name): +def store_collection(filepath, database_name, collection_name): + """Store collection documents to a json file. + + Args: + filepath (str): Path to a json file where documents will be stored. + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection to store. + """ + + # Make sure directory for output file exists + dirpath = os.path.dirname(filepath) + if not os.path.isdir(dirpath): + os.makedirs(dirpath) + + content = get_collection_documents(database_name, collection_name, True) + with open(filepath, "w") as stream: + stream.write(content) + + +def replace_collection_documents(docs, database_name, collection_name): + """Replace all documents in a collection with passed documents. + + Warnings: + All existing documents in collection will be removed if there are any. + + Args: + docs (list[dict[str, Any]]): New documents. + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection where new documents are + uploaded. + """ + + client = OpenPypeMongoConnection.get_mongo_client() + database = client[database_name] + if collection_name in database.list_collection_names(): + database.drop_collection(collection_name) + col = database[collection_name] + col.insert_many(docs) + + +def restore_collection(filepath, database_name, collection_name): + """Restore/replace collection from a json filepath. + + Warnings: + All existing documents in collection will be removed if there are any. + + Args: + filepath (str): Path to a json with documents. + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection where new documents are + uploaded. + """ + + docs = load_json_file(filepath) + replace_collection_documents(docs, database_name, collection_name) + + +def get_project_database(database_name=None): + """Database object where project collections are. + + Args: + database_name (Optional[str]): Custom name of database. + + Returns: + pymongo.database.Database: Collection related to passed project. + """ + + if not database_name: + database_name = get_project_database_name() + return OpenPypeMongoConnection.get_mongo_client()[database_name] + + +def get_project_connection(project_name, database_name=None): """Direct access to mongo collection. We're trying to avoid using direct access to mongo. This should be used @@ -223,13 +362,83 @@ def get_project_connection(project_name): api calls for that. Args: - project_name(str): Project name for which collection should be + project_name (str): Project name for which collection should be returned. + database_name (Optional[str]): Custom name of database. Returns: - pymongo.Collection: Collection realated to passed project. + pymongo.collection.Collection: Collection related to passed project. """ if not project_name: raise ValueError("Invalid project name {}".format(str(project_name))) - return get_project_database()[project_name] + return get_project_database(database_name)[project_name] + + +def get_project_documents(project_name, database_name=None): + """Query all documents from project collection. + + Args: + project_name (str): Name of project. + database_name (Optional[str]): Name of mongo database where to look for + project. + + Returns: + list[dict[str, Any]]: Documents in project collection. + """ + + if not database_name: + database_name = get_project_database_name() + return get_collection_documents(database_name, project_name) + + +def store_project_documents(project_name, filepath, database_name=None): + """Store project documents to a file as json string. + + Args: + project_name (str): Name of project to store. + filepath (str): Path to a json file where output will be stored. + database_name (Optional[str]): Name of mongo database where to look for + project. + """ + + if not database_name: + database_name = get_project_database_name() + + store_collection(filepath, database_name, project_name) + + +def replace_project_documents(project_name, docs, database_name=None): + """Replace documents in mongo with passed documents. + + Warnings: + Existing project collection is removed if exists in mongo. + + Args: + project_name (str): Name of project. + docs (list[dict[str, Any]]): Documents to restore. + database_name (Optional[str]): Name of mongo database where project + collection will be created. + """ + + if not database_name: + database_name = get_project_database_name() + replace_collection_documents(docs, database_name, project_name) + + +def restore_project_documents(project_name, filepath, database_name=None): + """Replace documents in mongo with passed documents. + + Warnings: + Existing project collection is removed if exists in mongo. + + Args: + project_name (str): Name of project. + filepath (str): File to json file with project documents. + database_name (Optional[str]): Name of mongo database where project + collection will be created. + """ + + if not database_name: + database_name = get_project_database_name() + restore_collection(filepath, database_name, project_name) diff --git a/openpype/lib/project_backpack.py b/openpype/lib/project_backpack.py index ff2f1d4b88..07107ec011 100644 --- a/openpype/lib/project_backpack.py +++ b/openpype/lib/project_backpack.py @@ -1,16 +1,19 @@ -"""These lib functions are primarily for development purposes. +"""These lib functions are for development purposes. -WARNING: This is not meant for production data. +WARNING: + This is not meant for production data. Please don't write code which is + dependent on functionality here. -Goal is to be able create package of current state of project with related -documents from mongo and files from disk to zip file and then be able recreate -the project based on the zip. +Goal is to be able to create package of current state of project with related +documents from mongo and files from disk to zip file and then be able +to recreate the project based on the zip. This gives ability to create project where a changes and tests can be done. -Keep in mind that to be able create a package of project has few requirements. -Possible requirement should be listed in 'pack_project' function. +Keep in mind that to be able to create a package of project has few +requirements. Possible requirement should be listed in 'pack_project' function. """ + import os import json import platform @@ -19,16 +22,12 @@ import shutil import datetime import zipfile -from bson.json_util import ( - loads, - dumps, - CANONICAL_JSON_OPTIONS +from openpype.client.mongo import ( + load_json_file, + get_project_connection, + replace_project_documents, + store_project_documents, ) -from openpype.client import ( - get_project, - get_whole_project, -) -from openpype.pipeline import AvalonMongoDB DOCUMENTS_FILE_NAME = "database" METADATA_FILE_NAME = "metadata" @@ -43,7 +42,52 @@ def add_timestamp(filepath): return new_base + ext -def pack_project(project_name, destination_dir=None): +def get_project_document(project_name, database_name=None): + """Query project document. + + Function 'get_project' from client api cannot be used as it does not allow + to change which 'database_name' is used. + + Args: + project_name (str): Name of project. + database_name (Optional[str]): Name of mongo database where to look for + project. + + Returns: + Union[dict[str, Any], None]: Project document or None. + """ + + col = get_project_connection(project_name, database_name) + return col.find_one({"type": "project"}) + + +def _pack_files_to_zip(zip_stream, source_path, root_path): + """Pack files to a zip stream. + + Args: + zip_stream (zipfile.ZipFile): Stream to a zipfile. + source_path (str): Path to a directory where files are. + root_path (str): Path to a directory which is used for calculation + of relative path. + """ + + for root, _, filenames in os.walk(source_path): + for filename in filenames: + filepath = os.path.join(root, filename) + # TODO add one more folder + archive_name = os.path.join( + PROJECT_FILES_DIR, + os.path.relpath(filepath, root_path) + ) + zip_stream.write(filepath, archive_name) + + +def pack_project( + project_name, + destination_dir=None, + only_documents=False, + database_name=None +): """Make a package of a project with mongo documents and files. This function has few restrictions: @@ -52,13 +96,18 @@ def pack_project(project_name, destination_dir=None): "{root[...]}/{project[name]}" Args: - project_name(str): Project that should be packaged. - destination_dir(str): Optional path where zip will be stored. Project's - root is used if not passed. + project_name (str): Project that should be packaged. + destination_dir (Optional[str]): Optional path where zip will be + stored. Project's root is used if not passed. + only_documents (Optional[bool]): Pack only Mongo documents and skip + files. + database_name (Optional[str]): Custom database name from which is + project queried. """ + print("Creating package of project \"{}\"".format(project_name)) # Validate existence of project - project_doc = get_project(project_name) + project_doc = get_project_document(project_name, database_name) if not project_doc: raise ValueError("Project \"{}\" was not found in database".format( project_name @@ -119,12 +168,7 @@ def pack_project(project_name, destination_dir=None): temp_docs_json = s.name # Query all project documents and store them to temp json - docs = list(get_whole_project(project_name)) - data = dumps( - docs, json_options=CANONICAL_JSON_OPTIONS - ) - with open(temp_docs_json, "w") as stream: - stream.write(data) + store_project_documents(project_name, temp_docs_json, database_name) print("Packing files into zip") # Write all to zip file @@ -133,16 +177,10 @@ def pack_project(project_name, destination_dir=None): zip_stream.write(temp_metadata_json, METADATA_FILE_NAME + ".json") # Add database documents zip_stream.write(temp_docs_json, DOCUMENTS_FILE_NAME + ".json") + # Add project files to zip - for root, _, filenames in os.walk(project_source_path): - for filename in filenames: - filepath = os.path.join(root, filename) - # TODO add one more folder - archive_name = os.path.join( - PROJECT_FILES_DIR, - os.path.relpath(filepath, root_path) - ) - zip_stream.write(filepath, archive_name) + if not only_documents: + _pack_files_to_zip(zip_stream, project_source_path, root_path) print("Cleaning up") # Cleanup @@ -152,80 +190,30 @@ def pack_project(project_name, destination_dir=None): print("*** Packing finished ***") -def unpack_project(path_to_zip, new_root=None): - """Unpack project zip file to recreate project. +def _unpack_project_files(unzip_dir, root_path, project_name): + """Move project files from unarchived temp folder to new root. + + Unpack is skipped if source files are not available in the zip. That can + happen if nothing was published yet or only documents were stored to + package. Args: - path_to_zip(str): Path to zip which was created using 'pack_project' - function. - new_root(str): Optional way how to set different root path for unpacked - project. + unzip_dir (str): Location where zip was unzipped. + root_path (str): Path to new root. + project_name (str): Name of project. """ - print("Unpacking project from zip {}".format(path_to_zip)) - if not os.path.exists(path_to_zip): - print("Zip file does not exists: {}".format(path_to_zip)) + + src_project_files_dir = os.path.join( + unzip_dir, PROJECT_FILES_DIR, project_name + ) + # Skip if files are not in the zip + if not os.path.exists(src_project_files_dir): return - tmp_dir = tempfile.mkdtemp(prefix="unpack_") - print("Zip is extracted to temp: {}".format(tmp_dir)) - with zipfile.ZipFile(path_to_zip, "r") as zip_stream: - zip_stream.extractall(tmp_dir) - - metadata_json_path = os.path.join(tmp_dir, METADATA_FILE_NAME + ".json") - with open(metadata_json_path, "r") as stream: - metadata = json.load(stream) - - docs_json_path = os.path.join(tmp_dir, DOCUMENTS_FILE_NAME + ".json") - with open(docs_json_path, "r") as stream: - content = stream.readlines() - docs = loads("".join(content)) - - low_platform = platform.system().lower() - project_name = metadata["project_name"] - source_root = metadata["root"] - root_path = source_root[low_platform] - - # Drop existing collection - dbcon = AvalonMongoDB() - database = dbcon.database - if project_name in database.list_collection_names(): - database.drop_collection(project_name) - print("Removed existing project collection") - - print("Creating project documents ({})".format(len(docs))) - # Create new collection with loaded docs - collection = database[project_name] - collection.insert_many(docs) - - # Skip change of root if is the same as the one stored in metadata - if ( - new_root - and (os.path.normpath(new_root) == os.path.normpath(root_path)) - ): - new_root = None - - if new_root: - print("Using different root path {}".format(new_root)) - root_path = new_root - - project_doc = get_project(project_name) - roots = project_doc["config"]["roots"] - key = tuple(roots.keys())[0] - update_key = "config.roots.{}.{}".format(key, low_platform) - collection.update_one( - {"_id": project_doc["_id"]}, - {"$set": { - update_key: new_root - }} - ) - # Make sure root path exists if not os.path.exists(root_path): os.makedirs(root_path) - src_project_files_dir = os.path.join( - tmp_dir, PROJECT_FILES_DIR, project_name - ) dst_project_files_dir = os.path.normpath( os.path.join(root_path, project_name) ) @@ -241,8 +229,83 @@ def unpack_project(path_to_zip, new_root=None): )) shutil.move(src_project_files_dir, dst_project_files_dir) + +def unpack_project( + path_to_zip, new_root=None, database_only=None, database_name=None +): + """Unpack project zip file to recreate project. + + Args: + path_to_zip (str): Path to zip which was created using 'pack_project' + function. + new_root (str): Optional way how to set different root path for + unpacked project. + database_only (Optional[bool]): Unpack only database from zip. + database_name (str): Name of database where project will be recreated. + """ + + if database_only is None: + database_only = False + + print("Unpacking project from zip {}".format(path_to_zip)) + if not os.path.exists(path_to_zip): + print("Zip file does not exists: {}".format(path_to_zip)) + return + + tmp_dir = tempfile.mkdtemp(prefix="unpack_") + print("Zip is extracted to temp: {}".format(tmp_dir)) + with zipfile.ZipFile(path_to_zip, "r") as zip_stream: + if database_only: + for filename in ( + "{}.json".format(METADATA_FILE_NAME), + "{}.json".format(DOCUMENTS_FILE_NAME), + ): + zip_stream.extract(filename, tmp_dir) + else: + zip_stream.extractall(tmp_dir) + + metadata_json_path = os.path.join(tmp_dir, METADATA_FILE_NAME + ".json") + with open(metadata_json_path, "r") as stream: + metadata = json.load(stream) + + docs_json_path = os.path.join(tmp_dir, DOCUMENTS_FILE_NAME + ".json") + docs = load_json_file(docs_json_path) + + low_platform = platform.system().lower() + project_name = metadata["project_name"] + source_root = metadata["root"] + root_path = source_root[low_platform] + + # Drop existing collection + replace_project_documents(project_name, docs, database_name) + print("Creating project documents ({})".format(len(docs))) + + # Skip change of root if is the same as the one stored in metadata + if ( + new_root + and (os.path.normpath(new_root) == os.path.normpath(root_path)) + ): + new_root = None + + if new_root: + print("Using different root path {}".format(new_root)) + root_path = new_root + + project_doc = get_project_document(project_name) + roots = project_doc["config"]["roots"] + key = tuple(roots.keys())[0] + update_key = "config.roots.{}.{}".format(key, low_platform) + collection = get_project_connection(project_name, database_name) + collection.update_one( + {"_id": project_doc["_id"]}, + {"$set": { + update_key: new_root + }} + ) + + _unpack_project_files(tmp_dir, root_path, project_name) + # CLeanup print("Cleaning up") shutil.rmtree(tmp_dir) - dbcon.uninstall() print("*** Unpack finished ***") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index dc5b3d63c3..6a24cb0ebc 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -353,12 +353,12 @@ class PypeCommands: version_packer = VersionRepacker(directory) version_packer.process() - def pack_project(self, project_name, dirpath): + def pack_project(self, project_name, dirpath, database_only): from openpype.lib.project_backpack import pack_project - pack_project(project_name, dirpath) + pack_project(project_name, dirpath, database_only) - def unpack_project(self, zip_filepath, new_root): + def unpack_project(self, zip_filepath, new_root, database_only): from openpype.lib.project_backpack import unpack_project - unpack_project(zip_filepath, new_root) + unpack_project(zip_filepath, new_root, database_only)