Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/blender-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,8 @@ jobs:
run: |
set -euo pipefail
# Frame-independent check only (no render): 9409 verts displaced via one
# foreach_get + one foreach_set; asserts count unchanged, Z span matches the
# amplitude, and a probe vertex matches the closed-form wave. Exits non-zero
# on failure.
# foreach_get + one foreach_set; asserts the Z span matches the amplitude
# and every vertex matches the closed-form wave. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/wave-displace/wave_displace.py --

Expand Down
Binary file modified docs/gallery/assets/depsgraph-export-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 4 additions & 15 deletions docs/gallery/damped-track-aim/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ <h1>damped-track-aim</h1>
</div>
<section class="detail-section md">
<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>
<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>
<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>
<h2>Run</h2>
<pre><code># Cheap correctness check (no render) — the CI check:
blender --background --python damped_track_aim.py --
Expand Down Expand Up @@ -228,7 +228,7 @@ <h2>Source</h2>
CORE_RADIUS = <span class="n">0.32</span>
NEEDLE_DEPTH = <span class="n">1.05</span>
NEEDLE_RADIUS = <span class="n">0.055</span>
<span class="c"># Local +Z must face the core; cos(3°) ≈ 0.9986 — leave a little room for</span>
<span class="c"># Local +Z must face the core; 0.998 = cos(3.6°) — leave a little room for</span>
<span class="c"># cone tessellation / float noise while still catching a flipped axis.</span>
MIN_AIM_DOT = <span class="n">0.998</span>
LIFT = <span class="n">1.55</span>
Expand Down Expand Up @@ -481,7 +481,6 @@ <h2>Source</h2>
scene.collection.objects.link(floor)

wall = bpy.data.objects.new(<span class="s">&quot;Wall&quot;</span>, floor_me.copy())
wall.data = floor_me.copy()
wall.data.materials.clear()
wall.data.materials.append(
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>)
Expand Down Expand Up @@ -555,18 +554,8 @@ <h2>Source</h2>
scene.eevee.taa_render_samples = <span class="n">128</span>
<span class="k">except</span> AttributeError:
<span class="k">pass</span>
<span class="c"># Bloom helps the ember core read without a compositor tree.</span>
<span class="k">for</span> attr, val <span class="k">in</span> (
(<span class="s">&quot;use_bloom&quot;</span>, <span class="k">True</span>),
(<span class="s">&quot;bloom_intensity&quot;</span>, <span class="n">0.12</span>),
(<span class="s">&quot;bloom_threshold&quot;</span>, <span class="n">0.85</span>),
(<span class="s">&quot;bloom_radius&quot;</span>, <span class="n">4.5</span>),
):
<span class="k">if</span> hasattr(scene.eevee, attr):
<span class="k">try</span>:
setattr(scene.eevee, attr, val)
<span class="k">except</span> Exception:
<span class="k">pass</span>
<span class="c"># No bloom here on purpose: EEVEE has no use_bloom on 4.2+ or 5.x —</span>
<span class="c"># glow lives in the compositor (see examples/compositor-glare).</span>

scene.render.resolution_x = <span class="n">1280</span>
scene.render.resolution_y = <span class="n">720</span>
Expand Down
183 changes: 163 additions & 20 deletions docs/gallery/depsgraph-export/index.html

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion docs/gallery/driver-wave/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ <h2>Source</h2>

<span class="k">def</span> wave_scale(i):
<span class="s">&quot;&quot;&quot;The driver function: column height profile, 0.4..2.4.&quot;&quot;&quot;</span>
<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>
<span class="k">return</span> <span class="n">1.4</span> + math.sin(i * <span class="n">0.6</span>)


<span class="k">def</span> build_columns():
Expand Down Expand Up @@ -351,6 +351,11 @@ <h2>Source</h2>
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()
<span class="k">if</span> engine == <span class="s">&#x27;cycles&#x27;</span>:
scene.cycles.samples = <span class="n">32</span>
<span class="k">else</span>:
<span class="k">try</span>:
scene.eevee.taa_render_samples = <span class="n">64</span>
<span class="k">except</span> AttributeError:
<span class="k">pass</span>
scene.render.resolution_x = <span class="n">1280</span>
scene.render.resolution_y = <span class="n">720</span>
scene.render.image_settings.file_format = <span class="s">&#x27;PNG&#x27;</span>
Expand Down
18 changes: 13 additions & 5 deletions docs/gallery/gn-sdf-remesh/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ <h2>Source</h2>
<span class="k">import</span> bmesh
sc = bpy.context.scene
fme = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>); bm = bmesh.new()
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()
<span class="k">try</span>:
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)
<span class="k">finally</span>:
bm.free()
fmat = bpy.data.materials.new(<span class="s">&quot;Studio&quot;</span>); fmat.use_nodes = <span class="k">True</span>
fb = fmat.node_tree.nodes.get(<span class="s">&#x27;Principled BSDF&#x27;</span>)
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>
Expand Down Expand Up @@ -302,11 +305,16 @@ <h2>Source</h2>
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>)
args = p.parse_args(argv)

