Hay Bale

showcase/hay-bale/

A bound hay bale whose twine belts follow the loaf's own raycast cross-section, asserting recomputed geometry budgets, not an API contract.

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

witnesses Recomputed: 2044 tris, two materials with 1436 hay and 304 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.921x0.508x0.417 m, zmin 0, hygiene and cross-shell z-fight 0, 5 shells, each belt wrapping under the loaf at zmin 1.5 mm, banded belt seat depth 4.0-11.1 mm over 20/20 stations on both belts, belt mirror error 0, cinch depth 10.5 mm, loaf 0.921x0.393 m, 22.4 bake texels per UV cell, LOD ratios in band, convex collider 100 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z and --float-belts 16; --slack-belt 18; --skew-belt and --odd-loaf 19; --low-bake 20.

category Village

tags mesh export showcase

blender --background --python showcase/hay-bale/hay_bale.py --

A showcase piece, not an example. Procedural bound straw bale (cinched pillow loaf, a lumpy stem-mat end, two orange polypropylene belts that wrap the loaf's own cross-section, a knot loop standing on each) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

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.90 × 0.48 × 0.38 m nominal bale. The loaf measures 0.921 × 0.508 × 0.393 m with its pillow bulge, and the envelope is 0.921 × 0.508 × 0.417 m once the belts and knot loops are on it.

How the belts are built#

The belt is not a constant rectangle. The loaf's cross-section at a belt station is rounded, bulged and waisted, so a rectangle stands proud at the middle of each face and is swallowed at the corners — which is exactly what the previous build did, leaving the wrap hanging 2.9–8.3 mm above the floor with a visible notch at the bottom edge.

Instead the loaf is built, bevelled and grounded first, then each belt raycasts the finished loaf at its own station (loaf_profile, BELT_SEGS samples) and follows that profile at a named bite. Where the run passes near the ground it is pressed flush and a little past flush (BELT_PRESS_H, BELT_UNDER_SINK) so the loop closes underneath without breaking the Z=0 plane the bale is grounded on.

Every closed-form term in the loaf shaping is even in x. An odd term (sin(x*24)) put the two belt stations on different surface heights and left the wraps 5.4 mm out of step, which no budget could see.

Budgets#

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file. Measurements below are identical on 4.5.11, 5.1.2 and 5.2.1 — construction uses no RNG and no version-dependent operator.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles1700–24002044 on all three
LOD1 ratio0.32–0.62 of base0.5000
LOD2 ratio0.10–0.35 of base0.2192
Materialsexactly 2 distinct, ≥24 hay, ≥24 twine2 slots, 1436 hay, 304 twine
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.921, 0.508, 0.417) m ± 0.01(0.9208, 0.5075, 0.4165)
Grounded zminwithin 1e-4 of 00.000000
Hygieneloose/nonman/zero-area/doubles/n-gons/z-fight = 0all 0
Shell countexactly 5 (loaf, 2 belts, 2 loops)5
Belt wrap zmineach belt shell ≤ 0.006 m0.001460 both
Belt seat depthevery station in [0.002, 0.020] m0.00400..0.01114 over 20/20 stations, both belts
Mirror symmetrybelts and loops matched within 5e-4 m0.000000
Loaf mirrorevery loaf vertex has an X-mirror partner within 2e-4 m0.000093
Cinch depthmid-span minus waist in [0.006, 0.020] m0.01053
Loaf X × Z(0.921, 0.393) m ± 0.02(0.9208, 0.3931)
Hay–twine gapBVH surface < 0.008 m0.00009
Collider tris≤ 400100
Bake texelssmallest UV cell ≥ 12 px at the baked resolution22.43 px at 1024 px
Exportwritten, size > 0200356 / 200356 / 200348 bytes (4.5.11 / 5.1.2 / 5.2.1)

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — 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, plus the texel floor. The bake was 256 px, which left 5.6 texels per UV cell and smeared neighbouring cells' normals into streaks across the straw; at 1024 px with a 0.02 m cage (was 0.04, which reached the twine standing proud of the loaf) every cell gets 22. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), which is not a gated axis; every geometric figure above is identical on all three binaries.

The export is deleted once its size is measured. Blender sets TMPDIR from its own preference, which resolves to the working directory on a stock portable build, so tempfile.gettempdir() is the repo root under CI and every run used to leave a .glb behind.

Why these budgets and not an AABB#

zmin alone cannot see a belt that stops at the bottom edge: the loaf still grounds the box. A single global hay-twine gap cannot see a wrap that hugs one side and gaps the other. A bounding box cannot see two belts 5.4 mm out of step, or a flank with no waist in it. Each of the five budgets in bold above exists because a defect got past everything else and had to be found by looking at a render.

The z-fight count is taken over cross-shell face pairs, not merely over faces that share no vertex: two quads two steps apart on one flat cap share no vertex and are coplanar by construction, and counting those reported 96 pairs, none of them a hazard.

Why the loaf is triangulated along its short diagonals#

The twine used to be a 16 × 24 mm band, and it read as packing tape on a parcel. Thinned to an 8 × 10 mm cord, the two belts came out 0.45 mm out of step, right against the 0.5 mm mirror budget. The loaf's vertices were mirror-symmetric to 71 um. The belt profile is raycast from the loaf at each station, though, and the loaf's quads are not planar: split along a fixed diagonal, the surface at -x and at +x differed by up to 5.6 mm on the same vertices. The old wide band happened to hide that.

