Skip to content

feat: add vertex-color-ao example (baked occlusion in a colour attribute)#119

Merged
TMHSDigital merged 1 commit into
mainfrom
feat/vertex-color-ao
Jul 25, 2026
Merged

feat: add vertex-color-ao example (baked occlusion in a colour attribute)#119
TMHSDigital merged 1 commit into
mainfrom
feat/vertex-color-ao

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

What this adds

examples/vertex-color-ao/ — ambient occlusion baked into a colour attribute, the cheap contact shadows an engine gets for free if it reads vertex colour, witnessed on a reusable stone village well (Well.Stone, 2.5 m, 11 parts, 11 materials, 6966 verts).

The point of the example is that this bake cannot be regression-tested into meaninglessness, because the hemisphere visibility integral has closed forms. Integrating the cosine-weighted measure over the directions an infinitely wide wall of height H blocks at distance d (the φ integral collapses to π/√(1+k²)):

A(k) = ½(1 − 1/√(1+k²)),  k = H/d      AO = 1 − A(k)

AO depends only on H/d and rises strictly with d — "a concave corner darkens monotonically with depth", as an equation. The check holds the integrator to that formula, then runs the same integrator over the asset that ships.

Contract and measured values (LIVE RUN)

Check Exit Measured Gate
Integrator vs 1 − A(H/d), k = 60.0…0.60 3 6.760e-04 2.5e-03
Unoccluded flat plate 4 1.000000000 exactly 1.0
Monotone with distance 4 0.508301 → 0.928467, strict
Asset range / spread 5 [0, 1], spread 1.000000 ≥ 0.25
FLOAT_COLOR round-trip 6 0.000e+00 1e-6
BYTE_COLOR vs independent sRGB model 6 3.189e-07 (raw round-trip error 3.782e-03) 1e-6
Depsgraph survival (both attrs, 11 parts) 7 0.000e+00, counts intact exactly 0
Reuse hygiene 8 min z 0.00e+00, render attr AO

The 2.5e-03 gate is justified rather than tuned: quasi-MC convergence is ~1/√n, measured worst error at 4096 samples is 6.8e-04, so the gate is ~3.5×. Tightening below ~1e-3 would make it a sampling-noise detector.

Runtime is ~1 s: the calibration rig gets 4096 samples over six points, the asset a cheap 64.

Falsification (LIVE RUN — every exit code proven to fail)

Probe harness .scratch/vca_falsify_probe.py; each monkeypatches one thing and runs the real check().

Probe Exit Measured error
full-sphere instead of hemisphere sampling 3 AO 0.752686 vs 0.508332, err 2.444e-01
uniform solid angle instead of cosine weighting 3 err 1.202e-02
RAY_EPS = 0 so rays self-hit 4 unoccluded plate 0.160644531, not 1.0
bake forced constant 5 spread 0.000000
sRGB model replaced by naive linear 8-bit 6 4.577e-03 disagreement
Subdivision modifier resampling the attribute 7 3270 evaluated vs 840 authored
a part sunk below ground 8 -1.200e-02
default Cube datablock name 8 namespace + default-name violation

Baseline re-run after restoring: exit 0.

The exit-7 probe found a defect in my own check. The depsgraph comparison originally used zip(src.data, eva.data), which stops at the shorter sequence — a resampled attribute reported a clean 0.0 deviation and the probe exited 0. The length guard exists because the falsification pass failed to fail. This is the single most valuable thing the falsification requirement did in this run.

Version witness (LIVE RUN)

  • .scratch/blender-5.1.2-windows-x64/blender.exe → reports 5.1.2
  • .scratch/blender-4.5.11-windows-x64/blender.exe → reports 4.5.11 LTS

Byte-identical: same 6966 verts, same closed-form errors to every printed digit, same storage deviations, same framing numbers.

Hazards found while authoring

  1. BYTE_COLOR is sRGB-encoded 8-bit, not linear 8-bit. Linear 0.735 reads back 0.7379107; peak round-trip error 3.782e-03. Confirmed, not guessed — the check reproduces the readback with an independent encode → quantise → decode model to 3.189e-07.
  2. bmesh.ops.bevel offsets along cached face normals. Moving verts does not refresh them; past 90° of staleness the offset flips and grows the solid outward. Measured on a ring of bevelled boxes: 0/36/72° correct, 108° grew by exactly one offset (12 mm) in both z directions, putting geometry below ground. Repro: .scratch/bev_probe3.py. normal_update() before every bevel is the fix; the hygiene check caught it.
  3. Point-domain AO needs vertices to vary across. An 8-vertex slab bakes to one near-constant value per face; draft 3 rendered as flat pale stone with a correct bake underneath. Parts are subdivided before baking.
  4. color_attributes enumeration order is not portableBYTE_COLOR first on 4.5.11, FLOAT_COLOR first on 5.1.2 for the same mesh. Look up by name.

I also audited the just-merged socket-attach-points for hazard 2 and for a suspected BMVert-identity aliasing issue: all 18 parts are byte-identical under an isolated-primitive rebuild, so no follow-up fix is needed there.

Gates

  • Framing (enforced): fill 0.839y / 0.522x, margins L 0.241 / R 0.238 / B 0.072 / T 0.089, touch=none cross=none.
  • Asset floors (enforced): no default names; 11 materials on an 11-part hero, dominant Stone@0.27; edge90 0.152; compactness 137.8 (informational).
  • Contact sheet: docs/gallery/contact-sheets/vertex-color-ao-contact-sheet.webp. Mean luminance 63.4 (armature-bend 71.1, bmesh-gear 77.7, damped-track-aim 43.9 — mid-range, deliberately not a fifth bright card; the first framing-clean draft measured 74.8 and the key/fill/wedge were pulled back for this reason). Clip fraction 0.000%. Stage darkness ✅, wedge warmth ✅, subject fill ✅, saturation ⚠️ (grey stone plus warm timber; least saturated after socket-attach-points), thumbnail legibility ✅. Verdict: holds the lineup.
  • Asset sheet: docs/gallery/asset-sheets/vertex-color-ao.webp vs the pinned reference set. Verdict: pass — structured masonry with per-stone joint detail and visible baked occlusion even under the neutral rig; not the least-designed object.
  • Generated HTML reviewed: card alt and witnesses callout carry the full strings — 1 - 0.5(1 - 1/sqrt(1+k^2)), 6.760e-04, 3.189e-07 all survive untruncated; em dashes are U+2014.
  • Playwright in situ: 44 cards, 0 broken images, native 1280×720, counter reads "44 EXAMPLES". The new card sits beside socket-attach-points and below modular-kit-snap/lightmap-uv-channel and reads distinctly from all three (pale stone vs dark drone vs two warm scenes).

featured_rank

Not assigned. The current featured set is stronger at card size; this subject's value is in the numbers, not the silhouette.

…ute)

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>
@github-actions github-actions Bot added documentation Improvements or additions to documentation ci labels Jul 25, 2026
@TMHSDigital
TMHSDigital merged commit 9ad353e into main Jul 25, 2026
9 checks passed
@TMHSDigital
TMHSDigital deleted the feat/vertex-color-ao branch July 25, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant