CompStart

ProУрокиКурсыЭлементыСофт

CompStart

Осваивайте визуальные эффекты и передовые техники.

Обучение

КурсыУрокиPro-функцииРесурсыСофт

Tools

Tech CheckDisplay CheckMotion BlurCIE 1931PhysLightNuke Tools

Компания

InstructorМиссияImpressumКонфиденциальностьУсловия

Проекты

Moray RenderEXR ConverterOCIO.ccNuke ToolsDerekVFX

Follow

YouTubeInstagramTikTokThreadsКонтакты

© 2026 CompStart. Все права защищены.

Быстрое согласование вершин

ToolSets / 3D
3D

2026-09-07

ToolSets/3d/reconcileVertexFast.nk

3d
примириться
вершина
отслеживание
камера
геометрия
трансформировать
полезность
View on GitHub

Захватите вершины сетки в 3D-просмотрщике, спроецируйте их через камеру и создайте усредненную 2D XY-анимацию для отслеживания или преобразований.

Функция Reconcile Vertex Fast преобразует выбранные вершины геометрии в единую двухмерную анимацию в экранном пространстве. Захватите одну или несколько вершин сетки ReadGeo в 3D-просмотрщике; несколько выбранных вершин сохраняются в виде пар objnum:index и усредняются при вычислении.

Подключите фоновое изображение (диапазон кадров), камеру и держите сетку видимой в активном 3D-просмотрщике. Вычислите проекции каждой захваченной вершины через камеру для каждого кадра, усредните положения на экране и запишите результат на регулятор XY Output.

Используйте Create Transform, когда вам нужен узел Transform, управляемый этой кривой — для сопоставления движений, меток или другой работы с 2D-следом, связанной с точками 3D-сетки. Очистка элементов управления сбрасывает данные захвата или выходную анимацию без перестроения группы.

Script code

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