From 3cde3aeedcba01f3fa1257546b2d36adaf8e0dca Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Fri, 3 Jul 2026 17:02:14 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20two=20new=20smoke-gated=20examples=20?= =?UTF-8?q?=E2=80=94=20wave-displace=20and=20driver-wave?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wave-displace witnesses bulk vertex IO at real scale: a 96x96 grid (9,409 verts) displaced into a standing wave with one foreach_get and one foreach_set. The check asserts count unchanged, Z span matches the amplitude, and a probe vertex matches the closed form. driver-wave witnesses the driver evaluation contract: a driver_namespace function drives sixteen column heights through SCRIPTED drivers; driven values must appear after a view-layer update on BOTH the evaluated copy and the flushed-back original. (The first draft asserted the original stays untouched — real Blender flushes driven values back for display; the example now teaches the true model.) Both run headless on Blender 4.4/5.1 locally and are wired into the blender-smoke workflow for 4.5 LTS + 5.1. Each ships a README, a CI-artifact hero and preview render, gallery.json + manifest entries, and a README gallery row. Gallery grows to 6 examples with new drivers/mesh/performance filter chips. Co-Authored-By: Claude Fable 5 --- .cursor-plugin/plugin.json | 4 +- .github/workflows/blender-smoke.yml | 20 + README.md | 28 ++ docs/gallery/assets/driver-wave-hero.webp | Bin 0 -> 10044 bytes docs/gallery/assets/wave-displace-hero.webp | Bin 0 -> 10042 bytes docs/gallery/driver-wave/index.html | 417 ++++++++++++++++++++ docs/gallery/index.html | 25 ++ docs/gallery/wave-displace/index.html | 393 ++++++++++++++++++ examples/driver-wave/README.md | 30 ++ examples/driver-wave/driver_wave.py | 174 ++++++++ examples/driver-wave/preview.webp | Bin 0 -> 9146 bytes examples/gallery.json | 18 + examples/wave-displace/README.md | 25 ++ examples/wave-displace/preview.webp | Bin 0 -> 9018 bytes examples/wave-displace/wave_displace.py | 151 +++++++ 15 files changed, 1284 insertions(+), 1 deletion(-) create mode 100644 docs/gallery/assets/driver-wave-hero.webp create mode 100644 docs/gallery/assets/wave-displace-hero.webp create mode 100644 docs/gallery/driver-wave/index.html create mode 100644 docs/gallery/wave-displace/index.html create mode 100644 examples/driver-wave/README.md create mode 100644 examples/driver-wave/driver_wave.py create mode 100644 examples/driver-wave/preview.webp create mode 100644 examples/wave-displace/README.md create mode 100644 examples/wave-displace/preview.webp create mode 100644 examples/wave-displace/wave_displace.py diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 7736cc5..8406623 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -60,8 +60,10 @@ ], "examples": [ "examples/depsgraph-export", + "examples/driver-wave", "examples/gn-sdf-remesh", "examples/swatch-grid", - "examples/turntable" + "examples/turntable", + "examples/wave-displace" ] } diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index 532d03c..7e78cb8 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -155,3 +155,23 @@ jobs: # > base. Exits non-zero on failure. xvfb-run -a "$BLENDER" --background \ --python examples/depsgraph-export/depsgraph_export.py -- + + - name: Shipped example - wave displace (foreach bulk IO) + 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. + xvfb-run -a "$BLENDER" --background \ + --python examples/wave-displace/wave_displace.py -- + + - name: Shipped example - driver wave (driver_namespace + depsgraph) + run: | + set -euo pipefail + # Frame-independent check only (no render): sixteen SCRIPTED drivers call a + # driver_namespace function; asserts the driven values on the evaluated copy + # AND the flushed-back original both match the closed form. Exits non-zero + # on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/driver-wave/driver_wave.py -- diff --git a/README.md b/README.md index f24e155..b9b434b 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,34 @@ A depsgraph-evaluated export — builds a cube with `SUBSURF`, measures the eval `evaluated_get().to_mesh()` / `to_mesh_clear()`, and asserts `wm.obj_export` ships the modifier-applied geometry (exported vertex count == evaluated > base). + + + + +Wave displace: a dense grid displaced into smooth standing-wave dunes, rendered with EEVEE + + + +### [wave-displace](examples/wave-displace/) + +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. Asserts the count is unchanged, +the Z span matches the amplitude, and a probe vertex matches the closed-form wave. + + + + + +Driver wave: sixteen columns whose heights form a sine skyline, each driven by a driver_namespace function, rendered with EEVEE + + + +### [driver-wave](examples/driver-wave/) + +A `driver_namespace` function driving sixteen column heights through SCRIPTED drivers. +Witnesses the evaluation contract: driven values appear after a view-layer update on the +evaluated copy **and** the flushed-back original, and both must match the closed form. + diff --git a/docs/gallery/assets/driver-wave-hero.webp b/docs/gallery/assets/driver-wave-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..da69c4269dad9c98492c9d7231c45ae63424fcaf GIT binary patch literal 10044 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 JT`&OPe*m0a`-}hp literal 0 HcmV?d00001 diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html new file mode 100644 index 0000000..3bffc92 --- /dev/null +++ b/docs/gallery/driver-wave/index.html @@ -0,0 +1,417 @@ + + + + + + driver-wave — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + + +
+

