Wooden Bucket

showcase/wooden-bucket/

A procedural coopered pail with a boarded, croze-seated floor, chord-lofted hoops, and a three-strand rope bail through the ear rings 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: 5458 tris, three materials with 1448 wood, 1020 metal, and 486 rope faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.396×0.346×0.534 m, 16 grounded staves, hoop bite 0.0025 m, bail-ear gap 0.0011 m, three bottom boards, no rope vertex inside an ear plate, LOD ratios in band, convex collider 276 tris, non-empty glTF. --skip-decimate exits 9; --float-handle exits 17 on the bail-ear budget; --sunk-rope exits 17 on the rope-clearance budget.

category Village

tags mesh export showcase

blender --background --python showcase/wooden-bucket/wooden_bucket.py --

A showcase piece, not an example. Procedural coopered pail (16 staves with seeded width jitter, a bottom of three boards seated in a croze above a chime, iron hoops lofted on the stave faces, ear plates and rings, a three-strand rope bail through the rings) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

Hoops evaluate the same host_outer_r chord as the stave flats. --round-band is the falsifier: a circle of radius_at(z) instead of the chord. The floor follows the inner stave polygon. Ear plates sit on the outer stave face below the rim; the bail is a semicircle whose ends pass through the rings and run on below them. --float-handle shrinks that semicircle and lifts it by what it lost, so the envelope stays put and only the bail-ear gap fails; --float-bottom shrinks the floor out of the croze.

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 triangles4000–58005458 / 5458 / 5458
LOD1 ratio0.32–0.62 of base0.4998 / 0.4998 / 0.4998
LOD2 ratio0.10–0.35 of base0.2199 / 0.2199 / 0.2191
Materialsexactly 3 distinct; ≥350 wood, ≥180 metal, ≥40 rope3 slots; 1448 / 1020 / 486
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.396, 0.346, 0.534) m ± 0.015(0.3957, 0.3462, 0.5338), zmin 0
Collider tris≤ 420276
Exportwritten, size > 0422992 / 422992 / 422984 bytes (4.5.11 / 5.1.2 / 5.2.1)

Base triangles rose from 4272 to 4872 in the quality pass: overlapping identical staves, circular hoops, and a 12-box bail became 16 jittered staves, chord-seated hoops, a polygonal croze floor, and a round bail through the ear rings. The second pass took it to 5458: the bail became three-strand rope (48 segments of a nine-vertex lobed section, running on through the rings) and the floor three boards.

DECIMATE COLLAPSE triangle counts are not identical across series, so LOD2 ratios differ slightly between versions. 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. Stave-width jitter uses fixed seed 17. Export byte counts differ 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
Named supports: 16 staveseach zmin ≤ 0.00116, stave_z 0.00000
Body plan0.335 m dia × 0.360 m high ± 0.040.3349 × 0.3600

Joint fit and seat#

AxisDeclaredMeasured (all three)
Bail-ear BVH gap≤ 0.006 m0.00107
Floor-croze rim BVH gap≤ 0.008 m0.00474
Bottom boards3; every seam 0.0006–0.0025 m3; 0.00120
Rope clears the ear plates2 plates; 0 rope vertices inside either2; 0
Hoop bite (host r − inner hoop r)0.0010–0.007 m0.00250

Falsifiers#

Each violates one named budget. All nine were 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-stavesnamed stave supports at Z=016
--float-handlebail-ear gap17
--float-bottomfloor-croze gap17
--round-bandhoop bite band18
--one-piece-bottombottom boards: 317
--sunk-roperope clears the ear plates17

Rope and ears#

The bail was a smooth eight-sided tube in a rope material, and it read as a plastic hose. It is now laid rope: a three-lobed section turned one vertex step per ring, so the lobes wind along the bail as strands. The section's frame is fixed to the bail's plane, so the lay does not jump where the ends turn vertical.

The ring offset from the ear plate was RING_MAJOR * 0.25, which put the rope's axis 3.25 mm off the plate and its body through it (hidden by the ring in the old stills). The offset is now sized from the rope's lobes plus 0.5 mm, and Rope clears the ear plates asserts it. --sunk-rope restores the old offset and puts 19 rope vertices inside a plate. The outer AABB was re-fitted to the honest envelope: 12.5 mm wider across the ears and 6 mm taller.

Surface#

Every stave and board carries a seeded PlankTone and a GrainDir (its own long axis) as face attributes, and wood_material stretches grain along that axis. The hoops, plates and rings are dark rusted iron. The temp .glb is removed once its size is measured.

Run#

blender --background --python wooden_bucket.py --
blender --background --python wooden_bucket.py -- --skip-decimate
blender --background --python wooden_bucket.py -- --stray-vert
blender --background --python wooden_bucket.py -- --lift-z
blender --background --python wooden_bucket.py -- --short-staves
blender --background --python wooden_bucket.py -- --float-handle
blender --background --python wooden_bucket.py -- --float-bottom
blender --background --python wooden_bucket.py -- --round-band
blender --background --python wooden_bucket.py -- --one-piece-bottom
blender --background --python wooden_bucket.py -- --sunk-rope
blender --background --python wooden_bucket.py -- --output bucket.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 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, or a named stave floats
17Joint fit: bail-ear gap, floor-croze gap, bottom boards and seams, or rope through an ear plate
18Seat: hoop bite band (--round-band)
19Body diameter or height off the stated real-world size
20Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/wooden-bucket/wooden_bucket.py View the script on GitHub →
A wooden stave bucket with three iron hoops and a twisted rope handle through iron ear rings.