Plot Camera
Draw your own fly-through on the panorama, and this node flies it
- panorama
- moge_model
- control_video
- control_mask
- condition_dir
- camera_preview
- rail_json
This is the flagship node of SplatKit - the one that turns "here's a panorama" into "here's a camera path moving through a 3D scene." One 360° pano is a single viewpoint, and you can't reconstruct 3D from one viewpoint. So Plot Camera invents the missing viewpoints: MoGe estimates depth, the pano becomes a mesh, you draw a path on the panorama, and the node renders an equirectangular control video plus a validity mask along that path. The control video is what you feed to WAN (with the Matrix-3D pano LoRA) so the video model fills the disocclusion holes with temporally coherent content. Everything after - SphereSfM, COLMAP, the trained splat - descends from this one render.
The best part is the editor. Open the graph and the panorama shows a MoGe point cloud overlay; you drag anchor points for the camera to fly through - the author calls it WYSIWYG, and it is. Anchors are literal coordinates in the same scene units as the overlay (+Z forward into the pano, +X right, +Y up, origin = start camera), one x, y, z per line or as JSON. There's deliberately no collision guard: the camera goes exactly where you put it, so yes, you can fly into a wall. Keep moves small - a single pano only holds so much parallax, and big sweeps mean big holes for WAN to invent.
The inputs that matter
panorama- your equirect image. Feed the same image you'll feed WAN, so everything lines up.anchors- at least two points. This is where you spend your time.orientation-look_forward(faces the path tangent, default),look_at_target(every frame aims at one shared point you set with the orange "look" marker), orper_point_look(each anchor has its own target, the aim sweeps between them).length- frame count; must match your Wan Conditioning length (default 81). Nudge it and nothing downstream matches.moge_level- geometry detail from the depth network, 0–9. 6 is a balanced default; drop to 3–4 while placing anchors for speed, raise to 9 for a final sharp mesh.
Outputs: control_video and control_mask (the WAN conditioning pair), condition_dir (cameras.npz + depth/mask sidecars, a drop-in for Wan Conditioning + SphereSfM), a matplotlib camera_preview (top-down X-Z and side Z-Y plots of the path - sanity-check before committing to a WAN pass), and rail_json - the exact camera path. Wire rail_json into HiRes Composite, not condition_dir. Several plot nodes sharing one dataset_dir all write condition/, so only the last one's rail survives there; rail_json is this node's own per-node copy.
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
Restart. First run auto-downloads the MoGe checkpoint (~1.2 GB into ComfyUI/models/MoGe) and the SphereSfM binary is fetched by the SfM nodes, not this one. You supply the WAN 2.1 i2v checkpoint and the pano LoRA (converted with tools/convert_pano_lora.py → pano_video_gen_720p_comfy.safetensors in your loras folder).
Gotchas
- The prompt matters. It must describe the actual scene in the panorama; a wrong prompt visibly degrades what WAN paints into the holes. This is the #1 quality lever in the whole pipeline.
- Static paths won't triangulate. SphereSfM is real SfM and needs parallax - orbit, push-in, or spiral trajectories work; a pure pan does nothing.
- If the JS editor fails to load, the node still renders - the path is just a text widget. And note that
moge_levelonly sets how much detail the depth network sees per sub-view; it never affects output resolution, so drop it to 3–4 while placing anchors and raise it back for the final render.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| panorama | IMAGE | — | |
| anchors | STRING | 0, 0, 0 0.6, 0.1, 1.5 -0.4, 0.2, 3.0 0.3, 0.0, 4.5 | Fly-through points, in the SAME units as the geometry overlay. Frame: +Z forward/into the pano, +X right, +Y up; origin = start camera. The camera goes EXACTLY to each point you place against the cloud (WYSIWYG). One 'x,y,z' per line, or JSON [[x,y,z],...]. Need at least 2 points. |
| orientation | COMBO | look_forward | look_forward = camera faces the path tangent (default). 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; the aim sweeps between them (set the target interactively in the editor). |
| length | INT | 819–257 | Number of frames. MUST match the Wan Conditioning length (81). |
| moge_level | INT | 60–9 | Geometry detail: the MoGe depth inference resolution level (0-9). Higher = sharper depth edges / thin structures but slower (~2x cost from 0 to 9); lower = faster, softer geometry. 6 is a balanced default -- drop to 3-4 for quick anchor placement, raise to 9 for a final sharp mesh. Output resolution is unaffected; this only sets how much detail the depth network sees per panorama sub-view. |
| dataset_diropt | STRING | Wire the Dataset Project node here. When set, condition/ is written under it; otherwise it falls back to a default output folder. | |
| moge_ckptopt | COMBO | MoGe 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_modelopt | MOGE_MODEL | Optional: a pre-loaded MoGe model from the MoGe Model Loader node. Overrides moge_ckpt; load once and reuse across nodes. | |
| look_at_targetopt | STRING | 0, 0, 3 | The 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)
| Name | Type | Description |
|---|---|---|
| control_video | IMAGE | — |
| control_mask | IMAGE | — |
| condition_dir | STRING | — |
| camera_preview | IMAGE | — |
| rail_json | STRING | — |