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

Rung 1 — deep relief, 15–20 mm

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.

What breaks: nothing. Still a heightfield, still watertight by construction, still zero supports. Every line of branch A code runs unchanged. This rung is one number in config.py.
Rung 2 — the layered diorama

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.
What breaks: nothing, per panel. Each layer is still a heightfield. What is new is orchestration (a band loop in 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.
Rung 3 — objects in the round

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.

What breaks: everything downstream of the heightfield. 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.
Rung 4 — the scene as a maquette

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.

What breaks: the single-STL, single-print, single-material simplicity. This is a multi-part build with assembly, finishing, and judgement calls. It is also the only rung that produces something a photograph cannot flatten back into a picture.

2. What carries over

Branch A machineryRung 1Rung 2Rung 3Rung 4
depth cache (depth_cache.npz)yesyes — it is the band planindirectly — crop selectionyes — it is the placement plan
conditioning (heightfield.py)yesyes, per bandnofor the terrain
mesh.py + watertight guaranteeyesyes, per panelnoterrain only
no-supports guarantee (§7.2)yesyesvoidterrain only
§10 slicer settingsyes, more soyesnew profile, supports onboth
the polarity check, §6.5 self-testyesyesn/apartially

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