Atlas Planar Hole Patch ◩
Fill relief-mesh holes with conservative local planes
- solve
- hole_mask
- solve
- remaining_holes
- report
- created_islands
When you derive a relief mesh from a photo, you get holes - the spots an occluder hid from the camera, where the mesh has no data. The naive fix is to just let the mesh sink or smear there. Atlas Planar Hole Patch is the considered fix: it fits conservative local planes into the holes, using the valid geometry that already surrounds them. It's the ◩ in Atlas/06 · Patch & Repair, and it's the first chapter of the pack's repair story.
"Conservative" is doing a lot of work there. This node does not invent surfaces from nothing - it recovers the relief mesh's image-space lattice, groups the missing quads into enclosed components, finds a cluster of agreeing normals around each island, averages them into one patch plane, and generates the patch interior on exact camera rays. Accepted patches reuse the perimeter indices, so they become part of the relief mesh with projective UVs - not separate wall/box primitives floating in the scene.
How it works
The gate structure is the real design. Every hole island has to clear a stack of tests before it's filled:
- enclosed_only (default on) - reject components touching the image frame. The doctrine: frame and sky openings are not occlusion holes, they're clean-plate/background-layer territory.
- normal_tolerance_deg - max boundary-normal spread; lower is safer at corners, higher accepts rougher/curved surfaces.
- max_plane_error_m - max 95th-percentile residual for the densest plane-position band among the agreeing normals. This is the "does the plane actually fit" check.
- min_normal_support_fraction - the share of surrounding valid normals that must agree on one plane angle; lower lets you do one-sided facade repairs.
- max_hole_fraction - largest accepted component as a fraction of relief grid cells, which keeps big sky/background gaps out of the fill path.
- max_patch_edge_factor / max_patch_depth_factor - reject grazing ray spikes and forward/backward Z needles that would produce spikes or floating patches.
Rejected islands pass through unchanged - the docstring's word is that ambiguous multi-surface boundaries "pass through for a clean plate or hidden-geometry layer to handle." This node is honest about what it won't touch.
Inputs and outputs
Required: solve. Optional: hole_mask (leave unwired to treat every hole in the target layers as a candidate - what you want for a whole-solve sweep; the gates still bound what gets filled), layer (blank = primary relief; a name = that layer; * = every relief mesh, chaining one pass per layer), and the gate inputs above.
Outputs: solve (patches added), remaining_holes, report, and created_islands (a mask of what it actually filled).
Installing it
Core-tier node:
cd <ComfyUI>/custom_nodes
git clone https://github.com/mikejamesvfx/atlas-camera.git
Restart, find it in Atlas/06 · Patch & Repair (ComfyUI Manager: "atlas-camera").
Common issues
Run order matters: this node goes before AtlasRetopologizeLayer, so retopology sees the relief plus accepted patches as one mesh. Run it after and the retopo ignores your patches.
If holes aren't being filled, read the report before loosening the gates - most likely they're frame-touching (flip enclosed_only only if you genuinely want frame-edge fills) or too large for max_hole_fraction. And remember this node fills closed holes; the open-tear leftovers are AtlasPathGuidedHoleRepair's job, feeding a second pass with relaxed settings.
Inputs (12)
| Name | Type | Default | Description |
|---|---|---|---|
| solve | ATLAS_SOLVE | — | |
| hole_maskopt | MASK | Selected relief holes to patch — usually the hole_mask output of AtlasDeriveReliefMesh or AtlasCleanPlateLayer, optionally through a scope mask. LEAVE UNWIRED to treat EVERY hole in the target layer(s) as a candidate, which is what you want for a whole-solve sweep; the enclosed_only / max_hole_fraction / max_components gates still bound what actually gets filled. | |
| layeropt | STRING | Blank = primary relief mesh; a ProjectionSource name = that layer's relief mesh; '*' = every relief mesh (primary + all sources), chaining the solve through one pass per layer. | |
| ring_cellsopt | INT | 21–12 | How many valid grid rings around each hole supply normal and plane-fit support. |
| max_componentsopt | INT | 641–1024 | Maximum eligible hole islands to fit, smallest first. Frame/oversize rejections do not consume this budget. |
| normal_tolerance_degopt | FLOAT | 251–89 | Maximum boundary-normal spread. Lower is safer at corners; higher accepts rougher/curved surfaces. |
| max_plane_error_mopt | FLOAT | 0.150.001–10 | Maximum 95th-percentile residual in metres for the densest plane-position band among agreeing normals. |
| max_hole_fractionopt | FLOAT | 0.0500.0001–1 | Largest accepted component as a fraction of relief grid cells. Keeps large sky/background gaps out. |
| enclosed_onlyopt | BOOLEAN | true | Reject components touching the image frame. Recommended: frame/sky openings are not occlusion holes. |
| min_normal_support_fractionopt | FLOAT | 0.300.1–1 | Minimum share of surrounding valid normals that must agree on one plane angle. Lower values allow one-sided facade/occlusion repairs. Agreeing normals are averaged; max_plane_error_m then checks their positional fit. |
| max_patch_edge_factoropt | FLOAT | 201–100 | Reject a generated island when its longest triangle edge exceeds this multiple of its source-pixel span times the boundary's median metres-per-pixel. Prevents grazing camera-ray spikes at any image/camera scale. |
| max_patch_depth_factoropt | FLOAT | 2.01–20 | Reject a patch whose nearest/farthest camera depth extends beyond this multiple of the fitted local support depth band. Catches forward and backward Z needles independently of the edge-length gate. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| solve | ATLAS_SOLVE | — |
| remaining_holes | MASK | — |
| report | STRING | — |
| created_islands | MASK | — |