Street Lantern

showcase/street-lantern/

A procedural hanging street lantern through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

Rendered headless by the showcase piece itself. Select it to enlarge.

witnesses Recomputed: 4726 tris, three materials with 2162 metal, 24 glass and 341 brass faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.300×0.786×1.287 m, zmin 0, hygiene 0, brace BVH gap 0, brace ends 13 mm into the post and the arm, hanger-roof gap −1 mm, post 0.100×1.120 m plumb 0, LOD ratios in band (5.2 COLLAPSE 2 tris leaner on LOD2), convex collider 86 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z 16; --float-brace 17; --sink-arm 18; --rake-post 19; --shallow-brace 20.

category Village

tags mesh export showcase

blender --background --python showcase/street-lantern/street_lantern.py --

A showcase piece, not an example. Procedural hanging street lantern (stepped iron foot, tapered cast-iron column with ten flutes, two brass collars, a lamplighter's ladder bar with ball ends, a braced arm hung with two wrought C-scrolls and a forged hook ring, a muntined cage with amber panes between a brass eave band and base tray, square pyramidal brass roof, finial and drip) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

The arm axis, drop length, and roof stack are one closed-form chain: the roof peak stays below the arm, the hanger is the arm's far station, and the brace is an oriented box tenoned into the post wall and into the arm's lower half. The square pyramid is lofted on the cage axes, not a 4-gon cone. Cage posts and rails share a named overlap (CAGE_JOINT); glass sits behind the muntins by PANE_REBATE. Collars and the arm root evaluate post_radius_at(z).

The first build parked the brace's post end 0.35 × BRACE_T outside the post and its arm end under the arm, so a single bevelled corner grazed each host and the brace read as a loose stick in both side elevations. The brace-to-arm gap budget passed anyway, because a grazing corner is a zero gap, and nothing measured the post end at all. The brace ends now start half a brace inside the post wall and on the arm's centre-line side, and the brace-bite budget below asserts that each end is actually inside its host. Brass is aged and rougher (it was bright polished gold at roughness 0.28).

The model was the plainest in its asset sheet: a plain octagonal post and a bare box cage. The column is now fluted (POST_FLUTES channels, FLUTE_DEPTH deep, lands at the declared post radius so the post-size budget still reads 0.10 m) and broken by two collars; a ladder bar with ball ends passes through the neck; two C-scrolls weld into the arm's underside (each kept under 0.10 m on Y so the brace classifier never claims one); a hook ring rings the arm at the lantern station; and the cage gains a brass eave band, a base tray and a drip finial. Base triangles rose from 3338 to 4726 and the band was re-declared around the new model (below). One construction hazard is recorded in the script: a 0.008 m flute depth puts a groove vertex exactly on the arm root's cap centre, remove_doubles fuses post and arm into one shell, and the joint audit then finds neither.

The hero is lit from inside: a shadowless warm point light sits at the centroid of the glass faces and the panes emit at 1.6, so the lantern throws its glow onto the post, arm and floor. The camera is closer and lower on a 40 mm lens; the first hero left the lantern small in an empty frame. The hero yaws the piece HERO_YAW_DEG (90) so the arm reaches across both the hero camera and the asset-sheet camera; at the old −28° it pointed down the sheet camera's axis and the lantern read end-on.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 0.30 m square foot, 1.12 m fluted post (0.10 m across the lands at the base), 0.50 m arm, 0.24 × 0.30 m cage; outer AABB 0.300 × 0.786 × 1.287 m.

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

AxisDeclaredMeasured (4.5.11 / 5.2.1)
Base triangles4200–52004726 / 4726
LOD1 ratio0.32–0.62 of base0.4998 / 0.4998
LOD2 ratio0.10–0.35 of base0.2196 / 0.2179
Materialsexactly 3 distinct; ≥200 metal, ≥8 glass, ≥24 brass3 slots; 2162 / 24 / 341
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.300, 0.786, 1.287) m ± 0.01(0.3000, 0.7860, 1.2874), zmin 0
Collider tris≤ 12086
Exportwritten, size > 0, removed after measuring353488 / 353488 / 353480 bytes (4.5.11 / 5.1.2 / 5.2.1)

Base triangles dropped from 3520 to 3338 in the first quality pass (closed square-pyramid roof, unshared cage crossings, pane rebate), then rose to 4726 when the model gained its column flutes, second collar, ladder bar, arm scrolls, hook ring, eave band, base tray and drip. The band moved from 3000–3800 to 4200–5200, the same ~±10 % slack around the measured count. The 5.1.2 column was dropped with this change: the new model was measured on 4.5.11 and 5.2.1 only.

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 8 tris leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Post plan0.100 m across × 1.120 m high ± (0.02, 0.04)0.1000 × 1.1200
Post plumb (top vs bottom XY)≤ 0.008 m0.00000

Joint fit and seat#

AxisDeclaredMeasured (all three)
Brace-to-arm BVH gap≤ 0.006 m0.00000
Brace bite: deepest brace vertex inside the post / inside the arm (signed distance to the host shell)≥ 0.004 m each0.01305 / 0.01282
Arm underside vs roof peak≥ 0.008 m clearance0.05300
Hanger-to-roof BVH gap≤ 0.004 m−0.00100
Scroll weld: deepest vertex of each of the 2 arm scrolls inside the arm≥ 0.004 m each, exactly 2 scrolls0.01709

Falsifiers#

Each violates one named budget. After the model change every falsifier was re-run on 4.5.11 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--float-bracebrace-to-arm BVH gap17
--sink-armarm-over-roof clearance18
--rake-postpost plumb19
--shallow-bracebrace bite (the first build's ends: −0.00638 m into the post, 0.00000 m into the arm)20
--float-scrollscroll weld (scrolls dropped 0.03 m: −0.00873 m)21

--sink-arm lowers only the arm. Hang-station hang_z stays at post top so the cage does not follow, which is what lets the clearance budget fire.

The brace-bite check runs last, after plumb, so a raked post is reported as plumb (19) rather than as a brace that no longer reaches it.

Run#

blender --background --python street_lantern.py --
blender --background --python street_lantern.py -- --skip-decimate
blender --background --python street_lantern.py -- --stray-vert
blender --background --python street_lantern.py -- --lift-z
blender --background --python street_lantern.py -- --float-brace
blender --background --python street_lantern.py -- --sink-arm
blender --background --python street_lantern.py -- --rake-post
blender --background --python street_lantern.py -- --shallow-brace
blender --background --python street_lantern.py -- --float-scroll
blender --background --python street_lantern.py -- --output lantern.png

Smoke passes no flags.

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family; 20–21 are file-local.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 3 distinct slots, or a face-count floor missed
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Mesh hygiene: loose, non-manifold, zero-area, doubles, n-gons, z-fight
16Not grounded: bounding box zmin off 0
17Joint fit: brace-to-arm gap (--float-brace)
18Seat: arm-roof clearance or hanger-roof gap (--sink-arm)
19Post plumb or post size off the stated real-world size (--rake-post)
20Brace bite: a brace end not inside the post or the arm (--shallow-brace)
21Scroll weld: an arm scroll not welded into the arm, or not exactly two (--float-scroll)
22Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/street-lantern/street_lantern.py View the script on GitHub →
A cast-iron street lamp: fluted post with brass collars and a ladder bar, a braced arm with wrought scrolls, and a banded brass lantern lit amber from inside.