Skip to content

Latest commit

 

History

History
222 lines (180 loc) · 10.7 KB

File metadata and controls

222 lines (180 loc) · 10.7 KB

crate-stack

Three shipping crates stacked, each yawed and offset differently

A stack of three shipping crates. A showcase piece, not an example — it witnesses no API contract. It asserts that generated geometry meets declared asset budgets, recomputed from the finished mesh.

What it composes

One build_crate generator, invoked three times with a per-instance seed, then the shipped pipeline:

Shipped content Used for
skills/mesh-editing-and-bmesh box, bevel and UV construction in one bmesh
skills/procedural-materials-and-shaders attribute-driven timber (per-plank tone, grain along each board) and rusted iron
skills/bake-high-to-low Cycles tangent-space normal bake, high onto low
skills/engine-export-presets Unity glTF (export_yup=True)
skills/depsgraph-and-evaluated-data evaluated triangle counts for the LOD ratios
snippets/decimate_to_budget.py LOD1 / LOD2 COLLAPSE chain
snippets/convex_hull_collider.py convex collider
snippets/lod_chain.py LOD naming and ratio pattern
examples/mesh-hygiene-audit hygiene combinatorics (copied, not imported)

Reuse with variation

The point of the piece. Each crate comes out of the same generator; what differs is the seed. That seed drives the yaw, the plank widths, and the board heights, so the three read as three of the same design rather than one model pasted three times.

True instancing — three objects sharing one mesh datablock — and per-instance geometry variation are mutually exclusive. This piece takes the variation, and the shipped asset is the flattened single mesh an engine would receive. The generator is reused; the geometry is not.

Two independent random streams keep that honest. The design stream (yaw, plank widths) is what --same-seed collapses. The placement stream (lateral offsets) stays per-instance either way, so a falsified stack has the same footprint as a good one and fails on the variation budget rather than on the bounding box.

Budgets

Declared in the script as named constants, recomputed from the generated mesh. Measured values below are from Blender 5.2.1; see the table at the end for the 4.5.11 / 5.1.2 figures.

Budget Band Measured
Base triangles 5200–7000 6060
LOD1 ratio 0.32–0.62 0.5000
LOD2 ratio 0.10–0.35 0.2198
Material slots exactly 2, distinct 2
Iron faces ≥ 900 1344
Timber faces ≥ 1800 2418
UV bounds inside 0..1 (0.0006, 0.0007)–(0.9994, 0.9993)
UV AABB overlap ≤ 1e-5 0.000000
Outer AABB 0.658 × 0.524 × 0.704 m ± 0.020 0.6583 × 0.5235 × 0.7040
Crate body footprint 0.588 × 0.428 m ± 0.015, in the crate's own frame 0.5882 × 0.4282 (all three)
Collider triangles ≤ 260 230
Normal bake {'FINISHED'} with image data {'FINISHED'}, has_data=True
glTF export file written, non-empty ~490 kB
Hygiene all zero loose 0/0, non-manifold 0, zero-area 0, doubles 0, n-gons 0, coplanar disjoint pairs 0
Grounded AABB |zmin| ≤ 1e-4 0.00000
Ground runners 3 named supports, each zmin ≤ 1e-3 3 at 0.00000
Crate-to-crate seat both seats, surface gap ≤ 0.0015 m 0.00000 (overlapping)
Yaw band each |yaw| in 0.040–0.192 rad 0.0681, 0.0943, 0.1093
Per-instance variation yaw spread ≥ 0.020 rad, plank spread ≥ 0.0008 m 0.0262 rad, 0.00205 m
Nail seat 48 nails; bite 0.3–1.0 mm into the strap, head ≥ 1.0 mm proud 48; bite 0.60 mm, proud 1.80 mm
Edge treatment manifold edges within 5° of a right angle: 0 timber 0, iron 0

Real-world size: each crate is 0.58 × 0.42 m at the timber and 0.588 × 0.428 m over the corner iron and its nail heads, 0.244 m tall including runners and lid. Three stacked come to 0.70 m — knee height, wider than tall.

Why the footprint is measured in the crate's own frame

The stack AABB is the union of three yawed boxes, so a crate could drift to any size underneath it and the outer budget would not notice. Each crate's footprint is therefore measured after un-rotating by the yaw that crate was measured to have, not the yaw it was built with. All three land on 0.5882 × 0.4282 m exactly, which is what makes the un-rotation trustworthy.

Why parts are classified by principal axes

Every part is an axis-aligned box in its crate's frame and then rotated about Z. A world-AABB shape filter measures the rotated bounding box, not the part: a 0.554 m board yawed 0.09 rad reports 0.061 m of depth instead of its 0.013 m thickness, and every filter keyed to thickness silently matches nothing. xy_principal recovers each box's own axes, and hands back the yaw as a by-product — which is where the variation budget's yaw numbers come from.

Why stack levels come from the runners

Binning parts against the declared stack pitch put a lid — which sits 8 mm below the next crate's base — on the wrong level the moment a falsifier shifted a crate, and the seat budget then compared a crate against itself. The bands are clustered from the runner heights the mesh actually has, so they follow the geometry including when a falsifier moves it.

Why the corner iron is one shell

