diff --git a/README.md b/README.md index ef3bc93..ec3d200 100644 --- a/README.md +++ b/README.md @@ -75,30 +75,35 @@ blender --background --python examples/bmesh-gear/bmesh_gear.py -- Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](showcase/README.md). -Shipping crate: a wooden slat crate with iron corner brackets on a dark studio floor, warm wedge on the back wall -Stone well: a round brick well with a shingled pyramid roof on posts, rope and bucket, on a dark studio floor -Wooden barrel: a staved barrel with iron hoops on a dark studio floor, warm wedge on the back wall -Campfire: a two-course stone fire ring with a tripod of logs on a dark studio floor, warm wedge on the back wall -Market stall: a timber frame with post feet, slatted counter, rafters, and a striped awning on a dark studio floor, warm wedge on the back wall -Street lantern: an iron post with brass collars, a braced arm, and a muntined amber cage on a dark studio floor -Treasure chest: a slatted wooden chest with iron bands, corner brackets, and an open lid on a dark studio floor -Terrain scatter: a Geometry Nodes dirt hill tile with bevelled masonry rocks on a dark studio floor -Fence kit: a timber post-and-rail section with pyramidal caps, iron shoes and straps on a dark studio floor -Watchtower: a timber lookout with X-braces, hatch ladder, plank platform, and a coursed shake roof on a dark studio floor -Cart: a two-wheel wooden cart with spoked wheels, slatted bed, and shafts on a dark studio floor -Park bench: a wrought-iron bench with scrolled legs and wooden slats on a dark studio floor -Wheelbarrow: a staved wooden tray with iron straps, a spoked wheel, rear legs, and handles on a dark studio floor -Anvil: a London-pattern anvil on a hooped timber stump on a dark studio floor -Water trough: a watertight wooden trough with plank ends, contained water, and trestle legs on a dark studio floor -Hitching post: a square timber post with a pyramidal cap, cross-arm, iron bands, and two hitching rings on a dark studio floor -Grindstone: a stone wheel on a timber A-frame with an iron axle, hubs, crank, and tray on a dark studio floor -Hand pump: a cast-iron village pump on a wooden plinth with a spout and handle on a dark studio floor -Signpost: a timber post with two painted fingerboards, iron straps, and a pyramidal cap on a dark studio floor -Chopping block: a hooped timber stump with an embedded axe on a dark studio floor -Wooden bucket: a coopered bucket with overlapping staves, iron hoops, and a rope bail on a dark studio floor -Wall torch: a coursed stone plaque with an iron sconce, wooden haft, and flame on a dark studio floor -Tavern stool: a round wooden seat on turned splayed legs with stretchers and iron ferrules on a dark studio floor -Iron cauldron: a bulged iron pot hanging from a timber tripod on a dark studio floor +
+24 showcase pieces — click to expand the preview grid + +

+ Shipping crate: a wooden slat crate with iron corner brackets on a dark studio floor, warm wedge on the back wall + Stone well: a round brick well with a shingled pyramid roof on posts, rope and bucket, on a dark studio floor + Wooden barrel: a staved barrel with iron hoops on a dark studio floor, warm wedge on the back wall + Campfire: a two-course stone fire ring with a tripod of logs on a dark studio floor, warm wedge on the back wall + Market stall: a timber frame with post feet, slatted counter, rafters, and a striped awning on a dark studio floor, warm wedge on the back wall + Street lantern: an iron post with brass collars, a braced arm, and a muntined amber cage on a dark studio floor + Treasure chest: a slatted wooden chest with iron bands, corner brackets, and an open lid on a dark studio floor + Terrain scatter: a Geometry Nodes dirt hill tile with bevelled masonry rocks on a dark studio floor + Fence kit: a timber post-and-rail section with pyramidal caps, iron shoes and straps on a dark studio floor + Watchtower: a timber lookout with X-braces, hatch ladder, plank platform, and a coursed shake roof on a dark studio floor + Cart: a two-wheel wooden cart with spoked wheels, slatted bed, and shafts on a dark studio floor + Park bench: a wrought-iron bench with scrolled legs and wooden slats on a dark studio floor + Wheelbarrow: a staved wooden tray with iron straps, a spoked wheel, rear legs, and handles on a dark studio floor + Anvil: a London-pattern anvil on a hooped timber stump on a dark studio floor + Water trough: a watertight wooden trough with plank ends, contained water, and trestle legs on a dark studio floor + Hitching post: a square timber post with a pyramidal cap, cross-arm, iron bands, and two hitching rings on a dark studio floor + Grindstone: a stone wheel on a timber A-frame with an iron axle, hubs, crank, and tray on a dark studio floor + Hand pump: a cast-iron village pump on a wooden plinth with a spout and handle on a dark studio floor + Signpost: a timber post with two painted fingerboards, iron straps, and a pyramidal cap on a dark studio floor + Chopping block: a hooped timber stump with an embedded axe on a dark studio floor + Wooden bucket: a coopered bucket with overlapping staves, iron hoops, and a rope bail on a dark studio floor + Wall torch: a dressed stone plaque with an iron sconce, wooden haft, and flame on a dark studio floor + Tavern stool: a round wooden seat on turned splayed legs with stretchers and iron ferrules on a dark studio floor + Iron cauldron: a bulged iron pot hanging from a timber tripod on a dark studio floor +

