Skip to content

Commit 2bd2041

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): gate compositor-glare and collision-hull-proxy stills; lint every gallery entry
These were the only 2 of 132 gallery entries whose render path never called gallery_framing, so their framing was eyeballed, not measured. collision-hull-proxy is also in the pinned asset-quality reference set yet never ran the asset-quality floors. - compositor-glare: check_framing (hero and elements = the rings, stage = floor) before the beauty render; exit 10. render_still now returns a code. - collision-hull-proxy: check_framing (hero = hydrant, elements = hydrant + hulls, stage = floor + wall; exit 10) and check_asset_quality on the hydrant (exit 11), both before the render. - READMEs list the new exits. - tests/check_render_gates.py (in Validate): every directory in examples/gallery.json or showcase/gallery.json must import gallery_framing and call check_framing or measure_framing_deviation. 132/132 pass. Proof (local, .scratch/blender-5.2.1-windows-x64, 5.2.1 LTS, EEVEE): - compositor-glare --output: framing_fill x=0.734 ok, margins min 0.117 ok, exit 0. Probe: camera pulled back to y=-30 -> "framing violation ... fill x=0.219 FAIL". - collision-hull-proxy --output: framing_fill y=0.833 ok, margins ok; aq_naming ok, materials n=4 dominant 0.50 ok, edge90 0.035 ok; exit 0. - Both gates passed as composed, so the committed heroes are unchanged. - check_render_gates on the pre-fix compositor_glare.py reports it. Closes #342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 275e23d commit 2bd2041

8 files changed

Lines changed: 230 additions & 28 deletions

File tree

‎.github/workflows/validate.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ jobs:
7070
- name: Check EEVEE engine-id mappings
7171
run: python3 tests/check_engine_id.py
7272

73+
- name: Every gallery still is gated by gallery_framing
74+
run: python3 tests/check_render_gates.py
75+
7376
- name: Check falsifiers declare the budget they target
7477
run: python3 tests/check_falsifier_targets.py
7578

‎docs/gallery/collision-hull-proxy/index.html‎

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,16 @@ <h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">
8181
blender --background --python collision_hull_proxy.py -- --output hydrant.png --engine cycles</code></pre>
8282
<h2 id="exit-codes">Exit codes<a class="anchor" href="#exit-codes" aria-label="Link to this section">#</a></h2>
8383
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it.</p>
84-
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Render vertex escapes its hull (<code>--shrink-hull</code> lands here)</td></tr><tr><td>4</td><td>Hull edge does not border exactly two faces</td></tr><tr><td>5</td><td>Signed volume ≤ 0 (inverted winding)</td></tr><tr><td>6</td><td>Hull vertex off its own face plane</td></tr><tr><td>7</td><td>Euler characteristic ≠ 2</td></tr><tr><td>8</td><td>Piece over the 255-face collision budget</td></tr><tr><td>9</td><td><code>--output</code> produced no file</td></tr></tbody></table></div>
84+
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Render vertex escapes its hull (<code>--shrink-hull</code> lands here)</td></tr><tr><td>4</td><td>Hull edge does not border exactly two faces</td></tr><tr><td>5</td><td>Signed volume ≤ 0 (inverted winding)</td></tr><tr><td>6</td><td>Hull vertex off its own face plane</td></tr><tr><td>7</td><td>Euler characteristic ≠ 2</td></tr><tr><td>8</td><td>Piece over the 255-face collision budget</td></tr><tr><td>9</td><td><code>--output</code> produced no file</td></tr><tr><td>10</td><td>Gallery framing violation (render path)</td></tr><tr><td>11</td><td>Gallery asset-quality violation (render path)</td></tr></tbody></table></div>
8585
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code> or <code>--shrink-hull</code>.</p>
8686
</section>
8787
<section class="detail-section src" id="source">
8888
<h2>Source</h2>
8989
<div class="src-meta">
9090
<code>examples/collision-hull-proxy/collision_hull_proxy.py</code>
91-
<span>524 lines &middot; <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/collision-hull-proxy/collision_hull_proxy.py">View on GitHub &rarr;</a></span>
91+
<span>549 lines &middot; <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/collision-hull-proxy/collision_hull_proxy.py">View on GitHub &rarr;</a></span>
9292
</div>
93-
<div class="code" data-lines="524">
93+
<div class="code" data-lines="549">
9494
<pre class="gutter" aria-hidden="true">1
9595
2
9696
3
@@ -614,7 +614,32 @@ <h2>Source</h2>
614614
521
615615
522
616616
523
617-
524</pre><pre class="code-lines"><span class="s">&quot;&quot;&quot;A fire hydrant prop plus its convex collision proxies — a runnable example.
617+
524
618+
525
619+
526
620+
527
621+
528
622+
529
623+
530
624+
531
625+
532
626+
533
627+
534
628+
535
629+
536
630+
537
631+
538
632+
539
633+
540
634+
541
635+
542
636+
543
637+
544
638+
545
639+
546
640+
547
641+
548
642+
549</pre><pre class="code-lines"><span class="s">&quot;&quot;&quot;A fire hydrant prop plus its convex collision proxies — a runnable example.
618643

