Nuke Python roto

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

rpNode = nuke.toNode('RotoPaint1')
cKnob = rpNode['curves']
_images/rotoPaintPrimer_01.png
_images/rotoPaintPrimer_01.png

读取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的动画轨迹来绘制画笔,将其可视化。


_images/paintTrajectory_01.png
_images/paintTrajectory_01.png

准备阶段,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())))
_images/paintTrajectory_02.png
_images/paintTrajectory_02.png

trackShape_01
trackShape_01
路径控制

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

animPath_01
animPath_01

animPath_02
animPath_02

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

animPath_03
animPath_03

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动画。

trackCV_01
trackCV_01

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

trackCV_02
trackCV_02

确认选中了节点图中的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的小面板,提供图元名字,点编号,帧范围。


shapeAndCVPanel_01
shapeAndCVPanel_01

怎么做小面板请看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)
trackCV_03
trackCV_03

trackCV_04
trackCV_04
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 使用下文描述的nuke.add...()函数,当有变量事件(比如,创建节点,加载脚本)时就自动调用python函数...
    N景波阅读 2,669评论 0 1
  • 在nuke中有好几种创建自定义panel的方法: 简单的panel命令 -- 常用任务,比如让用户确认或者获取文件...
    N景波阅读 3,267评论 3 3
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,104评论 5 13
  • 《心理罪》上线,作品在市场中呈现两种声音,目前据网络反馈收集,好评与差评都能听到声音。通过艺恩票房可以看到作品上映...
    宫骁葵阅读 606评论 0 1
  • 秋天到了,不少朋友出现感冒、咳嗽、腹泻等症状。出现这种情况大部分是由于身体在做排寒排湿工作,清走外邪,打通经络,以...
    逗逼小帽爱姜茶阅读 433评论 1 1