[`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

anvil render

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.
blender --background --python showcase/anvil/anvil.py --
@@ -264,8 +264,8 @@

anvil

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.

+

| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 6450–6800 | 6616 / 6616 / 6616 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 | | Materials | exactly 2 distinct, ≥24 metal faces | 2 slots, 628 wood, 2616 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.772, 0.419, 0.662) m ± 0.01 | (0.7720, 0.4183, 0.6618), zmin 0 | | Collider tris | ≤ 220 | 190 | | Export | written, size > 0 | 474984 / 474984 / 474972 bytes |

+

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.

Run

blender --background --python anvil.py --
@@ -319,23 +319,18 @@ 

Source

sys.dont_write_bytecode = True import gallery_framing # noqa: E402 -STUMP_R = 0.195 -STUMP_H = 0.34 -FACE_L = 0.40 -FACE_W = 0.125 -FACE_T = 0.048 -BODY_H = 0.18 -WAIST_L = 0.22 -WAIST_W = 0.088 -FOOT_L = 0.32 -FOOT_W = 0.15 -HORN_L = 0.28 -HEEL_L = 0.11 +STUMP_R = 0.20 +STUMP_H = 0.33 +FACE_L = 0.42 +FACE_W = 0.118 +FACE_T = 0.046 +HORN_L = 0.30 +HEEL_L = 0.12 BBOX_TOL = 0.01 -OUTER_SIZE = (0.746, 0.410, 0.682) -BASE_TRIS_MIN = 1620 -BASE_TRIS_MAX = 1780 +OUTER_SIZE = (0.772, 0.419, 0.662) +BASE_TRIS_MIN = 6450 +BASE_TRIS_MAX = 6800 LOD1_RATIO_MIN = 0.32 LOD1_RATIO_MAX = 0.62 LOD2_RATIO_MIN = 0.10 @@ -345,7 +340,7 @@

Source

MATERIAL_COUNT = 2 UV_EPS = 1e-4 UV_OVERLAP_MAX = 1e-5 -COLLIDER_TRIS_MAX = 180 +COLLIDER_TRIS_MAX = 220 BAKE_RES = 256 CAGE_EXTRUSION = 0.08 METAL_FACES_MIN = 24 @@ -548,7 +543,7 @@

Source

STUMP_R + 0.010, STUMP_R - 0.018, STUMP_H, - 20, + 24, WOOD_IDX, ) ) @@ -558,7 +553,7 @@

Source

(0.0, 0.0, STUMP_H + 0.010), STUMP_R - 0.022, 0.020, - 20, + 24, WOOD_IDX, ) ) @@ -586,36 +581,36 @@

Source

metal.extend( add_box( bm, - (0.0, 0.0, z0 + 0.024), - (0.38, 0.168, 0.048), + (0.0, 0.0, z0 + 0.028), + (0.44, 0.20, 0.056), METAL_IDX, ) ) metal.extend( add_box( bm, - (0.0, 0.0, z0 + 0.066), - (0.30, 0.132, 0.038), + (0.0, 0.0, z0 + 0.078), + (0.30, 0.130, 0.046), METAL_IDX, ) ) metal.extend( add_box( bm, - (0.0, 0.0, z0 + 0.148), - (0.176, 0.070, 0.130), + (0.0, 0.0, z0 + 0.155), + (0.150, 0.058, 0.112), METAL_IDX, ) ) metal.extend( add_box( bm, - (0.0, 0.0, z0 + 0.248), - (0.34, 0.112, 0.052), + (0.0, 0.0, z0 + 0.236), + (0.36, 0.108, 0.050), METAL_IDX, ) ) - face_z = z0 + 0.248 + 0.026 + FACE_T / 2.0 + face_z = z0 + 0.236 + 0.025 + FACE_T / 2.0 metal.extend( add_box( bm, @@ -625,15 +620,15 @@

Source

) ) - horn_z = face_z - 0.012 + horn_z = face_z - 0.006 metal.extend( add_cone( bm, - (FACE_L / 2.0 + HORN_L / 2.0 - 0.028, 0.0, horn_z), - 0.056, - 0.014, + (FACE_L / 2.0 + HORN_L / 2.0 - 0.050, 0.0, horn_z), + FACE_W * 0.46, + 0.012, HORN_L, - 16, + 20, METAL_IDX, euler=(0.0, math.pi / 2.0, 0.0), ) @@ -641,34 +636,34 @@

Source

metal.extend( add_box( bm, - (FACE_L / 2.0 - 0.018, 0.0, horn_z - 0.018), - (0.10, FACE_W * 0.90, 0.048), + (FACE_L / 2.0 - 0.010, 0.0, horn_z - 0.014), + (0.12, FACE_W * 0.88, 0.040), METAL_IDX, ) ) metal.extend( add_box( bm, - (-FACE_L / 2.0 - HEEL_L / 2.0 + 0.016, 0.0, face_z - 0.006), - (HEEL_L, FACE_W * 0.90, FACE_T * 0.78), + (-FACE_L / 2.0 - HEEL_L / 2.0 + 0.018, 0.0, face_z - 0.004), + (HEEL_L, FACE_W * 0.92, FACE_T * 0.82), METAL_IDX, ) ) metal.extend( add_box( bm, - (FACE_L * 0.18, 0.0, face_z + FACE_T / 2.0 - 0.005), - (0.032, 0.032, 0.012), + (-FACE_L * 0.22, 0.0, face_z + FACE_T / 2.0 - 0.005), + (0.028, 0.028, 0.012), METAL_IDX, ) ) metal.extend( add_cyl( bm, - (FACE_L * 0.30, 0.0, face_z + FACE_T / 2.0 - 0.004), - 0.009, + (FACE_L * 0.22, 0.0, face_z + FACE_T / 2.0 - 0.004), + 0.008, 0.012, - 10, + 12, METAL_IDX, ) ) @@ -686,15 +681,35 @@

Source

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 = False finally: bm.free() out = bpy.data.objects.new(name, me) @@ -843,10 +858,10 @@

Source

def check(skip_decimate): bpy.ops.wm.read_factory_settings(use_empty=True) - low = build_anvil_mesh("AnvilLow", bevel_offset=0.005, bevel_segments=2) - high = build_anvil_mesh("AnvilHigh", bevel_offset=0.005, bevel_segments=4) + low = build_anvil_mesh("AnvilLow", bevel_offset=0.010, bevel_segments=2) + high = build_anvil_mesh("AnvilHigh", bevel_offset=0.010, bevel_segments=4) wood = principled("AnvilStump", (0.34, 0.18, 0.07, 1.0), 0.0, 0.62) - metal = principled("AnvilSteel", (0.44, 0.45, 0.47, 1.0), 1.0, 0.30) + metal = principled("AnvilSteel", (0.16, 0.15, 0.14, 1.0), 0.90, 0.34) assign_slots(low, wood, metal) assign_slots(high, wood, metal) @@ -995,8 +1010,8 @@

Source

ob.hide_render = True ob.hide_viewport = True - low.rotation_euler.z = math.radians(-8.0) - low.rotation_euler.x = math.radians(0.0) + low.rotation_euler.z = math.radians(-42.0) + low.rotation_euler.x = math.radians(2.0) floor_me = bpy.data.meshes.new("Floor") bm = bmesh.new() @@ -1042,10 +1057,10 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (0.70, -2.20, 1.08) + cam.location = (1.32, -1.90, 1.04) scene.collection.objects.link(cam) aim = bpy.data.objects.new("Aim", None) - aim.location = (0.0, 0.0, 0.40) + aim.location = (0.0, 0.0, 0.44) scene.collection.objects.link(aim) con = cam.constraints.new("TRACK_TO") con.target = aim diff --git a/docs/gallery/assets/anvil-hero.webp b/docs/gallery/assets/anvil-hero.webp index 3caed77..d9cfb04 100644 Binary files a/docs/gallery/assets/anvil-hero.webp and b/docs/gallery/assets/anvil-hero.webp differ diff --git a/docs/gallery/assets/iron-cauldron-hero.webp b/docs/gallery/assets/iron-cauldron-hero.webp index 6873e35..c17a5b8 100644 Binary files a/docs/gallery/assets/iron-cauldron-hero.webp and b/docs/gallery/assets/iron-cauldron-hero.webp differ diff --git a/docs/gallery/assets/shipping-crate-hero.webp b/docs/gallery/assets/shipping-crate-hero.webp index fc9eebb..588b8d6 100644 Binary files a/docs/gallery/assets/shipping-crate-hero.webp and b/docs/gallery/assets/shipping-crate-hero.webp differ diff --git a/docs/gallery/assets/wall-torch-hero.webp b/docs/gallery/assets/wall-torch-hero.webp index 21046e4..cb700f3 100644 Binary files a/docs/gallery/assets/wall-torch-hero.webp and b/docs/gallery/assets/wall-torch-hero.webp differ diff --git a/docs/gallery/index.html b/docs/gallery/index.html index fe829c3..da86670 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -892,7 +892,7 @@

gn-socket-rename

shipping-crate

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.

View example
@@ -1035,7 +1035,7 @@

wheelbarrow

anvil

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.

View example
@@ -1118,12 +1118,12 @@

wooden-bucket

- wall-torch — 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… + wall-torch — 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…

wall-torch

-

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.

View example
@@ -1145,7 +1145,7 @@

tavern-stool

iron-cauldron

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.

View example
diff --git a/docs/gallery/iron-cauldron/index.html b/docs/gallery/iron-cauldron/index.html index 66fe0b0..e7fb5c5 100644 --- a/docs/gallery/iron-cauldron/index.html +++ b/docs/gallery/iron-cauldron/index.html @@ -253,7 +253,7 @@

iron-cauldron

iron-cauldron render

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.
blender --background --python showcase/iron-cauldron/iron_cauldron.py --
@@ -264,8 +264,8 @@

iron-cauldron

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 | 6850–7200 | 7012 / 7012 / 7012 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2199 / 0.2199 / 0.2199 | | Materials | exactly 2 distinct, ≥12 wood, ≥24 metal | 2 slots, 2754 wood, 706 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.572, 0.558, 0.786) m ± 0.01 | (0.5723, 0.5582, 0.7855), zmin 0.002 | | Collider tris | ≤ 180 | 130 | | Export | written, size > 0 | 501796 / 501796 / 501780 bytes |

-

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.

+

| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 3280–3520 | 3388 / 3388 / 3388 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2196 / 0.2196 / 0.2184 | | Materials | exactly 2 distinct, ≥12 wood, ≥24 metal | 2 slots, 450 wood, 1198 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.606, 0.591, 0.826) m ± 0.01 | (0.6061, 0.5911, 0.8255), zmin 0.002 | | Collider tris | ≤ 200 | 180 | | Export | written, size > 0 | 250748 / 250748 / 250736 bytes |

+

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.

Run

blender --background --python iron_cauldron.py --
@@ -321,24 +321,24 @@ 

Source

sys.dont_write_bytecode = True import gallery_framing # noqa: E402 -POT_H = 0.26 -POT_Z0 = 0.15 -POT_THICK = 0.016 -N_AROUND = 16 -N_RINGS = 5 -R_BOT = 0.065 -R_MID = 0.175 -R_TOP = 0.135 -APEX_Z = 0.78 -TRIPOD_R = 0.30 -BAIL_SEGS = 10 -BAIL_R = 0.010 +POT_H = 0.32 +POT_Z0 = 0.14 +POT_THICK = 0.014 +N_AROUND = 32 +N_RINGS = 12 +R_BOT = 0.050 +R_MID = 0.205 +R_TOP = 0.148 +APEX_Z = 0.82 +TRIPOD_R = 0.32 +BAIL_SEGS = 12 +BAIL_R = 0.009 BBOX_TOL = 0.01 # Fitted after locking geometry. Recomputed from bound_box. -OUTER_SIZE = (0.572, 0.558, 0.786) +OUTER_SIZE = (0.606, 0.591, 0.826) -BASE_TRIS_MIN = 6850 -BASE_TRIS_MAX = 7200 +BASE_TRIS_MIN = 3280 +BASE_TRIS_MAX = 3520 LOD1_RATIO_MIN = 0.32 LOD1_RATIO_MAX = 0.62 LOD2_RATIO_MIN = 0.10 @@ -348,7 +348,7 @@

Source

MATERIAL_COUNT = 2 UV_EPS = 1e-4 UV_OVERLAP_MAX = 1e-5 -COLLIDER_TRIS_MAX = 180 +COLLIDER_TRIS_MAX = 200 BAKE_RES = 256 CAGE_EXTRUSION = 0.08 METAL_FACES_MIN = 24 @@ -540,46 +540,47 @@

Source

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 = True if 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 = False for f in metal_faces: if f.is_valid: @@ -847,7 +848,7 @@

Source

low = build_cauldron_mesh("CauldronLow", bevel_offset=0.003, bevel_segments=2) high = build_cauldron_mesh("CauldronHigh", bevel_offset=0.003, bevel_segments=4) wood = principled("TripodWood", (0.38, 0.22, 0.09, 1.0), 0.0, 0.58) - metal = principled("CauldronIron", (0.22, 0.23, 0.24, 1.0), 1.0, 0.42) + metal = principled("CauldronIron", (0.10, 0.095, 0.09, 1.0), 0.92, 0.40) assign_slots(low, wood, metal) assign_slots(high, wood, metal) @@ -1045,10 +1046,10 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (1.55, -2.05, 1.35) + cam.location = (1.58, -2.18, 1.22) scene.collection.objects.link(cam) aim = bpy.data.objects.new("Aim", None) - aim.location = (0.0, 0.0, 0.38) + aim.location = (0.0, 0.0, 0.46) scene.collection.objects.link(aim) con = cam.constraints.new("TRACK_TO") con.target = aim diff --git a/docs/gallery/shipping-crate/index.html b/docs/gallery/shipping-crate/index.html index 41ae7b1..5bb0ce7 100644 --- a/docs/gallery/shipping-crate/index.html +++ b/docs/gallery/shipping-crate/index.html @@ -253,18 +253,18 @@

shipping-crate

shipping-crate render

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.
blender --background --python showcase/shipping-crate/shipping_crate.py --
-

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.

-

| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 500–620 | 552 / 552 / 552 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.4239 | | LOD2 ratio | 0.10–0.35 of base | 0.2174 / 0.2174 / 0.1558 | | Materials | exactly 2 distinct | 2 | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.256, 0.856, 0.748) m ± 0.01 | (1.2560, 0.8560, 0.7480), zmin 0 | | Collider tris | ≤ 48 | 20 | | Export | written, size > 0 | 42092 / 42092 / 42088 bytes |

+

| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 580–680 | 624 / 624 / 624 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.4455 | | LOD2 ratio | 0.10–0.35 of base | 0.2179 / 0.2179 / 0.1571 | | Materials | exactly 2 distinct, ≥24 metal faces | 2 slots, 180 wood, 132 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (1.352, 0.856, 0.748) m ± 0.01 | (1.3520, 0.8560, 0.7480), zmin 0 | | Collider tris | ≤ 48 | 36 | | Export | written, size > 0 | 50148 / 50148 / 50140 bytes |

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 @@

Source

COLLIDER_TRIS_MAX = 48 BAKE_RES = 256 CAGE_EXTRUSION = 0.08 +METAL_FACES_MIN = 24 WOOD_IDX = 0 METAL_IDX = 1 @@ -490,8 +486,8 @@

Source

WOOD_IDX, ) - plate = 0.13 - thick = 0.024 + plate = 0.18 + thick = 0.028 hx = sx / 2.0 + slat_h hy = sy / 2.0 + slat_h hz = sz @@ -519,6 +515,28 @@

Source

METAL_IDX, ) + handle_out = 0.038 + for sxn in (-1.0, 1.0): + hx_bar = sxn * (hx + handle_out) + add_cube( + bm, + (hx_bar, -0.12, sz * 0.48), + (0.020, 0.020, 0.12), + METAL_IDX, + ) + add_cube( + bm, + (hx_bar, 0.12, sz * 0.48), + (0.020, 0.020, 0.12), + METAL_IDX, + ) + add_cube( + bm, + (hx_bar, 0.0, sz * 0.54), + (0.020, 0.26, 0.020), + METAL_IDX, + ) + pack_uvs(bm) bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces)) for face in bm.faces: @@ -549,9 +567,18 @@

Source

def assign_slots(obj, wood, metal): - obj.data.materials.clear() - obj.data.materials.append(wood) - obj.data.materials.append(metal) + # Do not materials.clear() — that resets every polygon's material_index + # to 0 and the iron brackets vanish into wood. + mats = obj.data.materials + if len(mats) == 0: + mats.append(wood) + mats.append(metal) + return + mats[0] = wood + if len(mats) == 1: + mats.append(metal) + else: + mats[1] = metal def world_bbox(obj): @@ -684,7 +711,7 @@

Source

low = build_crate_mesh("CrateLow", bevel_offset=0.028, bevel_segments=2) high = build_crate_mesh("CrateHigh", bevel_offset=0.028, bevel_segments=4) wood = principled("CrateWood", (0.48, 0.22, 0.07, 1.0), 0.0, 0.50) - metal = principled("CrateMetal", (0.58, 0.60, 0.64, 1.0), 1.0, 0.22) + metal = principled("CrateMetal", (0.14, 0.13, 0.12, 1.0), 0.86, 0.36) assign_slots(low, wood, metal) assign_slots(high, wood, metal) @@ -695,6 +722,10 @@

Source

mats = [s for s in low.data.materials if s is not None] nmat = len(mats) distinct_mats = len({id(s) for s in mats}) + idx_counts = {} + for poly in low.data.polygons: + idx_counts[poly.material_index] = idx_counts.get(poly.material_index, 0) + 1 + print(f"measured mat_index_counts={idx_counts}") u0, v0, u1, v1, overlap, nfaces = uv_stats(low.data) bb = world_bbox(low) size_x = bb[3] - bb[0] @@ -756,6 +787,11 @@

Source

f"material slots {nmat} distinct {distinct_mats} != {MATERIAL_COUNT}", 5, ), None, None, None, None, None + if idx_counts.get(METAL_IDX, 0) < METAL_FACES_MIN: + return fail( + f"metal faces {idx_counts.get(METAL_IDX, 0)} < {METAL_FACES_MIN}", + 5, + ), None, None, None, None, None if u0 < -UV_EPS or v0 < -UV_EPS or u1 > 1.0 + UV_EPS or v1 > 1.0 + UV_EPS: return fail( f"UVs outside 0..1: ({u0:.4f},{v0:.4f})-({u1:.4f},{v1:.4f})", @@ -819,8 +855,8 @@

Source

ob.hide_render = True ob.hide_viewport = True - low.rotation_euler.z = math.radians(-28.0) - low.rotation_euler.x = math.radians(2.0) + low.rotation_euler.z = math.radians(-48.0) + low.rotation_euler.x = math.radians(4.0) floor_me = bpy.data.meshes.new("Floor") bm = bmesh.new() @@ -866,10 +902,10 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (2.30, -3.20, 1.92) + cam.location = (2.25, -2.75, 1.38) scene.collection.objects.link(cam) aim = bpy.data.objects.new("Aim", None) - aim.location = (0.0, 0.0, OUTER_SIZE[2] / 2.0 + 0.06) + aim.location = (0.0, 0.0, 0.40) scene.collection.objects.link(aim) con = cam.constraints.new("TRACK_TO") con.target = aim diff --git a/docs/gallery/wall-torch/index.html b/docs/gallery/wall-torch/index.html index 489dfc7..8e07e51 100644 --- a/docs/gallery/wall-torch/index.html +++ b/docs/gallery/wall-torch/index.html @@ -4,18 +4,18 @@ wall-torch — Examples — Blender Developer Tools - + - + - +