From Relief to Sculpture
Branch B of Painting → Bas-Relief — the route from a heightfield to a
thing you can walk around
A map, not a walkthrough — those get written after prints come off the plate.
0. The branch point
Everything in branch A rests on one assumption, stated in §7.2 of the guide: the surface is a
function z = f(x, y) — one height per pixel, no undercuts, every
point visible from directly above. That assumption is what makes the mesh watertight by construction,
supports unnecessary by construction, and the whole pipeline a matter of image processing rather than
geometry.
A sculpture breaks that assumption. Getting from relief to sculpture means giving it up — and you can give it up in instalments.
BRANCH A (the build guide) BRANCH B (this page)
z = f(x, y), one height per pixel the heightfield assumption dies
flat relief -> carved relief -> deep relief -> layered -> objects -> the scene
the recipe, guide §12-13: 15-20 mm diorama, in the as a
5 mm 8 mm, engraved (rung 1) real air round maquette
(guide ends here) (rung 2) (rung 3) (rung 4)
Each rung below states what you get, what it costs, and — the important line — which invariant of branch A breaks there. Nothing breaks until rung 3, which is why rungs 1 and 2 are printable immediately and rung 3 is a project.
1. The rungs
The build guide’s carving recipe (§12–13), pushed. RELIEF_MM = 15, perhaps 20; BASE_MM = 3 as in §13.
With the background-envelope subtraction in place, the extra millimetres go
to the object band rather than to the ramp — trunks become half-cylinders you can hook a
fingertip behind the silhouette of, hunters become figures rather than badges. High relief in the
sculptor's sense (haut-relief: more than half the implied depth of the form standing free)
starts around here.
Practical notes: the A1 mini's 180 mm Z is nowhere near a constraint; print time scales roughly
with relief volume (expect 9–14 h at 15 mm); consider BG_SUPPRESS 0.9 → 0.95
and a wider BG_WINDOW_MM (25–30) so the taller budget is spent on objects, not on
re-inflating the ramp. Side walls of tall terraces become genuinely visible surfaces — the
outer-wall speed/acceleration advice in §10 of the guide matters more here, not less.
config.py.The move that buys real, physical, parallax-bearing depth without any new mathematics: split the scene into 3–6 depth bands, condition and print each band as its own relief panel, and mount the panels with real air between them — 3–6 mm spacers, shadow-box style. The foreground trees literally stand in front of the village; move your head and the scene moves. No simulated lamp required: the parallax and the cast shadows are physical.
The depth map you already have contains the whole plan. Band thresholds come from the cached depth map's histogram; each band's mask selects its pixels; each layer runs through the existing conditioning with its own shallow relief (3–4 mm per panel reads well). Two problems and their fixes:
- Occlusion holes. The painting does not know what is behind a tree. Where a foreground band is cut away from a background band, the background has a tree-shaped hole. In a shadow box this mostly does not matter — the hole hides exactly behind the object that caused it — but oblique viewing angles reveal slivers. The fixes, in increasing effort: accept it (correct for a first build); dilate each background band a few mm so it overlaps behind its occluder; or inpaint the holes in image space before conditioning (any current inpainting model is adequate for filling snow and sky) — verify tooling at build time rather than trusting this page's date.
- Band edges. A hard depth threshold cuts objects in half where they straddle it (a tree rooted in band 1 with branches in band 2). Bands should be chosen at the depth map's natural valleys — the histogram between the foreground cluster and the village cluster is nearly empty, which is exactly where to cut — and the guided filter's edge-snapping already keeps object boundaries coherent within a band.
main.py, ~30 lines), frames/spacers
(design freedom: a printed frame is one more STL), and assembly. The watertightness and no-supports
guarantees hold for every part.Here the heightfield dies, because you are asking for the back of something the painting never painted. A single-image-to-3D model invents it. This is no longer speculative tooling: as of early 2026, Microsoft's TRELLIS.2 (MIT licence, 4B parameters) runs natively on Apple Silicon via the trellis-mac port — 24 GB+ unified memory recommended; roughly 3–5 minutes per object on an M4-class chip; output is a textured GLB mesh (~400 k vertices). Alternatives in the same family (Hunyuan3D, TripoSR for lighter machines) exist; evaluate at build time — this corner of the field moves fast enough that any specific ranking written today is stale in months.
The realistic subject is not the whole painting but its actors: crop the hunters-and-dogs group, or one tree, remove the background, feed the crop, get a statuette. The caveats: the back of the object is plausible invention, not Bruegel; fidelity ends at the silhouette; and the background-removal models these pipelines bundle are often non-commercial-licensed (trellis-mac's RMBG-2.0 is CC BY-NC) — irrelevant for a private print, relevant for anything published or sold.
mesh.py no longer applies (the model emits its own mesh, which arrives non-watertight
— hole-filling is one of trellis-mac's stated gaps); supports are now genuinely needed
(undercuts exist at last, and §7.2's guarantee is void by design); repair and orientation happen in
Blender/MeshLab. This is the standard workflow for photogrammetry and generated meshes; any established
repair-and-support guide for that class of mesh transfers wholesale.The composition of the two rungs above, and the end-state of this branch: a rung-1 deep relief as the terrain and backdrop, rung-3 statuettes of the principal actors placed on it — the painting as a stage set. This is how theatre designers and diorama artists have always solved “a painting in three dimensions,” and it degrades gracefully: every object you have not yet generated simply stays in the relief.
The depth map is the placement plan. A pixel's depth value locates its object's standoff from the backdrop; the object's printed footprint locates it in plan. Scale consistency between a generated statuette and the relief behind it is the one genuinely fiddly part — match silhouette heights against the relief's own rendering of the same object before gluing anything.
2. What carries over
| Branch A machinery | Rung 1 | Rung 2 | Rung 3 | Rung 4 |
|---|---|---|---|---|
depth cache (depth_cache.npz) | yes | yes — it is the band plan | indirectly — crop selection | yes — it is the placement plan |
conditioning (heightfield.py) | yes | yes, per band | no | for the terrain |
mesh.py + watertight guarantee | yes | yes, per panel | no | terrain only |
| no-supports guarantee (§7.2) | yes | yes | void | terrain only |
| §10 slicer settings | yes, more so | yes | new profile, supports on | both |
| the polarity check, §6.5 self-test | yes | yes | n/a | partially |
3. The first milestone, concretely
Rung 2, minimum version: a three-band diorama at 85 mm width — the test-strip philosophy of §10 applied to the branch. Foreground (hunters, dogs, the big trees), middle (village, ponds, skaters), far (mountains and sky). Three panels at 3 mm relief each, two 3D-printed spacer frames at 5 mm, one evening of printing, no new tools installed. The band loop:
# sketch — a band loop around the existing stage-4 conditioning
BANDS = [(0.55, 1.00, "1_foreground"), # thresholds on the cached depth map, chosen
(0.25, 0.55, "2_middle"), # at the histogram's natural valleys
(0.00, 0.25, "3_far")]
for lo, hi, name in BANDS:
mask = (depth_band := np.clip((d_raw - lo) / (hi - lo), 0, 1)) * ((d_raw >= lo) & (d_raw < hi))
band = condition(mask) # the existing guided/equalize/compress/sculpt chain
band = hf.apply_rim(band, C.RIM_MM, C.PITCH_MM)
mesh_and_export(band, f"{name}.stl", relief_mm=3.0)
That is the entire new code surface of rung 2's minimum version — a masked variant of the
conditioning loop the build guide’s main.py already runs. If the 85 mm version earns it, the full-size
build and the occlusion-dilation refinement follow; if it does not, the branch has cost one evening.
Decision heuristic for the whole branch: climb a rung only when the current one has produced a physical object you have judged in the hand. Every rung is retreat-safe — nothing above it invalidates the prints below it.
References
- The build guide (branch A) and its Poisson side note — all of rungs 1–2 run on that machinery.
- trellis-mac — TRELLIS.2 image-to-3D on Apple Silicon (MIT port; upstream model MIT; bundled RMBG-2.0 background removal is CC BY-NC). Facts checked 2026-08-02; re-verify before installing.
- Weyrich et al. (2007), Digital Bas-Relief from 3D Scenes — branch A's core, and the reason rung 1 is cheap: relief compression was designed to scale.