UV Layer Grid
The UV-layer authoring hazard — bmesh.ops.create_grid(..., calc_uvs=True) is a silent no-op unless a UV layer already exists; without one an Image Texture samples texel (0,0) everywhere.
examples/color-attribute-wheel/
The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain, filled by expanding per-vertex HSV across face corners with foreach_get/foreach_set, then wired into a shader Attribute node.
Rendered headless by the example itself. Select it to enlarge.
tags mesh materials attributes
runs on 4.5 LTS · 5.1 · 5.2 LTS
blender --background --python examples/color-attribute-wheel/color_attribute_wheel.py --
falsify it Breaks the contract on purpose and must exit 6
blender --background --python examples/color-attribute-wheel/color_attribute_wheel.py -- --point-domain
A runnable example that builds an HSV color wheel disc entirely with bmesh and colors it with Mesh.color_attributes.new() — the modern attributes API, not the deprecated Mesh.vertex_colors alias AI code keeps reaching for. It witnesses the domain trap that comes with it: a CORNER-domain attribute is sized to len(mesh.loops), not len(mesh.vertices), so per-vertex data has to be expanded across face corners before it is written. The material wires the same attribute into a Shader Attribute node (attribute_type='GEOMETRY') feeding Base Color — the step AI code most often skips, leaving the mesh gray even when the attribute data is correct.
What it witnesses: a FLOAT_COLOR attribute on the CORNER domain, filled with one foreach_get (loop → vertex index) and one foreach_set (loop color), never a per-loop Python assignment. The check asserts the attribute is sized to the loop count and not the vertex count, that it is color_attributes.active_color (so a renderer or exporter actually picks it up), and that several probe loops match the closed-form HSV value for the vertex they reference. A separate check in the render path confirms the Attribute node is actually linked to Base Color, not just present in the node tree.
The checked disc is flat and zero-thickness. The render path — only after check(), so the verified mesh is untouched until then — domes it into a shallow glazed plate (one foreach_get/foreach_set over vertex positions; the CORNER colours ride the same loops, so the colours on the plate are the colours the check verified), gives it a Solidify body, rings it with a turned brass bezel over a dark backing disc, and stands it at a 14° lean on a walnut studio easel. The glaze is a satin base under a thin clear coat; the dome curves the coat's reflection into a soft sheen instead of the hard horizon line a flat glaze draws. The path gates framing through examples/gallery_framing.py (exit 10): fill 0.739 y, every margin ≥ 0.050.
# Cheap correctness check (no render) — the CI check:
blender --background --python color_attribute_wheel.py --
# Falsifier: POINT-domain attribute. Must exit non-zero.
blender --background --python color_attribute_wheel.py -- --point-domain
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python color_attribute_wheel.py -- --output wheel.png
blender --background --python color_attribute_wheel.py -- --output wheel.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ closed form |
| 4 | Color attribute missing |
| 5 | Domain/type ≠ CORNER/FLOAT_COLOR (diagnostic, checked last) |
| 6 | Attribute sized to verts, not loops (--point-domain lands here) |
| 7 | active_color not set |
| 8 | Probe loop color off HSV closed form |
| 9 | --output produced no file |
| 10 | Framing gate violation on the --output path (gallery_framing) |
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. Its catalog falsifier is --point-domain (expects exit 6).