619644
Witnesses the collision contract every game prop pipeline (engines generally,
620645
FiveM/GTA-style prop workflows specifically) depends on: a prop&#x27;s collision is
@@ -654,6 +679,13 @@ <h2>Source</h2>
654679
<span class="k">import</span> bpy, bmesh, sys, os, math, argparse
655680
<span class="k">from</span> mathutils <span class="k">import</span> Vector
656681

682+
<span class="c"># Shared render gates (render path only) — see gallery_framing.py and</span>
683+
<span class="c"># gallery_asset_quality.py</span>
684+
sys.path.insert(<span class="n">0</span>, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
685+
sys.dont_write_bytecode = <span class="k">True</span> <span class="c"># keep examples/__pycache__ out of the repo tree</span>
686+
<span class="k">import</span> gallery_framing
687+
<span class="k">import</span> gallery_asset_quality
688+
657689
SEGMENTS = <span class="n">48</span> <span class="c"># lathe resolution of the render mesh</span>
658690
CAGE_SEG_BODY = <span class="n">8</span> <span class="c"># collision cage resolutions — the hull face budget</span>
659691
CAGE_SEG_CAP = <span class="n">8</span> <span class="c"># is spent through these, not through SEGMENTS</span>
@@ -1096,8 +1128,26 @@ <h2>Source</h2>
10961128
scene.render.filepath = path
10971129
<span class="c"># AgX would flatten the enamel toward pastel (docs/VISUAL-STYLE.md)</span>
10981130
scene.view_settings.view_transform = <span class="s">&#x27;Standard&#x27;</span>
1131+
<span class="c"># Layer 1 framing gate and the asset-quality floors, both before the</span>
1132+
<span class="c"># beauty render so a defective still ships no artifact. The hero is the</span>
1133+
<span class="c"># hydrant itself; its hulls are framed elements too.</span>
1134+
hydrant = [o <span class="k">for</span> g <span class="k">in</span> groups <span class="k">for</span> o <span class="k">in</span> g[<span class="s">&quot;render&quot;</span>]]
1135+
hulls = [hull <span class="k">for</span> _, hull, _ <span class="k">in</span> pieces]
1136+
fcode = gallery_framing.check_framing(
1137+
scene, cam, hero=hydrant, elements=hydrant + hulls, stage=[floor, wall],
1138+
)
1139+
<span class="k">if</span> fcode:
1140+
<span class="k">return</span> fcode
1141+
acode = gallery_asset_quality.check_asset_quality(
1142+
scene, cam, hydrant, stage=[floor, wall],
1143+
)
1144+
<span class="k">if</span> acode:
1145+
<span class="k">return</span> acode
10991146
bpy.ops.render.render(write_still=<span class="k">True</span>)
1100-
<span class="k">return</span> os.path.exists(path) <span class="k">and</span> os.path.getsize(path) &gt; <span class="n">0</span>
1147+
<span class="k">if</span> <span class="k">not</span> (os.path.exists(path) <span class="k">and</span> os.path.getsize(path) &gt; <span class="n">0</span>):
1148+
print(<span class="s">&quot;ERROR: render produced no file&quot;</span>, file=sys.stderr)
1149+
<span class="k">return</span> <span class="n">9</span>
1150+
<span class="k">return</span> <span class="n">0</span>
11011151

11021152

11031153
<span class="k">def</span> main():
@@ -1124,9 +1174,9 @@ <h2>Source</h2>
11241174
<span class="k">return</span> code
11251175

11261176
<span class="k">if</span> args.output:
1127-
<span class="k">if</span> <span class="k">not</span> render_still(groups, pieces, os.path.abspath(args.output), args.engine):
1128-
print(<span class="s">&quot;ERROR: render produced no file&quot;</span>, file=sys.stderr)
1129-
<span class="k">return</span> <span class="n">9</span>
1177+
rcode = render_still(groups, pieces, os.path.abspath(args.output), args.engine)
1178+
<span class="k">if</span> rcode:
1179+
<span class="k">return</span> rcode
11301180
print(<span class="s">f&quot;</span><span class="s">rendered still </span>{args.output}<span class="s">&quot;</span>)
11311181

11321182
print(<span class="s">&quot;collision-hull-proxy OK&quot;</span>)

‎docs/gallery/compositor-glare/index.html‎

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,16 @@ <h2 id="run">Run<a class="anchor" href="#run" aria-label="Link to this section">
7676
blender --background --python compositor_glare.py -- --output rings.png --engine cycles</code></pre>
7777
<h2 id="exit-codes">Exit codes<a class="anchor" href="#exit-codes" aria-label="Link to this section">#</a></h2>
7878
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it.</p>
79-
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Compositor tree / Glare / Threshold / link-chain contract (<code>--threshold-high</code> lands here)</td></tr><tr><td>4</td><td>Check render missing, tube dark, no halo, or halo does not fall off</td></tr><tr><td>5</td><td>Halo present with compositing off</td></tr><tr><td>6</td><td><code>--output</code> produced no file</td></tr></tbody></table></div>
79+
<div class="table-wrap"><table><thead><tr><th scope="col">Code</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Compositor tree / Glare / Threshold / link-chain contract (<code>--threshold-high</code> lands here)</td></tr><tr><td>4</td><td>Check render missing, tube dark, no halo, or halo does not fall off</td></tr><tr><td>5</td><td>Halo present with compositing off</td></tr><tr><td>6</td><td><code>--output</code> produced no file</td></tr><tr><td>10</td><td>Gallery framing violation (render path; checked before the beauty render)</td></tr></tbody></table></div>
8080
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code> or <code>--threshold-high</code>.</p>
8181
</section>
8282
<section class="detail-section src" id="source">
8383
<h2>Source</h2>
8484
<div class="src-meta">
8585
<code>examples/compositor-glare/compositor_glare.py</code>
86-
<span>379 lines &middot; <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/compositor-glare/compositor_glare.py">View on GitHub &rarr;</a></span>
86+
<span>398 lines &middot; <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/compositor-glare/compositor_glare.py">View on GitHub &rarr;</a></span>
8787
</div>
88-
<div class="code" data-lines="379">
88+
<div class="code" data-lines="398">
8989
<pre class="gutter" aria-hidden="true">1
9090
2
9191
3
@@ -464,7 +464,26 @@ <h2>Source</h2>
464464
376
465465
377
466466
378
467-
379</pre><pre class="code-lines"><span class="s">&quot;&quot;&quot;Bloom through the compositor across the 4.x -&gt; 5.x boundary — a runnable example.
467+
379
468+
380
469+
381
470+
382
471+
383
472+
384
473+
385
474+
386
475+
387
476+
388
477+
389
478+
390
479+
391
480+
392
481+
393
482+
394
483+
395
484+
396
485+
397
486+
398</pre><pre class="code-lines"><span class="s">&quot;&quot;&quot;Bloom through the compositor across the 4.x -&gt; 5.x boundary — a runnable example.
468487

469488
Witnesses the compositor-plumbing contract that AI-generated Blender code breaks
470489
constantly:
@@ -504,6 +523,11 @@ <h2>Source</h2>
504523
<span class="k">from</span> bpy_extras.object_utils <span class="k">import</span> world_to_camera_view
505524
<span class="k">from</span> mathutils <span class="k">import</span> Vector
506525

526+
<span class="c"># Shared Layer 1 framing measurement (render path only) — see gallery_framing.py</span>
527+
sys.path.insert(<span class="n">0</span>, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
528+
sys.dont_write_bytecode = <span class="k">True</span> <span class="c"># keep examples/__pycache__ out of the repo tree</span>
529+
<span class="k">import</span> gallery_framing
530+
507531
RING_MAJOR = <span class="n">0.7</span>
508532
RING_MINOR = <span class="n">0.12</span>
509533
RING_Z = <span class="n">0.85</span> <span class="c"># ring center height; bottom edge grazes the floor</span>
@@ -745,7 +769,11 @@ <h2>Source</h2>
745769

746770
<span class="k">def</span> render_still(scene, path, engine, samples, width):
747771
<span class="s">&quot;&quot;&quot;Dark-studio beauty pass: the same rings, now grounded on a reflective
748-
floor — the glow and its reflection are the compositor&#x27;s doing.&quot;&quot;&quot;</span>
772+
floor — the glow and its reflection are the compositor&#x27;s doing.
773+
774+
Returns 0, 10 (gallery framing violation, checked before the beauty
775+
render) or 6 (no file written).&quot;&quot;&quot;</span>
776+
rings = [o <span class="k">for</span> o <span class="k">in</span> scene.objects <span class="k">if</span> o.type == <span class="s">&#x27;MESH&#x27;</span>]
749777
<span class="c"># glossy dark floor stays below the bloom threshold; only the rings bloom</span>
750778
mesh = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
751779
half = <span class="n">30.0</span>
@@ -796,8 +824,18 @@ <h2>Source</h2>
796824
scene.render.image_settings.file_format = <span class="s">&#x27;WEBP&#x27;</span> <span class="k">if</span> path.lower().endswith(<span class="s">&quot;.webp&quot;</span>) <span class="k">else</span> <span class="s">&#x27;PNG&#x27;</span>
797825
scene.render.use_compositing = <span class="k">True</span>
798826
scene.render.filepath = path
827+
<span class="c"># Layer 1 framing gate (silhouette matte) — exit 10 on violation, before</span>
828+
<span class="c"># the beauty render so a defective composition ships no artifact.</span>
829+
fcode = gallery_framing.check_framing(
830+
scene, cam, hero=rings, elements=rings, stage=[floor],
831+
)
832+
<span class="k">if</span> fcode:
833+
<span class="k">return</span> fcode
799834
bpy.ops.render.render(write_still=<span class="k">True</span>)
800-
<span class="k">return</span> os.path.exists(path) <span class="k">and</span> os.path.getsize(path) &gt; <span class="n">0</span>
835+
<span class="k">if</span> <span class="k">not</span> (os.path.exists(path) <span class="k">and</span> os.path.getsize(path) &gt; <span class="n">0</span>):
836+
print(<span class="s">&quot;ERROR: render produced no file&quot;</span>, file=sys.stderr)
837+
<span class="k">return</span> <span class="n">6</span>
838+
<span class="k">return</span> <span class="n">0</span>
801839

802840

803841
<span class="k">def</span> main():
@@ -828,10 +866,10 @@ <h2>Source</h2>
828866
<span class="k">return</span> code
829867

830868
<span class="k">if</span> args.output:
831-
<span class="k">if</span> <span class="k">not</span> render_still(scene, os.path.abspath(args.output), args.engine, args.samples,
832-
args.width):
833-
print(<span class="s">&quot;ERROR: render produced no file&quot;</span>, file=sys.stderr)
834-
<span class="k">return</span> <span class="n">6</span>
869+
rcode = render_still(scene, os.path.abspath(args.output), args.engine, args.samples,
870+
args.width)
871+
<span class="k">if</span> rcode:
872+
<span class="k">return</span> rcode
835873
print(<span class="s">f&quot;</span><span class="s">rendered still </span>{args.output}<span class="s">&quot;</span>)
836874

837875
print(<span class="s">&quot;compositor-glare OK&quot;</span>)

‎examples/collision-hull-proxy/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ against it.
8686
| 7 | Euler characteristic ≠ 2 |
8787
| 8 | Piece over the 255-face collision budget |
8888
| 9 | `--output` produced no file |
89+
| 10 | Gallery framing violation (render path) |
90+
| 11 | Gallery asset-quality violation (render path) |
8991

9092
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
9193
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).

‎examples/collision-hull-proxy/collision_hull_proxy.py‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
import bpy, bmesh, sys, os, math, argparse
3939
from mathutils import Vector
4040

41+
# Shared render gates (render path only) — see gallery_framing.py and
42+
# gallery_asset_quality.py
43+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
44+
sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
45+
import gallery_framing
46+
import gallery_asset_quality
47+
4148
SEGMENTS = 48 # lathe resolution of the render mesh
4249
CAGE_SEG_BODY = 8 # collision cage resolutions — the hull face budget
4350
CAGE_SEG_CAP = 8 # is spent through these, not through SEGMENTS
@@ -480,8 +487,26 @@ def light(name, loc, energy, size, col, rot):
480487
scene.render.filepath = path
481488
# AgX would flatten the enamel toward pastel (docs/VISUAL-STYLE.md)
482489
scene.view_settings.view_transform = 'Standard'
490+
# Layer 1 framing gate and the asset-quality floors, both before the
491+
# beauty render so a defective still ships no artifact. The hero is the
492+
# hydrant itself; its hulls are framed elements too.
493+
hydrant = [o for g in groups for o in g["render"]]
494+
hulls = [hull for _, hull, _ in pieces]
495+
fcode = gallery_framing.check_framing(
496+
scene, cam, hero=hydrant, elements=hydrant + hulls, stage=[floor, wall],
497+
)
498+
if fcode:
499+
return fcode
500+
acode = gallery_asset_quality.check_asset_quality(
501+
scene, cam, hydrant, stage=[floor, wall],
502+
)
503+
if acode:
504+
return acode
483505
bpy.ops.render.render(write_still=True)
484-
return os.path.exists(path) and os.path.getsize(path) > 0
506+
if not (os.path.exists(path) and os.path.getsize(path) > 0):
507+
print("ERROR: render produced no file", file=sys.stderr)
508+
return 9
509+
return 0
485510

486511

487512
def main():
@@ -508,9 +533,9 @@ def main():
508533
return code
509534

510535
if args.output:
511-
if not render_still(groups, pieces, os.path.abspath(args.output), args.engine):
512-
print("ERROR: render produced no file", file=sys.stderr)
513-
return 9
536+
rcode = render_still(groups, pieces, os.path.abspath(args.output), args.engine)
537+
if rcode:
538+
return rcode
514539
print(f"rendered still {args.output}")
515540

516541
print("collision-hull-proxy OK")

‎examples/compositor-glare/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ against it.
5151
| 4 | Check render missing, tube dark, no halo, or halo does not fall off |
5252
| 5 | Halo present with compositing off |
5353
| 6 | `--output` produced no file |
54+
| 10 | Gallery framing violation (render path; checked before the beauty render) |
5455

5556
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
5657
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).

0 commit comments

Comments
 (0)