Skip to content

Commit eb03556

Browse files
committed
feat: add vertex-color-ao example (baked occlusion in a colour attribute)
Most bakes can only be regression-tested against a previous run. This one cannot drift silently, because the hemisphere visibility integral has closed forms: an unoccluded surface integrates to exactly 1, and a point a distance d from an infinitely wide wall of height H integrates to 1 - 0.5(1 - 1/sqrt(1+k^2)) with k = H/d. The check holds a deterministic cosine-weighted integrator to that formula across two decades of k, then runs the same integrator over the asset that ships, so what is in the attribute is the thing that was measured. The storage half is the trap worth shipping. BYTE_COLOR is not linear 8-bit, it is sRGB-encoded 8-bit: a linear 0.735 reads back 0.7379107. The check reproduces the readback with an independent encode/quantise/decode model rather than asserting a captured constant, so an exporter handing raw bytes to an engine expecting linear occlusion is caught by construction. Two authoring hazards are recorded with the geometry that provoked them: bmesh.ops.bevel offsets along cached face normals and flips outward once a transform leaves them more than 90 degrees stale, and point-domain AO needs subdivision or the crevice gradient never reaches the attribute at all. The depsgraph check originally compared zip(src.data, eva.data), which stops at the shorter sequence; a Subdivision modifier resampling the attribute reported a clean 0.0 deviation and the falsification probe exited 0. The length guard exists because the probe failed to fail. Signed-off-by: fOuttaMyPaint <tmhospitalitystrategies@gmail.com> Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent 4bfe83b commit eb03556

13 files changed

Lines changed: 2401 additions & 3 deletions

File tree

.cursor-plugin/plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"examples/triangulate-tangents",
9999
"examples/turntable",
100100
"examples/uv-layer-grid",
101+
"examples/vertex-color-ao",
101102
"examples/vertex-weight-limit",
102103
"examples/vse-cut-list",
103104
"examples/vse-gamma-cross",

.github/workflows/blender-smoke.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,3 +609,18 @@ jobs:
609609
# root scale down into the children. Exits non-zero on failure.
610610
xvfb-run -a "$BLENDER" --background \
611611
--python examples/socket-attach-points/socket_attach_points.py --
612+
613+
- name: Shipped example - vertex colour AO (baked occlusion attribute)
614+
run: |
615+
set -euo pipefail
616+
# Check only (no render): a deterministic cosine-weighted hemisphere
617+
# AO integrator validated against the closed form
618+
# 1 - 0.5(1 - 1/sqrt(1+k^2)) at six probe distances, an unoccluded
619+
# plate baking to exactly 1.0, strict monotonicity with distance,
620+
# then the same integrator run over an 11-part stone well: values in
621+
# [0,1] with real spread, FLOAT_COLOR exact round-trip vs BYTE_COLOR
622+
# matching an independent sRGB quantisation model, both attributes
623+
# surviving depsgraph evaluation at deviation 0 with element counts
624+
# intact, and reuse hygiene. Exits non-zero on failure.
625+
xvfb-run -a "$BLENDER" --background \
626+
--python examples/vertex-color-ao/vertex_color_ao.py --

README.md

Lines changed: 23 additions & 2 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>43 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>44 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 43 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 44 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

@@ -843,6 +843,27 @@ Companion to [`prop-origin-transform`](examples/prop-origin-transform/)
843843
[`parent-inverse-orrery`](examples/parent-inverse-orrery/) (parent-inverse
844844
under animation).
845845

