Skip to content

Commit 0cbaafe

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): square up the hay bale and tie it with visible twine
A 14 mm ridge crumpled the loaf's top edge into a stuffed sack, the 8 x 10 mm dark cord read as a scored cut, and each knot was a 60 mm ring lying flat like a printed symbol. The ridge is 6 mm, the twine a 10 x 14 mm orange cord standing 6 mm proud, and each knot a 13 mm loop standing in the belt's plane. The 256 px bake became 1024 px with a texel floor (exit 20, --low-bake) and a 0.02 m cage. The twine falsifiers were resized to their own budgets after the knots began to set the envelope top. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 505bb1b commit 0cbaafe

6 files changed

Lines changed: 207 additions & 81 deletions

File tree

9.39 KB
Loading
2.35 KB
Loading

‎docs/gallery/hay-bale/index.html‎

Lines changed: 92 additions & 37 deletions
Large diffs are not rendered by default.

‎showcase/hay-bale/README.md‎

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Hay bale
22

33
A showcase piece, not an example. Procedural bound straw bale (cinched
4-
pillow loaf, end-grain strata, two sisal belts that wrap the loaf's own
5-
cross-section, tied hitch loops) then the shipped pipeline: unique-cell
4+
pillow loaf, end-grain strata, two orange polypropylene belts that
5+
wrap the loaf's own cross-section, a knot loop standing on each) then the shipped pipeline: unique-cell
66
UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity
77
glTF export.
88

@@ -17,9 +17,9 @@ witness an API contract. "It rendered without error" is not a check.
1717
Hygiene combinatorics match `examples/mesh-hygiene-audit` (copied, not
1818
imported).
1919

20-
Intended size: 0.90 × 0.48 × 0.38 m nominal bale; the loaf measures
21-
0.922 × 0.530 × 0.409 m once the pillow bulge, the belts and the hitch
22-
loops are on it.
20+
Intended size: 0.90 × 0.48 × 0.38 m nominal bale. The loaf measures
21+
0.921 × 0.508 × 0.393 m with its pillow bulge, and the envelope is
22+
0.921 × 0.508 × 0.417 m once the belts and knot loops are on it.
2323

2424
## How the belts are built
2525

@@ -49,29 +49,34 @@ RNG and no version-dependent operator.
4949

