Atlas Conditioning Bundle π
Hand the video model pixel motion, not the words 'slow dolly left'
- solve
- source_image
- camera_path
- exclude_mask
- bundle
- projected
- depth_preview
- normal_preview
- flow_preview
- position_preview
- valid
- ghost
- report
Every hosted video platform ships a "camera control" box that takes a sentence. Slow dolly left, 35mm, a bit of tilt. The model then improvises what that means for your shot. Atlas Conditioning Bundle π is the other approach: it computes the exact displacement of every pixel and hands that over instead.
What it does
Feed it a solve that carries serialized projection meshes, the plate, and optionally a camera path. It renders each view of the move ONCE and reads metric depth, world normals, world position, forward and backward flow, per-frame K, and the ghost class map off the same z-buffer that chose the render's colours. That's the design point - no two passes can disagree with each other, or with the RGB, about what the geometry is.
Flow is derived, never estimated. With depth and two calibrated cameras the displacement is closed-form, and the unit test pins it against -fxΒ·t/Z at exactly 0.0e+00 error. A learned flow network here would be estimating a quantity Atlas already knows. The occlusion flag tests the target frame's own depth rather than inferring occlusion from flow inconsistency, which is strictly stronger: it knows which pixels became occluded, not just which look weird.
Why ATLAS_CONDITIONING is its own socket
You'll notice you can't wire bundle into an IMAGE node, and that's deliberate. Metric depth in metres and flow in pixels both leave [0, 1], and every preview or save hop would quantise them. ComfyUI refusing the wire is the protection, not a bug. This is a different animal from a ControlNet depth pass, where a normalised per-frame map is exactly what you want.
Practical consequence worth knowing before you build: in this pack, ATLAS_CONDITIONING is consumed by AtlasWriteConditioningEXR and AtlasAdherenceScore. So today this node is a measurement-and-export front end - conditioning you can inspect, score, and hand to a comp - not a drop-in for every video pack's control input.
Inputs and outputs
Required: solve (must carry serialized projection meshes - run AtlasDeriveProjectionGeometry or clean-plate layers first; without geometry the node refuses), source_image (the plate).
Optional, and these are the ones you'll actually touch:
camera_path- the move to measure. Without it you only get the solved camera rendered, which has no flow and no disocclusion by definition. Fine as an identity check, useless as conditioning.resolution(1024, long edge) - the rasterizer is pure numpy and O(faces Γ pixels) per frame, and it renders every frame. On long moves over dense relief meshes, keep this moderate or usestride.hole_dilate_px(0) - grows the uncovered region before classifying. A z-buffered render leaves one-pixel seams along silhouettes; a few px turns speckle into coherent blobs. It only ever marks more as unseen, so it can't invent coverage.exclude_mask- sky on a matte, a SkyDome. Excluded pixels classify LOW_CONFIDENCE and leave the fractions entirely.last_n(0 = every frame) andstride- measure the tail of a move, or every Nth frame. Flow is then computed between the KEPT frames, so magnitudes are per-stride, and the report says so rather than letting you misread it.emit_flow,emit_normals,emit_position(all true).
Outputs: bundle β AtlasWriteConditioningEXR or AtlasAdherenceScore; projected; depth_preview, normal_preview, flow_preview, position_preview (display only); valid (MASK); ghost (MASK); report (JSON).
One detail that saves a support thread: the previews print their normalisation constants in report. Depth near/far is per sequence, not per frame, because a per-frame stretch makes a dolly look static; flow is at a fixed pixel scale, because an auto-scaled preview hides the magnitude change you're inspecting.
Install
cd <ComfyUI>/custom_nodes
git clone https://github.com/mikejamesvfx/atlas-camera.git
Or Manager β atlas-camera (ComfyUI Registry). Restart. No pip step for the core nodes; the solve that feeds this wants numpy, so you're fine on a stock ComfyUI. MIT, Python 3.10+. The quickstart example in the repo (examples/atlas_input_quickstart_workflow.json) is the sane way to see the upstream half before wiring this in.
Traps
resolution Γ frame count is the real cost, so a 200-frame move at 2048 will make you think the node hung. And a keyed zoom behaves correctly-but-surprisingly: radial flow, a stationary eye, and legitimately no disocclusion, because a focal change moves no eye and reveals nothing.
Inputs (11)
| Name | Type | Default | Description |
|---|---|---|---|
| solve | ATLAS_SOLVE | A solve carrying serialized projection meshes β run AtlasDeriveProjectionGeometry or clean-plate layers upstream. Without geometry there is nothing to measure and this node refuses. | |
| source_image | IMAGE | The plate. Textures the primary geometry, and sets the baseline the class map is measured against. | |
| camera_pathopt | ATLAS_CAMERA_PATH | The move to measure along. Without one only the solved camera is rendered, which has no flow and no disocclusion by definition. | |
| resolutionopt | INT | 1024256β4096 | Long edge. The rasterizer is pure numpy and O(faces x pixels) PER FRAME, and this renders every frame β keep it moderate on long moves over dense relief meshes, or use stride. |
| hole_dilate_pxopt | INT | 00β64 | Grow the uncovered region before classifying. A z-buffered render leaves one-pixel seams along silhouettes; a few px turns speckle into coherent blobs. Only ever marks MORE as unseen, so it cannot invent coverage. |
| exclude_maskopt | MASK | Region already handled by something other than geometry β sky on a matte or a SkyDome. Excluded pixels are classified LOW_CONFIDENCE and left out of the fractions entirely. | |
| last_nopt | INT | 00β4096 | Measure only the move's last N frames (0 = every frame). The end of a move is where disocclusion peaks. |
| strideopt | INT | 11β64 | Measure every Nth frame. Flow is then computed between the KEPT frames, so its magnitudes are per-stride, not per-source-frame β the report says so. |
| emit_flowopt | BOOLEAN | true | Forward and backward flow, derived from geometry. Costs one back-projection and two projections per frame. |
| emit_normalsopt | BOOLEAN | true | β |
| emit_positionopt | BOOLEAN | true | β |
Outputs (9)
| Name | Type | Description |
|---|---|---|
| bundle | ATLAS_CONDITIONING | β |
| projected | IMAGE | β |
| depth_preview | IMAGE | β |
| normal_preview | IMAGE | β |
| flow_preview | IMAGE | β |
| position_preview | IMAGE | β |
| valid | MASK | β |
| ghost | MASK | β |
| report | STRING | β |