Each strap used to be two overlapping boxes, one per leg of the L. Both boxes had a vertical edge on the strap's outer corner, and once the iron was chamfered each box laid a chamfer strip on that same line: a coplanar cross-shell pair at all twelve corners, which the z-fight budget caught. The strap is now a single L-section extrusion — each cap two convex quads meeting on the inner-corner diagonal, so no n-gon — with one outer corner. The iron bevel passes material=METAL_IDX; left to its default, bevel gave the chamfer faces slot 0 and the plates rendered and classified as timber.

Why nails are told from plates by the strap's size

Nail and plate shells are both iron; the nail audit separates them by world-AABB extent. The threshold is IRON_WRAP * 0.5, derived from the strap. A fixed 10 mm threshold dropped 8 of 48 nails as soon as the heads grew to 10 mm, because a yawed head's world AABB is wider than the head.

Timber surface

paint_planks writes two face attributes: PlankTone, a seeded tone per shell, and GrainDir, the shell's own long axis recovered from its vertices. The wood shader samples noise in object space with the component along GrainDir compressed, so the grain streaks run along each board whatever the crate's yaw, and scales the colour by the plank's tone. Neither attribute is a budget: a tone cannot be seen by an assertion, which is why the inspection sheet looked at it.

Determinism

Fixed seed 41; no unseeded randomness. Identical geometry across runs on one binary and across 4.5.11, 5.1.2 and 5.2.1 — every measured value above is byte-identical on the three. LOD2 used to differ — DECIMATE COLLAPSE produced 878 triangles on 5.2 and 934 on 4.5 and 5.1 on the earlier mesh — and on the current mesh lands on 1332 on all three. The LOD gate stays a ratio band (0.10–0.35) rather than an exact count, because that agreement is a property of this mesh, not of the modifier.

Falsifiers

Each breaks one pipeline stage so a named budget fails. All eight were run on 4.5.11, 5.1.2 and 5.2.1 and produced the same exit code on all three.

Flag Breaks Exit
--skip-decimate drops the DECIMATE modifiers, LOD1 ratio goes to 1.0000 9
--stray-vert adds one loose vertex inside the silhouette, so hygiene catches it rather than the bounding box 15
--lift-z lifts the whole mesh 50 mm off the floor 16
--short-skids floats one of the three ground runners 12 mm; the other two still ground the AABB, so only the named-support budget sees it 16
--float-stack lifts the top crate 9 mm clear of the lid below, opening a 34 mm seat gap 18
--same-seed gives all three crates the same design seed; yaw spread and plank spread both go to 0 20
--float-nails lifts every nail head 1.5 mm along its plate normal; bite goes to −0.90 mm, nail seat budget 18
--sharp-iron skips the iron chamfer pass; 216 right-angle iron edges, edge-treatment budget 21

Exit codes

File-local and sequential. 9 is a valid check code; there is no rule against it. 1 is the FATAL wrapper — a crash, never a named check.

Code Meaning
0 Success
1 Uncaught exception (FATAL wrapper)
2 argparse / usage
3 Mesh did not build, or has no UV layer
4 Base triangle count outside band
5 Material slots, or a material's face floor
6 UVs outside 0..1
7 UV AABB overlap above tolerance
8 Outer AABB off declared size
9 LOD1 or LOD2 ratio outside band (--skip-decimate)
10 Framing gate (examples/gallery_framing.py, render path only)
11 Collider triangles above ceiling
12 Normal bake failed or produced no image data
13 glTF export missing or empty
14 --output produced no file
15 Mesh hygiene (--stray-vert)
16 Grounded zmin, or a named ground runner floating (--lift-z, --short-skids)
18 Crate-to-crate seat gap (--float-stack), or a nail head off its strap seat band (--float-nails)
19 Crate body footprint off declared size
20 Per-instance variation collapsed (--same-seed)
21 A manifold edge within 5° of a right angle: a chamfer pass skipped (--sharp-iron)

17 is unused here: this piece has no diagonal member. 15–19 are reserved across showcase pieces for the hygiene family, so the numbering skips rather than reuses.

Run it

# Budget check, no render. ~1.4-1.7 s on 4.5 / 5.1 / 5.2.
blender --background --python crate_stack.py --

# Falsifier: the three crates become copies. Must exit 20.
blender --background --python crate_stack.py -- --same-seed

# Falsifier: the top crate floats off the lid below. Must exit 18.
blender --background --python crate_stack.py -- --float-stack

# Render the gallery still (EEVEE; --engine cycles on a GPU-less host).
blender --background --python crate_stack.py -- --output stack.webp

Smoke runs the check-only path. It does not pass --output or any falsifier.

Cross-version measurements

Value 4.5.11 5.1.2 5.2.1
Base triangles 6060 6060 6060
LOD1 tris / ratio 3030 / 0.5000 3030 / 0.5000 3030 / 0.5000
LOD2 tris / ratio 1332 / 0.2198 1332 / 0.2198 1332 / 0.2198
Outer AABB 0.6583 × 0.5235 × 0.7040 same same
Collider tris 230 230 230
Yaws (rad) 0.0943, −0.1093, 0.0681 same same
Seat gap 0.00000 0.00000 0.00000
Nails / bite / proud 48 / 0.60 mm / 1.80 mm same same
Right-angle edges 0 0 0
Check wall-clock ~1.59 s ~1.41 s ~1.65 s