Butter Churn

showcase/butter-churn/

A procedural plunge butter churn — sixteen tapered oak staves, three iron hoops, a three-board bottom, a two-board lid and a maple cross dasher — carried through UVs, bake, LOD, compound 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: 5684 tris, three materials with 2240 oak, 160 maple and 576 iron faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.340×0.340×1.050 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, all sixteen staves on the floor, three bottom boards with 1.2 mm seams, the handle 14 mm into the slats, three hoops 2.5 mm into the staves and 4.9 mm proud, the lid 1.4–2.0 mm onto the stave tops, 5 mm of clearance round the handle, and a 514 mm plunge stroke for a 101 mm reach. --wide-dasher exits 20 at 140 mm while the plunger still clears the staves at rest; --tight-hole exits 18.

category Village

tags mesh export showcase

blender --background --python showcase/butter-churn/butter_churn.py --

A plunge butter churn: a tall body of sixteen oak staves that narrows toward the top, three forged iron hoops, a bottom of three boards set in a croze, a lid of two boards with a hole for the handle, and a maple cross dasher whose handle runs up through the lid. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

What it composes#

Shipped contentUsed for
skills/mesh-editing-and-bmeshlofted staves, banded hoops, clipped board polygons, lathed handle, all in one bmesh
skills/custom-propertiesface attributes (PlankTone, GrainDir) read by the wood shaders
skills/procedural-materials-and-shadersoak with grain along each stave, pale maple, rusted rough iron
skills/bake-high-to-lowCycles tangent-space normal bake, high onto low
skills/engine-export-presetsUnity glTF (export_yup=True)
skills/depsgraph-and-evaluated-dataevaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.pyLOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.pyone hull for the body and lid, one for the handle above it
snippets/lod_chain.pyLOD naming and ratio pattern
examples/mesh-hygiene-audithygiene combinatorics (copied, not imported)

The budget that matters#

A churn is for plunging. The body narrows toward the top, so a plunger that clears the staves at the bottom can jam partway up the stroke, and nothing else notices:

The piece reads the inner wall off the staves ring by ring and the plunger's reach off the dasher's slats, about the handle's axis. It finds the height at which the wall comes within WALL_CLEAR (10 mm) of the plunger, stops at the lid if that comes first, and asserts the travel from the plunger's rest height as a stroke floor of 400 mm. Measured: 513.8 mm, for a 100.6 mm reach.

--wide-dasher is the falsifier built for exactly this. It widens the slats to 125 mm. The plunger still clears the staves at rest, but it can travel only 140.4 mm before it would meet the wall, and the run exits

  1. Every other budget passes and the AABB is unchanged.

Budgets#

Declared in the script as named constants, recomputed from the generated mesh. Measured values are from Blender 5.2.1; every one is byte-identical on 4.5.11 and 5.1.2 (only the glTF file size differs, by 8 bytes, which is exporter metadata and not a budget).

BudgetBandMeasured
Base triangles5000–65005684
LOD1 ratio0.32–0.620.5000
LOD2 ratio0.10–0.350.2199
Material slotsexactly 3, distinct3
Oak / maple / iron faces≥ 2000 / 140 / 5002240 / 160 / 576
UV boundsinside 0..1(0.0013, 0.0013)–(0.9987, 0.9987)
UV AABB overlap≤ 1e-50.000000
Outer AABB0.340 × 0.340 × 1.050 m ± 0.0200.3401 × 0.3401 × 1.0500
Collider triangles≤ 200178 (two hulls)
Normal bake{'FINISHED'} with image data{'FINISHED'}, has_data=True
glTF exportfile written, non-empty~240 kB
Hygieneall zeroloose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar cross-shell pairs 0
Grounded AABB\zmin\≤ 1e-40.00000
Named staves16 staves, each zmin ≤ 1e-416 at 0.00000
Bottom boards3 boards, seams 0.6–2.5 mm3, 1.20 mm
Handle bitehandle's foot 8–20 mm into the slats14.0 mm
Hoop seat3 hoops, inner face 1.0–4.5 mm into the staves, outer face ≥ 3 mm proud2.50 mm in, 4.90 mm proud
Lid seat2 boards, each 1–4 mm onto the stave tops1.40–2.00 mm
Handle clearancelid hole over handle radius, 2–10 mm5.00 mm
Plunge stroke≥ 400 mm513.8 mm

Real-world size: 0.34 m across the foot, 0.75 m to the rim and 1.05 m to the top of the handle. It is a farmhouse plunge churn.

Construction#

Findings the budgets forced#

The fixes:

The count is 0.

Conventions walked#

Every convention in showcase/README.md, and whether it applies here.

