[`shipping-crate`](showcase/shipping-crate/) — procedural crate through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets. Falsifier `--skip-decimate` exits 9.
@@ -148,6 +153,8 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
[`iron-cauldron`](showcase/iron-cauldron/) — procedural hanging iron cauldron on a timber tripod through the same pipeline. Falsifier `--skip-decimate` exits 9.
+
+
## Examples
Runnable, smoke-gated demos live in [`examples/`](examples/) — each is executed headless on
diff --git a/docs/gallery/anvil/index.html b/docs/gallery/anvil/index.html
index da9f14a..8f1f41d 100644
--- a/docs/gallery/anvil/index.html
+++ b/docs/gallery/anvil/index.html
@@ -253,7 +253,7 @@
anvil
Rendered headless by the example itself — click to zoom.
-
witnesses Recomputed: 1696 tris, two materials with 274 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.746×0.410×0.682 m, LOD ratios in band (5.2 COLLAPSE more aggressive on LOD2), convex collider 162 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
witnesses Recomputed: 6616 tris, two materials with 2616 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.772×0.419×0.662 m, LOD ratios in band, convex collider 190 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
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.
-
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1620–1780 | 1696 / 1696 / 1696 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2193 / 0.2193 / 0.2182 | | Materials | exactly 2 distinct, ≥24 metal faces | 2 slots, 274 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.746, 0.410, 0.682) m ± 0.01 | (0.7460, 0.4095, 0.6820), zmin 0 | | Collider tris | ≤ 180 | 162 | | Export | written, size > 0 | 130852 / 130852 / 130840 bytes |
-
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is more aggressive on LOD2. 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. Construction uses no RNG. Export byte counts differ by 4 B on 5.2.1 (glTF serializer), not a gated axis.
DECIMATE COLLAPSE triangle counts are not identical across series in general — the gate is a ratio band, not an exact count. On this mesh LOD ratios matched across 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.
--skip-decimate skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and exit 9 fires. That is the named budget the falsifier violates.
if v.co.z < 0.0:
v.co.z = 0.0
+ if bevel_offset > 0.0:
+ metal_edges = list(
+ {e for v in metal for e in v.link_edges if v.is_valid}
+ )
+ if metal_edges:
+ ret = bmesh.ops.bevel(
+ bm,
+ geom=metal_edges,
+ offset=max(bevel_offset, 0.012),
+ segments=bevel_segments,
+ profile=0.5,
+ affect="EDGES",
+ clamp_overlap=True,
+ )
+ for f in ret.get("faces") or []:
+ f.material_index = METAL_IDX
+
pack_uvs(bm)
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
for face in bm.faces:
- face.smooth = False
+ face.smooth = True
+ for edge in bm.edges:
+ edge.smooth = True
+ if edge.is_manifold and len(edge.link_faces) == 2:
+ if edge.calc_face_angle() > math.radians(38.0):
+ edge.smooth = False
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
me.update()
- for poly in me.polygons:
- poly.use_smooth = Falsefinally:
bm.free()
out = bpy.data.objects.new(name, me)
@@ -843,10 +858,10 @@
A procedural shipping crate through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.
-
witnesses Recomputed: 552 tris, two materials, UVs in 0..1 with zero AABB overlap, outer AABB 1.256×0.856×0.748 m, LOD ratios in band (5.2 COLLAPSE more aggressive), convex collider 20 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
witnesses Recomputed: 624 tris, two materials with 132 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.352×0.856×0.748 m, LOD ratios in band (5.2 COLLAPSE more aggressive), convex collider 36 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
A procedural blacksmith anvil on a timber stump through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.
-
witnesses Recomputed: 1696 tris, two materials with 274 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.746×0.410×0.682 m, LOD ratios in band (5.2 COLLAPSE more aggressive on LOD2), convex collider 162 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
witnesses Recomputed: 6616 tris, two materials with 2616 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.772×0.419×0.662 m, LOD ratios in band, convex collider 190 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
A procedural wall-mounted torch sconce with a coursed stone plaque, iron bracket, wooden haft, and emissive flame through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.
-
witnesses Recomputed: 1520 tris, four materials with 540 stone, 130 metal, and 28 flame faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.440×0.343×0.740 m, LOD ratios in band (5.2 COLLAPSE 4 tris leaner on LOD2), convex collider 94 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
A procedural wall-mounted torch sconce with a dressed stone plaque, iron bracket, wooden haft, and emissive flame through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.
+
witnesses Recomputed: 848 tris, four materials with 162 stone, 134 metal, and 44 flame faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.384×0.360×0.670 m, LOD ratios in band (5.2 COLLAPSE 8 tris leaner on LOD2), convex collider 92 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
A procedural hanging iron cauldron with a bulged open pot, bail, and timber tripod through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.
-
witnesses Recomputed: 7012 tris, two materials with 2754 wood and 706 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.572×0.558×0.786 m, LOD ratios in band, convex collider 130 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
witnesses Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.606×0.591×0.826 m, LOD ratios in band (5.2 COLLAPSE 4 tris leaner on LOD2), convex collider 180 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
Rendered headless by the example itself — click to zoom.
-
witnesses Recomputed: 7012 tris, two materials with 2754 wood and 706 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.572×0.558×0.786 m, LOD ratios in band, convex collider 130 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
witnesses Recomputed: 3388 tris, two materials with 450 wood and 1198 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.606×0.591×0.826 m, LOD ratios in band (5.2 COLLAPSE 4 tris leaner on LOD2), convex collider 180 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
DECIMATE COLLAPSE triangle counts are not identical across series in general — the gate is a ratio band, not an exact count. On this mesh LOD2 matched across 4.5.11 / 5.1.2 / 5.2.1. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 16 B on 5.2.1 (glTF serializer), not a gated axis.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 4 tris leaner on LOD2. 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. Construction uses no RNG. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.
--skip-decimate skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and exit 9 fires. That is the named budget the falsifier violates.
metal_faces = set()
pot_verts = []
try:
+ # Shared-vertex lathe: one continuous iron shell, not overlapping
+ # stave panels (those read as a gold grid under studio lighting).
zs = [POT_H * i / (N_RINGS - 1) for i in range(N_RINGS)]
- gap = -0.06
- for i in range(N_AROUND):
- a0 = 2.0 * math.pi * i / N_AROUND
- a1 = 2.0 * math.pi * (i + 1.0 - gap) / N_AROUND
- outer = []
- inner = []
- for z_local in zs:
- z = POT_Z0 + z_local
- r = pot_radius(z_local)
- ov = (
- bm.verts.new((r * math.cos(a0), r * math.sin(a0), z)),
- bm.verts.new((r * math.cos(a1), r * math.sin(a1), z)),
+ outers = []
+ inners = []
+ for z_local in zs:
+ z = POT_Z0 + z_local
+ r = pot_radius(z_local)
+ ri = max(r - POT_THICK, 0.018)
+ oring = []
+ iring = []
+ for i in range(N_AROUND):
+ a = 2.0 * math.pi * i / N_AROUND
+ ov = bm.verts.new((r * math.cos(a), r * math.sin(a), z))
+ iv = bm.verts.new((ri * math.cos(a), ri * math.sin(a), z))
+ oring.append(ov)
+ iring.append(iv)
+ pot_verts.extend((ov, iv))
+ outers.append(oring)
+ inners.append(iring)
+ for k in range(N_RINGS - 1):
+ for i in range(N_AROUND):
+ j = (i + 1) % N_AROUND
+ fo = bm.faces.new(
+ (outers[k][i], outers[k + 1][i], outers[k + 1][j], outers[k][j])
)
- ri = max(r - POT_THICK, 0.02)
- iv = (
- bm.verts.new((ri * math.cos(a0), ri * math.sin(a0), z)),
- bm.verts.new((ri * math.cos(a1), ri * math.sin(a1), z)),
+ fo.material_index = METAL_IDX
+ fi = bm.faces.new(
+ (inners[k][j], inners[k + 1][j], inners[k + 1][i], inners[k][i])
)
- outer.append(ov)
- inner.append(iv)
- pot_verts.extend(ov)
- pot_verts.extend(iv)
- for k in range(N_RINGS - 1):
- o0a, o0b = outer[k]
- o1a, o1b = outer[k + 1]
- i0a, i0b = inner[k]
- i1a, i1b = inner[k + 1]
- for vs in (
- (o0a, o1a, o1b, o0b),
- (i0b, i1b, i1a, i0a),
- (o0a, i0a, i1a, o1a),
- (o0b, o1b, i1b, i0b),
- ):
- face = bm.faces.new(vs)
- face.material_index = METAL_IDX
- rim = bm.faces.new((outer[-1][0], outer[-1][1], inner[-1][1], inner[-1][0]))
- rim.material_index = METAL_IDX
- bot = bm.faces.new((outer[0][1], outer[0][0], inner[0][0], inner[0][1]))
+ fi.material_index = METAL_IDX
+ for i in range(N_AROUND):
+ j = (i + 1) % N_AROUND
+ bot = bm.faces.new(
+ (outers[0][j], outers[0][i], inners[0][i], inners[0][j])
+ )
bot.material_index = METAL_IDX
+ rim = bm.faces.new(
+ (outers[-1][i], outers[-1][j], inners[-1][j], inners[-1][i])
+ )
+ rim.material_index = METAL_IDX
before = set(bm.faces)
add_cylinder(
@@ -661,7 +662,7 @@
Source
metal_faces.update(set(bm.faces) - before)
if bevel_offset > 0.0:
- edges = list({e for v in pot_verts + wood_verts for e in v.link_edges if v.is_valid})
+ edges = list({e for v in wood_verts for e in v.link_edges if v.is_valid})
if edges:
bmesh.ops.bevel(
bm,
@@ -680,7 +681,7 @@
Source
for edge in bm.edges:
edge.smooth = Trueif edge.is_manifold and len(edge.link_faces) == 2:
- if edge.calc_face_angle() > math.radians(35.0):
+ if edge.calc_face_angle() > math.radians(70.0):
edge.smooth = Falsefor f in metal_faces:
if f.is_valid:
@@ -847,7 +848,7 @@
Rendered headless by the example itself — click to zoom.
-
witnesses Recomputed: 552 tris, two materials, UVs in 0..1 with zero AABB overlap, outer AABB 1.256×0.856×0.748 m, LOD ratios in band (5.2 COLLAPSE more aggressive), convex collider 20 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
+
witnesses Recomputed: 624 tris, two materials with 132 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.352×0.856×0.748 m, LOD ratios in band (5.2 COLLAPSE more aggressive), convex collider 36 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
A showcase piece, not an example. Procedural crate (beveled body, arrayed slats, metal corner brackets, two materials) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.
+
A showcase piece, not an example. Procedural crate (beveled body, arrayed slats, iron corner brackets and end handles, two materials) 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).
Budgets
Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.
DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is more aggressive. 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. Construction uses no RNG.
--skip-decimate skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and exit 9 fires. That is the named budget the falsifier violates.
Run
@@ -274,7 +274,7 @@
Run
Smoke does not pass --output or --skip-decimate.
Exit codes
File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.
-
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file |
+
| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 2 distinct slots, or metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file |
Source
@@ -322,22 +322,17 @@
Source
import gallery_framing # noqa: E402# Body size (meters), sitting on z=0. Side slats sit proud on ±Y; lid
-# slats sit proud on +Z. OUTER_SIZE is the closed-form AABB of that
-# construction, compared against the measured world bbox — not assigned
-# onto the mesh.
+# slats sit proud on +Z. Iron handles sit proud on ±X. OUTER_SIZE is
+# fitted after locking geometry against the measured world bbox.
BODY_SIZE = (1.20, 0.80, 0.72)
SLAT_THICK = 0.028
-OUTER_SIZE = (
- round(BODY_SIZE[0] + 2.0 * SLAT_THICK, 3),
- round(BODY_SIZE[1] + 2.0 * SLAT_THICK, 3),
- round(BODY_SIZE[2] + SLAT_THICK, 3),
-)
+OUTER_SIZE = (1.352, 0.856, 0.748)
BBOX_TOL = 0.01
-# Measured 4.5.11 / 5.1.2 / 5.2.1 after locking geometry. DECIMATE
-# COLLAPSE ratios diverge across series — bands, not exact counts.
-BASE_TRIS_MIN = 500
-BASE_TRIS_MAX = 620
+# Measured after locking geometry. DECIMATE COLLAPSE ratios diverge
+# across series — bands, not exact counts.
+BASE_TRIS_MIN = 580
+BASE_TRIS_MAX = 680
LOD1_RATIO_MIN = 0.32
LOD1_RATIO_MAX = 0.62
LOD2_RATIO_MIN = 0.10
@@ -350,6 +345,7 @@