Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/gallery/assets/hay-bale-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/contact-sheets/hay-bale-contact-sheet.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 92 additions & 37 deletions docs/gallery/hay-bale/index.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1179,12 +1179,12 @@ <h2><a href="wooden-ladder/">wooden-ladder</a></h2>
</article>
<article class="card" data-tags="mesh export showcase">
<a class="card-media" href="hay-bale/" tabindex="-1" aria-hidden="true">
<img src="assets/hay-bale-hero.webp" alt="A golden fibrous hay bale bound by two thin twine cords with knotted loops on top." loading="lazy" decoding="async" />
<img src="assets/hay-bale-hero.webp" alt="A golden fibrous hay bale, square-edged and slightly bulging, bound by two orange twine cords with a small knot loop standing on each." loading="lazy" decoding="async" />
</a>
<div class="card-body">
<h2><a href="hay-bale/">hay-bale</a></h2>
<p class="teaches">A bound hay bale whose twine belts follow the loaf&#x27;s own raycast cross-section, asserting recomputed geometry budgets, not an API contract.</p>
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 2012 tris, two materials with 712 hay and 304 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.921x0.530x0.409 m, zmin 0, hygiene and cross-shell z-fight 0, 5 shells, each belt wrapping under the loaf at zmin 2.6 mm, banded belt seat depth 4.0-16.8 mm over 20/20 stations on both belts, belt mirror error 0, cinch depth 14.2 mm, LOD ratios in band, convex collider 156 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z and --float-belts 16; --slack-belt 18; --skew-belt 19.</p>
<p class="witnesses"><span class="tag">witnesses</span> 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 140 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.</p>
<a class="card-link" href="hay-bale/">View showcase piece<span class="sr-only"> hay-bale</span> <span aria-hidden="true">&rarr;</span></a>
</div>
</article>
Expand Down
8 changes: 7 additions & 1 deletion showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,13 @@ entry in `showcase/gallery.json`, and a rendered still.
tolerance, so a 1.5 mm model change tipped it from exit 16 to exit 8.
Make the falsifier move only what its budget measures — the shoes
float, the legs stretch to the floor — and check its AABB delta against
the tolerance, not just its exit code.
the tolerance, not just its exit code. Re-check it whenever a design
change moves which part sets the envelope. `hay-bale`'s ridge dropped
from 14 mm to 6 mm, which made the knot loops the top of the box. The
twine falsifiers, sized at 20 mm and 10 mm when the loaf's crest was the
top, then lifted the knots past `BBOX_TOL` and exited 8. The fix was to
size each one to its own budget, 8 mm against a 6 mm wrap gate and 6 mm
against a 2 mm seat floor, never to widen the tolerance.
- **Stone is not noise stretched over a block.** A noise-driven colour
mix sampled so its features streak read as wood grain on
`stone-archway` at hero scale. Stone wants isotropic object-space
Expand Down
4 changes: 2 additions & 2 deletions showcase/gallery.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@
"name": "hay-bale",
"dir": "showcase/hay-bale",
"teaches": "A bound hay bale whose twine belts follow the loaf's own raycast cross-section, asserting recomputed geometry budgets, not an API contract.",
"alt": "A golden fibrous hay bale bound by two thin twine cords with knotted loops on top.",
"witnessesFix": "Recomputed: 2012 tris, two materials with 712 hay and 304 twine faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.921x0.530x0.409 m, zmin 0, hygiene and cross-shell z-fight 0, 5 shells, each belt wrapping under the loaf at zmin 2.6 mm, banded belt seat depth 4.0-16.8 mm over 20/20 stations on both belts, belt mirror error 0, cinch depth 14.2 mm, LOD ratios in band, convex collider 156 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z and --float-belts 16; --slack-belt 18; --skew-belt 19.",
"alt": "A golden fibrous hay bale, square-edged and slightly bulging, bound by two orange twine cords with a small knot loop standing on each.",
"witnessesFix": "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 140 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.",
"hero": "docs/gallery/assets/hay-bale-hero.webp",
"preview": "showcase/hay-bale/preview.webp",
"tags": [
Expand Down
61 changes: 39 additions & 22 deletions showcase/hay-bale/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Hay bale

A showcase piece, not an example. Procedural bound straw bale (cinched
pillow loaf, end-grain strata, two sisal belts that wrap the loaf's own
cross-section, tied hitch loops) then the shipped pipeline: unique-cell
pillow loaf, end-grain strata, 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.

Expand All @@ -17,9 +17,9 @@ witness an API contract. "It rendered without error" is not a check.
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.922 × 0.530 × 0.409 m once the pillow bulge, the belts and the hitch
loops are on it.
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

Expand Down Expand Up @@ -49,29 +49,34 @@ RNG and no version-dependent operator.

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

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. Export byte counts differ by 8 B on 5.2.1 (glTF
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.

Expand Down Expand Up @@ -126,7 +131,7 @@ 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 seven proven on 4.5.11, 5.1.2 and 5.2.1.
that code. All eight proven on 4.5.11, 5.1.2 and 5.2.1.

| Flag | Target budget | Exit |
| --- | --- | --- |
Expand All @@ -137,14 +142,19 @@ that code. All seven proven on 4.5.11, 5.1.2 and 5.2.1.
| `--slack-belt` | banded belt seat depth | 18 |
| `--skew-belt` | belt mirror symmetry | 19 |
| `--odd-loaf` | loaf X-mirror symmetry | 19 |
| `--low-bake` | bake 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.
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

Expand All @@ -157,6 +167,7 @@ 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
```

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

The baked normal is chained under that bump (`wire_normal`). The twine
is an 8 × 10 mm cord, and the hitch loop's tube is sized from the same
`TWINE_T`. The earlier note here, that the surface belonged to the
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.

Expand Down Expand Up @@ -220,3 +236,4 @@ File-local. `9` is a valid check code. `10` is reserved for
| 17 | Shell count, or hay–twine BVH gap above 8 mm |
| 18 | Banded belt seat depth (`--slack-belt`) |
| 19 | Loaf X-mirror symmetry (`--odd-loaf`), belt mirror symmetry (`--skew-belt`), cinch depth, or loaf real-world size |
| 20 | Bake texel density below the floor (`--low-bake`) |
98 changes: 76 additions & 22 deletions showcase/hay-bale/hay_bale.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,15 @@
BALE_Y = 0.48
BALE_Z = 0.38
# Baling twine is a cord, not a strap. At 16 x 24 mm the wrap read as
# packing tape on a parcel; 8 x 10 mm reads as cord pulled into the straw.
TWINE_T = 0.008
TWINE_W = 0.010
# packing tape on a parcel. At 8 x 10 mm, 4 mm of it stood proud of a
# waisted loaf and read as a scored groove; 10 x 14 mm stands 6 mm proud
# and reads as cord pulled into the straw.
TWINE_T = 0.010
TWINE_W = 0.014
TWINE_EMBED = 0.004
# The knot: a tight loop standing on the cord in the belt's own plane. A
# 60 mm ring lying flat on the top read as a printed symbol, not a tie.
KNOT_MAJOR = 0.013
BELT_XS = (-0.225, 0.225)
LOAF_CUTS = 8
# The high mesh carries the straw; the bake moves it onto the low mesh. A
Expand All @@ -59,10 +64,13 @@
FLAKE_W = 0.075
FLAKE_AMP = 0.011
BULGE = 0.055
RIDGE_AMP = 0.014
# Longitudinal straw ridge. At 14 mm it crumpled the top edge in profile,
# and the bale read as a stuffed sack; the flakes on the high mesh carry
# the straw texture through the bake instead.
RIDGE_AMP = 0.006
BBOX_TOL = 0.01
# Fitted after locking geometry. Recomputed from bound_box.
OUTER_SIZE = (0.922, 0.530, 0.409)
OUTER_SIZE = (0.921, 0.508, 0.417)

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

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

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

HAY_IDX = 0
Expand Down Expand Up @@ -735,17 +753,18 @@ def build_bale_mesh(name, bevel_offset, bevel_segments, cuts=LOAF_CUTS,
# alongside its belt. Standing it upright made a wire hoop that
# broke the silhouette; laying it in XY reads as tied cord.
top_z = max(pz for _py, pz in profile)
# The knot loop stands in the belt's plane (YZ), centred on the
# belt's station, its foot sunk to the cord's mid-line so it grows
# out of the wrap. The tube is the same cord as the wrap.
minor = TWINE_T * 0.39
add_rim(
bm,
# Offset outward from the centre, not along +X: a bare
# `x + TWINE_W` put the two loops at -0.201 and +0.249.
(x + math.copysign(TWINE_W, x), 0.052,
top_z - TWINE_EMBED + TWINE_T * 0.45),
0.030,
# The loop is the same cord as the wrap: tube sized from it.
TWINE_T * 0.39,
(x, 0.052,
top_z - TWINE_EMBED + TWINE_T * 0.5 + KNOT_MAJOR + minor * 0.5),
KNOT_MAJOR,
minor,
TWINE_IDX,
euler=(0.0, 0.0, 0.0),
euler=(0.0, math.pi / 2.0, 0.0),
n_major=12,
n_minor=6,
)
Expand Down Expand Up @@ -897,9 +916,11 @@ def straw_material(name):
def bale_materials():
"""(hay, twine): shared by the check, the render and inspection."""
hay = straw_material("BaleHay")
# Orange polypropylene baling twine. A dark brown cord lying in the
# straw's waist read as a scored cut in every view, not as a tie.
twine = principled(
"BaleTwine", (0.30, 0.22, 0.11, 1.0), 0.0, 0.62,
noise_scale=60.0, wear=(0.17, 0.12, 0.06, 1.0),
"BaleTwine", (0.62, 0.20, 0.035, 1.0), 0.0, 0.48,
noise_scale=60.0, wear=(0.44, 0.13, 0.025, 1.0),
)
return hay, twine

Expand Down Expand Up @@ -1039,8 +1060,26 @@ def export_unity(path, objects):
)


def texel_audit(mesh, img):
"""Smallest UV cell, in baked texels along its longer side.

Copied from showcase/tavern-stool (do not import across pieces).
"""
uv = mesh.uv_layers.active
if uv is None or img is None:
return 0.0
res = min(img.size[0], img.size[1])
data = uv.data
worst = 1e9
for poly in mesh.polygons:
us = [data[i].uv[0] for i in poly.loop_indices]
vs = [data[i].uv[1] for i in poly.loop_indices]
worst = min(worst, max(max(us) - min(us), max(vs) - min(vs)) * res)
return worst


def check(skip_decimate, lift_z=False, stray_vert=False, slack_belt=False,
float_belts=False, skew_belt=False, odd_loaf=False):
float_belts=False, skew_belt=False, odd_loaf=False, low_bake=False):
bpy.ops.wm.read_factory_settings(use_empty=True)
low = build_bale_mesh(
"BaleLow", bevel_offset=0.016, bevel_segments=2,
Expand Down Expand Up @@ -1088,10 +1127,12 @@ def check(skip_decimate, lift_z=False, stray_vert=False, slack_belt=False,
size_y = bb[4] - bb[1]
size_z = bb[5] - bb[2]

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

lod1 = make_lod(low, "BaleLOD1", LOD1_TARGET, skip_decimate)
lod2 = make_lod(low, "BaleLOD2", LOD2_TARGET, skip_decimate)
Expand Down Expand Up @@ -1390,6 +1431,12 @@ def half_y_at(x0, half_window):
f"{LOAF_XZ} m bale",
19,
), None, None, None, None, None
if texel < TEXEL_MIN:
return fail(
f"bake texel density {texel:.2f} px per UV cell < {TEXEL_MIN} "
"(--low-bake is the designed fail)",
20,
), None, None, None, None, None
return 0, low, high, hay, tex, collider


Expand Down Expand Up @@ -1556,6 +1603,12 @@ def main():
help="falsification: use the odd sin() ridge term so the loaf stops "
"being mirror-symmetric in X",
)
p.add_argument(
"--low-bake",
action="store_true",
help="falsification: bake the normal map at 256 px, so the "
"texel-density budget fails",
)
args = p.parse_args(argv)

code, low, _high, hay, tex, _col = check(
Expand All @@ -1566,6 +1619,7 @@ def main():
slack_belt=args.slack_belt,
skew_belt=args.skew_belt,
odd_loaf=args.odd_loaf,
low_bake=args.low_bake,
)
if code:
return code
Expand Down
Binary file modified showcase/hay-bale/preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading