【NUKE教程】Nuke Python 自定义Panel

10 九月, 2016
17
0

nuke_Panel
在nuke中有好几种创建自定义panel的方法:

  • 简单的panel命令 – 常用任务,比如让用户确认或者获取文件名
  • 简单panel对象 – 定制某些扩展,创建简单。如果需要一个简单的定制panel,可以添加大量knob
  • python panel – 创建更加复杂,提供node中所有的knob,callback,部分layout控制

简单panel命令

nuke有大量的简单panel,不用繁琐的定制就能使用。

    • 消息框
      nuke.message(‘Just saying hi’)
      Nuke Python 自定义Panel
    • 用户查询 给用户一个“yes、no”选择:
if nuke.ask('Are you sure you want to create  a Blur node? '):
        nuke.createNode('Blur')

Nuke Python 自定义Panel

    • 显示窗口
def showChannels():
    return '\n'.join( nuke.thisNode().channels() )
node = nuke.selectedNode()
nuke.display('showChannels()', node, 'show channels for %s' % node.name() )

这是个非模式panel,运行showChannels,用其结果来填充窗口。node作为第二参数。

Nuke Python 自定义Panel

    • 获取用户输入-从用户获取输入并赋给所选节点的label
txt = nuke.getInput('Change label', 'new label')
if txt:
    for n in nuke.selectedNodes():
        n['label'].setValue(txt)

Nuke Python 自定义Panel

    • 颜色选择:给选定节点赋予指定颜色
col = nuke.getColor()
if col:
        for n in nuke.selectedNodes():
            n['title_color'].setValue(col)
            n['gl_color'].setValue(col)

Nuke Python 自定义Panel

    • 文件浏览,返回文件全路径:
	
filePath = nuke.getFilename('get file contents', '*.txt *.xml')

Nuke Python 自定义Panel

    • 序列浏览器 列出图像序列
	
seqPath = nuke.getClipname('get sequence')

Nuke Python 自定义Panel

    • 获取帧范围,从用户那获取帧范围,如果是立体的,则附带在哪个view操作:
      ret = nuke.getFramesAndViews(‘get range’, ‘1-10’ )
      返回值是字符串的list,和要求的view:
ret = nuke.getFramesAndViews('get range', '1-10')
range = ret[0]
views = ret[1]
print 'range is', range
print 'views are', views

Nuke Python 自定义Panel 多视图脚本: Nuke Python 自定义Panel

简单panel对象

创建一个简单panel对象: p = nuke.Panel(‘my custom panel’) 检查哪些knob可用是个好主意: dir(p) 添加knobs:

p.addClipnameSearch('clip path', '/tmp')
p.addFilenameSearch('file path', '/tmp')
p.addTextFontPulldown('font browser', '/myFonts/')
p.addRGBColorChip('some pretty color', '')
p.addExpressionInput('enter an expression', '4*25')
p.addBooleanCheckBox('yes or no?', True)
p.addEnumerationPulldown('my choices', 'A B C')
p.addScriptCommand('tcl or python code', '')
p.addSingleLineInput('just one line', 'not much space')
p.addMultilineTextInput('multiple lines of user input text', 'lineA\nlineB')
p.addNotepad('write something', 'some very long text could go in here. For now this is just some random default value')
p.addPasswordInput('password', 'donttellanyone')
p.addButton('push here')
p.addButton('or here')
p.addButton('or even here')

打开panel:
ret = p.show()

Nuke Python 自定义Panel

panel关闭后,获取某个值:
print p.value(‘clip path’)
pirnt p.value(‘file path’)

python panel

shapePanel

有两个下拉菜单的panel,第一个knob控制第二个knob shape的显示

Nuke Python 自定义Panel
Nuke Python 自定义Panel

开始,创建一个继承nukescripts.PythonPanels的类,运行其构造并传入一个标题

class ShapePanel( nukescripts.PythonPanel):
        def __init__(self):
                nukescripts.PythonPanel.__init__(self, 'RotoPaint Elements')

用参数传递我们想分析的节点,在init中的参数列表中,赋值给self.rpNode:

class ShapePanel( nukescripts.PythonPanel):
    def __init__(self, node):
        nukescripts.PythonPanel.__init__(self, 'RotoPaint Elements')
        self.rpNode = node

使用nuke.Enumeration_Knob创建下拉列表:
self.typeKnob = nuke.Enumeration_Knob(‘element’, ‘element’, [‘Shape’, ‘Strokes’])

