Grindstone

showcase/grindstone/

A procedural grindstone on a timber trestle 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: 1668 tris, three materials with 400 wood / 240 stone / 364 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.722×0.419×0.610 m, zmin 0, hygiene 0 including zero coplanar cross-shell pairs, four shoes planted, trough seated in the sills and biting both stretchers 4 mm, stone dip 24 mm, no unchamfered right-angle edge, 29.44 bake texels per UV cell, LOD ratios in band, convex collider 202 tris, non-empty glTF. --skip-decimate exits 9 on LOD1; --stray-vert / --flush-shoes exit 15 on hygiene; --lift-z / --short-legs exit 16 on grounded; --float-crank / --float-legs exit 17 on joint fit; --no-dip / --narrow-trough / --low-stretchers exit 18 on seat; --sharp-handle exits 20 on edge treatment; --low-bake exits 21 on texel density.

category Village

tags mesh export showcase

blender --background --python showcase/grindstone/grindstone.py --

A showcase piece, not an example. A procedural grindstone: a sandstone wheel, a timber A-frame trestle, iron shoes, axle, hubs and crank, and an open trough. The piece then runs the shipped pipeline: unique-cell UVs, a Cycles high-to-low normal bake, an LOD chain, a convex collider, and a Unity glTF export.

Each A-frame is a king post with two diagonals tenoned 12 mm into its faces and into a fatter sill. It is not two sticks meeting at a point. The sills stand in iron shoes, cups one 6 mm reveal proud of the sill on every side. The trough is one manifold basin seated into the sills and carried on two end stretchers, which it bites by 4 mm. Every timber and iron edge is chamfered. The stone is 48 smooth segments with hard chamfer rings, in a mottled sandstone material. Stated size is a 0.50 m × 0.095 m stone in a 0.72 × 0.42 × 0.61 m frame.

The hero turns the crank toward the camera, since the crank is the part that says the wheel is turned by hand, and fills the trough. The water is render-only dressing parented to the piece, set 12 mm under the rim, so the stone (28 mm under the rim) runs 16 mm under water; the stone darkens and glosses in a band that fades out 45 mm above the water line. Neither the water nor the wet band is part of the asset: no budget reads them, the export does not carry them, and the asset sheet renders the grindstone dry.

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 triangles1560–17801668 / 1668 / 1668
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2194 / 0.2194 / 0.2194
Materialsexactly 3 distinct; ≥ 280 wood, ≥ 190 stone, ≥ 260 metal faces3 slots; 400 / 240 / 364
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.722, 0.419, 0.610) m ± 0.015(0.7220, 0.4185, 0.6100), zmin 0
Collider tris≤ 320202
Bake texelssmallest UV cell ≥ 12 px at the baked resolution29.44 px at 1024 px
Exportwritten, size > 0147532 / 147532 / 147528 bytes (4.5.11 / 5.1.2 / 5.2.1)

The collider ceiling moved from 240 to 320 with the stone. 48 segments put more of its rim on the hull (198 → 304 triangles).

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 cross-shell face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 4 shoeseach zmin ≤ 1e-34, shoe_z 0.00000
Stone size0.500 m dia × 0.095 m thick ± 0.02 / ± 0.0150.5000 × 0.0950
Edge treatmentmanifold edges within 5° of 90° = 00 wood, 0 stone, 0 iron

The coplanar budget counts pairs from different shells within 0.05 m, using hay-bale's constants. The first build matched only faces whose centres fell within 0.1 mm of each other, and no real overlap ever does. It reported 0 while the shoes shared three planes with every sill end, seen as a lit slit in each foot close-up.

Joint fit and seat#

AxisDeclaredMeasured (all three)
Crank-axle gap≤ 0.008 m0.00376
Shoe-wood BVH gap≤ 0.008 m (overlap is 0)0.00000
Trough-sill per-side gap≤ 0.008 m (overlap is 0)0.00000
Stone dip into trough0.015–0.045 m0.02416
Trough floor zmin≥ 0.040 m0.05800
Trough bearing2 end stretchers, each biting the trough floor 0.002–0.008 m2, [0.00400, 0.00400]

DECIMATE COLLAPSE triangle counts can differ across series, so the gate is a ratio band, not an exact count. Bake pixels are stochastic. The gate is has_data plus operator FINISHED plus the texel floor, not byte-identity. Per-member wood tone comes from random.Random(TONE_SEED), so it is identical run to run. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), which is not a gated axis.

Falsifiers#

Each violates one named budget. All thirteen 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
--flush-shoescoplanar cross-shell pairs: shoes sized to the sill and flush with its end (0 → 4 pairs)15
--lift-zbounding box zmin is 016
--short-legsnamed shoe supports at Z=016
--float-crankcrank-axle gap17
--float-legsshoe-wood join17
--narrow-troughtrough-sill seat18
--no-dipstone dip band18
--low-stretcherstrough bearing: stretchers back at sill height, 5.9 mm under the trough18
--sharp-handleedge treatment: the crank handle's rims left square (20 right-angle edges)20
--low-bakebake texel density: 256 px bake (7.36 px per cell)21

--flush-shoes moves the shoe envelope 12 mm inside BBOX_TOL (15 mm), so the AABB gate cannot steal it. --sharp-handle leaves the triangle count inside its band, so only the edge budget sees it.

Run#

blender --background --python grindstone.py --
blender --background --python grindstone.py -- --skip-decimate
blender --background --python grindstone.py -- --stray-vert
blender --background --python grindstone.py -- --flush-shoes
blender --background --python grindstone.py -- --lift-z
blender --background --python grindstone.py -- --short-legs
blender --background --python grindstone.py -- --float-crank
blender --background --python grindstone.py -- --float-legs
blender --background --python grindstone.py -- --narrow-trough
blender --background --python grindstone.py -- --no-dip
blender --background --python grindstone.py -- --low-stretchers
blender --background --python grindstone.py -- --sharp-handle
blender --background --python grindstone.py -- --low-bake
blender --background --python grindstone.py -- --output grindstone.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, coplanar cross-shell pairs
16Not grounded: bounding box zmin off 0, or a named shoe floats
17Joint fit: crank-axle gap, or shoe-wood join
18Seat: trough-sill gap, stone dip band, trough on the dirt, or trough not bearing on its stretchers
19Stone diameter or thickness off the stated real-world size
20A right-angle edge survived the chamfer passes (--sharp-handle lands here)
21Bake texel density below the floor (--low-bake lands here)
22Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/grindstone/grindstone.py View the script on GitHub →
A sandstone grindstone wheel dipping into a water-filled trough, turning in a chamfered timber A-frame with its iron crank toward the viewer, the sills capped by iron shoes.