diff --git a/openpype/hosts/fusion/api/plugin.py b/openpype/hosts/fusion/api/plugin.py new file mode 100644 index 0000000000..63a74fbdb5 --- /dev/null +++ b/openpype/hosts/fusion/api/plugin.py @@ -0,0 +1,221 @@ +from copy import deepcopy +import os + +from openpype.hosts.fusion.api import ( + get_current_comp, + comp_lock_and_undo_chunk, +) + +from openpype.lib import ( + BoolDef, + EnumDef, +) +from openpype.pipeline import ( + legacy_io, + Creator, + CreatedInstance +) + + +class GenericCreateSaver(Creator): + default_variants = ["Main", "Mask"] + description = "Fusion Saver to generate image sequence" + icon = "fa5.eye" + + instance_attributes = [ + "reviewable" + ] + + settings_category = "fusion" + + image_format = "exr" + + # TODO: This should be renamed together with Nuke so it is aligned + temp_rendering_path_template = ( + "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") + + def create(self, subset_name, instance_data, pre_create_data): + self.pass_pre_attributes_to_instance(instance_data, pre_create_data) + + instance = CreatedInstance( + family=self.family, + subset_name=subset_name, + data=instance_data, + creator=self, + ) + data = instance.data_to_store() + comp = get_current_comp() + with comp_lock_and_undo_chunk(comp): + args = (-32768, -32768) # Magical position numbers + saver = comp.AddTool("Saver", *args) + + self._update_tool_with_data(saver, data=data) + + # Register the CreatedInstance + self._imprint(saver, data) + + # Insert the transient data + instance.transient_data["tool"] = saver + + self._add_instance_to_context(instance) + + return instance + + def collect_instances(self): + comp = get_current_comp() + tools = comp.GetToolList(False, "Saver").values() + for tool in tools: + data = self.get_managed_tool_data(tool) + if not data: + continue + + # Add instance + created_instance = CreatedInstance.from_existing(data, self) + + # Collect transient data + created_instance.transient_data["tool"] = tool + + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + new_data = created_inst.data_to_store() + tool = created_inst.transient_data["tool"] + self._update_tool_with_data(tool, new_data) + self._imprint(tool, new_data) + + def remove_instances(self, instances): + for instance in instances: + # Remove the tool from the scene + + tool = instance.transient_data["tool"] + if tool: + tool.Delete() + + # Remove the collected CreatedInstance to remove from UI directly + self._remove_instance_from_context(instance) + + def _imprint(self, tool, data): + # Save all data in a "openpype.{key}" = value data + + # Instance id is the tool's name so we don't need to imprint as data + data.pop("instance_id", None) + + active = data.pop("active", None) + if active is not None: + # Use active value to set the passthrough state + tool.SetAttrs({"TOOLB_PassThrough": not active}) + + for key, value in data.items(): + tool.SetData(f"openpype.{key}", value) + + def _update_tool_with_data(self, tool, data): + """Update tool node name and output path based on subset data""" + if "subset" not in data: + return + + original_subset = tool.GetData("openpype.subset") + original_format = tool.GetData( + "openpype.creator_attributes.image_format" + ) + + subset = data["subset"] + if ( + original_subset != subset + or original_format != data["creator_attributes"]["image_format"] + ): + self._configure_saver_tool(data, tool, subset) + + def _configure_saver_tool(self, data, tool, subset): + formatting_data = deepcopy(data) + + # get frame padding from anatomy templates + frame_padding = self.project_anatomy.templates["frame_padding"] + + # get output format + ext = data["creator_attributes"]["image_format"] + + # Subset change detected + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) + formatting_data.update({ + "workdir": workdir, + "frame": "0" * frame_padding, + "ext": ext, + "product": { + "name": formatting_data["subset"], + "type": formatting_data["family"], + }, + }) + + # build file path to render + filepath = self.temp_rendering_path_template.format(**formatting_data) + + comp = get_current_comp() + tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) + + # Rename tool + if tool.Name != subset: + print(f"Renaming {tool.Name} -> {subset}") + tool.SetAttrs({"TOOLS_Name": subset}) + + def get_managed_tool_data(self, tool): + """Return data of the tool if it matches creator identifier""" + data = tool.GetData("openpype") + if not isinstance(data, dict): + return + + required = { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + } + for key, value in required.items(): + if key not in data or data[key] != value: + return + + # Get active state from the actual tool state + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + data["active"] = not passthrough + + # Override publisher's UUID generation because tool names are + # already unique in Fusion in a comp + data["instance_id"] = tool.Name + + return data + + def get_instance_attr_defs(self): + """Settings for publish page""" + return self.get_pre_create_attr_defs() + + def pass_pre_attributes_to_instance(self, instance_data, pre_create_data): + creator_attrs = instance_data["creator_attributes"] = {} + for pass_key in pre_create_data.keys(): + creator_attrs[pass_key] = pre_create_data[pass_key] + + def _get_render_target_enum(self): + rendering_targets = { + "local": "Local machine rendering", + "frames": "Use existing frames", + } + if "farm_rendering" in self.instance_attributes: + rendering_targets["farm"] = "Farm rendering" + + return EnumDef( + "render_target", items=rendering_targets, label="Render target" + ) + + def _get_reviewable_bool(self): + return BoolDef( + "review", + default=("reviewable" in self.instance_attributes), + label="Review", + ) + + def _get_image_format_enum(self): + image_format_options = ["exr", "tga", "tif", "png", "jpg"] + return EnumDef( + "image_format", + items=image_format_options, + default=self.image_format, + label="Output Image Format", + ) diff --git a/openpype/hosts/fusion/plugins/create/create_image_saver.py b/openpype/hosts/fusion/plugins/create/create_image_saver.py new file mode 100644 index 0000000000..490228d488 --- /dev/null +++ b/openpype/hosts/fusion/plugins/create/create_image_saver.py @@ -0,0 +1,64 @@ +from openpype.lib import NumberDef + +from openpype.hosts.fusion.api.plugin import GenericCreateSaver +from openpype.hosts.fusion.api import get_current_comp + + +class CreateImageSaver(GenericCreateSaver): + """Fusion Saver to generate single image. + + Created to explicitly separate single ('image') or + multi frame('render) outputs. + + This might be temporary creator until 'alias' functionality will be + implemented to limit creation of additional product types with similar, but + not the same workflows. + """ + identifier = "io.openpype.creators.fusion.imagesaver" + label = "Image (saver)" + name = "image" + family = "image" + description = "Fusion Saver to generate image" + + default_frame = 0 + + def get_detail_description(self): + return """Fusion Saver to generate single image. + + This creator is expected for publishing of single frame `image` product + type. + + Artist should provide frame number (integer) to specify which frame + should be published. It must be inside of global timeline frame range. + + Supports local and deadline rendering. + + Supports selection from predefined set of output file extensions: + - exr + - tga + - png + - tif + - jpg + + Created to explicitly separate single frame ('image') or + multi frame ('render') outputs. + """ + + def get_pre_create_attr_defs(self): + """Settings for create page""" + attr_defs = [ + self._get_render_target_enum(), + self._get_reviewable_bool(), + self._get_frame_int(), + self._get_image_format_enum(), + ] + return attr_defs + + def _get_frame_int(self): + return NumberDef( + "frame", + default=self.default_frame, + label="Frame", + tooltip="Set frame to be rendered, must be inside of global " + "timeline range" + ) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 5870828b41..3a8ffe890b 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,187 +1,42 @@ -from copy import deepcopy -import os +from openpype.lib import EnumDef -from openpype.hosts.fusion.api import ( - get_current_comp, - comp_lock_and_undo_chunk, -) - -from openpype.lib import ( - BoolDef, - EnumDef, -) -from openpype.pipeline import ( - legacy_io, - Creator as NewCreator, - CreatedInstance, - Anatomy, -) +from openpype.hosts.fusion.api.plugin import GenericCreateSaver -class CreateSaver(NewCreator): +class CreateSaver(GenericCreateSaver): + """Fusion Saver to generate image sequence of 'render' product type. + + Original Saver creator targeted for 'render' product type. It uses + original not to descriptive name because of values in Settings. + """ identifier = "io.openpype.creators.fusion.saver" label = "Render (saver)" name = "render" family = "render" - default_variants = ["Main", "Mask"] description = "Fusion Saver to generate image sequence" - icon = "fa5.eye" - instance_attributes = ["reviewable"] - image_format = "exr" + default_frame_range_option = "asset_db" - # TODO: This should be renamed together with Nuke so it is aligned - temp_rendering_path_template = ( - "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}" - ) + def get_detail_description(self): + return """Fusion Saver to generate image sequence. - def create(self, subset_name, instance_data, pre_create_data): - self.pass_pre_attributes_to_instance(instance_data, pre_create_data) + This creator is expected for publishing of image sequences for 'render' + product type. (But can publish even single frame 'render'.) - instance_data.update( - {"id": "pyblish.avalon.instance", "subset": subset_name} - ) + Select what should be source of render range: + - "Current asset context" - values set on Asset in DB (Ftrack) + - "From render in/out" - from node itself + - "From composition timeline" - from timeline - comp = get_current_comp() - with comp_lock_and_undo_chunk(comp): - args = (-32768, -32768) # Magical position numbers - saver = comp.AddTool("Saver", *args) + Supports local and farm rendering. - self._update_tool_with_data(saver, data=instance_data) - - # Register the CreatedInstance - instance = CreatedInstance( - family=self.family, - subset_name=subset_name, - data=instance_data, - creator=self, - ) - data = instance.data_to_store() - self._imprint(saver, data) - - # Insert the transient data - instance.transient_data["tool"] = saver - - self._add_instance_to_context(instance) - - return instance - - def collect_instances(self): - comp = get_current_comp() - tools = comp.GetToolList(False, "Saver").values() - for tool in tools: - data = self.get_managed_tool_data(tool) - if not data: - continue - - # Add instance - created_instance = CreatedInstance.from_existing(data, self) - - # Collect transient data - created_instance.transient_data["tool"] = tool - - self._add_instance_to_context(created_instance) - - def update_instances(self, update_list): - for created_inst, _changes in update_list: - new_data = created_inst.data_to_store() - tool = created_inst.transient_data["tool"] - self._update_tool_with_data(tool, new_data) - self._imprint(tool, new_data) - - def remove_instances(self, instances): - for instance in instances: - # Remove the tool from the scene - - tool = instance.transient_data["tool"] - if tool: - tool.Delete() - - # Remove the collected CreatedInstance to remove from UI directly - self._remove_instance_from_context(instance) - - def _imprint(self, tool, data): - # Save all data in a "openpype.{key}" = value data - - # Instance id is the tool's name so we don't need to imprint as data - data.pop("instance_id", None) - - active = data.pop("active", None) - if active is not None: - # Use active value to set the passthrough state - tool.SetAttrs({"TOOLB_PassThrough": not active}) - - for key, value in data.items(): - tool.SetData(f"openpype.{key}", value) - - def _update_tool_with_data(self, tool, data): - """Update tool node name and output path based on subset data""" - if "subset" not in data: - return - - original_subset = tool.GetData("openpype.subset") - original_format = tool.GetData( - "openpype.creator_attributes.image_format" - ) - - subset = data["subset"] - if ( - original_subset != subset - or original_format != data["creator_attributes"]["image_format"] - ): - self._configure_saver_tool(data, tool, subset) - - def _configure_saver_tool(self, data, tool, subset): - formatting_data = deepcopy(data) - - # get frame padding from anatomy templates - anatomy = Anatomy() - frame_padding = anatomy.templates["frame_padding"] - - # get output format - ext = data["creator_attributes"]["image_format"] - - # Subset change detected - workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - formatting_data.update( - {"workdir": workdir, "frame": "0" * frame_padding, "ext": ext} - ) - - # build file path to render - filepath = self.temp_rendering_path_template.format(**formatting_data) - - comp = get_current_comp() - tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) - - # Rename tool - if tool.Name != subset: - print(f"Renaming {tool.Name} -> {subset}") - tool.SetAttrs({"TOOLS_Name": subset}) - - def get_managed_tool_data(self, tool): - """Return data of the tool if it matches creator identifier""" - data = tool.GetData("openpype") - if not isinstance(data, dict): - return - - required = { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - } - for key, value in required.items(): - if key not in data or data[key] != value: - return - - # Get active state from the actual tool state - attrs = tool.GetAttrs() - passthrough = attrs["TOOLB_PassThrough"] - data["active"] = not passthrough - - # Override publisher's UUID generation because tool names are - # already unique in Fusion in a comp - data["instance_id"] = tool.Name - - return data + Supports selection from predefined set of output file extensions: + - exr + - tga + - png + - tif + - jpg + """ def get_pre_create_attr_defs(self): """Settings for create page""" @@ -193,29 +48,6 @@ class CreateSaver(NewCreator): ] return attr_defs - def get_instance_attr_defs(self): - """Settings for publish page""" - return self.get_pre_create_attr_defs() - - def pass_pre_attributes_to_instance(self, instance_data, pre_create_data): - creator_attrs = instance_data["creator_attributes"] = {} - for pass_key in pre_create_data.keys(): - creator_attrs[pass_key] = pre_create_data[pass_key] - - # These functions below should be moved to another file - # so it can be used by other plugins. plugin.py ? - def _get_render_target_enum(self): - rendering_targets = { - "local": "Local machine rendering", - "frames": "Use existing frames", - } - if "farm_rendering" in self.instance_attributes: - rendering_targets["farm"] = "Farm rendering" - - return EnumDef( - "render_target", items=rendering_targets, label="Render target" - ) - def _get_frame_range_enum(self): frame_range_options = { "asset_db": "Current asset context", @@ -227,42 +59,5 @@ class CreateSaver(NewCreator): "frame_range_source", items=frame_range_options, label="Frame range source", - ) - - def _get_reviewable_bool(self): - return BoolDef( - "review", - default=("reviewable" in self.instance_attributes), - label="Review", - ) - - def _get_image_format_enum(self): - image_format_options = ["exr", "tga", "tif", "png", "jpg"] - return EnumDef( - "image_format", - items=image_format_options, - default=self.image_format, - label="Output Image Format", - ) - - def apply_settings(self, project_settings): - """Method called on initialization of plugin to apply settings.""" - - # plugin settings - plugin_settings = project_settings["fusion"]["create"][ - self.__class__.__name__ - ] - - # individual attributes - self.instance_attributes = plugin_settings.get( - "instance_attributes", self.instance_attributes - ) - self.default_variants = plugin_settings.get( - "default_variants", self.default_variants - ) - self.temp_rendering_path_template = plugin_settings.get( - "temp_rendering_path_template", self.temp_rendering_path_template - ) - self.image_format = plugin_settings.get( - "image_format", self.image_format + default=self.default_frame_range_option ) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index a6628300db..f23e4d0268 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -95,7 +95,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): label = "Collect Inputs" order = pyblish.api.CollectorOrder + 0.2 hosts = ["fusion"] - families = ["render"] + families = ["render", "image"] def process(self, instance): diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 4d6da79b77..a0131248e8 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -57,6 +57,18 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_with_handle = comp_start end_with_handle = comp_end + frame = instance.data["creator_attributes"].get("frame") + # explicitly publishing only single frame + if frame is not None: + frame = int(frame) + + start = frame + end = frame + handle_start = 0 + handle_end = 0 + start_with_handle = frame + end_with_handle = frame + # Include start and end render frame in label subset = instance.data["subset"] label = ( diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index a7daa0b64c..366eaa905c 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -50,7 +50,7 @@ class CollectFusionRender( continue family = inst.data["family"] - if family != "render": + if family not in ["render", "image"]: continue task_name = context.data["task"] @@ -59,7 +59,7 @@ class CollectFusionRender( instance_families = inst.data.get("families", []) subset_name = inst.data["subset"] instance = FusionRenderInstance( - family="render", + family=family, tool=tool, workfileComp=comp, families=instance_families, diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index 0798e7c8b7..da9b6ce41f 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin): label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["fusion"] - families = ["render", "workfile"] + families = ["render", "image", "workfile"] def process(self, context): diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 6908889eb4..e268f8adec 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -17,7 +17,7 @@ class ValidateBackgroundDepth( order = pyblish.api.ValidatorOrder label = "Validate Background Depth 32 bit" hosts = ["fusion"] - families = ["render"] + families = ["render", "image"] optional = True actions = [SelectInvalidAction, publish.RepairAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py index 748047e8cf..6e6d10e09a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py +++ b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py @@ -9,7 +9,7 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Comp Saved" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] def process(self, context): diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 35c92163eb..d5c618af58 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -15,7 +15,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Create Folder Checked" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [RepairAction, SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 537e43c875..38cd578ff2 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -17,7 +17,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Filename Has Extension" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_image_frame.py b/openpype/hosts/fusion/plugins/publish/validate_image_frame.py new file mode 100644 index 0000000000..734203f31c --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validate_image_frame.py @@ -0,0 +1,27 @@ +import pyblish.api + +from openpype.pipeline import PublishValidationError + + +class ValidateImageFrame(pyblish.api.InstancePlugin): + """Validates that `image` product type contains only single frame.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Image Frame" + families = ["image"] + hosts = ["fusion"] + + def process(self, instance): + render_start = instance.data["frameStartHandle"] + render_end = instance.data["frameEndHandle"] + too_many_frames = (isinstance(instance.data["expectedFiles"], list) + and len(instance.data["expectedFiles"]) > 1) + + if render_end - render_start > 0 or too_many_frames: + desc = ("Trying to render multiple frames. 'image' product type " + "is meant for single frame. Please use 'render' creator.") + raise PublishValidationError( + title="Frame range outside of comp range", + message=desc, + description=desc + ) diff --git a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py index 06cd0ca186..edf219e752 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py @@ -7,8 +7,8 @@ class ValidateInstanceFrameRange(pyblish.api.InstancePlugin): """Validate instance frame range is within comp's global render range.""" order = pyblish.api.ValidatorOrder - label = "Validate Filename Has Extension" - families = ["render"] + label = "Validate Frame Range" + families = ["render", "image"] hosts = ["fusion"] def process(self, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index faf2102a8b..0103e990fb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -13,7 +13,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Saver Has Input" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 9004976dc5..6019bee93a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -9,7 +9,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Saver Passthrough" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py index efa7295d11..f6aba170c0 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py @@ -64,7 +64,7 @@ class ValidateSaverResolution( order = pyblish.api.ValidatorOrder label = "Validate Asset Resolution" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] optional = True actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5b6ceb2fdb..d1693ef3dc 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -11,7 +11,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Unique Subsets" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 8579442625..15b6bfc09b 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -32,6 +32,18 @@ "farm_rendering" ], "image_format": "exr" + }, + "CreateImageSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ], + "image_format": "exr" } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index fbd856b895..8669842087 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -74,7 +74,56 @@ "type": "dict", "collapsible": true, "key": "CreateSaver", - "label": "Create Saver", + "label": "Create Render Saver", + "is_group": true, + "children": [ + { + "type": "text", + "key": "temp_rendering_path_template", + "label": "Temporary rendering path template" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "key": "instance_attributes", + "label": "Instance attributes", + "type": "enum", + "multiselection": true, + "enum_items": [ + { + "reviewable": "Reviewable" + }, + { + "farm_rendering": "Farm rendering" + } + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselect": false, + "enum_items": [ + {"exr": "exr"}, + {"tga": "tga"}, + {"png": "png"}, + {"tif": "tif"}, + {"jpg": "jpg"} + ] + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CreateImageSaver", + "label": "Create Image Saver", "is_group": true, "children": [ { diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index 21189b390e..bf295f3064 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -25,6 +25,24 @@ def _create_saver_instance_attributes_enum(): ] +def _image_format_enum(): + return [ + {"value": "exr", "label": "exr"}, + {"value": "tga", "label": "tga"}, + {"value": "png", "label": "png"}, + {"value": "tif", "label": "tif"}, + {"value": "jpg", "label": "jpg"}, + ] + + +def _frame_range_options_enum(): + return [ + {"value": "asset_db", "label": "Current asset context"}, + {"value": "render_range", "label": "From render in/out"}, + {"value": "comp_range", "label": "From composition timeline"}, + ] + + class CreateSaverPluginModel(BaseSettingsModel): _isGroup = True temp_rendering_path_template: str = Field( @@ -59,10 +77,29 @@ class HooksModel(BaseSettingsModel): ) +class CreateSaverModel(CreateSaverPluginModel): + default_frame_range_option: str = Field( + default="asset_db", + enum_resolver=_frame_range_options_enum, + title="Default frame range source" + ) + + +class CreateImageSaverModel(CreateSaverPluginModel): + default_frame: int = Field( + 0, + title="Default rendered frame" + ) class CreatPluginsModel(BaseSettingsModel): - CreateSaver: CreateSaverPluginModel = Field( - default_factory=CreateSaverPluginModel, - title="Create Saver" + CreateSaver: CreateSaverModel = Field( + default_factory=CreateSaverModel, + title="Create Saver", + description="Creator for render product type (eg. sequence)" + ) + CreateImageSaver: CreateImageSaverModel = Field( + default_factory=CreateImageSaverModel, + title="Create Image Saver", + description="Creator for image product type (eg. single)" ) @@ -117,15 +154,21 @@ DEFAULT_VALUES = { "reviewable", "farm_rendering" ], - "output_formats": [ - "exr", - "jpg", - "jpeg", - "jpg", - "tiff", - "png", - "tga" - ] + "image_format": "exr", + "default_frame_range_option": "asset_db" + }, + "CreateImageSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{product[name]}/{product[name]}.{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ], + "image_format": "exr", + "default_frame": 0 } } } diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/fusion/server/version.py +++ b/server_addon/fusion/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3"