再一次,在全局命名空间中引用这个knob的值,读取so easy。提供给knob的三个参数是:

  • element knob的对象名字
  • element knob的标签
  • [‘Shapes’, ‘Strokes’] 下拉菜单中显示的选项

创建第二个enumeration knob, 但其选项设置为空, 动态设置:
self.elementKnob = nuke.Enumeration_Knob(‘curve’, ‘curve’, [])

现在添加两个knob:
for k in ( self,typeKnob, self.elementKnob):
self.addKnob(k)

目前的代码:

class ShapePanel( nukescripts.PythonPanel):
    def __init__(self, node):
        nukescripts.PythonPanel.__init__(self, 'RotoPaint Elements')
        self.rpNode = node
        self.typeKnob = nuke.Enumeration_Knob('element', 'element', ['Shapes', 'Strokes'])
        self.elementKnob = nuke.Enumeration_Knob('curve', 'curve', [])
        for k in (self.typeKnob, self.elementKnob):
            self.addKnob(k)

如果你创建了这个类的例子,可以用showModalDialog()将其作为模式对话框打开。创建一个RotoPaint节点,名为RotoPaint1.

node = nuke.toNode('RotoPaint1')
p = ShapePanel(node)
p.showModalDialog()

Nuke Python 自定义Panel

注意showModalDialog给我了带ok,cancel按钮的对话框。下面将其美化下:

默认情况下,enumeration knob从新一行开始,但是如果我们清除了这个标志位,就可以让两个knob在同一行:

class ShapePanel( nukescripts.PythonPanel):
        def __init__(self, node):
                nukescripts.PythonPanel.__init__(self, 'RotoPaint Elements')
                self.rpNode = node
                self.typeKnob = nuke.Enumeration_Knob('element', 'element/shape', ['Shapes', 'Strokes'])
                self.elementKnob = nuke.Enumeration_Knob('curve', '', [])
                self.elementKnob.clearFlag(nuke.STARTLINE)
                for k in (self.typeKnob, self.elementKnob):
                        self.addKnob(k)

这也改变了标签上的第一个knob,并将第二个上的knob移除,这看上去更加简洁: Nuke Python 自定义Panel 最后一件在构造时要做的事情就是 创建一个字典,按类型保存元素。在init函数最后面添加如下代码: self.curveDict = {} 实际上不真需要,可以init在全局范围内初始化变量, 用节点的shape和stroke来填充字典: def getData( self): self.curveDict={‘Shapes’:[], ‘Strokes’:[] } rootLayer = self.rpNode[‘curves’].rootLayer
1. 用填充第一个enumeration knob的关键字来填充字典(shape,stroke)
2. 抓取node的curve knob,其能列出Roto或者RotoPaint的所有元素,并获取其rootLayer

rootLayer是个可迭代的对象,能产生curve knob中列出的所有元素。因此可以遍历获取其所有孩子元素。检查对应元素的类型,保存进
对应的字典:

for e in rootLayer:
    if isinstance(e, nuke.rotopaint.Shape):
        self.curveDict['Shapes'].append(e.name)
    elif isinstance( e, nuke.rotopaint.Stroke):
        self.curveDict['Stokes'].append( e.name)

用感兴趣的数据填充字典,通过’shape’和‘stroke’保存。在init最后添加如下代码:

class ShapePanel(nukescripts.PythonPanel):
    def __init__(self, node):
    '''List all roto paint nodes and the name of their respective shapes and strokes'''
    nukescripts.PythonPanel.__init__(self, 'RotoPaint Elements')
    self.rpNode = node
    # CREATE KNOBS
    self.typeKnob = nuke.Enumeration_Knob('element', 'element / curve', ['Shapes', 'Strokes'])
    self.elementKnob = nuke.Enumeration_Knob('curve', '', [])
    self.elementKnob.clearFlag(nuke.STARTLINE)
    # ADD KNOBS
    for k in (self.typeKnob, self.elementKnob):
        self.addKnob(k)
    # STORE DICTIONARY OF ELEMENTS PER TYPE
    self.curveDict = {}
    # FILL DICTIONARY
    self.getData()

最后,添加knobChanged方法,每次panel内的knob有变化就触发此函数。knobChanged方法接收一个额外的参数,将正在变化的knob传入
函数内。

