Nodes/ComfyUI-SplatKit/Camera Plot Fly-Through (Perspective)
ComfyUI Node

Camera Plot Fly-Through (Perspective)

The same path, shot with a real lens

By mickmumpitz·Created about a month ago·Updated about 22 hours ago· 17
Camera Plot Fly-Through (Perspective)
  • panorama
  • moge_model
  • frames
  • hole_mask
  • splat_mask
  • cameras_json
  • camera_preview
anchors0, 0, 0 0.2, 0.02, 0.5 0.4, 0.05, 1.0
orientationlook_forward
length81
width1920
height1080
focal_mm28.0
sensor_width_mm36.00
edge_modelayered
point_budget4000
mesh_width2048
edge_rtol0.050
bg_extend_px24
moge_level9
merge_long1920
output_namecomfy_camplot_persp
dataset_dir
moge_ckpt
look_at_target0, 0, 3

Plot Camera gives you an equirect control video for WAN conditioning. This node is its perspective sibling: same editor, same anchor format, same MoGe mesh - but it renders a real pinhole camera with a focal length and sensor size, producing finished-looking perspective frames instead of an equirect inpainting target. If you want a fly-through that looks like footage shot on a 28mm lens rather than a warped 360° environment, this is the node. It's also the higher-fidelity cousin of HiRes Pano Fly-Through for when you think in lens millimetres instead of FOV degrees.

The mechanics: MoGe depth → mesh → pinhole rasterization, with colour sampled from the full-resolution panorama at whatever output size you ask for. Because it's a real pinhole render, detail is limited by the input panorama's resolution, not by width/height - the node prints a "pano width for 1:1" line so you know when you're zooming past what the source can support. Unlike the equirect node, the camera is not re-aimed at the pano centre on frame 0 - it looks exactly where the editor's heading arrow points.

Inputs that matter

  • anchors - same WYSIWYG format as Plot Camera (+Z forward, +X right, +Y up, origin = start camera). Two points minimum.
  • focal_mm and sensor_width_mm - the lens, as written on the barrel. 14mm ≈ 104°, 24mm ≈ 74°, 28mm ≈ 65°, 50mm ≈ 40° on full frame (sensor 36.0). Only the focal-to-sensor ratio matters, so leave sensor at 36 unless you think in Super35 (24.89) or MFT (17.3). Wide lenses show more of the pano per pixel (sharper) but expose more disocclusion.
  • length - keep it at 4n+1 (81, 121…) if this feeds a WAN/VACE pass.
  • edge_mode - what happens where the move tears the environment open: layered (re-grow a real background layer; sharpest, slowest), fill (fast push-pull), cut (leave the holes and let hole_mask drive a WAN inpaint), or stretch (rubber-sheet smear; only for tiny moves).

Outputs: frames (the renders), hole_mask (1 = kept, so invert it for inpainting), splat_mask (white = real pano detail, black = synthesized - feed this to a trainer to exclude fake pixels), cameras_json (exact intrinsics K for the views), and camera_preview. cameras_json wires straight into Add HiRes Views to Dataset so the views register with their true focal length rather than an estimated one. The optional mesh_width (2048 default) is geometry resolution independent of output resolution; 4096 is 33M triangles, much slower but sharper silhouettes for long lenses. moge_level defaults to 9 here - the author's note is "this node is about quality."

Install & gotchas

Clone, install requirements, restart - the standard pack install:

cd ComfyUI/custom_nodes
git clone https://github.com/mickmumpitz/ComfyUI-SplatKit
python_embeded\python.exe -m pip install -r ComfyUI-SplatKit/requirements.txt

First run pulls the MoGe checkpoint into ComfyUI/models/MoGe. The two traps: long lenses magnify the pano hard (check the sharpness warning it prints, and raise mesh_width to keep silhouettes clean), and big moves at wide angles mean big holes - cut + WAN handles those, stretch just smears. Keep moves small and let bg_extend_px (the layered-mode regrow distance) exceed the disocclusion width if holes survive.

CategorySplatKit

Inputs (20)