846+
</td>
847+
</tr>
848+
<tr>
849+
<td width="46%" valign="middle">
850+
<a href="examples/vertex-color-ao/"><img src="examples/vertex-color-ao/preview.webp" alt="Vertex colour AO: a stone village well on a dark studio stage, its masonry joints, shaft mouth and coping undersides darkened by ambient occlusion baked into a colour attribute rather than by the lights" /></a>
851+
</td>
852+
<td valign="middle">
853+
854+
### [vertex-color-ao](examples/vertex-color-ao/)
855+
856+
Baked occlusion in a colour attribute, checked against a **formula** instead
857+
of a captured value: the cosine-weighted hemisphere integral for a wall of
858+
height `H` at distance `d` is `1 - ½(1 - 1/√(1+k²))`, `k = H/d`, and the
859+
integrator matches it to **6.760e-04** across two decades of `k`. An
860+
unoccluded plate bakes to exactly **1.0**. `FLOAT_COLOR` round-trips exactly
861+
while **`BYTE_COLOR` is sRGB-encoded 8-bit** — 0.735 reads back
862+
**0.7379107**, matching an independent encode/quantise/decode model to
863+
**3.189e-07**. Both survive depsgraph evaluation at deviation **0.0**.
864+
Companion to [`color-attribute-wheel`](examples/color-attribute-wheel/) and
865+
[`attribute-domain-shear`](examples/attribute-domain-shear/).
866+
846867
</td>
847868
</tr>
848869
</table>

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
112112
- ~~Modular kit snap witness~~ **SHIPPED** as `examples/modular-kit-snap/` — tiling corridor segment: 16 boundary verts on `x ∈ {0, TILE}` within 1e-6 m, end rings coincident under the tile offset (nearest-key match), linked-duplicate joint deviation 0.0, shell bbox == declared tile (dev 0.0), manifold except the 16 open-end edges, 20 detail parts contained inside the tile; unsnapped probe (3 mm skew, 2 mm nudge) fails at exits 3/4 with measured errors; sorted-zip ring matching mispairs displaced verts — match by nearest key (authoring hazard, fixed); byte-identical on 4.5.11 and 5.1.2
113113
- ~~Lightmap UV channel witness~~ **SHIPPED** as `examples/lightmap-uv-channel/` — two-channel UV contract on a market cart: per-part `UVMap`/`UVLight` with `active`/`active_render` re-asserted after the ops (edit-mode UV ops clear both flags; `uv_layers.new()` never moves them), UV0 drift 0.0 (clobber trap measured 2.068), every UV1 loop in [0,1], 0 overlapping islands via independent binned strict-SAT (15 hits when falsified), min island distance 0.00401 vs nominal margin 0.002 (`MARGIN_DIV × 0.01` semantics measured live); held `MeshUVLoopLayer.data` after edit-mode UV ops segfaults 4.5.11 (5/5, EXCEPTION_ACCESS_VIOLATION), silent no-op pack without prior unwrap; byte-identical on 4.5.11 and 5.1.2 (3680 islands)
114114
- ~~Socket attach-point witness~~ **SHIPPED** as `examples/socket-attach-points/` — named `SKT_` empties as the spawn contract on a survey drone: 7 socket world matrices within 1.788e-07 of the authored transform with orthonormal right-handed bases (Gram 3.576e-07, det err 2.980e-07), socket +Z == the mount pad's Newell normal (1.794e-07) by a quaternion-swing construction independent of the Gram-Schmidt basis, origin on the mount-face centroid (6.687e-08), documented up-axis fallback for the +Z/−Z mounts, module seating offset exactly 0.0 with strictly identity local matrices, rigid invariance under a re-pose (2.384e-07); `transform_apply` on an **Empty** root preserves world matrices (2.235e-07) but clears **7/7** `matrix_parent_inverse` and pushes the root scale down into every child (0.35 for an applied 1.35) — both halves asserted; a child left selected during the apply drifts sockets **2.335 m**; `bm.normal_update()` does not fix inward winding (`recalc_face_normals` does — the lofted fuselage rendered as a flat white panel); no-parent-inverse probe jumps 0.690128 m; byte-identical on 4.5.11 and 5.1.2
115+
- ~~Vertex-colour AO witness~~ **SHIPPED** as `examples/vertex-color-ao/` — baked occlusion in a colour attribute on a stone village well, checked against a closed form rather than a captured value: the cosine-weighted hemisphere integral for an infinitely wide wall of height H at distance d is `AO = 1 − ½(1 − 1/√(1+k²))`, `k = H/d`, matched to **6.760e-04** across k = 60.0…0.60 (gate 2.5e-03, QMC noise ~1/√n at 4096 samples); unoccluded plate bakes to **exactly 1.0**; strictly monotone 0.508301 → 0.928467; asset values in [0,1] with spread 1.0; **`BYTE_COLOR` is sRGB-encoded 8-bit, not linear** (0.735 → 0.7379107, peak round-trip error 3.782e-03, matching an independent encode/quantise/decode model to 3.189e-07) while `FLOAT_COLOR` is exact; both survive depsgraph evaluation at deviation 0.0; `bmesh.ops.bevel` offsets along **cached** face normals and flips outward past 90° of staleness (12 mm below ground on a ring of boxes, fixed by `normal_update()`); point-domain AO needs subdivision or the crevice gradient never reaches the attribute; `color_attributes` enumeration order differs between 4.5.11 and 5.1.2 — look up by name; byte-identical on 4.5.11 and 5.1.2 (6966 verts)
115116
- UV atlas **utilization** witness — coverage/wasted-texel closed forms for a packed lightmap atlas (the non-overlap, unit-square, margin, and active/active_render contracts shipped in `lightmap-uv-channel`; utilization is the remaining unbuilt slice of the old "UV atlas pack" candidate)
116117
- ~~GAMMA_CROSS blend-curve witness~~ **SHIPPED** as `examples/vse-gamma-cross/` — the cross blends in a gamma-0.5 space: `((1-t)·√A + t·√B)²` with `t = (frame − start)/duration`, never 1 inside the effect; mid-cross dips 0.115 below the sRGB lerp from crimson/teal (closed form (0.341, 0.349, 0.463) confirmed per frame); AgX-default sampling poisons the fit (0.146 red-channel error, `view_transform='Standard'` mandatory); deleting a consumed input orphans-and-deletes the effect — follow-up to `vse-cut-list`
117118
- Falsy `bpy_prop_collection` trap snippet: an empty collection is falsy, so `editor.strips or editor.sequences` silently falls through to the legacy accessor on an empty timeline — always branch on `hasattr`; likely generalizes across the API (found authoring `vse-cut-list`)
32.9 KB
Loading
17.8 KB
Loading
45.6 KB
Loading

