From f9f95b84e68da86ce53f9881ee59b98acb6d9aef Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 25 Jan 2023 11:09:19 +0000 Subject: [PATCH 01/30] 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 02/30] 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 03/30] 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 04/30] 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 05/30] 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 06/30] 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 07/30] 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 08/30] 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 09/30] 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 10/30] 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 11/30] 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 12/30] 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 13/30] 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 14/30] 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 15/30] 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 16/30] 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 17/30] 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 18/30] 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 19/30] 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 20/30] 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 21/30] 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 22/30] 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 4dd58e15d89383a870890562e9f084ee3fb189bf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 11:13:04 +0100 Subject: [PATCH 23/30] 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 24/30] 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 25/30] 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 f05f7510b4256964de741b6fd982327da9e4e1aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 21:39:49 +0200 Subject: [PATCH 26/30] 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 27/30] 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 60d386b127badba113199c94111bd76de1dee041 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:53:17 +0200 Subject: [PATCH 28/30] :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 29/30] 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 30/30] 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):