driver-wave

+

A driver_namespace function driving sixteen column heights through SCRIPTED drivers — the sine skyline is entirely driver-evaluated.

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses Driven values appear after a view-layer update in two places that must agree: the evaluated copy and the original datablock the animation system flushes for display.
+
+
blender --background --python examples/driver-wave/driver_wave.py --
+ +
+
+

A runnable example that drives sixteen column heights from a custom function registered in bpy.app.driver_namespace — the pattern from drivers-and-app-handlers. Each column gets a SCRIPTED driver on Z scale whose expression calls wave_scale(i), producing a sine skyline.

+

What it witnesses: the driver evaluation contract. Driven values appear only after a view-layer update, and they land in two places that must agree: the depsgraph-evaluated copy (evaluated_get(dg).scale) and the original datablock, which the animation system flushes for display. The check asserts both against the closed-form profile.

+

Note for real add-ons: driver_namespace entries do not persist in .blend files — re-register them from a load_post handler, or every driver that calls them fails on file open. Headless, registering before driver creation (as here) is enough.

+

Run

+
# Cheap correctness check (no render) — the CI check:
+blender --background --python driver_wave.py --
+
+# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
+blender --background --python driver_wave.py -- --output driver.png
+blender --background --python driver_wave.py -- --output driver.png --engine cycles
+

It exits non-zero on failure (driven value wrong, or the flush-back disagreed). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.

+
+
+

Source

