Nodes/Camera Path/Camera Path Video
ComfyUI Node

Camera Path Video

Get a real camera move out of one photo, no 3D app

By phazei·Created 25 days ago·Updated 20 days ago· 7
Camera Path Video
  • source
  • moge_geometry
  • background
  • camera_video
  • camera_path
  • video_mask
◄frame_count120►
◄fps24.0►
◄markersfalse►
◄prune_depth_edgestrue►
◄preview_qualityMedium (512)►
◄keyframes{ "version": 2, "pivots": [ { "id": "p1", "keys": [ { "frame": 0, "x": 0.0, "y": 0.0, "z": 1.0, "tilt": 0.0, "roll": 0.0, "heading": 0.0 } ] } ], "camera": [ { "frame": 0, "pivot": "p1", "pivot_target": "p1", "pivot_blend": 0.0, "azimuth": 0.0, "elevation": 0.0, "distance": 1.0, "lateral": 0.0, "height": 0.0, "pan": 0.0, "tilt": 0.0, "roll": 0.0, "lock": 0.0, "dolly": 0.0 }, { "frame": 48, "pivot": "p1", "pivot_target": "p1", "pivot_blend": 0.0, "azimuth": 20.0, "elevation": 0.0, "distance": 1.0, "lateral": 0.0, "height": 0.0, "pan": 0.0, "tilt": 0.0, "roll": 0.0, "lock": 0.0, "dolly": 0.0 } ] }►
◄camera_path—►

What it actually does

Most "camera move" tools in ComfyUI generate the move: you prompt for a dolly-in, roll the dice, and hope the model agrees with you. This one doesn't generate anything. It takes your image (or a batch of frames), the geometry MoGe read out of it, and a camera path you draw in a small 3D editor bolted onto the node - then reprojects the scene from those virtual camera positions. You get exactly the move you set, every run, no seed roulette.

Where you'd reach for it: you want a reference video, not a finished shot. In 2026 the thing that eats warp videos is a control LoRA - Lightricks' IC-LoRAs condition on a control video's structure, and cseti's CrossView-Warp IC-LoRA for LTX 2.3 lets you define the new camera angle by drawing it on an orbit sphere. Camera Path Video is the piece that draws it. It's also the honest version of the old 2D parallax trick: same idea, but from a real point map instead of a depth model's guesswork.

The orbit and look-at math is adapted from ComfyUI-CrossViewWarp (Apache-2.0) by way of a Camera H3 depth warp, so think of it as the CrossViewWarp sibling that grew a real path editor.

How the mechanism works

MoGe doesn't hand you a depth map; it hands you an affine-invariant per-pixel point map plus a validity mask that drops sky and other undefined regions. That's why this works at all: colour comes from source, 3D position from moge_geometry, and a coloured point cloud falls out for free.

The renderer is a z-buffered point splat on whatever device ComfyUI is using - around 6 ms for a 720p frame on a GPU, around 140 ms on CPU. Output frame i uses source frame i and its geometry, and once the source runs out the last frame is reused: one still plus frame_count = 120 is one scene seen from 120 camera positions. Between keyframes each axis rides a monotone cubic (PCHIP) curve, so keyframes are hit exactly and nothing overshoots. Source frames are never interpolated in time, so this won't smooth a choppy clip.

The pivot is the point the camera circles, placed in units of an automatic pivot depth the node picks itself - the lower quartile of depth across the middle of the frame, which lands on your subject rather than the backdrop. Everything is relative to that, so azimuth 0, distance 1 reproduces your input framing and a path drops onto other footage without rescaling.

Inputs and outputs that matter

You'll wire two things: source (IMAGE - one image or a batch, and it sets the output resolution) and moge_geometry (from the native Load MoGe Model → Run MoGe Inference nodes).

Then the few that actually change your output:

  • frame_count - default 120. Output is [frame_count, height, width, 3].
  • background - the colour of holes, the pixels no point reached. Default black; if your downstream is a CrossViewWarp-style warp video, set #ff00ff, because that's what it was trained on. The editor's render pane follows it immediately.
  • prune_depth_edges - on by default. Removes points crossing depth discontinuities, trading streaks for holes. The preview updates live; run the node again to change the video.

