Merge pull request #3245 from pypeclub/feature/OP-3207_Nuke-multiple-baking-streams-with-correct-slate

This commit is contained in:
Milan Kolar 2022-06-22 13:52:32 +02:00 committed by GitHub
commit 6cb50ad693
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 345 additions and 197 deletions

View file

@ -26,7 +26,11 @@ from .pipeline import (
update_container,
)
from .lib import (
maintained_selection
maintained_selection,
reset_selection,
get_view_process_node,
duplicate_node
)
from .utils import (
@ -58,6 +62,9 @@ __all__ = (
"update_container",
"maintained_selection",
"reset_selection",
"get_view_process_node",
"duplicate_node",
"colorspace_exists_on_node",
"get_colorspace_list"

View file

@ -3,6 +3,7 @@ from pprint import pformat
import re
import six
import platform
import tempfile
import contextlib
from collections import OrderedDict
@ -711,6 +712,20 @@ def get_imageio_input_colorspace(filename):
return preset_clrsp
def get_view_process_node():
reset_selection()
ipn_orig = None
for v in nuke.allNodes(filter="Viewer"):
ipn = v['input_process_node'].getValue()
if "VIEWER_INPUT" not in ipn:
ipn_orig = nuke.toNode(ipn)
ipn_orig.setSelected(True)
if ipn_orig:
return duplicate_node(ipn_orig)
def on_script_load():
''' Callback for ffmpeg support
'''
@ -2374,6 +2389,8 @@ def process_workfile_builder():
env_value_to_bool,
get_custom_workfile_template
)
# to avoid looping of the callback, remove it!
nuke.removeOnCreate(process_workfile_builder, nodeClass="Root")
# get state from settings
workfile_builder = get_current_project_settings()["nuke"].get(
@ -2429,9 +2446,6 @@ def process_workfile_builder():
if not openlv_on or not os.path.exists(last_workfile_path):
return
# to avoid looping of the callback, remove it!
nuke.removeOnCreate(process_workfile_builder, nodeClass="Root")
log.info("Opening last workfile...")
# open workfile
open_file(last_workfile_path)
@ -2617,6 +2631,57 @@ class DirmapCache:
return cls._sync_module
@contextlib.contextmanager
def _duplicate_node_temp():
"""Create a temp file where node is pasted during duplication.
This is to avoid using clipboard for node duplication.
"""
duplicate_node_temp_path = os.path.join(
tempfile.gettempdir(),
"openpype_nuke_duplicate_temp_{}".format(os.getpid())
)
# This can happen only if 'duplicate_node' would be
if os.path.exists(duplicate_node_temp_path):
log.warning((
"Temp file for node duplication already exists."
" Trying to remove {}"
).format(duplicate_node_temp_path))
os.remove(duplicate_node_temp_path)
try:
# Yield the path where node can be copied
yield duplicate_node_temp_path
finally:
# Remove the file at the end
os.remove(duplicate_node_temp_path)
def duplicate_node(node):
reset_selection()
# select required node for duplication
node.setSelected(True)
with _duplicate_node_temp() as filepath:
# copy selected to temp filepath
nuke.nodeCopy(filepath)
# reset selection
reset_selection()
# paste node and selection is on it only
dupli_node = nuke.nodePaste(filepath)
# reset selection
reset_selection()
return dupli_node
def dirmap_file_name_filter(file_name):
"""Nuke callback function with single full path argument.

View file

@ -14,12 +14,12 @@ from openpype.pipeline import (
from .lib import (
Knobby,
check_subsetname_exists,
reset_selection,
maintained_selection,
set_avalon_knob_data,
add_publish_knob,
get_nuke_imageio_settings,
set_node_knobs_from_settings
set_node_knobs_from_settings,
get_view_process_node
)
@ -216,37 +216,6 @@ class ExporterReview(object):
self.data["representations"].append(repre)
def get_view_input_process_node(self):
"""
Will get any active view process.
Arguments:
self (class): in object definition
Returns:
nuke.Node: copy node of Input Process node
"""
reset_selection()
ipn_orig = None
for v in nuke.allNodes(filter="Viewer"):
ip = v["input_process"].getValue()
ipn = v["input_process_node"].getValue()
if "VIEWER_INPUT" not in ipn and ip:
ipn_orig = nuke.toNode(ipn)
ipn_orig.setSelected(True)
if ipn_orig:
# copy selected to clipboard
nuke.nodeCopy("%clipboard%")
# reset selection
reset_selection()
# paste node and selection is on it only
nuke.nodePaste("%clipboard%")
# assign to variable
ipn = nuke.selectedNode()
return ipn
def get_imageio_baking_profile(self):
from . import lib as opnlib
nuke_imageio = opnlib.get_nuke_imageio_settings()
@ -311,7 +280,7 @@ class ExporterReviewLut(ExporterReview):
self._temp_nodes = []
self.log.info("Deleted nodes...")
def generate_lut(self):
def generate_lut(self, **kwargs):
bake_viewer_process = kwargs["bake_viewer_process"]
bake_viewer_input_process_node = kwargs[
"bake_viewer_input_process"]
@ -329,7 +298,7 @@ class ExporterReviewLut(ExporterReview):
if bake_viewer_process:
# Node View Process
if bake_viewer_input_process_node:
ipn = self.get_view_input_process_node()
ipn = get_view_process_node()
if ipn is not None:
# connect
ipn.setInput(0, self.previous_node)
@ -511,7 +480,7 @@ class ExporterReviewMov(ExporterReview):
if bake_viewer_process:
if bake_viewer_input_process_node:
# View Process node
ipn = self.get_view_input_process_node()
ipn = get_view_process_node()
if ipn is not None:
# connect
ipn.setInput(0, self.previous_node)

View file

@ -1,4 +1,5 @@
import os
from pprint import pformat
import re
import pyblish.api
import openpype
@ -50,6 +51,8 @@ class ExtractReviewDataMov(openpype.api.Extractor):
with maintained_selection():
generated_repres = []
for o_name, o_data in self.outputs.items():
self.log.debug(
"o_name: {}, o_data: {}".format(o_name, pformat(o_data)))
f_families = o_data["filter"]["families"]
f_task_types = o_data["filter"]["task_types"]
f_subsets = o_data["filter"]["subsets"]
@ -88,7 +91,13 @@ class ExtractReviewDataMov(openpype.api.Extractor):
# check if settings have more then one preset
# so we dont need to add outputName to representation
# in case there is only one preset
multiple_presets = bool(len(self.outputs.keys()) > 1)
multiple_presets = len(self.outputs.keys()) > 1
# adding bake presets to instance data for other plugins
if not instance.data.get("bakePresets"):
instance.data["bakePresets"] = {}
# add preset to bakePresets
instance.data["bakePresets"][o_name] = o_data
# create exporter instance
exporter = plugin.ExporterReviewMov(

View file

@ -1,11 +1,16 @@
import os
from pprint import pformat
import nuke
import copy
import pyblish.api
import openpype
from openpype.hosts.nuke.api.lib import maintained_selection
from openpype.hosts.nuke.api import (
maintained_selection,
duplicate_node,
get_view_process_node
)
class ExtractSlateFrame(openpype.api.Extractor):
@ -15,14 +20,13 @@ class ExtractSlateFrame(openpype.api.Extractor):
"""
order = pyblish.api.ExtractorOrder - 0.001
order = pyblish.api.ExtractorOrder + 0.011
label = "Extract Slate Frame"
families = ["slate"]
hosts = ["nuke"]
# Settings values
# - can be extended by other attributes from node in the future
key_value_mapping = {
"f_submission_note": [True, "{comment}"],
"f_submitting_for": [True, "{intent[value]}"],
@ -30,44 +34,107 @@ class ExtractSlateFrame(openpype.api.Extractor):
}
def process(self, instance):
if hasattr(self, "viewer_lut_raw"):
self.viewer_lut_raw = self.viewer_lut_raw
else:
self.viewer_lut_raw = False
if "representations" not in instance.data:
instance.data["representations"] = []
self._create_staging_dir(instance)
with maintained_selection():
self.log.debug("instance: {}".format(instance))
self.log.debug("instance.data[families]: {}".format(
instance.data["families"]))
self.render_slate(instance)
if instance.data.get("bakePresets"):
for o_name, o_data in instance.data["bakePresets"].items():
self.log.info("_ o_name: {}, o_data: {}".format(
o_name, pformat(o_data)))
self.render_slate(
instance,
o_name,
o_data["bake_viewer_process"],
o_data["bake_viewer_input_process"]
)
else:
# backward compatibility
self.render_slate(instance)
# also render image to sequence
self._render_slate_to_sequence(instance)
def _create_staging_dir(self, instance):
def render_slate(self, instance):
node_subset_name = instance.data.get("name", None)
node = instance[0] # group node
self.log.info("Creating staging dir...")
if "representations" not in instance.data:
instance.data["representations"] = list()
staging_dir = os.path.normpath(
os.path.dirname(instance.data['path']))
os.path.dirname(instance.data["path"]))
instance.data["stagingDir"] = staging_dir
self.log.info(
"StagingDir `{0}`...".format(instance.data["stagingDir"]))
frame_start = instance.data["frameStart"]
frame_end = instance.data["frameEnd"]
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
def _check_frames_exists(self, instance):
# rendering path from group write node
fpath = instance.data["path"]
frame_length = int(
(frame_end - frame_start + 1) + (handle_start + handle_end)
)
# instance frame range with handles
first = instance.data["frameStartHandle"]
last = instance.data["frameEndHandle"]
padding = fpath.count('#')
test_path_template = fpath
if padding:
repl_string = "#" * padding
test_path_template = fpath.replace(
repl_string, "%0{}d".format(padding))
for frame in range(first, last + 1):
test_file = test_path_template % frame
if not os.path.exists(test_file):
self.log.debug("__ test_file: `{}`".format(test_file))
return None
return True
def render_slate(
self,
instance,
output_name=None,
bake_viewer_process=True,
bake_viewer_input_process=True
):
"""Slate frame renderer
Args:
instance (PyblishInstance): Pyblish instance with subset data
output_name (str, optional):
Slate variation name. Defaults to None.
bake_viewer_process (bool, optional):
Switch for viewer profile baking. Defaults to True.
bake_viewer_input_process (bool, optional):
Switch for input process node baking. Defaults to True.
"""
slate_node = instance.data["slateNode"]
# rendering path from group write node
fpath = instance.data["path"]
# instance frame range with handles
first_frame = instance.data["frameStartHandle"]
last_frame = instance.data["frameEndHandle"]
# fill slate node with comments
self.add_comment_slate_node(instance, slate_node)
# solve output name if any is set
_output_name = output_name or ""
if _output_name:
_output_name = "_" + _output_name
slate_first_frame = first_frame - 1
temporary_nodes = []
collection = instance.data.get("collection", None)
if collection:
@ -75,99 +142,101 @@ class ExtractSlateFrame(openpype.api.Extractor):
fname = os.path.basename(collection.format(
"{head}{padding}{tail}"))
fhead = collection.format("{head}")
collected_frames_len = int(len(collection.indexes))
# get first and last frame
first_frame = min(collection.indexes) - 1
self.log.info('frame_length: {}'.format(frame_length))
self.log.info(
'len(collection.indexes): {}'.format(collected_frames_len)
)
if ("slate" in instance.data["families"]) \
and (frame_length != collected_frames_len):
first_frame += 1
last_frame = first_frame
else:
fname = os.path.basename(instance.data.get("path", None))
fname = os.path.basename(fpath)
fhead = os.path.splitext(fname)[0] + "."
first_frame = instance.data.get("frameStartHandle", None) - 1
last_frame = first_frame
if "#" in fhead:
fhead = fhead.replace("#", "")[:-1]
previous_node = node
self.log.debug("__ first_frame: {}".format(first_frame))
self.log.debug("__ slate_first_frame: {}".format(slate_first_frame))
# get input process and connect it to baking
ipn = self.get_view_process_node()
if ipn is not None:
ipn.setInput(0, previous_node)
previous_node = ipn
temporary_nodes.append(ipn)
# fallback if files does not exists
if self._check_frames_exists(instance):
# Read node
r_node = nuke.createNode("Read")
r_node["file"].setValue(fpath)
r_node["first"].setValue(first_frame)
r_node["origfirst"].setValue(first_frame)
r_node["last"].setValue(last_frame)
r_node["origlast"].setValue(last_frame)
r_node["colorspace"].setValue(instance.data["colorspace"])
previous_node = r_node
temporary_nodes = [previous_node]
else:
previous_node = slate_node.dependencies().pop()
temporary_nodes = []
if not self.viewer_lut_raw:
# only create colorspace baking if toggled on
if bake_viewer_process:
if bake_viewer_input_process:
# get input process and connect it to baking
ipn = get_view_process_node()
if ipn is not None:
ipn.setInput(0, previous_node)
previous_node = ipn
temporary_nodes.append(ipn)
# add duplicate slate node and connect to previous
duply_slate_node = duplicate_node(slate_node)
duply_slate_node.setInput(0, previous_node)
previous_node = duply_slate_node
temporary_nodes.append(duply_slate_node)
# add viewer display transformation node
dag_node = nuke.createNode("OCIODisplay")
dag_node.setInput(0, previous_node)
previous_node = dag_node
temporary_nodes.append(dag_node)
else:
# add duplicate slate node and connect to previous
duply_slate_node = duplicate_node(slate_node)
duply_slate_node.setInput(0, previous_node)
previous_node = duply_slate_node
temporary_nodes.append(duply_slate_node)
# create write node
write_node = nuke.createNode("Write")
file = fhead + "slate.png"
path = os.path.join(staging_dir, file).replace("\\", "/")
instance.data["slateFrame"] = path
file = fhead[:-1] + _output_name + "_slate.png"
path = os.path.join(
instance.data["stagingDir"], file).replace("\\", "/")
# add slate path to `slateFrames` instance data attr
if not instance.data.get("slateFrames"):
instance.data["slateFrames"] = {}
instance.data["slateFrames"][output_name or "*"] = path
# create write node
write_node["file"].setValue(path)
write_node["file_type"].setValue("png")
write_node["raw"].setValue(1)
write_node.setInput(0, previous_node)
temporary_nodes.append(write_node)
# fill slate node with comments
self.add_comment_slate_node(instance)
# Render frames
nuke.execute(write_node.name(), int(first_frame), int(last_frame))
# also render slate as sequence frame
nuke.execute(node_subset_name, int(first_frame), int(last_frame))
self.log.debug(
"slate frame path: {}".format(instance.data["slateFrame"]))
nuke.execute(
write_node.name(), int(slate_first_frame), int(slate_first_frame))
# Clean up
for node in temporary_nodes:
nuke.delete(node)
def get_view_process_node(self):
# Select only the target node
if nuke.selectedNodes():
[n.setSelected(False) for n in nuke.selectedNodes()]
def _render_slate_to_sequence(self, instance):
# set slate frame
first_frame = instance.data["frameStartHandle"]
slate_first_frame = first_frame - 1
ipn_orig = None
for v in [n for n in nuke.allNodes()
if "Viewer" in n.Class()]:
ip = v['input_process'].getValue()
ipn = v['input_process_node'].getValue()
if "VIEWER_INPUT" not in ipn and ip:
ipn_orig = nuke.toNode(ipn)
ipn_orig.setSelected(True)
# render slate as sequence frame
nuke.execute(
instance.data["name"],
int(slate_first_frame),
int(slate_first_frame)
)
if ipn_orig:
nuke.nodeCopy('%clipboard%')
[n.setSelected(False) for n in nuke.selectedNodes()] # Deselect all
nuke.nodePaste('%clipboard%')
ipn = nuke.selectedNode()
return ipn
def add_comment_slate_node(self, instance):
node = instance.data.get("slateNode")
if not node:
return
def add_comment_slate_node(self, instance, node):
comment = instance.context.data.get("comment")
intent = instance.context.data.get("intent")
@ -186,8 +255,8 @@ class ExtractSlateFrame(openpype.api.Extractor):
"intent": intent
})
for key, value in self.key_value_mapping.items():
enabled, template = value
for key, _values in self.key_value_mapping.items():
enabled, template = _values
if not enabled:
self.log.debug("Key \"{}\" is disabled".format(key))
continue
@ -221,5 +290,5 @@ class ExtractSlateFrame(openpype.api.Extractor):
))
except NameError:
self.log.warning((
"Failed to set value \"{}\" on node attribute \"{}\""
"Failed to set value \"{0}\" on node attribute \"{0}\""
).format(value))

