Campfire

showcase/campfire/

A procedural campfire 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: 2760 tris, three materials with 216 wood, 48 ash and 1416 stone faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.802×0.802×0.404 m, LOD ratios in band, convex collider 162 tris, hygiene 0, 14 grounded bottom stones, teepee kiss 0.00034 m, course seat 0, stone variation 0.01269 m, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-stones 16; --float-logs 17; --gap-courses 18; --uniform-stones 20.

category Village

tags mesh export showcase

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

A showcase piece, not an example. Procedural campfire (two-course running-bond ring of 14 rounded fieldstones per course, pit-floor cobbles, ash with embers, a kissing log tripod that overshoots a shared apex ring, floor logs, and charcoal) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

Courses share a Z plane; --gap-courses is the falsifier. Teepee axes aim at a ring of radius (STICK_R * 0.55) / sin(π/N) and overshoot so the caps cross; --float-logs inflates that ring.

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 triangles1400–36002760 / 2760 / 2760
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.4993
LOD2 ratio0.10–0.35 of base0.2196 / 0.2196 / 0.2196
Materialsexactly 3 distinct; ≥80 wood, ≥24 ash, ≥200 stone3 slots; 216 / 48 / 1416
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(0.807, 0.808, 0.404) m ± 0.015(0.8024, 0.8022, 0.4037), zmin 0
Collider tris≤ 360162
Exportwritten, size > 0231712 / 231712 / 231708 bytes (4.5.11 / 5.1.2 / 5.2.1)

Base triangles dropped from 3616 to 2200 in the quality pass: the old piece stacked identical boxes with a mortar gap and a teepee that met at one point. The rebuild is two seated courses plus crossing sticks; hidden overlapping volume went away. The second pass took it to 2760: each stone is now a lofted, chamfered lump instead of a four-sided wedge.

DECIMATE COLLAPSE triangle counts are not guaranteed identical across series — 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. Stone-width jitter uses fixed seed 17; each stone's shape draws from its own seeded generator. glTF byte size differs on 5.2.1.

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 disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named supports: 14 bottom stoneseach zmin ≤ 0.00114, stone_z 0.00000
Ring plan0.807 m dia × 0.176 m high ± 0.040.8023 × 0.1759
Stone variation (exit 20)per course, spread of stone outer radius, and of crown height on the top course, each ≥ 0.006 m0.01269 (smallest spread)

Joint fit and seat#

AxisDeclaredMeasured (all three)
Teepee kiss (BVH nearest at caps)≤ 0.008 m0.00034
Course seat (upper zmin − lower zmax)abs ≤ 0.004 m0.00000

Stones, wood and ash#

The ring was 28 identical curved bricks: flat tops, flat faces, one height, one grey. It read as a concrete kerb. Each stone is now a chamfered octagon section lofted along its span, swelling to a seeded belly mid-stone and pinched and capped at the ends, with a seeded radial offset and outer-face wobble. Top-course stones get a seeded domed crown. The beds stay flat, and the bottom course's crown stays at the bed height, so the upper course still seats (course seat 0.00000). The stated ring height rose from 0.156 to 0.176 m for the crowns.

Stone variation asserts the ring is not one stone repeated. --uniform-stones gives every stone the same draw — each jitter at its upper bound, so the ring keeps the envelope of its widest seeded stone and only this budget fails (exit 20, spread 0.00000).

Fire, char and soot (render and shaders only; no budget changed). The logs were a clean bark brown and the ring a pale concrete grey, so nothing in the still said the fire had ever been lit. A heat field (distance from a core just above the ash bed) now drives three things in the shared shaders: wood chars to near-black toward the flames, the hottest charred wood shows cracked embers (a Voronoi edge network, masked by a coarse noise so only some cracks are live), and stones are smoke-blackened where they face the fire. Stone base colours dropped to a warm dark fieldstone with a wider per-stone tone spread. The render adds a warm point light in the ember bed. Material slots and face counts are unchanged.

Stone is isotropic mottling, speckle and pitting with a seeded tone per stone (stone-archway's recipe, reading PieceTone); sticks and logs carry their own tone and grain along their axis; the ash has sparse ember glow. The temp .glb is removed after its size is measured.

Falsifiers#

Each violates one named budget. All seven 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
--lift-zbounding box zmin is 016
--short-stonesnamed bottom-course supports at Z=016
--float-logsteepee kiss17
--gap-coursescourse seat18
--uniform-stonesstone variation20

Run#

blender --background --python campfire.py --
blender --background --python campfire.py -- --skip-decimate
blender --background --python campfire.py -- --stray-vert
blender --background --python campfire.py -- --lift-z
blender --background --python campfire.py -- --short-stones
blender --background --python campfire.py -- --float-logs
blender --background --python campfire.py -- --gap-courses
blender --background --python campfire.py -- --uniform-stones
blender --background --python campfire.py -- --output campfire.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; 20 is file-local.

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, z-fight
16Not grounded: bounding box zmin off 0, or a named bottom stone floats
17Joint fit: teepee kiss (--float-logs)
18Seat: course gap (--gap-courses)
19Ring diameter or height off the stated real-world size
20Stone variation: the ring stones are identical (--uniform-stones)
21Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/campfire/campfire.py View the script on GitHub →
A campfire of crossed logs in a teepee, the sticks charring toward the flames and the floor logs glowing through ember cracks, inside a two-course ring of warm fieldstones.