top of page

LINK TOOLS

  • jnietomonco
  • Feb 8, 2023
  • 1 min read

This code was done by Jed Smith and it is really useful to create Roto or Translate nodes linked to a Tracker node. The hotkeys I use for this tool are the ones Jed had it written for: alt+o for a linked Roto, and alt+l for a linked Transform.


ree

If you want you can see the code (and many other amazing ones from Jed) on his GitHub.

Expand Link Tools

import nuke, nukescripts, nuke.rotopaint as rp, _curvelib as cl

'''
## LINK TOOLS
	Utility functions for creating linked nodes. This script can create roto linked to trackers, 
	transform linked to trackers, or cameras linked to each other, or held on a projection frame. 

Installation:
Put link_tools.py somewhere in your nuke path.
You can add the script as commands to your Nodes panel. This code creates a custom menu entry called "Scripts".
I have them set to the shortcuts Alt-O for roto and Alt-L for generalized link (works with tracker, transform, and camera nodes).

import link_tools
nuke.toolbar('Nodes').addMenu('Scripts').addCommand('Link Roto', 'link_tools.link("roto")', 'alt+o')
nuke.toolbar('Nodes').addMenu('Scripts').addCommand('Link Tool', 'link_tools.link()', 'alt+l')
'''



def link_transform(target_node):
	"""
	Utility function for link_transform: creates linked transform node
	"""
	target_name = target_node.name()
	nclass = target_node.Class()
	if "Tracker" not in nclass and "Transform" not in nclass:
		print "Must select Tracker or Transform node!"
		return
	grid_x = int(nuke.toNode('preferences').knob('GridWidth').value())
	grid_y = int(nuke.toNode('preferences').knob('GridHeight').value())
	target_node.setSelected(False)
	# Create linked Transform Node
	trans = nuke.nodes.Transform()
	trans.setName('TransformLink')
	trans.setSelected(True)
	trans.knob('help').setValue("<b>TransformLink</b>\n\nA Transform node with options for linking to a Tracker or a Transform node. \n\nAllows you to set a seperate identity transform frame from the linked Tracker. Select the link target and click 'Set Target', or set the link target by name. You can also bake the expression link into keyframes if you want it independant from the target node. \n\nThe transform Matchmoves or Stabilizes depending on what the parent tracker node is set to, but you can invert this by enabling the 'invert' checkbox.")
	trans.knob('label').setValue('Matchmove: [value link_target]')
	trans.setXYpos(target_node.xpos()-grid_x*0, target_node.ypos()+grid_y*2)
	trans.addKnob(nuke.Tab_Knob('TrackLink'))
	trans.addKnob(nuke.Int_Knob('identity_frame', 'Identity Frame'))
	if 'Tracker' in nclass:
		trans.knob('identity_frame').setValue( int(target_node.knob('reference_frame').value()) )
	else:
		trans.knob('identity_frame').setValue(nuke.frame())
	trans.addKnob(nuke.PyScript_Knob('identity_to_curframe', 'Set to Current Frame'))
	trans.knob('identity_to_curframe').setTooltip("Set identity frame to current frame.")
	trans.knob('identity_to_curframe').setCommand("nuke.thisNode().knob('identity_frame').setValue(nuke.frame())")
	trans.addKnob(nuke.PyScript_Knob('del_relative', 'Del Relative'))
	trans.knob('del_relative').setTooltip('Delete relative transformation.\n\nUse original Transform values.')
	trans.knob('del_relative').setCommand("# Delete Rel - remove identity frame transformations on transform link\ndef del_relative():\n	n = nuke.thisNode()\n	# Get target node name\n	target_name = n['translate'].animation(0).expression().split(' - ')[0].split('parent.')[1].split('.translate')[0]\n	n.knob('translate').clearAnimated()\n	n.knob('translate').setExpression('parent.{0}.translate'.format(target_name))\n	n.knob('rotate').clearAnimated()\n	n.knob('rotate').setExpression('parent.{0}.rotate'.format(target_name))\n	n.knob('scale').clearAnimated()\n	n.knob('scale').setExpression('parent.{0}.scale'.format(target_name))\n	n.knob('skewX').clearAnimated()\n	n.knob('skewX').setExpression('parent.{0}.skewX'.format(target_name))\n	n.knob('skewY').clearAnimated()\n	n.knob('skewY').setExpression('parent.{0}.skewY'.format(target_name))\n	n.knob('center').clearAnimated()\n	n.knob('center').setExpression('parent.{0}.center'.format(target_name))\nif __name__ == '__main__':\n	del_relative()")
	trans.addKnob(nuke.String_Knob('link_target', 'Link Target', target_name))
	trans.knob('link_target').setTooltip('Type in a node name to link to.')
	trans.addKnob(nuke.PyScript_Knob('set_target', 'Set Target'))
	trans.knob('set_target').clearFlag(nuke.STARTLINE)
	trans.knob('set_target').setTooltip('Sets the link target to the selected node.\n\nIf no node is selected, set target node to the value of Link Target.')
	trans.knob('set_target').setCommand("# Set Target Button\ndef link_to_target():\n	trans = nuke.thisNode()\n	selected_nodes = nuke.selectedNodes()\n	if len(selected_nodes) is 0:\n		target_name = nuke.thisNode().knob('link_target').getValue()\n		target_node = nuke.toNode(target_name)\n		if target_node is None:\n			nuke.message('Target node does not exist!')\n			return\n	if len(selected_nodes) is 1:\n		target_node = selected_nodes[0]\n		target_name = target_node.name()\n		trans.knob('link_target').setValue(target_name)\n	if len(selected_nodes) > 1:\n		print 'Error: Exactly 1 target node must be specified.'\n		return\n	target_class = target_node.Class()\n	if target_class in ['Tracker3', 'Tracker4', 'Transform', 'TransformMasked']:\n		trans.knob('translate').clearAnimated()\n		trans.knob('translate').setExpression('parent.{0}.translate - parent.{0}.translate(identity_frame)'.format(target_name))\n		trans.knob('rotate').clearAnimated()\n		trans.knob('rotate').setExpression('parent.{0}.rotate - parent.{0}.rotate(identity_frame)'.format(target_name))\n		trans.knob('scale').clearAnimated()\n		trans.knob('scale').setExpression('parent.{0}.scale / parent.{0}.scale(identity_frame)'.format(target_name))\n		trans.knob('skewX').clearAnimated()\n		trans.knob('skewX').setExpression('parent.{0}.skewX - parent.{0}.skewX(identity_frame)'.format(target_name))\n		trans.knob('skewY').clearAnimated()\n		trans.knob('skewY').setExpression('parent.{0}.skewY - parent.{0}.skewY(identity_frame)'.format(target_name))\n		trans.knob('center').clearAnimated()\n		trans.knob('center').setExpression('parent.{0}.center+parent.{0}.translate(identity_frame)'.format(target_name))\n		if target_class in ['Tracker3', 'Tracker4']:\n			trans.knob('identity_frame').setValue( int(nuke.toNode(target_name).knob('reference_frame').getValue()) )\n	else:\n		print 'Error: Must select a Tracker or Transform class node.'\nif __name__ == '__main__':\n	link_to_target()")
	trans.addKnob(nuke.PyScript_Knob('bake_link', 'Bake Expression Links'))
	trans.knob('bake_link').setFlag(nuke.STARTLINE)
	trans.knob('bake_link').setTooltip('Bake expression links to keyframes')
	trans.knob('bake_link').setCommand("# Bake Expression Links Button\ndef bake_expression_links():\n	trans = nuke.thisNode()\n	link_target_name = trans.knob('link_target').getValue()\n	target_node = nuke.toNode(link_target_name)\n	if target_node.knob('translate').isAnimated():\n		# Get animation framerange from translate knob of target tracker node\n		target_anims = target_node.knob('translate').animations()\n		first_key = target_anims[0].keys()[0].x\n		last_key = target_anims[0].keys()[-1].x\n	else:\n		nuke.message('No Keys on Source!')\n		return\n	# Bake knob values\n	def bake_knob( node, knob, first_key, last_key):\n		first_key = str(first_key)\n		last_key = str(last_key)\n		def horizontal_ends(knob, i):\n			# Set start and end keyframe to horizontal interpolation so it matches expression before and after animation\n			anim = knob.animation(i)\n			anim.changeInterpolation( [anim.keys()[0], anim.keys()[-1]], nuke.HORIZONTAL)\n			return\n		if knob.width() == 1:\n			nuke.animation('{0}.{1}'.format(node.name(), knob.name()), 'generate', (first_key, last_key, '1', 'y', '{0}'.format(knob.name())))\n			horizontal_ends(knob, 0)\n		if knob.width() == 2: \n			if knob.name() == 'scale':\n				if knob.singleValue() == True:\n					nuke.animation('{0}.scale'.format(node.name()), 'generate', (first_key, last_key, '1', 'y', 'scale'))\n					horizontal_ends(knob, 0)\n				else:\n					for i, dim in enumerate(['w', 'h']):\n						nuke.animation('{0}.{1}.{2}'.format(node.name(), knob.name(), dim), 'generate', (first_key, last_key, '1', 'y', '{0}.{1}'.format(knob.name(), dim)))\n						horizontal_ends(knob, i)\n			else:\n				for i, dim in enumerate(['x', 'y']):\n					nuke.animation('{0}.{1}.{2}'.format(node.name(), knob.name(), dim), 'generate', (first_key, last_key, '1', 'y', '{0}.{1}'.format(knob.name(), dim)))\n					horizontal_ends(knob, i)\n	for knob_name, knob in trans.knobs().iteritems():\n	    if knob_name in ['translate', 'rotate', 'scale', 'skewX', 'skewY', 'center']:\n	        print 'Baking {0}'.format(knob_name)\n	        bake_knob(trans, knob, first_key, last_key)\nif __name__ == '__main__':\n	bake_expression_links()")
	trans.addKnob(nuke.Text_Knob(""))
	trans.addKnob(nuke.Double_Knob('motionblur_gui', 'gui mblur'))
	trans.knob('motionblur_gui').setValue(0)
	trans.knob('motionblur_gui').setTooltip('Set the $gui motion blur value')
	# Link knobs 
	trans.knob('translate').setExpression('parent.{0}.translate - parent.{0}.translate(identity_frame)'.format(target_name))
	trans.knob('rotate').setExpression('parent.{0}.rotate - parent.{0}.rotate(identity_frame)'.format(target_name))
	trans.knob('scale').setExpression('parent.{0}.scale / parent.{0}.scale(identity_frame)'.format(target_name))
	trans.knob('skewX').setExpression('parent.{0}.skewX - parent.{0}.skewX(identity_frame)'.format(target_name))
	trans.knob('skewY').setExpression('parent.{0}.skewY - parent.{0}.skewY(identity_frame)'.format(target_name))
	trans.knob('center').setExpression('parent.{0}.center+parent.{0}.translate(identity_frame)'.format(target_name))
	trans.knob('motionblur').setExpression('$gui ? motionblur_gui : 4')
	trans.knob('shutteroffset').setValue('centred')



def link_roto(tracker_node, roto_node=False):
	'''
	Utility function: Creates a layer in roto_node linked to tracker_node
	if roto_node is False, creates a roto node next to tracker node to link to
	'''
	grid_x = int(nuke.toNode('preferences').knob('GridWidth').value())
	grid_y = int(nuke.toNode('preferences').knob('GridHeight').value())

	tracker_name = tracker_node.name()
	tracker_node.setSelected(False)

	# If Roto node not selected, create one.
	if not roto_node:
		roto_node = nuke.nodes.Roto()
		roto_node.setXYpos(tracker_node.xpos()-grid_x*0, tracker_node.ypos()+grid_y*2)
		roto_node.setSelected(True)

	# Create linked layer in Roto Node
	curves_knob = roto_node["curves"]
	stab_layer = rp.Layer(curves_knob)
	stab_layer.name = "stab_"+tracker_name

	trans_curve_x = cl.AnimCurve()
	trans_curve_y = cl.AnimCurve()

	trans_curve_x.expressionString = "parent.{0}.translate.x".format(tracker_name)
	trans_curve_y.expressionString = "parent.{0}.translate.y".format(tracker_name)
	trans_curve_x.useExpression = True
	trans_curve_y.useExpression = True

	rot_curve = cl.AnimCurve()
	rot_curve.expressionString = "parent.{0}.rotate".format(tracker_name)
	rot_curve.useExpression = True

	scale_curve = cl.AnimCurve()
	scale_curve.expressionString = "parent.{0}.scale".format(tracker_name)
	scale_curve.useExpression = True

	center_curve_x = cl.AnimCurve()
	center_curve_y = cl.AnimCurve()
	center_curve_x.expressionString = "parent.{0}.center.x".format(tracker_name)
	center_curve_y.expressionString = "parent.{0}.center.y".format(tracker_name)
	center_curve_x.useExpression = True
	center_curve_y.useExpression = True

	# Define variable for accessing the getTransform()
	transform_attr = stab_layer.getTransform()
	# Set the Animation Curve for the Translation attribute to the value of the previously defined curve, for both x and y
	transform_attr.setTranslationAnimCurve(0, trans_curve_x)
	transform_attr.setTranslationAnimCurve(1, trans_curve_y)
	# Index value of setRotationAnimCurve is 2 even though there is only 1 parameter...
	# http://www.mail-archive.com/nuke-python@support.thefoundry.co.uk/msg02295.html
	transform_attr.setRotationAnimCurve(2, rot_curve)
	transform_attr.setScaleAnimCurve(0, scale_curve)
	transform_attr.setScaleAnimCurve(1, scale_curve)
	transform_attr.setPivotPointAnimCurve(0, center_curve_x)
	transform_attr.setPivotPointAnimCurve(1, center_curve_y)
	curves_knob.rootLayer.append(stab_layer)



def link_camera(src_cam, proj_frame, expr_link, clone, index):
	"""
		Create a linked camera to src_cam -- this camera can be frozen on a projection frame
	"""
	# Default grid size is 110x24. This enables moving by grid increments.
	# http://forums.thefoundry.co.uk/phpBB2/viewtopic.php?t=3739&sid=c40e65b1f575ba9166583faf807184ee
	# Offset by 1 grid in x for each additional iteration
	grid_x = int(nuke.toNode('preferences').knob('GridWidth').value())
	grid_y = int(nuke.toNode('preferences').knob('GridHeight').value())*index
	
	src_cam.setSelected(False)

	# Create Projection Camera
	proj_cam = nuke.nodes.Camera2()

	# Create Projection Frame knob
	frame_tab = nuke.Tab_Knob('Frame') 
	proj_cam.addKnob(frame_tab)
	proj_frame_knob = nuke.Double_Knob('proj_frame')
	if clone:
		proj_frame_knob.setExpression('t')
	else:
		proj_frame_knob.setValue(proj_frame)
	
	proj_cam.addKnob(proj_frame_knob)

	## Copy the knob values of the Source Camera
	for knob_name, knob in src_cam.knobs().iteritems():
		# For Animated knobs, copy or link the values depending on if Expression Links are enabled.
		if knob.isAnimated():
			#print "setting animated knob", knob_name
			if expr_link == True:
				#print "setting expression for animated knobs"
				for index in range(src_cam.knob(knob_name).arraySize()):
					if src_cam.knob(knob_name).isAnimated(index):
						proj_cam[knob_name].copyAnimation(index, src_cam[knob_name].animation(index))
				proj_cam[knob_name].setExpression('parent.' + src_cam.name() + "." + knob_name + "(proj_frame)")
			# http://www.nukepedia.com/python/knob-animation-and-python-a-primer/
			else:
				for index in range(src_cam.knob(knob_name).arraySize()):
					if src_cam.knob(knob_name).isAnimated(index):
						proj_cam[knob_name].copyAnimation(index, src_cam[knob_name].animation(index))
				proj_cam[knob_name].setExpression('curve(proj_frame)')
		
		# For all non-animated knobs that are not set to default values, match value from src_cam
		elif hasattr(knob, "notDefault") and knob.notDefault():
			try:
				#print "changing ", knob_name
				proj_cam[knob_name].setValue(knob.value())
			except TypeError:
				pass

	# Set label, color, name, and position
	proj_cam.setXYpos(src_cam.xpos()-grid_x, src_cam.ypos()-grid_y*4)	
	if clone:
		proj_cam.setName("{0}_CLONE_".format(src_cam.name()))
		proj_cam["gl_color"].setValue(0xff5f00ff)
		proj_cam["tile_color"].setValue(0xff5f00ff)
	else:
		proj_cam.setName("{0}_PROJ_".format(src_cam.name()))
		proj_cam["gl_color"].setValue(0xffff)
		proj_cam["tile_color"].setValue(0xffff)
		proj_cam["label"].setValue("FRAME [value proj_frame]")