preview_quality only changes how heavy the editor feels while you drag - output resolution is untouched. fps is the editor's timeline rate and nothing else, since the output is a plain image batch: set the rate on whatever node saves the video. keyframes is the JSON the editor writes. camera_path is socket-only and seeds the editor rather than overwriting it; Use input adopts a connected path, Clear path goes back to the source camera.

Outputs: camera_video (the IMAGE batch - feed it straight to a video model or IC-LoRA as a control video), camera_path (the same keyframes as JSON, so you can stash a move and drop it onto entirely different footage), and video_mask (MASK, 1 wherever no point was reprojected, including pruning holes - hand it to an inpaint or video pass when the revealed regions need filling).

Install

ComfyUI Manager, search ComfyUI-Camera-Path, or:

cd ComfyUI/custom_nodes
git clone https://github.com/phazei/ComfyUI-Camera-Path

Restart. Nothing else to install: there is no requirements.txt, and pyproject.toml declares an empty dependency list, because the node leans on the torch/numpy/Pillow ComfyUI already ships. No weights come with the pack either - MoGe's model file lives behind the core Load MoGe Model node. One caveat: this is a V3-API pack (comfy_api.latest, comfy_entrypoint(), no NODE_CLASS_MAPPINGS anywhere), so it needs a reasonably current ComfyUI or it won't register at all.

Where people get burned

The big one is expectation, not bugs. Past roughly 40 degrees of orbit on a single still, most of the frame is holes, because the source camera never saw those pixels. That's physics, not a setting - keep the move tighter or mask them and let an inpaint or video model fill the rest. video_mask exists precisely for that.

Smaller traps: if your photo was taken looking down or rolled, the orbit comes out tilted until you set tilt/roll on the pivot and straighten it. The point cloud only appears after the node has run once - before that you get a placeholder frame so you can block a move out with nothing wired up. After a run it's cached, so redrawing a different move is free until you change source or the geometry.

Categoryimage/video

Inputs (10)

NameTypeDefaultDescription
sourceIMAGEOne image or a batch of frames. Supplies the colour of the point cloud and the output resolution.
moge_geometryMOGE_GEOMETRYGeometry from Run MoGe Inference for the same image or frames.
frame_countINT1201–8192Number of output frames.
fpsFLOAT24.00.1–240Rate the editor's timeline and playback run at. The output is a plain image batch, so this does not change what is rendered; set the same rate on whichever node saves the video.
markersBOOLEANfalseBurn a lattice of small coloured spheres into the video. They sit in the scene with the geometry, so a downstream model can read the parallax from them. The editor shows them while this is on.
backgroundCOLOR#000000Colour of the pixels no point reached -- the holes video_mask marks. Match whatever your downstream model was trained on; CrossViewWarp-style warp videos use magenta, #ff00ff.
prune_depth_edgesBOOLEANtrueRemove depth-edge streaks, trading them for holes in video_mask. The preview updates immediately; run again to update the video.
preview_qualityCOMBOMedium (512)Editor preview detail only. Higher levels cost more while dragging. Updates immediately from the cache; output resolution is unchanged.
keyframesSTRING{ "version": 2, "pivots": [ { "id": "p1", "keys": [ { "frame": 0, "x": 0.0, "y": 0.0, "z": 1.0, "tilt": 0.0, "roll": 0.0, "heading": 0.0 } ] } ], "camera": [ { "frame": 0, "pivot": "p1", "pivot_target": "p1", "pivot_blend": 0.0, "azimuth": 0.0, "elevation": 0.0, "distance": 1.0, "lateral": 0.0, "height": 0.0, "pan": 0.0, "tilt": 0.0, "roll": 0.0, "lock": 0.0, "dolly": 0.0 }, { "frame": 48, "pivot": "p1", "pivot_target": "p1", "pivot_blend": 0.0, "azimuth": 20.0, "elevation": 0.0, "distance": 1.0, "lateral": 0.0, "height": 0.0, "pan": 0.0, "tilt": 0.0, "roll": 0.0, "lock": 0.0, "dolly": 0.0 } ] }Camera keyframes as JSON. The editor writes this for you.
camera_pathoptSTRINGOptional path from another node. Press Use input in the editor to load it; it never silently replaces what you have authored.

Outputs (3)

NameTypeDescription
camera_videoIMAGEThe rendered camera move, at the source resolution.
camera_pathSTRINGThe camera path as JSON. Feed it back into camera_path to reuse or route it.
video_maskMASK1 wherever no point was reprojected, including depth-pruning holes.