diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py index 03ec184524..85a42830a4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -1,6 +1,6 @@ import json import pyblish.api -from openpype.hosts.aftereffects.api import list_instances +from openpype.hosts.aftereffects.api import AfterEffectsHost class PreCollectRender(pyblish.api.ContextPlugin): @@ -25,7 +25,7 @@ class PreCollectRender(pyblish.api.ContextPlugin): self.log.debug("Not applicable for New Publisher, skip") return - for inst in list_instances(): + for inst in AfterEffectsHost().list_instances(): if inst.get("creator_attributes"): raise ValueError("Instance created in New publisher, " "cannot be published in Pyblish.\n" diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 2433364f85..b99a7a9548 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -1,10 +1,12 @@ import os +import shutil import pyblish.api import clique import nuke from openpype.pipeline import publish +from openpype.lib import collect_frames class NukeRenderLocal(publish.ExtractorColormanaged): @@ -13,6 +15,8 @@ class NukeRenderLocal(publish.ExtractorColormanaged): Extract the result of savers by starting a comp render This will run the local render of Fusion. + Allows to use last published frames and overwrite only specific ones + (set in instance.data.get("frames_to_fix")) """ order = pyblish.api.ExtractorOrder @@ -21,7 +25,6 @@ class NukeRenderLocal(publish.ExtractorColormanaged): families = ["render.local", "prerender.local", "still.local"] def process(self, instance): - families = instance.data["families"] child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance @@ -32,17 +35,16 @@ class NukeRenderLocal(publish.ExtractorColormanaged): if x.Class() == "Write": node = x + self.log.debug("instance collected: {}".format(instance.data)) + + node_subset_name = instance.data.get("name", None) + first_frame = instance.data.get("frameStartHandle", None) - last_frame = instance.data.get("frameEndHandle", None) - node_subset_name = instance.data["subset"] - - self.log.info("Starting render") - self.log.info("Start frame: {}".format(first_frame)) - self.log.info("End frame: {}".format(last_frame)) + filenames = [] node_file = node["file"] - # Collecte expected filepaths for each frame + # Collect expected filepaths for each frame # - for cases that output is still image is first created set of # paths which is then sorted and converted to list expected_paths = list(sorted({ @@ -50,22 +52,37 @@ class NukeRenderLocal(publish.ExtractorColormanaged): for frame in range(first_frame, last_frame + 1) })) # Extract only filenames for representation - filenames = [ + filenames.extend([ os.path.basename(filepath) for filepath in expected_paths - ] + ]) # Ensure output directory exists. out_dir = os.path.dirname(expected_paths[0]) if not os.path.exists(out_dir): os.makedirs(out_dir) - # Render frames - nuke.execute( - str(node_subset_name), - int(first_frame), - int(last_frame) - ) + frames_to_render = [(first_frame, last_frame)] + + frames_to_fix = instance.data.get("frames_to_fix") + if instance.data.get("last_version_published_files") and frames_to_fix: + frames_to_render = self._get_frames_to_render(frames_to_fix) + anatomy = instance.context.data["anatomy"] + self._copy_last_published(anatomy, instance, out_dir, + filenames) + + for render_first_frame, render_last_frame in frames_to_render: + + self.log.info("Starting render") + self.log.info("Start frame: {}".format(render_first_frame)) + self.log.info("End frame: {}".format(render_last_frame)) + + # Render frames + nuke.execute( + str(node_subset_name), + int(render_first_frame), + int(render_last_frame) + ) ext = node["file_type"].value() colorspace = node["colorspace"].value() @@ -106,6 +123,7 @@ class NukeRenderLocal(publish.ExtractorColormanaged): out_dir )) + families = instance.data["families"] # redefinition of families if "render.local" in families: instance.data['family'] = 'render' @@ -133,3 +151,58 @@ class NukeRenderLocal(publish.ExtractorColormanaged): self.log.info('Finished render') self.log.debug("_ instance.data: {}".format(instance.data)) + + def _copy_last_published(self, anatomy, instance, out_dir, + expected_filenames): + """Copies last published files to temporary out_dir. + + These are base of files which will be extended/fixed for specific + frames. + Renames published file to expected file name based on frame, eg. + test_project_test_asset_subset_v005.1001.exr > new_render.1001.exr + """ + last_published = instance.data["last_version_published_files"] + last_published_and_frames = collect_frames(last_published) + + expected_and_frames = collect_frames(expected_filenames) + frames_and_expected = {v: k for k, v in expected_and_frames.items()} + for file_path, frame in last_published_and_frames.items(): + file_path = anatomy.fill_root(file_path) + if not os.path.exists(file_path): + continue + target_file_name = frames_and_expected.get(frame) + if not target_file_name: + continue + + out_path = os.path.join(out_dir, target_file_name) + self.log.debug("Copying '{}' -> '{}'".format(file_path, out_path)) + shutil.copy(file_path, out_path) + + # TODO shouldn't this be uncommented + # instance.context.data["cleanupFullPaths"].append(out_path) + + def _get_frames_to_render(self, frames_to_fix): + """Return list of frame range tuples to render + + Args: + frames_to_fix (str): specific or range of frames to be rerendered + (1005, 1009-1010) + Returns: + (list): [(1005, 1005), (1009-1010)] + """ + frames_to_render = [] + + for frame_range in frames_to_fix.split(","): + if frame_range.isdigit(): + render_first_frame = frame_range + render_last_frame = frame_range + elif '-' in frame_range: + frames = frame_range.split('-') + render_first_frame = int(frames[0]) + render_last_frame = int(frames[1]) + else: + raise ValueError("Wrong format of frames to fix {}" + .format(frames_to_fix)) + frames_to_render.append((render_first_frame, + render_last_frame)) + return frames_to_render diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py index cba361a8d4..fe70b37cb1 100644 --- a/openpype/lib/file_transaction.py +++ b/openpype/lib/file_transaction.py @@ -189,6 +189,6 @@ class FileTransaction(object): def _same_paths(self, src, dst): # handles same paths but with C:/project vs c:/project if os.path.exists(src) and os.path.exists(dst): - return os.path.samefile(src, dst) + return os.stat(src) == os.stat(dst) return src == dst diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py new file mode 100644 index 0000000000..bdd49585a5 --- /dev/null +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -0,0 +1,80 @@ +import pyblish.api +from openpype.lib.attribute_definitions import ( + TextDef, + BoolDef +) + +from openpype.pipeline.publish import OpenPypePyblishPluginMixin +from openpype.client.entities import ( + get_last_version_by_subset_name, + get_representations +) + + +class CollectFramesFixDef( + pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin +): + """Provides text field to insert frame(s) to be rerendered. + + Published files of last version of an instance subset are collected into + instance.data["last_version_published_files"]. All these but frames + mentioned in text field will be reused for new version. + """ + order = pyblish.api.CollectorOrder + 0.495 + label = "Collect Frames to Fix" + targets = ["local"] + hosts = ["nuke"] + families = ["render", "prerender"] + enabled = True + + def process(self, instance): + attribute_values = self.get_attr_values_from_data(instance.data) + frames_to_fix = attribute_values.get("frames_to_fix") + rewrite_version = attribute_values.get("rewrite_version") + + if frames_to_fix: + instance.data["frames_to_fix"] = frames_to_fix + + subset_name = instance.data["subset"] + asset_name = instance.data["asset"] + + project_entity = instance.data["projectEntity"] + project_name = project_entity["name"] + + version = get_last_version_by_subset_name(project_name, + subset_name, + asset_name=asset_name) + if not version: + self.log.warning("No last version found, " + "re-render not possible") + return + + representations = get_representations(project_name, + version_ids=[version["_id"]]) + published_files = [] + for repre in representations: + if repre["context"]["family"] not in self.families: + continue + + for file_info in repre.get("files"): + published_files.append(file_info["path"]) + + instance.data["last_version_published_files"] = published_files + self.log.debug("last_version_published_files::{}".format( + instance.data["last_version_published_files"])) + + if rewrite_version: + instance.data["version"] = version["name"] + # limits triggering version validator + instance.data.pop("latestVersion") + + @classmethod + def get_attribute_defs(cls): + return [ + TextDef("frames_to_fix", label="Frames to fix", + placeholder="5,10-15", + regex="[0-9,-]+"), + BoolDef("rewrite_version", label="Rewrite latest version", + default=False), + ] diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e1219fb68d..7b73943c37 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -534,6 +534,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["representation"] = repre["name"] template_data["ext"] = repre["ext"] + # allow overwriting existing version + template_data["version"] = version["name"] + # add template data for colorspaceData if repre.get("colorspaceData"): colorspace = repre["colorspaceData"]["colorspace"]