NameTypeDefaultDescription
panoramaIMAGE
anchorsSTRING0, 0, 0 0.2, 0.02, 0.5 0.4, 0.05, 1.0Fly-through points, in the SAME literal units as the geometry overlay: +Z forward/into the pano, +X right, +Y up, origin = the pano camera. The camera goes EXACTLY where you place each point (WYSIWYG, no rescaling, no collision guard). One 'x,y,z' per line, or JSON [[x,y,z],...]; 6 numbers adds a per-anchor look target. Need at least 2 points. Keep moves SMALL -- a single pano only holds so much parallax.
orientationCOMBOlook_forwardlook_forward = camera faces the path tangent. look_at_target = every frame aims at the SAME shared point -- set it in the look_at_target widget or drag the orange 'look' marker in the editor. per_point_look = each anchor has its own draggable look target and the aim sweeps between them. Unlike the equirect node the camera is NOT re-aimed at the pano centre on frame 0 -- it looks exactly where the editor's heading arrow points.
lengthINT811–513Frames along the path. 1 = a single still at the first anchor. If this feeds a WAN/VACE pass keep it at 4n+1 (81, 121, ...).
widthINT1920256–8192Output width. A REAL pinhole render -- detail is limited by the INPUT PANORAMA's resolution, not by this number. See the 'pano width for 1:1' line the node prints.
heightINT1080256–8192Output height. With square pixels the aspect ratio sets the VERTICAL field of view, exactly like cropping a sensor to 16:9.
focal_mmFLOAT28.04–400Lens focal length, as written on the barrel. Wider = more FOV = more of the pano per pixel = sharper, but more disocclusion visible. On full-frame: 14mm=104deg, 24mm=74deg, 28mm=65deg, 35mm=54deg, 50mm=40deg. Long lenses magnify the pano hard -- check the sharpness warning the node prints.
sensor_width_mmFLOAT36.003–120Sensor/gate WIDTH the focal length is quoted against. Full-frame 36.0 (default) | Super35 / cine 24.89 | APS-C 23.6 | MFT 17.3 | Super16 12.52. Only the focal:sensor RATIO matters, so leave it at 36 if you just want to think in full-frame millimetres.
edge_modeCOMBOlayeredWhat happens where the move tears the environment open. layered = re-grow a real background layer from the pano (sharpest, slowest). fill = fast soft push-pull fill. cut = leave the holes and report them in hole_mask -- use this to drive a WAN/VACE inpaint. stretch = never tear (rubber-sheet smear); only for tiny moves.
point_budgetINT4000500–40000Max points in the editor's geometry overlay cloud. Placement only -- it never affects the render. 4000 is plenty.
mesh_widthoptCOMBO2048GEOMETRY (depth-grid) resolution, independent of output resolution -- colour always comes from the full-res panorama. 4096 = 33M triangles: much slower, sharper silhouettes. Worth raising for long lenses, which magnify silhouette stair-stepping.
edge_rtoloptFLOAT0.0500.005–0.5Depth-edge sensitivity (relative depth jump counting as a discontinuity). Lower = more geometry treated as an edge = more holes cut but fewer smears.
bg_extend_pxoptINT244–128layered mode: how far (in depth-grid pixels) the background is re-grown behind each silhouette. Must exceed the disocclusion width; raise it if holes survive at larger moves.
moge_leveloptINT90–9MoGe detail level. 9 = max; this node is about quality.
merge_longoptINT1920512–4096Panorama depth-merge resolution (long side). The dominant MoGe cost; 1920x960 is the full-quality setting.
output_nameoptSTRINGcomfy_camplot_persp
dataset_diroptSTRINGWire a Dataset Project node here to write cameras_json under it instead of under output_name.
moge_ckptoptCOMBOMoGe checkpoint from ComfyUI/models/MoGe. 'auto (download)' fetches 'Ruicheng/moge-vitl' into that folder on first use (~1.2GB). Drop your own model.pt in models/MoGe to pick it here, or wire a MoGe Model Loader node into 'moge_model'.
moge_modeloptMOGE_MODELOptional: a pre-loaded MoGe model from the MoGe Model Loader node. Overrides moge_ckpt; load once and reuse across nodes.
look_at_targetoptSTRING0, 0, 3The single world point ALL frames aim at when orientation = look_at_target. 'x, y, z' in the SAME literal units as the anchors / geometry overlay. Draggable in the editor (the orange 'look' marker). Ignored by the other orientation modes.

Outputs (5)

NameTypeDescription
framesIMAGE
hole_maskIMAGE
splat_maskIMAGE
cameras_jsonSTRING
camera_previewIMAGE