# Copyright (c) 2010 The Foundry Visionmongers Ltd. All Rights Reserved.
import os
import re
import sys
from threading import Thread
import nuke_internal as nuke
from nuke import memory2 as memory
from . import flipbooking
from .fnFlipbookRenderer import getFlipbookRenderer
from .panels import PythonPanel
from .utils import executeInMainThreadWithResult
##############################################################################
# Constants for the module
##############################################################################
_USE_SETTINGS_FROM_CUSTOM = 'Custom'
_FRAME_RANGE_GLOBAL = 'global'
_FRAME_RANGE_INPUT = 'input'
_FRAME_RANGE_INOUT = 'in-out'
_FRAME_RANGE_VISIBLE = 'visible'
_FRAME_RANGE_CUSTOM = 'custom'
##############################################################################
# Dialogs
##############################################################################
[docs]class DialogState:
def __init__(self):
self._state = {}
[docs] def get(self, knob, defaultValue = None):
"""Return the given knob's stored last state value.
If none exists, defaultValue is returned.
Values are stored in a dict referenced by knob name, so names must be unique!"""
return self.getValue(knob.name(), defaultValue)
[docs] def save(self, knob):
"""Store the knob's current value as the 'last state' for the next time the dialog is opened.
Values are stored in a dict referenced by knob name, so names must be unique!"""
self.saveValue(knob.name(), knob.value())
[docs] def setKnob(self, knob, defaultValue = None):
"""Convenience method for setting a value straight on a knob."""
knob.setValue(self.get(knob, defaultValue))
[docs] def saveValue(self, id, value):
"""Stores the value with the given id."""
self._state[id] = value
[docs] def getValue(self, id, defaultValue = None):
"""Recalls the value. If it was not set before, it will return the defaultValue."""
return self._state.get(id, defaultValue)
_gRenderDialogState = DialogState()
_gFlipbookDialogState = DialogState()
_gViewerCaptureDialogState = DialogState()
[docs]class ExecuteDialog(PythonPanel):
def _titleString(self):
return "Execute"
def _idString(self):
return "uk.co.thefoundry.ExecuteDialog"
def _addLeadingKnobs(self):
"""Add knobs that must appear first."""
return
def _addPreKnobs(self):
"""Add knobs that must appear before the render knobs."""
return
def _addPostKnobs(self):
"""Add knobs that must appear after the render knobs."""
return
def _addTrailingKnobs(self):
"""Add knobs that must appear at the very end."""
return
def _getDefaultViews(self):
oc = nuke.OutputContext()
allViews = [oc.viewname(i) for i in range(1, oc.viewcount())]
return " ".join(allViews)
def _addViewKnob(self):
"""Add knobs for view selection."""
oc = nuke.OutputContext()
if (oc.viewcount() > 2):
self._viewSelection = nuke.MultiView_Knob("multi_view", "Views")
self._viewSelection.fromScript(self._state.get(self._viewSelection, self._getDefaultViews()))
self.addKnob(self._viewSelection)
self._viewSelection.clearFlag(nuke.NO_MULTIVIEW)
[docs] def addKnob(self, knob):
"""Add the knob and make sure it cannot be animated."""
knob.setFlag(nuke.NO_ANIMATION | nuke.NO_MULTIVIEW)
super(ExecuteDialog, self).addKnob(knob)
def __init__(self, dialogState, groupContext, nodeSelection = [], exceptOnError = True):
self._state = dialogState
self._nodeSelection = nodeSelection
self._exceptOnError = exceptOnError
self._dialogKnobs = None
PythonPanel.__init__(self, self._titleString(), self._idString(), False)
self._viewers = {}
for n in nuke.allNodes("Viewer", groupContext):
self._viewers[n.name()] = n
self._addKnobs()
def _addKnobs(self):
self._dialogKnobs = []
beforeKnobs = self.knobs()
self._addLeadingKnobs()
self._addPreKnobs()
# Frame range knobs
specialRanges = [_FRAME_RANGE_GLOBAL, _FRAME_RANGE_INPUT, _FRAME_RANGE_CUSTOM]
for viewer in self._viewers.keys():
specialRanges.append(viewer + '/' + _FRAME_RANGE_INOUT)
specialRanges.append(viewer + '/' + _FRAME_RANGE_VISIBLE)
self._rangeEnum = nuke.CascadingEnumeration_Knob( "frame_range", "Frame range", specialRanges )
self._rangeEnum.setTooltip("Select the frame range for the flipbook")
self._state.setKnob(self._rangeEnum, _FRAME_RANGE_INPUT)
self.addKnob( self._rangeEnum )
self._frameRange = nuke.String_Knob( "frame_range_string", "")
self._frameRange.setTooltip("Custom frame range")
self._frameRange.clearFlag(nuke.STARTLINE)
if self._rangeEnum.value() == _FRAME_RANGE_CUSTOM:
self._state.setKnob(self._frameRange, str(nuke.root().frameRange()))
else:
self._setFrameRangeFromSource(self._rangeEnum.value())
self.addKnob(self._frameRange)
self._addPostKnobs()
self._addViewKnob()
self._addTrailingKnobs()
self._continueOnError = nuke.Boolean_Knob("continue", "Continue on error")
self._continueOnError.setTooltip("Continue on error")
self._state.setKnob(self._continueOnError, True)
self._continueOnError.setFlag(nuke.STARTLINE)
self.addKnob(self._continueOnError)
self._dialogKnobs = set(beforeKnobs.values()) ^ set(self.knobs().values())
def knobChanged( self, knob ):
self._state.save(knob)
if (knob == self._frameRange):
self._rangeEnum.setValue("custom")
self._state.save(self._rangeEnum)
self._state.saveValue("customRange", knob.value())
if (knob == self._rangeEnum):
self._setFrameRangeFromSource(knob.value())
self._state.save(self._frameRange)
def _setFrameRangeFromSource(self, source):
if (source == _FRAME_RANGE_INPUT):
try:
activeInput = nuke.activeViewer().activeInput()
self._frameRange.setValue(str(nuke.activeViewer().node().upstreamFrameRange(activeInput)))
except:
self._frameRange.setValue(str(nuke.root().frameRange()))
elif (source == _FRAME_RANGE_GLOBAL):
self._frameRange.setValue(str(nuke.root().frameRange()))
elif (source == _FRAME_RANGE_CUSTOM):
customRange = self._state.getValue("customRange", None)
if customRange:
self._frameRange.setValue(str(customRange))
else:
self._frameRangeFromViewer(*source.split('/'));
def _frameRangeFromViewer( self, viewer, frameRange ):
"""Set the framerange knob to have the framerange from the given viewer."""
if frameRange == _FRAME_RANGE_VISIBLE:
viewerRange = str(self._viewers[viewer].visibleRange())
else:
viewerRange = str(self._viewers[viewer].playbackRange())
self._frameRange.setValue(viewerRange)
def _selectedViews(self):
try:
return self._viewSelection.value().split()
except AttributeError:
# If we didn't add the view selection knob, there should be just the one view.
return [nuke.OutputContext().viewname(1)]
def addToPane(self):
PythonPanel.addToPane(self, pane = nuke.thisPane())
def run(self):
frame_ranges = nuke.FrameRanges(self._frameRange.value().split(','))
views = self._selectedViews()
try:
nuke.Undo().disable()
nuke.executeMultiple(self._nodeSelection, frame_ranges, views, continueOnError = self._continueOnError.value())
except RuntimeError as e:
if self._exceptOnError or e.args[0][0:9] != "Cancelled": # TO DO: change this to an exception type
raise
finally:
nuke.Undo().enable()
[docs]class RenderDialog(ExecuteDialog):
deprecatedWarningShown = False
shouldShowBGRender = False
ShowWarning = True
def _titleString(self):
return "Render"
def _idString(self):
return "uk.co.thefoundry.RenderDialog"
def __init__(self,
dialogState,
groupContext,
nodeSelection = [],
exceptOnError = True,
allowFrameServer = True):
self._allowFrameServer = allowFrameServer
ExecuteDialog.__init__(self, dialogState, groupContext, nodeSelection, exceptOnError)
def _addPreKnobs( self ):
if self.isTimelineWrite():
self._timelineRender = nuke.Boolean_Knob("timeline_render", "Render to timeline" )
self._state.setKnob( self._timelineRender, True)
self._timelineRender.setFlag(nuke.STARTLINE)
self.addKnob(self._timelineRender)
def _showDeprecatedWarningMessage(self):
if not RenderDialog.deprecatedWarningShown:
RenderDialog.deprecatedWarningShown = True;
nuke.tprint("Render in Background option functionality has been deprecated and replaced with the Frame Server.")
def _addTrailingKnobs(self):
# Proxy
self._useProxy = nuke.Boolean_Knob("use_proxy", "Use proxy")
self._useProxy.setTooltip("Use proxy")
self._useProxy.setFlag(nuke.STARTLINE)
self._state.setKnob(self._useProxy, nuke.root().proxy())
self.addKnob(self._useProxy)
self._frameserverRender = nuke.Boolean_Knob("frameserver_render", "Render using frame server")
self._frameserverRender.setTooltip("Render using frame server")
select_frame_server = nuke.toNode("preferences").knob("RenderWithFrameServer").getValue()
self._state.setKnob(self._frameserverRender, select_frame_server)
self._frameserverRender.setVisible(self.frameserverRenderAvailable())
self._frameserverRender.setFlag(nuke.STARTLINE)
self.addKnob(self._frameserverRender)
self._bgRender = nuke.Boolean_Knob("bg_render", "Render in background")
self._state.setKnob(self._bgRender, False)
self._bgRender.setVisible(self.backgroundRenderAvailable())
self._bgRender.setFlag(nuke.STARTLINE)
self.addKnob(self._bgRender)
self._numThreads = nuke.Int_Knob("num_threads", "Thread limit")
self._numThreads.setVisible(self.isBackgrounded())
self._state.setKnob(self._numThreads, max(int(nuke.NUM_CPUS / 2), 1))
self.addKnob(self._numThreads)
self._maxMem = nuke.String_Knob("max_memory", "Memory limit")
self._state.setKnob(self._maxMem, str(max(int(memory.maxUsage() / 2097152), 16)) + "M")
self._maxMem.setVisible(self.isBackgrounded())
self.addKnob(self._maxMem)
if self.isBackgrounded():
self._showDeprecatedWarningMessage()
def _getBackgroundLimits(self):
#Deprecated
if re.search(r"^(?<![\d.])[0-9]+(?![\d.])[kmgt]", self._maxMem.value(), re.IGNORECASE) == None:
# regex to match a string that starts with an int not float followed by letter kmgt, any training string or spaces are ignored.
raise Exception('The memory limit specified does not match the required format.')
return {
"maxThreads": self._numThreads.value(),
"maxCache": self._maxMem.value() }
def knobChanged( self, knob ):
ExecuteDialog.knobChanged(self, knob)
timelineRender = False
try:
timelineRender = self._timelineRender.value()
except:
pass
bgRenderVisible = not timelineRender and self.backgroundRenderAvailable()
showBGRenderOptions = bgRenderVisible and self.isBackgrounded()
self._bgRender.setVisible(bgRenderVisible)
self._numThreads.setVisible(showBGRenderOptions)
self._maxMem.setVisible(showBGRenderOptions)
#if knob changed is the bgRender, set it visible
if knob is self._bgRender:
self._showDeprecatedWarningMessage()
if timelineRender and self.isTimelineWrite():
self._rangeEnum.setValue( _FRAME_RANGE_GLOBAL )
self._setFrameRangeFromSource(self._rangeEnum.value())
self._rangeEnum.setEnabled( False )
self._frameRange.setEnabled( False )
self._frameserverRender.setVisible(False)
self._useProxy.setVisible( False )
self._continueOnError.setVisible( False )
else:
self._rangeEnum.setEnabled( True )
self._frameRange.setEnabled( True )
if self._allowFrameServer:
self._frameserverRender.setVisible(self.frameserverRenderAvailable())
self._useProxy.setVisible( True )
self._continueOnError.setVisible( True )
[docs] def isBackgrounded(self):
"""Return whether the background rendering option is enabled."""
#Deprecated
return self._bgRender.value() and self.backgroundRenderAvailable()
[docs] def isFrameserverEnabled(self):
"""Return whether the frame server option is enabled."""
return self._frameserverRender.value() and self.frameserverRenderAvailable()
[docs] def backgroundRenderAvailable(self):
"""Return whether background rendering should be allowed for this render"""
# If frame server is not available for any reason, show the 'Render in background' option
return RenderDialog.shouldShowBGRender or not self.frameserverRenderAvailable()
def frameserverRenderAvailable(self):
import hiero.ui.nuke_bridge.FnNsFrameServer
return (hiero.ui.nuke_bridge.FnNsFrameServer.isServerRunning() and
self._allowFrameServer and not self.renderContainsContainers())
def extractContainerNodes(self, nodeSelection):
containerWriteNodes = []
nonContainerWriteNodes = []
#extract mov nodes from the rest
for node in nodeSelection:
knob = node.knobs().get("file_type",None)
if knob:
fileType = knob.value()
if fileType in ("mov", "mov32", "mov64", "ffmpeg", "mxf"):
containerWriteNodes.append(node)
else:
nonContainerWriteNodes.append(node)
return (nonContainerWriteNodes, containerWriteNodes)
def isTimelineWrite(self):
if not nuke.env['studio']:
return False
if len(self._nodeSelection) > 1 or len(self._nodeSelection) < 1:
return False
write = self._nodeSelection[0]
if write == nuke.root():
## must be a render of all 'write' nodes, this is tricky as there may be other executable nodes apart from write nodes
## lets assume the write nodes are write, writegeo, particlecache, and diskcache
# there is a bug here however as there may be groups they are executable which will be skipped right now
writeNodes = nuke.allNodes( 'Write' )
writeNodes.extend( nuke.allNodes( 'WriteGeo') )
writeNodes.extend( nuke.allNodes( 'ParticleCache') )
writeNodes.extend( nuke.allNodes( 'DiskCache') )
if len(writeNodes) > 1:
return False
if len(writeNodes) > 0:
write = writeNodes[0]
timelineWriteNode = None
try:
from foundry.frameserver.nuke.workerapplication import GetWriteNode
timelineWriteNode = GetWriteNode()
except:
pass
if not timelineWriteNode:
return False
if timelineWriteNode.fullName() != write.fullName():
return False
## double check that this script is actually in a timeline
try:
from hiero.ui import isInAnyProject
return isInAnyProject( nuke.scriptName() )
except:
pass
return False
def saveFileToRender(self, prefix, forceSaveNew):
import datetime
now = datetime.datetime.now()
timestamp = now.strftime('%H%M_%S.%f_%d-%m-%Y')
if nuke.env['nc']:
nukeExt = ".nknc"
if nuke.env['indie']:
nukeExt = ".nkind"
else:
nukeExt = ".nk"
wasSaved = False
#if file is unsaved
if nuke.Root().name() == 'Root':
fileName = "%s.%s%s"%(prefix,timestamp, nukeExt)
filePath = "/".join([os.environ["NUKE_TEMP_DIR"], fileName])
nuke.scriptSaveToTemp(filePath)
wasSaved = True
else:
originalPath = nuke.scriptName()
#if file is saved but not up to date
if nuke.Root().modified() == True or forceSaveNew:
extensionIndex = originalPath.rfind(nukeExt)
noExt = originalPath[:extensionIndex] if extensionIndex>=0 else originalPath
head, tail = os.path.split(noExt)
fileName = "%s.%s.%s%s"%(prefix,tail,timestamp,nukeExt)
filePath = "/".join([head,fileName])
try:
nuke.scriptSaveToTemp(filePath)
wasSaved = True
except RuntimeError:
nuke.tprint("Exception when saving script to: %s. Saving to NUKE_TEMP_DIR"%filePath)
fileName = originalPath.split()[-1]
filePath = "/".join([os.environ["NUKE_TEMP_DIR"], fileName])
nuke.scriptSaveToTemp(filePath)
wasSaved = True
#if file is saved
else:
filePath = originalPath
return (filePath, wasSaved)
def getNodeSelection(self):
nodeSelection = self._nodeSelection
nodesToRenderCount= len(nodeSelection)
if nodesToRenderCount==1:
if nodeSelection[0] == nuke.root():
nodeSelection = nuke.allNodes("Write")
nodeSelection.extend(nuke.allNodes("DeepWrite"))
return nodeSelection
def renderContainsContainers(self):
(_, containerNodesToRender) = self.extractContainerNodes(self.getNodeSelection())
if containerNodesToRender:
return True
return False
def renderToFrameServer(self,frame_ranges, views):
nodeSelection = self.getNodeSelection()
(nodesToRender, containerNodesToRender) = self.extractContainerNodes(nodeSelection)
#check views and frame pading
for node in nodeSelection:
path = node["file"].getText()
needsPadding = frame_ranges.getRange(0).frames()>1
hasPadding = nuke.filename( node, nuke.REPLACE ) != path or node["file_type"].value()=="mov"
if needsPadding and not hasPadding:
nuke.message("%s cannot be executed for multiple frames."%node.fullName())
return
#there's no way to find if writer can write multiple views per file in python
needsViews = (len(views)!=1 or views[0] != "main" )
hasViews = path.find("%v")!=-1 or path.find("%V")!=-1 or node["file_type"].value()=="exr"
if needsViews and not hasViews:
nuke.message("%s cannot be executed for multiple views."%node.fullName())
return
#if there's any container to be rendered, show a warning, render synchronously later
if containerNodesToRender:
if RenderDialog.ShowWarning:
from PySide6.QtWidgets import QCheckBox, QMessageBox
message = "It is currently not possible to render container formats using the frame server. Select Continue to render in the current Nuke session.\n\nPlease see the user guide for more information."
messageBox = QMessageBox(QMessageBox.Warning, "Warning", message, QMessageBox.Cancel)
messageBox.addButton("Continue",QMessageBox.AcceptRole)
dontShowCheckBox = QCheckBox("Don't show this message again")
dontShowCheckBox.blockSignals(True)
messageBox.addButton(dontShowCheckBox, QMessageBox.ResetRole)
result = messageBox.exec_()
if result != QMessageBox.AcceptRole:
return
if dontShowCheckBox.isChecked():
RenderDialog.ShowWarning = False
import hiero.ui.nuke_bridge.FnNsFrameServer as FrameServer
sortRenderOrder = lambda w : w.knobs()["render_order"].getValue()
if nodesToRender:
nodesToRender.sort(key = sortRenderOrder)
name, wasSaved = self.saveFileToRender("tmp_unsaved",False)
try:
for node in nodesToRender:
FrameServer.renderFrames(
name,
frame_ranges,
node.fullName(),
self._selectedViews())
finally:
if wasSaved:
os.unlink(name)
if containerNodesToRender:
containerNodesToRender.sort(key = sortRenderOrder)
nuke.executeMultiple(containerNodesToRender, frame_ranges, views, continueOnError = self._continueOnError.value())
def run(self):
##this will render the full framerange of the script
if self.isTimelineWrite() and self._timelineRender.value():
from hiero.ui.nuke_bridge.nukestudio import scriptSaveAndReRender
scriptSaveAndReRender()
return
frame_ranges = nuke.FrameRanges(self._frameRange.value().split(','))
views = self._selectedViews()
rootProxyMode = nuke.root().proxy()
try:
nuke.Undo().disable()
nuke.root().setProxy(self._useProxy.value())
if self.isFrameserverEnabled():
self.renderToFrameServer(frame_ranges,views)
elif self.isBackgrounded():
nuke.executeBackgroundNuke(nuke.EXE_PATH, self._nodeSelection,
frame_ranges, views, self._getBackgroundLimits(), continueOnError = self._continueOnError.value())
else:
nuke.executeMultiple(self._nodeSelection, frame_ranges, views, continueOnError = self._continueOnError.value())
except RuntimeError as e:
if self._exceptOnError or e.args[0][0:9] != "Cancelled": # TO DO: change this to an exception type
raise
finally:
nuke.root().setProxy(rootProxyMode)
nuke.Undo().enable()
[docs]class FlipbookDialog( RenderDialog ):
def _titleString( self ):
return "Flipbook"
def _idString( self ):
return "uk.co.thefoundry.FlipbookDialog"
def __init__(self, dialogState, groupContext, node, takeNodeSettings):
# Init attributes
self._node = node
self._takeNodeSettings = takeNodeSettings
self._customKnobs = []
# init super
RenderDialog.__init__(self, dialogState, groupContext)
# Override the initial frame range value
self._state.setKnob(self._rangeEnum, _FRAME_RANGE_INPUT)
self._setFrameRangeFromSource(self._rangeEnum.value())
if self._takeNodeSettings:
self._viewerForSettings.setValue(node.name())
self.knobChanged(self._viewerForSettings)
def _addLeadingKnobs( self ):
flipbookNames = flipbooking.gFlipbookFactory.getNames()
self._flipbookEnum = nuke.Enumeration_Knob( "flipbook", "Flipbook", flipbookNames )
self._flipbookEnum.setTooltip("Select Flipbook Player")
defaultFlipbook = nuke.toNode('preferences').knob('defaultFlipbook').getValue()
if not defaultFlipbook or not defaultFlipbook in flipbookNames:
defaultFlipbook = flipbookNames[0]
self._state.setKnob(self._flipbookEnum, defaultFlipbook)
self.addKnob( self._flipbookEnum )
self._viewerForSettings = nuke.Enumeration_Knob("viewer_settings", "Use Settings From", list(self._viewers.keys()) + [_USE_SETTINGS_FROM_CUSTOM])
self._viewerForSettings.setTooltip("Select your viewer setting for the flipbook")
if not self._takeNodeSettings:
self._viewerForSettings.setValue(_USE_SETTINGS_FROM_CUSTOM)
self.addKnob(self._viewerForSettings)
def _addPreKnobs( self ):
# Region of Interest knobs
self._useRoi = nuke.Boolean_Knob("use_roi", "Enable ROI")
self._useRoi.setTooltip("Use Region of Interest (ROI)")
self._useRoi.setFlag(nuke.STARTLINE)
self._state.setKnob(self._useRoi, False)
self.addKnob(self._useRoi)
self._roi = nuke.BBox_Knob("roi", "Region of Interest")
self._state.setKnob(self._roi, (0, 0, 0, 0))
self.addKnob(self._roi)
self._roi.setVisible(self._useRoi.value())
# Channel knobs
self._channels = nuke.Channel_Knob( "channels_knob", "Channels")
self._channels.setTooltip("Select channels for the flipbook")
if self._node.Class() == "Write":
self._channels.setValue(self._node.knob("channels").value())
else:
self._state.setKnob(self._channels, "rgba")
self._channels.setFlag(nuke.STARTLINE | nuke.NO_CHECKMARKS)
self.addKnob( self._channels )
def _addPostKnobs( self ):
# LUT knobs
self._luts = nuke.Enumeration_Knob("lut", "LUT", nuke.ViewerProcess.registeredNames())
self._luts.setTooltip("Select LUT for the flipbook")
if self._takeNodeSettings:
self._state.setKnob(self._luts, self._lutFromViewer(self._viewerForSettings.value()))
else:
self._state.setKnob(self._luts, self._lutFromViewer())
self.addKnob(self._luts)
self._burnInLUT = nuke.Boolean_Knob("burnin", "Burn in the LUT")
self._burnInLUT.setTooltip("Burn in the LUT")
self._state.setKnob(self._burnInLUT, False)
self.addKnob(self._burnInLUT)
# Audio knobs
audioList = []
audioList.append( "None" )
for node in nuke.allNodes("AudioRead"):
audioList.append( node.name() )
self._audioSource = nuke.Enumeration_Knob( "audio", "Audio", audioList )
self._audioSource.setTooltip("Include audio to flipbook")
self._state.setKnob(self._audioSource, audioList[0] )
self.addKnob( self._audioSource )
super(FlipbookDialog, self)._addPostKnobs()
self.flipbookKnobs()
def flipbookKnobs(self):
try:
beforeKnobs = self.knobs()
flipbookToRun = flipbooking.gFlipbookFactory.getApplication(self._flipbookEnum.value())
flipbookToRun.dialogKnobs(self)
afterKnobs = self.knobs()
self._customKnobs = list(set(beforeKnobs.values()) ^ set(afterKnobs.values()))
except NotImplementedError:
pass
def _getDefaultViews(self):
return nuke.activeViewer().view()
def _addViewKnob(self):
oc = nuke.OutputContext()
self._views = [oc.viewname(i) for i in range(1, oc.viewcount())]
if (oc.viewcount() > 2):
supportedViews = self._selectedFlipbook().capabilities()["maximumViews"]
if (int(supportedViews) > 1):
self._viewSelection = nuke.MultiView_Knob("views", "Views")
else:
self._viewSelection = nuke.OneView_Knob("views", "View", self._views)
activeView = nuke.activeViewer().view()
if activeView == "":
activeView = self._views[0]
# Retrieve previous view selection or default to selecting all available views
previousViews = self._state.getValue(self._viewSelection.name(), " ".join(self._views)).split()
# Get the intersection of the previous selection and the available views
viewsToRestore = set(self._views).intersection(previousViews)
if viewsToRestore:
self._viewSelection.setValue(" ".join(viewsToRestore))
else:
self._viewSelection.setValue(activeView)
self.addKnob(self._viewSelection)
self._viewSelection.clearFlag(nuke.NO_MULTIVIEW)
def _selectedFlipbook(self):
return flipbooking.gFlipbookFactory.getApplication(self._flipbookEnum.value())
def _lutFromViewer(self, viewerName = ""):
try:
if viewerName == "":
return nuke.ViewerProcess.node().knob("current").value()
else:
return nuke.ViewerProcess.node(viewer=viewerName).knob("current").value()
except AttributeError:
return "None"
def _isViewerSettingKnob(self, knob):
return knob == self._useRoi or knob == self._roi or knob == self._channels or knob == self._useProxy or knob == self._frameRange or knob == self._rangeEnum or knob == self._luts
def _setKnobAndStore(self, knob, val):
knob.setValue(val)
self._state.save(knob)
def knobChanged(self, knob):
RenderDialog.knobChanged(self, knob)
if (knob == self._viewerForSettings):
if self._viewerForSettings.value() != _USE_SETTINGS_FROM_CUSTOM:
viewer = self._viewers[self._viewerForSettings.value()]
self._setKnobAndStore(self._useRoi, viewer.roiEnabled())
roi = viewer.roi()
if roi != None:
self._roi.fromDict(roi)
self._state.save(self._roi)
self._channels.fromScript(viewer.knob("channels").toScript())
self._state.save(self._channels)
self._setKnobAndStore(self._useProxy, nuke.root().proxy())
self._frameRangeFromViewer(viewer.name(), _FRAME_RANGE_INOUT)
self._state.save(self._frameRange)
self._setKnobAndStore(self._rangeEnum, viewer.name() + '/' + _FRAME_RANGE_INOUT)
self._roi.setVisible(self._useRoi.value())
self._setKnobAndStore(self._luts, self._lutFromViewer(viewer.name()))
elif (knob == self._useRoi):
self._roi.setVisible(self._useRoi.value())
elif self._isViewerSettingKnob(knob):
self._viewerForSettings.setValue(_USE_SETTINGS_FROM_CUSTOM)
self._state.save(self._viewerForSettings)
elif knob == self._luts:
self._burnInLUT.setEnabled(self._luts.value() != "None")
if knob == self._flipbookEnum:
for k in self._dialogKnobs:
self.removeKnob(k)
self.removeKnob(self.okButton)
self.removeKnob(self.cancelButton)
self._customKnobs = []
self._addKnobs()
self._makeOkCancelButton()
elif knob in self._customKnobs:
try:
flipbookToRun = flipbooking.gFlipbookFactory.getApplication(self._flipbookEnum.value())
flipbookToRun.dialogKnobChanged(self, knob)
except NotImplementedError:
pass
def _getIntermediateFileType(self):
return _gFlipbookDialogState.getValue('intermediateFormat', 'exr')
def _getIntermediatePath(self):
"""Get the path for the temporary files. May be filled in using printf syntax."""
flipbooktmp=""
if flipbooktmp == "":
try:
flipbooktmp = self._selectedFlipbook().cacheDir()
except:
try:
flipbooktmp = os.environ["NUKE_DISK_CACHE"]
except:
flipbooktmp = nuke.value("preferences.DiskCachePath")
if len(self._selectedViews()) > 1:
flipbookFileNameTemp = "nuke_tmp_flip.%04d.%V." + self._getIntermediateFileType()
else:
flipbookFileNameTemp = "nuke_tmp_flip.%04d." + self._getIntermediateFileType()
flipbooktmpdir = os.path.join(flipbooktmp, "flipbook")
if not os.path.exists(flipbooktmpdir):
os.mkdir(flipbooktmpdir)
if not os.path.isdir(flipbooktmpdir):
raise RuntimeError("%s already exists and is not a directory, please delete before flipbooking again" % flipbooktmpdir)
flipbooktmp = os.path.join(flipbooktmpdir, flipbookFileNameTemp)
if nuke.env['WIN32']:
flipbooktmp = re.sub(r"\\", "/", str(flipbooktmp))
return flipbooktmp
def _requireIntermediateNode(self, nodeToTest):
if nodeToTest.Class() == "Read" or nodeToTest.Class() == "Write":
flipbookToRun = flipbooking.gFlipbookFactory.getApplication(self._flipbookEnum.value())
flipbookCapabilities = flipbookToRun.capabilities()
# Check if we can read it in directly..
filePath = nuke.filename(nodeToTest)
# There might be a prefix that overrides the extension, if so, this will
# confuse the flipbook probably, so just create a render.
if ':' in filePath:
readerPrefix = filePath.split(':')[0]
if len(readerPrefix) > 1: # 1 is a drive letter
return True
fileExtension = os.path.splitext(filePath)[1].lower()[1:]
# Check if the flipbook supports the file extension. Allow a wildcard to
# indicate 'all files' (such as for the internal flipbook functionality)
supportedFileTypes = flipbookCapabilities.get("fileTypes", [])
flipbookSupportsFileType = (supportedFileTypes == ["*"]) or (fileExtension in supportedFileTypes)
if not flipbookSupportsFileType:
return True
# Not all flipbooks can handle weird channels
flipbookSupportsArbitraryChannels = flipbookCapabilities.get("arbitraryChannels", False)
if self._channels.value() not in set(["rgb", "rgba", "alpha"]) and not flipbookSupportsArbitraryChannels:
return True
channelKnob = nodeToTest.knob("channels")
if channelKnob != None and channelKnob.value() != self._channels.value():
return True
## if flipbook doesn't support roi and the roi option is on we need the intermediate node because we're going to insert a crop
if self._useRoi.value() and self._useRoi.enabled():
if not flipbookCapabilities.get("roi", False):
return True
if self._burnInLUT.value() and self._burnInLUT.enabled():
return True
# Separate view files
# TODO Temporary workaround to deal with MediaSource not being aware of
# the %V notation for specifiying separate view files.
if "%V" in filePath or "%v" in filePath:
return True
return False
else:
return True
def _getBurninWriteColorspace(self):
"""Helper function to get the appropriate colorspace to set on the Write node when burning the
viewer color transform into the render."""
writeColorspace = None
lut = self._getLUT()
# Determine whether we're using original Nuke, rather than OCIO, color management.
usingNukeColorspaces = False
rootNode = nuke.root()
if rootNode:
colorManagementKnob = rootNode.knob("colorManagement")
if colorManagementKnob:
usingNukeColorspaces = (colorManagementKnob.value() == "Nuke") # Note: Must use value() rather than getValue().
if usingNukeColorspaces:
# We're using Nuke colorspace management so the our lut knob will correspond to the appropriate
# colorspace name, except for rec1886, which we need to map to Gamma2.4.
# If the lut is the special case of None then don't set writeColorspace (to match the original behaviour).
# (The rec1886 -> Gamma2.4 mapping matches what's done in register_default_viewer_processes, in
# NukeScripts/src/ViewerProcess.py - I suppose we could consider adding something into
# nuke/src/Python/PythonObjects/ViewerProcess.cpp to allow the python code to look-up into ViewerProcessMap
# so we could parse the registered args to obtain the colorspace but that seems like overkill and not much
# less fragile.)
if lut == "rec1886":
writeColorspace = "Gamma2.4"
elif lut != "None":
writeColorspace = lut
else:
# We're using OCIO color management so we expect our lut string to contain a view name
# followed by a single space and a display name in parantheses.
# For example, the aces 1.0.1 config has lots of views but all for the display called ACES,
# hence here we might get lut set to, for example,
# DCDM P3 gamut clip (ACES)
# As another example, the spi-vfx config has views for two displays, DCIP3 and sRGB, so we might get
# Film (DCIP3)
# or
# Film (sRGB)
# etc.
# The nuke-default config has a single display called 'default' so we get lut set to
# None (default)
# or
# sRGB (default)
# etc. The nuke-default case is a bit confusing because the _view_ names almost match colorspace names
# and also correspond to legacy Nuke colorspace managament options.
#
# Anyway, what we actually need to return is the actual colorspace the particualr combination of
# view and display name map to, as defined in the relevant OCIO config file.
displayName = ""
viewName = ""
NUKE_LUT_FORMAT = r'(.*) \((.*)\)$'
matchResult = re.match( NUKE_LUT_FORMAT , lut)
if matchResult is not None:
viewName = matchResult.group(1)
displayName = matchResult.group(2)
if rootNode:
writeColorspace = rootNode.getOCIOColorspaceFromViewTransform(displayName, viewName)
return writeColorspace
def _createIntermediateNode(self):
"""Create a write node to render out the current node so that output may be used for flipbooking."""
flipbooktmp = self._getIntermediatePath()
fieldname = "file"
if self._useProxy.value():
fieldname = "proxy"
fixup = nuke.createNode("Group", "tile_color 0xff000000", inpanel = False)
with fixup:
fixup.setName("Flipbook")
inputNode = nuke.createNode("Input", inpanel = False)
shuffle = nuke.createNode("Shuffle", inpanel = False)
shuffle.knob("in").setValue(self._channels.value())
if self._useRoi.value():
crop = nuke.createNode( "Crop", inpanel = False )
crop['box'].fromScript( self._roi.toScript() )
write = nuke.createNode("Write", fieldname+" {"+flipbooktmp+"}", inpanel = False)
write.knob('file_type').setValue(self._getIntermediateFileType())
selectedViews = self._selectedViews()
write.knob('views').fromScript(" ".join(selectedViews))
if self._getIntermediateFileType() == "exr":
write.knob('compression').setValue("B44")
# Set the 'heroview' to be the first of the selected views. If we don't
# do this then then 'heroview' is by default set to be 1 which may not
# be a valid view for this clip. The 'heroview' is used as a fallback if
# no view has been set on the reader. This assumes the user has selected
# sensible views if they haven't then the write may still fail.
if len(selectedViews) > 0:
firstView = nuke.OutputContext().viewFromName(selectedViews[0])
write.knob('heroview').setValue(firstView)
writeColorspace = ""
if self._burnInLUT.value():
# The user has chosen to burn the viewer transform into the intermedate render so set the colorspace
# on the Write node appropriately.
writeColorspace = self._getBurninWriteColorspace()
else:
# The viewer transform is not being burnt into the intermediate render, set the Write node's colorspace
# to whatever the current working space is - when reading the file back in the flipbook we'll ssume
# the media was written out in the working space.
rootNode = nuke.root()
if rootNode:
workingSpaceKnob = rootNode.knob("workingSpaceLUT")
if workingSpaceKnob:
writeColorspace = workingSpaceKnob.value()
if writeColorspace:
write.knob('colorspace').setValue(writeColorspace)
outputNode = nuke.createNode("Output", inpanel = False)
#If called on a Viewer connect fixup node to the one immediately above if exists.
if self._node.Class() == "Viewer":
fixup.setInput(0, self._node.input(int(nuke.knob(self._node.fullName()+".input_number"))))
else:
fixup.setInput(0, self._node)
return (fixup, write)
def _getLUT(self):
return self._luts.value()
def _getAudio(self):
nukeNode = nuke.toNode( self._audioSource.value() )
ret = ""
if nukeNode != None:
ret = nukeNode["file"].getEvaluatedValue()
return ret
def _getOptions(self, nodeToFlipbook):
options = {
}
try:
options['pixelAspect'] = float(nuke.value(nodeToFlipbook.name()+".pixel_aspect"))
except:
pass
try:
f = nodeToFlipbook.format()
options['dimensions'] = { 'width' : f.width(), 'height' : f.height() }
except:
pass
# LUT
if not self._burnInLUT.value():
inputColourspace = "linear"
outputColourspace = "linear"
# Check if we have a different than linear input
if self._node.Class() == "Read" or self._node.Class() == "Write":
lut = self._node.knob("colorspace").value()
# Might be in the format of "default (foo)", if so, get at "foo".
if lut[:7] == "default":
lut = lut[9:-1]
inputColourspace = lut
# Check our output
lut = self._getLUT()
if lut != "None":
outputColourspace = lut
if inputColourspace == outputColourspace:
options["lut"] = inputColourspace
else:
options["lut"] = inputColourspace + "-" + outputColourspace
# AUDIO
audioTrack = self._getAudio()
if audioTrack != "":
options["audio"] = audioTrack
# ROI
if self._useRoi.value():
roi = self._roi.toDict()
if (roi["r"] - roi["x"] > 0) and (roi["t"] - roi["y"] > 0):
options["roi"] = bboxToTopLeft(int(nuke.value(nodeToFlipbook.name()+".actual_format.height")), roi)
return options
def run(self):
flipbookToRun = flipbooking.gFlipbookFactory.getApplication(self._flipbookEnum.value())
if (flipbookToRun):
if not os.access(flipbookToRun.path(), os.X_OK):
raise RuntimeError("%s cannot be executed (%s)." % (flipbookToRun.name(), flipbookToRun.path(),) )
nodeToFlipbook = None
rootProxyMode = nuke.root().proxy()
try:
# Need this to prevent Bug 5295
nuke.Undo().disable()
nuke.root().setProxy(self._useProxy.value())
calledOnNode = self._node
if self._node.Class() == "Viewer":
self._node = self._node.input(int(self._node.knob("input_number").value()))
renderer = getFlipbookRenderer(self, flipbookToRun)
renderer.doFlipbook()
except Exception as e:
import traceback
traceback.print_exc(file=sys.stdout)
print("exception ",e)
finally:
#if an intermediate node was created, delete it
if self._node != renderer._nodeToFlipbook:
nuke.delete(renderer._nodeToFlipbook)
nuke.root().setProxy(rootProxyMode)
nuke.Undo().enable()
else:
raise RuntimeError("No flipbook called " + self._flipbookEnum.value() + " found. Was it deregistered while the dialog was open?")
[docs]class ViewerCaptureDialog( FlipbookDialog ):
def _titleString( self ):
return "Capture"
def _idString( self ):
return "uk.co.thefoundry.ViewerCaptureDialog"
def __init__(self, dialogState, groupContext, node):
# init super
FlipbookDialog.__init__(self, dialogState, groupContext, node, True)
self._frameserverRender.setVisible(False)
self._bgRender.setVisible(False)
self._useProxy.setVisible(False)
self._continueOnError.setVisible(False)
self._viewerForSettings.setVisible(False)
self._useRoi.setVisible(False)
self._roi.setVisible(False)
self._channels.setVisible(False)
self._luts.setVisible(False)
self._audioSource.setVisible(False)
self._burnInLUT.setVisible(False)
try:
## this may not exist, just ignore if not present
self._viewSelection.setVisible(False)
except:
pass
customWriteActive = node['file'].getValue() != self._getIntermediatePath() and node['file'].getValue() != ''
self._customWrite = nuke.Boolean_Knob( 'custom', 'Customise write path' )
self._customWrite.setValue( customWriteActive )
self.addKnob( self._customWrite )
self._noFlipbook = nuke.Boolean_Knob( 'write', 'No flipbook' )
self._noFlipbook.setFlag( nuke.STARTLINE )
self._noFlipbook.setVisible( customWriteActive )
self._noFlipbook.setValue( self._state.get( self._noFlipbook, False ) )
self.addKnob( self._noFlipbook )
self._file = nuke.File_Knob( 'file', 'Write path' )
defaultPath = self._node['file'].value()
if defaultPath == self._getIntermediatePath():
defaultPath = ''
self._file.setValue ( self._state.get(self._file, defaultPath ) )
self._file.setVisible( customWriteActive )
self.addKnob( self._file )
def knobChanged( self, knob ):
try:
FlipbookDialog.knobChanged(self, knob)
self._frameserverRender.setVisible(False)
if (knob == self._customWrite):
self._noFlipbook.setVisible( self._customWrite.value() )
self._file.setVisible( self._customWrite.value() )
if ( not self._customWrite.value() ):
self._node['file'].setValue( self._getIntermediatePath() )
elif( knob == self._noFlipbook ):
self._flipbookEnum.setEnabled( not self._noFlipbook.value() )
except:
pass
def _getIntermediateFileType(self):
return 'jpg'
[docs] def captureViewer(self):
""" Return an instance of a CaptureViewer class, which when executed captures the viewer.
"""
flipbook = flipbooking.gFlipbookFactory.getApplication(self._flipbookEnum.value())
if flipbook and not os.access(flipbook.path(), os.X_OK):
raise RuntimeError("%s cannot be executed (%s)." % (flipbook.name(), flipbook.path(),) )
# build up the args
frameRange = self._frameRange.value()
viewer = self._node
selectedViews = self._selectedViews()
defaultWritePath = self._getIntermediatePath()
customWritePath = self._file.value() if self._customWrite.value() else ""
doFlipbook = not self._noFlipbook.value()
doCleanup = False
return captureViewer.CaptureViewer(flipbook, frameRange, viewer, selectedViews, defaultWritePath, customWritePath, doFlipbook, doCleanup)
def _showDialog( dialog ):
""" Shows the with showModalDialog() and then calls dialog.run() if it returns True """
# If there is a viewer playing, stop it, it interferes with updating knobs on the dialog
if nuke.activeViewer():
nuke.activeViewer().stop()
if (dialog.showModalDialog() == True):
dialog.run()
[docs]def showExecuteDialog(nodesToExecute, exceptOnError = True):
"""Present a dialog that executes the given list of nodes."""
groupContext = nuke.root()
d = ExecuteDialog(_gRenderDialogState, groupContext, nodesToExecute, exceptOnError)
_showDialog(d)
[docs]def showRenderDialog(nodesToRender, exceptOnError = True, allowFrameServer = True):
"""Present a dialog that renders the given list of nodes."""
groupContext = nuke.root()
d = RenderDialog(_gRenderDialogState, groupContext, nodesToRender, exceptOnError, allowFrameServer)
_showDialog(d)
def _getFlipbookDialog(node, takeNodeSettings = False):
"""Returns the flipbook dialog object created when flipbooking node"""
if node is None:
raise RuntimeError("Can't launch flipbook, require a node.");
if node.Class() == "Viewer" and node.inputs() == 0:
raise RuntimeError("Can't launch flipbook, there is nothing connected to the viewed input.");
if not (nuke.canCreateNode("Write")):
nuke.message("Flipbooking is not permitted in Nuke Assist")
return
groupContext = nuke.root()
e = FlipbookDialog(_gFlipbookDialogState, groupContext, node, takeNodeSettings)
return e
[docs]def showFlipbookDialog(node, takeNodeSettings = False):
"""Present a dialog that flipbooks the given node."""
e = _getFlipbookDialog( node, takeNodeSettings )
_showDialog(e)
# because the capture button could have been triggered via a progress bar in update_handles
# we defer the capture run until the main event loop is pumped
[docs]class ViewerCaptureDialogThread(Thread):
def __init__(self, captureViewer):
Thread.__init__(self)
self.captureViewer = captureViewer
[docs] def run(self):
## only runs the dialog capture function when we are outside update_handles
executeInMainThreadWithResult( self.captureViewer, )
[docs]def showViewerCaptureDialog(node):
if node is None:
raise RuntimeError("Can't launch flipbook, requires a viewer node.");
if node.Class() != "Viewer" and node.inputs() == 0:
raise RuntimeError("Can't launch flipbook, this is not a viewer node.");
if not (nuke.canCreateNode("Write")):
nuke.message("Viewer capture is not permitted in Nuke Assist")
return
groupContext = nuke.root()
e = ViewerCaptureDialog(_gViewerCaptureDialogState, groupContext, node)
if (e.showModalDialog() == True):
# Bug 38516 - Be careful about what gets passed to the ViewerCaptureDialogThread, since anything
# created here will be destroyed in a different thread. This can cause problems, such as crashes,
# if a Qt widget gets passed.
captureViewer = e.captureViewer()
thread = ViewerCaptureDialogThread(captureViewer)
thread.start()
[docs]def showFlipbookDialogForSelected():
"""Present a dialog that flipbooks the currently selected node."""
try:
showFlipbookDialog(nuke.selectedNode())
except ValueError as ve:
raise RuntimeError("Can't launch flipbook, %s." % (ve.args[0]))
[docs]def bboxToTopLeft(height, roi):
"""Convert the roi passed from a origin at the bottom left to the top left.
Also replaces the r and t keys with w and h keys.
@param height: the height used to determine the top.
@param roi: the roi with a bottom left origin, must have x, y, r & t keys.
@result dict with x, y, w & h keys"""
topLeftRoi = {
"x": roi["x"],
"y": height - roi["y"] - (roi["t"] - roi["y"]),
"w": roi["r"] - roi["x"],
"h": roi["t"] - roi["y"] }
return topLeftRoi
[docs]def setRenderDialogDefaultOption(name, value):
""" Set a particular option to the given value. The type of the value differs per option, giving the wrong value may result in exceptions. The options are read every time the dialog is opened, though not every knob in the dialog has it's value stored."""
_gRenderDialogState.saveValue(name, value)
[docs]def setFlipbookDefaultOption(name, value):
""" Set a particular option to the given value. The type of the value differs per option, giving the wrong value may result in exceptions. The options are read every time the dialog is opened, though not every knob in the dialog has it's value stored."""
_gFlipbookDialogState.saveValue(name, value)