From fea5c80b2b99a3d981799046c9a19dd586c61d5d Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:03:40 -0400 Subject: [PATCH 1/3] feat: add GLB-in engine-ready AI asset pipeline template Headless entry point composing cleanup, LOD, collider, and engine-preset export. Helpers are duplicated from the named snippets because templates are not a package. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- .cursor-plugin/plugin.json | 1 + .../ai-asset-pipeline-template/README.md | 110 +++++ .../ai-asset-pipeline-template/pipeline.py | 396 ++++++++++++++++++ 3 files changed, 507 insertions(+) create mode 100644 templates/ai-asset-pipeline-template/README.md create mode 100644 templates/ai-asset-pipeline-template/pipeline.py diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index d01e9b9..582f1e7 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -68,6 +68,7 @@ "snippets/version-branch-skeleton.py" ], "templates": [ + "templates/ai-asset-pipeline-template", "templates/extension-addon-template", "templates/headless-batch-script-template" ], diff --git a/templates/ai-asset-pipeline-template/README.md b/templates/ai-asset-pipeline-template/README.md new file mode 100644 index 0000000..ac3da2c --- /dev/null +++ b/templates/ai-asset-pipeline-template/README.md @@ -0,0 +1,110 @@ +# AI Asset Pipeline Template + +A working starter for a headless Blender job that takes a GLB in and +writes an engine-ready LOD set plus optional collider. Provider-agnostic: +the input is a file path, not a generation vendor. + +## Usage + +```powershell +blender --background --python pipeline.py -- ` + --input .\source.glb ` + --outdir .\out ` + --preset unity ` + --lod-budgets 1024,256,64 ` + --collider convex +``` + +Linux / macOS: + +```bash +blender --background --python pipeline.py -- \ + --input ./source.glb \ + --outdir ./out \ + --preset unity \ + --lod-budgets 1024,256,64 \ + --collider convex +``` + +The `--` separator is required. Everything before it is consumed by +Blender (`--background`, `--python`). Everything after it is forwarded +to `pipeline.py` as `sys.argv`. + +Optional `--draco` enables glTF Draco compression on export. + +`--preset` is one of `unity` (Y-up glTF), `godot` (Z-up glTF), `unreal` +(centimeter glTF: 100x bake then `export_yup=True`). FBX for Unreal lives +in `snippets/export_preset_unreal.py`; this template emits GLB so a CI +job can check the `glTF` magic bytes the same way for every preset. + +## What it does + +1. Parses script-side args after `--`. +2. Imports the GLB into an empty scene. +3. Checks scene units (metric meters), applies object rotation/scale, + sits the origin on the lowest Z, recalculates face normals, and + prints the evaluated triangle count. +4. Builds an LOD chain from `--lod-budgets`. +5. Optionally builds a convex hull or AABB box collider. +6. Exports each LOD (and the collider) under the chosen engine preset. +7. Returns explicit exit codes so a CI pipeline can detect failures. + +Cleanup order follows `ai-mesh-cleanup`. LOD, collider, and export +helpers are duplicated from the snippets named in `pipeline.py`'s +header; templates are not a package. + +## Exit codes + +Same convention as `templates/headless-batch-script-template/` +(`script.py` / its README: 0 success, 2+ distinct failure modes; +argparse usage errors also exit 2). Not a repo-wide table; examples such +as `export-preset-axis` number their own checks independently. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 2 | Input file missing, or argparse rejected the flags (including an unsupported `--preset`) | +| 3 | Input is not a readable GLB (bad magic or import failure) | +| 4 | `--lod-budgets` missing, non-positive, or not strictly decreasing | +| 5 | Import produced no mesh | +| 6 | `outdir` is not a directory, or glTF export failed / wrote no file | + +## Expected environment + +- Blender on the system `PATH` (or invoked by absolute path). +- `--outdir` already exists. The script does not create it. +- `--input` is a GLB with at least one mesh. With no meshes the script + returns exit code 5. + +## Common gotchas + +- **Forgetting the `--`**. Blender treats the following args as its + own and complains. The script never sees them. +- **Output path with spaces on Windows**. Quote the whole path: + `--outdir ".\out folder"`. +- **Running without `--background`**. The script still works, but + Blender opens a UI window and stays open after the script finishes. + Use `--background` for unattended runs. +- **Unreal mutates selected meshes** (100x scale bake). Each export + selects one object. Do not re-export the same object as Unity afterward + without restoring scale. +- **Operators that need a 3D Viewport context**. Some operators only + work when a `VIEW_3D` area exists. In headless mode, none does. + Either rewrite using `bpy.data.*`, or fabricate a window+area via + `temp_override` (advanced; see the `headless-batch-scripting` skill). + +## Extending the template + +This template covers one pipeline (import, clean, LOD, collider, export). +For more complex workflows, factor each step into its own function and +return early with distinct exit codes. The `main()` function is the +orchestration point; everything else should be pure helpers. + +## See also + +- Skill `ai-mesh-cleanup` for the cleanup order. +- Skill `engine-export-presets` for Unity / Godot / Unreal axis and units. +- Skill `headless-batch-scripting` for the full pattern catalog. +- Rule `prefer-temp-override-over-context-copy` for why we avoid + `bpy.context.copy()`. +- Snippet `lod_chain.py`, `convex_hull_collider.py`, `export_preset_unity.py`. diff --git a/templates/ai-asset-pipeline-template/pipeline.py b/templates/ai-asset-pipeline-template/pipeline.py new file mode 100644 index 0000000..de8d498 --- /dev/null +++ b/templates/ai-asset-pipeline-template/pipeline.py @@ -0,0 +1,396 @@ +# AI asset pipeline template. +# +# Run with: +# blender --background --python pipeline.py -- \ +# --input /path/to/source.glb \ +# --outdir /path/to/out \ +# --preset unity \ +# --lod-budgets 1024,256,64 \ +# --collider convex \ +# --draco +# +# Everything after the `--` token is forwarded to this script as sys.argv. +# Anything before `--` is consumed by Blender itself. +# +# GLB in, engine-ready LOD set plus optional collider out. Provider-agnostic. +# Templates are standalone (not a package). Helpers below are duplicated from: +# snippets/decimate_to_budget.py (evaluated_triangle_count, decimate_to_budget) +# snippets/lod_chain.py (make_lod_chain; itself duplicates the above) +# snippets/convex_hull_collider.py +# snippets/export_preset_unity.py / export_preset_godot.py / export_preset_unreal.py +# Cleanup order follows skills/ai-mesh-cleanup/SKILL.md. +# Exit codes follow templates/headless-batch-script-template/script.py: +# 0 success, 2+ distinct failure modes, argparse usage also exits 2. +# +# References: +# docs.blender.org/manual/en/latest/advanced/command_line/arguments.html +# docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf +# docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.fbx + +import argparse +import os +import sys + +import bmesh +import bpy + + +def parse_args(argv): + """Parse args after the `--` separator that Blender passes through.""" + if "--" in argv: + script_args = argv[argv.index("--") + 1:] + else: + script_args = [] + + parser = argparse.ArgumentParser( + description="Import a GLB, clean it, emit LODs and a collider, export.", + ) + parser.add_argument( + "--input", + required=True, + help="Path to the source .glb file.", + ) + parser.add_argument( + "--outdir", + required=True, + help="Directory to write LOD and collider files into.", + ) + parser.add_argument( + "--preset", + required=True, + choices=["unity", "godot", "unreal"], + help="Engine export preset.", + ) + parser.add_argument( + "--lod-budgets", + default="1024,256,64", + help="Comma-separated decreasing triangle budgets (default: 1024,256,64).", + ) + parser.add_argument( + "--collider", + choices=["convex", "box", "none"], + default="convex", + help="Collider to emit (default: convex).", + ) + parser.add_argument( + "--draco", + action="store_true", + help="Enable glTF Draco mesh compression on export.", + ) + return parser.parse_args(script_args) + + +def parse_budgets(text): + parts = [p.strip() for p in text.split(",") if p.strip()] + if not parts: + return None + try: + budgets = [int(p) for p in parts] + except ValueError: + return None + if any(b <= 0 for b in budgets): + return None + if any(budgets[i] <= budgets[i + 1] for i in range(len(budgets) - 1)): + return None + return budgets + + +def scene_units_are_meters(scene): + units = scene.unit_settings + if units.system not in {"METRIC", "NONE"}: + return False + return abs(units.scale_length - 1.0) < 1e-6 + + +def scale_is_identity(obj, tol=1e-6): + sx, sy, sz = obj.scale + return abs(sx - 1.0) < tol and abs(sy - 1.0) < tol and abs(sz - 1.0) < tol + + +def apply_object_transform(obj): + with bpy.context.temp_override( + object=obj, active_object=obj, selected_objects=[obj] + ): + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + +def origin_to_base(obj): + mesh = obj.data + n = len(mesh.vertices) + if n == 0: + return + flat = [0.0] * (n * 3) + mesh.vertices.foreach_get("co", flat) + min_z = min(flat[2::3]) + for i in range(n): + flat[i * 3 + 2] -= min_z + mesh.vertices.foreach_set("co", flat) + mesh.update() + obj.location.z += min_z + + +def recalc_normals(obj): + bm = bmesh.new() + try: + bm.from_mesh(obj.data) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + bm.to_mesh(obj.data) + obj.data.update() + finally: + bm.free() + + +def evaluated_triangle_count(obj): + # Duplicated from snippets/decimate_to_budget.py + depsgraph = bpy.context.evaluated_depsgraph_get() + eval_obj = obj.evaluated_get(depsgraph) + eval_mesh = eval_obj.to_mesh() + try: + eval_mesh.calc_loop_triangles() + return len(eval_mesh.loop_triangles) + finally: + eval_obj.to_mesh_clear() + + +def decimate_to_budget(obj, target_tris): + # Duplicated from snippets/decimate_to_budget.py + current = evaluated_triangle_count(obj) + if current == 0 or current <= target_tris: + return None + ratio = min(1.0, target_tris / current) + mod = obj.modifiers.new("DecimateBudget", "DECIMATE") + mod.decimate_type = "COLLAPSE" + mod.ratio = ratio + return mod + + +def make_lod_chain(obj, budgets): + # Duplicated from snippets/lod_chain.py + lods = [] + for i, budget in enumerate(budgets): + mesh = obj.data.copy() + lod = bpy.data.objects.new(f"{obj.name}_LOD{i}", mesh) + lod.matrix_world = obj.matrix_world.copy() + bpy.context.scene.collection.objects.link(lod) + decimate_to_budget(lod, budget) + lods.append(lod) + return lods + + +def convex_hull_collider(obj, name=None): + # Duplicated from snippets/convex_hull_collider.py + mesh = bpy.data.meshes.new(name or f"{obj.name}_Collider") + bm = bmesh.new() + try: + bm.from_mesh(obj.data) + result = bmesh.ops.convex_hull(bm, input=bm.verts) + interior = result.get("geom_interior") or [] + unused = result.get("geom_unused") or [] + if interior: + bmesh.ops.delete(bm, geom=interior, context="VERTS") + if unused: + bmesh.ops.delete(bm, geom=unused, context="VERTS") + bm.to_mesh(mesh) + mesh.update() + finally: + bm.free() + collider = bpy.data.objects.new(name or f"{obj.name}_Collider", mesh) + bpy.context.scene.collection.objects.link(collider) + collider.matrix_world = obj.matrix_world.copy() + return collider + + +def box_collider(obj, name=None): + mesh_in = obj.data + n = len(mesh_in.vertices) + if n == 0: + return None + flat = [0.0] * (n * 3) + mesh_in.vertices.foreach_get("co", flat) + xs, ys, zs = flat[0::3], flat[1::3], flat[2::3] + min_x, max_x = min(xs), max(xs) + min_y, max_y = min(ys), max(ys) + min_z, max_z = min(zs), max(zs) + size = (max_x - min_x, max_y - min_y, max_z - min_z) + center = ( + 0.5 * (min_x + max_x), + 0.5 * (min_y + max_y), + 0.5 * (min_z + max_z), + ) + mesh = bpy.data.meshes.new(name or f"{obj.name}_BoxCollider") + bm = bmesh.new() + try: + geom = bmesh.ops.create_cube(bm, size=1.0) + for vert in geom["verts"]: + vert.co.x = vert.co.x * size[0] + center[0] + vert.co.y = vert.co.y * size[1] + center[1] + vert.co.z = vert.co.z * size[2] + center[2] + bm.to_mesh(mesh) + mesh.update() + finally: + bm.free() + collider = bpy.data.objects.new(name or f"{obj.name}_BoxCollider", mesh) + bpy.context.collection.objects.link(collider) + collider.matrix_world = obj.matrix_world.copy() + return collider + + +def apply_selected_mesh_transforms(): + # Duplicated from snippets/export_preset_unity.py + for obj in list(bpy.context.selected_objects): + if obj.type != "MESH": + continue + apply_object_transform(obj) + + +def select_only(obj): + for other in bpy.data.objects: + other.select_set(False) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + +def export_preset(filepath, preset, draco): + apply_selected_mesh_transforms() + if preset == "unity": + bpy.ops.export_scene.gltf( + filepath=filepath, + use_selection=True, + export_yup=True, + export_apply=True, + export_draco_mesh_compression_enable=draco, + export_animations=False, + ) + return + if preset == "godot": + bpy.ops.export_scene.gltf( + filepath=filepath, + use_selection=True, + export_yup=False, + export_apply=True, + export_draco_mesh_compression_enable=draco, + export_animations=False, + ) + return + for obj in list(bpy.context.selected_objects): + if obj.type != "MESH": + continue + obj.scale = ( + obj.scale[0] * 100.0, + obj.scale[1] * 100.0, + obj.scale[2] * 100.0, + ) + apply_selected_mesh_transforms() + bpy.ops.export_scene.gltf( + filepath=filepath, + use_selection=True, + export_yup=True, + export_apply=True, + export_draco_mesh_compression_enable=draco, + export_animations=False, + ) + + +def glb_magic_ok(path): + try: + with open(path, "rb") as handle: + return handle.read(4) == b"glTF" + except OSError: + return False + + +def main(): + args = parse_args(sys.argv) + + budgets = parse_budgets(args.lod_budgets) + if budgets is None: + print( + "ERROR: --lod-budgets must be comma-separated positive ints, strictly decreasing", + file=sys.stderr, + ) + return 4 + + if not os.path.isfile(args.input): + print(f"ERROR: input file missing: {args.input}", file=sys.stderr) + return 2 + + if not glb_magic_ok(args.input): + print(f"ERROR: input is not a readable GLB: {args.input}", file=sys.stderr) + return 3 + + if not os.path.isdir(args.outdir): + print(f"ERROR: outdir is not a directory: {args.outdir}", file=sys.stderr) + return 6 + + bpy.ops.wm.read_factory_settings(use_empty=True) + try: + bpy.ops.import_scene.gltf(filepath=args.input.replace("\\", "/")) + except RuntimeError as exc: + print(f"ERROR: glTF import failed: {exc}", file=sys.stderr) + return 3 + + meshes = [obj for obj in bpy.data.objects if obj.type == "MESH"] + if not meshes: + print("ERROR: no mesh objects in the input GLB", file=sys.stderr) + return 5 + + print(f"Found {len(meshes)} mesh object(s): {[o.name for o in meshes]}") + + scene = bpy.context.scene + if not scene_units_are_meters(scene): + scene.unit_settings.system = "METRIC" + scene.unit_settings.scale_length = 1.0 + print("Set scene units to metric meters") + + hero = max(meshes, key=evaluated_triangle_count) + if not scale_is_identity(hero): + apply_object_transform(hero) + print(f"Applied object scale/rotation on {hero.name}") + origin_to_base(hero) + recalc_normals(hero) + src_tris = evaluated_triangle_count(hero) + print(f"evaluated_tris={src_tris} object={hero.name}") + + lods = make_lod_chain(hero, budgets) + for lod, budget in zip(lods, budgets): + print( + f"{lod.name} budget={budget} evaluated_tris={evaluated_triangle_count(lod)}" + ) + + collider = None + if args.collider == "convex": + collider = convex_hull_collider(hero) + print(f"collider={collider.name} verts={len(collider.data.vertices)}") + elif args.collider == "box": + collider = box_collider(hero) + print(f"collider={collider.name} verts={len(collider.data.vertices)}") + + written = [] + try: + for i, lod in enumerate(lods): + select_only(lod) + path = os.path.join(args.outdir, f"lod{i}.glb").replace("\\", "/") + export_preset(path, args.preset, args.draco) + written.append(path) + print(f"Wrote {path}") + if collider is not None: + select_only(collider) + path = os.path.join(args.outdir, "collider.glb").replace("\\", "/") + export_preset(path, args.preset, args.draco) + written.append(path) + print(f"Wrote {path}") + except RuntimeError as exc: + print(f"ERROR: glTF export failed: {exc}", file=sys.stderr) + return 6 + + for path in written: + if not (os.path.isfile(path) and os.path.getsize(path) > 0): + print(f"ERROR: export produced no file: {path}", file=sys.stderr) + return 6 + + return 0 + + +if __name__ == "__main__": + exit_code = main() + sys.exit(exit_code) From 4f64babe19339e8d39ad2eebaef68908277f407a Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:03:40 -0400 Subject: [PATCH 2/3] ci: smoke the AI asset pipeline template on 4.5 and 5.2 Same blender-smoke.yml pattern as the headless glTF template: generate a fixture, assert exit 0 and glTF magic, then assert missing-input exit 2. Does not add 5.1 to the PR matrix. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- .github/workflows/blender-smoke.yml | 33 +++++++++++++++++++++++++++++ tests/smoke/make_pipeline_glb.py | 27 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/smoke/make_pipeline_glb.py diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index e67acee..aed52e3 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -122,6 +122,39 @@ jobs: [ "$code" -eq 2 ] || { echo "::error::expected exit 2 for no-mesh input, got $code"; exit 1; } echo "no-mesh exit code = $code (correct)" + - name: Build pipeline template fixture GLB + run: | + set -euo pipefail + xvfb-run -a "$BLENDER" --background --python tests/smoke/make_pipeline_glb.py -- \ + "$RUNNER_TEMP/out/pipeline_src.glb" + test -s "$RUNNER_TEMP/out/pipeline_src.glb" || { echo "::error::pipeline fixture missing"; exit 1; } + + - name: Headless AI asset pipeline template runs (exit 0, LODs produced) + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/out/pipeline" + xvfb-run -a "$BLENDER" --background --python templates/ai-asset-pipeline-template/pipeline.py -- \ + --input "$RUNNER_TEMP/out/pipeline_src.glb" \ + --outdir "$RUNNER_TEMP/out/pipeline" \ + --preset unity \ + --lod-budgets 1024,256,64 \ + --collider convex + test -s "$RUNNER_TEMP/out/pipeline/lod0.glb" || { echo "::error::lod0 missing/empty"; exit 1; } + test -s "$RUNNER_TEMP/out/pipeline/collider.glb" || { echo "::error::collider missing/empty"; exit 1; } + head -c4 "$RUNNER_TEMP/out/pipeline/lod0.glb" | grep -q "glTF" || { echo "::error::lod0 not a glTF binary"; exit 1; } + + - name: Headless pipeline template missing input returns exit 2 + run: | + set +e + xvfb-run -a "$BLENDER" --background --python templates/ai-asset-pipeline-template/pipeline.py -- \ + --input "$RUNNER_TEMP/out/does-not-exist.glb" \ + --outdir "$RUNNER_TEMP/out/pipeline" \ + --preset unity + code=$? + set -e + [ "$code" -eq 2 ] || { echo "::error::expected exit 2 for missing input, got $code"; exit 1; } + echo "missing-input exit code = $code (correct)" + - name: Headless render template runs (exit 0, PNG produced) run: | set -euo pipefail diff --git a/tests/smoke/make_pipeline_glb.py b/tests/smoke/make_pipeline_glb.py new file mode 100644 index 0000000..f5acd59 --- /dev/null +++ b/tests/smoke/make_pipeline_glb.py @@ -0,0 +1,27 @@ +import bmesh +import bpy +import sys + +out = sys.argv[sys.argv.index("--") + 1 :][0] +bpy.ops.wm.read_factory_settings(use_empty=True) +me = bpy.data.meshes.new("Fixture") +bm = bmesh.new() +try: + bmesh.ops.create_uvsphere(bm, u_segments=48, v_segments=24, radius=1.0) + bm.to_mesh(me) +finally: + bm.free() +obj = bpy.data.objects.new("Fixture", me) +bpy.context.collection.objects.link(obj) +obj.select_set(True) +bpy.context.view_layer.objects.active = obj +path = out.replace("\\", "/") +bpy.ops.export_scene.gltf( + filepath=path, + export_format="GLB", + use_selection=True, + export_yup=True, + export_apply=True, + export_animations=False, +) +print(f"saved fixture {path}") From a46f0c3e09e44c8204fde8b34dbd5c0b207864cc Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:03:41 -0400 Subject: [PATCH 3/3] docs: mark the AI asset pipeline template sub-track delivered Inventory counts go to 3 templates. The live-session agent bridge stays unpinned. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- AGENTS.md | 4 ++-- CLAUDE.md | 12 ++++++++++-- README.md | 10 ++++++---- ROADMAP.md | 4 ++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 61e2776..7ce0ebd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ a `.cursor-plugin/plugin.json` manifest so the ecosystem drift checker classifies it as a `cursor-plugin`. This is content the AI loads when the user asks Blender questions or works on Blender add-ons in Cursor or Claude Code. -The content base is 15 skills, 9 rules, 2 templates, 24 snippets, and 54 +The content base is 15 skills, 9 rules, 3 templates, 24 snippets, and 54 examples (counts are CI-enforced against README.md and the manifest). The full inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy and authoring rules: copy `examples/bmesh-gear/`; the render look is specified @@ -33,7 +33,7 @@ in `docs/VISUAL-STYLE.md`; the canonical run prompt is Blender-Developer-Tools/ skills//SKILL.md # 15 skill files rules/.mdc # 9 rule files - templates// # 2 starter templates + templates// # 3 starter templates snippets/.py # 24 standalone Python snippets examples// # 54 runnable smoke-gated examples (+ gallery.json) examples/gallery_framing.py # shared Layer 1 framing measurement (render path only) diff --git a/CLAUDE.md b/CLAUDE.md index 8aa9418..6a7445d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ The **Blender Developer Tools** repository is at **v0.55.0**. It packages skills ``` skills//SKILL.md - AI workflow definitions, 15 total rules/.mdc - Anti-pattern rules, 9 total -templates// - Starter projects, 2 total +templates// - Starter projects, 3 total snippets/.py - Standalone code patterns, 24 total examples// - Runnable smoke-gated examples, 54 total (+ gallery.json) scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only) @@ -62,7 +62,7 @@ VERSION - Source of truth for the repo version | no-unapplied-modifiers-on-export | `*.py` | Export with live modifiers when the export does not request evaluated geometry | | use-correct-axis-rna-per-exporter | `*.py` | `export_scene.gltf` with FBX `axis_forward`/`axis_up`, or `export_scene.fbx` with glTF `export_yup` | -## Templates (2) +## Templates (3) `templates/extension-addon-template/` is a copy-paste-ready Blender extension demonstrating: @@ -80,6 +80,14 @@ VERSION - Source of truth for the repo version - glTF export via `bpy.ops.export_scene.gltf` - Explicit exit codes for CI integration +`templates/ai-asset-pipeline-template/` is a working starter for a headless GLB-in / engine-ready-out job: + +- `argparse` parsing of args after the `--` separator +- Import, unit-scale check, transform apply, origin, normals, evaluated tris +- LOD chain and optional convex/box collider +- Unity / Godot / Unreal glTF export via the engine-export-presets contract +- Explicit exit codes matching `headless-batch-script-template` (0, then 2+) + ## Snippets (24) Small standalone `.py` files at `snippets/.py`, each 5 to 50 lines. diff --git a/README.md b/README.md index 3ed577c..1b4a4ec 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- 15 skills  •  9 rules  •  2 templates  •  24 snippets  •  54 examples + 15 skills  •  9 rules  •  3 templates  •  24 snippets  •  54 examples

