DerekVFX LogoDerekVFX

Blog

Nuke

Projects

Photography

Filmography

OCIO.ccExternal link

DerekVFX LogoDerekVFX

©2009 - 2026 DerekVFX

ContactIMDBLinkedInGitHubOCIO.ccExternal link
←ToolSets / 3D

reconcile3DFast

3D2026-08-04ToolSets/3d/reconcile3DFast.nk
View on GitHub

ToolSets/3d/reconcile3DFast.nk

Script Code

set cut_paste_input [stack 0]
version 13.0 v1
push 0
push 0
push $cut_paste_input
Group {
inputs 3
name reconcile3DFast
tile_color 0x421010ff
selected true
xpos 12819
ypos -4378
addUserKnob {20 PlanarProjection l reconcile3DFast}
addUserKnob {22 Calculate t "Calculate the reconciled 2D points for each 3D point." T "import nuke\nfrom nukescripts import snap3d as sn\nimport math\n\n\n## Based on Planar Projection by Vit Sedlacek and Jed Smith\n\n\ndef cameraProjectionMatrix(cameraNode, frame, imageformat):\n ## modified code from nukescripts/Snap3D\n\n # Matrix to transform points into camera-relative coords.\n wm = nuke.math.Matrix4()\n for i in range(16):\n wm\[i] = cameraNode\[\"matrix\"].getValueAt(frame, i)\n\n wm.transpose()\n camTransform = wm.inverse()\n\n # Matrix to take the camera projection knobs into account\n roll = float(cameraNode\[\"winroll\"].getValueAt(frame, 0))\n scale_x = float(cameraNode\[\"win_scale\"].getValueAt(frame, 0))\n scale_y = float(cameraNode\[\"win_scale\"].getValueAt(frame, 1))\n translate_x = float(cameraNode\[\"win_translate\"].getValueAt(frame, 0))\n translate_y = float(cameraNode\[\"win_translate\"].getValueAt(frame, 1))\n m = nuke.math.Matrix4()\n m.makeIdentity()\n m.rotateZ(math.radians(roll))\n m.scale(1.0 / scale_x, 1.0 / scale_y, 1.0)\n m.translate(-translate_x, -translate_y, 0.0)\n\n # Projection matrix based on the focal length, aperture and clipping planes of the camera\n focal_length = float(cameraNode\[\"focal\"].getValueAt(frame))\n h_aperture = float(cameraNode\[\"haperture\"].getValueAt(frame))\n near = float(cameraNode\[\"near\"].getValueAt(frame))\n far = float(cameraNode\[\"far\"].getValueAt(frame))\n projection_mode = int(cameraNode\[\"projection_mode\"].getValueAt(frame))\n p = nuke.math.Matrix4()\n p.projection(focal_length / h_aperture, near, far, projection_mode == 0)\n\n # Matrix to translate the projected points into normalised pixel coords\n imageAspect = float(imageformat.height()) / float(imageformat.width())\n\n t = nuke.math.Matrix4()\n t.makeIdentity()\n t.translate(1.0, 1.0 - (1.0 - imageAspect / float(imageformat.pixelAspect())), 0.0)\n\n # Matrix to scale normalised pixel coords into actual pixel coords.\n x_scale = float(imageformat.width()) / 2.0\n y_scale = x_scale * imageformat.pixelAspect()\n s = nuke.math.Matrix4()\n s.makeIdentity()\n s.scale(x_scale, y_scale, 1.0)\n\n # The projection matrix transforms points into camera coords, modifies based\n # on the camera knob values, projects points into clip coords, translates the\n # clip coords so that they lie in the range 0,0 - 2,2 instead of -1,-1 - 1,1,\n # then scales the clip coords to proper pixel coords.\n return s * t * p * m * camTransform\n\n\ndef projectPoints(frame, camera=None, point=None, imageformat=None):\n # Modify projectpoint function in nukescripts.snap3d to add frame argument\n if not imageformat:\n imageformat = nuke.root()\[\"format\"].value()\n camMatrix = cameraProjectionMatrix(camera, frame, imageformat)\n if camMatrix == None:\n raise RuntimeError(\"snap3d.cameraProjectionMatrix() returned None for camera.\")\n\n if not (isinstance(point, list) or isinstance(point, tuple)):\n raise ValueError(\"Argument point must be a list or tuple.\")\n\n for point in point:\n # Would be nice to not do this for every item but since lists/tuples can\n # containg anything...\n if isinstance(point, nuke.math.Vector3):\n pt = point\n elif isinstance(point, list) or isinstance(point, tuple):\n pt = nuke.math.Vector3(point\[0], point\[1], point\[2])\n else:\n raise ValueError(\n \"All items in point must be nuke.math.Vector3 or list/tuple of 3 floats.\"\n )\n\n tPos = camMatrix * nuke.math.Vector4(pt.x, pt.y, pt.z, 1.0)\n # print tPos\n try:\n yield nuke.math.Vector2(tPos.x / tPos.w, tPos.y / tPos.w)\n except ZeroDivisionError:\n print(f\"Zero Division Error on frame \{frame\} with point data \{point\}\")\n yield nuke.math.Vector2()\n\n\ndef calculate(node):\n # Get the input Camera and verify it is right. (Assume camera is topnode of input to handle dots)\n cam_input = node.input(1)\n # Sanity check\n if not (cam_input and isinstance(cam_input, nuke.Node)):\n nuke.message(\"A Camera node must be connected.\")\n return\n if \"Camera\" in cam_input.Class():\n cam = cam_input\n else:\n cam = nuke.toNode(nuke.tcl(\"full_name \[topnode %s]\" % cam_input.name()))\n\n # AXIS INPUT\n axis_input = node.input(2)\n if not (axis_input and isinstance(axis_input, nuke.Node)):\n nuke.message(\"A axis node must be connected.\")\n return\n if \"Axis\" in axis_input.Class():\n axis = axis_input\n else:\n axis = nuke.toNode(nuke.tcl(\"full_name \[topnode %s]\" % axis_input.name()))\n\n # BG INPUT\n bg = node.input(0)\n if not bg:\n nuke.message(\n \"BG not connected, so the root format will be used to reconcile the 3D point into screen space.\"\n )\n\n # Get framerange to operate on from camera animation curves\n first = None\n last = None\n try:\n if cam\[\"translate\"].isAnimated():\n for curve in cam\[\"translate\"].animations():\n if first == None:\n first = int(curve.keys()\[0].x)\n else:\n first = min(first, int(curve.keys()\[0].x))\n for curve in cam\[\"translate\"].animations():\n if last == None:\n last = int(curve.keys()\[-1].x)\n else:\n last = max(last, int(curve.keys()\[-1].x))\n elif cam\[\"rotate\"].isAnimated():\n for curve in cam\[\"rotate\"].animations():\n if first == None:\n first = int(curve.keys()\[0].x)\n else:\n\n first = min(first, int(curve.keys()\[0].x))\n for curve in cam\[\"rotate\"].animations():\n if last == None:\n last = int(curve.keys()\[-1].x)\n else:\n last = max(last, int(curve.keys()\[-1].x))\n else:\n nuke.message(\"Input Camera is not animated.\")\n return\n except:\n nuke.message(\n \"Something went wrong getting the camera animation. Using Root framerange...\"\n )\n first = nuke.root().firstFrame()\n last = nuke.root().lastFrame()\n\n framerange = nuke.FrameRange(\"\{0\}-\{1\}\".format(first, last))\n\n # Only run if the ip knob is not default\n ipknob = axis\[\"translate\"]\n\n opknob = node\[\"op1\"]\n opknob.clearAnimated()\n opknob.setAnimated()\n\n # Building the data into a list of AnimationKey objects,\n # and then applying that list to the knob using addKey is significantly faster than other methods.\n # This makes the Calculate button instantaneous instead of taking forever.\n point_animcurve = \[\[], \[]]\n for frame in framerange:\n\n # print \"values are \", ipknob.getValueAt(frame)\n # Sample input point knob on every frame if it's animated or expression-linked\n if ipknob.isAnimated() or ipknob.hasExpression():\n point = next(projectPoints(\n frame, cam, \[ipknob.getValueAt(frame)], node.format()\n ))\n else:\n point = next(projectPoints(frame, cam, \[ipknob.value()], node.format()))\n for index in range(2):\n point_animcurve\[index].append(nuke.AnimationKey(frame, point\[index]))\n\n for index, curve in enumerate(opknob.animations()):\n curve.addKey(point_animcurve\[index])\n\n\nif __name__ == \"__main__\":\n calculate(nuke.thisNode())\n" +STARTLINE}
addUserKnob {22 clear_out l Clear t "Clear output knobs." -STARTLINE T "n = nuke.thisNode()\n\nkop = n\['op1']\nkop.clearAnimated()\nkop.setValue(kop.defaultValue())"}
addUserKnob {12 op1 l "XY output"}
addUserKnob {22 Transform T "# Create tracker node\nfrom __future__ import with_statement\n\ngrid_x = int(nuke.toNode('preferences').knob('GridWidth').value())\ngrid_y = int(nuke.toNode('preferences').knob('GridHeight').value())\npproj = nuke.thisNode()\n\nwith nuke.root():\n transform_node = nuke.nodes.Transform()\n transform_node.setXYpos(pproj.xpos() - grid_x * 0, pproj.ypos() + grid_y * 2)\n \[n.setSelected(False) for n in nuke.allNodes()]\n transform_node.setSelected(True)\n\n if pproj\['op1'].isAnimated():\n tknob = transform_node\['translate']\n tknob.clearAnimated()\n tknob.setAnimated()\n for x, c in enumerate(tknob.animations()):\n opknob = pproj\['op1']\n if opknob.isAnimated():\n c.addKey(opknob.animation(x).keys())\n else:\n tknob.clearAnimated()" +STARTLINE}
addUserKnob {26 ""}
addUserKnob {26 lbl l "" +STARTLINE T "Reconcile3DFast v1.1"}
addUserKnob {22 btn l DerekVFX.ca T "nuke.tcl('start', 'https://derekvfx.ca')" +STARTLINE}
}
Input {
inputs 0
name cam
label "\[value number]"
xpos 400
ypos -301
number 1
}
Input {
inputs 0
name axis
label "\[value number]"
xpos 197
ypos -307
number 2
}
Input {
inputs 0
name img
label "\[value number]"
xpos 620
ypos -302
}
Output {
name Output1
selected true
xpos 620
ypos 158
}
end_group