The loaf is now triangulated with quad_method="SHORT_EDGE". On mirror-symmetric vertices the shorter diagonal is mirror-symmetric too, and a tie only happens where a quad is planar, where either split is the same surface. The belt mirror error went back to exactly 0 and the seats to identical values at both stations. This is the surface an engine renders, so it is fixed in the mesh, not only in the sampling. Averaging the two stations' profiles was tried first and rejected: it put one belt 0.52 mm into the loaf, below the seat band, because neither station's real surface is the mean.

The loaf's own symmetry is now asserted directly (Loaf mirror, exit 19). Every loaf vertex needs a partner at (-x, y, z) within LOAF_MIRROR_EPS. The belt budget only ever caught an odd shaping term second-hand, through the belts' extents. --odd-loaf adds a 2 mm odd sin displacement to the finished loaf and fails it. It is applied after the bevel because, put into the shaping function, even 0.3 mm of odd term flipped which edges cleared the bevel's 50-degree selection and grew the envelope 16 mm, so the bounding box fired instead.

Falsifiers#

Each breaks one stage so a named budget fails and the piece exits that code. All eight proven on 4.5.11, 5.1.2 and 5.2.1.

FlagTarget budgetExit
--skip-decimateLOD1 ratio band9
--stray-verthygiene: loose vertices15
--lift-zgrounded AABB zmin16
--float-beltsper-belt wrap zmin16
--slack-beltbanded belt seat depth18
--skew-beltbelt mirror symmetry19
--odd-loafloaf X-mirror symmetry19
--low-bakebake texel density: 256 px bake (under 12 px per cell)20

--stray-vert parks its loose vertex inside the bale: above it, the bounding-box budget (exit 8) fired first and the run proved nothing about hygiene. --skew-belt shifts one belt along X rather than Z for the same reason — a Z skew landed a belt face coplanar with a loaf face and tripped the z-fight budget (exit 15) instead of its target. --float-belts moves twine only, so the loaf still grounds the AABB and the per-belt gate has to be the thing that fires. With the ridge at 6 mm the knot loops set the envelope top, so the two twine falsifiers are sized to their own budgets and kept inside BBOX_TOL: --float-belts lifts 8 mm (was 20) against a 6 mm wrap gate, and --slack-belt backs off 6 mm (was 10) against a 2 mm seat floor. At the old sizes both exited 8.

Run#

blender --background --python hay_bale.py --
blender --background --python hay_bale.py -- --skip-decimate
blender --background --python hay_bale.py -- --stray-vert
blender --background --python hay_bale.py -- --lift-z
blender --background --python hay_bale.py -- --float-belts
blender --background --python hay_bale.py -- --slack-belt
blender --background --python hay_bale.py -- --skew-belt
blender --background --python hay_bale.py -- --odd-loaf
blender --background --python hay_bale.py -- --low-bake
blender --background --python hay_bale.py -- --output bale.png

Smoke does not pass --output or any falsifier flag.

Shading#

Decided per part, not globally. The loaf is flat-shaded: straw is a chunky matte mass and the facets read as compressed flakes. Smoothing the whole bale erased every ridge and nap in it and left a featureless pillow that read as a bar of soap. Twine is cord, so the belts and hitch loops stay smooth.

The low mesh carries the form — bulge, waist, ridges, end relief — and the high mesh carries the straw: LOAF_CUTS_HIGH subdivision plus per-band flake offsets, moved onto the low mesh by the bake stage. A low mesh dense enough to model flakes directly would cost thousands of triangles for detail this piece already has a bake for.

Surface#

The bale used to read as a wrapped parcel: flat mustard hay, and a wide dark band for twine. Later it read as an upholstered cushion: its fibres ran along the length like brushed felt. The hay is now straw_material:

It is all object-space, so it runs continuously across the per-face UV cells. The baked normal is chained under that bump (wire_normal) at NRM_STRENGTH 0.55. The UV budget (zero per-face AABB overlap) gives every face its own cell, so the bake's tangent frame breaks at every cell edge; at full strength those breaks cut dark gashes along the top edge. The end face used to be sin(z) strata, which baked into cake layers gouged across the end. It is now a product of slow terms in y and z, which reads as the stem mat of the last flake. That changed the hull: the collider went from 140 to 100 tris. The twine is a 10 × 14 mm orange polypropylene cord standing 6 mm proud. As an 8 × 10 mm dark brown cord it stood 4 mm proud of the waisted loaf and read as a scored cut in every view. The longitudinal ridge dropped from 14 mm to 6 mm, because at 14 mm it crumpled the top edge in profile and the bale read as a stuffed sack. Each knot is a tight loop, KNOT_MAJOR 13 mm, standing in the belt's plane with its tube sized from TWINE_T. A 60 mm ring lying flat on the top read as a printed symbol. The earlier note here, that the surface belonged to the material and bake and not to more geometry, was right. The cord was the other half of the parcel read.

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 hay/twine 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
15Hygiene: loose, non-manifold, zero-area, doubles, n-gons, or cross-shell z-fight (--stray-vert)
16Grounded: AABB zmin (--lift-z), or a belt that does not wrap under (--float-belts)
17Shell count, or hay–twine BVH gap above 8 mm
18Banded belt seat depth (--slack-belt)
19Loaf X-mirror symmetry (--odd-loaf), belt mirror symmetry (--skew-belt), cinch depth, or loaf real-world size
20Bake texel density below the floor (--low-bake)
21Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/hay-bale/hay_bale.py View the script on GitHub →
A golden hay bale of packed flakes, its stems and flake seams showing on the flank and a stem mat on the end, bound by two orange twine cords with a small knot loop standing on each.