docs/gallery/index.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ <h1>Examples Gallery</h1>
272272
autocomplete="off" spellcheck="false" aria-label="Search examples" />
273273
<button class="q-clear" id="qClear" type="button" aria-label="Clear search" hidden>&times;</button>
274274
</div>
275-
<span class="count" id="count" role="status" aria-live="polite">43 examples</span>
275+
<span class="count" id="count" role="status" aria-live="polite">44 examples</span>
276276
<div class="density" role="group" aria-label="Card density">
277277
<button class="density-btn" data-density="compact" type="button" aria-pressed="false">Compact</button>
278278
<button class="density-btn" data-density="detailed" type="button" aria-pressed="false">Detailed</button>
@@ -795,6 +795,17 @@ <h2><a href="socket-attach-points/">socket-attach-points</a></h2>
795795
<a class="card-link" href="socket-attach-points/">View example <span aria-hidden="true">&rarr;</span></a>
796796
</div>
797797
</article>
798+
<article class="card" data-tags="mesh attributes game-pipeline render">
799+
<a class="card-media" href="vertex-color-ao/" aria-label="vertex-color-ao example detail page">
800+
<img src="assets/vertex-color-ao-hero.webp" alt="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." loading="lazy" decoding="async" />
801+
</a>
802+
<div class="card-body">
803+
<h2><a href="vertex-color-ao/">vertex-color-ao</a></h2>
804+
<p class="teaches">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.</p>
805+
<p class="witnesses"><span class="tag">witnesses</span> Integrator matches 1 - 0.5(1 - 1/sqrt(1+k^2)) to 6.760e-04, unoccluded plate exactly 1.0, FLOAT_COLOR exact vs BYTE_COLOR sRGB-quantised (model agreement 3.189e-07), evaluated deviation 0.0.</p>
806+
<a class="card-link" href="vertex-color-ao/">View example <span aria-hidden="true">&rarr;</span></a>
807+
</div>
808+
</article>
798809
</div>
799810
<p class="noresults" id="noResults" hidden>No examples match the current filters.
800811
<button class="chip" id="resetFilters" type="button">Clear search and tags</button></p>

0 commit comments

Comments
 (0)