Skip to content

Commit dcb9b0a

Browse files
feat: add png-exr-alpha float PNG false-unpremul witness
AI float scratch buffers routinely die on Image.save() to PNG — Blender writes RGBA16 and unpremultiplies as if associated-alpha, clamping dark straight values at low alpha to white (closed-form err 0.98). Ship the contract beside OpenEXR and byte-PNG controls so it cannot quietly drift. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 44356f4 commit dcb9b0a

13 files changed

Lines changed: 1446 additions & 13 deletions

File tree

.cursor-plugin/plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"examples/grease-pencil-rosette",
7373
"examples/image-pixels-testcard",
7474
"examples/parent-inverse-orrery",
75+
"examples/png-exr-alpha",
7576
"examples/shader-node-group",
7677
"examples/shape-key-blend",
7778
"examples/swatch-grid",

.github/workflows/blender-smoke.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,15 @@ jobs:
338338
# non-zero on failure.
339339
xvfb-run -a "$BLENDER" --background \
340340
--python examples/uv-layer-grid/uv_layer_grid.py --
341+
342+
- name: Shipped example - png vs exr alpha (float PNG false unpremul)
343+
run: |
344+
set -euo pipefail
345+
# Check only (no render): float_buffer=True Image.save() to PNG writes
346+
# RGBA16 and unpremultiplies as if associated-alpha, so straight-authored
347+
# dark values at low alpha clamp to white (closed-form err 0.98 at
348+
# RGB 0.02 / a=1/255); OpenEXR round-trips within 1e-5; byte→PNG is
349+
# straight RGBA8; EXR color_mode=RGB drops alpha to opaque. Exits
350+
# non-zero on failure.
351+
xvfb-run -a "$BLENDER" --background \
352+
--python examples/png-exr-alpha/png_exr_alpha.py --

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The content base (counts are CI-enforced against README.md and the manifest):
2727
- 2 templates: `extension-addon-template` for Extensions Platform add-ons,
2828
and `headless-batch-script-template` for unattended batch jobs.
2929
- 17 snippets covering canonical patterns.
30-
- 21 examples under `examples/<name>/`: runnable scripts that assert a real
30+
- 22 examples under `examples/<name>/`: runnable scripts that assert a real
3131
API contract with deterministic checks, exit non-zero on failure, and
3232
optionally render a still via `--output`. Each is executed headless on
3333
Blender 4.5 LTS and 5.1 by `blender-smoke.yml`; its render ships in the
@@ -42,7 +42,7 @@ Blender-Developer-Tools/
4242
rules/<rule-name>.mdc # 6 rule files
4343
templates/<template-name>/ # 2 starter templates
4444
snippets/<snippet-name>.py # 17 standalone Python snippets
45-
examples/<name>/ # 21 runnable smoke-gated examples (+ gallery.json)
45+
examples/<name>/ # 22 runnable smoke-gated examples (+ gallery.json)
4646
scripts/build_gallery.py # generates docs/gallery/ (stdlib only)
4747
scripts/site/ # vendored landing-page build (build_site.py + template)
4848
docs/gallery/ # committed generated gallery pages + hero assets

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ skills/<skill-name>/SKILL.md - AI workflow definitions, 12 total
1919
rules/<rule-name>.mdc - Anti-pattern rules, 6 total
2020
templates/<template-name>/ - Starter projects, 2 total
2121
snippets/<snippet-name>.py - Standalone code patterns, 17 total
22-
examples/<name>/ - Runnable smoke-gated examples, 21 total (+ gallery.json)
22+
examples/<name>/ - Runnable smoke-gated examples, 22 total (+ gallery.json)
2323
scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only)
2424
scripts/site/ - Vendored landing-page build (Jinja2)
2525
docs/gallery/ - Committed generated gallery pages + hero renders
@@ -80,11 +80,11 @@ v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh
8080

8181
v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`.
8282

83-
## Examples (21)
83+
## Examples (22)
8484

8585
Runnable scripts at `examples/<name>/`, each asserting a real API contract with
8686
deterministic checks (exit non-zero on failure) and optionally rendering a still via
87-
`--output`. All twenty-one run headless on Blender 4.5 LTS and 5.1 in `blender-smoke.yml`;
87+
`--output`. All twenty-two run headless on Blender 4.5 LTS and 5.1 in `blender-smoke.yml`;
8888
their renders ship in the site gallery at `docs/gallery/`. `examples/gallery.json` is the
8989
gallery's source of truth. When authoring a new one, copy the anatomy of
9090
`examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</p>
1919

2020
<p align="center">
21-
<strong>12 skills</strong> &nbsp;&bull;&nbsp; <strong>6 rules</strong> &nbsp;&bull;&nbsp; <strong>2 templates</strong> &nbsp;&bull;&nbsp; <strong>17 snippets</strong> &nbsp;&bull;&nbsp; <strong>21 examples</strong>
21+
<strong>12 skills</strong> &nbsp;&bull;&nbsp; <strong>6 rules</strong> &nbsp;&bull;&nbsp; <strong>2 templates</strong> &nbsp;&bull;&nbsp; <strong>17 snippets</strong> &nbsp;&bull;&nbsp; <strong>22 examples</strong>
2222
</p>
2323

2424
<p align="center">
@@ -36,7 +36,7 @@
3636

3737
## Overview
3838

39-
This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 21 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support.
39+
This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 22 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support.
4040

4141
The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly.
4242

@@ -85,7 +85,7 @@ or expand a category below.
8585
</p>
8686

