Wooden Ladder

showcase/wooden-ladder/

A procedural timber ladder with raked tapered stiles, six turned rungs, ferrule caps, and level shoe plates 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: 3000 tris, two materials with 684 wood and 976 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.490×0.376×1.487 m, zmin 0, hygiene and coplanar pairs 0, tenon clearance 8 mm, shoe bite 20 mm, rung pitch even to 0.002 m, LOD ratios in band, convex collider 68 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert and --twin-sole exit 15; --lift-z exits 16; --fat-rungs exits 17; --short-stile exits 18; --drift-rungs exits 19.

category Village

tags mesh export showcase

blender --background --python showcase/wooden-ladder/wooden_ladder.py --

A showcase piece, not an example. Procedural timber ladder: two stiles tapered from 64 mm to 50 mm, raked 12°, six turned hickory rungs (a barrel that shoulders in just under its radius and swells 14 % at mid-span, the profile a lathe leaves on a ladder round) through-tenoned into oak rails with their end grain showing 2 mm proud of the outer faces, two iron tie-rods under the bottom and top rungs clamped by square nuts over round washers, iron ferrules at the top, and level shoe plates under sleeves that follow the rake. Then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

Hero. The ladder leans on a render-only barn wall of staggered weathered boards (a Brick texture turned onto the wall plane, with a stretched noise for grain), and a hand-coiled rope hangs over the second-highest rung. The rung is found the way rung_pitch finds rungs: the wooden shells wide in X and short in Z. The coil is a teardrop laid from a three-lobed section, per the showcase rope rule. It hangs forward of the rung, away from the wall. The wall and the coil are staging, not part of the asset, its budgets or its export. The asset sheet judges the bare LadderLow.

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

Intended size: stile length 1.50 m, overall about 0.49 × 0.38 × 1.49 m. A household ladder, not a 3 m orchard ladder.

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). Coplanar-pair counting matches showcase/signpost.

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; 5.1.2 not re-run after the tie-rod pass)
Base triangles1800–31003000 / 3000
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2200 / 0.2200
Materialsexactly 2 distinct, ≥280 wood, ≥180 metal2 slots, 684 wood, 976 metal
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.490, 0.376, 1.487) m ± 0.01(0.4903, 0.3760, 1.4870)
Groundedabs(zmin) ≤ 1e-4, each sole at 0, rail end ≥ 0.012 mzmin 0, sole 0, rail 0.02037
Mesh hygienenon-manifold, loose v/e, doubles at 1e-5, zero-area, n-gons, coplanar pairs all 0all 0
Parts26 shells (2 stiles, 6 rungs, 2 sleeves, 2 cap sleeves, 2 plates, 2 soles, 2 tie-rods, 4 washers, 4 nuts)26 (2 / 6 / 2 / 2)
Rung depth clearance≥ 0.003 m inside the stile faces0.00800
Tenon engagement≥ 0.008 m past the stile inner face0.05387 (through)
Through-tenon proudtenon end 0.001–0.004 m past the stile outer face0.00199
Shoe bite / coverbite ≥ 0.010 m, cover ≥ 0.016 m0.02000 / 0.03800
Rung pitch6 rungs; every rung-to-rung gap within 0.002 m of the mean6; 0.00000
Tie-rod seat2 rods, 4 washers, 4 nuts; washer into stile and nut into washer 0.0002–0.002 m; on the rod axis within 0.0015 m; thread ≥ 0.003 m proud of each nut2 / 4 / 4; bite 0.00049–0.00051; axis 0; thread 0.00450
Collider tris≤ 18068
Exportwritten, size > 0228568 / 228568 / 228560 bytes (4.5.11 / 5.1.2 / 5.2.1)

Base triangles rose from 936 to 2536. The previous mesh was two boxes, six cloned dowels, and four chrome cubes. Turned rungs (tenon and barrel in one shell) plus ferrule collars and chamfered soles added the triangles. Nothing was removed as a hidden face.

A later pass took them to 2680. The bare ladder was still the plainest object in its asset-sheet lineup, and the parts budget is exact (16 shells), so the detail had to live inside existing shells. Each rung gained one ring at mid-span (+24 triangles per rung): the barrel shoulders in at 0.90 × its radius (RUNG_SHOULDER) and swells to 1.14 × (RUNG_SWELL). That turns a dowel into a lathe-turned round. The tenons are unchanged, so clearance, engagement and pitch read the same. The breakout margin moved 0.07 mm (0.03780 → 0.03787) because the shell's worst vertex is now measured across seven rings. The wood shader also changed. The grain noise runs across the stile's 50 mm width at a readable scale (WOOD_GRAIN_SCALE 34 → 95, streak stretch 0.94 → 0.985); before, about one noise period spanned a stile, so it rendered as flat tan. A short-range AO term darkens the rung housings and the wood under the iron sleeves.