ConventionAppliesHow
Deterministic, budgets declared, assertions recomputeyesno RNG; every value above is read off the mesh
Falsifier fails the budget it targetsyestable below, proven on all three binaries
Hygiene incl. cross-shell coplanaryesexit 15; canted stave ends and stepped boards keep it at 0
Named supportsyesall sixteen staves (--short-stave)
Seat conformance, sampled per segmentyesevery hoop vertex against the stave vertex under it (--float-hoops)
Bands on a curved host are built on the host's arcyeshoop vertices at the staves' own angles
A band wraps its host, never sunk into ityesinner face bites, outer face asserted proud
A head or bottom is boards, not a slabyesthree bottom boards with a banded seam (--one-piece-bottom); the lid is two boards
A vessel holds its contentsn/athe churn shows no contents
A joint bites; touching is not joiningyeshandle into the slats (--short-handle)
Carried parts bite their bearersyeslid on the stave tops (--float-lid)
What passes through a part must clear its mountyeshandle through the lid's hole (--tight-hole)
Identical boards read as CGyesper-stave and per-board PlankTone, grain along each piece's longest extent
Hoops are forged iron, not brightyesnear-black and rust, roughness 0.55–0.85, metallic 0.65
Shading is part of the modelyesstaves smooth across their width with every edge over 35° hard, so they read as coopered boards; handle smooth (turned)
One substance, one slotyesoak, maple, iron
Chamfer n-gon caps, then triangulateyesbottom and lid boards
Sort bmesh operator inputsyesbevel edges sorted by index
Level on the stage; stage 60 myesturned about Z only; 60 m floor and wall
Keep a falsifier's envelope stillyesevery falsifier leaves the AABB unchanged
Rope, masonry, roofs, rings, scatter, mirrored assembliesnothe piece has none of these

Falsifiers#

Each breaks one pipeline stage so a named budget fails. All ten were run on 4.5.11, 5.1.2 and 5.2.1 and exited the same declared code on all three.

FlagTarget budgetBreaksExit
--skip-decimateLOD1 ratiodrops the DECIMATE modifiers, LOD1 ratio goes to 1.00009
--stray-vertmesh hygieneadds one loose vertex inside the body15
--lift-zgrounded zminlifts the whole mesh 50 mm16
--short-stavenamed stavesstops one stave's foot 6 mm above the floor; the rest ground the AABB16
--one-piece-bottombottom boardsone disk instead of three boards17
--short-handlehandle bitestarts the handle 5 mm above the slats; −5.0 mm17
--float-hoopshoop seatbuilds every hoop 4 mm off the staves; −3.5 mm18
--float-lidlid seatlifts the lid 5 mm; −3.6 to −3.0 mm18
--tight-holehandle clearancecuts the hole 2 mm inside the handle; −2.0 mm18
--wide-dasherplunge strokewidens the slats to 125 mm; 140.4 mm20

Exit codes#

File-local and sequential. 9 is a valid check code. 1 is the FATAL wrapper — a crash, never a named check. 19 (plumb and real-world size) is reserved across pieces and unused here.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build, or has no UV layer
4Base triangle count outside band
5Material slots, or a material's face floor
6UVs outside 0..1
7UV AABB overlap above tolerance
8Outer AABB off declared size
9LOD1 or LOD2 ratio outside band (--skip-decimate)
10Framing gate (examples/gallery_framing.py, render path only)
11Collider triangles above ceiling
12Normal bake failed or produced no image data
13glTF export missing or empty
14--output produced no file
15Mesh hygiene (--stray-vert)
16Grounded zmin, or a stave floating (--lift-z, --short-stave)
17Bottom boards or handle bite (--one-piece-bottom, --short-handle)
18Hoop seat, lid seat or handle clearance (--float-hoops, --float-lid, --tight-hole)
20Plunge stroke below floor (--wide-dasher)
21Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Run it#

# Budget check, no render. ~1.8 s on 4.5 and 5.2.
blender --background --python butter_churn.py --

# Falsifier: the plunger jams partway up the narrowing body. Must exit 20.
blender --background --python butter_churn.py -- --wide-dasher

# Falsifier: the lid's hole is narrower than the handle. Must exit 18.
blender --background --python butter_churn.py -- --tight-hole

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host).
blender --background --python butter_churn.py -- --output butter_churn.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

Cross-version measurements#

Value4.5.115.1.25.2.1
Base triangles568456845684
LOD1 / LOD2 tris2842 / 1250samesame
Face counts (oak / maple / iron)2240 / 160 / 576samesame
Outer AABB0.3401 × 0.3401 × 1.0500samesame
Collider tris178178178
Plunge stroke / reach513.8 mm / 100.6 mmsamesame
glTF bytes239804239804239796

Source

showcase/butter-churn/butter_churn.py View the script on GitHub →
A tall staved oak butter churn narrowing toward the top, bound by three dark iron hoops, with a two-board lid and a pale maple dasher handle rising through it.