怎么创建roto 图形和 画笔
当获取或者设置roto,rotopaint节点时,需要读取节点的curves knob
rpNode = nuke.toNode('RotoPaint1')
cKnob = rpNode['curves']
读取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的动画轨迹来绘制画笔,将其可视化。
准备阶段,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())))
路径控制
这个基本上是在线版的trackShape,其使用python代码将Transform节点的translate knob链接到给定的图元。那么用户的knob就可以
沿着路径给transform定位了。
path控制着Transform沿着RotoPaint节点中的Brush1移动的比例。需要将python代码放入translate的knob来实现:
在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动画。
确保viewer中的label points勾选了,这就能看到CV点的标号了,这能帮助识别你想创建Tracker的那个。
确认选中了节点图中的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
记得导入小面板代码,修改其参数,就不用上面硬编码了:
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)