A third pass changed the model itself. The bare ladder was still the plainest object in its asset sheet, because the exact parts budget left every detail inside the existing shells. The parts budget is now 26: two iron tie-rods run through both rails under the bottom and top rungs, each end clamped by a square nut over a round washer bedded on the rail's outer face. That is the rod a wooden ladder carries to stop the rails spreading off the rungs. The triangle ceiling rose 2800 → 3100 for the hardware (+320 triangles; the old ceiling had 120 to spare). The rungs are now through-tenoned: each tenon runs right through its rail and stands 2 mm proud, so the old breakout margin (a blind tenon stopping short of the outer face) became a through-tenon proud band, and the tenon's inboard ring moved to 4 mm inside the rail so the joint audit always samples tenon section in the wood. The shader reads a per-shell Species attribute: rungs are pale hickory against darker oak rails, and end grain (the tenon ends, the rail tops) takes the finish darker. The rung finders now require the wood material, because the tie-rods are as wide as a rung. The new tie-rod seat budget gets its own falsifier, --float-nuts (exit 20).

The joint budgets are measured per rung against each stile, from vertex positions in the un-raked construction frame, and the reported value is the worst case. Clearance is the tenon, not the barrel: the barrel is thicker only between the stiles.

DECIMATE COLLAPSE triangle counts are not identical across series — the gate is a ratio band, not an exact count. On this mesh the three binaries agreed. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form; the only RNG is the seeded per-piece wood tone. Bevel inputs are sorted by edge index, so the face order is the same on every run; a Python set of edges handed to the bevel had made it vary. Export byte counts differ on 5.2.1 (glTF serializer), not a gated axis. Euler is 24 (multi-body) and is not gated to 2.

Rungs, wood and staging#

The rung heights were jittered on purpose (up to ±18 mm, RUNG_Z_OFFSETS) and the barrel thickness varied from 0.82× to 1.22×. That is variation put into function: a climber's feet find rungs blind, so an even pitch is part of what a ladder is. The rungs now sit at one pitch, and the turning varies by a few percent. Rung pitch asserts it; --drift-rungs restores the old heights and exits 19 (worst gap 0.03287 m off the mean). Tone and grain now vary per rail and rung instead. The temp .glb is removed after its size is measured.

The hero stood the ladder at its 12° rake with nothing to lean on. The render path now adds a render-only wall section on the side the rails rake toward, with its face through the rail tops, and turns the ladder so that side faces away from the camera.

Each falsifier violates exactly one named budget. Proven on Blender 4.5.11 LTS and 5.2.1 LTS (the binaries' own --version; 5.1.2 not re-run after the tie-rod pass):

FalsifierBudget violatedExitMeasured failure
--skip-decimateLOD1 ratio band9ratio 1.0000
--stray-vertMesh hygiene, loose verts15loose_v=1
--twin-soleCoplanar face pairs15zfight=6
--lift-zGrounded zmin16zmin 0.050000
--fat-rungsRung depth clearance17clearance 0
--short-stileShoe bite / cover18bite 0.070, cover −0.012
--drift-rungsRung pitch19worst gap 0.03287 off the mean
--float-nutsTie-rod seat20seat bite −0.00350, thread 0.00050 proud

Default exit is 0 on both.

Run#

blender --background --python wooden_ladder.py --
blender --background --python wooden_ladder.py -- --skip-decimate
blender --background --python wooden_ladder.py -- --stray-vert
blender --background --python wooden_ladder.py -- --twin-sole
blender --background --python wooden_ladder.py -- --lift-z
blender --background --python wooden_ladder.py -- --fat-rungs
blender --background --python wooden_ladder.py -- --short-stile
blender --background --python wooden_ladder.py -- --drift-rungs
blender --background --python wooden_ladder.py -- --float-nuts
blender --background --python wooden_ladder.py -- --output preview.webp --engine cycles

Smoke passes no flags: no --output, no falsifier.

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal 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, including coplanar pairs (--stray-vert, --twin-sole)
16Grounded zmin, or a rail end sitting in the tread (--lift-z)
17Part count or rung-to-stile joint fit, including through-tenon proud (--fat-rungs)
18Shoe bite / cover (--short-stile)
19Rung pitch (--drift-rungs)
20Tie-rod seat (--float-nuts)
21Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/wooden-ladder/wooden_ladder.py View the script on GitHub →
An oak ladder with six pale turned hickory rungs through-tenoned into its rails, iron tie-rods under the end rungs, leaning on a board wall with a rope coil on a rung.