<span class="c"># EEVEE-id inversion witnessed for real: the OTHER era&#x27;s id must be</span>
<span class="c"># rejected by this build, the helper&#x27;s accepted</span>
eid = get_eevee_engine_id()
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>
bpy.context.scene.render.engine = eid
<span class="k">if</span> bpy.context.scene.render.engine != expected:
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>
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>
<span class="k">try</span>:
bpy.context.scene.render.engine = wrong
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>
<span class="k">except</span> TypeError:
<span class="k">pass</span> <span class="c"># correctly rejected</span>
bpy.context.scene.render.engine = eid <span class="c"># raises TypeError if the helper&#x27;s id is invalid</span>

obj = build()
base = len(obj.data.vertices)
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ <h2><a href="depsgraph-export/">depsgraph-export</a></h2>
<div class="card-body">
<h2><a href="wave-displace/">wave-displace</a></h2>
<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>
<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>
<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>
<a class="card-link" href="wave-displace/">View example <span aria-hidden="true">&rarr;</span></a>
</div>
</article>
Expand Down
47 changes: 33 additions & 14 deletions docs/gallery/swatch-grid/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,12 @@ <h1>swatch-grid</h1>
<section class="detail-section md">
<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>
<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>
<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>
<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>
<h2>Run</h2>
<pre><code># Default: render with the build&#x27;s EEVEE engine (needs a GPU/display)
<pre><code># Cheap correctness check (materials + engine-id witness, no render):
blender --background --python swatch_grid.py --

# Render and pixel-verify with the build&#x27;s EEVEE engine (needs a GPU/display):
blender --background --python swatch_grid.py -- --output swatch.png

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

Run headless:
By default it runs only the correctness check (no render) — the CI smoke check.
Pass --output to also render and pixel-verify a still:

blender --background --python swatch_grid.py -- # check only
blender --background --python swatch_grid.py -- --output swatch.png
blender --background --python swatch_grid.py -- --output s.png --engine cycles --samples 8 --width 640

Expand Down Expand Up @@ -304,9 +310,11 @@ <h2>Source</h2>
<span class="k">for</span> c <span class="k">in</span> range(GRID_COLS):
me = bpy.data.meshes.new(<span class="s">f&quot;</span><span class="s">S</span>{i}<span class="s">&quot;</span>)
bm = bmesh.new()
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>)
bm.to_mesh(me)
bm.free()
<span class="k">try</span>:
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>)
bm.to_mesh(me)
<span class="k">finally</span>:
bm.free()
<span class="k">for</span> poly <span class="k">in</span> me.polygons:
poly.use_smooth = <span class="k">True</span>
ob = bpy.data.objects.new(<span class="s">f&quot;</span><span class="s">S</span>{i}<span class="s">&quot;</span>, me)
Expand Down Expand Up @@ -364,7 +372,7 @@ <h2>Source</h2>
<span class="k">def</span> main():
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> []
p = argparse.ArgumentParser(description=<span class="s">&quot;Render a procedural-materials swatch grid.&quot;</span>)
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>)
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>)
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>,
help=<span class="s">&quot;auto/eevee use the version-correct EEVEE id; cycles for GPU-less hosts&quot;</span>)
p.add_argument(<span class="s">&quot;--samples&quot;</span>, type=int, default=<span class="n">32</span>)
Expand All @@ -378,15 +386,25 @@ <h2>Source</h2>
build_scene(mats)

sc = bpy.context.scene
<span class="c"># EEVEE engine-id proof: frame-independent, must hold even when we render with Cycles.</span>
<span class="c"># EEVEE engine-id proof: frame-independent, must hold even when we render with</span>
<span class="c"># Cycles. Witness the inversion for real: the OTHER era&#x27;s id must be rejected</span>
<span class="c"># by this build, and the helper&#x27;s id must be accepted.</span>
eid = get_eevee_engine_id()
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>
sc.render.engine = eid
<span class="k">if</span> sc.render.engine != expected:
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>
<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)
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>
<span class="k">try</span>:
sc.render.engine = wrong
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>
<span class="s">&quot;the engine-id inversion this example witnesses is gone&quot;</span>, file=sys.stderr)
<span class="k">return</span> <span class="n">5</span>
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>)
<span class="k">except</span> TypeError:
<span class="k">pass</span> <span class="c"># correctly rejected</span>
sc.render.engine = eid <span class="c"># the helper&#x27;s id must exist (raises TypeError if not)</span>
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>
<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>)

<span class="k">if</span> <span class="k">not</span> args.output:
print(<span class="s">&quot;swatch-grid OK&quot;</span>)
<span class="k">return</span> <span class="n">0</span>

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
sc.render.engine = render_engine
Expand Down Expand Up @@ -414,6 +432,7 @@ <h2>Source</h2>
<span class="k">if</span> <span class="k">not</span> (non_black <span class="k">and</span> regions_ok):
print(<span class="s">&quot;ERROR: render failed verification (black or wrong region count)&quot;</span>, file=sys.stderr)
<span class="k">return</span> <span class="n">3</span>
print(<span class="s">&quot;swatch-grid OK&quot;</span>)
<span class="k">return</span> <span class="n">0</span>


Expand Down
Loading
Loading