@@ -36,7 +36,7 @@ ## Overview -This repository ships **15 skills, 9 rules, 2 templates, 24 snippets, and 54 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable. +This repository ships **15 skills, 9 rules, 3 templates, 24 snippets, and 54 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable. The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly. @@ -44,7 +44,7 @@ The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capabl | --- | --- | | **Skills** | Guided workflows: scaffolding, operators, panels, properties, mesh and bmesh, headless batch, slotted actions, geometry nodes, procedural materials, depsgraph queries, drivers and handlers, `bl_info` migration, video sequencer, imported-mesh cleanup, engine export presets | | **Rules** | Guardrails for the most common AI mistakes: ops-in-loops, bmesh leaks, legacy `bl_info` only, prop assignments, deprecated context-copy override, per-element loops over bulk mesh data, import without scale check, export without evaluated geometry, mixed glTF/FBX axis RNA | -| **Templates** | A working Extensions Platform add-on starter and a headless batch script starter | +| **Templates** | A working Extensions Platform add-on starter, a headless batch script starter, and a GLB-in engine-ready asset pipeline | | **Snippets** | 24 small standalone Python files demonstrating canonical patterns | ## Quick start @@ -1026,7 +1026,7 @@ the duplicates, then glTF ships 24 tris / 48 positions / 8 unique. ``` skills//SKILL.md - 15 skill files, YAML frontmatter, one canonical pattern each rules/.mdc - 9 rule files, anti-pattern + correction -templates// - 2 template directories (extension-addon-template, headless-batch-script-template) +templates// - 3 template directories (extension-addon-template, headless-batch-script-template, ai-asset-pipeline-template) snippets/.py - 24 standalone Python snippets, 5 to 50 lines each ``` @@ -1052,6 +1052,8 @@ Symlink or clone this repo, then point Cursor at it as a skills/rules source. `templates/headless-batch-script-template/` is a working starter for unattended Blender batch jobs. It opens a `.blend`, optionally adds and applies a modifier to every mesh, and exports to glTF, with explicit exit codes for CI integration. Run with `blender --background --python script.py -- --output ...`. +`templates/ai-asset-pipeline-template/` is a working starter for a headless GLB-in / engine-ready-out job. It imports a GLB, runs the `ai-mesh-cleanup` order, emits an LOD chain and optional collider, and exports under a Unity, Godot, or Unreal glTF preset. Run with `blender --background --python pipeline.py -- --input ... --outdir ... --preset unity`. + ## Snippets Each snippet is a standalone Python file under `snippets/`. They are not loaded as a package. Open one, copy the relevant lines into your script, and adapt the names. Each file's header comment cites the Blender doc URL or research section the pattern came from. diff --git a/ROADMAP.md b/ROADMAP.md index 6153754..6559f82 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,7 +20,7 @@ derives the actual version from conventional-commit types. | Modal operators, USD, mathutils | — | — | — | — | Upcoming | | AI asset pipeline: post-generation cleanup | 14 | 8 | 2 | 21 | Shipped (v0.54.0) | | AI asset pipeline: engine export presets | 15 | 9 | 2 | 24 | Shipped | -| AI asset pipeline: headless template | - | - | - | - | Upcoming | +| AI asset pipeline: headless template | 15 | 9 | 3 | 24 | Shipped | | AI asset pipeline: live-session bridge (spike) | - | - | - | - | Upcoming | | Stable | — | — | — | — | Upcoming | @@ -97,7 +97,7 @@ Provider-agnostic GLB-in / engine-ready-out. This repo does not generate meshes. - **Post-generation cleanup skills.** Import and unit-scale normalization, transform apply and origin, poly-budget decimate, LOD chain, collision mesh. Phase 1 shipped in v0.54.0 as `ai-mesh-cleanup`, four snippets, two rules. Bake/UV/atlas follow on. - **Engine export presets.** **Delivered.** Unity (Y-up glTF), Godot (Z-up glTF, meters), Unreal (centimeter glTF bake and FBX `global_scale`). Skill `engine-export-presets`, three snippets, rule `use-correct-axis-rna-per-exporter`, witness `examples/export-preset-axis/`. Draco remains opt-in via `gltf_draco_export.py`. -- **`ai-asset-pipeline-template/`.** Third template. Headless: GLB path in; LOD set, convex collider, engine-preset export; explicit CI exit codes. Pattern: `templates/headless-batch-script-template/`. Phase 3. Unpinned. +- **`ai-asset-pipeline-template/`.** **Delivered.** Third template. Headless: GLB path in; LOD set, convex or box collider, engine-preset export; explicit CI exit codes. Pattern: `templates/headless-batch-script-template/`. - **Live-session agent bridge.** Research spike, not a committed deliverable. MCP server or socket listener so an agent can execute against a running Blender instance instead of blind `--background` scripts. Built on `templates/extension-addon-template/`. Needs its own design pass. Unpinned. ## Candidate pool (next content)