SplatCam · Image → 3D Camera
One image in, a camera you can fly out — SplatCam hands your video model a real camera move
- image
- model_3d
- image
- mask
- depth
- video
Prompting a video model to "orbit around the subject" is a coin flip. It'll do a pan, or a slow zoom, or drift off somewhere you never asked about, and you can't correct it without burning another generation. SplatCam takes the opposite route: instead of describing the camera move in words, you perform it. One image in, and it builds a 3D Gaussian splat you can orbit, zoom, and pan with your mouse - then it hands the recorded flight to your video model as a camera-motion reference. The name is accurate in a way that matters: it turns an image into a camera.
The whole thing is one node. You feed it a single image, and SplatCam sends it to fal.ai's hosted TripoSplat to build the splat (~$0.05, cached so the same image never bills twice), or you feed model_3d instead - your own splat, a 3DGS .ply/.splat from a gsplat run or Postshot, or a glb/obj/stl mesh - and fal is never called at all. That fully-local path is the sleeper feature. It means you get a polished 3D viewport with zero CUDA install pain, which is a genuinely rare sentence in the image-to-3D corner of this ecosystem (see the ComfyUI-3D-Pack story in the KB for how bad this usually gets).
How it works
The trick that trips people up is that it's a two-queue node. ComfyUI executes on the server, but your camera lives in the browser, so:
- Queue once. The splat builds (or loads) and appears in a live viewport rendered right on the node - a self-contained WebGL2 splat renderer, no CDN, one sorted pass that produces colour, silhouette, and depth together via dual render targets.
- Fly. Drag to orbit, scroll to zoom, shift-drag to pan. The mask and depth previews track the camera live.
- Record. Pick
record_mode, hit ● Record, fly your move, hit ■ Stop. - Queue again. The node emits your take. The splat cache makes this second queue free and instant.
Recordings are deterministic: locked to capture_width × capture_height (not your on-screen window), resampled to a constant record_fps with the real wall-clock duration preserved even when the browser drops frames. Meshes are surface-sampled into Gaussians locally, so geometry, depth, and mask stay exact - only colour comes from the texture.
Inputs and outputs that matter
The honest short list: image or model_3d (connect one; if both, the 3D wins), record_mode (rgb, mask, or depth - depth is the one you'll actually want, more below), and capture_width / capture_height, which size both the emitted stills and your recordings.
Everything else has sensible defaults. num_gaussians (262,144) is the TripoSplat target and the sampling density for mesh inputs. fal_seed of -1 lets fal choose - but changing it is a new cache key and therefore a new paid generation, which is exactly the trap. force_regenerate bypasses the cache and bills you again; you almost never want it. And viewport_state is machine-managed - the node stashes its splat token and camera there, so don't edit it by hand.
Four outputs, all from the exact angle you chose: image (the splat rendered from your viewport), mask (silhouette, 1 where the model covers a pixel), depth (near surfaces bright), and video - the recorded orbit as clean constant-rate h264, wired straight into Save Video. Note that video is empty until you actually record a take; if you haven't, downstream save nodes are politely skipped rather than writing black frames.
Install
ComfyUI Manager (search SplatCam), or manually:
cd ComfyUI/custom_nodes
git clone https://github.com/tahabahrami/ComfyUI-SplatCam
/path/to/ComfyUI/python -m pip install -r ComfyUI-SplatCam/requirements.txt
Restart and hard-refresh the browser. Dependencies are aiohttp, Pillow, trimesh, and PyAV (av) for the transcoding - all pip-installable, nothing compiled, no CUDA gauntlet. If you use the image path you'll also need a fal key, configured once:
python3 ComfyUI-SplatCam/scripts/configure.py
It's stored owner-only (0600), never in a workflow or log, and FAL_KEY in the environment overrides it.
The camera-reference recipe
This is the use case the README argues hardest, and the argument is sound. Set record_mode to depth - a depth take carries only geometry and camera motion, so a video model has no colours, textures, or background to copy by accident. Then give the model your source image as a reference image (not a first frame - those are competing modes and the frame wins), and tell it in the prompt that the reference video is only a camera-motion reference, matching orbit direction, speed, and timing exactly. Ask for the same duration you recorded so the motion maps 1:1. Two gotchas: Seedance 2.0 enforces a ~409,600-px minimum reference resolution, so keep capture at 640×640 or larger - and some providers reject reference videos containing realistic human faces, stylised or otherwise.
The name is a little misleading in one direction: it doesn't export meshes or splat files. It's deliberately splat-native - a camera tool, not an asset exporter. If you need the mesh, use a mesh-generation node; if you need the camera, this is the node.
Inputs (12)
| Name | Type | Default | Description |
|---|---|---|---|
| num_gaussians | INT | 26214432768–262144 | Gaussian count: requested from TripoSplat for image inputs, and used as the surface-sampling density for mesh inputs. |
| num_inference_steps | INT | 201–50 | Flow-matching sampler steps. |
| guidance_scale | FLOAT | 3.00–10 | Classifier-free guidance strength. |
| fal_seed | INT | -1-1–2147483647 | Deterministic seed sent to fal; -1 omits it. Changing this is a new cache key and therefore a new paid generation. |
| capture_width | INT | 102464–4096 | Pixel width of the emitted view, mask, and depth images. |
| capture_height | INT | 102464–4096 | Pixel height of the emitted view, mask, and depth images. |
| record_fps | INT | 241–60 | Frame rate used when recording and transcoding an orbit take. |
| record_mode | COMBO | rgb | Which pass the recorded take captures. 'depth' gives a video models can read as pure camera motion and geometry, with no colour or texture to copy; 'mask' records the silhouette only. |
| force_regenerate | BOOLEAN | false | Pay fal for a fresh splat even when this exact image and settings are already cached locally. |
| viewport_state | STRING | Managed by the node's viewport. Holds the splat token, camera, and capture handles; never edit it by hand. | |
| imageopt | IMAGE | Source image; fal TripoSplat builds the splat (~$0.05, cached). Connect one IMAGE; the first of a batch is used. Not needed when a splat is connected. | |
| model_3dopt | SPLAT,MESH,FILE_3D,FILE_3D_SPLAT_ANY,FILE_3D_PLY,FILE_3D_SPLAT,FILE_3D_GLB,FILE_3D_GLTF,FILE_3D_OBJ,FILE_3D_STL | Bring your own 3D — fully local, fal is never called. Accepts a SPLAT, a MESH (e.g. from a local image-to-3D model), a 3DGS .ply/.splat, or a glb/gltf/obj/stl mesh file (surface-sampled into Gaussians). Takes precedence over the image input. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| mask | MASK | Splat silhouette: 1 where the model covers a pixel, 0 elsewhere. |
| depth | IMAGE | Alpha-weighted depth of the current view; near surfaces are bright. |
| video | VIDEO | Recorded orbit as MP4. Empty until a take is recorded. |