CompStart

Pro레슨코스엘리먼트소프트웨어

CompStart

최첨단 시각효과 기술을 마스터하세요.

학습

코스레슨Pro 기능에셋소프트웨어

Tools

Tech CheckDisplay CheckMotion BlurCIE 1931PhysLightNuke Tools

회사

Instructor미션임프레숨개인정보처리방침이용약관

프로젝트

Moray RenderEXR ConverterOCIO.ccNuke ToolsDerekVFX

Follow

YouTubeInstagramTikTokThreads문의

© 2026 CompStart. All rights reserved.

Reconcile3D Fast

ToolSets / 3D
3D

2026-09-07

ToolSets/3d/reconcile3DFast.nk

3D
조정하다
카메라
중심선
투사
공익사업
변환
추적
View on GitHub

애니메이션된 카메라를 통해 축 변환을 카메라 범위 전체에 걸쳐 2D XY로 투영하고, 선택적으로 변환 결과를 내보낼 수 있습니다.

Reconcile3D Fast는 배경(또는 루트 형식), 카메라 및 축을 입력받아 축 변환 값을 화면 공간으로 투영합니다. 계산 버튼을 누르면 조정된 2D 좌표가 카메라 애니메이션 범위 전체에 걸쳐 XY 출력 노브에 기록됩니다.

이 스크립트는 전체 Reconcile3D 패스를 실행하지 않고 알려진 3D 포인트에서 2D 트랙 또는 오프셋이 필요할 때 사용합니다. 카메라와 축은 점 뒤에 위치할 수 있으며, 스크립트는 최상위 노드를 확인합니다. 애니메이션이 적용되지 않은 카메라는 사용할 수 없으며, 변환 또는 회전 곡선에서 범위를 읽을 수 없는 경우 루트 프레임 범위가 사용됩니다.

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