From 72c2f1e24920a1f300fa8905f894d9d43452fafa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Oct 2024 23:35:52 +0800 Subject: [PATCH 01/36] wip for creator, collector and extractor for editorial product type --- .../plugins/create/create_editorial.py | 63 +++++++++++++++++++ .../plugins/publish/collect_editorial.py | 42 +++++++++++++ .../plugins/publish/extract_editorial.py | 33 ++++++++++ 3 files changed, 138 insertions(+) create mode 100644 client/ayon_unreal/plugins/create/create_editorial.py create mode 100644 client/ayon_unreal/plugins/publish/collect_editorial.py create mode 100644 client/ayon_unreal/plugins/publish/extract_editorial.py diff --git a/client/ayon_unreal/plugins/create/create_editorial.py b/client/ayon_unreal/plugins/create/create_editorial.py new file mode 100644 index 00000000..8d3da727 --- /dev/null +++ b/client/ayon_unreal/plugins/create/create_editorial.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import unreal + +from ayon_core.pipeline import CreatorError +from ayon_unreal.api.plugin import ( + UnrealAssetCreator +) + + +class CreateEditorial(UnrealAssetCreator): + """Create Editorial + Process publishes the selected level sequences with metadata info + """ + + identifier = "io.ayon.creators.unreal.editorial" + label = "Editorial" + product_type = "editorial" + icon = "camera" + + def create_instance( + self, instance_data, product_name, pre_create_data, + selection, level + ): + instance_data["members"] = selection + instance_data["sequence"] = selection[0] + instance_data["level"] = level + + super(CreateEditorial, self).create( + product_name, + instance_data, + pre_create_data) + + def create(self, product_name, instance_data, pre_create_data): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + 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 CreatorError("Please select at least one Level Sequence.") + + master_lvl = None + + for sel in selection: + search_path = Path(sel).parent.as_posix() + # Get the master level. + try: + 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 CreatorError("Could not find any map for the selected sequence.") + + self.create_instance( + instance_data, product_name, pre_create_data, + selection, master_lvl) diff --git a/client/ayon_unreal/plugins/publish/collect_editorial.py b/client/ayon_unreal/plugins/publish/collect_editorial.py new file mode 100644 index 00000000..0207f231 --- /dev/null +++ b/client/ayon_unreal/plugins/publish/collect_editorial.py @@ -0,0 +1,42 @@ +from pathlib import Path + +import unreal + +from ayon_core.pipeline import get_current_project_name +from ayon_core.pipeline import Anatomy +from ayon_unreal.api import pipeline +import pyblish.api + + +class CollectEditorial(pyblish.api.InstancePlugin): + """ This collector will collect all the editorial info + """ + order = pyblish.api.CollectorOrder + hosts = ["unreal"] + families = ["editorial"] + label = "Collect Editorial" + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path( + instance.data.get('sequence')).get_asset() + + subscenes = pipeline.get_subsequences(sequence) + sub_seq_obj_list = [] + if subscenes: + for sub_seq in subscenes: + sub_seq_obj = sub_seq.get_sequence() + if sub_seq_obj is None: + continue + curr_editorial_data = { + "shot_name": sub_seq_obj.get_name(), + "sequence": sub_seq_obj, + "output": (f"{sequence.get_name()}/" + f"{sub_seq_obj.get_name()}"), + "frame_range": ( + sub_seq.get_start_frame(), + sub_seq.get_end_frame() + ) + } + sub_seq_obj_list.append(curr_editorial_data) + instance.data.update({"sequence_data": sub_seq_obj_list}) diff --git a/client/ayon_unreal/plugins/publish/extract_editorial.py b/client/ayon_unreal/plugins/publish/extract_editorial.py new file mode 100644 index 00000000..91337108 --- /dev/null +++ b/client/ayon_unreal/plugins/publish/extract_editorial.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Extract Editorial from Unreal.""" +import os + +import unreal + +from ayon_core.pipeline import publish + + + +class ExtractEditorial(publish.Extractor): + """Extract Editorial""" + + label = "Extract Editorial" + hosts = ["unreal"] + families = ["editorial"] + + def process(self, instance): + pass + staging_dir = self.staging_dir(instance) + sequence_data = instance.data.get("sequence_data") + filename = "{}.edl".format(instance.name) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'edl', + 'ext': 'edl', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) From ef4e87d5d53ea3e98083ff520b79fb3d95dfa2fa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Oct 2024 23:47:29 +0800 Subject: [PATCH 02/36] remove collect editorial as it is not gonna use for this workflow --- client/ayon_unreal/api/otio.py | 0 client/ayon_unreal/api/rendering.py | 40 +++++++++++++++++++ .../plugins/publish/extract_editorial.py | 14 +++---- 3 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 client/ayon_unreal/api/otio.py diff --git a/client/ayon_unreal/api/otio.py b/client/ayon_unreal/api/otio.py new file mode 100644 index 00000000..e69de29b diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 2558824c..415a2258 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -252,3 +252,43 @@ def start_rendering(): executor.on_individual_job_finished_delegate.add_callable_unique( _job_finish_callback) # Only available on PIE Executor executor.execute(queue) + + +def clear_render_queue(): + # Movie render queue + subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) + queue = subsystem.get_queue() + if not queue.get_jobs(): + return + for job in queue.get_jobs(): + queue.delete_job(job) + + +def editorial_rendering(shot_name, sequence_path, master_level): + subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) + job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) + job.set_editor_property("job_name", f"{shot_name}") + + job.sequence.assign(unreal.SoftObjectPath(sequence_path)) + job.map.assign(unreal.SoftObjectPath(master_level)) + + config = job.get_configuration() + output_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting) + # output_setting.set_editor_property( + # "output_directory", + # unreal.DirectoryPath(os.path.dirname(shot_render_path)) + # ) + # output_setting.set_editor_property( + # "file_name_format", + # os.path.basename(shot_render_path).rsplit(".mov", 1)[0] + # ) + + output_setting.set_editor_property("output_resolution", unreal.IntPoint(1920 / 2, 1080 / 2)) + output_setting.set_editor_property("override_existing_output", True) # Overwrite existing files + + pro_res_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineAppleProResOutput) + pro_res_setting.set_editor_property("codec", unreal.AppleProResEncoderCodec.PRO_RES_422_PROXY) + + # Render itself + executor = unreal.MoviePipelinePIEExecutor() + subsystem.render_queue_with_executor_instance(executor) diff --git a/client/ayon_unreal/plugins/publish/extract_editorial.py b/client/ayon_unreal/plugins/publish/extract_editorial.py index 91337108..443f3d0d 100644 --- a/client/ayon_unreal/plugins/publish/extract_editorial.py +++ b/client/ayon_unreal/plugins/publish/extract_editorial.py @@ -16,18 +16,16 @@ class ExtractEditorial(publish.Extractor): families = ["editorial"] def process(self, instance): - pass staging_dir = self.staging_dir(instance) - sequence_data = instance.data.get("sequence_data") filename = "{}.edl".format(instance.name) if "representations" not in instance.data: instance.data["representations"] = [] - representation = { - 'name': 'edl', - 'ext': 'edl', - 'files': filename, - "stagingDir": staging_dir, - } + representation = { + 'name': 'edl', + 'ext': 'edl', + 'files': filename, + "stagingDir": staging_dir, + } instance.data["representations"].append(representation) From 9f24405a60d41e4adec48c7c86da8e91baedc5fc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Oct 2024 23:49:49 +0800 Subject: [PATCH 03/36] remove otio-related workflow as it is not related to this PR --- client/ayon_unreal/api/otio.py | 0 client/ayon_unreal/api/rendering.py | 30 ------------- .../plugins/publish/collect_editorial.py | 42 ------------------- 3 files changed, 72 deletions(-) delete mode 100644 client/ayon_unreal/api/otio.py delete mode 100644 client/ayon_unreal/plugins/publish/collect_editorial.py diff --git a/client/ayon_unreal/api/otio.py b/client/ayon_unreal/api/otio.py deleted file mode 100644 index e69de29b..00000000 diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 415a2258..a889fdaa 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -262,33 +262,3 @@ def clear_render_queue(): return for job in queue.get_jobs(): queue.delete_job(job) - - -def editorial_rendering(shot_name, sequence_path, master_level): - subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) - job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) - job.set_editor_property("job_name", f"{shot_name}") - - job.sequence.assign(unreal.SoftObjectPath(sequence_path)) - job.map.assign(unreal.SoftObjectPath(master_level)) - - config = job.get_configuration() - output_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting) - # output_setting.set_editor_property( - # "output_directory", - # unreal.DirectoryPath(os.path.dirname(shot_render_path)) - # ) - # output_setting.set_editor_property( - # "file_name_format", - # os.path.basename(shot_render_path).rsplit(".mov", 1)[0] - # ) - - output_setting.set_editor_property("output_resolution", unreal.IntPoint(1920 / 2, 1080 / 2)) - output_setting.set_editor_property("override_existing_output", True) # Overwrite existing files - - pro_res_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineAppleProResOutput) - pro_res_setting.set_editor_property("codec", unreal.AppleProResEncoderCodec.PRO_RES_422_PROXY) - - # Render itself - executor = unreal.MoviePipelinePIEExecutor() - subsystem.render_queue_with_executor_instance(executor) diff --git a/client/ayon_unreal/plugins/publish/collect_editorial.py b/client/ayon_unreal/plugins/publish/collect_editorial.py deleted file mode 100644 index 0207f231..00000000 --- a/client/ayon_unreal/plugins/publish/collect_editorial.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -import unreal - -from ayon_core.pipeline import get_current_project_name -from ayon_core.pipeline import Anatomy -from ayon_unreal.api import pipeline -import pyblish.api - - -class CollectEditorial(pyblish.api.InstancePlugin): - """ This collector will collect all the editorial info - """ - order = pyblish.api.CollectorOrder - hosts = ["unreal"] - families = ["editorial"] - label = "Collect Editorial" - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - sequence = ar.get_asset_by_object_path( - instance.data.get('sequence')).get_asset() - - subscenes = pipeline.get_subsequences(sequence) - sub_seq_obj_list = [] - if subscenes: - for sub_seq in subscenes: - sub_seq_obj = sub_seq.get_sequence() - if sub_seq_obj is None: - continue - curr_editorial_data = { - "shot_name": sub_seq_obj.get_name(), - "sequence": sub_seq_obj, - "output": (f"{sequence.get_name()}/" - f"{sub_seq_obj.get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), - sub_seq.get_end_frame() - ) - } - sub_seq_obj_list.append(curr_editorial_data) - instance.data.update({"sequence_data": sub_seq_obj_list}) From f96b691089841b942dde48305588de497c8701dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Oct 2024 17:58:04 +0800 Subject: [PATCH 04/36] wip the creator and render for clip function --- client/ayon_unreal/api/lib.py | 14 ++ client/ayon_unreal/api/rendering.py | 30 +++ .../plugins/create/create_editorial.py | 63 ----- .../plugins/create/create_editorial_clip.py | 220 ++++++++++++++++++ ...ct_editorial.py => extract_shot_render.py} | 10 +- 5 files changed, 269 insertions(+), 68 deletions(-) delete mode 100644 client/ayon_unreal/plugins/create/create_editorial.py create mode 100644 client/ayon_unreal/plugins/create/create_editorial_clip.py rename client/ayon_unreal/plugins/publish/{extract_editorial.py => extract_shot_render.py} (77%) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index f3f14425..fecbe085 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -132,3 +132,17 @@ def import_camera_to_level_sequence(sequence, parent_id, version_id, namespace, unreal.log(f"Spawning camera: {camera_actor_name}") for actor in camera_actors: actor.set_actor_label(camera_actor_name) + + +def get_shot_tracks(sel_objects): + selection = [ + a for a in sel_objects + if a.get_class().get_name() == "LevelSequence" + ] + + sub_sequence_tracks = [ + sel.find_master_tracks_by_type( + unreal.MovieSceneSubTrack) for sel in selection + ] + return [track.get_name() for track in sub_sequence_tracks + if isinstance(track, unreal.MovieSceneCinematicShotTrack)] diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index a889fdaa..712ce690 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -262,3 +262,33 @@ def clear_render_queue(): return for job in queue.get_jobs(): queue.delete_job(job) + + +def editorial_rendering(shot_name, sequence_path, master_level, shot_render_path): + subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) + job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) + job.set_editor_property("job_name", f"{shot_name}") + + job.sequence.assign(unreal.SoftObjectPath(sequence_path)) + job.map.assign(unreal.SoftObjectPath(master_level)) + + config = job.get_configuration() + output_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting) + output_setting.set_editor_property( + "output_directory", + unreal.DirectoryPath(os.path.dirname(shot_render_path)) + ) + output_setting.set_editor_property( + "file_name_format", + os.path.basename(shot_render_path).rsplit(".mov", 1)[0] + ) + + output_setting.set_editor_property("output_resolution", unreal.IntPoint(1920 / 2, 1080 / 2)) + output_setting.set_editor_property("override_existing_output", True) # Overwrite existing files + + pro_res_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineAppleProResOutput) + pro_res_setting.set_editor_property("codec", unreal.AppleProResEncoderCodec.PRO_RES_422_PROXY) + + # Render itself + executor = unreal.MoviePipelinePIEExecutor() + subsystem.render_queue_with_executor_instance(executor) diff --git a/client/ayon_unreal/plugins/create/create_editorial.py b/client/ayon_unreal/plugins/create/create_editorial.py deleted file mode 100644 index 8d3da727..00000000 --- a/client/ayon_unreal/plugins/create/create_editorial.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_unreal.api.plugin import ( - UnrealAssetCreator -) - - -class CreateEditorial(UnrealAssetCreator): - """Create Editorial - Process publishes the selected level sequences with metadata info - """ - - identifier = "io.ayon.creators.unreal.editorial" - label = "Editorial" - product_type = "editorial" - icon = "camera" - - def create_instance( - self, instance_data, product_name, pre_create_data, - selection, level - ): - instance_data["members"] = selection - instance_data["sequence"] = selection[0] - instance_data["level"] = level - - super(CreateEditorial, self).create( - product_name, - instance_data, - pre_create_data) - - def create(self, product_name, instance_data, pre_create_data): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - 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 CreatorError("Please select at least one Level Sequence.") - - master_lvl = None - - for sel in selection: - search_path = Path(sel).parent.as_posix() - # Get the master level. - try: - 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 CreatorError("Could not find any map for the selected sequence.") - - self.create_instance( - instance_data, product_name, pre_create_data, - selection, master_lvl) diff --git a/client/ayon_unreal/plugins/create/create_editorial_clip.py b/client/ayon_unreal/plugins/create/create_editorial_clip.py new file mode 100644 index 00000000..647ce261 --- /dev/null +++ b/client/ayon_unreal/plugins/create/create_editorial_clip.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import unreal + +from ayon_core.pipeline import CreatorError +from ayon_unreal.api.lib import get_shot_tracks +from ayon_unreal.api.plugin import ( + UnrealAssetCreator +) +from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef + + +class CreateShotClip(UnrealAssetCreator): + """Create Clip + Process publishes shot from the selected level sequences + """ + + identifier = "io.ayon.creators.unreal.clip" + label = "Editorial Clip" + product_type = "clip" + icon = "film" + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + + def create(self, product_name, instance_data, pre_create_data): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + 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 CreatorError("Please select at least one Level Sequence.") + + master_lvl = None + + for sel in selection: + search_path = Path(sel).parent.as_posix() + # Get the master level. + try: + 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 CreatorError("Could not find any map for the selected sequence.") + + if not get_shot_tracks(sel_objects): + raise CreatorError("No movie shot tracks found in the selected level sequence") + + instance_data["members"] = selection + instance_data["level"] = master_lvl + + super(CreateShotClip, self).create( + product_name, + instance_data, + pre_create_data) + # TODO: create sub-instances for publishing + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + def header_label(text): + return f"
{text}" + gui_tracks = get_shot_tracks(self.sel_objects) + + return attrs + [ + # hierarchyData + UILabelDef( + label=header_label("Shot Template Keywords") + ), + TextDef( + "folder", + label="{folder}", + tooltip="Name of folder used for root of generated shots.\n", + default="shot", + ), + TextDef( + "episode", + label="{episode}", + tooltip=f"Name of episode.\n", + default="ep01", + ), + TextDef( + "sequence", + label="{sequence}", + tooltip=f"Name of sequence of shots.\n", + default="sq01", + ), + TextDef( + "track", + label="{track}", + tooltip=f"Name of timeline track.\n", + default="{_track_}", + ), + TextDef( + "shot", + label="{shot}", + tooltip="Name of shot. '#' is converted to padded number.", + default="sh###", + ), + + # renameHierarchy + UILabelDef( + label=header_label("Shot Hierarchy and Rename Settings") + ), + TextDef( + "hierarchy", + label="Shot Parent Hierarchy", + tooltip="Parents folder for shot root folder, " + "Template filled with *Hierarchy Data* section", + default="{folder}/{sequence}", + ), + BoolDef( + "clipRename", + label="Rename Shots/Clips", + tooltip="Renaming selected clips on fly", + default=False, + ), + TextDef( + "clipName", + label="Rename Template", + tooltip="template for creating shot names, used for " + "renaming (use rename: on)", + default="{sequence}{shot}", + ), + NumberDef( + "countFrom", + label="Count Sequence from", + tooltip="Set where the sequence number starts from", + default=10, + ), + NumberDef( + "countSteps", + label="Stepping Number", + tooltip="What number is adding every new step", + default=10, + ), + + # verticalSync + UILabelDef( + label="Vertical Synchronization of Attributes" + ), + BoolDef( + "vSyncOn", + label="Enable Vertical Sync", + tooltip="Switch on if you want clips above " + "each other to share its attributes", + default=True, + ), + EnumDef( + "vSyncTrack", + label="Hero Track", + tooltip="Select driving track name which should " + "be mastering all others", + items=gui_tracks or [""], + ), + + # publishSettings + UILabelDef( + label=header_label("Clip Publish Settings") + ), + EnumDef( + "clip_variant", + label="Product Variant", + tooltip="Chosen variant which will be then used for " + "product name, if " + "is selected, name of track layer will be used", + items=['', 'main', 'bg', 'fg', 'bg', 'animatic'], + ), + EnumDef( + "productType", + label="Product Type", + tooltip="How the product will be used", + items=['plate'], # it is prepared for more types + ), + EnumDef( + "reviewTrack", + label="Use Review Track", + tooltip="Generate preview videos on fly, if " + "'< none >' is defined nothing will be generated.", + items=['< none >'] + gui_tracks, + ), + BoolDef( + "export_audio", + label="Include audio", + tooltip="Process subsets with corresponding audio", + default=False, + ), + BoolDef( + "sourceResolution", + label="Source resolution", + tooltip="Is resolution taken from timeline or source?", + default=False, + ), + + # shotAttr + UILabelDef( + label=header_label("Shot Attributes"), + ), + NumberDef( + "workfileFrameStart", + label="Workfiles Start Frame", + tooltip="Set workfile starting frame number", + default=1001, + ), + NumberDef( + "handleStart", + label="Handle Start (head)", + tooltip="Handle at start of clip", + default=0, + ), + NumberDef( + "handleEnd", + label="Handle End (tail)", + tooltip="Handle at end of clip", + default=0, + ), + ] diff --git a/client/ayon_unreal/plugins/publish/extract_editorial.py b/client/ayon_unreal/plugins/publish/extract_shot_render.py similarity index 77% rename from client/ayon_unreal/plugins/publish/extract_editorial.py rename to client/ayon_unreal/plugins/publish/extract_shot_render.py index 443f3d0d..d7dc1655 100644 --- a/client/ayon_unreal/plugins/publish/extract_editorial.py +++ b/client/ayon_unreal/plugins/publish/extract_shot_render.py @@ -11,20 +11,20 @@ class ExtractEditorial(publish.Extractor): """Extract Editorial""" - label = "Extract Editorial" + label = "Extract Shot Render(Clip)" hosts = ["unreal"] - families = ["editorial"] + families = ["clip"] def process(self, instance): staging_dir = self.staging_dir(instance) - filename = "{}.edl".format(instance.name) + filename = "{}.mov".format(instance.name) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'edl', - 'ext': 'edl', + 'name': 'mov', + 'ext': 'mov', 'files': filename, "stagingDir": staging_dir, } From bbfafef035e3a3df53afe6f50994f16c574208e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Oct 2024 18:17:18 +0800 Subject: [PATCH 05/36] wip the creator --- .../plugins/create/create_editorial_clip.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/client/ayon_unreal/plugins/create/create_editorial_clip.py b/client/ayon_unreal/plugins/create/create_editorial_clip.py index 647ce261..e1156e1a 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_clip.py +++ b/client/ayon_unreal/plugins/create/create_editorial_clip.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from pathlib import Path - +import ast import unreal -from ayon_core.pipeline import CreatorError +from ayon_core.pipeline import CreatorError, CreatedInstance from ayon_unreal.api.lib import get_shot_tracks from ayon_unreal.api.plugin import ( UnrealAssetCreator @@ -22,6 +22,25 @@ class CreateShotClip(UnrealAssetCreator): icon = "film" sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + def _default_collect_instances(self): + # cache instances if missing + self.get_cached_instances(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', '{}')) + instance['members'] = ast.literal_eval( + instance.get('members', '[]')) + instance['families'] = ast.literal_eval( + instance.get('families', '[]')) + instance['active'] = ast.literal_eval( + instance.get('active', '')) + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + def create(self, product_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() From 58e46bfcfdcabeed6d2249faf92ad688a9ec7e43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Oct 2024 13:26:03 +0800 Subject: [PATCH 06/36] wip on the creator and collector --- client/ayon_unreal/api/lib.py | 5 +++-- ...eate_editorial_clip.py => create_editorial.py} | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) rename client/ayon_unreal/plugins/create/{create_editorial_clip.py => create_editorial.py} (96%) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index fecbe085..c9079e0c 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -141,8 +141,9 @@ def get_shot_tracks(sel_objects): ] sub_sequence_tracks = [ - sel.find_master_tracks_by_type( - unreal.MovieSceneSubTrack) for sel in selection + track for sel in selection for track in + sel.find_master_tracks_by_type(unreal.MovieSceneSubTrack) ] + return [track.get_name() for track in sub_sequence_tracks if isinstance(track, unreal.MovieSceneCinematicShotTrack)] diff --git a/client/ayon_unreal/plugins/create/create_editorial_clip.py b/client/ayon_unreal/plugins/create/create_editorial.py similarity index 96% rename from client/ayon_unreal/plugins/create/create_editorial_clip.py rename to client/ayon_unreal/plugins/create/create_editorial.py index e1156e1a..f2cdd3a8 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_clip.py +++ b/client/ayon_unreal/plugins/create/create_editorial.py @@ -11,14 +11,14 @@ from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef -class CreateShotClip(UnrealAssetCreator): +class CreateEditorial(UnrealAssetCreator): """Create Clip Process publishes shot from the selected level sequences """ - identifier = "io.ayon.creators.unreal.clip" - label = "Editorial Clip" - product_type = "clip" + identifier = "io.ayon.creators.unreal.editorial" + label = "Editorial" + product_type = "Editorial" icon = "film" sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() @@ -72,19 +72,18 @@ def create(self, product_name, instance_data, pre_create_data): instance_data["members"] = selection instance_data["level"] = master_lvl - super(CreateShotClip, self).create( + super(CreateEditorial, self).create( product_name, instance_data, pre_create_data) # TODO: create sub-instances for publishing - def get_pre_create_attr_defs(self): - attrs = super().get_pre_create_attr_defs() + def get_instance_attr_defs(self): def header_label(text): return f"
{text}" gui_tracks = get_shot_tracks(self.sel_objects) - return attrs + [ + return [ # hierarchyData UILabelDef( label=header_label("Shot Template Keywords") From b6082c4f0cde0e7886dfb3bb50f554b2a7f5140f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Oct 2024 14:33:13 +0800 Subject: [PATCH 07/36] wip on the creator --- client/ayon_unreal/api/lib.py | 16 +++++++++++++--- .../plugins/create/create_editorial.py | 10 ++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index c9079e0c..4d86e5f1 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -134,7 +134,12 @@ def import_camera_to_level_sequence(sequence, parent_id, version_id, namespace, actor.set_actor_label(camera_actor_name) -def get_shot_tracks(sel_objects): +def get_shot_tracks(sel_objects=None): + if sel_objects is None: + asset_registry = unreal.AssetRegistryHelpers.get_asset_registry() + all_assets = asset_registry.get_all_assets() + sel_objects = [obj.get_asset() for obj in all_assets] + selection = [ a for a in sel_objects if a.get_class().get_name() == "LevelSequence" @@ -145,5 +150,10 @@ def get_shot_tracks(sel_objects): sel.find_master_tracks_by_type(unreal.MovieSceneSubTrack) ] - return [track.get_name() for track in sub_sequence_tracks - if isinstance(track, unreal.MovieSceneCinematicShotTrack)] + movie_shot_tracks = [track for track in sub_sequence_tracks + if isinstance(track, unreal.MovieSceneCinematicShotTrack)] + shot_display_names = [section.get_shot_display_name() for shot_tracks in + movie_shot_tracks for section in shot_tracks.get_sections()] + shot_sections_tracks = [section.get_shot_display_name() for shot_tracks in + movie_shot_tracks for section in shot_tracks.get_sections()] + return shot_display_names, shot_sections_tracks diff --git a/client/ayon_unreal/plugins/create/create_editorial.py b/client/ayon_unreal/plugins/create/create_editorial.py index f2cdd3a8..8d6e9764 100644 --- a/client/ayon_unreal/plugins/create/create_editorial.py +++ b/client/ayon_unreal/plugins/create/create_editorial.py @@ -20,7 +20,6 @@ class CreateEditorial(UnrealAssetCreator): label = "Editorial" product_type = "Editorial" icon = "film" - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() def _default_collect_instances(self): # cache instances if missing @@ -38,6 +37,8 @@ def _default_collect_instances(self): instance.get('families', '[]')) instance['active'] = ast.literal_eval( instance.get('active', '')) + instance['shot_tracks'] = ast.literal_eval( + instance.get('shot_tracks', '[]')) created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) @@ -66,22 +67,23 @@ def create(self, product_name, instance_data, pre_create_data): except IndexError: raise CreatorError("Could not find any map for the selected sequence.") - if not get_shot_tracks(sel_objects): + _, shot_sections = get_shot_tracks(sel_objects) + if not shot_sections: raise CreatorError("No movie shot tracks found in the selected level sequence") instance_data["members"] = selection instance_data["level"] = master_lvl + instance_data["shot_tracks"] = shot_sections super(CreateEditorial, self).create( product_name, instance_data, pre_create_data) - # TODO: create sub-instances for publishing def get_instance_attr_defs(self): def header_label(text): return f"
{text}" - gui_tracks = get_shot_tracks(self.sel_objects) + gui_tracks, _ = get_shot_tracks() return [ # hierarchyData From 427828e831d51c122c5872a3c32145d34fcd4950 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 15 Oct 2024 14:41:52 +0800 Subject: [PATCH 08/36] use editorial pkg product type --- .../plugins/create/create_editorial.py | 240 ------------------ .../create/create_editorial_package.py | 78 ++++++ ...=> extract_intermediate_representation.py} | 15 +- 3 files changed, 86 insertions(+), 247 deletions(-) delete mode 100644 client/ayon_unreal/plugins/create/create_editorial.py create mode 100644 client/ayon_unreal/plugins/create/create_editorial_package.py rename client/ayon_unreal/plugins/publish/{extract_shot_render.py => extract_intermediate_representation.py} (62%) diff --git a/client/ayon_unreal/plugins/create/create_editorial.py b/client/ayon_unreal/plugins/create/create_editorial.py deleted file mode 100644 index 8d6e9764..00000000 --- a/client/ayon_unreal/plugins/create/create_editorial.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path -import ast -import unreal - -from ayon_core.pipeline import CreatorError, CreatedInstance -from ayon_unreal.api.lib import get_shot_tracks -from ayon_unreal.api.plugin import ( - UnrealAssetCreator -) -from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef - - -class CreateEditorial(UnrealAssetCreator): - """Create Clip - Process publishes shot from the selected level sequences - """ - - identifier = "io.ayon.creators.unreal.editorial" - label = "Editorial" - product_type = "Editorial" - icon = "film" - - def _default_collect_instances(self): - # cache instances if missing - self.get_cached_instances(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', '{}')) - instance['members'] = ast.literal_eval( - instance.get('members', '[]')) - instance['families'] = ast.literal_eval( - instance.get('families', '[]')) - instance['active'] = ast.literal_eval( - instance.get('active', '')) - instance['shot_tracks'] = ast.literal_eval( - instance.get('shot_tracks', '[]')) - created_instance = CreatedInstance.from_existing(instance, self) - self._add_instance_to_context(created_instance) - - def create(self, product_name, instance_data, pre_create_data): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - 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 CreatorError("Please select at least one Level Sequence.") - - master_lvl = None - - for sel in selection: - search_path = Path(sel).parent.as_posix() - # Get the master level. - try: - 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 CreatorError("Could not find any map for the selected sequence.") - - _, shot_sections = get_shot_tracks(sel_objects) - if not shot_sections: - raise CreatorError("No movie shot tracks found in the selected level sequence") - - instance_data["members"] = selection - instance_data["level"] = master_lvl - instance_data["shot_tracks"] = shot_sections - - super(CreateEditorial, self).create( - product_name, - instance_data, - pre_create_data) - - def get_instance_attr_defs(self): - def header_label(text): - return f"
{text}" - gui_tracks, _ = get_shot_tracks() - - return [ - # hierarchyData - UILabelDef( - label=header_label("Shot Template Keywords") - ), - TextDef( - "folder", - label="{folder}", - tooltip="Name of folder used for root of generated shots.\n", - default="shot", - ), - TextDef( - "episode", - label="{episode}", - tooltip=f"Name of episode.\n", - default="ep01", - ), - TextDef( - "sequence", - label="{sequence}", - tooltip=f"Name of sequence of shots.\n", - default="sq01", - ), - TextDef( - "track", - label="{track}", - tooltip=f"Name of timeline track.\n", - default="{_track_}", - ), - TextDef( - "shot", - label="{shot}", - tooltip="Name of shot. '#' is converted to padded number.", - default="sh###", - ), - - # renameHierarchy - UILabelDef( - label=header_label("Shot Hierarchy and Rename Settings") - ), - TextDef( - "hierarchy", - label="Shot Parent Hierarchy", - tooltip="Parents folder for shot root folder, " - "Template filled with *Hierarchy Data* section", - default="{folder}/{sequence}", - ), - BoolDef( - "clipRename", - label="Rename Shots/Clips", - tooltip="Renaming selected clips on fly", - default=False, - ), - TextDef( - "clipName", - label="Rename Template", - tooltip="template for creating shot names, used for " - "renaming (use rename: on)", - default="{sequence}{shot}", - ), - NumberDef( - "countFrom", - label="Count Sequence from", - tooltip="Set where the sequence number starts from", - default=10, - ), - NumberDef( - "countSteps", - label="Stepping Number", - tooltip="What number is adding every new step", - default=10, - ), - - # verticalSync - UILabelDef( - label="Vertical Synchronization of Attributes" - ), - BoolDef( - "vSyncOn", - label="Enable Vertical Sync", - tooltip="Switch on if you want clips above " - "each other to share its attributes", - default=True, - ), - EnumDef( - "vSyncTrack", - label="Hero Track", - tooltip="Select driving track name which should " - "be mastering all others", - items=gui_tracks or [""], - ), - - # publishSettings - UILabelDef( - label=header_label("Clip Publish Settings") - ), - EnumDef( - "clip_variant", - label="Product Variant", - tooltip="Chosen variant which will be then used for " - "product name, if " - "is selected, name of track layer will be used", - items=['', 'main', 'bg', 'fg', 'bg', 'animatic'], - ), - EnumDef( - "productType", - label="Product Type", - tooltip="How the product will be used", - items=['plate'], # it is prepared for more types - ), - EnumDef( - "reviewTrack", - label="Use Review Track", - tooltip="Generate preview videos on fly, if " - "'< none >' is defined nothing will be generated.", - items=['< none >'] + gui_tracks, - ), - BoolDef( - "export_audio", - label="Include audio", - tooltip="Process subsets with corresponding audio", - default=False, - ), - BoolDef( - "sourceResolution", - label="Source resolution", - tooltip="Is resolution taken from timeline or source?", - default=False, - ), - - # shotAttr - UILabelDef( - label=header_label("Shot Attributes"), - ), - NumberDef( - "workfileFrameStart", - label="Workfiles Start Frame", - tooltip="Set workfile starting frame number", - default=1001, - ), - NumberDef( - "handleStart", - label="Handle Start (head)", - tooltip="Handle at start of clip", - default=0, - ), - NumberDef( - "handleEnd", - label="Handle End (tail)", - tooltip="Handle at end of clip", - default=0, - ), - ] diff --git a/client/ayon_unreal/plugins/create/create_editorial_package.py b/client/ayon_unreal/plugins/create/create_editorial_package.py new file mode 100644 index 00000000..f39fa90b --- /dev/null +++ b/client/ayon_unreal/plugins/create/create_editorial_package.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from pathlib import Path +import ast +import unreal + +from ayon_core.pipeline import CreatorError, CreatedInstance +from ayon_unreal.api.lib import get_shot_tracks +from ayon_unreal.api.plugin import ( + UnrealAssetCreator +) + + +class CreateEditorialPackage(UnrealAssetCreator): + """Create Editorial Package.""" + + identifier = "io.ayon.creators.unreal.editorial_pkg" + label = "Editorial Package" + product_type = "editorial_pkg" + icon = "camera" + + def _default_collect_instances(self): + # cache instances if missing + self.get_cached_instances(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', '{}')) + instance['members'] = ast.literal_eval( + instance.get('members', '[]')) + instance['families'] = ast.literal_eval( + instance.get('families', '[]')) + instance['active'] = ast.literal_eval( + instance.get('active', '')) + instance['shot_tracks'] = ast.literal_eval( + instance.get('shot_tracks', '[]')) + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + + def create(self, product_name, instance_data, pre_create_data): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + 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 CreatorError("Please select at least one Level Sequence.") + + master_lvl = None + + for sel in selection: + search_path = Path(sel).parent.as_posix() + # Get the master level. + try: + 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 CreatorError("Could not find any map for the selected sequence.") + + _, shot_sections = get_shot_tracks(sel_objects) + if not shot_sections: + raise CreatorError("No movie shot tracks found in the selected level sequence") + + instance_data["members"] = selection + instance_data["level"] = master_lvl + instance_data["shot_tracks"] = shot_sections + + super(CreateEditorialPackage, self).create( + product_name, + instance_data, + pre_create_data) diff --git a/client/ayon_unreal/plugins/publish/extract_shot_render.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py similarity index 62% rename from client/ayon_unreal/plugins/publish/extract_shot_render.py rename to client/ayon_unreal/plugins/publish/extract_intermediate_representation.py index d7dc1655..37cc2d12 100644 --- a/client/ayon_unreal/plugins/publish/extract_shot_render.py +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -3,17 +3,18 @@ import os import unreal +import pyblish.api from ayon_core.pipeline import publish -class ExtractEditorial(publish.Extractor): - """Extract Editorial""" +class ExtractIntermediateRepresentation(publish.Extractor): + """Extract Intermediate Files for Editorial Package""" - label = "Extract Shot Render(Clip)" - hosts = ["unreal"] - families = ["clip"] + label = "Extract Intermediate Representation" + order = pyblish.api.ExtractorOrder - 0.45 + families = ["editorial_pkg"] def process(self, instance): staging_dir = self.staging_dir(instance) @@ -23,8 +24,8 @@ def process(self, instance): instance.data["representations"] = [] representation = { - 'name': 'mov', - 'ext': 'mov', + "name": "intermediate", + "ext": "mov", 'files': filename, "stagingDir": staging_dir, } From f4a69568c8fccc56d09804cfad34f183691f7086 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 15 Oct 2024 14:50:56 +0800 Subject: [PATCH 09/36] remove to return unnecessary function --- client/ayon_unreal/api/lib.py | 8 ++++---- .../plugins/create/create_editorial_package.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index 4d86e5f1..0bc48c6c 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -152,8 +152,8 @@ def get_shot_tracks(sel_objects=None): movie_shot_tracks = [track for track in sub_sequence_tracks if isinstance(track, unreal.MovieSceneCinematicShotTrack)] - shot_display_names = [section.get_shot_display_name() for shot_tracks in - movie_shot_tracks for section in shot_tracks.get_sections()] - shot_sections_tracks = [section.get_shot_display_name() for shot_tracks in + # shot_display_names = [section.get_shot_display_name() for shot_tracks in + # movie_shot_tracks for section in shot_tracks.get_sections()] + shot_sections_tracks = [section for shot_tracks in movie_shot_tracks for section in shot_tracks.get_sections()] - return shot_display_names, shot_sections_tracks + return shot_sections_tracks diff --git a/client/ayon_unreal/plugins/create/create_editorial_package.py b/client/ayon_unreal/plugins/create/create_editorial_package.py index f39fa90b..37950e9f 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_package.py +++ b/client/ayon_unreal/plugins/create/create_editorial_package.py @@ -64,7 +64,7 @@ def create(self, product_name, instance_data, pre_create_data): except IndexError: raise CreatorError("Could not find any map for the selected sequence.") - _, shot_sections = get_shot_tracks(sel_objects) + shot_sections = get_shot_tracks(sel_objects) if not shot_sections: raise CreatorError("No movie shot tracks found in the selected level sequence") From 496272202044f460de71d9bbf26443b83e1079b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 15 Oct 2024 17:39:06 +0800 Subject: [PATCH 10/36] wip on implementation --- client/ayon_unreal/api/lib.py | 26 ++++++----- client/ayon_unreal/api/rendering.py | 33 +++++++++----- .../create/create_editorial_package.py | 26 +---------- .../extract_intermediate_representation.py | 45 ++++++++++++++----- 4 files changed, 72 insertions(+), 58 deletions(-) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index 0bc48c6c..0193abd6 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -134,12 +134,7 @@ def import_camera_to_level_sequence(sequence, parent_id, version_id, namespace, actor.set_actor_label(camera_actor_name) -def get_shot_tracks(sel_objects=None): - if sel_objects is None: - asset_registry = unreal.AssetRegistryHelpers.get_asset_registry() - all_assets = asset_registry.get_all_assets() - sel_objects = [obj.get_asset() for obj in all_assets] - +def get_shot_track_names(sel_objects=None, get_name=True): selection = [ a for a in sel_objects if a.get_class().get_name() == "LevelSequence" @@ -152,8 +147,17 @@ def get_shot_tracks(sel_objects=None): movie_shot_tracks = [track for track in sub_sequence_tracks if isinstance(track, unreal.MovieSceneCinematicShotTrack)] - # shot_display_names = [section.get_shot_display_name() for shot_tracks in - # movie_shot_tracks for section in shot_tracks.get_sections()] - shot_sections_tracks = [section for shot_tracks in - movie_shot_tracks for section in shot_tracks.get_sections()] - return shot_sections_tracks + if get_name: + return [section.get_shot_display_name() for shot_tracks in + movie_shot_tracks for section in shot_tracks.get_sections()] + else: + return [section for shot_tracks in + movie_shot_tracks for section in shot_tracks.get_sections()] + + +def get_shot_tracks(members): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + selected_sequences = [ + ar.get_asset_by_object_path(member).get_asset() for member in members + ] + return get_shot_track_names(selected_sequences, get_name=False) diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 712ce690..8fec5244 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -264,11 +264,12 @@ def clear_render_queue(): queue.delete_job(job) -def editorial_rendering(shot_name, sequence_path, master_level, shot_render_path): +def editorial_rendering(track, shot_name, sequence_path, master_level, render_dir, render_filename): subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) + queue = subsystem.get_queue() job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) job.set_editor_property("job_name", f"{shot_name}") - + job.author = "Ayon" job.sequence.assign(unreal.SoftObjectPath(sequence_path)) job.map.assign(unreal.SoftObjectPath(master_level)) @@ -276,19 +277,29 @@ def editorial_rendering(shot_name, sequence_path, master_level, shot_render_path output_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting) output_setting.set_editor_property( "output_directory", - unreal.DirectoryPath(os.path.dirname(shot_render_path)) - ) - output_setting.set_editor_property( - "file_name_format", - os.path.basename(shot_render_path).rsplit(".mov", 1)[0] + unreal.DirectoryPath(render_dir) ) + output_setting.set_editor_property("file_name_format", f"{render_filename}" + ".{frame_number}") - output_setting.set_editor_property("output_resolution", unreal.IntPoint(1920 / 2, 1080 / 2)) + output_setting.set_editor_property("output_resolution", unreal.IntPoint(1920, 1080)) output_setting.set_editor_property("override_existing_output", True) # Overwrite existing files + output_setting.set_editor_property("use_custom_playback_range", True) + output_setting.set_editor_property("custom_start_frame", track.get_start_frame()) + output_setting.set_editor_property("custom_end_frame", track.get_end_frame()) + config.find_or_add_setting_by_class( + unreal.MoviePipelineDeferredPassBase) - pro_res_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineAppleProResOutput) - pro_res_setting.set_editor_property("codec", unreal.AppleProResEncoderCodec.PRO_RES_422_PROXY) - + set_output_extension_from_settings("exr", config) # Render itself executor = unreal.MoviePipelinePIEExecutor() subsystem.render_queue_with_executor_instance(executor) + # Register the callback + executor.on_executor_finished_delegate.add_callable(render_finished) + + +# Define the callback function +def render_finished(executor, is_success): + if is_success: + unreal.log("Render finished successfully!") + else: + unreal.log_warning("Render did not finish successfully.") diff --git a/client/ayon_unreal/plugins/create/create_editorial_package.py b/client/ayon_unreal/plugins/create/create_editorial_package.py index 37950e9f..27d11577 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_package.py +++ b/client/ayon_unreal/plugins/create/create_editorial_package.py @@ -4,7 +4,7 @@ import unreal from ayon_core.pipeline import CreatorError, CreatedInstance -from ayon_unreal.api.lib import get_shot_tracks +from ayon_unreal.api.lib import get_shot_track_names from ayon_unreal.api.plugin import ( UnrealAssetCreator ) @@ -18,27 +18,6 @@ class CreateEditorialPackage(UnrealAssetCreator): product_type = "editorial_pkg" icon = "camera" - def _default_collect_instances(self): - # cache instances if missing - self.get_cached_instances(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', '{}')) - instance['members'] = ast.literal_eval( - instance.get('members', '[]')) - instance['families'] = ast.literal_eval( - instance.get('families', '[]')) - instance['active'] = ast.literal_eval( - instance.get('active', '')) - instance['shot_tracks'] = ast.literal_eval( - instance.get('shot_tracks', '[]')) - created_instance = CreatedInstance.from_existing(instance, self) - self._add_instance_to_context(created_instance) - def create(self, product_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() @@ -64,13 +43,12 @@ def create(self, product_name, instance_data, pre_create_data): except IndexError: raise CreatorError("Could not find any map for the selected sequence.") - shot_sections = get_shot_tracks(sel_objects) + shot_sections = get_shot_track_names(sel_objects) if not shot_sections: raise CreatorError("No movie shot tracks found in the selected level sequence") instance_data["members"] = selection instance_data["level"] = master_lvl - instance_data["shot_tracks"] = shot_sections super(CreateEditorialPackage, self).create( product_name, diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py index 37cc2d12..7f3af070 100644 --- a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -6,7 +6,8 @@ import pyblish.api from ayon_core.pipeline import publish - +from ayon_unreal.api.rendering import editorial_rendering, clear_render_queue +from ayon_unreal.api.lib import get_shot_tracks class ExtractIntermediateRepresentation(publish.Extractor): @@ -17,16 +18,36 @@ class ExtractIntermediateRepresentation(publish.Extractor): families = ["editorial_pkg"] def process(self, instance): - staging_dir = self.staging_dir(instance) - filename = "{}.mov".format(instance.name) - if "representations" not in instance.data: instance.data["representations"] = [] - - representation = { - "name": "intermediate", - "ext": "mov", - 'files': filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) + staging_dir = self.staging_dir(instance) + master_level = instance.data["level"] + members = instance.data["members"] + files = [] + representation_list = [] + clear_render_queue() + for track in get_shot_tracks(members): + folder_path = instance.data["folderPath"] + folder_path_name = folder_path.lstrip("/").replace("/", "_") + + staging_dir = self.staging_dir(instance) + timeline_name = track.get_shot_display_name() + subfolder_name = folder_path_name + "_" + timeline_name + + staging_dir = os.path.normpath( + os.path.join(staging_dir, subfolder_name)) + filename = f"{instance.name}_{timeline_name}" + filename_ext = f"{instance.name}_{timeline_name}.exr" + editorial_rendering( + track, timeline_name, members[0], master_level, staging_dir, filename) + files.append(filename_ext) + + representation = { + "name": "intermediate", + "ext": "exr", + 'files': files, + "stagingDir": staging_dir, + 'tags': ['review'] + } + representation_list.append(representation) + instance.data["representations"].extend(representation_list) From ea40c2d660a4517b64b23584a69da7ad8bdd8a98 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Oct 2024 21:01:43 +0800 Subject: [PATCH 11/36] draft the otio_unreal_export script --- client/ayon_unreal/otio/unreal_export.py | 440 ++++++++++++++++++ .../extract_intermediate_representation.py | 53 --- 2 files changed, 440 insertions(+), 53 deletions(-) create mode 100644 client/ayon_unreal/otio/unreal_export.py delete mode 100644 client/ayon_unreal/plugins/publish/extract_intermediate_representation.py diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py new file mode 100644 index 00000000..480eac8a --- /dev/null +++ b/client/ayon_unreal/otio/unreal_export.py @@ -0,0 +1,440 @@ +""" compatibility OpenTimelineIO 0.12.0 and newer +""" + +import os +import re +import ast +import opentimelineio as otio + + +TRACK_TYPES = { + "video": otio.schema.TrackKind.Video, + "audio": otio.schema.TrackKind.Audio +} +MARKER_COLOR_MAP = { + "magenta": otio.schema.MarkerColor.MAGENTA, + "red": otio.schema.MarkerColor.RED, + "yellow": otio.schema.MarkerColor.YELLOW, + "green": otio.schema.MarkerColor.GREEN, + "cyan": otio.schema.MarkerColor.CYAN, + "blue": otio.schema.MarkerColor.BLUE, +} + + +class CTX: + project_fps = None + timeline = None + include_tags = True + + +def flatten(list_): + for item_ in list_: + if isinstance(item_, (list, tuple)): + for sub_item in flatten(item_): + yield sub_item + else: + yield item_ + + +def create_otio_rational_time(frame, fps): + return otio.opentime.RationalTime( + float(frame), + float(fps) + ) + + +def create_otio_time_range(start_frame, frame_duration, fps): + return otio.opentime.TimeRange( + start_time=create_otio_rational_time(start_frame, fps), + duration=create_otio_rational_time(frame_duration, fps) + ) + + +def _get_metadata(item): + if hasattr(item, 'metadata'): + return {key: value for key, value in dict(item.metadata()).items()} + return {} + + +def create_time_effects(otio_clip, track_item): + # get all subtrack items + subTrackItems = flatten(track_item.parent().subTrackItems()) + speed = track_item.playbackSpeed() + + otio_effect = None + # retime on track item + if speed != 1.: + # make effect + otio_effect = otio.schema.LinearTimeWarp() + otio_effect.name = "Speed" + otio_effect.time_scalar = speed + + # freeze frame effect + if speed == 0.: + otio_effect = otio.schema.FreezeFrame() + otio_effect.name = "FreezeFrame" + + if otio_effect: + # add otio effect to clip effects + otio_clip.effects.append(otio_effect) + + # loop through and get all Timewarps + for effect in subTrackItems: + if ((track_item not in effect.linkedItems()) + and (len(effect.linkedItems()) > 0)): + continue + # avoid all effect which are not TimeWarp and disabled + if "TimeWarp" not in effect.name(): + continue + + if not effect.isEnabled(): + continue + + node = effect.node() + name = node["name"].value() + + # solve effect class as effect name + _name = effect.name() + if "_" in _name: + effect_name = re.sub(r"(?:_)[_0-9]+", "", _name) # more numbers + else: + effect_name = re.sub(r"\d+", "", _name) # one number + + metadata = {} + # add knob to metadata + for knob in ["lookup", "length"]: + value = node[knob].value() + animated = node[knob].isAnimated() + if animated: + value = [ + ((node[knob].getValueAt(i)) - i) + for i in range( + track_item.timelineIn(), track_item.timelineOut() + 1) + ] + + metadata[knob] = value + + # make effect + otio_effect = otio.schema.TimeEffect() + otio_effect.name = name + otio_effect.effect_name = effect_name + otio_effect.metadata.update(metadata) + + # add otio effect to clip effects + otio_clip.effects.append(otio_effect) + + +def create_otio_reference(clip): + metadata = _get_metadata(clip) + media_source = clip.mediaSource() + + # get file info for path and start frame + file_info = media_source.fileinfos().pop() + frame_start = file_info.startFrame() + path = file_info.filename() + + # get padding and other file infos + padding = media_source.filenamePadding() + file_head = media_source.filenameHead() + is_sequence = not media_source.singleFile() + frame_duration = media_source.duration() + fps = CTX.project_fps + extension = os.path.splitext(path)[-1] + + if is_sequence: + metadata.update({ + "isSequence": True, + "padding": padding + }) + + # add resolution metadata + metadata.update({ + "ayon.source.colourtransform": clip.sourceMediaColourTransform(), + "ayon.source.width": int(media_source.width()), + "ayon.source.height": int(media_source.height()), + "ayon.source.pixelAspect": float(media_source.pixelAspect()) + }) + + otio_ex_ref_item = None + + if is_sequence: + # if it is file sequence try to create `ImageSequenceReference` + # the OTIO might not be compatible so return nothing and do it old way + try: + dirname = os.path.dirname(path) + otio_ex_ref_item = otio.schema.ImageSequenceReference( + target_url_base=dirname + os.sep, + name_prefix=file_head, + name_suffix=extension, + start_frame=frame_start, + frame_zero_padding=padding, + rate=fps, + available_range=create_otio_time_range( + frame_start, + frame_duration, + fps + ) + ) + except AttributeError: + pass + + if not otio_ex_ref_item: + section_filepath = "something.mov" + # in case old OTIO or video file create `ExternalReference` + otio_ex_ref_item = otio.schema.ExternalReference( + target_url=section_filepath, + available_range=create_otio_time_range( + frame_start, + frame_duration, + fps + ) + ) + + # add metadata to otio item + add_otio_metadata(otio_ex_ref_item, media_source, **metadata) + + return otio_ex_ref_item + + +def get_marker_color(tag): + icon = tag.icon() + pat = r'icons:Tag(?P\w+)\.\w+' + + res = re.search(pat, icon) + if res: + color = res.groupdict().get('color') + if color.lower() in MARKER_COLOR_MAP: + return MARKER_COLOR_MAP[color.lower()] + + return otio.schema.MarkerColor.RED + + +def create_otio_markers(otio_item, item): + for tag in item.tags(): + if not tag.visible(): + continue + + if tag.name() == 'Copy': + # Hiero adds this tag to a lot of clips + continue + + frame_rate = CTX.project_fps + + marked_range = otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + tag.inTime(), + frame_rate + ), + duration=otio.opentime.RationalTime( + int(tag.metadata().dict().get('tag.length', '0')), + frame_rate + ) + ) + # add tag metadata but remove "tag." string + metadata = {} + + for key, value in tag.metadata().dict().items(): + _key = key.replace("tag.", "") + + try: + # capture exceptions which are related to strings only + _value = ast.literal_eval(value) + except (ValueError, SyntaxError): + _value = value + + metadata.update({_key: _value}) + + # Store the source item for future import assignment + metadata['hiero_source_type'] = item.__class__.__name__ + + marker = otio.schema.Marker( + name=tag.name(), + color=get_marker_color(tag), + marked_range=marked_range, + metadata=metadata + ) + + otio_item.markers.append(marker) + + +def create_otio_clip(track_item): + clip = track_item.source() + speed = track_item.playbackSpeed() + # flip if speed is in minus + source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() + + duration = int(track_item.duration()) + + fps = CTX.project_fps + name = track_item.name() + + media_reference = create_otio_reference(clip) + source_range = create_otio_time_range( + int(source_in), + int(duration), + fps + ) + + otio_clip = otio.schema.Clip( + name=name, + source_range=source_range, + media_reference=media_reference + ) + + # Add tags as markers + if CTX.include_tags: + create_otio_markers(otio_clip, track_item) + create_otio_markers(otio_clip, track_item.source()) + + # only if video + if not clip.mediaSource().hasAudio(): + # Add effects to clips + create_time_effects(otio_clip, track_item) + + return otio_clip + + +def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): + return otio.schema.Gap( + source_range=create_otio_time_range( + gap_start, + (clip_start - tl_start_frame) - gap_start, + fps + ) + ) + + +def _create_otio_timeline(): + project = CTX.timeline.project() + metadata = _get_metadata(CTX.timeline) + + metadata.update({ + "ayon.timeline.width": int(CTX.timeline.format().width()), + "ayon.timeline.height": int(CTX.timeline.format().height()), + "ayon.timeline.pixelAspect": int(CTX.timeline.format().pixelAspect()), # noqa + "ayon.project.useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), # noqa + "ayon.project.lutSetting16Bit": project.lutSetting16Bit(), + "ayon.project.lutSetting8Bit": project.lutSetting8Bit(), + "ayon.project.lutSettingFloat": project.lutSettingFloat(), + "ayon.project.lutSettingLog": project.lutSettingLog(), + "ayon.project.lutSettingViewer": project.lutSettingViewer(), + "ayon.project.lutSettingWorkingSpace": project.lutSettingWorkingSpace(), # noqa + "ayon.project.lutUseOCIOForExport": project.lutUseOCIOForExport(), + "ayon.project.ocioConfigName": project.ocioConfigName(), + "ayon.project.ocioConfigPath": project.ocioConfigPath() + }) + + start_time = create_otio_rational_time( + CTX.timeline.timecodeStart(), CTX.project_fps) + + return otio.schema.Timeline( + name=CTX.timeline.name(), + global_start_time=start_time, + metadata=metadata + ) + + +def create_otio_track(track_type, track_name): + return otio.schema.Track( + name=track_name, + kind=TRACK_TYPES[track_type] + ) + + +def add_otio_gap(track_item, otio_track, prev_out): + gap_length = track_item.timelineIn() - prev_out + if prev_out != 0: + gap_length -= 1 + + gap = otio.opentime.TimeRange( + duration=otio.opentime.RationalTime( + gap_length, + CTX.project_fps + ) + ) + otio_gap = otio.schema.Gap(source_range=gap) + otio_track.append(otio_gap) + + +def add_otio_metadata(otio_item, media_source, **kwargs): + metadata = _get_metadata(media_source) + + # add additional metadata from kwargs + if kwargs: + metadata.update(kwargs) + + # add metadata to otio item metadata + for key, value in metadata.items(): + otio_item.metadata.update({key: value}) + + +def create_otio_timeline(): + + def set_prev_item(itemindex, track_item): + # Add Gap if needed + if itemindex == 0: + # if it is first track item at track then add + # it to previous item + return track_item + + else: + # get previous item + return track_item.parent().items()[itemindex - 1] + + # get current timeline + CTX.timeline = "get your unreal timeline" + CTX.project_fps = CTX.timeline.framerate().toFloat() + + # convert timeline to otio + otio_timeline = _create_otio_timeline() + + # loop all defined track types + for track in CTX.timeline.items(): + # skip if track is disabled + if not track.isEnabled(): + continue + + # convert track to otio + otio_track = create_otio_track( + type(track), track.name()) + + for itemindex, track_item in enumerate(track): + # Add Gap if needed + if itemindex == 0: + # if it is first track item at track then add + # it to previous item + prev_item = track_item + + else: + # get previous item + prev_item = track_item.parent().items()[itemindex - 1] + + # calculate clip frame range difference from each other + clip_diff = track_item.timelineIn() - prev_item.timelineOut() + + # add gap if first track item is not starting + # at first timeline frame + if itemindex == 0 and track_item.timelineIn() > 0: + add_otio_gap(track_item, otio_track, 0) + + # or add gap if following track items are having + # frame range differences from each other + elif itemindex and clip_diff != 1: + add_otio_gap(track_item, otio_track, prev_item.timelineOut()) + + # create otio clip and add it to track + otio_clip = create_otio_clip(track_item) + otio_track.append(otio_clip) + + # Add tags as markers + if CTX.include_tags: + create_otio_markers(otio_track, track) + + # add track to otio timeline + otio_timeline.tracks.append(otio_track) + + return otio_timeline + + +def write_to_file(otio_timeline, path): + otio.adapters.write_to_file(otio_timeline, path) diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py deleted file mode 100644 index 7f3af070..00000000 --- a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -"""Extract Editorial from Unreal.""" -import os - -import unreal -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_unreal.api.rendering import editorial_rendering, clear_render_queue -from ayon_unreal.api.lib import get_shot_tracks - - -class ExtractIntermediateRepresentation(publish.Extractor): - """Extract Intermediate Files for Editorial Package""" - - label = "Extract Intermediate Representation" - order = pyblish.api.ExtractorOrder - 0.45 - families = ["editorial_pkg"] - - def process(self, instance): - if "representations" not in instance.data: - instance.data["representations"] = [] - staging_dir = self.staging_dir(instance) - master_level = instance.data["level"] - members = instance.data["members"] - files = [] - representation_list = [] - clear_render_queue() - for track in get_shot_tracks(members): - folder_path = instance.data["folderPath"] - folder_path_name = folder_path.lstrip("/").replace("/", "_") - - staging_dir = self.staging_dir(instance) - timeline_name = track.get_shot_display_name() - subfolder_name = folder_path_name + "_" + timeline_name - - staging_dir = os.path.normpath( - os.path.join(staging_dir, subfolder_name)) - filename = f"{instance.name}_{timeline_name}" - filename_ext = f"{instance.name}_{timeline_name}.exr" - editorial_rendering( - track, timeline_name, members[0], master_level, staging_dir, filename) - files.append(filename_ext) - - representation = { - "name": "intermediate", - "ext": "exr", - 'files': files, - "stagingDir": staging_dir, - 'tags': ['review'] - } - representation_list.append(representation) - instance.data["representations"].extend(representation_list) From 9377c8dde1981ce466d1262f6fe38de5b89ed9a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Oct 2024 21:16:42 +0800 Subject: [PATCH 12/36] draft the otio_unreal_export script --- client/ayon_unreal/otio/unreal_export.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 480eac8a..19d12427 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -149,7 +149,6 @@ def create_otio_reference(clip): # add resolution metadata metadata.update({ - "ayon.source.colourtransform": clip.sourceMediaColourTransform(), "ayon.source.width": int(media_source.width()), "ayon.source.height": int(media_source.height()), "ayon.source.pixelAspect": float(media_source.pixelAspect()) @@ -368,7 +367,7 @@ def add_otio_metadata(otio_item, media_source, **kwargs): otio_item.metadata.update({key: value}) -def create_otio_timeline(): +def create_otio_timeline(sequence): def set_prev_item(itemindex, track_item): # Add Gap if needed @@ -382,8 +381,8 @@ def set_prev_item(itemindex, track_item): return track_item.parent().items()[itemindex - 1] # get current timeline - CTX.timeline = "get your unreal timeline" - CTX.project_fps = CTX.timeline.framerate().toFloat() + CTX.timeline = "total sections lives in Shot tracks in level sequence" + CTX.project_fps = sequence.get_display_rate() # convert timeline to otio otio_timeline = _create_otio_timeline() From 30014788e9062d53fa110dfa39212fc2560dedc7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 1 Nov 2024 22:45:18 +0800 Subject: [PATCH 13/36] wip creator --- client/ayon_unreal/api/rendering.py | 55 +--- .../create/create_editorial_package.py | 260 ++++++++++++++++-- .../publish/collect_intermediate_render.py | 0 3 files changed, 247 insertions(+), 68 deletions(-) create mode 100644 client/ayon_unreal/plugins/publish/collect_intermediate_render.py diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 8fec5244..855b5f0e 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -130,7 +130,7 @@ def start_rendering(): for i in instances: data = pipeline.parse_container(i.get_path_name()) - if data["productType"] == "render": + if data["productType"] == "render" or "editorial_pkg": inst_data.append(data) try: @@ -159,6 +159,8 @@ def start_rendering(): current_level_name = current_level.get_outer().get_path_name() for i in inst_data: + if i["productType"] == "editorial_pkg": + render_dir = f"{root}/{project_name}/editorial_pkg" sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() sequences = [{ @@ -252,54 +254,3 @@ def start_rendering(): executor.on_individual_job_finished_delegate.add_callable_unique( _job_finish_callback) # Only available on PIE Executor executor.execute(queue) - - -def clear_render_queue(): - # Movie render queue - subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) - queue = subsystem.get_queue() - if not queue.get_jobs(): - return - for job in queue.get_jobs(): - queue.delete_job(job) - - -def editorial_rendering(track, shot_name, sequence_path, master_level, render_dir, render_filename): - subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) - queue = subsystem.get_queue() - job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) - job.set_editor_property("job_name", f"{shot_name}") - job.author = "Ayon" - job.sequence.assign(unreal.SoftObjectPath(sequence_path)) - job.map.assign(unreal.SoftObjectPath(master_level)) - - config = job.get_configuration() - output_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting) - output_setting.set_editor_property( - "output_directory", - unreal.DirectoryPath(render_dir) - ) - output_setting.set_editor_property("file_name_format", f"{render_filename}" + ".{frame_number}") - - output_setting.set_editor_property("output_resolution", unreal.IntPoint(1920, 1080)) - output_setting.set_editor_property("override_existing_output", True) # Overwrite existing files - output_setting.set_editor_property("use_custom_playback_range", True) - output_setting.set_editor_property("custom_start_frame", track.get_start_frame()) - output_setting.set_editor_property("custom_end_frame", track.get_end_frame()) - config.find_or_add_setting_by_class( - unreal.MoviePipelineDeferredPassBase) - - set_output_extension_from_settings("exr", config) - # Render itself - executor = unreal.MoviePipelinePIEExecutor() - subsystem.render_queue_with_executor_instance(executor) - # Register the callback - executor.on_executor_finished_delegate.add_callable(render_finished) - - -# Define the callback function -def render_finished(executor, is_success): - if is_success: - unreal.log("Render finished successfully!") - else: - unreal.log_warning("Render did not finish successfully.") diff --git a/client/ayon_unreal/plugins/create/create_editorial_package.py b/client/ayon_unreal/plugins/create/create_editorial_package.py index 27d11577..415d8d88 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_package.py +++ b/client/ayon_unreal/plugins/create/create_editorial_package.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- from pathlib import Path -import ast import unreal -from ayon_core.pipeline import CreatorError, CreatedInstance -from ayon_unreal.api.lib import get_shot_track_names +from ayon_core.pipeline import CreatorError +from ayon_unreal.api.pipeline import get_subsequences from ayon_unreal.api.plugin import ( UnrealAssetCreator ) +from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef class CreateEditorialPackage(UnrealAssetCreator): @@ -18,8 +18,32 @@ class CreateEditorialPackage(UnrealAssetCreator): product_type = "editorial_pkg" icon = "camera" + def create_instance( + self, instance_data, product_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(CreateEditorialPackage, self).create( + product_name, + instance_data, + pre_create_data) + def create(self, product_name, instance_data, pre_create_data): + self.create_from_existing_sequence( + product_name, instance_data, pre_create_data) + + def create_from_existing_sequence( + self, product_name, instance_data, pre_create_data + ): ar = unreal.AssetRegistryHelpers.get_asset_registry() + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [ a.get_path_name() for a in sel_objects @@ -28,12 +52,27 @@ def create(self, product_name, instance_data, pre_create_data): if len(selection) == 0: raise CreatorError("Please select at least one Level Sequence.") - master_lvl = None + seq_data = None for sel in selection: - search_path = Path(sel).parent.as_posix() - # Get the master level. + selected_asset = ar.get_asset_by_object_path(sel).get_asset() + selected_asset_path = selected_asset.get_path_name() + selected_asset_name = selected_asset.get_name() + search_path = Path(selected_asset_path).parent.as_posix() + package_name = f"{search_path}/{selected_asset_name}" + # Get the master sequence and the master level. + # There should be only one sequence and one level in the directory. try: + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_names=[package_name], + package_paths=[search_path], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + unreal.log("sequences") + unreal.log(sequences) + master_seq_obj = sequences[0].get_asset() + master_seq = master_seq_obj.get_path_name() ar_filter = unreal.ARFilter( class_names=["World"], package_paths=[search_path], @@ -41,16 +80,205 @@ def create(self, product_name, instance_data, pre_create_data): levels = ar.get_assets(ar_filter) master_lvl = levels[0].get_asset().get_path_name() except IndexError: - raise CreatorError("Could not find any map for the selected sequence.") + raise RuntimeError( + "Could not find the hierarchy for the selected sequence.") - shot_sections = get_shot_track_names(sel_objects) - if not shot_sections: - raise CreatorError("No movie shot tracks found in the selected level sequence") + 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())} - instance_data["members"] = selection - instance_data["level"] = master_lvl + seq_data_list = [master_seq_data] + unreal.log("data_list") + unreal.log(seq_data_list) + for seq in seq_data_list: + subscenes = get_subsequences(seq.get('sequence')) + unreal.log(subscenes) + unreal.log("subscenes") + for sub_seq in subscenes: + sub_seq_obj = sub_seq.get_sequence() + curr_data = { + "sequence": sub_seq_obj, + "output": (f"{seq.get('output')}/" + f"{sub_seq_obj.get_name()}"), + "frame_range": ( + sub_seq.get_start_frame(), + sub_seq.get_end_frame() - 1)} - super(CreateEditorialPackage, self).create( - product_name, - instance_data, - pre_create_data) + # 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 + + self.create_instance( + instance_data, product_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data) + + def get_instance_attr_defs(self): + def header_label(text): + return f"
{text}" + return [ + # hierarchyData + UILabelDef( + label=header_label("Shot Template Keywords") + ), + TextDef( + "folder", + label="{folder}", + tooltip="Name of folder used for root of generated shots.\n", + default="shot", + ), + TextDef( + "episode", + label="{episode}", + tooltip=f"Name of episode.\n", + default="ep01", + ), + TextDef( + "sequence", + label="{sequence}", + tooltip=f"Name of sequence of shots.\n", + default="sq01", + ), + TextDef( + "track", + label="{track}", + tooltip=f"Name of timeline track.\n", + default="{_track_}", + ), + TextDef( + "shot", + label="{shot}", + tooltip="Name of shot. '#' is converted to padded number.", + default="sh###", + ), + + # renameHierarchy + UILabelDef( + label=header_label("Shot Hierarchy and Rename Settings") + ), + TextDef( + "hierarchy", + label="Shot Parent Hierarchy", + tooltip="Parents folder for shot root folder, " + "Template filled with *Hierarchy Data* section", + default="{folder}/{sequence}", + ), + BoolDef( + "clipRename", + label="Rename Shots/Clips", + tooltip="Renaming selected clips on fly", + default=False, + ), + TextDef( + "clipName", + label="Rename Template", + tooltip="template for creating shot names, used for " + "renaming (use rename: on)", + default="{sequence}{shot}", + ), + NumberDef( + "countFrom", + label="Count Sequence from", + tooltip="Set where the sequence number starts from", + default=10, + ), + NumberDef( + "countSteps", + label="Stepping Number", + tooltip="What number is adding every new step", + default=10, + ), + + # verticalSync + UILabelDef( + label="Vertical Synchronization of Attributes" + ), + BoolDef( + "vSyncOn", + label="Enable Vertical Sync", + tooltip="Switch on if you want clips above " + "each other to share its attributes", + default=True, + ), + EnumDef( + "vSyncTrack", + label="Hero Track", + tooltip="Select driving track name which should " + "be mastering all others", + items= [""], + ), + + # publishSettings + UILabelDef( + label=header_label("Clip Publish Settings") + ), + EnumDef( + "clip_variant", + label="Product Variant", + tooltip="Chosen variant which will be then used for " + "product name, if " + "is selected, name of track layer will be used", + items=['', 'main', 'bg', 'fg', 'bg', 'animatic'], + ), + EnumDef( + "productType", + label="Product Type", + tooltip="How the product will be used", + items=['plate'], # it is prepared for more types + ), + BoolDef( + "export_audio", + label="Include audio", + tooltip="Process subsets with corresponding audio", + default=False, + ), + BoolDef( + "sourceResolution", + label="Source resolution", + tooltip="Is resolution taken from timeline or source?", + default=False, + ), + + # shotAttr + UILabelDef( + label=header_label("Shot Attributes"), + ), + NumberDef( + "workfileFrameStart", + label="Workfiles Start Frame", + tooltip="Set workfile starting frame number", + default=1001, + ), + NumberDef( + "handleStart", + label="Handle Start (head)", + tooltip="Handle at start of clip", + default=0, + ), + NumberDef( + "handleEnd", + label="Handle End (tail)", + tooltip="Handle at end of clip", + default=0, + ), + ] diff --git a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py new file mode 100644 index 00000000..e69de29b From 521d94bf71dd2ae2222ea0732791050e58503c63 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 1 Nov 2024 23:24:39 +0800 Subject: [PATCH 14/36] wip creator --- client/ayon_unreal/api/pipeline.py | 24 ++++++++ .../create/create_editorial_package.py | 59 ++----------------- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/client/ayon_unreal/api/pipeline.py b/client/ayon_unreal/api/pipeline.py index b8fcbc01..21223b79 100644 --- a/client/ayon_unreal/api/pipeline.py +++ b/client/ayon_unreal/api/pipeline.py @@ -526,6 +526,30 @@ def get_subsequences(sequence: unreal.LevelSequence): return [] +def get_movie_shot_tracks(sequence: unreal.LevelSequence): + """Get list of movie shot tracks from sequence. + + Args: + sequence (unreal.LevelSequence): Sequence + + Returns: + list(unreal.LevelSequence): List of movie shot tracks + + """ + tracks = sequence.find_master_tracks_by_type(unreal.MovieSceneSubTrack) + subscene_track = next( + ( + t + for t in tracks + if t.get_class() == unreal.MovieSceneCinematicShotTrack.static_class() + ), + None, + ) + if subscene_track is not None and subscene_track.get_sections(): + return subscene_track.get_sections() + return [] + + def set_sequence_hierarchy( seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths ): diff --git a/client/ayon_unreal/plugins/create/create_editorial_package.py b/client/ayon_unreal/plugins/create/create_editorial_package.py index 415d8d88..b1671300 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_package.py +++ b/client/ayon_unreal/plugins/create/create_editorial_package.py @@ -3,7 +3,7 @@ import unreal from ayon_core.pipeline import CreatorError -from ayon_unreal.api.pipeline import get_subsequences +from ayon_unreal.api.pipeline import get_subsequences, get_movie_shot_tracks from ayon_unreal.api.plugin import ( UnrealAssetCreator ) @@ -20,15 +20,12 @@ class CreateEditorialPackage(UnrealAssetCreator): def create_instance( self, instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl, seq_data + selected_asset_path, master_seq, master_lvl ): 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(CreateEditorialPackage, self).create( product_name, @@ -83,55 +80,9 @@ def create_from_existing_sequence( raise RuntimeError( "Could not find the hierarchy for the selected sequence.") - 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())} - - seq_data_list = [master_seq_data] - unreal.log("data_list") - unreal.log(seq_data_list) - for seq in seq_data_list: - subscenes = get_subsequences(seq.get('sequence')) - unreal.log(subscenes) - unreal.log("subscenes") - for sub_seq in subscenes: - sub_seq_obj = sub_seq.get_sequence() - curr_data = { - "sequence": sub_seq_obj, - "output": (f"{seq.get('output')}/" - f"{sub_seq_obj.get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), - sub_seq.get_end_frame() - 1)} - - # 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 - - self.create_instance( - instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl, seq_data) + self.create_instance( + instance_data, product_name, pre_create_data, + selected_asset_path, master_seq, master_lvl) def get_instance_attr_defs(self): def header_label(text): From 1e60e0138a72f5b3f8085f9a4e48569b795f2636 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Nov 2024 17:47:22 +0800 Subject: [PATCH 15/36] add intermediate renders and supports rendering for editorial package --- client/ayon_unreal/api/lib.py | 6 +- client/ayon_unreal/api/pipeline.py | 11 ++ .../create/create_editorial_package.py | 171 ++---------------- .../publish/collect_intermediate_render.py | 95 ++++++++++ 4 files changed, 121 insertions(+), 162 deletions(-) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index 3b92ced1..8839d241 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -319,14 +319,12 @@ def get_shot_track_names(sel_objects=None, get_name=True): sel.find_master_tracks_by_type(unreal.MovieSceneSubTrack) ] - movie_shot_tracks = [track for track in sub_sequence_tracks - if isinstance(track, unreal.MovieSceneCinematicShotTrack)] if get_name: return [section.get_shot_display_name() for shot_tracks in - movie_shot_tracks for section in shot_tracks.get_sections()] + sub_sequence_tracks for section in shot_tracks.get_sections()] else: return [section for shot_tracks in - movie_shot_tracks for section in shot_tracks.get_sections()] + sub_sequence_tracks for section in shot_tracks.get_sections()] def get_shot_tracks(members): diff --git a/client/ayon_unreal/api/pipeline.py b/client/ayon_unreal/api/pipeline.py index 21223b79..66779863 100644 --- a/client/ayon_unreal/api/pipeline.py +++ b/client/ayon_unreal/api/pipeline.py @@ -1217,3 +1217,14 @@ def generate_master_level_sequence(tools, asset_dir, asset_name, [asset_level]) return shot, master_level, asset_level, sequences, frame_ranges + + + +def get_shot_filename_by_frame_range(files, frameStart, frameEnd): + pattern = re.compile(r'\d{4}(?=\.)') + frames = [ + file for file in files + if int(pattern.search(file).group())>=frameStart or + int(pattern.search(file).group())>=frameEnd + ] + return frames diff --git a/client/ayon_unreal/plugins/create/create_editorial_package.py b/client/ayon_unreal/plugins/create/create_editorial_package.py index b1671300..24bd51a7 100644 --- a/client/ayon_unreal/plugins/create/create_editorial_package.py +++ b/client/ayon_unreal/plugins/create/create_editorial_package.py @@ -3,11 +3,9 @@ import unreal from ayon_core.pipeline import CreatorError -from ayon_unreal.api.pipeline import get_subsequences, get_movie_shot_tracks from ayon_unreal.api.plugin import ( UnrealAssetCreator ) -from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef class CreateEditorialPackage(UnrealAssetCreator): @@ -20,12 +18,16 @@ class CreateEditorialPackage(UnrealAssetCreator): def create_instance( self, instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl + 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(CreateEditorialPackage, self).create( product_name, @@ -49,7 +51,7 @@ def create_from_existing_sequence( if len(selection) == 0: raise CreatorError("Please select at least one Level Sequence.") - seq_data = None + seq_data = {} for sel in selection: selected_asset = ar.get_asset_by_object_path(sel).get_asset() @@ -66,8 +68,6 @@ def create_from_existing_sequence( package_paths=[search_path], recursive_paths=False) sequences = ar.get_assets(ar_filter) - unreal.log("sequences") - unreal.log(sequences) master_seq_obj = sequences[0].get_asset() master_seq = master_seq_obj.get_path_name() ar_filter = unreal.ARFilter( @@ -79,157 +79,12 @@ def create_from_existing_sequence( except IndexError: raise RuntimeError( "Could not find the hierarchy for the selected sequence.") - + seq_data.update({ + "output": f"{selected_asset_name}", + "frame_range": ( + selected_asset.get_playback_start(), + selected_asset.get_playback_end()) + }) self.create_instance( instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl) - - def get_instance_attr_defs(self): - def header_label(text): - return f"
{text}" - return [ - # hierarchyData - UILabelDef( - label=header_label("Shot Template Keywords") - ), - TextDef( - "folder", - label="{folder}", - tooltip="Name of folder used for root of generated shots.\n", - default="shot", - ), - TextDef( - "episode", - label="{episode}", - tooltip=f"Name of episode.\n", - default="ep01", - ), - TextDef( - "sequence", - label="{sequence}", - tooltip=f"Name of sequence of shots.\n", - default="sq01", - ), - TextDef( - "track", - label="{track}", - tooltip=f"Name of timeline track.\n", - default="{_track_}", - ), - TextDef( - "shot", - label="{shot}", - tooltip="Name of shot. '#' is converted to padded number.", - default="sh###", - ), - - # renameHierarchy - UILabelDef( - label=header_label("Shot Hierarchy and Rename Settings") - ), - TextDef( - "hierarchy", - label="Shot Parent Hierarchy", - tooltip="Parents folder for shot root folder, " - "Template filled with *Hierarchy Data* section", - default="{folder}/{sequence}", - ), - BoolDef( - "clipRename", - label="Rename Shots/Clips", - tooltip="Renaming selected clips on fly", - default=False, - ), - TextDef( - "clipName", - label="Rename Template", - tooltip="template for creating shot names, used for " - "renaming (use rename: on)", - default="{sequence}{shot}", - ), - NumberDef( - "countFrom", - label="Count Sequence from", - tooltip="Set where the sequence number starts from", - default=10, - ), - NumberDef( - "countSteps", - label="Stepping Number", - tooltip="What number is adding every new step", - default=10, - ), - - # verticalSync - UILabelDef( - label="Vertical Synchronization of Attributes" - ), - BoolDef( - "vSyncOn", - label="Enable Vertical Sync", - tooltip="Switch on if you want clips above " - "each other to share its attributes", - default=True, - ), - EnumDef( - "vSyncTrack", - label="Hero Track", - tooltip="Select driving track name which should " - "be mastering all others", - items= [""], - ), - - # publishSettings - UILabelDef( - label=header_label("Clip Publish Settings") - ), - EnumDef( - "clip_variant", - label="Product Variant", - tooltip="Chosen variant which will be then used for " - "product name, if " - "is selected, name of track layer will be used", - items=['', 'main', 'bg', 'fg', 'bg', 'animatic'], - ), - EnumDef( - "productType", - label="Product Type", - tooltip="How the product will be used", - items=['plate'], # it is prepared for more types - ), - BoolDef( - "export_audio", - label="Include audio", - tooltip="Process subsets with corresponding audio", - default=False, - ), - BoolDef( - "sourceResolution", - label="Source resolution", - tooltip="Is resolution taken from timeline or source?", - default=False, - ), - - # shotAttr - UILabelDef( - label=header_label("Shot Attributes"), - ), - NumberDef( - "workfileFrameStart", - label="Workfiles Start Frame", - tooltip="Set workfile starting frame number", - default=1001, - ), - NumberDef( - "handleStart", - label="Handle Start (head)", - tooltip="Handle at start of clip", - default=0, - ), - NumberDef( - "handleEnd", - label="Handle End (tail)", - tooltip="Handle at end of clip", - default=0, - ), - ] + selected_asset_path, master_seq, master_lvl, seq_data) diff --git a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py index e69de29b..f99bfd14 100644 --- a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py +++ b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py @@ -0,0 +1,95 @@ +from pathlib import Path + +import unreal +import os +from ayon_core.pipeline import get_current_project_name +from ayon_core.pipeline import Anatomy +from ayon_unreal.api import pipeline +from ayon_unreal.api.lib import get_shot_tracks +import pyblish.api + + +class CollectIntermediateRender(pyblish.api.InstancePlugin): + """ This collector will try to find + all the rendered frames for intermediate rendering + + Secondary step after local rendering. Should collect all rendered files and + add them as representation. + """ + order = pyblish.api.CollectorOrder + 0.001 + hosts = ["unreal"] + families = ["editorial_pkg"] + label = "Collect Intermediate Render" + + def process(self, instance): + self.log.debug("Collecting rendered files") + context = instance.context + + data = instance.data + data['remove'] = True + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path( + data.get('sequence')).get_asset() + members = instance.data["members"] + for track in get_shot_tracks(members): + track_name = track.get_shot_display_name() + + product_type = "render" + new_product_name = f"{data.get('productName')}_{track_name}" + new_instance = context.create_instance( + new_product_name + ) + new_instance[:] = track_name + + new_data = new_instance.data + + new_data["folderPath"] = instance.data["folderPath"] + new_data["setMembers"] = track_name + new_data["productName"] = new_product_name + new_data["productType"] = product_type + new_data["family"] = product_type + new_data["families"] = [product_type, "review"] + new_data["parent"] = data.get("parent") + new_data["level"] = data.get("level") + new_data["output"] = data['output'] + new_data["fps"] = sequence.get_display_rate().numerator + new_data["frameStart"] = int(track.get_start_frame()) + new_data["frameEnd"] = int(track.get_end_frame()) + new_data["sequence"] = track_name + new_data["master_sequence"] = data["master_sequence"] + new_data["master_level"] = data["master_level"] + + self.log.debug(f"new instance data: {new_data}") + + try: + project = get_current_project_name() + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + except Exception as e: + raise Exception(( + "Could not find render root " + "in anatomy settings.")) from e + + render_dir = f"{root}/{project}/editorial_pkg/{data.get('output')}" + render_path = Path(render_dir) + self.log.debug(f"Collecting render path: {render_path}") + frames = [str(x) for x in render_path.iterdir() if x.is_file()] + frames = pipeline.get_shot_filename_by_frame_range( + frames, track.get_start_frame(), track.get_end_frame()) + frames = pipeline.get_sequence(frames) + image_format = next((os.path.splitext(x)[-1].lstrip(".") + for x in frames), "exr") + + if "representations" not in new_instance.data: + new_instance.data["representations"] = [] + + repr = { + 'frameStart': int(track.get_start_frame()), + 'frameEnd': int(track.get_end_frame()), + 'name': image_format, + 'ext': image_format, + 'files': frames, + 'stagingDir': render_dir, + 'tags': ['review'] + } + new_instance.data["representations"].append(repr) From 4864345c3446fbbcb89607d12fbf3b0c16633670 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Nov 2024 18:01:39 +0800 Subject: [PATCH 16/36] add the check on the publish error in the collector if the users do not render out the frame first, and make sure the product name unique --- .../publish/collect_intermediate_render.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py index f99bfd14..547567fb 100644 --- a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py +++ b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py @@ -2,8 +2,8 @@ import unreal import os -from ayon_core.pipeline import get_current_project_name -from ayon_core.pipeline import Anatomy +from ayon_core.pipeline import get_current_project_name, Anatomy +from ayon_core.pipeline.publish import PublishError from ayon_unreal.api import pipeline from ayon_unreal.api.lib import get_shot_tracks import pyblish.api @@ -31,11 +31,13 @@ def process(self, instance): sequence = ar.get_asset_by_object_path( data.get('sequence')).get_asset() members = instance.data["members"] - for track in get_shot_tracks(members): + for i, track in enumerate(get_shot_tracks(members)): track_name = track.get_shot_display_name() product_type = "render" - new_product_name = f"{data.get('productName')}_{track_name}" + new_product_name = ( + f"{data.get('productName')}_{track_name}_{i + 1}" + ) new_instance = context.create_instance( new_product_name ) @@ -72,6 +74,13 @@ def process(self, instance): render_dir = f"{root}/{project}/editorial_pkg/{data.get('output')}" render_path = Path(render_dir) + if not os.path.exists(render_path): + msg = ( + f"Render directory {render_path} not found." + " Please render with the render instance" + ) + self.log.error(msg) + raise PublishError(msg, title="Render directory not found.") self.log.debug(f"Collecting render path: {render_path}") frames = [str(x) for x in render_path.iterdir() if x.is_file()] frames = pipeline.get_shot_filename_by_frame_range( From 306cd6e438599b4da70c508a6c2ca3bc293c7523 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Nov 2024 18:57:30 +0800 Subject: [PATCH 17/36] make sure the frame range is correct --- client/ayon_unreal/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_unreal/api/pipeline.py b/client/ayon_unreal/api/pipeline.py index 66779863..168f0f01 100644 --- a/client/ayon_unreal/api/pipeline.py +++ b/client/ayon_unreal/api/pipeline.py @@ -1224,7 +1224,7 @@ def get_shot_filename_by_frame_range(files, frameStart, frameEnd): pattern = re.compile(r'\d{4}(?=\.)') frames = [ file for file in files - if int(pattern.search(file).group())>=frameStart or - int(pattern.search(file).group())>=frameEnd + if int(pattern.search(file).group())>=frameStart and + int(pattern.search(file).group())<=frameEnd ] return frames From 6f93a9c49e92aec0e2573d220a16a58d4901b917 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Nov 2024 21:49:34 +0800 Subject: [PATCH 18/36] edit rendering py for rendering out the image in different directory --- client/ayon_unreal/api/rendering.py | 51 ++++++++++++------- .../publish/collect_intermediate_render.py | 4 +- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 855b5f0e..3826de72 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -1,10 +1,11 @@ import os - +import ast import unreal from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy from ayon_unreal.api import pipeline +from ayon_unreal.api.lib import get_shot_tracks from ayon_core.tools.utils import show_message_dialog @@ -159,6 +160,7 @@ def start_rendering(): current_level_name = current_level.get_outer().get_path_name() for i in inst_data: + unreal.log(i) if i["productType"] == "editorial_pkg": render_dir = f"{root}/{project_name}/editorial_pkg" sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() @@ -175,22 +177,36 @@ 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 seq in sequences: - subscenes = pipeline.get_subsequences(seq.get('sequence')) - - if subscenes: - for sub_seq in subscenes: - sequences.append({ - "sequence": sub_seq.get_sequence(), - "output": (f"{seq.get('output')}/" - f"{sub_seq.get_sequence().get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), sub_seq.get_end_frame()) - }) - else: - # Avoid rendering camera sequences - if "_camera" not in seq.get('sequence').get_name(): - render_list.append(seq) + if i["productType"] == "render": + for seq in sequences: + subscenes = pipeline.get_subsequences(seq.get('sequence')) + + if subscenes: + for sub_seq in subscenes: + sequences.append({ + "sequence": sub_seq.get_sequence(), + "output": (f"{seq.get('output')}/" + f"{sub_seq.get_sequence().get_name()}"), + "frame_range": ( + sub_seq.get_start_frame(), sub_seq.get_end_frame()) + }) + else: + # Avoid rendering camera sequences + if "_camera" not in seq.get('sequence').get_name(): + render_list.append(seq) + + elif i["productType"] == "editorial_pkg": + members = ast.literal_eval(i["members"]) + for i, track in enumerate(get_shot_tracks(members)): + track_name = track.get_shot_display_name() + track_section = [{ + "sequence": track, + "output": f"{i['output']}/{track_name}_{i + 1}", + "frame_range": ( + int(track.get_start_frame()), + int(track.get_end_frame())) + }] + render_list.extend(track_section) if i["master_level"] != current_level_name: unreal.log_warning( @@ -215,7 +231,6 @@ def start_rendering(): # read in the job's OnJobFinished callback. We could, # for instance, pass the AyonPublishInstance's path to the job. # job.user_data = "" - output_dir = render_setting.get('output') shot_name = render_setting.get('sequence').get_name() diff --git a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py index 547567fb..252d4f49 100644 --- a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py +++ b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py @@ -71,8 +71,8 @@ def process(self, instance): raise Exception(( "Could not find render root " "in anatomy settings.")) from e - render_dir = f"{root}/{project}/editorial_pkg/{data.get('output')}" + render_dir = f"{render_dir}/{track_name}_{i + 1}" render_path = Path(render_dir) if not os.path.exists(render_path): msg = ( @@ -83,8 +83,6 @@ def process(self, instance): raise PublishError(msg, title="Render directory not found.") self.log.debug(f"Collecting render path: {render_path}") frames = [str(x) for x in render_path.iterdir() if x.is_file()] - frames = pipeline.get_shot_filename_by_frame_range( - frames, track.get_start_frame(), track.get_end_frame()) frames = pipeline.get_sequence(frames) image_format = next((os.path.splitext(x)[-1].lstrip(".") for x in frames), "exr") From c6afe1ec5cb8fe157050924c7825382f03120495 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Nov 2024 21:52:36 +0800 Subject: [PATCH 19/36] fix the enumerate value --- client/ayon_unreal/api/rendering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 3826de72..d09c02f1 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -197,11 +197,11 @@ def start_rendering(): elif i["productType"] == "editorial_pkg": members = ast.literal_eval(i["members"]) - for i, track in enumerate(get_shot_tracks(members)): + for e, track in enumerate(get_shot_tracks(members)): track_name = track.get_shot_display_name() track_section = [{ "sequence": track, - "output": f"{i['output']}/{track_name}_{i + 1}", + "output": f"{i['output']}/{track_name}_{e + 1}", "frame_range": ( int(track.get_start_frame()), int(track.get_end_frame())) From be237a5f59bb1e44deb2e56c1fd07ea96555bb80 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Nov 2024 16:01:01 +0800 Subject: [PATCH 20/36] update the intermediate render product type as editorial publish and implement the functions for storing the relevant editorial data --- client/ayon_unreal/api/pipeline.py | 11 ------ client/ayon_unreal/api/plugin.py | 37 +++++++++++++++++++ .../publish/collect_intermediate_render.py | 4 +- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/client/ayon_unreal/api/pipeline.py b/client/ayon_unreal/api/pipeline.py index 168f0f01..21223b79 100644 --- a/client/ayon_unreal/api/pipeline.py +++ b/client/ayon_unreal/api/pipeline.py @@ -1217,14 +1217,3 @@ def generate_master_level_sequence(tools, asset_dir, asset_name, [asset_level]) return shot, master_level, asset_level, sequences, frame_ranges - - - -def get_shot_filename_by_frame_range(files, frameStart, frameEnd): - pattern = re.compile(r'\d{4}(?=\.)') - frames = [ - file for file in files - if int(pattern.search(file).group())>=frameStart and - int(pattern.search(file).group())<=frameEnd - ] - return frames diff --git a/client/ayon_unreal/api/plugin.py b/client/ayon_unreal/api/plugin.py index 6212d1c0..1ccde98c 100644 --- a/client/ayon_unreal/api/plugin.py +++ b/client/ayon_unreal/api/plugin.py @@ -22,6 +22,7 @@ BoolDef, UILabelDef ) +from ayon_core.pipeline.constants import AVALON_INSTANCE_ID from ayon_core.pipeline import ( AutoCreator, Creator, @@ -483,3 +484,39 @@ def _remove_Loaded_asset(self, container): unreal.AppMsgType.YES_NO) if (remove_asset_confirmation_dialog == unreal.AppReturnType.YES): remove_loaded_asset(container) + + +def get_editorial_publish_data( + folder_path, + product_name, + version=None, + task=None, +) -> dict: + """Get editorial publish data from context. + + Args: + folder_path (str): Folder path where editorial package is located. + product_name (str): Editorial product name. + version (Optional[str]): Editorial product version. Defaults to None. + task (Optional[str]): Associated task name. Defaults to None (no task). + + Returns: + dict: Editorial publish data. + """ + data = { + "id": AVALON_INSTANCE_ID, + "family": "editorial_pkg", + "productType": "editorial_pkg", + "productName": product_name, + "folderPath": folder_path, + "active": True, + "publish": True, + } + + if version: + data["version"] = version + + if task: + data["task"] = task + + return data diff --git a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py index 252d4f49..db724da1 100644 --- a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py +++ b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py @@ -34,7 +34,7 @@ def process(self, instance): for i, track in enumerate(get_shot_tracks(members)): track_name = track.get_shot_display_name() - product_type = "render" + product_type = data["productType"] new_product_name = ( f"{data.get('productName')}_{track_name}_{i + 1}" ) @@ -93,7 +93,7 @@ def process(self, instance): repr = { 'frameStart': int(track.get_start_frame()), 'frameEnd': int(track.get_end_frame()), - 'name': image_format, + 'name': "intermediate", 'ext': image_format, 'files': frames, 'stagingDir': render_dir, From 2cbab6e0e1eeb63adb7fde9c3e075d8e17f2c3f2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Nov 2024 18:45:42 +0800 Subject: [PATCH 21/36] edit the unreal_export.py --- client/ayon_unreal/api/plugin.py | 36 ----- client/ayon_unreal/otio/unreal_export.py | 159 ++++------------------- 2 files changed, 28 insertions(+), 167 deletions(-) diff --git a/client/ayon_unreal/api/plugin.py b/client/ayon_unreal/api/plugin.py index 1ccde98c..87f2acd8 100644 --- a/client/ayon_unreal/api/plugin.py +++ b/client/ayon_unreal/api/plugin.py @@ -484,39 +484,3 @@ def _remove_Loaded_asset(self, container): unreal.AppMsgType.YES_NO) if (remove_asset_confirmation_dialog == unreal.AppReturnType.YES): remove_loaded_asset(container) - - -def get_editorial_publish_data( - folder_path, - product_name, - version=None, - task=None, -) -> dict: - """Get editorial publish data from context. - - Args: - folder_path (str): Folder path where editorial package is located. - product_name (str): Editorial product name. - version (Optional[str]): Editorial product version. Defaults to None. - task (Optional[str]): Associated task name. Defaults to None (no task). - - Returns: - dict: Editorial publish data. - """ - data = { - "id": AVALON_INSTANCE_ID, - "family": "editorial_pkg", - "productType": "editorial_pkg", - "productName": product_name, - "folderPath": folder_path, - "active": True, - "publish": True, - } - - if version: - data["version"] = version - - if task: - data["task"] = task - - return data diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 19d12427..99bbc5f8 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -4,11 +4,13 @@ import os import re import ast +import unreal +from ayon_unreal.api.lib import get_shot_tracks import opentimelineio as otio TRACK_TYPES = { - "video": otio.schema.TrackKind.Video, + "MovieSceneSubTrack": otio.schema.TrackKind.Video, "audio": otio.schema.TrackKind.Audio } MARKER_COLOR_MAP = { @@ -25,6 +27,7 @@ class CTX: project_fps = None timeline = None include_tags = True + instance = None def flatten(list_): @@ -56,74 +59,6 @@ def _get_metadata(item): return {} -def create_time_effects(otio_clip, track_item): - # get all subtrack items - subTrackItems = flatten(track_item.parent().subTrackItems()) - speed = track_item.playbackSpeed() - - otio_effect = None - # retime on track item - if speed != 1.: - # make effect - otio_effect = otio.schema.LinearTimeWarp() - otio_effect.name = "Speed" - otio_effect.time_scalar = speed - - # freeze frame effect - if speed == 0.: - otio_effect = otio.schema.FreezeFrame() - otio_effect.name = "FreezeFrame" - - if otio_effect: - # add otio effect to clip effects - otio_clip.effects.append(otio_effect) - - # loop through and get all Timewarps - for effect in subTrackItems: - if ((track_item not in effect.linkedItems()) - and (len(effect.linkedItems()) > 0)): - continue - # avoid all effect which are not TimeWarp and disabled - if "TimeWarp" not in effect.name(): - continue - - if not effect.isEnabled(): - continue - - node = effect.node() - name = node["name"].value() - - # solve effect class as effect name - _name = effect.name() - if "_" in _name: - effect_name = re.sub(r"(?:_)[_0-9]+", "", _name) # more numbers - else: - effect_name = re.sub(r"\d+", "", _name) # one number - - metadata = {} - # add knob to metadata - for knob in ["lookup", "length"]: - value = node[knob].value() - animated = node[knob].isAnimated() - if animated: - value = [ - ((node[knob].getValueAt(i)) - i) - for i in range( - track_item.timelineIn(), track_item.timelineOut() + 1) - ] - - metadata[knob] = value - - # make effect - otio_effect = otio.schema.TimeEffect() - otio_effect.name = name - otio_effect.effect_name = effect_name - otio_effect.metadata.update(metadata) - - # add otio effect to clip effects - otio_clip.effects.append(otio_effect) - - def create_otio_reference(clip): metadata = _get_metadata(clip) media_source = clip.mediaSource() @@ -149,8 +84,8 @@ def create_otio_reference(clip): # add resolution metadata metadata.update({ - "ayon.source.width": int(media_source.width()), - "ayon.source.height": int(media_source.height()), + "ayon.source.width": 1920, + "ayon.source.height": 1080, "ayon.source.pixelAspect": float(media_source.pixelAspect()) }) @@ -178,7 +113,7 @@ def create_otio_reference(clip): pass if not otio_ex_ref_item: - section_filepath = "something.mov" + section_filepath = "something.mp4" # in case old OTIO or video file create `ExternalReference` otio_ex_ref_item = otio.schema.ExternalReference( target_url=section_filepath, @@ -285,10 +220,10 @@ def create_otio_clip(track_item): create_otio_markers(otio_clip, track_item) create_otio_markers(otio_clip, track_item.source()) - # only if video - if not clip.mediaSource().hasAudio(): - # Add effects to clips - create_time_effects(otio_clip, track_item) + # # only if video + # if not clip.mediaSource().hasAudio(): + # # Add effects to clips + # create_time_effects(otio_clip, track_item) return otio_clip @@ -303,9 +238,9 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def _create_otio_timeline(): - project = CTX.timeline.project() - metadata = _get_metadata(CTX.timeline) +def _create_otio_timeline(instance): + project = CTX.timeline.get_name() + metadata = _get_metadata(instance) metadata.update({ "ayon.timeline.width": int(CTX.timeline.format().width()), @@ -367,63 +302,25 @@ def add_otio_metadata(otio_item, media_source, **kwargs): otio_item.metadata.update({key: value}) -def create_otio_timeline(sequence): - - def set_prev_item(itemindex, track_item): - # Add Gap if needed - if itemindex == 0: - # if it is first track item at track then add - # it to previous item - return track_item - - else: - # get previous item - return track_item.parent().items()[itemindex - 1] - +def create_otio_timeline(instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path( + instance.data.get('sequence')).get_asset() # get current timeline - CTX.timeline = "total sections lives in Shot tracks in level sequence" - CTX.project_fps = sequence.get_display_rate() - + CTX.timeline = sequence + CTX.project_fps = CTX.timeline.get_display_rate() # convert timeline to otio - otio_timeline = _create_otio_timeline() - + otio_timeline = _create_otio_timeline(instance) + members = instance.data["members"] # loop all defined track types - for track in CTX.timeline.items(): - # skip if track is disabled - if not track.isEnabled(): - continue - + for track in get_shot_tracks(members): # convert track to otio otio_track = create_otio_track( - type(track), track.name()) - - for itemindex, track_item in enumerate(track): - # Add Gap if needed - if itemindex == 0: - # if it is first track item at track then add - # it to previous item - prev_item = track_item - - else: - # get previous item - prev_item = track_item.parent().items()[itemindex - 1] - - # calculate clip frame range difference from each other - clip_diff = track_item.timelineIn() - prev_item.timelineOut() - - # add gap if first track item is not starting - # at first timeline frame - if itemindex == 0 and track_item.timelineIn() > 0: - add_otio_gap(track_item, otio_track, 0) - - # or add gap if following track items are having - # frame range differences from each other - elif itemindex and clip_diff != 1: - add_otio_gap(track_item, otio_track, prev_item.timelineOut()) - - # create otio clip and add it to track - otio_clip = create_otio_clip(track_item) - otio_track.append(otio_clip) + track.get_class().get_name(), track.get_display_name()) + + # create otio clip and add it to track + otio_clip = create_otio_clip(track) + otio_track.append(otio_clip) # Add tags as markers if CTX.include_tags: From b146626d5092e790b7071caa0a67854f9e17cea3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Nov 2024 22:03:21 +0800 Subject: [PATCH 22/36] coverting otio logic to accompany wioth unreal in unreal_export.py --- client/ayon_unreal/api/lib.py | 5 + client/ayon_unreal/otio/unreal_export.py | 184 ++++++----------------- 2 files changed, 48 insertions(+), 141 deletions(-) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index 8839d241..0005d803 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -333,3 +333,8 @@ def get_shot_tracks(members): ar.get_asset_by_object_path(member).get_asset() for member in members ] return get_shot_track_names(selected_sequences, get_name=False) + + +def get_screen_resolution(): + game_user_settings = unreal.GameUserSettings.get_game_user_settings() + return game_user_settings.get_screen_resolution() diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 99bbc5f8..5944b818 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -5,21 +5,13 @@ import re import ast import unreal -from ayon_unreal.api.lib import get_shot_tracks +from ayon_unreal.api.lib import get_shot_tracks, get_screen_resolution import opentimelineio as otio TRACK_TYPES = { "MovieSceneSubTrack": otio.schema.TrackKind.Video, - "audio": otio.schema.TrackKind.Audio -} -MARKER_COLOR_MAP = { - "magenta": otio.schema.MarkerColor.MAGENTA, - "red": otio.schema.MarkerColor.RED, - "yellow": otio.schema.MarkerColor.YELLOW, - "green": otio.schema.MarkerColor.GREEN, - "cyan": otio.schema.MarkerColor.CYAN, - "blue": otio.schema.MarkerColor.BLUE, + "MovieSceneAudioTrack": otio.schema.TrackKind.Audio } @@ -30,15 +22,6 @@ class CTX: instance = None -def flatten(list_): - for item_ in list_: - if isinstance(item_, (list, tuple)): - for sub_item in flatten(item_): - yield sub_item - else: - yield item_ - - def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), @@ -59,9 +42,9 @@ def _get_metadata(item): return {} -def create_otio_reference(clip): - metadata = _get_metadata(clip) - media_source = clip.mediaSource() +def create_otio_reference(instance, section): + metadata = _get_metadata(instance) + media_source = section.mediaSource() # get file info for path and start frame file_info = media_source.fileinfos().pop() @@ -83,10 +66,10 @@ def create_otio_reference(clip): }) # add resolution metadata + resolution = get_screen_resolution() metadata.update({ - "ayon.source.width": 1920, - "ayon.source.height": 1080, - "ayon.source.pixelAspect": float(media_source.pixelAspect()) + "ayon.source.width": resolution.x, + "ayon.source.height": resolution.y, }) otio_ex_ref_item = None @@ -130,102 +113,34 @@ def create_otio_reference(clip): return otio_ex_ref_item -def get_marker_color(tag): - icon = tag.icon() - pat = r'icons:Tag(?P\w+)\.\w+' - - res = re.search(pat, icon) - if res: - color = res.groupdict().get('color') - if color.lower() in MARKER_COLOR_MAP: - return MARKER_COLOR_MAP[color.lower()] - - return otio.schema.MarkerColor.RED - - -def create_otio_markers(otio_item, item): - for tag in item.tags(): - if not tag.visible(): - continue +def create_otio_clip(instance, target_track): + for section in target_track.get_sections(): + # flip if speed is in minus + shot_start = section.get_start_frame() + duration = int(section.get_end_frame() - section.get_start_frame()) + 1 - if tag.name() == 'Copy': - # Hiero adds this tag to a lot of clips - continue + fps = CTX.project_fps + name = section.get_shot_display_name() - frame_rate = CTX.project_fps - - marked_range = otio.opentime.TimeRange( - start_time=otio.opentime.RationalTime( - tag.inTime(), - frame_rate - ), - duration=otio.opentime.RationalTime( - int(tag.metadata().dict().get('tag.length', '0')), - frame_rate - ) + media_reference = create_otio_reference(instance, section) + source_range = create_otio_time_range( + int(shot_start), + int(duration), + fps ) - # add tag metadata but remove "tag." string - metadata = {} - - for key, value in tag.metadata().dict().items(): - _key = key.replace("tag.", "") - - try: - # capture exceptions which are related to strings only - _value = ast.literal_eval(value) - except (ValueError, SyntaxError): - _value = value - - metadata.update({_key: _value}) - - # Store the source item for future import assignment - metadata['hiero_source_type'] = item.__class__.__name__ - marker = otio.schema.Marker( - name=tag.name(), - color=get_marker_color(tag), - marked_range=marked_range, - metadata=metadata + otio_clip = otio.schema.Clip( + name=name, + source_range=source_range, + media_reference=media_reference ) - otio_item.markers.append(marker) + # # only if video + # if not clip.mediaSource().hasAudio(): + # # Add effects to clips + # create_time_effects(otio_clip, track_item) - -def create_otio_clip(track_item): - clip = track_item.source() - speed = track_item.playbackSpeed() - # flip if speed is in minus - source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() - - duration = int(track_item.duration()) - - fps = CTX.project_fps - name = track_item.name() - - media_reference = create_otio_reference(clip) - source_range = create_otio_time_range( - int(source_in), - int(duration), - fps - ) - - otio_clip = otio.schema.Clip( - name=name, - source_range=source_range, - media_reference=media_reference - ) - - # Add tags as markers - if CTX.include_tags: - create_otio_markers(otio_clip, track_item) - create_otio_markers(otio_clip, track_item.source()) - - # # only if video - # if not clip.mediaSource().hasAudio(): - # # Add effects to clips - # create_time_effects(otio_clip, track_item) - - return otio_clip + return otio_clip def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): @@ -239,23 +154,13 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): def _create_otio_timeline(instance): - project = CTX.timeline.get_name() metadata = _get_metadata(instance) - + resolution = get_screen_resolution() metadata.update({ - "ayon.timeline.width": int(CTX.timeline.format().width()), - "ayon.timeline.height": int(CTX.timeline.format().height()), - "ayon.timeline.pixelAspect": int(CTX.timeline.format().pixelAspect()), # noqa - "ayon.project.useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), # noqa - "ayon.project.lutSetting16Bit": project.lutSetting16Bit(), - "ayon.project.lutSetting8Bit": project.lutSetting8Bit(), - "ayon.project.lutSettingFloat": project.lutSettingFloat(), - "ayon.project.lutSettingLog": project.lutSettingLog(), - "ayon.project.lutSettingViewer": project.lutSettingViewer(), - "ayon.project.lutSettingWorkingSpace": project.lutSettingWorkingSpace(), # noqa - "ayon.project.lutUseOCIOForExport": project.lutUseOCIOForExport(), - "ayon.project.ocioConfigName": project.ocioConfigName(), - "ayon.project.ocioConfigPath": project.ocioConfigPath() + "ayon.timeline.width": int(resolution.x), + "ayon.timeline.height": int(resolution.y), + # "ayon.project.ocioConfigName": unreal.OpenColorIOConfiguration().get_name(), + # "ayon.project.ocioConfigPath": unreal.OpenColorIOConfiguration().configuration_file }) start_time = create_otio_rational_time( @@ -275,8 +180,8 @@ def create_otio_track(track_type, track_name): ) -def add_otio_gap(track_item, otio_track, prev_out): - gap_length = track_item.timelineIn() - prev_out +def add_otio_gap(track_section, otio_track, prev_out): + gap_length = track_section.get_start_frame() - prev_out if prev_out != 0: gap_length -= 1 @@ -290,8 +195,8 @@ def add_otio_gap(track_item, otio_track, prev_out): otio_track.append(otio_gap) -def add_otio_metadata(otio_item, media_source, **kwargs): - metadata = _get_metadata(media_source) +def add_otio_metadata(instance, otio_item, **kwargs): + metadata = _get_metadata(instance) # add additional metadata from kwargs if kwargs: @@ -299,7 +204,7 @@ def add_otio_metadata(otio_item, media_source, **kwargs): # add metadata to otio item metadata for key, value in metadata.items(): - otio_item.metadata.update({key: value}) + instance.data.update({key: value}) def create_otio_timeline(instance): @@ -313,19 +218,16 @@ def create_otio_timeline(instance): otio_timeline = _create_otio_timeline(instance) members = instance.data["members"] # loop all defined track types - for track in get_shot_tracks(members): + for target_track in get_shot_tracks(members): # convert track to otio otio_track = create_otio_track( - track.get_class().get_name(), track.get_display_name()) + target_track.get_class().get_name(), + target_track.get_display_name()) # create otio clip and add it to track - otio_clip = create_otio_clip(track) + otio_clip = create_otio_clip(instance, target_track) otio_track.append(otio_clip) - # Add tags as markers - if CTX.include_tags: - create_otio_markers(otio_track, track) - # add track to otio timeline otio_timeline.tracks.append(otio_track) From 9291eaa78e2ea5826ccd3856b43a77a2761a7ea6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Nov 2024 22:04:57 +0800 Subject: [PATCH 23/36] coverting otio logic to accompany wioth unreal in unreal_export.py --- client/ayon_unreal/otio/unreal_export.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 5944b818..64aec580 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -195,8 +195,8 @@ def add_otio_gap(track_section, otio_track, prev_out): otio_track.append(otio_gap) -def add_otio_metadata(instance, otio_item, **kwargs): - metadata = _get_metadata(instance) +def add_otio_metadata(otio_item, media_source, **kwargs): + metadata = _get_metadata(media_source) # add additional metadata from kwargs if kwargs: @@ -204,7 +204,7 @@ def add_otio_metadata(instance, otio_item, **kwargs): # add metadata to otio item metadata for key, value in metadata.items(): - instance.data.update({key: value}) + otio_item.metadata.update({key: value}) def create_otio_timeline(instance): From 78d794e048cb40fe5ab4250f6415b4b41283eaf8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Nov 2024 22:32:52 +0800 Subject: [PATCH 24/36] implement unreal logic into unreal_export.py --- client/ayon_unreal/api/pipeline.py | 34 ++++++++++++++++++ client/ayon_unreal/otio/unreal_export.py | 44 +++++++++++------------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/client/ayon_unreal/api/pipeline.py b/client/ayon_unreal/api/pipeline.py index 21223b79..c64437d5 100644 --- a/client/ayon_unreal/api/pipeline.py +++ b/client/ayon_unreal/api/pipeline.py @@ -952,6 +952,40 @@ def get_sequence(files): return [os.path.basename(filename) for filename in collections[0]] +def get_sequence_for_otio(files): + """Get sequence from filename. + + This will only return files if they exist on disk as it tries + to collect the sequence using the filename pattern and searching + for them on disk. + + Supports negative frame ranges like -001, 0000, 0001 and -0001, + 0000, 0001. + + Arguments: + files (str): List of files + + Returns: + Optional[list[str]]: file sequence. + Optional[str]: file head. + + """ + base_filenames = [os.path.basename(filename) for filename in files] + collections, _remainder = clique.assemble( + base_filenames, + patterns=[clique.PATTERNS["frames"]], + minimum_items=1) + + if len(collections) > 1: + raise ValueError( + f"Multiple collections found for {collections}. " + "This is a bug.") + filename_head = collections[0].head + filename_padding = collections[0].padding + filename_tail = collections[0].tail + return filename_head, filename_padding, filename_tail + + def find_camera_actors_in_camera_tracks(sequence) -> list[Any]: """Find the camera actors in the tracks from the Level Sequence diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 64aec580..6ed2e19f 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -2,10 +2,11 @@ """ import os -import re -import ast import unreal +from pathlib import Path +from ayon_core.pipeline import get_current_project_name, Anatomy from ayon_unreal.api.lib import get_shot_tracks, get_screen_resolution +from ayon_unreal.api.pipeline import get_sequence_for_otio import opentimelineio as otio @@ -42,22 +43,21 @@ def _get_metadata(item): return {} -def create_otio_reference(instance, section): +def create_otio_reference(instance, section, section_number, + frame_start, frame_duration, is_sequence=False): metadata = _get_metadata(instance) - media_source = section.mediaSource() - - # get file info for path and start frame - file_info = media_source.fileinfos().pop() - frame_start = file_info.startFrame() - path = file_info.filename() + project = get_current_project_name() + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + track_name = section.get_shot_display_name() + render_dir = f"{root}/{project}/editorial_pkg/{instance.data.get('output')}" + render_dir = f"{render_dir}/{track_name}_{section_number + 1}" + render_path = Path(render_dir) + frames = [str(x) for x in render_path.iterdir() if x.is_file()] # get padding and other file infos - padding = media_source.filenamePadding() - file_head = media_source.filenameHead() - is_sequence = not media_source.singleFile() - frame_duration = media_source.duration() + file_head, padding, extension = get_sequence_for_otio(frames) fps = CTX.project_fps - extension = os.path.splitext(path)[-1] if is_sequence: metadata.update({ @@ -78,9 +78,8 @@ def create_otio_reference(instance, section): # if it is file sequence try to create `ImageSequenceReference` # the OTIO might not be compatible so return nothing and do it old way try: - dirname = os.path.dirname(path) otio_ex_ref_item = otio.schema.ImageSequenceReference( - target_url_base=dirname + os.sep, + target_url_base=render_dir + os.sep, name_prefix=file_head, name_suffix=extension, start_frame=frame_start, @@ -96,7 +95,7 @@ def create_otio_reference(instance, section): pass if not otio_ex_ref_item: - section_filepath = "something.mp4" + section_filepath = f"{render_dir}/{file_head}.mp4" # in case old OTIO or video file create `ExternalReference` otio_ex_ref_item = otio.schema.ExternalReference( target_url=section_filepath, @@ -108,13 +107,13 @@ def create_otio_reference(instance, section): ) # add metadata to otio item - add_otio_metadata(otio_ex_ref_item, media_source, **metadata) + add_otio_metadata(otio_ex_ref_item, **metadata) return otio_ex_ref_item def create_otio_clip(instance, target_track): - for section in target_track.get_sections(): + for section_number, section in enumerate(target_track.get_sections()): # flip if speed is in minus shot_start = section.get_start_frame() duration = int(section.get_end_frame() - section.get_start_frame()) + 1 @@ -122,7 +121,7 @@ def create_otio_clip(instance, target_track): fps = CTX.project_fps name = section.get_shot_display_name() - media_reference = create_otio_reference(instance, section) + media_reference = create_otio_reference(instance, section, section_number, shot_start, duration) source_range = create_otio_time_range( int(shot_start), int(duration), @@ -195,9 +194,8 @@ def add_otio_gap(track_section, otio_track, prev_out): otio_track.append(otio_gap) -def add_otio_metadata(otio_item, media_source, **kwargs): - metadata = _get_metadata(media_source) - +def add_otio_metadata(otio_item, **kwargs): + metadata = {} # add additional metadata from kwargs if kwargs: metadata.update(kwargs) From beb46a3eb3ff61fd36545fc1f6fb6c0496dd7c4b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Nov 2024 18:25:22 +0800 Subject: [PATCH 25/36] impelement the extractor for intermediate render --- client/ayon_unreal/api/rendering.py | 46 +++----- client/ayon_unreal/otio/unreal_export.py | 25 ++--- .../publish/collect_intermediate_render.py | 102 ------------------ .../extract_intermediate_representation.py | 65 +++++++++++ 4 files changed, 87 insertions(+), 151 deletions(-) delete mode 100644 client/ayon_unreal/plugins/publish/collect_intermediate_render.py create mode 100644 client/ayon_unreal/plugins/publish/extract_intermediate_representation.py diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index d09c02f1..43a234b9 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -177,36 +177,22 @@ 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. - if i["productType"] == "render": - for seq in sequences: - subscenes = pipeline.get_subsequences(seq.get('sequence')) - - if subscenes: - for sub_seq in subscenes: - sequences.append({ - "sequence": sub_seq.get_sequence(), - "output": (f"{seq.get('output')}/" - f"{sub_seq.get_sequence().get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), sub_seq.get_end_frame()) - }) - else: - # Avoid rendering camera sequences - if "_camera" not in seq.get('sequence').get_name(): - render_list.append(seq) - - elif i["productType"] == "editorial_pkg": - members = ast.literal_eval(i["members"]) - for e, track in enumerate(get_shot_tracks(members)): - track_name = track.get_shot_display_name() - track_section = [{ - "sequence": track, - "output": f"{i['output']}/{track_name}_{e + 1}", - "frame_range": ( - int(track.get_start_frame()), - int(track.get_end_frame())) - }] - render_list.extend(track_section) + for seq in sequences: + subscenes = pipeline.get_subsequences(seq.get('sequence')) + + if subscenes: + for sub_seq in subscenes: + sequences.append({ + "sequence": sub_seq.get_sequence(), + "output": (f"{seq.get('output')}/" + f"{sub_seq.get_sequence().get_name()}"), + "frame_range": ( + sub_seq.get_start_frame(), sub_seq.get_end_frame()) + }) + else: + # Avoid rendering camera sequences + if "_camera" not in seq.get('sequence').get_name(): + render_list.append(seq) if i["master_level"] != current_level_name: unreal.log_warning( diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 6ed2e19f..9ae02ad4 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -20,7 +20,6 @@ class CTX: project_fps = None timeline = None include_tags = True - instance = None def create_otio_rational_time(frame, fps): @@ -37,15 +36,9 @@ def create_otio_time_range(start_frame, frame_duration, fps): ) -def _get_metadata(item): - if hasattr(item, 'metadata'): - return {key: value for key, value in dict(item.metadata()).items()} - return {} - - def create_otio_reference(instance, section, section_number, frame_start, frame_duration, is_sequence=False): - metadata = _get_metadata(instance) + metadata = {} project = get_current_project_name() anatomy = Anatomy(project) @@ -107,7 +100,7 @@ def create_otio_reference(instance, section, section_number, ) # add metadata to otio item - add_otio_metadata(otio_ex_ref_item, **metadata) + add_otio_metadata(otio_ex_ref_item, metadata) return otio_ex_ref_item @@ -152,15 +145,14 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def _create_otio_timeline(instance): - metadata = _get_metadata(instance) +def _create_otio_timeline(instance) resolution = get_screen_resolution() - metadata.update({ + metadata = { "ayon.timeline.width": int(resolution.x), "ayon.timeline.height": int(resolution.y), # "ayon.project.ocioConfigName": unreal.OpenColorIOConfiguration().get_name(), # "ayon.project.ocioConfigPath": unreal.OpenColorIOConfiguration().configuration_file - }) + } start_time = create_otio_rational_time( CTX.timeline.timecodeStart(), CTX.project_fps) @@ -194,12 +186,7 @@ def add_otio_gap(track_section, otio_track, prev_out): otio_track.append(otio_gap) -def add_otio_metadata(otio_item, **kwargs): - metadata = {} - # add additional metadata from kwargs - if kwargs: - metadata.update(kwargs) - +def add_otio_metadata(otio_item, metadata): # add metadata to otio item metadata for key, value in metadata.items(): otio_item.metadata.update({key: value}) diff --git a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py b/client/ayon_unreal/plugins/publish/collect_intermediate_render.py deleted file mode 100644 index db724da1..00000000 --- a/client/ayon_unreal/plugins/publish/collect_intermediate_render.py +++ /dev/null @@ -1,102 +0,0 @@ -from pathlib import Path - -import unreal -import os -from ayon_core.pipeline import get_current_project_name, Anatomy -from ayon_core.pipeline.publish import PublishError -from ayon_unreal.api import pipeline -from ayon_unreal.api.lib import get_shot_tracks -import pyblish.api - - -class CollectIntermediateRender(pyblish.api.InstancePlugin): - """ This collector will try to find - all the rendered frames for intermediate rendering - - Secondary step after local rendering. Should collect all rendered files and - add them as representation. - """ - order = pyblish.api.CollectorOrder + 0.001 - hosts = ["unreal"] - families = ["editorial_pkg"] - label = "Collect Intermediate Render" - - def process(self, instance): - self.log.debug("Collecting rendered files") - context = instance.context - - data = instance.data - data['remove'] = True - ar = unreal.AssetRegistryHelpers.get_asset_registry() - sequence = ar.get_asset_by_object_path( - data.get('sequence')).get_asset() - members = instance.data["members"] - for i, track in enumerate(get_shot_tracks(members)): - track_name = track.get_shot_display_name() - - product_type = data["productType"] - new_product_name = ( - f"{data.get('productName')}_{track_name}_{i + 1}" - ) - new_instance = context.create_instance( - new_product_name - ) - new_instance[:] = track_name - - new_data = new_instance.data - - new_data["folderPath"] = instance.data["folderPath"] - new_data["setMembers"] = track_name - new_data["productName"] = new_product_name - new_data["productType"] = product_type - new_data["family"] = product_type - new_data["families"] = [product_type, "review"] - new_data["parent"] = data.get("parent") - new_data["level"] = data.get("level") - new_data["output"] = data['output'] - new_data["fps"] = sequence.get_display_rate().numerator - new_data["frameStart"] = int(track.get_start_frame()) - new_data["frameEnd"] = int(track.get_end_frame()) - new_data["sequence"] = track_name - new_data["master_sequence"] = data["master_sequence"] - new_data["master_level"] = data["master_level"] - - self.log.debug(f"new instance data: {new_data}") - - try: - project = get_current_project_name() - anatomy = Anatomy(project) - root = anatomy.roots['renders'] - except Exception as e: - raise Exception(( - "Could not find render root " - "in anatomy settings.")) from e - render_dir = f"{root}/{project}/editorial_pkg/{data.get('output')}" - render_dir = f"{render_dir}/{track_name}_{i + 1}" - render_path = Path(render_dir) - if not os.path.exists(render_path): - msg = ( - f"Render directory {render_path} not found." - " Please render with the render instance" - ) - self.log.error(msg) - raise PublishError(msg, title="Render directory not found.") - self.log.debug(f"Collecting render path: {render_path}") - frames = [str(x) for x in render_path.iterdir() if x.is_file()] - frames = pipeline.get_sequence(frames) - image_format = next((os.path.splitext(x)[-1].lstrip(".") - for x in frames), "exr") - - if "representations" not in new_instance.data: - new_instance.data["representations"] = [] - - repr = { - 'frameStart': int(track.get_start_frame()), - 'frameEnd': int(track.get_end_frame()), - 'name': "intermediate", - 'ext': image_format, - 'files': frames, - 'stagingDir': render_dir, - 'tags': ['review'] - } - new_instance.data["representations"].append(repr) diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py new file mode 100644 index 00000000..2f1e619b --- /dev/null +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -0,0 +1,65 @@ +from pathlib import Path + +import unreal +import os +from ayon_core.pipeline import get_current_project_name, Anatomy +from ayon_core.pipeline import publish +from ayon_core.pipeline.publish import PublishError +from ayon_unreal.api import pipeline +import pyblish.api + + +class ExtractIntermediateRepresentation(publish.Extractor): + """ This extractor will try to find + all the rendered frames, converting them into the mp4 file and publish it. + """ + + hosts = ["unreal"] + families = ["editorial_pkg"] + label = "Extract Intermediate Representation" + + def process(self, instance): + self.log.debug("Collecting rendered files") + + data = instance.data + data['remove'] = True + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path( + data.get('sequence')).get_asset() + + try: + project = get_current_project_name() + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + except Exception as e: + raise Exception(( + "Could not find render root " + "in anatomy settings.")) from e + render_dir = f"{root}/{project}/editorial_pkg/{data.get('output')}" + render_path = Path(render_dir) + if not os.path.exists(render_path): + msg = ( + f"Render directory {render_path} not found." + " Please render with the render instance" + ) + self.log.error(msg) + raise PublishError(msg, title="Render directory not found.") + self.log.debug(f"Collecting render path: {render_path}") + frames = [str(x) for x in render_path.iterdir() if x.is_file()] + frames = pipeline.get_sequence(frames) + image_format = next((os.path.splitext(x)[-1].lstrip(".") + for x in frames), "exr") + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'frameStart': int(sequence.get_playback_start()), + 'frameEnd': int(sequence.get_playback_end()), + 'name': "intermediate", + 'ext': image_format, + 'files': frames, + 'stagingDir': render_dir, + 'tags': ['review', 'remove'] + } + instance.data["representations"].append(representation) From fd080238bab90eb335bde466f79be41d805fff15 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 12:19:44 +0800 Subject: [PATCH 26/36] implement pre-launch hook for otio installation --- client/ayon_unreal/hooks/pre_otio_install.py | 238 ++++++++++++++++++ .../plugins/publish/collect_version.py | 39 +++ .../extract_intermediate_representation.py | 5 +- 3 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 client/ayon_unreal/hooks/pre_otio_install.py create mode 100644 client/ayon_unreal/plugins/publish/collect_version.py diff --git a/client/ayon_unreal/hooks/pre_otio_install.py b/client/ayon_unreal/hooks/pre_otio_install.py new file mode 100644 index 00000000..1e9b7699 --- /dev/null +++ b/client/ayon_unreal/hooks/pre_otio_install.py @@ -0,0 +1,238 @@ +import os +import subprocess +from platform import system +from ayon_applications import PreLaunchHook, LaunchTypes + + +class InstallOtioToBlender(PreLaunchHook): + """Install Qt binding to Unreal's python packages. + + Prelaunch hook does 2 things: + 1.) Unreal's python packages are pushed to the beginning of PYTHONPATH. + 2.) Check if Unreal has installed otio and will try to install if not. + + For pipeline implementation is required to have Qt binding installed in + Unreal's python packages. + """ + + app_groups = {"unreal"} + launch_types = {LaunchTypes.local} + + def execute(self): + # Prelaunch hook is not crucial + try: + self.inner_execute() + except Exception: + self.log.warning( + "Processing of {} crashed.".format(self.__class__.__name__), + exc_info=True + ) + + def inner_execute(self): + platform = system().lower() + executable = self.launch_context.executable.executable_path + expected_executable = "UnrealEditor" + if platform == "windows": + expected_executable += ".exe" + + if os.path.basename(executable) != expected_executable: + self.log.info(( + f"Executable does not lead to {expected_executable} file." + "Can't determine Unreal's python to check/install" + " otio binding." + )) + return + + versions_dir = self.find_parent_directory(executable) + otio_binding = "opentimelineio" + otio_binding_version = None + + python_dir = os.path.join(versions_dir, "ThirdParty", "Python3", "Win64") + print(python_dir) + python_version = "python" + + if platform == "windows": + python_executable = os.path.join(python_dir, "python.exe") + else: + python_executable = os.path.join(python_dir, python_version) + # Check for python with enabled 'pymalloc' + if not os.path.exists(python_executable): + python_executable += "m" + + if not os.path.exists(python_executable): + self.log.warning( + "Couldn't find python executable for Unreal. {}".format( + executable + ) + ) + return + + # Check if otio is installed and skip if yes + if self.is_otio_installed(python_executable, otio_binding): + self.log.debug("Unreal has already installed otio.") + return + + # Install otio in Unreal's python + if platform == "windows": + result = self.install_otio_windows( + python_executable, + otio_binding, + otio_binding_version + ) + else: + result = self.install_otio( + python_executable, + otio_binding, + otio_binding_version + ) + + if result: + self.log.info( + f"Successfully installed {otio_binding} module to Unreal." + ) + else: + self.log.warning( + f"Failed to install {otio_binding} module to Unreal." + ) + + def install_otio_windows( + self, + python_executable, + otio_binding, + otio_binding_version + ): + """Install otio python module to Unreal's python. + + Installation requires administration rights that's why it is required + to use "pywin32" module which can execute command's and ask for + administration rights. + """ + try: + import win32con + import win32process + import win32event + import pywintypes + from win32comext.shell.shell import ShellExecuteEx + from win32comext.shell import shellcon + except Exception: + self.log.warning("Couldn't import \"pywin32\" modules") + return + + if otio_binding_version: + otio_binding = f"{otio_binding}=={otio_binding_version}" + + try: + # Parameters + # - use "-m pip" as module pip to install otio and argument + # "--ignore-installed" is to force install module to Unreal's + # site-packages and make sure it is binary compatible + fake_exe = "fake.exe" + args = [ + fake_exe, + "-m", + "pip", + "install", + "--ignore-installed", + otio_binding, + ] + + parameters = ( + subprocess.list2cmdline(args) + .lstrip(fake_exe) + .lstrip(" ") + ) + + # Execute command and ask for administrator's rights + process_info = ShellExecuteEx( + nShow=win32con.SW_SHOWNORMAL, + fMask=shellcon.SEE_MASK_NOCLOSEPROCESS, + lpVerb="runas", + lpFile=python_executable, + lpParameters=parameters, + lpDirectory=os.path.dirname(python_executable) + ) + process_handle = process_info["hProcess"] + win32event.WaitForSingleObject(process_handle, win32event.INFINITE) + returncode = win32process.GetExitCodeProcess(process_handle) + return returncode == 0 + except pywintypes.error: + pass + + def install_otio( + self, + python_executable, + otio_binding, + otio_binding_version, + ): + """Install Qt binding python module to Unreal's python.""" + if otio_binding_version: + otio_binding = f"{otio_binding}=={otio_binding_version}" + try: + # Parameters + # - use "-m pip" as module pip to install qt binding and argument + # "--ignore-installed" is to force install module to Unreal's + # site-packages and make sure it is binary compatible + # TODO find out if Unreal 4.x on linux/darwin does install + # qt binding to correct place. + args = [ + python_executable, + "-m", + "pip", + "install", + "--ignore-installed", + otio_binding, + ] + process = subprocess.Popen( + args, stdout=subprocess.PIPE, universal_newlines=True + ) + process.communicate() + return process.returncode == 0 + except PermissionError: + self.log.warning( + "Permission denied with command:" + "\"{}\".".format(" ".join(args)) + ) + except OSError as error: + self.log.warning(f"OS error has occurred: \"{error}\".") + except subprocess.SubprocessError: + pass + + def is_otio_installed(self, python_executable, otio_binding): + """Check if OTIO module is in Unreal's pip list. + + Check that otio is installed directly in Unreal's site-packages. + It is possible that it is installed in user's site-packages but that + may be incompatible with Unreal's python. + """ + + otio_binding_low = otio_binding.lower() + # Get pip list from Unreal's python executable + args = [python_executable, "-m", "pip", "list"] + process = subprocess.Popen(args, stdout=subprocess.PIPE) + stdout, _ = process.communicate() + lines = stdout.decode().split(os.linesep) + # Second line contain dashes that define maximum length of module name. + # Second column of dashes define maximum length of module version. + package_dashes, *_ = lines[1].split(" ") + package_len = len(package_dashes) + + # Got through printed lines starting at line 3 + for idx in range(2, len(lines)): + line = lines[idx] + if not line: + continue + package_name = line[0:package_len].strip() + if package_name.lower() == otio_binding_low: + return True + return False + + def find_parent_directory(self, file_path, target_dir="Binaries"): + # Split the path into components + path_components = file_path.split(os.sep) + + # Traverse the path components to find the target directory + for i in range(len(path_components) - 1, -1, -1): + if path_components[i] == target_dir: + # Join the components to form the target directory path + return os.sep.join(path_components[:i + 1]) + return None diff --git a/client/ayon_unreal/plugins/publish/collect_version.py b/client/ayon_unreal/plugins/publish/collect_version.py new file mode 100644 index 00000000..4fa23fe5 --- /dev/null +++ b/client/ayon_unreal/plugins/publish/collect_version.py @@ -0,0 +1,39 @@ +import ayon_api + +import pyblish.api + + +class CollectVersion(pyblish.api.InstancePlugin): + """ + Collect version for editorial package publish + """ + + order = pyblish.api.CollectorOrder - 0.49 + hosts = ["unreal"] + families = ["editorial_pkg"] + label = "Collect Version" + + def process(self, instance): + project_name = instance.context.data["projectName"] + version = instance.data.get("version") + if version is not None: + # get version from publish data and rise it one up + version += 1 + + # make sure last version of product is higher than current + # expected current version from publish data + folder_entity = ayon_api.get_folder_by_path( + project_name=project_name, + folder_path=instance.data["folderPath"], + ) + last_version = ayon_api.get_last_version_by_product_name( + project_name=project_name, + product_name=instance.data["productName"], + folder_id=folder_entity["id"], + ) + if last_version is not None: + last_version = int(last_version["version"]) + if version <= last_version: + version = last_version + 1 + + instance.data["version"] = version diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py index 2f1e619b..c1d7c83a 100644 --- a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -1,12 +1,12 @@ from pathlib import Path +import pyblish.api import unreal import os from ayon_core.pipeline import get_current_project_name, Anatomy from ayon_core.pipeline import publish from ayon_core.pipeline.publish import PublishError from ayon_unreal.api import pipeline -import pyblish.api class ExtractIntermediateRepresentation(publish.Extractor): @@ -15,14 +15,13 @@ class ExtractIntermediateRepresentation(publish.Extractor): """ hosts = ["unreal"] + order = pyblish.api.ExtractorOrder - 0.45 families = ["editorial_pkg"] label = "Extract Intermediate Representation" def process(self, instance): self.log.debug("Collecting rendered files") - data = instance.data - data['remove'] = True ar = unreal.AssetRegistryHelpers.get_asset_registry() sequence = ar.get_asset_by_object_path( data.get('sequence')).get_asset() From adf1d84452147f1f51bfb9147811936003def51b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 15:39:18 +0800 Subject: [PATCH 27/36] implement extract edidtorial package.py --- client/ayon_unreal/api/pipeline.py | 4 +- client/ayon_unreal/otio/unreal_export.py | 38 ++++- .../publish/extract_editorial_package.py | 149 ++++++++++++++++++ 3 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 client/ayon_unreal/plugins/publish/extract_editorial_package.py diff --git a/client/ayon_unreal/api/pipeline.py b/client/ayon_unreal/api/pipeline.py index c64437d5..66232f3b 100644 --- a/client/ayon_unreal/api/pipeline.py +++ b/client/ayon_unreal/api/pipeline.py @@ -980,10 +980,8 @@ def get_sequence_for_otio(files): raise ValueError( f"Multiple collections found for {collections}. " "This is a bug.") - filename_head = collections[0].head filename_padding = collections[0].padding - filename_tail = collections[0].tail - return filename_head, filename_padding, filename_tail + return filename_padding def find_camera_actors_in_camera_tracks(sequence) -> list[Any]: diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 9ae02ad4..2c8ad5a2 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -49,7 +49,14 @@ def create_otio_reference(instance, section, section_number, render_path = Path(render_dir) frames = [str(x) for x in render_path.iterdir() if x.is_file()] # get padding and other file infos - file_head, padding, extension = get_sequence_for_otio(frames) + padding = get_sequence_for_otio(frames) + published_file_path = None + for repre in instance.data["representations"]: + if repre["name"] == "intermediate": + published_file_path = _get_published_path(instance, repre) + break + published_dir = os.path.dirname(published_file_path) + file_head, extension = os.path.splitext(os.path.basename(published_file_path)) fps = CTX.project_fps if is_sequence: @@ -72,7 +79,7 @@ def create_otio_reference(instance, section, section_number, # the OTIO might not be compatible so return nothing and do it old way try: otio_ex_ref_item = otio.schema.ImageSequenceReference( - target_url_base=render_dir + os.sep, + target_url_base=published_dir + os.sep, name_prefix=file_head, name_suffix=extension, start_frame=frame_start, @@ -88,7 +95,7 @@ def create_otio_reference(instance, section, section_number, pass if not otio_ex_ref_item: - section_filepath = f"{render_dir}/{file_head}.mp4" + section_filepath = f"{published_dir}/{file_head}.mp4" # in case old OTIO or video file create `ExternalReference` otio_ex_ref_item = otio.schema.ExternalReference( target_url=section_filepath, @@ -145,7 +152,7 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def _create_otio_timeline(instance) +def _create_otio_timeline(): resolution = get_screen_resolution() metadata = { "ayon.timeline.width": int(resolution.x), @@ -155,10 +162,10 @@ def _create_otio_timeline(instance) } start_time = create_otio_rational_time( - CTX.timeline.timecodeStart(), CTX.project_fps) + CTX.timeline.get_playback_start(), CTX.project_fps) return otio.schema.Timeline( - name=CTX.timeline.name(), + name=CTX.timeline.get_name(), global_start_time=start_time, metadata=metadata ) @@ -200,7 +207,7 @@ def create_otio_timeline(instance): CTX.timeline = sequence CTX.project_fps = CTX.timeline.get_display_rate() # convert timeline to otio - otio_timeline = _create_otio_timeline(instance) + otio_timeline = _create_otio_timeline() members = instance.data["members"] # loop all defined track types for target_track in get_shot_tracks(members): @@ -221,3 +228,20 @@ def create_otio_timeline(instance): def write_to_file(otio_timeline, path): otio.adapters.write_to_file(otio_timeline, path) + + +def _get_published_path(instance, representation): + """Calculates expected `publish` folder""" + # determine published path from Anatomy. + template_data = instance.data.get("anatomyData") + + template_data["representation"] = representation["name"] + template_data["ext"] = representation["ext"] + template_data["comment"] = None + + anatomy = instance.context.data["anatomy"] + template_data["root"] = anatomy.roots + template = anatomy.get_template_item("publish", "default", "path") + template_filled = template.format_strict(template_data) + file_path = Path(template_filled) + return file_path.as_posix() \ No newline at end of file diff --git a/client/ayon_unreal/plugins/publish/extract_editorial_package.py b/client/ayon_unreal/plugins/publish/extract_editorial_package.py new file mode 100644 index 00000000..e6897a2e --- /dev/null +++ b/client/ayon_unreal/plugins/publish/extract_editorial_package.py @@ -0,0 +1,149 @@ +from pathlib import Path +import unreal +import pyblish.api +import opentimelineio as otio +from ayon_core.pipeline import publish +from ayon_unreal.otio import unreal_export + + +class ExtractEditorialPackage(publish.Extractor): + """ This extractor will try to find + all the rendered frames, converting them into the mp4 file and publish it. + """ + + hosts = ["unreal"] + families = ["editorial_pkg"] + order = pyblish.api.ExtractorOrder + 0.45 + label = "Extract Editorial Package" + + def process(self, instance): + # create representation data + if "representations" not in instance.data: + instance.data["representations"] = [] + anatomy = instance.context.data["anatomy"] + folder_path = instance.data["folderPath"] + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path( + instance.data.get('sequence')).get_asset() + timeline_name = sequence.get_name() + folder_path_name = folder_path.lstrip("/").replace("/", "_") + + staging_dir = Path(self.staging_dir(instance)) + subfolder_name = folder_path_name + "_" + timeline_name + + # new staging directory for each timeline + staging_dir = staging_dir / subfolder_name + self.log.info(f"Staging directory: {staging_dir}") + + # otio file path + otio_file_path = staging_dir / f"{subfolder_name}.otio" + + + # Find Intermediate file representation file name + published_file_path = None + for repre in instance.data["representations"]: + if repre["name"] == "intermediate": + published_file_path = self._get_published_path(instance, repre) + break + + if published_file_path is None: + raise ValueError("Intermediate representation not found") + # export otio representation + self.export_otio_representation(instance, otio_file_path) + # Finding clip references and replacing them with rootless paths + # of video files + otio_timeline = otio.adapters.read_from_file(otio_file_path.as_posix()) + for track in otio_timeline.tracks: + for clip in track: + # skip transitions + if isinstance(clip, otio.schema.Transition): + continue + # skip gaps + if isinstance(clip, otio.schema.Gap): + # get duration of gap + continue + + if hasattr(clip.media_reference, "target_url"): + path_to_media = Path(published_file_path) + # remove root from path + success, rootless_path = anatomy.find_root_template_from_path( # noqa + path_to_media.as_posix() + ) + if success: + media_source_path = rootless_path + else: + media_source_path = path_to_media.as_posix() + + new_media_reference = otio.schema.ExternalReference( + target_url=media_source_path, + available_range=otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + value=clip.range_in_parent().start_time.value, + rate=sequence.get_display_rate() + ), + duration=otio.opentime.RationalTime( + value=int(clip.range_in_parent().end_time.value-clip.range_in_parent().start_time.value) + 1, # noqa + rate=sequence.get_display_rate() + ), + ), + ) + clip.media_reference = new_media_reference + + # replace clip source range with track parent range + clip.source_range = otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + value=clip.range_in_parent().start_time.value, + rate=sequence.get_display_rate(), + ), + duration=clip.range_in_parent().duration, + ) + + # reference video representations also needs to reframe available + # frames and clip source + + # new otio file needs to be saved as new file + otio_file_path_replaced = staging_dir / f"{subfolder_name}_remap.otio" + otio.adapters.write_to_file( + otio_timeline, otio_file_path_replaced.as_posix()) + + self.log.debug( + f"OTIO file with replaced references: {otio_file_path_replaced}") + + # create drp workfile representation + representation_otio = { + "name": "editorial_pkg", + "ext": "otio", + "files": f"{subfolder_name}_remap.otio", + "stagingDir": staging_dir.as_posix(), + } + self.log.debug(f"OTIO representation: {representation_otio}") + instance.data["representations"].append(representation_otio) + + self.log.info( + "Added OTIO file representation: " + f"{otio_file_path}" + ) + + def export_otio_representation(self, instance, filepath): + otio_timeline = unreal_export.create_otio_timeline(instance) + unreal_export.write_to_file(otio_timeline, filepath.as_posix()) + + # check if file exists + if not filepath.exists(): + raise FileNotFoundError(f"OTIO file not found: {filepath}") + + def _get_published_path(self, instance, representation): + """Calculates expected `publish` folder""" + # determine published path from Anatomy. + template_data = instance.data.get("anatomyData") + + template_data["representation"] = representation["name"] + template_data["ext"] = representation["ext"] + template_data["comment"] = None + + anatomy = instance.context.data["anatomy"] + template_data["root"] = anatomy.roots + template = anatomy.get_template_item("publish", "default", "path") + template_filled = template.format_strict(template_data) + file_path = Path(template_filled) + return file_path.as_posix() From 454c94107bbd686119e4103886ccb6cba280d22c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 16:12:06 +0800 Subject: [PATCH 28/36] make sure the opentimelineio is using 0.16.0 --- client/ayon_unreal/hooks/pre_otio_install.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_unreal/hooks/pre_otio_install.py b/client/ayon_unreal/hooks/pre_otio_install.py index 1e9b7699..7c79d192 100644 --- a/client/ayon_unreal/hooks/pre_otio_install.py +++ b/client/ayon_unreal/hooks/pre_otio_install.py @@ -48,7 +48,6 @@ def inner_execute(self): otio_binding_version = None python_dir = os.path.join(versions_dir, "ThirdParty", "Python3", "Win64") - print(python_dir) python_version = "python" if platform == "windows": @@ -118,8 +117,8 @@ def install_otio_windows( self.log.warning("Couldn't import \"pywin32\" modules") return - if otio_binding_version: - otio_binding = f"{otio_binding}=={otio_binding_version}" + + otio_binding = f"{otio_binding}==0.16.0" try: # Parameters From aa662db17a06f66397603cc115e524830671807f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 16:56:37 +0800 Subject: [PATCH 29/36] edit the unreal export.py for getting correct track name --- client/ayon_unreal/api/lib.py | 7 ++--- client/ayon_unreal/api/rendering.py | 1 - client/ayon_unreal/otio/unreal_export.py | 9 ++++-- ..._version.py => collect_extract_package.py} | 29 ++++++++++++++++--- 4 files changed, 34 insertions(+), 12 deletions(-) rename client/ayon_unreal/plugins/publish/{collect_version.py => collect_extract_package.py} (53%) diff --git a/client/ayon_unreal/api/lib.py b/client/ayon_unreal/api/lib.py index 0005d803..84728b53 100644 --- a/client/ayon_unreal/api/lib.py +++ b/client/ayon_unreal/api/lib.py @@ -320,11 +320,10 @@ def get_shot_track_names(sel_objects=None, get_name=True): ] if get_name: - return [section.get_shot_display_name() for shot_tracks in - sub_sequence_tracks for section in shot_tracks.get_sections()] + return [shot_tracks.get_display_name() for shot_tracks in + sub_sequence_tracks] else: - return [section for shot_tracks in - sub_sequence_tracks for section in shot_tracks.get_sections()] + return [shot_tracks for shot_tracks in sub_sequence_tracks] def get_shot_tracks(members): diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 43a234b9..fa47e55d 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -5,7 +5,6 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy from ayon_unreal.api import pipeline -from ayon_unreal.api.lib import get_shot_tracks from ayon_core.tools.utils import show_message_dialog diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 2c8ad5a2..7b6f1b44 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -11,7 +11,8 @@ TRACK_TYPES = { - "MovieSceneSubTrack": otio.schema.TrackKind.Video, + "MovieSceneCinematicShotTrack": otio.schema.TrackKind.Video, + "MovieSceneCameraCutTrack": otio.schema.TrackKind.Video, "MovieSceneAudioTrack": otio.schema.TrackKind.Audio } @@ -205,7 +206,9 @@ def create_otio_timeline(instance): instance.data.get('sequence')).get_asset() # get current timeline CTX.timeline = sequence - CTX.project_fps = CTX.timeline.get_display_rate() + frame_rate_obj = CTX.timeline.get_display_rate() + frame_rate = frame_rate_obj.numerator / frame_rate_obj.denominator + CTX.project_fps = frame_rate # convert timeline to otio otio_timeline = _create_otio_timeline() members = instance.data["members"] @@ -214,7 +217,7 @@ def create_otio_timeline(instance): # convert track to otio otio_track = create_otio_track( target_track.get_class().get_name(), - target_track.get_display_name()) + f"{target_track.get_display_name()}") # create otio clip and add it to track otio_clip = create_otio_clip(instance, target_track) diff --git a/client/ayon_unreal/plugins/publish/collect_version.py b/client/ayon_unreal/plugins/publish/collect_extract_package.py similarity index 53% rename from client/ayon_unreal/plugins/publish/collect_version.py rename to client/ayon_unreal/plugins/publish/collect_extract_package.py index 4fa23fe5..f057c3ad 100644 --- a/client/ayon_unreal/plugins/publish/collect_version.py +++ b/client/ayon_unreal/plugins/publish/collect_extract_package.py @@ -1,17 +1,20 @@ +import os +from pathlib import Path +from ayon_core.pipeline.publish import PublishError import ayon_api - import pyblish.api +from ayon_core.pipeline import get_current_project_name, Anatomy -class CollectVersion(pyblish.api.InstancePlugin): +class CollectEditorialPackage(pyblish.api.InstancePlugin): """ - Collect version for editorial package publish + Collect neccessary data for editorial package publish """ order = pyblish.api.CollectorOrder - 0.49 hosts = ["unreal"] families = ["editorial_pkg"] - label = "Collect Version" + label = "Collect Editorial Package" def process(self, instance): project_name = instance.context.data["projectName"] @@ -37,3 +40,21 @@ def process(self, instance): version = last_version + 1 instance.data["version"] = version + + try: + project = get_current_project_name() + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + except Exception as e: + raise Exception(( + "Could not find render root " + "in anatomy settings.")) from e + render_dir = f"{root}/{project}/editorial_pkg/{instance.data.get('output')}" + render_path = Path(render_dir) + if not os.path.exists(render_path): + msg = ( + f"Render directory {render_path} not found." + " Please render with the render instance" + ) + self.log.error(msg) + raise PublishError(msg, title="Render directory not found.") From 726e1697845c694ca72584bddbac07ba27dc4c1b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 19:24:45 +0800 Subject: [PATCH 30/36] update the extract data for the representation --- client/ayon_unreal/otio/unreal_export.py | 2 ++ .../publish/extract_editorial_package.py | 29 +++++++++++++------ .../extract_intermediate_representation.py | 13 +++++++-- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index 7b6f1b44..a2789289 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -230,6 +230,8 @@ def create_otio_timeline(instance): def write_to_file(otio_timeline, path): + directory = os.path.dirname(path) + os.makedirs(directory, exist_ok=True) otio.adapters.write_to_file(otio_timeline, path) diff --git a/client/ayon_unreal/plugins/publish/extract_editorial_package.py b/client/ayon_unreal/plugins/publish/extract_editorial_package.py index e6897a2e..787007fd 100644 --- a/client/ayon_unreal/plugins/publish/extract_editorial_package.py +++ b/client/ayon_unreal/plugins/publish/extract_editorial_package.py @@ -50,6 +50,18 @@ def process(self, instance): raise ValueError("Intermediate representation not found") # export otio representation self.export_otio_representation(instance, otio_file_path) + frame_rate_obj = sequence.get_display_rate() + frame_rate = frame_rate_obj.numerator / frame_rate_obj.denominator + timeline_start_frame = sequence.get_playback_start() + timeline_end_frame = sequence.get_playback_end() + timeline_duration = timeline_end_frame - timeline_start_frame + 1 + self.log.info( + f"Timeline: {sequence.get_name()}, " + f"Start: {timeline_start_frame}, " + f"End: {timeline_end_frame}, " + f"Duration: {timeline_duration}, " + f"FPS: {frame_rate}" + ) # Finding clip references and replacing them with rootless paths # of video files otio_timeline = otio.adapters.read_from_file(otio_file_path.as_posix()) @@ -73,17 +85,14 @@ def process(self, instance): media_source_path = rootless_path else: media_source_path = path_to_media.as_posix() - new_media_reference = otio.schema.ExternalReference( target_url=media_source_path, available_range=otio.opentime.TimeRange( start_time=otio.opentime.RationalTime( - value=clip.range_in_parent().start_time.value, - rate=sequence.get_display_rate() + value=timeline_start_frame, rate=frame_rate ), duration=otio.opentime.RationalTime( - value=int(clip.range_in_parent().end_time.value-clip.range_in_parent().start_time.value) + 1, # noqa - rate=sequence.get_display_rate() + value=timeline_duration, rate=frame_rate ), ), ) @@ -92,12 +101,14 @@ def process(self, instance): # replace clip source range with track parent range clip.source_range = otio.opentime.TimeRange( start_time=otio.opentime.RationalTime( - value=clip.range_in_parent().start_time.value, - rate=sequence.get_display_rate(), + value=( + timeline_start_frame + + clip.range_in_parent().start_time.value + ), + rate=frame_rate, ), duration=clip.range_in_parent().duration, ) - # reference video representations also needs to reframe available # frames and clip source @@ -138,7 +149,7 @@ def _get_published_path(self, instance, representation): template_data = instance.data.get("anatomyData") template_data["representation"] = representation["name"] - template_data["ext"] = representation["ext"] + template_data["ext"] = "mp4" template_data["comment"] = None anatomy = instance.context.data["anatomy"] diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py index c1d7c83a..19adc7aa 100644 --- a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -52,13 +52,20 @@ def process(self, instance): if "representations" not in instance.data: instance.data["representations"] = [] + instance.data["families"].append("review") + + frame_rate_obj = sequence.get_display_rate() + frame_rate = frame_rate_obj.numerator / frame_rate_obj.denominator + instance.data["frameStart"] = int(sequence.get_playback_start()) + instance.data["frameEnd"] = int(sequence.get_playback_end()) + instance.data["fps"] = frame_rate representation = { - 'frameStart': int(sequence.get_playback_start()), - 'frameEnd': int(sequence.get_playback_end()), + 'frameStart': instance.data["frameStart"], + 'frameEnd': instance.data["frameEnd"], 'name': "intermediate", 'ext': image_format, 'files': frames, 'stagingDir': render_dir, - 'tags': ['review', 'remove'] + 'tags': ['review'] } instance.data["representations"].append(representation) From 8e5a72660cae61e6c4004f74a0629c07892927b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 19:26:27 +0800 Subject: [PATCH 31/36] add remove tags --- client/ayon_unreal/plugins/publish/collect_render_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_unreal/plugins/publish/collect_render_instances.py b/client/ayon_unreal/plugins/publish/collect_render_instances.py index fdfdb89e..bf63afee 100644 --- a/client/ayon_unreal/plugins/publish/collect_render_instances.py +++ b/client/ayon_unreal/plugins/publish/collect_render_instances.py @@ -119,6 +119,6 @@ def process(self, instance): 'ext': image_format, 'files': frames, 'stagingDir': render_dir, - 'tags': ['review'] + 'tags': ['review', 'remove'] } new_instance.data["representations"].append(repr) From 90a08c928705b45ba36e7e4f552a954ab7d48555 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 21:15:28 +0800 Subject: [PATCH 32/36] add remove tags --- .../plugins/publish/collect_extract_package.py | 12 ++++++++++++ .../plugins/publish/extract_editorial_package.py | 7 +++---- .../publish/extract_intermediate_representation.py | 7 +------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/client/ayon_unreal/plugins/publish/collect_extract_package.py b/client/ayon_unreal/plugins/publish/collect_extract_package.py index f057c3ad..edc9a792 100644 --- a/client/ayon_unreal/plugins/publish/collect_extract_package.py +++ b/client/ayon_unreal/plugins/publish/collect_extract_package.py @@ -1,8 +1,11 @@ import os from pathlib import Path from ayon_core.pipeline.publish import PublishError + import ayon_api import pyblish.api +import unreal + from ayon_core.pipeline import get_current_project_name, Anatomy @@ -41,6 +44,15 @@ def process(self, instance): instance.data["version"] = version + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path( + instance.data.get('sequence')).get_asset() + instance.data["frameStart"] = int(sequence.get_playback_start()) + instance.data["frameEnd"] = int(sequence.get_playback_end()) + frame_rate_obj = sequence.get_display_rate() + frame_rate = frame_rate_obj.numerator / frame_rate_obj.denominator + instance.data["fps"] = frame_rate + try: project = get_current_project_name() anatomy = Anatomy(project) diff --git a/client/ayon_unreal/plugins/publish/extract_editorial_package.py b/client/ayon_unreal/plugins/publish/extract_editorial_package.py index 787007fd..bf44f9ab 100644 --- a/client/ayon_unreal/plugins/publish/extract_editorial_package.py +++ b/client/ayon_unreal/plugins/publish/extract_editorial_package.py @@ -50,10 +50,9 @@ def process(self, instance): raise ValueError("Intermediate representation not found") # export otio representation self.export_otio_representation(instance, otio_file_path) - frame_rate_obj = sequence.get_display_rate() - frame_rate = frame_rate_obj.numerator / frame_rate_obj.denominator - timeline_start_frame = sequence.get_playback_start() - timeline_end_frame = sequence.get_playback_end() + frame_rate = instance.data["fps"] + timeline_start_frame = instance.data["frameStart"] + timeline_end_frame = instance.data["frameEnd"] timeline_duration = timeline_end_frame - timeline_start_frame + 1 self.log.info( f"Timeline: {sequence.get_name()}, " diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py index 19adc7aa..6bdbe14f 100644 --- a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -54,11 +54,6 @@ def process(self, instance): instance.data["families"].append("review") - frame_rate_obj = sequence.get_display_rate() - frame_rate = frame_rate_obj.numerator / frame_rate_obj.denominator - instance.data["frameStart"] = int(sequence.get_playback_start()) - instance.data["frameEnd"] = int(sequence.get_playback_end()) - instance.data["fps"] = frame_rate representation = { 'frameStart': instance.data["frameStart"], 'frameEnd': instance.data["frameEnd"], @@ -66,6 +61,6 @@ def process(self, instance): 'ext': image_format, 'files': frames, 'stagingDir': render_dir, - 'tags': ['review'] + 'tags': ['review', 'remove'] } instance.data["representations"].append(representation) From b08b4103ad49506ac23cf8eeac5c1fd0ac7429b8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Nov 2024 21:23:34 +0800 Subject: [PATCH 33/36] ruff cosmetic fix --- client/ayon_unreal/api/plugin.py | 1 - client/ayon_unreal/api/rendering.py | 1 - client/ayon_unreal/otio/unreal_export.py | 2 +- .../plugins/publish/extract_intermediate_representation.py | 5 ----- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/client/ayon_unreal/api/plugin.py b/client/ayon_unreal/api/plugin.py index 87f2acd8..6212d1c0 100644 --- a/client/ayon_unreal/api/plugin.py +++ b/client/ayon_unreal/api/plugin.py @@ -22,7 +22,6 @@ BoolDef, UILabelDef ) -from ayon_core.pipeline.constants import AVALON_INSTANCE_ID from ayon_core.pipeline import ( AutoCreator, Creator, diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index fa47e55d..6d9913bd 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -1,5 +1,4 @@ import os -import ast import unreal from ayon_core.settings import get_project_settings diff --git a/client/ayon_unreal/otio/unreal_export.py b/client/ayon_unreal/otio/unreal_export.py index a2789289..3e4451da 100644 --- a/client/ayon_unreal/otio/unreal_export.py +++ b/client/ayon_unreal/otio/unreal_export.py @@ -249,4 +249,4 @@ def _get_published_path(instance, representation): template = anatomy.get_template_item("publish", "default", "path") template_filled = template.format_strict(template_data) file_path = Path(template_filled) - return file_path.as_posix() \ No newline at end of file + return file_path.as_posix() diff --git a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py index 6bdbe14f..b4d9b850 100644 --- a/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py +++ b/client/ayon_unreal/plugins/publish/extract_intermediate_representation.py @@ -1,7 +1,6 @@ from pathlib import Path import pyblish.api -import unreal import os from ayon_core.pipeline import get_current_project_name, Anatomy from ayon_core.pipeline import publish @@ -22,10 +21,6 @@ class ExtractIntermediateRepresentation(publish.Extractor): def process(self, instance): self.log.debug("Collecting rendered files") data = instance.data - ar = unreal.AssetRegistryHelpers.get_asset_registry() - sequence = ar.get_asset_by_object_path( - data.get('sequence')).get_asset() - try: project = get_current_project_name() anatomy = Anatomy(project) From f5a6ce1c7ae7c0a3730bea63ece61c0915c8ee11 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Nov 2024 17:49:17 +0800 Subject: [PATCH 34/36] uses get_name instead of get_sequence().get_name() --- client/ayon_unreal/api/rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 6d9913bd..e6934092 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -183,7 +183,7 @@ def start_rendering(): sequences.append({ "sequence": sub_seq.get_sequence(), "output": (f"{seq.get('output')}/" - f"{sub_seq.get_sequence().get_name()}"), + f"{sub_seq.get_name()}"), "frame_range": ( sub_seq.get_start_frame(), sub_seq.get_end_frame()) }) From ac16030bfa411f9ea533871d44c75f5987b91ad9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Nov 2024 17:53:37 +0800 Subject: [PATCH 35/36] uses get_name instead of get_sequence().get_name() --- client/ayon_unreal/api/rendering.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index e6934092..281f8a0d 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -158,7 +158,6 @@ def start_rendering(): current_level_name = current_level.get_outer().get_path_name() for i in inst_data: - unreal.log(i) if i["productType"] == "editorial_pkg": render_dir = f"{root}/{project_name}/editorial_pkg" sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() @@ -180,10 +179,13 @@ def start_rendering(): if subscenes: for sub_seq in subscenes: + sub_seq_obj = sub_seq.get_sequence() + if sub_seq_obj is None: + continue sequences.append({ - "sequence": sub_seq.get_sequence(), + "sequence": sub_seq_obj, "output": (f"{seq.get('output')}/" - f"{sub_seq.get_name()}"), + f"{sub_seq_obj.get_name()}"), "frame_range": ( sub_seq.get_start_frame(), sub_seq.get_end_frame()) }) From aa61db2e767f5d685c917d7128282f3904a1ce78 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Nov 2024 21:51:19 +0800 Subject: [PATCH 36/36] make sure the image sequences not being rendered per sub sequence during rendering --- client/ayon_unreal/api/rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_unreal/api/rendering.py b/client/ayon_unreal/api/rendering.py index 281f8a0d..c31b10bf 100644 --- a/client/ayon_unreal/api/rendering.py +++ b/client/ayon_unreal/api/rendering.py @@ -177,7 +177,7 @@ def start_rendering(): for seq in sequences: subscenes = pipeline.get_subsequences(seq.get('sequence')) - if subscenes: + if subscenes and i["productType"] != "editorial_pkg": for sub_seq in subscenes: sub_seq_obj = sub_seq.get_sequence() if sub_seq_obj is None: