Captura los vértices de la malla en el visor 3D, proyéctalos a través de una cámara y genera una trayectoria XY 2D promedio para su uso en la función Transformación.
Reconcile Vertex Fast convierte los vértices geométricos seleccionados en una única animación 2D en el espacio de la pantalla. Seleccione los vértices de una malla ReadGeo en el visor 3D, capture sus índices y, a continuación, proyéctelos a través de una cámara conectada en el rango de fotogramas de la imagen de entrada.
En cada fotograma, se promedian los múltiples vértices capturados para generar una curva de salida XY. Úsela cuando necesite un seguimiento 2D rápido a partir de puntos de malla conocidos, sin necesidad de un flujo de trabajo de reconciliación 3D completo.
Conecta la imagen para el rango de fotogramas, la cámara para la proyección y mantén la malla visible en un visor 3D activo para el cálculo. Tras el cálculo, puedes crear un nodo de transformación con la curva de traslación precalculada o controlar otros nodos desde el control de salida XY.
set cut_paste_input [stack 0]
version 13.0 v1
push 0
push 0
push $cut_paste_input
Group {
inputs 3
name ReconcileVertex
tile_color 0x7f3030ff
selected true
xpos 12819
ypos -4378
addUserKnob {20 ReconcileVertexTab l ReconcileVertex}
addUserKnob {26 hdr_capture l "" +STARTLINE T "<b>1. Capture Vertices</b>"}
addUserKnob {26 capture_help l "" +STARTLINE T "Select vertices on your ReadGeo mesh in the 3D viewer, then click Capture. Multiple vertices are averaged."}
addUserKnob {22 btn_capture l "Capture Selection" t "Read the current vertex selection from the 3D viewer and store the vertex indices." T "import nuke\n\ntry:\n from nukescripts.snap3d import selectedVertexInfos\nexcept ImportError:\n from nukescripts import snap3d\n selectedVertexInfos = snap3d.selectedVertexInfos\n\nnode = nuke.thisNode()\ninfos = list(selectedVertexInfos(0.5))\nif not infos:\n nuke.message(\"No vertices selected. Select vertices on your ReadGeo mesh in the 3D viewer first.\")\nelse:\n pairs = \[\]\n for v in infos:\n pairs.append(\"{0}:{1}\".format(v.objnum, v.index))\n idx_str = \", \".join(pairs)\n node\[\"vertex_indices\"\].setValue(idx_str)\n node\[\"status\"\].setValue(\"{0} vertices captured\".format(len(pairs)))\n" +STARTLINE}
addUserKnob {22 btn_clear_capture l Clear t "Clear captured vertex indices." -STARTLINE T "import nuke\nn = nuke.thisNode()\nn\[\"vertex_indices\"\].setValue(\"\")\nn\[\"status\"\].setValue(\"Cleared\")\n"}
addUserKnob {1 vertex_indices l "Vertex Indices" t "Captured vertex indices as objnum:index pairs. You can also type these manually."}
addUserKnob {26 ""}
addUserKnob {26 hdr_calc l "" +STARTLINE T "<b>2. Calculate</b>"}
addUserKnob {26 calc_help l "" +STARTLINE T "Connect Camera and BG image (frame range from img input). Outputs averaged 2D position."}
addUserKnob {22 btn_calculate l Calculate t "Project captured vertices through the camera, average their screen positions, and store as a single 2D animation curve." T "import nuke\nimport math\n\n\ndef _get_cam(node):\n cam_input = node.input(1)\n if not (cam_input and isinstance(cam_input, nuke.Node)):\n return None\n if \"Camera\" in cam_input.Class():\n return cam_input\n top = nuke.toNode(nuke.tcl(\"full_name \[topnode %s\]\" % cam_input.name()))\n if top and \"Camera\" in top.Class():\n return top\n return None\n\n\ndef _cam_projection_matrix(cam, frame, fmt):\n wm = nuke.math.Matrix4()\n for i in range(16):\n wm\[i\] = cam\[\"matrix\"\].getValueAt(frame, i)\n wm.transpose()\n camTransform = wm.inverse()\n\n roll = float(cam\[\"winroll\"\].getValueAt(frame, 0))\n sx = float(cam\[\"win_scale\"\].getValueAt(frame, 0))\n sy = float(cam\[\"win_scale\"\].getValueAt(frame, 1))\n tx = float(cam\[\"win_translate\"\].getValueAt(frame, 0))\n ty = float(cam\[\"win_translate\"\].getValueAt(frame, 1))\n m = nuke.math.Matrix4()\n m.makeIdentity()\n m.rotateZ(math.radians(roll))\n m.scale(1.0 / sx, 1.0 / sy, 1.0)\n m.translate(-tx, -ty, 0.0)\n\n focal = float(cam\[\"focal\"\].getValueAt(frame))\n hap = float(cam\[\"haperture\"\].getValueAt(frame))\n near = float(cam\[\"near\"\].getValueAt(frame))\n far = float(cam\[\"far\"\].getValueAt(frame))\n proj_mode = int(cam\[\"projection_mode\"\].getValueAt(frame))\n p = nuke.math.Matrix4()\n p.projection(focal / hap, near, far, proj_mode == 0)\n\n aspect = float(fmt.height()) / float(fmt.width())\n t = nuke.math.Matrix4()\n t.makeIdentity()\n t.translate(1.0, 1.0 - (1.0 - aspect / float(fmt.pixelAspect())), 0.0)\n\n x_sc = float(fmt.width()) / 2.0\n y_sc = x_sc * fmt.pixelAspect()\n s = nuke.math.Matrix4()\n s.makeIdentity()\n s.scale(x_sc, y_sc, 1.0)\n\n return s * t * p * m * camTransform\n\n\ndef _project(cam_matrix, world_pos):\n v4 = nuke.math.Vector4(world_pos.x, world_pos.y, world_pos.z, 1.0)\n tp = cam_matrix * v4\n try:\n return (tp.x / tp.w, tp.y / tp.w)\n except ZeroDivisionError:\n return (0.0, 0.0)\n\n\ndef _get_frame_range(node):\n bg = node.input(0)\n if bg:\n first = bg.firstFrame()\n last = bg.lastFrame()\n if first != last:\n return nuke.FrameRange(\"{0}-{1}\".format(first, last))\n return nuke.FrameRange(\"{0}-{1}\".format(nuke.root().firstFrame(), nuke.root().lastFrame()))\n\n\ndef _find_geo_knob():\n if not nuke.activeViewer():\n return None\n viewer = nuke.activeViewer()\n viewer_node = viewer.node()\n if hasattr(viewer, \"getGeometryNodes\"):\n for n in viewer.getGeometryNodes():\n if \"geo_select\" in n.knobs():\n return n\[\"geo_select\"\]\n for n in nuke.allNodes(recurseGroups=True):\n if \"geo_select\" in n.knobs():\n return n\[\"geo_select\"\]\n if \"geo\" in viewer_node.knobs():\n k = viewer_node\[\"geo\"\]\n if hasattr(k, \"getGeometry\"):\n return k\n return None\n\n\ndef calculate(node):\n idx_str = node\[\"vertex_indices\"\].value().strip()\n if not idx_str:\n nuke.message(\"No vertices captured. Use Capture Selection first.\")\n return\n\n pairs = \[\]\n for part in idx_str.split(\",\"):\n part = part.strip()\n if \":\" in part:\n obj, vtx = part.split(\":\", 1)\n pairs.append((int(obj.strip()), int(vtx.strip())))\n if not pairs:\n nuke.message(\"Could not parse vertex indices.\")\n return\n\n cam = _get_cam(node)\n if cam is None:\n nuke.message(\"Connect a Camera to input 2 (cam).\")\n return\n\n fmt = node.format()\n if not fmt:\n fmt = nuke.root()\[\"format\"\].value()\n\n frange = _get_frame_range(node)\n geo_knob = _find_geo_knob()\n if geo_knob is None:\n nuke.message(\"No active 3D viewer found. Open a 3D viewer with your geometry visible.\")\n return\n\n num_verts = len(pairs)\n keys_x = \[\]\n keys_y = \[\]\n\n task = nuke.ProgressTask(\"Reconcile Vertices\")\n total = int(frange.last()) - int(frange.first()) + 1\n\n tmp = nuke.nodes.CurveTool()\n try:\n for i, frame in enumerate(frange):\n if task.isCancelled():\n break\n task.setProgress(int(100.0 * i / max(total, 1)))\n task.setMessage(\"Frame %d\" % frame)\n\n nuke.execute(tmp, frame, frame)\n cam_matrix = _cam_projection_matrix(cam, frame, fmt)\n\n objs = geo_knob.getGeometry()\n if objs is None:\n continue\n\n sum_x = 0.0\n sum_y = 0.0\n count = 0\n for obj_idx, vtx_idx in pairs:\n if obj_idx >= len(objs):\n continue\n pts = objs\[obj_idx\].points()\n if vtx_idx >= len(pts):\n continue\n pos = pts\[vtx_idx\]\n xform = objs\[obj_idx\].transform()\n wp = xform * nuke.math.Vector4(pos.x, pos.y, pos.z, 1.0)\n world_pos = nuke.math.Vector3(wp.x, wp.y, wp.z)\n sx, sy = _project(cam_matrix, world_pos)\n sum_x += sx\n sum_y += sy\n count += 1\n\n if count > 0:\n keys_x.append(nuke.AnimationKey(frame, sum_x / count))\n keys_y.append(nuke.AnimationKey(frame, sum_y / count))\n finally:\n nuke.delete(tmp)\n del task\n\n opknob = node\[\"output\"\]\n opknob.clearAnimated()\n opknob.setAnimated()\n for ch, curve in enumerate(opknob.animations()):\n curve.addKey(\[keys_x, keys_y\]\[ch\])\n\n node\[\"status\"\].setValue(\"{0} verts averaged, projected over {1}\".format(num_verts, frange))\n\n\nif __name__ == \"__main__\":\n calculate(nuke.thisNode())\n" +STARTLINE}
addUserKnob {22 btn_clear_calc l Clear t "Clear the output animation." -STARTLINE T "import nuke\n\nnode = nuke.thisNode()\nopknob = node\[\"output\"\]\nopknob.clearAnimated()\nopknob.setValue(0, 0)\nopknob.setValue(0, 1)\nnode\[\"status\"\].setValue(\"Output cleared\")\n"}
addUserKnob {12 output l "XY Output"}
addUserKnob {26 ""}
addUserKnob {26 hdr_output l "" +STARTLINE T "<b>3. Output</b>"}
addUserKnob {22 btn_transform l "Create Transform" t "Create a Transform node with the calculated 2D translate curve." T "import nuke\n\nnode = nuke.thisNode()\nopknob = node\[\"output\"\]\nif not opknob.isAnimated():\n nuke.message(\"No calculated data. Run Calculate first.\")\nelse:\n grid_y = int(nuke.toNode(\"preferences\").knob(\"GridHeight\").value())\n with nuke.root():\n xf = nuke.nodes.Transform()\n xf.setXYpos(node.xpos(), node.ypos() + grid_y * 3)\n tknob = xf\[\"translate\"\]\n tknob.clearAnimated()\n tknob.setAnimated()\n src_anims = opknob.animations()\n for ch, curve in enumerate(tknob.animations()):\n if ch < len(src_anims):\n curve.addKey(src_anims\[ch\].keys())\n node\[\"status\"\].setValue(\"Transform node created\")\n" +STARTLINE}
addUserKnob {26 ""}
addUserKnob {26 status l Status T "Ready"}
addUserKnob {26 ""}
addUserKnob {26 lbl l "" +STARTLINE T "ReconcileVertex v2.1"}
addUserKnob {22 btn_website 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 geo
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