Proyecta la traslación de un eje a través de una cámara animada en coordenadas XY 2D en todo el rango de la cámara, con exportación de transformación opcional.
Reconcile3D Fast toma un fondo (o formato raíz), una cámara y un eje, y luego proyecta la traducción del eje al espacio de la pantalla. Presione Calcular para escribir los puntos 2D conciliados en el control de salida XY a lo largo del rango de animación de la cámara.
Úselo cuando necesite trayectorias 2D o desplazamientos desde puntos 3D conocidos sin ejecutar una pasada completa de Reconcile3D. La cámara y el eje pueden estar detrás de los puntos; el script resuelve el nodo superior. Las cámaras no animadas se rechazan; si el rango no se puede leer a partir de las curvas de traslación o rotación, se utiliza el rango del fotograma raíz.
Clear restablece la salida XY. Transform crea un nodo Transform y copia las claves XY animadas en translate para que puedas controlar el trabajo 2D desde la ruta reconciliada.
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