【NUKE教程】Nuke Python Roto

10 九月, 2016
25
0

nukepyroto
怎么创建roto 图形和 画笔
当获取或者设置roto,rotopaint节点时,需要读取节点的curves knob

rpNode = nuke.toNode('RotoPaint1')
cKnob = rpNode['curves']

Nuke Python roto

读取root层:

	
root = cKnob.rootLayer

每层中curves 的knob是一个迭代对象,其产生每层的成员:

for shape in root:
    print shape.name

使用曲线的knob的toElement方法,通过名字读取层。

for shape in cKnob.toElement('Layer1'):
    print shape.name
# Result:
Layer1
Brush2
Bezier1

用同样的方法来读取图形中的控制点和画笔:

for p in cKnob.toElement('Layer1/Brush1'):
    print p
# Result:
Brush1
Bezier2

curve knob有三种类型的对象:

    • shapes 描述Beziers 和B样条
    • strokes 描述画笔
    • layers 描述不同的层

想创建新的画笔和层,需要导入RotoPaint的api,使用别名来简化使用

import nuke.rotopaint as rp

类 Shape,Stroke,Layer,ShapeControlPoint,AnimControlPoints,还有更多需要使用python来创建RotoPaint元素的。

例子:

paintTrajectory

这段代码 沿着Array_knob的动画轨迹来绘制画笔,将其可视化。
Nuke Python roto

准备阶段,Transform节点的translate knob设置一些关键帧,它就可以沿着屏幕运动。在脚本编辑器中,给knob赋值,并赋值一个帧范
围(比如1-100):

knob = nuke.toNode('Transform1')['translate']
frameRange = nuke.FrameRange('1-100')

使用的knob至少有两个域,可以提供x,y。因此快速检测下:

if knob.arraySize() != 2:
    raise TypeError, 'knob must have array size of 2'

如果knob有效,就抓取其父节点,创建RotoPaint节点,引用其curves knob:

parentNode = knob.node()
paintNode = nuke.createNode('RotoPaint')
curvesKnob = paintNode['curves']

需要使用到nuke.rotopaint模块:

	
import nuke.rotopaint as rp

使用Stroke类创建一个画笔:

	
stroke = rp.Stroke(curvesKnob)

下一步,遍历所有帧,抓取knob的值:

for f in frameRange:
    pos = knob.valueAt(f)

如果knob的父节点有center knob,或许要偏移下这个值,保证stroke落在轨迹上,获取这个偏移量吧:

try:
    offset = parentNode['center'].valueAt(f)
except NameError:
    offset = (0, 0)

给新控制点计算stroke的x,y

	
finalPos = [sum(p) for p in zip(pos, offset) ]

用RotoPaint模块的AnimControlPoint创建新的控制点,并计算x,y位置。然后新控制点添加给stroke:

	
stroke.append( rp.AnimControlPoint(*finalPos))

给stroke个新名字,可以在curves的knob里面显示,最后,将其添加到root层:

stroke.name = 'trajectory for %s.%s' %(parentNode.name(), knob.name() )
curvesKnob.rootLayer.append(stroke)

目前所有的代码:

import nuke.rotopaint as rp
knob = nuke.toNode('Transform1')['translate']
frameRange = nuke.FrameRange('1-100')
if knob.arraySize() != 2:
    raise TypeError, 'knob must have array size of 2'
parentNode = knob.node()
paintNode = nuke.createNode('RotoPaint')
curvesKnob = paintNode['curves']
stroke = rp.Stroke(curvesKnob)
for f in frameRange:
    pos = knob.valueAt(f)
    try :
        # IF PARENT NODE HAS "CENTER" KNOB ADD THE OFFSET TO LINE UP STROKE PROPERLY
        offset = parentNode['center'].valueAt(f)
    except NameError:
        # OTHERWISE NO OFFSET IS APPLIED
        offset =(0, 0)
    finalPos = [ sum(p) for p in zip(pos, offset) ]
    stroke.append(rp.AnimControlPoint(*finalPos))
stroke.name = 'trajectory for %s.%s' %(parentNode.name(), knob.name())
curvesKnob.rootLayer.append(stroke)

调用这段代码的好地方应该是animation menu,这样就可以从对应knob的动画菜单直接调用。将代码包装成函数,并接收knob和
frame range作为参数:

def paintTrajectory(knob, frameRange):
    if knob.arraySize() != 2:
        raise TypeError, 'knob must have array size of 2'
    parentNode = knob.node()
    paintNode = nuke.createNode('RotoPaint')
    curvesKnob = paintNode['curves']
    stroke = rp.Stroke(curvesKnob)
    ctrlPoints = []
    for f in frameRange:
        pos = knob.valueAt(f)
        try :
            # IF PARENT NODE HAS "CENTER" KNOB ADD THE OFFSET TO LINE UP STROKE PROPERLY
            offset = parentNode['center'].valueAt(f)
        except NameError:
            # OTHERWISE NO OFFSET IS APPLIED
            offset =(0, 0)
        finalPos = [ sum(p) for p in zip(pos, offset) ]
        stroke.append(rp.AnimControlPoint(*finalPos))
    stroke.name = 'trajectory for %s.%s' %(parentNode.name(), knob.name())
    curvesKnob.rootLayer.append(stroke)

现在创建一个辅助函数,从knob获取帧范围,这样,用户就不用自己输入了。我们这样做的,遍历knob的动画曲线,获取其帧范围。
首先,初始化FrameRanges对象,保存所有knob的帧范围:

def getKnobRange(knob):
        allRanges = nuke.FrameRanges()

下一步,遍历curves,创建帧范围。如果没有找到关键帧,那么曲线可能是表达式定义的,那么就利用脚本的范围。一旦有了第一帧和
最后一帧,创建帧对象,并添加到帧范围里面:

for anim in knob.animations():
    if not anim.keys():
        first = nuke.root().firstFrame()
        last = nuke.root().lastFrame()
        allRanges.add(nuke.FrameRange(first, last))
    allKeys  = anim.keys()
    allRanges.add(nuke.FrameRange( allKeys[0].x, allKeys[-1].x, 1))

所有范围收集完成后,使用FrameRanges.minFrame(), FrameRanges.maxFrame()获取最小,最大帧。

	
return nuke.FrameRange( allRanges.minFrame(), allRanges.maxFrame(), 1)

所有代码如下:

import nuke
import nuke.rotopaint as rp
def getKnobRange( knob ):
    '''
    Return a frame range object of the knob's animation range.
    If the knob has no keyframes the script range is returned
    args:
       knob - animated knob
    '''
    allRanges = nuke.FrameRanges()
    for anim in knob.animations():
        if not anim.keys():
            #KNOB ONLY HAS EXPRESSION WITHOUT KEYS SO USE SCRIPT RANGE
            first = nuke.root().firstFrame()
            last = nuke.root().lastFrame()
            allRanges.add( nuke.FrameRange( first, last ) )
        else:
            # GET FIRST FRAME
            allKeys = anim.keys()
            allRanges.add( nuke.FrameRange(  allKeys[0].x, allKeys[-1].x, 1 ) )
    return nuke.FrameRange( allRanges.minFrame(), allRanges.maxFrame(), 1 )
def paintTrajectory( knob, frameRange ):
    '''
    Create a paint stroke that visualises a knob's animation path
    args:
        knob - Array knob with 2 fields. Presumably this is a XY_Knob but can be any
        frameRange - Range for which to draw the trajectory.
                     This is an iterable object containing the requested frames.
                     Default is current script range
    '''
    if knob.arraySize() != 2:
        raise TypeError, 'knob must have array size of 2'
    parentNode = knob.node()
    paintNode = nuke.createNode('RotoPaint')
    curvesKnob = paintNode['curves']
    stroke = rp.Stroke( curvesKnob )
    ctrlPoints = []
    for f in frameRange:
        pos = knob.valueAt( f )
        try :
            # IF PARENT NODE HAS "CENTER" KNOB ADD THE OFFSET TO LINE UP STROKE PROPERLY
            offset = parentNode['center'].valueAt( f )
        except NameError:
            # OTHERWISE NO OFFSET IS APPLIED
            offset = ( 0, 0 )
        finalPos = [ sum(p) for p in zip( pos, offset ) ]
        stroke.append( rp.AnimControlPoint( *finalPos ) )
    stroke.name = 'trajectory for %s.%s' % ( parentNode.name(), knob.name() )
    curvesKnob.rootLayer.append( stroke )

两个函数准备好了,现在就可运行来绘制动画路径了:

knob = nuke.toNode('Transform1')['translate']
paintTrajectory(knob,getKnobRange(knob))

就像上面提到的,使用这段代码的最好地方是在动画菜单里面 nuke.thisKnob()

import examples
nuke.menu('Animation').addCommand('Paint Trajectory', lambda: examples.paintTrajectory(nuke.thisKnob(),
examples.getKnobRange(nuke.thisKnob())))

Nuke Python roto
Nuke Python roto

路径控制

这个基本上是在线版的trackShape,其使用python代码将Transform节点的translate knob链接到给定的图元。那么用户的knob就可以
沿着路径给transform定位了。

Nuke Python roto
Nuke Python roto

