Watchtower

showcase/watchtower/

A procedural timber watchtower 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: 6840 tris, three materials with 300 metal and 126 roof faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.594×1.594×2.973 m, grounded zmin, 4 iron shoes, rail engage 18 mm, all four side rails tenoned at both ends, both joists bearing 31 mm into deck girts, collar standoff 2 mm, post plumb 0, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 122 tris, non-empty glTF. Hygiene all zero. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-shoes 16; --short-rails/--open-sides/--short-joists 17; --float-band 18; --rake-posts 19.

category Village

tags mesh export showcase

blender --background --python showcase/watchtower/watchtower.py --

A showcase piece, not an example. Procedural timber lookout (corner posts through a plank platform, lower-bay X-braces, hatch and ladder whose stiles plant at Z=0, coursed shake roof over a solid cone, mitered iron collars and shoes) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

The first quality pass left the platform floating: there were no bearers at deck level, the two joists stopped 0.19 m short of the front edge and at the back edge in the air, the plank ends hung past nothing at the sides, and the side guardrails stopped 0.30 m short of the front posts for a "hatch clearance" the hatch never needed (it sits at the centre of the front edge). Four deck girts now tenon into the posts under the planks, the joists run girt to girt, the hatch trims end on the front girt, and the side rails span post to post like the back rail. Two budgets assert it. Wood carries grain and a tone per piece, the shakes are weathered, and the iron is rusted rather than chrome.

The old roof was a wood U with incomplete shake coverage; rails showed through the eave. The cone plus non-overlapping courses close the pyramid. Shoes are added after the zmin snap so a bevel undershoot cannot lift them.

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).

Budgets#

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

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles6200–82006840 / 6840 / 6840
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2187
Materialsexactly 3 distinct; ≥80 metal, ≥40 roof faces3 slots; 300 metal, 126 roof
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.594, 1.594, 2.973) m ± 0.01(1.5940, 1.5940, 2.9731), zmin 0
Collider tris≤ 180122
Exportwritten, size > 0, removed after measuring492624 / 492624 / 492616 bytes (4.5.11 / 5.1.2 / 5.2.1)

Base triangles rose from 5220 to 6408 in the first quality pass (a taller eave-true roof and coursed shakes) and to 6840 in the second (four deck girts), not hidden interior faces.

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
Named supports: 4 iron shoeseach zmin ≤ 1e-34, shoe_z 0.00000
Plan1.56 m eave × 2.97 m height ± 0.08 / ± 0.281.5940 × 2.9731

Joint fit and seat#

AxisDeclaredMeasured (all three)
Rail-to-post engage (back rails)≥ 0.012 m0.0180
Side-rail engage: 4 side rails, each tenoned into the front and the back post≥ 0.012 m at both ends0.0180
Deck bearing: 4 deck girts; each of 2 joists inside the front and the back girt≥ 0.012 m at both ends0.0315
Girt-collar standoff≤ 0.010 m0.00200
Post plumb (XY drift)≤ 0.010 m0.00000

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 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; the per-piece wood tone is drawn from a seeded random.Random(TONE_SEED), the same every run. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.

Falsifiers#

Each violates one named budget. Every falsifier was run on 4.5.11, 5.1.2 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
--short-shoesnamed shoe supports at Z=016
--short-railsrail-to-post joint fit17
--open-sidesside-rail engage (front ends stop 0.30 m short again: −0.2460 m)17
--short-joistsdeck bearing (joists back to the old span, girts kept: −0.1625 m)17
--float-bandiron-collar seat18
--rake-postspost plumb19

Run#

blender --background --python watchtower.py --
blender --background --python watchtower.py -- --skip-decimate
blender --background --python watchtower.py -- --stray-vert
blender --background --python watchtower.py -- --lift-z
blender --background --python watchtower.py -- --short-shoes
blender --background --python watchtower.py -- --short-rails
blender --background --python watchtower.py -- --open-sides
blender --background --python watchtower.py -- --short-joists
blender --background --python watchtower.py -- --float-band
blender --background --python watchtower.py -- --rake-posts
blender --background --python watchtower.py -- --output tower.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.

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 metal/roof faces missing
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, or a named shoe floats
17Joint fit: rail-to-post engage, side-rail engage, or deck bearing
18Seat: girt-collar standoff
19Post plumb or plan off the stated real-world size
20Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Hero#

The still used to tilt the tower 2° about X, which the showcase rule "level on the stage" forbids (it sinks one row of shoes and lifts the other); it now turns about Z only. The camera moved in and down (fill y 0.856, was ~0.77) so the tower reads as tall rather than lost in the frame, and the shake roof went from flat grey to weathered cedar brown with silvered wear. Geometry and budgets are unchanged.

Source

showcase/watchtower/watchtower.py View the script on GitHub →
A grained timber watchtower on braced legs, seen from low and close, with a ladder, a railed platform on deck girts, rusted iron collars and a weathered cedar shake roof.