Skip to content

Commit b8b0a42

Browse files
TMHSDigitalclaude
andauthored
fix: harden example checks and close review findings across eight examples (#56)
A full review pass over the sixteen examples found the oldest four predate the bmesh-gear anatomy and carried real debt; this closes everything actionable: - depsgraph-export: replace the shipped scratch docstring ("Candidate B ... (SCRATCH)") with the standard anatomy, and add the missing --output render path (base cube beside its evaluated form, resting the subsurf ball on the floor via its own evaluated bounds) so the gallery assets now come from the shipped script; OBJ path moves to --obj. - swatch-grid: add the check-only default mode CI conventions promise (--output was required), and make the EEVEE engine-id assertion real. - swatch-grid / turntable / gn-sdf-remesh: the old engine-id check compared a value to itself and could never fire; it now witnesses the inversion by asserting the WRONG era's id is rejected (TypeError) and the helper's id accepted - live-verified on 4.5.11 and 5.1.2. - wave-displace: the vertex-count guard was unreachable (foreach_set cannot resize); replaced with a strictly stronger every-vertex closed-form check that a stride/interleave bug cannot hide from. - damped-track-aim: delete the inert legacy EEVEE bloom block (use_bloom exists on neither supported version and contradicted compositor-glare), fix the orphaned wall mesh copy, and correct the cos(3.6deg) comment. - driver-wave: remove the unreachable else branch in the driver function. - Backport the always-free-bmesh try/finally and the taa_render_samples shim to the early examples that lacked them; sync READMEs, gallery.json, the smoke workflow comment, and regenerate docs/gallery. All six touched checks pass check-only on Blender 4.5.11 LTS and 5.1.2; swatch-grid's CI render invocation and the new depsgraph-export render were run and visually inspected on both. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5811020 commit b8b0a42

23 files changed

Lines changed: 510 additions & 159 deletions

File tree

.github/workflows/blender-smoke.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,8 @@ jobs:
160160
run: |
161161
set -euo pipefail
162162
# Frame-independent check only (no render): 9409 verts displaced via one
163-
# foreach_get + one foreach_set; asserts count unchanged, Z span matches the
164-
# amplitude, and a probe vertex matches the closed-form wave. Exits non-zero
165-
# on failure.
163+
# foreach_get + one foreach_set; asserts the Z span matches the amplitude
164+
# and every vertex matches the closed-form wave. Exits non-zero on failure.
166165
xvfb-run -a "$BLENDER" --background \
167166
--python examples/wave-displace/wave_displace.py --
168167
290 Bytes
Loading

docs/gallery/damped-track-aim/index.html

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ <h1>damped-track-aim</h1>
184184
</div>
185185
<section class="detail-section md">
186186
<p>A runnable example that aims twelve brass spikes at an ember core with <code>Object.constraints.new(&#x27;DAMPED_TRACK&#x27;)</code> — the data-API path, not <code>bpy.ops.object.constraint_add</code> (which needs an active object and fails in headless loops). Damped Track is the twist-stable aim constraint: it points one local axis at a target without the roll fights Track To is known for.</p>
187-
<p><strong>What it witnesses:</strong> every spike carries exactly one unmuted <code>DAMPED_TRACK</code> bound to the core on <code>TRACK_Z</code>. After a depsgraph update, each evaluated local <code>+Z</code> aligns with the world vector toward the core (dot ≥ 0.998 ≈ 3°). A missing constraint, a muted one, a <code>TRACK_TO</code> stand-in, or a flipped axis fails the check.</p>
187+
<p><strong>What it witnesses:</strong> every spike carries exactly one unmuted <code>DAMPED_TRACK</code> bound to the core on <code>TRACK_Z</code>. After a depsgraph update, each evaluated local <code>+Z</code> aligns with the world vector toward the core (dot ≥ 0.998 ≈ 3.6°). A missing constraint, a muted one, a <code>TRACK_TO</code> stand-in, or a flipped axis fails the check.</p>
188188
<h2>Run</h2>
189189
<pre><code># Cheap correctness check (no render) — the CI check:
190190
blender --background --python damped_track_aim.py --
@@ -228,7 +228,7 @@ <h2>Source</h2>
228228
CORE_RADIUS = <span class="n">0.32</span>
229229
NEEDLE_DEPTH = <span class="n">1.05</span>
230230
NEEDLE_RADIUS = <span class="n">0.055</span>
231-
<span class="c"># Local +Z must face the core; cos(3°) ≈ 0.9986 — leave a little room for</span>
231+
<span class="c"># Local +Z must face the core; 0.998 = cos(3.6°) — leave a little room for</span>
232232
<span class="c"># cone tessellation / float noise while still catching a flipped axis.</span>
233233
MIN_AIM_DOT = <span class="n">0.998</span>
234234
LIFT = <span class="n">1.55</span>
@@ -481,7 +481,6 @@ <h2>Source</h2>
481481
scene.collection.objects.link(floor)
482482

483483
wall = bpy.data.objects.new(<span class="s">&quot;Wall&quot;</span>, floor_me.copy())
484-
wall.data = floor_me.copy()
485484
wall.data.materials.clear()
486485
wall.data.materials.append(
487486
make_dielectric(<span class="s">&quot;StudioWall&quot;</span>, (<span class="n">0.012</span>, <span class="n">0.013</span>, <span class="n">0.016</span>), roughness=<span class="n">0.55</span>)
@@ -555,18 +554,8 @@ <h2>Source</h2>
555554
scene.eevee.taa_render_samples = <span class="n">128</span>
556555
<span class="k">except</span> AttributeError:
557556
<span class="k">pass</span>
558-
<span class="c"># Bloom helps the ember core read without a compositor tree.</span>
559-
<span class="k">for</span> attr, val <span class="k">in</span> (
560-
(<span class="s">&quot;use_bloom&quot;</span>, <span class="k">True</span>),
561-
(<span class="s">&quot;bloom_intensity&quot;</span>, <span class="n">0.12</span>),
562-
(<span class="s">&quot;bloom_threshold&quot;</span>, <span class="n">0.85</span>),
563-
(<span class="s">&quot;bloom_radius&quot;</span>, <span class="n">4.5</span>),
564-
):
565-
<span class="k">if</span> hasattr(scene.eevee, attr):
566-
<span class="k">try</span>:
567-
setattr(scene.eevee, attr, val)
568-
<span class="k">except</span> Exception:
569-
<span class="k">pass</span>
557+
<span class="c"># No bloom here on purpose: EEVEE has no use_bloom on 4.2+ or 5.x —</span>
558+
<span class="c"># glow lives in the compositor (see examples/compositor-glare).</span>
570559

571560
scene.render.resolution_x = <span class="n">1280</span>
572561
scene.render.resolution_y = <span class="n">720</span>

docs/gallery/depsgraph-export/index.html

Lines changed: 163 additions & 20 deletions
Large diffs are not rendered by default.

docs/gallery/driver-wave/index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ <h2>Source</h2>
226226

227227
<span class="k">def</span> wave_scale(i):
228228
<span class="s">&quot;&quot;&quot;The driver function: column height profile, 0.4..2.4.&quot;&quot;&quot;</span>
229-
<span class="k">return</span> <span class="n">1.4</span> + math.sin(i * <span class="n">0.6</span>) <span class="k">if</span> i &gt;= <span class="n">0</span> <span class="k">else</span> <span class="n">1.0</span>
229+
<span class="k">return</span> <span class="n">1.4</span> + math.sin(i * <span class="n">0.6</span>)
230230

231231

232232
<span class="k">def</span> build_columns():
@@ -351,6 +351,11 @@ <h2>Source</h2>
351351
scene.render.engine = <span class="s">&#x27;CYCLES&#x27;</span> <span class="k">if</span> engine == <span class="s">&#x27;cycles&#x27;</span> <span class="k">else</span> eevee_engine_id()
352352
<span class="k">if</span> engine == <span class="s">&#x27;cycles&#x27;</span>:
353353
scene.cycles.samples = <span class="n">32</span>
354+
<span class="k">else</span>:
355+
<span class="k">try</span>:
356+
scene.eevee.taa_render_samples = <span class="n">64</span>
357+
<span class="k">except</span> AttributeError:
358+
<span class="k">pass</span>
354359
scene.render.resolution_x = <span class="n">1280</span>
355360
scene.render.resolution_y = <span class="n">720</span>
356361
scene.render.image_settings.file_format = <span class="s">&#x27;PNG&#x27;</span>

docs/gallery/gn-sdf-remesh/index.html

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,10 @@ <h2>Source</h2>
261261
<span class="k">import</span> bmesh
262262
sc = bpy.context.scene
263263
fme = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>); bm = bmesh.new()
264-
bmesh.ops.create_grid(bm, x_segments=<span class="n">1</span>, y_segments=<span class="n">1</span>, size=<span class="n">30.0</span>); bm.to_mesh(fme); bm.free()
264+
<span class="k">try</span>:
265+
bmesh.ops.create_grid(bm, x_segments=<span class="n">1</span>, y_segments=<span class="n">1</span>, size=<span class="n">30.0</span>); bm.to_mesh(fme)
266+
<span class="k">finally</span>:
267+
bm.free()
265268
fmat = bpy.data.materials.new(<span class="s">&quot;Studio&quot;</span>); fmat.use_nodes = <span class="k">True</span>
266269
fb = fmat.node_tree.nodes.get(<span class="s">&#x27;Principled BSDF&#x27;</span>)
267270
fb.inputs[<span class="s">&#x27;Base Color&#x27;</span>].default_value = (<span class="n">0.055</span>, <span class="n">0.06</span>, <span class="n">0.07</span>, <span class="n">1</span>) <span class="c"># dark graphite studio</span>
@@ -302,11 +305,16 @@ <h2>Source</h2>
302305
p.add_argument(<span class="s">&quot;--engine&quot;</span>, choices=[<span class="s">&quot;auto&quot;</span>, <span class="s">&quot;cycles&quot;</span>], default=<span class="s">&quot;auto&quot;</span>)
303306
args = p.parse_args(argv)
304307

308+
<span class="c"># EEVEE-id inversion witnessed for real: the OTHER era&#x27;s id must be</span>
309+
<span class="c"># rejected by this build, the helper&#x27;s accepted</span>
305310
eid = get_eevee_engine_id()
306-
expected = <span class="s">&#x27;BLENDER_EEVEE&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span>
307-
bpy.context.scene.render.engine = eid
308-
<span class="k">if</span> bpy.context.scene.render.engine != expected:
309-
print(<span class="s">f&quot;</span><span class="s">ERROR: EEVEE id </span>{eid}<span class="s"> != expected </span>{expected}<span class="s">&quot;</span>, file=sys.stderr); <span class="k">return</span> <span class="n">5</span>
311+
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span>
312+
<span class="k">try</span>:
313+
bpy.context.scene.render.engine = wrong
314+
print(<span class="s">f&quot;</span><span class="s">ERROR: wrong-era EEVEE id &#x27;</span>{wrong}<span class="s">&#x27; was accepted</span><span class="s">&quot;</span>, file=sys.stderr); <span class="k">return</span> <span class="n">5</span>
315+
<span class="k">except</span> TypeError:
316+
<span class="k">pass</span> <span class="c"># correctly rejected</span>
317+
bpy.context.scene.render.engine = eid <span class="c"># raises TypeError if the helper&#x27;s id is invalid</span>
310318

311319
obj = build()
312320
base = len(obj.data.vertices)

docs/gallery/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ <h2><a href="depsgraph-export/">depsgraph-export</a></h2>
250250
<div class="card-body">
251251
<h2><a href="wave-displace/">wave-displace</a></h2>
252252
<p class="teaches">Bulk vertex IO at real scale — 9,409 vertices displaced into a standing wave with one foreach_get and one foreach_set, no per-vertex access.</p>
253-
<p class="witnesses"><span class="tag">witnesses</span> The bulk path is correct, not just fast: vertex count unchanged, Z span matches the wave amplitude, probe vertex matches the closed form exactly.</p>
253+
<p class="witnesses"><span class="tag">witnesses</span> The bulk path is correct, not just fast: the Z span matches the wave amplitude and every vertex matches the closed-form wave, so a stride bug in the flat buffer cannot hide.</p>
254254
<a class="card-link" href="wave-displace/">View example <span aria-hidden="true">&rarr;</span></a>
255255
</div>
256256
</article>

docs/gallery/swatch-grid/index.html

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,12 @@ <h1>swatch-grid</h1>
185185
<section class="detail-section md">
186186
<p>A runnable example that renders a 3×2 grid of spheres — one material per cell — to a single PNG. It demonstrates the <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/skills/procedural-materials-and-shaders/SKILL.md"><code>procedural-materials-and-shaders</code></a> patterns end to end:</p>
187187
<ul><li><strong>Principled BSDF</strong> metals (gold, copper: high metallic, low roughness) and dielectrics (red/blue plastic, white rough), configured with <strong>string socket lookups</strong> and <strong>4-tuple colors</strong>.</li><li>The <strong>emission</strong> pattern (an emissive orange swatch).</li><li>The cross-version <strong><code>set_specular</code> shim</strong> (<code>Specular</code><code>Specular IOR Level</code>, renamed in Blender 4.0).</li></ul>
188-
<p>It doubles as a live proof of the <strong>EEVEE engine-id</strong> behavior: the version-branch helper resolves <code>BLENDER_EEVEE</code> on Blender 5.x and <code>BLENDER_EEVEE_NEXT</code> on 4.2–4.5, and the chosen id is asserted against the running build before rendering — so a regression in that mapping fails the example, not just the docs.</p>
188+
<p>It doubles as a live proof of the <strong>EEVEE engine-id</strong> behavior: the version-branch helper resolves <code>BLENDER_EEVEE</code> on Blender 5.x and <code>BLENDER_EEVEE_NEXT</code> on 4.2–4.5, and the check witnesses the inversion for real — the *other* era&#x27;s id must be <strong>rejected</strong> by the running build (assignment raises <code>TypeError</code>) and the helper&#x27;s id accepted — so a regression in that mapping fails the example, not just the docs.</p>
189189
<h2>Run</h2>
190-
<pre><code># Default: render with the build&#x27;s EEVEE engine (needs a GPU/display)
190+
<pre><code># Cheap correctness check (materials + engine-id witness, no render):
191+
blender --background --python swatch_grid.py --
192+
193+
# Render and pixel-verify with the build&#x27;s EEVEE engine (needs a GPU/display):
191194
blender --background --python swatch_grid.py -- --output swatch.png
192195

193196
# GPU-less / CI hosts: render the pixels with Cycles (CPU). The EEVEE id is still
@@ -212,7 +215,10 @@ <h2>Source</h2>
212215
the version-branch helper resolves `BLENDER_EEVEE` on Blender 5.x and `BLENDER_EEVEE_NEXT`
213216
on 4.2-4.5, and the chosen id is asserted against the build before rendering.
214217

215-
Run headless:
218+
By default it runs only the correctness check (no render) — the CI smoke check.
219+
Pass --output to also render and pixel-verify a still:
220+
221+
blender --background --python swatch_grid.py -- # check only
216222
blender --background --python swatch_grid.py -- --output swatch.png
217223
blender --background --python swatch_grid.py -- --output s.png --engine cycles --samples 8 --width 640
218224

@@ -304,9 +310,11 @@ <h2>Source</h2>
304310
<span class="k">for</span> c <span class="k">in</span> range(GRID_COLS):
305311
me = bpy.data.meshes.new(<span class="s">f&quot;</span><span class="s">S</span>{i}<span class="s">&quot;</span>)
306312
bm = bmesh.new()
307-
bmesh.ops.create_uvsphere(bm, u_segments=<span class="n">48</span>, v_segments=<span class="n">24</span>, radius=<span class="n">0.92</span>)
308-
bm.to_mesh(me)
309-
bm.free()
313+
<span class="k">try</span>:
314+
bmesh.ops.create_uvsphere(bm, u_segments=<span class="n">48</span>, v_segments=<span class="n">24</span>, radius=<span class="n">0.92</span>)
315+
bm.to_mesh(me)
316+
<span class="k">finally</span>:
317+
bm.free()
310318
<span class="k">for</span> poly <span class="k">in</span> me.polygons:
311319
poly.use_smooth = <span class="k">True</span>
312320
ob = bpy.data.objects.new(<span class="s">f&quot;</span><span class="s">S</span>{i}<span class="s">&quot;</span>, me)
@@ -364,7 +372,7 @@ <h2>Source</h2>
364372
<span class="k">def</span> main():
365373
argv = sys.argv[sys.argv.index(<span class="s">&quot;--&quot;</span>) + <span class="n">1</span>:] <span class="k">if</span> <span class="s">&quot;--&quot;</span> <span class="k">in</span> sys.argv <span class="k">else</span> []
366374
p = argparse.ArgumentParser(description=<span class="s">&quot;Render a procedural-materials swatch grid.&quot;</span>)
367-
p.add_argument(<span class="s">&quot;--output&quot;</span>, required=<span class="k">True</span>, help=<span class="s">&quot;Output PNG path&quot;</span>)
375+
p.add_argument(<span class="s">&quot;--output&quot;</span>, default=<span class="k">None</span>, help=<span class="s">&quot;optional: render a still PNG here&quot;</span>)
368376
p.add_argument(<span class="s">&quot;--engine&quot;</span>, choices=[<span class="s">&quot;auto&quot;</span>, <span class="s">&quot;eevee&quot;</span>, <span class="s">&quot;cycles&quot;</span>], default=<span class="s">&quot;auto&quot;</span>,
369377
help=<span class="s">&quot;auto/eevee use the version-correct EEVEE id; cycles for GPU-less hosts&quot;</span>)
370378
p.add_argument(<span class="s">&quot;--samples&quot;</span>, type=int, default=<span class="n">32</span>)
@@ -378,15 +386,25 @@ <h2>Source</h2>
378386
build_scene(mats)
379387

380388
sc = bpy.context.scene
381-
<span class="c"># EEVEE engine-id proof: frame-independent, must hold even when we render with Cycles.</span>
389+
<span class="c"># EEVEE engine-id proof: frame-independent, must hold even when we render with</span>
390+
<span class="c"># Cycles. Witness the inversion for real: the OTHER era&#x27;s id must be rejected</span>
391+
<span class="c"># by this build, and the helper&#x27;s id must be accepted.</span>
382392
eid = get_eevee_engine_id()
383-
expected = <span class="s">&#x27;BLENDER_EEVEE&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span>
384-
sc.render.engine = eid
385-
<span class="k">if</span> sc.render.engine != expected:
386-
print(<span class="s">f&quot;</span><span class="s">ERROR: EEVEE id helper returned &#x27;</span>{eid}<span class="s">&#x27;, engine is &#x27;</span>{sc.render.engine}<span class="s">&#x27;, </span><span class="s">&quot;</span>
387-
<span class="s">f&quot;</span><span class="s">expected &#x27;</span>{expected}<span class="s">&#x27;</span><span class="s">&quot;</span>, file=sys.stderr)
393+
wrong = <span class="s">&#x27;BLENDER_EEVEE_NEXT&#x27;</span> <span class="k">if</span> bpy.app.version &gt;= (<span class="n">5</span>, <span class="n">0</span>, <span class="n">0</span>) <span class="k">else</span> <span class="s">&#x27;BLENDER_EEVEE&#x27;</span>
394+
<span class="k">try</span>:
395+
sc.render.engine = wrong
396+
print(<span class="s">f&quot;</span><span class="s">ERROR: wrong-era EEVEE id &#x27;</span>{wrong}<span class="s">&#x27; was accepted by this build — </span><span class="s">&quot;</span>
397+
<span class="s">&quot;the engine-id inversion this example witnesses is gone&quot;</span>, file=sys.stderr)
388398
<span class="k">return</span> <span class="n">5</span>
389-
print(<span class="s">f&quot;</span><span class="s">eevee_engine_id=</span>{eid}<span class="s"> (expected </span>{expected}<span class="s">) OK; set_specular resolved &#x27;</span>{specular_socket}<span class="s">&#x27;</span><span class="s">&quot;</span>)
399+
<span class="k">except</span> TypeError:
400+
<span class="k">pass</span> <span class="c"># correctly rejected</span>
401+
sc.render.engine = eid <span class="c"># the helper&#x27;s id must exist (raises TypeError if not)</span>
402+
print(<span class="s">f&quot;</span><span class="s">eevee_engine_id=</span>{eid}<span class="s"> accepted, &#x27;</span>{wrong}<span class="s">&#x27; rejected OK; </span><span class="s">&quot;</span>
403+
<span class="s">f&quot;</span><span class="s">set_specular resolved &#x27;</span>{specular_socket}<span class="s">&#x27;</span><span class="s">&quot;</span>)
404+
405+
<span class="k">if</span> <span class="k">not</span> args.output:
406+
print(<span class="s">&quot;swatch-grid OK&quot;</span>)
407+
<span class="k">return</span> <span class="n">0</span>
390408

391409
render_engine = <span class="s">&#x27;CYCLES&#x27;</span> <span class="k">if</span> args.engine == <span class="s">&#x27;cycles&#x27;</span> <span class="k">else</span> eid
392410
sc.render.engine = render_engine
@@ -414,6 +432,7 @@ <h2>Source</h2>
414432
<span class="k">if</span> <span class="k">not</span> (non_black <span class="k">and</span> regions_ok):
415433
print(<span class="s">&quot;ERROR: render failed verification (black or wrong region count)&quot;</span>, file=sys.stderr)
416434
<span class="k">return</span> <span class="n">3</span>
435+
print(<span class="s">&quot;swatch-grid OK&quot;</span>)
417436
<span class="k">return</span> <span class="n">0</span>
418437

419438

0 commit comments

Comments
 (0)