path控制着Transform沿着RotoPaint节点中的Brush1移动的比例。需要将python代码放入translate的knob来实现:
Nuke Python roto

x 表达式中代码如下:

try:
   shape = nuke.toNode('RotoPaint1')['curves'].toElement('Brush1').evaluate(nuke.frame())
except:
   pass
ret = shape.getPoint(nuke.thisNode()['path'].value()).x

y中代码如下:

try:
   shape = nuke.toNode('RotoPaint1')['curves'].toElement('Brush1').evaluate(nuke.frame())
except:
   pass
ret = shape.getPoint(nuke.thisNode()['path'].value()).y

曲线是三次曲线,图形请看上图。
nuke脚本nuke script

trackCV

下面代码给图形的控制点创建了个Tracker节点。首先创建Roto节点,在其中画一条贝塞尔曲线,并K动画。
Nuke Python roto

确保viewer中的label points勾选了,这就能看到CV点的标号了,这能帮助识别你想创建Tracker的那个。
Nuke Python roto

确认选中了节点图中的Roto节点,通过抓取选中节点启动脚本。

	
node = nuke.selectedNode()

选定你要跟踪的帧范围,图元的名字,点编号。下面是硬编码,后续可以做一个界面:

fRange = nuke.FrameRange('1-100')
shapeName = 'Bezier1'
cv = 0

脚本运行时,想看到tracker的创建,那么就需要在另一个线程里面做这个工作了。这个函数是cvTracker其参数如下:

  • node Roto节点
  • shapeName 包含控制点的图元
  • cvID 要跟踪的控制点序号
  • fRange 跟踪的帧范围

下面代码启动另一个线程调用此函数:

	
threading.Thread(None, _cvTracker, args=(node, shapeName, cv, fRange)).start()

到现在为止,代码如下:

import nuke.rotopaint as rp
node = nuke.selectedNode()
fRange = nuke.FrameRange('1-100')
shapeName = 'Bezier1'
cv = 0
threading.Thread(None, _cvTracker, args=(node, shapeName, cv, fRange)).start()

显示实现这个函数:

def _cvTracker(node, shapeName, cvID, fRange):
    shape = node['curves'].toElement(shapeName)

使用toElement方法,能通过名字获取图元并能定位我们索引的点,例子中,点编号为0:

shapePoint = shape[cvID]

添加点错误处理,防止索引的点不存在:

try:
    shapePoint = shape[cvID]
except IndexError:
    nuke.message('Index %s not found in %s.%s' %())
    return

变量shapePoint中保存的ShapeControlPoint保存了所有的属性,main和feature曲线的邻接关系,中心点。我们仅想跟踪
main 曲线的中心点,获取代码如下:

animPoint = shapePoint.center

animPoint提供了x,y坐标,创建一个Tracker节点来保存动画:

	
tracker = nuke.createNode('Tracker3')

给一个提示标签,让track1的knob接收动画:

tracker['label'].setValue('tracking cv#%s in %s.%s' %(cvID, node.name(), shape.name))
trackerKnob = tracker['track1']
trackerKnob.setAnimated()

在做跟踪前,设置一个进度条,允许用户取消进度:

task = nuke.ProgressTask('CV Tracker')
task.setMessage('tracking CV'

现在遍历请求的帧。循环中我们会检测用户是否点击了进度条上的Cancle

for f in fRange:
    if task.isCancelled():
        nuke.executeInMainThread(nuke.message, args=("CV Track Cancelled"))
        break

下一步设置处理的进度:

	
task.setProgress(int(float(f)/fRange.last() * 100))

现在可以做具体的跟踪工作了。获取循环中对应帧的AnimationControlPoint

pos = animPoint.getPosition(f)

最后,给trackerknob设置新位置。我们在主线程中做这个,脚本运行时能看到关键帧的生成:

nuke.executeInMainThreadWithResult(trackerKnob.setValueAt, args=(pos.x, f, 0)) # SET X VALUE
nuke.executeInMainThreadWithResult(trackerKnob.setValueAt, args=(pos.y, f, 1)) # SET Y VALUE

整个函数如下:

def _cvTracker(node, shapeName, cvID, fRange):
    shape = node['curves'].toElement(shapeName)
    # SHAPE CONTROL POINT
    try:
        shapePoint = shape[cvID]
    except IndexError:
        nuke.message('Index %s not found in %s.%s' %())
        return
    # ANIM CONTROL POINT
    animPoint = shapePoint.center
    # CREATE A TRACKER NODE TO HOLD THE DATA
    tracker = nuke.createNode('Tracker3')
    tracker['label'].setValue('tracking cv#%s in %s.%s' %(cvID, node.name(), shape.name))
    trackerKnob = tracker['track1']
    trackerKnob.setAnimated()
    # SET UP PROGRESS BAR
    task = nuke.ProgressTask('CV Tracker')
    task.setMessage('tracking CV')
        # DO THE WORK
    for f in fRange:
        if task.isCancelled():
            nuke.executeInMainThread(nuke.message, args=("CV Track Cancelled"))
            break
        task.setProgress(int(float(f)/fRange.last() * 100))
                # GET POSITION
        pos = animPoint.getPosition(f)
        nuke.executeInMainThreadWithResult(trackerKnob.setValueAt, args=(pos.x, f, 0))
        nuke.executeInMainThreadWithResult(trackerKnob.setValueAt, args=(pos.y, f, 1))

注意: 不要在主线程中运行此函数,因为 nuke.executeInMainThreadWithResult会让Nuke卡死。

为了让程序交互性更好,做一个python的小面板,提供图元名字,点编号,帧范围。
Nuke Python roto

怎么做小面板请看ShapeAndCVPanel
记得导入小面板代码,修改其参数,就不用上面硬编码了:

import examples
node = nuke.selectedNode()
p = examples.ShapeAndCVPanel(node)
if p.showModalDialog():
    fRange = nuke.FrameRange(p.fRange.value())
    shapeName = p.shape.value()
    cv = p.cv.value()
    threading.Thread(None, _cvTracker, args=(node, shapeName, cv, fRange)).start()

将代码封装一下,供Properties 属性菜单上的右键使用。同样会添加错误处理代码保证选取的节点时Roto或者RotoPaint:

def trackCV():
    node = nuke.selectedNode()
    # BAIL OUT IF THE NODE IS NOT WHAT WE NEED
    if node.Class() not in ('Roto', 'RotoPaint'):
        nuke.message('Unsupported node type. Node must be of class Roto or RotoPaint')
        return
    p = examples.ShapeAndCVPanel(node)
    if p.showModalDialog():
        fRange = nuke.FrameRange(p.fRange.value())
        shapeName = p.shape.value()
        cv = p.cv.value()
        threading.Thread(None, _cvTracker, args=(node, shapeName, cv, fRange)).start()

最终代码:

import examples
import nuke
import nukescripts
import threading
def _cvTracker( node, shapeName, cvID, fRange ):
    shape = node['curves'].toElement( shapeName )
    # SHAPE CONTROL POINT
    try:
        shapePoint = shape[cvID]
    except IndexError:
        nuke.message( 'Index %s not found in %s.%s' % (  ) )
        return
    # ANIM CONTROL POINT
    animPoint = shapePoint.center
    # CREATE A TRACKER NODE TO HOLD THE DATA
    tracker = nuke.createNode( 'Tracker3' )
    tracker['label'].setValue( 'tracking cv#%s in %s.%s' % ( cvID, node.name(), shape.name ) )
    trackerKnob = tracker['track1']
    trackerKnob.setAnimated()
    # SET UP PROGRESS BAR
    task = nuke.ProgressTask( 'CV Tracker' )
    task.setMessage( 'tracking CV' )
    # DO THE WORK
    for f in fRange:
        if task.isCancelled():
            nuke.executeInMainThread( nuke.message, args=( "CV Track Cancelled" ) )
            break
        task.setProgress( int( float(f)/fRange.last() * 100 ) )
        # GET POSITION
        pos = animPoint.getPosition( f )
        nuke.executeInMainThreadWithResult( trackerKnob.setValueAt, args=( pos.x, f, 0 ) ) # SET X VALUE
        nuke.executeInMainThreadWithResult( trackerKnob.setValueAt, args=( pos.y, f, 1 ) ) # SET Y VALUE
def trackCV():
    # GET THE SELECTED NODE. SINCE WE PLAN ON CALLING THIS FROM THE PROPERTIES MENU
    # WE CAN BE SURE THAT THE SELECTED NODE IS ALWAYS THE ONE THE USER CLICKED IN
    node = nuke.selectedNode()
    # BAIL OUT IF THE NODE IS NOT WHAT WE NEED
    if node.Class() not in ('Roto', 'RotoPaint'):
        nuke.message( 'Unsupported node type. Node must be of class Roto or RotoPaint' )
        return
    p = examples.ShapeAndCVPanel( node )
    if p.showModalDialog():
        fRange = nuke.FrameRange( p.fRange.value() )
        shapeName = p.shape.value()
        cv = p.cv.value()
        threading.Thread( None, _cvTracker, args=(node, shapeName, cv, fRange) ).start()

添加到Properties右键菜单的代码:

nuke.menu('Properties').addCommand('Track CV', examples.trackCV)

Nuke Python roto
Nuke Python roto