8787
<details>
88-
<summary><strong>Materials, shading &amp; compositing</strong> — 5 examples</summary>
88+
<summary><strong>Materials, shading &amp; compositing</strong> — 6 examples</summary>
8989

9090
<table>
9191
<tr>
@@ -166,12 +166,22 @@ non-destructive path.
166166

167167
</td>
168168
</tr>
169-
</table>
169+
<tr>
170+
<td width="46%" valign="middle">
171+
<a href="examples/png-exr-alpha/"><img src="examples/png-exr-alpha/preview.webp" alt="PNG vs EXR alpha: two easel panels on a dark studio floor — left float→PNG false-unpremul clamps dark mid-tones to white at low alpha, right the EXR-clean authored straight buffer with surviving red and green primaries" /></a>
172+
</td>
173+
<td valign="middle">
170174

171-
</details>
175+
### [png-exr-alpha](examples/png-exr-alpha/)
172176

173-
<details>
174-
<summary><strong>Mesh, curves &amp; text</strong> — 7 examples</summary>
177+
`float_buffer=True` images saved to PNG are written as RGBA16 and unpremultiplied
178+
as if associated-alpha — straight-authored dark values at low alpha clamp to white
179+
(closed-form error **0.98** at `(0.02, a=1/255)`). OpenEXR preserves float RGBA;
180+
byte images stay straight 8-bit. Also witnesses `EXR color_mode='RGB'` dropping alpha.
181+
182+
</td>
183+
</tr>
184+
</table>
175185

176186
<table>
177187
<tr>

ROADMAP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
9999
- Additional snippets for asset library scripting, EXR baking, multi-file extensions
100100
- Gallery coverage follow-ups from the GPv3 review: light-linking, VSE sequences-to-strips witness (`grease-pencil-rosette`, `armature-bend`, `text-version-stamp`, and `image-pixels-testcard` shipped first)
101101
- ~~UV-layer authoring witness~~ **SHIPPED** as `examples/uv-layer-grid/``create_grid(..., calc_uvs=True)` silent no-op without a pre-existing UV layer; closed-form UV fill + explicit assignment fallback; dual-panel render (flat texel (0,0) vs neon checker)
102-
- Image save-format witness: float images saved to PNG with varying alpha suffer premultiply quantization (~0.98 worst-case round-trip error at 8-bit alpha extremes); EXR vs PNG storage contract
102+
- ~~Image save-format witness~~ **SHIPPED** as `examples/png-exr-alpha/` — float→PNG is RGBA16 and false-unpremultiplies as if associated-alpha (closed-form err 0.98 at RGB 0.02 / a=1/255); OpenEXR preserves float RGBA; byte→PNG is straight RGBA8; `EXR color_mode='RGB'` drops alpha
103103
- Attribute domain witness: writing a POINT-domain color attribute and reading it as if it were CORNER (or vice versa) silently shears colors across shared verts — companion to `color-attribute-wheel`
104+
- Light-linking collection witness: object light linking / receiver collections so a key illuminates only the hero (common AI miss when staging multi-light studios)
105+
- VSE sequences-to-strips witness: building a short cut list from `sequences.new_effect` / movie strips and asserting frame ranges survive reload
104106

105107
## Future (uncommitted)
106108

13.9 KB
Loading

docs/gallery/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ <h1>Examples Gallery</h1>
174174
</header>
175175
<div class="chips" role="toolbar" aria-label="Filter examples by topic">
176176
<button class="chip active" data-tag="" type="button">All</button>
177+
<button class="chip" data-tag="alpha" type="button">alpha</button>
177178
<button class="chip" data-tag="animation" type="button">animation</button>
178179
<button class="chip" data-tag="armature" type="button">armature</button>
179180
<button class="chip" data-tag="attributes" type="button">attributes</button>
@@ -423,6 +424,17 @@ <h2><a href="image-pixels-testcard/">image-pixels-testcard</a></h2>
423424
<a class="card-link" href="image-pixels-testcard/">View example <span aria-hidden="true">&rarr;</span></a>
424425
</div>
425426
</article>
427+
<article class="card" data-tags="images rendering alpha">
428+
<a class="card-media" href="png-exr-alpha/" aria-label="png-exr-alpha example detail page">
429+
<img src="assets/png-exr-alpha-hero.webp" alt="png-exr-alpha — Float-image PNG save trap — float_buffer=True Image.save() writes RGBA16 and unpremultiplies as if associated-alpha, clamping straight-authored dark values at…" loading="lazy" decoding="async" />
430+
</a>
431+
<div class="card-body">
432+
<h2><a href="png-exr-alpha/">png-exr-alpha</a></h2>
433+
<p class="teaches">Float-image PNG save trap — float_buffer=True Image.save() writes RGBA16 and unpremultiplies as if associated-alpha, clamping straight-authored dark values at low alpha to white (closed-form err 0.98 at RGB 0.02 / a=1/255).</p>
434+
<p class="witnesses"><span class="tag">witnesses</span> float→PNG IHDR is RGBA16 and matches q16(min(1, c/q16(a))); OpenEXR round-trips within 1e-5; byte→PNG is straight RGBA8; EXR color_mode=RGB drops alpha to opaque.</p>
435+
<a class="card-link" href="png-exr-alpha/">View example <span aria-hidden="true">&rarr;</span></a>
436+
</div>
437+
</article>
426438
<article class="card" data-tags="mesh bmesh uv materials">
427439
<a class="card-media" href="uv-layer-grid/" aria-label="uv-layer-grid example detail page">
428440
<img src="assets/uv-layer-grid-hero.webp" alt="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…" loading="lazy" decoding="async" />

0 commit comments

Comments
 (0)