View file

@ -3,7 +3,10 @@ import os
import nuke
import pyblish.api
import openpype
from openpype.hosts.nuke.api.lib import maintained_selection
from openpype.hosts.nuke.api import (
maintained_selection,
get_view_process_node
)
if sys.version_info[0] >= 3:
@ -17,7 +20,7 @@ class ExtractThumbnail(openpype.api.Extractor):
"""
order = pyblish.api.ExtractorOrder + 0.01
order = pyblish.api.ExtractorOrder + 0.011
label = "Extract Thumbnail"
families = ["review"]
@ -39,15 +42,32 @@ class ExtractThumbnail(openpype.api.Extractor):
self.log.debug("instance.data[families]: {}".format(
instance.data["families"]))
self.render_thumbnail(instance)
if instance.data.get("bakePresets"):
for o_name, o_data in instance.data["bakePresets"].items():
self.render_thumbnail(instance, o_name, **o_data)
else:
viewer_process_swithes = {
"bake_viewer_process": True,
"bake_viewer_input_process": True
}
self.render_thumbnail(instance, None, **viewer_process_swithes)
def render_thumbnail(self, instance):
def render_thumbnail(self, instance, output_name=None, **kwargs):
first_frame = instance.data["frameStartHandle"]
last_frame = instance.data["frameEndHandle"]
# find frame range and define middle thumb frame
mid_frame = int((last_frame - first_frame) / 2)
# solve output name if any is set
output_name = output_name or ""
if output_name:
output_name = "_" + output_name
bake_viewer_process = kwargs["bake_viewer_process"]
bake_viewer_input_process_node = kwargs[
"bake_viewer_input_process"]
node = instance[0] # group node
self.log.info("Creating staging dir...")
@ -106,17 +126,7 @@ class ExtractThumbnail(openpype.api.Extractor):
temporary_nodes.append(rnode)
previous_node = rnode
# bake viewer input look node into thumbnail image
if self.bake_viewer_input_process:
# get input process and connect it to baking
ipn = self.get_view_process_node()
if ipn is not None:
ipn.setInput(0, previous_node)
previous_node = ipn
temporary_nodes.append(ipn)
reformat_node = nuke.createNode("Reformat")
ref_node = self.nodes.get("Reformat", None)
if ref_node:
for k, v in ref_node:
@ -129,8 +139,16 @@ class ExtractThumbnail(openpype.api.Extractor):
previous_node = reformat_node
temporary_nodes.append(reformat_node)
# bake viewer colorspace into thumbnail image
if self.bake_viewer_process:
# only create colorspace baking if toggled on
if bake_viewer_process:
if bake_viewer_input_process_node:
# get input process and connect it to baking
ipn = get_view_process_node()
if ipn is not None:
ipn.setInput(0, previous_node)
previous_node = ipn
temporary_nodes.append(ipn)
dag_node = nuke.createNode("OCIODisplay")
dag_node.setInput(0, previous_node)
previous_node = dag_node
@ -138,7 +156,7 @@ class ExtractThumbnail(openpype.api.Extractor):
# create write node
write_node = nuke.createNode("Write")
file = fhead + "jpg"
file = fhead[:-1] + output_name + ".jpg"
name = "thumbnail"
path = os.path.join(staging_dir, file).replace("\\", "/")
instance.data["thumbnail"] = path
@ -168,30 +186,3 @@ class ExtractThumbnail(openpype.api.Extractor):
# Clean up
for node in temporary_nodes:
nuke.delete(node)
def get_view_process_node(self):
# Select only the target node
if nuke.selectedNodes():
[n.setSelected(False) for n in nuke.selectedNodes()]
ipn_orig = None
for v in [n for n in nuke.allNodes()
if "Viewer" == n.Class()]:
ip = v['input_process'].getValue()
ipn = v['input_process_node'].getValue()
if "VIEWER_INPUT" not in ipn and ip:
ipn_orig = nuke.toNode(ipn)
ipn_orig.setSelected(True)
if ipn_orig:
nuke.nodeCopy('%clipboard%')
# Deselect all
[n.setSelected(False) for n in nuke.selectedNodes()]
nuke.nodePaste('%clipboard%')
ipn = nuke.selectedNode()
return ipn

View file

@ -147,7 +147,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
# mapping of instance properties to be transfered to new instance for every
# specified family
instance_transfer = {
"slate": ["slateFrame"],
"slate": ["slateFrames"],
"review": ["lutPath"],
"render2d": ["bakingNukeScripts", "version"],
"renderlayer": ["convertToScanline"]

View file

@ -1,4 +1,6 @@
import os
from pprint import pformat
import re
import openpype.api
import pyblish
from openpype.lib import (
@ -21,6 +23,8 @@ class ExtractReviewSlate(openpype.api.Extractor):
families = ["slate", "review"]
match = pyblish.api.Subset
SUFFIX = "_slate"
hosts = ["nuke", "shell"]
optional = True
@ -29,28 +33,19 @@ class ExtractReviewSlate(openpype.api.Extractor):
if "representations" not in inst_data:
raise RuntimeError("Burnin needs already created mov to work on.")
suffix = "_slate"
slate_path = inst_data.get("slateFrame")
# get slates frame from upstream
slates_data = inst_data.get("slateFrames")
if not slates_data:
# make it backward compatible and open for slates generator
# premium plugin
slates_data = {
"*": inst_data["slateFrame"]
}
self.log.info("_ slates_data: {}".format(pformat(slates_data)))
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
slate_streams = get_ffprobe_streams(slate_path, self.log)
# Try to find first stream with defined 'width' and 'height'
# - this is to avoid order of streams where audio can be as first
# - there may be a better way (checking `codec_type`?)+
slate_width = None
slate_height = None
for slate_stream in slate_streams:
if "width" in slate_stream and "height" in slate_stream:
slate_width = int(slate_stream["width"])
slate_height = int(slate_stream["height"])
break
# Raise exception of any stream didn't define input resolution
if slate_width is None:
raise AssertionError((
"FFprobe couldn't read resolution from input file: \"{}\""
).format(slate_path))
if "reviewToWidth" in inst_data:
use_legacy_code = True
else:
@ -77,6 +72,12 @@ class ExtractReviewSlate(openpype.api.Extractor):
streams = get_ffprobe_streams(
input_path, self.log
)
# get slate data
slate_path = self._get_slate_path(input_file, slates_data)
self.log.info("_ slate_path: {}".format(slate_path))
slate_width, slate_height = self._get_slates_resolution(slate_path)
# Get video metadata
(
input_width,
@ -138,7 +139,7 @@ class ExtractReviewSlate(openpype.api.Extractor):
_remove_at_end = []
ext = os.path.splitext(input_file)[1]
output_file = input_file.replace(ext, "") + suffix + ext
output_file = input_file.replace(ext, "") + self.SUFFIX + ext
_remove_at_end.append(input_path)
@ -369,6 +370,43 @@ class ExtractReviewSlate(openpype.api.Extractor):
self.log.debug(inst_data["representations"])
def _get_slate_path(self, input_file, slates_data):
slate_path = None
for sl_n, _slate_path in slates_data.items():
if "*" in sl_n:
slate_path = _slate_path
break
elif re.search(sl_n, input_file):
slate_path = _slate_path
break
if not slate_path:
raise AttributeError(
"Missing slates paths: {}".format(slates_data))
return slate_path
def _get_slates_resolution(self, slate_path):
slate_streams = get_ffprobe_streams(slate_path, self.log)
# Try to find first stream with defined 'width' and 'height'
# - this is to avoid order of streams where audio can be as first
# - there may be a better way (checking `codec_type`?)+
slate_width = None
slate_height = None
for slate_stream in slate_streams:
if "width" in slate_stream and "height" in slate_stream:
slate_width = int(slate_stream["width"])
slate_height = int(slate_stream["height"])
break
# Raise exception of any stream didn't define input resolution
if slate_width is None:
raise AssertionError((
"FFprobe couldn't read resolution from input file: \"{}\""
).format(slate_path))
return (slate_width, slate_height)
def _get_video_metadata(self, streams):
input_timecode = ""
input_width = None