5050
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
5151
| --- | --- | --- |
52-
| Base triangles | 1700–2400 | 2012 on all three |
52+
| Base triangles | 1700–2400 | 2044 on all three |
5353
| LOD1 ratio | 0.32–0.62 of base | 0.5000 |
54-
| LOD2 ratio | 0.10–0.35 of base | 0.2197 |
55-
| Materials | exactly 2 distinct, ≥24 hay, ≥24 twine | 2 slots, 1404 hay, 304 twine |
54+
| LOD2 ratio | 0.10–0.35 of base | 0.2192 |
55+
| Materials | exactly 2 distinct, ≥24 hay, ≥24 twine | 2 slots, 1436 hay, 304 twine |
5656
| UVs | in `0..1`, AABB overlap ≤ 1e-5 | in range, overlap 0 |
57-
| Outer AABB | (0.922, 0.530, 0.409) m ± 0.01 | (0.9208, 0.5295, 0.4087) |
57+
| Outer AABB | (0.921, 0.508, 0.417) m ± 0.01 | (0.9208, 0.5075, 0.4165) |
5858
| Grounded zmin | within 1e-4 of 0 | 0.000000 |
5959
| Hygiene | loose/nonman/zero-area/doubles/n-gons/**z-fight** = 0 | all 0 |
6060
| Shell count | exactly 5 (loaf, 2 belts, 2 loops) | 5 |
61-
| **Belt wrap zmin** | each belt shell ≤ 0.006 m | 0.002846 both |
62-
| **Belt seat depth** | every station in [0.002, 0.020] m | 0.00400..0.00903 over 20/20 stations, both belts |
61+
| **Belt wrap zmin** | each belt shell ≤ 0.006 m | 0.001460 both |
62+
| **Belt seat depth** | every station in [0.002, 0.020] m | 0.00400..0.01114 over 20/20 stations, both belts |
6363
| **Mirror symmetry** | belts and loops matched within 5e-4 m | 0.000000 |
64-
| **Loaf mirror** | every loaf vertex has an X-mirror partner within 2e-4 m | 0.000071 |
65-
| **Cinch depth** | mid-span minus waist in [0.006, 0.020] m | 0.01420 |
66-
| Loaf X × Z | (0.922, 0.409) m ± 0.02 | (0.9208, 0.4087) |
67-
| Hay–twine gap | BVH surface < 0.008 m | 0.00002 |
68-
| Collider tris | ≤ 400 | 120 |
69-
| Export | written, size > 0 | 197504 / 197504 / 197496 bytes |
64+
| **Loaf mirror** | every loaf vertex has an X-mirror partner within 2e-4 m | 0.000093 |
65+
| **Cinch depth** | mid-span minus waist in [0.006, 0.020] m | 0.01053 |
66+
| Loaf X × Z | (0.921, 0.393) m ± 0.02 | (0.9208, 0.3931) |
67+
| Hay–twine gap | BVH surface < 0.008 m | 0.00009 |
68+
| Collider tris | ≤ 400 | 140 |
69+
| Bake texels | smallest UV cell ≥ 12 px at the baked resolution | 22.43 px at 1024 px |
70+
| Export | written, size > 0 | 203508 / 203508 / 203496 bytes |
7071

7172
DECIMATE COLLAPSE triangle counts are **not** guaranteed identical
7273
across series — the gate is a ratio band, not an exact count. Bake
7374
pixels are stochastic; the gate is `has_data` plus operator `FINISHED`,
74-
not byte-identity. Export byte counts differ by 8 B on 5.2.1 (glTF
75+
not byte-identity, plus the texel floor. The bake was 256 px, which left
76+
5.6 texels per UV cell and smeared neighbouring cells' normals into
77+
streaks across the straw; at 1024 px with a 0.02 m cage (was 0.04, which
78+
reached the twine standing proud of the loaf) every cell gets 22. Export
79+
byte counts differ by 12 B on 5.2.1 (glTF
7580
serializer), which is not a gated axis; every geometric figure above is
7681
identical on all three binaries.
7782

@@ -126,7 +131,7 @@ and grew the envelope 16 mm, so the bounding box fired instead.
126131
## Falsifiers
127132

128133
Each breaks one stage so a **named** budget fails and the piece exits
129-
that code. All seven proven on 4.5.11, 5.1.2 and 5.2.1.
134+
that code. All eight proven on 4.5.11, 5.1.2 and 5.2.1.
130135

131136
| Flag | Target budget | Exit |
132137
| --- | --- | --- |
@@ -137,14 +142,19 @@ that code. All seven proven on 4.5.11, 5.1.2 and 5.2.1.
137142
| `--slack-belt` | banded belt seat depth | 18 |
138143
| `--skew-belt` | belt mirror symmetry | 19 |
139144
| `--odd-loaf` | loaf X-mirror symmetry | 19 |
145+
| `--low-bake` | bake texel density: 256 px bake (under 12 px per cell) | 20 |
140146

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

149159
## Run
150160

@@ -157,6 +167,7 @@ blender --background --python hay_bale.py -- --float-belts
157167
blender --background --python hay_bale.py -- --slack-belt
158168
blender --background --python hay_bale.py -- --skew-belt
159169
blender --background --python hay_bale.py -- --odd-loaf
170+
blender --background --python hay_bale.py -- --low-bake
160171
blender --background --python hay_bale.py -- --output bale.png
161172
```
162173

@@ -188,8 +199,13 @@ dark band for twine. The hay is now `straw_material`:
188199
light.
189200

190201
The baked normal is chained under that bump (`wire_normal`). The twine
191-
is an 8 × 10 mm cord, and the hitch loop's tube is sized from the same
192-
`TWINE_T`. The earlier note here, that the surface belonged to the
202+
is a 10 × 14 mm orange polypropylene cord standing 6 mm proud. As an
203+
8 × 10 mm dark brown cord it stood 4 mm proud of the waisted loaf and read
204+
as a scored cut in every view. The longitudinal ridge dropped from 14 mm to
205+
6 mm, because at 14 mm it crumpled the top edge in profile and the bale read
206+
as a stuffed sack. Each knot is a tight loop, `KNOT_MAJOR` 13 mm, standing
207+
in the belt's plane with its tube sized from `TWINE_T`. A 60 mm ring lying
208+
flat on the top read as a printed symbol. The earlier note here, that the surface belonged to the
193209
material and bake and not to more geometry, was right. The cord was the
194210
other half of the parcel read.
195211

@@ -220,3 +236,4 @@ File-local. `9` is a valid check code. `10` is reserved for
220236
| 17 | Shell count, or hay–twine BVH gap above 8 mm |
221237
| 18 | Banded belt seat depth (`--slack-belt`) |
222238
| 19 | Loaf X-mirror symmetry (`--odd-loaf`), belt mirror symmetry (`--skew-belt`), cinch depth, or loaf real-world size |
239+
| 20 | Bake texel density below the floor (`--low-bake`) |

‎showcase/hay-bale/hay_bale.py‎

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,15 @@
4646
BALE_Y = 0.48
4747
BALE_Z = 0.38
4848
# Baling twine is a cord, not a strap. At 16 x 24 mm the wrap read as
49-
# packing tape on a parcel; 8 x 10 mm reads as cord pulled into the straw.
50-
TWINE_T = 0.008
51-
TWINE_W = 0.010
49+
# packing tape on a parcel. At 8 x 10 mm, 4 mm of it stood proud of a
50+
# waisted loaf and read as a scored groove; 10 x 14 mm stands 6 mm proud
51+
# and reads as cord pulled into the straw.
52+
TWINE_T = 0.010
53+
TWINE_W = 0.014
5254
TWINE_EMBED = 0.004
55+
# The knot: a tight loop standing on the cord in the belt's own plane. A
56+
# 60 mm ring lying flat on the top read as a printed symbol, not a tie.
57+
KNOT_MAJOR = 0.013
5358
BELT_XS = (-0.225, 0.225)
5459
LOAF_CUTS = 8
5560
# The high mesh carries the straw; the bake moves it onto the low mesh. A
@@ -59,10 +64,13 @@
5964
FLAKE_W = 0.075
6065
FLAKE_AMP = 0.011
6166
BULGE = 0.055
62-
RIDGE_AMP = 0.014
67+
# Longitudinal straw ridge. At 14 mm it crumpled the top edge in profile,
68+
# and the bale read as a stuffed sack; the flakes on the high mesh carry
69+
# the straw texture through the bake instead.
70+
RIDGE_AMP = 0.006
6371
BBOX_TOL = 0.01
6472
# Fitted after locking geometry. Recomputed from bound_box.
65-
OUTER_SIZE = (0.922, 0.530, 0.409)
73+
OUTER_SIZE = (0.921, 0.508, 0.417)
6674

6775
# The belt follows the loaf's own cross-section, sampled by raycast, instead
6876
# of being a constant rectangle: a rectangle stands proud at the middle of
@@ -90,8 +98,15 @@
9098
UV_EPS = 1e-4
9199
UV_OVERLAP_MAX = 1e-5
92100
COLLIDER_TRIS_MAX = 400
93-
BAKE_RES = 256
94-
CAGE_EXTRUSION = 0.04
101+
# One UV cell per face: at 256 px each cell got 5.6 texels, and the
102+
# bilinear lookup smeared neighbouring cells' normals into streaks across
103+
# the straw (tavern-stool's defect). 1024 px, with a floor asserted.
104+
BAKE_RES = 1024
105+
LOW_BAKE_RES = 256
106+
TEXEL_MIN = 12.0
107+
# The cage covers the flake relief on the high mesh (11 mm) and the bevel
108+
# difference; at 0.04 it reached the twine standing proud of the loaf.
109+
CAGE_EXTRUSION = 0.02
95110
HAY_FACES_MIN = 24
96111
TWINE_FACES_MIN = 24
97112

@@ -137,11 +152,14 @@
137152
CINCH_MAX = 0.020
138153
# Loaf length and height against the stated real-world bale. Y is left to
139154
# the cinch budget above, since the pillow bulge dominates it.
140-
LOAF_XZ = (0.922, 0.409)
155+
LOAF_XZ = (0.921, 0.393)
141156
LOAF_XZ_TOL = 0.02
142157

143-
FLOAT_BELT_Z = 0.02
144-
SLACK_BELT = 0.010
158+
# Sized to their own budgets and kept inside BBOX_TOL: with the ridge at
159+
# 6 mm the knot sets the envelope top, and 20 / 10 mm moved it past the
160+
# bounding box, which then fired (exit 8) instead of 16 / 18.
161+
FLOAT_BELT_Z = 0.008
162+
SLACK_BELT = 0.006
145163
SKEW_BELT_X = 0.004
146164

147165
HAY_IDX = 0
@@ -735,17 +753,18 @@ def build_bale_mesh(name, bevel_offset, bevel_segments, cuts=LOAF_CUTS,
735753
# alongside its belt. Standing it upright made a wire hoop that
736754
# broke the silhouette; laying it in XY reads as tied cord.
737755
top_z = max(pz for _py, pz in profile)
756+
# The knot loop stands in the belt's plane (YZ), centred on the
757+
# belt's station, its foot sunk to the cord's mid-line so it grows
758+
# out of the wrap. The tube is the same cord as the wrap.
759+
minor = TWINE_T * 0.39
738760
add_rim(
739761
bm,
740-
# Offset outward from the centre, not along +X: a bare
741-
# `x + TWINE_W` put the two loops at -0.201 and +0.249.
742-
(x + math.copysign(TWINE_W, x), 0.052,
743-
top_z - TWINE_EMBED + TWINE_T * 0.45),
744-
0.030,
745-
# The loop is the same cord as the wrap: tube sized from it.
746-
TWINE_T * 0.39,
762+
(x, 0.052,
763+
top_z - TWINE_EMBED + TWINE_T * 0.5 + KNOT_MAJOR + minor * 0.5),
764+
KNOT_MAJOR,
765+
minor,
747766
TWINE_IDX,
748-
euler=(0.0, 0.0, 0.0),
767+
euler=(0.0, math.pi / 2.0, 0.0),
749768
n_major=12,
750769
n_minor=6,
751770
)
@@ -897,9 +916,11 @@ def straw_material(name):
897916
def bale_materials():
898917
"""(hay, twine): shared by the check, the render and inspection."""
899918
hay = straw_material("BaleHay")
919+
# Orange polypropylene baling twine. A dark brown cord lying in the
920+
# straw's waist read as a scored cut in every view, not as a tie.
900921
twine = principled(
901-
"BaleTwine", (0.30, 0.22, 0.11, 1.0), 0.0, 0.62,
902-
noise_scale=60.0, wear=(0.17, 0.12, 0.06, 1.0),
922+
"BaleTwine", (0.62, 0.20, 0.035, 1.0), 0.0, 0.48,
923+
noise_scale=60.0, wear=(0.44, 0.13, 0.025, 1.0),
903924
)
904925
return hay, twine
905926

@@ -1039,8 +1060,26 @@ def export_unity(path, objects):
10391060
)
10401061

10411062

1063+
def texel_audit(mesh, img):
1064+
"""Smallest UV cell, in baked texels along its longer side.
1065+
1066+
Copied from showcase/tavern-stool (do not import across pieces).
1067+
"""
1068+
uv = mesh.uv_layers.active
1069+
if uv is None or img is None:
1070+
return 0.0
1071+
res = min(img.size[0], img.size[1])
1072+
data = uv.data
1073+
worst = 1e9
1074+
for poly in mesh.polygons:
1075+
us = [data[i].uv[0] for i in poly.loop_indices]
1076+
vs = [data[i].uv[1] for i in poly.loop_indices]
1077+
worst = min(worst, max(max(us) - min(us), max(vs) - min(vs)) * res)
1078+
return worst
1079+
1080+
10421081
def check(skip_decimate, lift_z=False, stray_vert=False, slack_belt=False,
1043-
float_belts=False, skew_belt=False, odd_loaf=False):
1082+
float_belts=False, skew_belt=False, odd_loaf=False, low_bake=False):
10441083
bpy.ops.wm.read_factory_settings(use_empty=True)
10451084
low = build_bale_mesh(
10461085
"BaleLow", bevel_offset=0.016, bevel_segments=2,
@@ -1088,10 +1127,12 @@ def check(skip_decimate, lift_z=False, stray_vert=False, slack_belt=False,
10881127
size_y = bb[4] - bb[1]
10891128
size_z = bb[5] - bb[2]
10901129

1091-
img, tex = setup_bake_image(low, hay)
1130+
img, tex = setup_bake_image(low, hay, LOW_BAKE_RES if low_bake else BAKE_RES)
10921131
if img is None:
10931132
return fail("hay bale has no UV layer", 3), None, None, None, None, None
10941133
bake_result = bake_normal(high, low)
1134+
texel = texel_audit(low.data, img)
1135+
print(f"measured bake_res={img.size[0]} texel_min={texel:.2f}")
10951136

10961137
lod1 = make_lod(low, "BaleLOD1", LOD1_TARGET, skip_decimate)
10971138
lod2 = make_lod(low, "BaleLOD2", LOD2_TARGET, skip_decimate)
@@ -1390,6 +1431,12 @@ def half_y_at(x0, half_window):
13901431
f"{LOAF_XZ} m bale",
13911432
19,
13921433
), None, None, None, None, None
1434+
if texel < TEXEL_MIN:
1435+
return fail(
1436+
f"bake texel density {texel:.2f} px per UV cell < {TEXEL_MIN} "
1437+
"(--low-bake is the designed fail)",
1438+
20,
1439+
), None, None, None, None, None
13931440
return 0, low, high, hay, tex, collider
13941441

13951442

@@ -1556,6 +1603,12 @@ def main():
15561603
help="falsification: use the odd sin() ridge term so the loaf stops "
15571604
"being mirror-symmetric in X",
15581605
)
1606+
p.add_argument(
1607+
"--low-bake",
1608+
action="store_true",
1609+
help="falsification: bake the normal map at 256 px, so the "
1610+
"texel-density budget fails",
1611+
)
15591612
args = p.parse_args(argv)
15601613

15611614
code, low, _high, hay, tex, _col = check(
@@ -1566,6 +1619,7 @@ def main():
15661619
slack_belt=args.slack_belt,
15671620
skew_belt=args.skew_belt,
15681621
odd_loaf=args.odd_loaf,
1622+
low_bake=args.low_bake,
15691623
)
15701624
if code:
15711625
return code

‎showcase/hay-bale/preview.webp‎

8.04 KB
Loading

0 commit comments

Comments
 (0)