+
+ examples/driver-wave/driver_wave.py + View on GitHub → +
+
"""Driver-namespace scale drivers, evaluated through the depsgraph — a runnable example.
+
+Witnesses the drivers-and-app-handlers contract end to end: a custom function
+is registered in `bpy.app.driver_namespace`, sixteen columns get a SCRIPTED
+driver on Z scale calling it, and the check reads the driven values back
+after a view-layer update — from the evaluated copy AND from the original
+(the animation system flushes driven values back to the original datablock
+for display, so both must agree). Asserts both against the closed-form
+profile. Exits non-zero on failure.
+
+By default it runs only the correctness check (no render) — the CI smoke
+check. Pass --output to also render a still:
+
+    blender --background --python driver_wave.py --                 # check only
+    blender --background --python driver_wave.py -- --output d.png  # + render
+"""
+import bpy, bmesh, sys, os, math, argparse
+
+COUNT = 16
+SPACING = 0.72
+BASE = 0.28
+
+
+def wave_scale(i):
+    """The driver function: column height profile, 0.4..2.4."""
+    return 1.4 + math.sin(i * 0.6) if i >= 0 else 1.0
+
+
+def build_columns():
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    # driver_namespace entries do not persist in .blend files; real add-ons
+    # re-register them from a load_post handler. Headless, registering before
+    # driver creation is enough.
+    bpy.app.driver_namespace["wave_scale"] = wave_scale
+
+    me = bpy.data.meshes.new("Column")
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_cube(bm, size=1.0)
+        bm.to_mesh(me)
+    finally:
+        bm.free()
+
+    objs = []
+    x0 = -(COUNT - 1) * SPACING / 2
+    for i in range(COUNT):
+        obj = bpy.data.objects.new(f"Col.{i:02d}", me)
+        obj.location = (x0 + i * SPACING, 0.0, 0.0)
+        obj.scale = (BASE, BASE, 1.0)
+        fcu = obj.driver_add("scale", 2)
+        fcu.driver.type = 'SCRIPTED'
+        fcu.driver.expression = f"wave_scale({i})"
+        bpy.context.collection.objects.link(obj)
+        objs.append(obj)
+    return objs
+
+
+def check(objs):
+    bpy.context.view_layer.update()
+    dg = bpy.context.evaluated_depsgraph_get()
+    for i, obj in enumerate(objs):
+        expect = wave_scale(i)
+        driven = obj.evaluated_get(dg).scale[2]
+        if abs(driven - expect) > 1e-4:
+            print(f"ERROR: col {i} evaluated scale {driven:.4f} != wave_scale {expect:.4f}",
+                  file=sys.stderr)
+            return 3
+        # drivers flush back to the original datablock for display — both agree
+        if abs(obj.scale[2] - expect) > 1e-4:
+            print(f"ERROR: col {i} original scale {obj.scale[2]:.4f} not flushed "
+                  f"(expected {expect:.4f})", file=sys.stderr)
+            return 4
+    lo = min(wave_scale(i) for i in range(COUNT))
+    hi = max(wave_scale(i) for i in range(COUNT))
+    print(f"columns={COUNT} driven_range={lo:.3f}..{hi:.3f} flushed_to_original=True")
+    return 0
+
+
+def eevee_engine_id():
+    return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT'
+
+
+def render_still(objs, path, engine):
+    scene = bpy.context.scene
+    mat = bpy.data.materials.new("ColMat")
+    mat.use_nodes = True
+    bsdf = mat.node_tree.nodes["Principled BSDF"]
+    bsdf.inputs["Base Color"].default_value = (0.62, 0.68, 0.78, 1.0)
+    bsdf.inputs["Roughness"].default_value = 0.45
+    objs[0].data.materials.append(mat)  # shared mesh -> all columns
+
+    # columns stand on the floor: lift each by its DRIVEN half-height
+    dg = bpy.context.evaluated_depsgraph_get()
+    for obj in objs:
+        obj.location.z = obj.evaluated_get(dg).scale[2] / 2
+
+    floor_me = bpy.data.meshes.new("Floor")
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=30.0)
+        bm.to_mesh(floor_me)
+    finally:
+        bm.free()
+    floor = bpy.data.objects.new("Floor", floor_me)
+    fmat = bpy.data.materials.new("FloorMat")
+    fmat.use_nodes = True
+    fb = fmat.node_tree.nodes["Principled BSDF"]
+    fb.inputs["Base Color"].default_value = (0.55, 0.57, 0.62, 1.0)
+    fb.inputs["Roughness"].default_value = 0.9
+    floor_me.materials.append(fmat)
+    scene.collection.objects.link(floor)
+
+    world = bpy.data.worlds.new("World")
+    world.use_nodes = True
+    world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.045, 0.05, 0.06, 1.0)
+    scene.world = world
+
+    key = bpy.data.lights.new("Key", 'AREA'); key.energy = 1000.0; key.size = 6.0
+    key_ob = bpy.data.objects.new("Key", key)
+    key_ob.location = (-4.5, -5.5, 6.5)
+    key_ob.rotation_euler = (math.radians(46), 0.0, math.radians(-33))
+    scene.collection.objects.link(key_ob)
+    fill = bpy.data.lights.new("Fill", 'AREA'); fill.energy = 350.0; fill.size = 8.0
+    fill_ob = bpy.data.objects.new("Fill", fill)
+    fill_ob.location = (5.5, -4.0, 3.5)
+    fill_ob.rotation_euler = (math.radians(62), 0.0, math.radians(48))
+    scene.collection.objects.link(fill_ob)
+
+    cam_data = bpy.data.cameras.new("Cam"); cam_data.lens = 45.0
+    cam = bpy.data.objects.new("Cam", cam_data)
+    cam.location = (0.0, -13.0, 2.4)
+    cam.rotation_euler = (math.radians(83), 0.0, 0.0)
+    scene.collection.objects.link(cam)
+    scene.camera = cam
+
+    scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
+    if engine == 'cycles':
+        scene.cycles.samples = 32
+    scene.render.resolution_x = 1280
+    scene.render.resolution_y = 720
+    scene.render.image_settings.file_format = 'PNG'
+    scene.render.filepath = path
+    bpy.ops.render.render(write_still=True)
+    return os.path.exists(path) and os.path.getsize(path) > 0
+
+
+def main():
+    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
+    p = argparse.ArgumentParser()
+    p.add_argument("--output", default=None, help="optional: render a still PNG here")
+    p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
+                   help="render engine for --output (cycles for GPU-less hosts)")
+    args = p.parse_args(argv)
+
+    objs = build_columns()
+    code = check(objs)
+    if code:
+        return code
+
+    if args.output:
+        if not render_still(objs, os.path.abspath(args.output), args.engine):
+            print("ERROR: render produced no file", file=sys.stderr)
+            return 6
+        print(f"rendered still {args.output}")
+
+    print("driver-wave OK")
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+
+
+ +
+
+ generated from examples/gallery.json + CC-BY-NC-ND-4.0 + exit 0 +
+
+ + + diff --git a/docs/gallery/index.html b/docs/gallery/index.html index 888d16c..5d4b39e 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -176,9 +176,12 @@

Examples Gallery

+ + +
@@ -227,6 +230,28 @@

depsgraph-export

View example +
+ + wave-displace — 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 + +
+

wave-displace

+

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.

+

witnesses The bulk path is correct, not just fast: vertex count unchanged, Z span matches the wave amplitude, probe vertex matches the closed form exactly.

+ View example +
+
+
+ + driver-wave — A driver_namespace function driving sixteen column heights through SCRIPTED drivers — the sine skyline is entirely driver-evaluated + +
+

driver-wave

+

A driver_namespace function driving sixteen column heights through SCRIPTED drivers — the sine skyline is entirely driver-evaluated.

+

witnesses Driven values appear after a view-layer update in two places that must agree: the evaluated copy and the original datablock the animation system flushes for display.

+ View example +
+