def link(link_type=None):
	nodes = nuke.selectedNodes()
	track_nodes = [n for n in nodes if 'Tracker' in n.Class()]
	cam_nodes = [n for n in nodes if n.Class() in ['Camera', 'Camera2']]
	roto_nodes = [n for n in nodes if n.Class() in ['Roto', 'RotoPaint', 'SplineWarp3']]
	xform_nodes = [n for n in nodes if n.Class() == 'Transform']

	if link_type == 'roto':
		if len(track_nodes) == 0:
			print "Error: At least one Tracker node must be selected."
			return
		if len(roto_nodes) > 1 and len(track_nodes) > 1:
			print "Error: if multiple roto nodes are selected, exactly 1 Tracker node must be selected."
			return
		for track_node in track_nodes:
			if len(roto_nodes) is 0:
				link_roto(track_node)
			if len(roto_nodes) >= 1:
				for roto_node in roto_nodes:
					link_roto(track_node, roto_node)
	
	if link_type == None:
		for track_node in track_nodes:
			link_transform(track_node)
		for xform_node in xform_nodes:
			link_transform(xform_node)
		for cam_node in cam_nodes:
			## Set up camera linker panel
			p = nuke.Panel('Camera Linker: {0}'.format(cam_node.name()))
			p.setWidth(450)
			p.addSingleLineInput('Frame', str(nuke.frame()))
			p.addBooleanCheckBox("Clone", False)
			p.addBooleanCheckBox('Link', True)
			if p.show():
				clone = p.value('Clone')
				framestring = p.value('Frame')
				expr_link = p.value('Link')
			else:
				# Cancelled
				return
			## Parse frame string
			if "," in framestring:
				framelist = map(int, framestring.split(','))
				for i, frame in enumerate(framelist):
					link_camera(cam_node, frame, expr_link, clone, i)
			else:
				try:
					framestring = int(framestring)
				except:
					print "Error converting frame to integer!"
					return
				link_camera(cam_node, framestring, expr_link, clone, 0)







Javier Nieto Moncó © 2025

  • linkedin
  • vimeo
  • generic-social-link
bottom of page