Skip to content

Commit

Permalink
Merge pull request mbrainerd#1 from aparna-d2/feature-442
Browse files Browse the repository at this point in the history
#442: run nuke as subprocess to enable the app across multiple engines
  • Loading branch information
mbrainerd committed Mar 28, 2018
2 parents ee874b0 + d6bf546 commit 942a400
Show file tree
Hide file tree
Showing 6 changed files with 478 additions and 169 deletions.
2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def render_and_submit_version(self, template, fields, first_frame, last_frame, s
# Make sure we don't overwrite the caller's fields
fields = copy.copy(fields)

# Tweak fields so that we'll be getting nuke formated sequence markers (%03d, %04d etc):
# Tweak fields so that we'll be getting nuke formatted sequence markers (%03d, %04d etc):
for key_name in [key.name for key in template.keys.values() if isinstance(key, sgtk.templatekey.SequenceKey)]:
fields[key_name] = "FORMAT: %d"

Expand Down
7 changes: 5 additions & 2 deletions hooks/codec_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import os
import sys

import nuke

HookBaseClass = sgtk.get_hook_baseclass()

class CodecSettings(HookBaseClass):
Expand All @@ -27,6 +25,11 @@ def get_quicktime_settings(self, **kwargs):
Returns a dictionary of settings to be used for the Write Node that generates
the Quicktime in Nuke.
"""
# This hook file gets loaded even if overriden in sgtk_config.
# import causes error if not launched from nuke.
# Therefore, moving the import into the function.
import nuke

settings = {}
if sys.platform in ["darwin", "win32"]:
settings["file_type"] = "mov"
Expand Down
258 changes: 258 additions & 0 deletions hooks/nuke_batch_render_movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import os
import pickle
import sys
import traceback
import getopt

from sgtk.context import Context

import nuke


def __create_scale_node(width, height):
"""
Create the Nuke scale node to resize the content.
"""
scale = nuke.nodes.Reformat()
scale["type"].setValue("to box")
scale["box_width"].setValue(width)
scale["box_height"].setValue(height)
scale["resize"].setValue("fit")
scale["box_fixed"].setValue(True)
scale["center"].setValue(True)
scale["black_outside"].setValue(True)
return scale


def __create_output_node(path, codec_settings, logger=None):
"""
Create the Nuke output node for the movie.
"""
# get the Write node settings we'll use for generating the Quicktime

wn_settings = codec_settings.get('quicktime', {})

node = nuke.nodes.Write(file_type=wn_settings.get("file_type", ''))

# apply any additional knob settings provided by the hook. Now that the knob has been
# created, we can be sure specific file_type settings will be valid.
for knob_name, knob_value in wn_settings.iteritems():
if knob_name != "file_type":
node.knob(knob_name).setValue(knob_value)

# Don't fail if we're in proxy mode. The default Nuke publish will fail if
# you try and publish while in proxy mode. But in earlier versions of
# tk-multi-publish (< v0.6.9) if there is no proxy template set, it falls
# back on the full-res version and will succeed. This handles that case
# and any custom cases where you may want to send your proxy render to
# screening room.
root_node = nuke.root()
is_proxy = root_node['proxy'].value()
if is_proxy:
if logger:
logger.info("Proxy mode is ON. Rendering proxy.")
node["proxy"].setValue(path.replace(os.sep, "/"))
else:
node["file"].setValue(path.replace(os.sep, "/"))

return node


def render_movie_in_nuke(path, output_path,
width, height,
first_frame, last_frame,
version, name,
color_space,
app_settings,
ctx, render_info,
is_subprocess=False):
"""
Use Nuke to render a movie. This assumes we're running _inside_ Nuke.
:param path: Path to the input frames for the movie
:param output_path: Path to the output movie that will be rendered
:param width: Width of the output movie
:param height: Height of the output movie
:param first_frame: Start frame for the output movie
:param last_frame: End frame for the output movie
:param version: Version number to use for the output movie slate and burn-in
:param name: Name to use in the slate for the output movie
:param color_space: Colorspace of the input frames
"""
output_node = None
root_node = nuke.root()

if is_subprocess:
# set Nuke root settings (since this is a subprocess with a fresh session)
root_node["first_frame"].setValue(first_frame)
root_node["last_frame"].setValue(last_frame)

# create group where everything happens
group = nuke.nodes.Group()

# now operate inside this group
group.begin()

try:
# create read node
read = nuke.nodes.Read(name="source", file=path.replace(os.sep, "/"))
read["on_error"].setValue("black")
read["first"].setValue(first_frame)
read["last"].setValue(last_frame)
if color_space:
read["colorspace"].setValue(color_space)

if is_subprocess:
# set root_format = res of read node
read_format = read.format()
read_format.add('READ_FORMAT')
root_node.knob('format').setValue('READ_FORMAT')

# now create the slate/burnin node
burn = nuke.nodePaste(render_info.get('burnin_nk'))
burn.setInput(0, read)

font = render_info.get('slate_font')

# set the fonts for all text fields
# TODO: find by class instead of using node names
burn.node("top_left_text")["font"].setValue(font)
burn.node("top_right_text")["font"].setValue(font)
burn.node("bottom_left_text")["font"].setValue(font)
burn.node("framecounter")["font"].setValue(font)
burn.node("slate_info")["font"].setValue(font)

# add the logo
logo = app_settings.get('slate_logo', '')
if not os.path.isfile(logo):
logo = ''

burn.node("logo")["file"].setValue(logo)

# format the burnins
ver_num_pad = app_settings.get('version_number_padding', 4)
version_padding_format = "%%0%dd" % ver_num_pad
version_str = version_padding_format % version

if ctx.task:
version_label = "%s, v%s" % (ctx.task["name"], version_str)
elif ctx.step:
version_label = "%s, v%s" % (ctx.step["name"], version_str)
else:
version_label = "v%s" % version_str

# TODO: use context names instead positional so that the nodes can be moved around
burn.node("top_left_text")["message"].setValue(ctx.project["name"])
burn.node("top_right_text")["message"].setValue(ctx.entity["name"])
burn.node("bottom_left_text")["message"].setValue(version_label)

# and the slate
slate_str = "Project: %s\n" % ctx.project["name"]
slate_str += "%s: %s\n" % (ctx.entity["type"], ctx.entity["name"])
slate_str += "Name: %s\n" % name.capitalize()
slate_str += "Version: %s\n" % version_str

if ctx.task:
slate_str += "Task: %s\n" % ctx.task["name"]
elif ctx.step:
slate_str += "Step: %s\n" % ctx.step["name"]

slate_str += "Frames: %s - %s\n" % (first_frame, last_frame)

burn.node("slate_info")["message"].setValue(slate_str)

# Create a scale node
scale = __create_scale_node(width, height)
scale.setInput(0, burn)

# Create the output node
output_node = __create_output_node(output_path, render_info.get('codec_settings', {}))
output_node.setInput(0, scale)
finally:
group.end()

if output_node:
# Make sure the output folder exists
output_folder = os.path.dirname(output_path)
# TODO: jsmk stuff?
if not os.path.isdir(output_folder):
os.makedirs(output_folder)

# Render the outputs, first view only
nuke.executeMultiple([output_node], ([first_frame-1, last_frame, 1],),
[nuke.views()[0]])

# Cleanup after ourselves
nuke.delete(group)


def get_usage():
return '''
Usage: python {0} [ OPTIONS ]
-h | --help ... print this usage message and exit.
--path <FRAME_PATH> ... specify full path to frames, with frame spec ... e.g. ".%04d.exr"
--output_path <OUTPUT_MOVIE_PATH> ... specify full path to output movie
--width <WIDTH> ... specify width for output movie
--height <HEIGHT> ... specify height for output movie
--first_frame <FIRST_FRAME> ... specify first frame number of the input frames
--last_frame <LAST_FRAME> ... specify last frame number of the input frames
--version <VERSION> ... specify version number for the slate
--name <NAME> ... specify name for the slate
--color_space <COLOR_SPACE> ... specify color space to use for the movie generation
--app_settings <APP_SETTINGS> ... specify app settings from the Toolkit app calling this
--shotgun_context <SHOTGUN_CONTEXT> ... specify shotgun context from the Toolkit app calling this
--render_info <RENDER_INFO> ... specify render info from the Toolkit app calling this
'''.format(os.path.basename(sys.argv[0]))


if __name__ == '__main__':
# TODO: copied maquino's code. Refactor?
data_keys = [
'path', 'output_path', 'width', 'height', 'first_frame', 'last_frame',
'version', 'name', 'color_space', 'app_settings', 'shotgun_context', 'render_info',
]

non_str_data_list = {
'width', 'height', 'first_frame', 'last_frame', 'version',
'app_settings', 'shotgun_context', 'render_info',
}

short_opt_str = "h"
long_opt_list = ['help'] + ['{0}='.format(k) for k in data_keys]

input_data = {}
try:
opt_list, arg_list = getopt.getopt(sys.argv[1:], short_opt_str, long_opt_list)
except getopt.GetoptError as err:
sys.stderr.write(str(err))
sys.stderr.write(get_usage())
sys.exit(1)

for opt, opt_value in opt_list:
if opt in ('-h', '--help'):
print get_usage()
sys.exit(0)
elif opt.replace('--', '') in data_keys:
d_key = opt.replace('--', '')
if d_key == 'shotgun_context':
input_data[d_key] = Context.deserialize(opt_value)
elif d_key in non_str_data_list:
input_data[d_key] = pickle.loads(opt_value)
else:
input_data[d_key] = opt_value

for d_key in data_keys:
if d_key not in input_data:
sys.stderr.write('ERROR - missing input argument for "--{0}". Aborting'.format(d_key))
sys.stderr.write(get_usage())
sys.exit(2)

render_movie_in_nuke(input_data['path'], input_data['output_path'],
input_data['width'], input_data['height'],
input_data['first_frame'], input_data['last_frame'],
input_data['version'], input_data['name'],
input_data['color_space'],
input_data['app_settings'],
input_data['shotgun_context'],
input_data['render_info'],
is_subprocess=True)
28 changes: 24 additions & 4 deletions info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,43 @@ configuration:
if they are defined."

slate_logo:
type: config_path
description: This is the path to an image to use on the slate such as a
type: template
fields: context
description: This is the template for a path to an image to use on the slate such as a
company logo. The supplied image will be reformated into a
400 pixel box and the lower left corner of the logo will be
aligned 100 pixels to the right and 100 pixels above the
lower left corner of the slate. You can use an image with
an alpha channel if you want to add transparency. Currently
any image format supported by Nuke is adequate. If this
setting is an empty string, no logo will be applied.
default_value: ""

codec_settings_hook:
type: hook
description: Hook for customizing codec settings used when generating items
for review.
default_value: '{self}/codec_settings.py'

nuke_linux_path:
type: str
description: The path to the application executable on Linux.
default_value: ""

nuke_windows_path:
type: str
description: The path to the application executable on Windows.
default_value: ""

nuke_mac_path:
type: str
description: The path to the application executable on Mac OS X.
default_value: ""

burnin_path:
type: template
fields: context
description: Template for burnin nuke file path

# the Shotgun fields that this app needs in order to operate correctly
requires_shotgun_fields:

Expand All @@ -103,7 +123,7 @@ requires_core_version: "v0.14.0"
requires_engine_version:

# For now this app is only compatible with Nuke.
supported_engines: ["tk-nuke"]
supported_engines:

# the frameworks required to run this app
frameworks:
Loading

0 comments on commit 942a400

Please sign in to comment.