Vertex Color AO
A stone well carrying baked ambient occlusion in a colour attribute, with the bake held to the closed-form hemisphere integral rather than to a captured value.
examples/lightmap-uv-channel/
A market cart carrying the two-channel UV contract for baked lighting: UV0 untouched, UVLight packed with no overlaps and a respected margin.
Rendered headless by the example itself. Select it to enlarge.
tags uv game-pipeline rendering
runs on 4.5 LTS · 5.1 · 5.2 LTS
blender --background --python examples/lightmap-uv-channel/lightmap_uv_channel.py --
falsify it Breaks the contract on purpose and must exit 7
blender --background --python examples/lightmap-uv-channel/lightmap_uv_channel.py -- --overlap-islands
A runnable example building the second UV layer engines require for baked lighting, on an asset built to be reused: a market cart assembled from 24 named, watertight parts — a plank-grooved bed with side rails, corner brackets, and fasteners, spoked wheel assemblies (felloe ring + eight spokes + hub + iron tyre + bolt ring) on a capped axle, draw shafts with a crossbar and a prop leg, four posts, and a ribbed canvas canopy. Each post rises from inside the bed into the canopy's underside, its top sheared to the arc at its own station for a uniform 12 mm bite. Fixed 1.35 m posts used to stop 0.33 m short of the arc, so the canopy floated. The UV1 contract is unchanged: every part keeps its island count, zero overlap and minimum island distance. Ground-level pivot at z=0, identity transforms by construction, Cart.* datablocks. (An earlier revision shipped a soft uniform-tan cart with lumpy polygon wheels and was remodeled under the asset-quality gate; its wheel material was also silently never applied — a name-key lookup bug the exact per-part mapping now replaces.) UV0 is the texture channel (deterministic dominant-axis box projection per face); UVLight is the baked-lighting atlas (smart_project → lightmap_pack).
What it witnesses (all independently derived, nothing trusted from the packer):
uv_layers.new() does not move the active flags — the first layer keeps active + active_render, so a UV op runs against channel 0 unless you move active yourself (probed: unwrap+pack with the flags unmoved destroys channel 0, measured drift 2.068). Worse, the edit-mode UV ops clear both flags — the sequence must re-assert them. The check pins, per part: exactly two layers UVMap/UVLight, active == UVLight (edit target), active_render == UVMap (render target).[0, 1] within 1e-5.MARGIN_DIV semantics measured live: nominal margin == MARGIN_DIV × 0.01 UV units; the check requires the measured min island-pair distance ≥ half the nominal — measured 0.00401 (≈ 2× the nominal 0.002, the packer's per-island margin applied twice).What each check catches on failure: wrong layer names/count (exit 3); flags not re-established after the ops (exit 4); channel 0 touched by the UV1 unwrap (exit 5 — probed via the clobber trap, drift 2.068); UV1 loops outside the unit square (exit 6); overlapping islands (exit 7 — probed by translating an island onto a neighbor, 15 SAT hits); margin floor broken (exit 8 — probed with MARGIN_DIV=0, measured 2e-5 < 0.001); non-watertight part (exit 9).
Authoring hazards pinned while building this (probes in .scratch-style bisect scripts; the reasons the check code re-fetches layers by name and re-asserts flags):
MeshUVLoopLayer reference across edit-mode UV ops and then iterating its .data segfaults Blender 4.5.11 headless (EXCEPTION_ACCESS_VIOLATION, 5/5 repro); the same read survives on 5.1.2. Re-fetch mesh.uv_layers[name] after CustomData-reallocating calls. Second confirmation of the UV-handle lifetime hazard found authoring triangulate-tangents.bpy.ops.uv.lightmap_pack silently packs nothing (exit OK) when the active layer has no UVs yet — unwrap first, then pack.Pipeline arc neighbors: UV-layer authoring in uv-layer-grid, tangent-space from UVs in triangulate-tangents, export round-trip in gltf-export-roundtrip, pivot discipline in prop-origin-transform.
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.2.1 (24 parts, 3428 islands, drift 0, overlap 0, min island distance 0.00401). The UV layout is packer-version-dependent by design; the contracts are layout-independent invariants.
Render as proof: the cart beside its enlarged UV1 atlas display — a slate face in a walnut frame on a plinth with a brass UV1 LIGHTMAP plate, a faint quarter grid marking the [0,1] square the bounds check holds — the Bed's packed lightmap built from live UV data, so a change in the atlas moves the board geometry. The falsification variant (--falsify) translates the second-largest island onto the largest: a big emissive-red island visibly stacked over the atlas (15 SAT hits in the check probe). The render path also gates the asset itself through examples/gallery_asset_quality.py (naming, material variation, edge treatment — exit 11): 24 named parts, 5 materials, right-angle share 0.138. Wood, canvas and iron carry procedural grain, weave and pitting over Object coordinates; no check reads a material.
blender --background --python lightmap_uv_channel.py --
blender --background --python lightmap_uv_channel.py -- --overlap-islands # falsifier: overlap scan reports the dragged island; exits 7
blender --background --python lightmap_uv_channel.py -- --output atlas.png
blender --background --python lightmap_uv_channel.py -- --falsify overlap.png
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | UV layer names ≠ UV0 / UV1 |
| 4 | UVMap / UV1 render/clone flags wrong after re-assert |
| 5 | Channel 0 (UV0) touched by the UV1 unwrap |
| 6 | UV1 loops outside [0,1] |
| 7 | SAT overlap self-test failed, or UV1 triangle pairs overlap (--overlap-islands lands here) |
| 8 | Min island distance below margin |
| 9 | Part is not watertight |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 11 | Gallery asset-quality violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify. Its catalog falsifier is --overlap-islands (expect exit 7): the same island drag as --falsify, applied before the SAT scan.