CompStart

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

CompStart

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

Обучение

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

Tools

Tech CheckDisplay CheckMotion BlurCIE 1931PhysLightNuke Tools

Компания

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

Проекты

Moray RenderEXR ConverterOCIO.ccNuke ToolsDerekVFX

Follow

YouTubeInstagramTikTokThreadsКонтакты

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

Reconcile3D Fast

ToolSets / 3D
3D

2026-09-07

ToolSets/3d/reconcile3DFast.nk

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

Функция проецирует перемещение по осям через анимированную камеру в 2D-режим XY в пределах диапазона камеры, с возможностью экспорта в формат Transform.

Reconcile3D Fast принимает фон (или корневой формат), камеру и ось, а затем проецирует преобразование оси в экранное пространство. Нажмите «Вычислить», чтобы записать согласованные 2D-точки в выходной параметр XY по всему диапазону анимации камеры.

Используйте его, когда вам нужны 2D-треки или смещения относительно известных 3D-точек без выполнения полного прохода Reconcile3D. Камера и оси могут располагаться за точками; скрипт определяет верхний узел. Неанимированные камеры отклоняются; если диапазон не может быть считан из кривых перемещения или вращения, используется диапазон корневого кадра.

Функция Clear сбрасывает выходные данные XY. Функция Transform создает узел Transform и копирует анимированные ключи XY в узел translate, что позволяет управлять 2D-графикой, используя согласованный путь.

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