来看运行情况,或者调试。当你改变panel内容是,这是打印knob名字的好方法,并留意脚本编辑器的输出panel:
def knobChanged( self, knob):
print knob.name()
如果你在这步运行panel的代码,注意用knob的showPanel打开panel的触发器 knobChanged的回调函数。
Nuke Python 自定义Panel

如果你换了第一个enumeration knob,你将看到它的名字会被打印到输出panel上。
现在,用它干点有用的事。我们已经有一个字典存储了所有的shape和stroke,因此我们要做的就是读取并赋予对应的值(基于第一个knob
的当前值):

def knobChanged(self, knob):
    self.elementKnob.setValues(self.curveDict[ self.typeKnob.value() ])

通过typeKnob里面找到的当前值可以从字典里面读取self.elementKnob的值。但是我们仅想panel打开或者typeKnob改变时才做:

def knobChanged(self, knob):
    if knob is self.typeKnob or knob.name()=='showPanel':
        self.elementKnob.setValues(self.curveDict[ self.typeKnob.value() ])

现在可以把第一个enumeration knob从shape转换到stroke,并看到第二个konb自动更新自己的content
最终代码:

import nuke
import nukescripts
class ShapePanel( nukescripts.PythonPanel ):
    def __init__( self, node ):
        '''List all roto paint nodes and the name of their respective shapes and strokes'''
        nukescripts.PythonPanel.__init__( self, 'RotoPaint Elements' )
        self.rpNode = node
        # CREATE KNOBS
        self.typeKnob = nuke.Enumeration_Knob( 'element', 'element / curve', ['Shapes', 'Strokes'] )
        self.elementKnob = nuke.Enumeration_Knob( 'curve', '', [] )
        self.elementKnob.clearFlag( nuke.STARTLINE )
        # ADD KNOBS
        for k in ( self.typeKnob, self.elementKnob ):
            self.addKnob( k )
        # STORE DICTIONARY OF ELEMENTS PER TYPE
        self.curveDict = {}
        # FILL DICTIONARY
        self.getData()
    def getData( self ):
        '''return a nested dictionary of all shapes and strokes per node'''
        self.curveDict={ 'Shapes':[], 'Strokes':[] }
        rootLayer = self.rpNode['curves'].rootLayer
        for e in rootLayer:
            if isinstance( e, nuke.rotopaint.Shape ):
                self.curveDict[ 'Shapes' ].append( e.name )
            elif isinstance( e, nuke.rotopaint.Stroke ):
                self.curveDict[ 'Strokes' ].append( e.name )
    def knobChanged( self, knob ):
        if knob is self.typeKnob or knob.name()=='showPanel':
            self.elementKnob.setValues( self.curveDict[ self.typeKnob.value() ] )

通过showModalDialog模块来调用panel,我们知道用户的决定,任何事情发生前的确认或者取消。用户点ok就返回true,否则返回false。 在全局范围内引用knobs,在panel关闭后也可以读取knob的值。例子:

node = nuke.toNode('RotoPaint1')
p = ShapePanel(node)
if p.showModalDialog():
    print p.elementKnob.value()

当点击ok关闭panel时,就打印出值,否则啥也不发生。

 

ShapeAndCVPanel

这个简单的面板扫描Roto或RotoPaint节点的shape,并将找到的所有shape的name放入下拉列表中。允许输入帧范围,指定CV编号。这是
用来trackCV
Nuke Python 自定义Panel

从导入使用的包开始

import nuke
import nukescripts
import nuke.rotopaint as rp

然后创建一个类继承nukescripts中的PythonPanel,接收一个名为node的参数。调用父类的构造函数给panle一个标题。

class ShapeAndCVPanel(nukescripts.PythonPanel):
def __init__(self, node):
        nukescripts.PythonPanel.__init__(self, 'Get Shape and CV index')

在全局命名空间中引用传给panel的node,方便在其他地方使用。

self.rpNode = node

从node的curvesknob中获取root 层。

	
root = node['curves'].rootLayer

使用列表生成来获取节点中的所有shape name:

shapeNames = [c.name for c in root if isinstance(c, rp.Shape)]

添加快速检测:

if not shapeNames:
    nuke.message('No Shapes found in %s' % node.name()
    return

将knob添加到panel。字符串knob用来接收帧范围:

	
self.fRange = nuke.String_Knob('fRange', 'Track Range')

注意 我们在panel的全局命名空间中引用knob(self.),因此后续可以在panle外访问

String knob接收第三个可选参数,来提供默认值。 此处使用来设置默认值。

self.fRange = nuke.String_Knob('fRange', 'Track Range', '%s-%s' % (nuke.root().fristFrame(), nuke.root().lastFrame()))

接下来就是将名字添加到 enumeration knob中:

	
self.shape = nuke.Enumeration_Knob('shape', 'Shape', shapeNames)

添加一个整数knob让用户输入CV的编号:

self.cv = nuke.Int_Knob('pointNumber', 'Point Number')

检查整数knob,如果有错,需要显示提示信息:

	
self.warning = nuke.Text_Knob('warning', 'invalid index')

使用Html代码让提示更加醒目:

	
self.warning = nuke.Text_Knob('warning', 'invalid index')

静态文本knob挨着pointNumber knob,在此加入一行来分割:

self.warning.clearFlag(nuke.STARTLINE)

打开panle是需要隐藏警告提示的内容:

	
self.warning.setVisible(False)

最后将四个knob添加到panel:

self.addKnob(self.fRange)
self.addKnob(self.shape)
self.addKnob(self.cv)
self.addKnob(self.warning)

想偷懒的可以这么做:

for k in (self.fRange, self.shape, self.cv, self.warning):
        self.addKnob(k)

零碎代码整合起来后如下:

import nuke
import nukescripts
import nuke.rotopaint as rp
class ShapeAndCVPanel(nukescripts.PythonPanel):
    def __init__(self, node):
        nukescripts.PythonPanel.__init__(self, 'Get Shape and CV index')
        self.rpNode  = node
        # GET THE NODES ROOT LAYER AND COLLECT ALL SHAPES IN IT
        root = node['curves'].rootLayer
        shapeNames = [ c.name for c in root if isinstance(c, rp.Shape) ]
        if not shapeNames:
            nuke.message('No Shapes found in %s' % node.name())
            return
        # CREATE KNOBS
        self.fRange = nuke.String_Knob('fRange', 'Track Range', '%s-%s' % (nuke.root().firstFrame(), nuke.root().lastFrame()))
        self.shape = nuke.Enumeration_Knob('shape', 'Shape', shapeNames)
        self.cv = nuke.Int_Knob('pointNumber', 'Point Number')
        self.warning = nuke.Text_Knob('warning', 'invalid index')
        self.warning.clearFlag(nuke.STARTLINE)
        self.warning.setVisible(False)
        # ADD KNOBS
        for k in (self.fRange, self.shape, self.cv, self.warning):
            self.addKnob(k)

为了检测给定点编号是否正确,需要使用knobChanged方法,当knob改变时会自动运行。此方法需要knob参数,其提供了此 功能,并且knob触发了这个回调:

	
def knobChanged(self, knob):

仅需要self.cvself.shape索引的knob变动时调用此函数:

def knobChanged(self, knob):
    if knob in(self.cv, self.shape):

如上发生,就要讲shape的knob设置如下:

	
currentShape = self.rpNode['curves'].toElement(self.shape.value())

我们仅想知道shape中有多少个点:

	
size = len([pt for pt in currentShape])

现在检测pointNumber的值是否在合理范围内: validNumber = -1 < knob.value() < size 如果点在合理范围内,上面的判别式返回true,否则返回false。取非来作为控制warnging knob显示的开关。

	
self.warning.setVisible(not validNumber)

最终代码如下:

import nuke
import nukescripts
import nuke.rotopaint as rp
class ShapeAndCVPanel( nukescripts.PythonPanel ):
    def __init__( self, node ):
        nukescripts.PythonPanel.__init__( self, 'Get Shape and CV index' )
        self.rpNode  = node
        # GET THE NODES ROOT LAYER AND COLLECT ALL SHAPES IN IT
        root = node['curves'].rootLayer
        shapeNames = [ c.name for c in root if isinstance( c, rp.Shape ) ]
        if not shapeNames:
            nuke.message( 'No Shapes found in %s' % node.name() )
            return
        # CREATE KOBS
        self.fRange = nuke.String_Knob( 'fRange', 'Track Range', '%s-%s' % ( nuke.root().firstFrame(), nuke.root().lastFrame() ) )
        self.shape = nuke.Enumeration_Knob( 'shape', 'Shape', shapeNames )
        self.cv = nuke.Int_Knob( 'pointNumber', 'Point Number' )
        self.warning = nuke.Text_Knob( 'warning', 'invalid index' )
        self.warning.clearFlag( nuke.STARTLINE )
        self.warning.setVisible( False )
        # ADD KOBS
        for k in ( self.fRange, self.shape, self.cv, self.warning ):
            self.addKnob( k )
    def knobChanged( self, knob ):
        # IF AN INVALID INDEX IS SHOWN DISPLAY THE WARNING TEXT
        if knob in( self.cv, self.shape ):
            currentShape = self.rpNode['curves'].toElement( self.shape.value() )
            size = len( [pt for pt in currentShape] )
            validNumber = -1 < knob.value() < size
            self.warning.setVisible( not validNumber )

测试panle,创建Roto或RotoPaint节点,新的shape,运行:

	
ShapeAndCVPanel(nuke.selectedNode()).showModalDialog()

panel显示如下:
Nuke Python 自定义Panel

如果点编号不合法显示如下:
Nuke Python 自定义Panel

增加panel的宽度,显示完整的警告信息:

p = ShapeAndCVPanel(nuke.selectedNode())
p.setMinimumSize(400, 50)
p.showModalDialog()

 

搜索和替换面板

下面的代码创建了一个面板,在NUKE脚本中执行搜索和替换操作。其使用了帮助函数search来简化代码。视频教程在Nukepedia.

def search(searchstr, nodes):
    """ Search in nodes with file knobs. """
    fileKnobNodes = [i for i in nodes if __NodeHasKnobWithName(i, 'file')]
    proxyKnobNodes = [i for i in nodes if __NodeHasKnobWithName(i, 'proxy')]
    if not fileKnobNodes and not proxyKnobNodes: raise ValueError, "No file nodes selected"
    nodeMatches = []
    knobMatches = []
    for i in fileKnobNodes:
        if __FindNode(searchstr, i['file']):
            nodeMatches.append(i)
            knobMatches.append(i['file'])
    for i in proxyKnobNodes:
        if __FindNode(searchstr, i['proxy']):
            nodeMatches.append(i)
            knobMatches.append(i['proxy'])
    return nodeMatches, knobMatches

URL是构造函数第二个参数,这能让面板保存或作为自定义布局的一部分再次唤醒:

class SearchReplacePanel(nukescripts.PythonPanel):
    def __init__(self):
        nukescripts.PythonPanel.__init__(self, 'Search and Replace', 'com.ohufx.SearchReplace')
        # CREATE KNOBS
        self.nodesChoice = nuke.Enumeration_Knob('nodes', 'Source Nodes', ['all', 'selected'])
        self.searchStr = nuke.String_Knob('searchStr', 'Search for:')
        self.update = nuke.PyScript_Knob('update', 'Update')
        self.info = nuke.Multiline_Eval_String_Knob('info', 'Found')
        self.info.setEnabled(False)
        self.replaceStr = nuke.String_Knob('replaceStr', 'Replace with:')
        self.replace = nuke.PyScript_Knob('replace', 'Replace')
        # ADD KNOBS
        self.addKnob(self.nodesChoice)
        self.addKnob(self.searchStr)
        self.addKnob(self.update)
        self.addKnob(self.info)
        self.addKnob(self.replaceStr)
        self.addKnob(self.replace)
        self.matches = None
    def search(self, nodes):
        nodeMatches, knobMatches = search(self.searchStr.value(), nodes)
        nodes = [n.name() for n in nodeMatches]
        infoStr = '%s node(s) found:\n\t%s' % (len(nodes), ', '.join(nodes))
        self.info.setValue(infoStr)
        return knobMatches
    def knobChanged(self, knob):
        if knob in (self.searchStr, self.update, self.nodesChoice):
            srcNodes = { 'all': nuke.allNodes(), 'selected': nuke.selectedNodes() }
            self.matches = self.search(srcNodes[self.nodesChoice.value()])
        elif knob is self.replace and self.matches is not None:
            for k in self.matches:
                newStr = re.sub(self.searchStr.value(), self.replaceStr.value(), k.value())
                k.setValue(newStr)

下面是将面板添加到Panel的默认方法(下面代码放入menu.py):

def addSRPanel():
    global srPanel
    srPanel = SearchReplacePanel()
    return srPanel.addToPane()
paneMenu = nuke.menu('Pane')
paneMenu.addCommand('SearchReplace', addSRPanel)
nukescripts.registerPanel('com.ohufx.SearchReplace', addSRPanel)

Nuke Python 自定义Panel