Imported from Zawaro/blender-cnc-templates (
AGENTS.md). Install upstream withnpx skills add Zawaro/blender-cnc-templates. Copyright stays with the author.
AGENTS.md
Project summary
Generates .blend scene templates for multiple Blender versions (2.79–5.1). Each variant is isolated in its own folder. Output goes to release/{variant}/.
Directory structure
{variant}/ # One per Blender target
constants.py # TEMPLATE_PREFIX, TEMPLATE_VARIANT
main.py # ~20 lines: compat setup + scene creation + save .blend
generate_scripts.py # ~5 lines: delegates to shared/script_gen.py
world_materials.py # (eevee, legacy_eevee, legacy_cycles only) variant-specific world materials
scripts/ # Generated .txt scripts (gitignored)
requirements.txt
README.md
legacy/ is unique: has its own scene_builder.py, plane_materials.py,
compat.py, world_materials.py — does NOT use shared/scenes.py.
legacy_cycles/ has a local plane_materials.py.
shared/
compat.py # BaseCompat interface + get_compat() factory
compat_279.py # Blender 2.79 adapters
compat_280.py # Blender 2.80–2.92 adapters
compat_293.py # Blender 2.93 adapters (CryptomatteV2 + Shader to RGB)
compat_300.py # Blender 3.0 adapters
compat_420.py # Blender 4.2 adapters
compat_500.py # Blender 5.0 adapters
scenes.py # BaseScene + all game classes (single copy)
plane_materials.py # Material factories (single copy)
world_materials.py # World material factories (single copy, compat-aware)
scene_names.py # SCENE_SUFFIX_MAP (single source of truth)
script_gen.py # Data-driven .txt script generator
build_utils.py # Load scripts into Blender + save .blend
constants.py # TEMPLATE_VERSION + filter width/size values
node_arrange.py # Auto-layout for Blender nodes
node_arrange_legacy.py
tests/
conftest.py # Fixtures: clean_scene, compat (parses pytest-blender version)
test_compat.py # Compat layer interface compliance
test_compat_runtime.py # Compat methods on mock bpy objects (no Blender needed)
test_scene_names.py # Scene name mapping completeness
test_script_gen.py # Script generation (no bpy needed)
test_version.py # Version single source of truth + env tests
test_env.py # .env format and build script tests
test_plane_materials_bpy.py # Material factories (needs Blender)
test_build_utils_bpy.py # load_scripts (needs Blender)
test_scenes_bpy.py # BaseScene integration (needs Blender)
release/ # Output .blend/.zip/.7z files (gitignored)
test.sh # Local multi-version test runner
Build commands
uv run ./build.sh # Interactive: pick variant, auto-finds Blender, generates + renders
uv run python {variant}/generate_scripts.py # Generate scripts only (no Blender needed)
uv run pytest tests/ -v # Run tests (no Blender needed)
build.sh flow: compute BUILD_NUMBER from git rev-list --count HEAD → select variant → find Blender executable → create per-variant .venv via bootstrap.sh if missing → pip install from requirements.txt → run generate_scripts.py → run Blender headless with main.py → archive to .zip + .7z.
bootstrap.sh creates .venv in each variant folder and installs fake-bpy-module for IDE autocomplete.
Variants
| Variant | Blender | Compositing method | Notes |
|---|---|---|---|
| legacy | 2.79 | None (internal render) | Unique: own scene_builder.py, no shared/scenes.py |
| legacy_cycles | 2.79 | IDMask (Object Index) | Cycles only, Python 3.5 compat |
| legacy_eevee | 2.80–2.92 | ShadowLayer (2 view layers) | Eevee, no CryptomatteV2 |
| eevee | 2.93 | CryptomatteV2 | Eevee, needs 2.93+ |
| cyclesx | 3.0–3.6 | CryptomatteV2 | Cycles |
| eevee_next | 4.2–4.3 | CryptomatteV2 | Eevee Next |
| hi_five | 5.0–5.1 | CryptomatteV2 | Eevee Next |
Version management
shared/constants.py— single source of truth forTEMPLATE_VERSION.pyproject.tomlderives its version from it (dynamic via setuptools attr). Never bump the version manually — release-please owns it.- Release flow: on version-worthy commits (
feat:/fix:) tomain, release-please opens achore: releasePR (.github/workflows/release-please.yml). Merging it bumpsTEMPLATE_VERSION, regeneratesCHANGELOG.md, builds all 7 variants, publishes.blend/.zip/.7zrelease assets, and tags{tag}_build{N}. - Each variant's
constants.pyhasTEMPLATE_VARIANT(matches folder name) andTEMPLATE_PREFIX build.shcomputesBUILD_NUMBERfrom git commit count and exports itshared/build_utils.pyreadsBUILD_NUMBERfrom env:{prefix}_{version}_build{number}_{date}.blend- Output goes to
release/{variant}/subdirectories
Architecture
Compat layer
All Blender API differences across versions (2.79–5.1) are abstracted through shared/compat.py. Each variant has a compat class implementing the same interface:
- Compositor access:
scene.node_tree(2.79–4.x) vsscene.compositing_node_group(5.0) - Node types:
CompositorNodeSepHSVAvsCompositorNodeSeparateColor,CompositorNodeMixRGBvsShaderNodeMix - Node group I/O:
inputs.new()vsinterface.new_socket() - Switch node toggle:
.checkvs.inputs[0].default_value - Material transparency:
blend_method/shadow_methodvssurface_render_method - Light creation:
lamp_add(2.79) vslight_add(2.80+) - Eevee settings: GTAO/TAA/SSR (2.80–4.x) vs
use_shadowsonly (5.0) - Cryptomatte:
has_cryptomatte()returns False for 2.79/2.80–2.92, True for 2.93+ - Shader to RGB:
has_shader_to_rgb()returns False for 2.79/2.80–2.92, True for 2.93+
Compositing (three methods)
create_composite_nodes() in shared/scenes.py uses a three-way branch:
- CryptomatteV2 (3.0+):
CompositorNodeCryptomatteV2withmatte_idmatching shadow plane names - ShadowLayer (2.80–2.92): Two view layers — default excludes shadows, ShadowLayer renders only shadows. Uses
CompositorNodeRLayerswith.layer = "ShadowLayer" - IDMask (2.79):
CompositorNodeIDMaskwithpass_indexon shadow planes, readsIndexOBoutput at index 14
Scene creation flow
Each variant's main.py is ~20 lines:
get_compat(major, minor)— detect versionregister_world(suffix, world_cls)— register world material classescls(compat)for each scene class — creates full Blender sceneload_scripts()+setup_text_editor()+save_blend()— finalize
Exception: legacy/ uses its own scene_builder.py with hardcoded camera/light configs per game. It does not import from shared/scenes.py.
World materials
shared/world_materials.py— cyclesx/eevee_next/hi_five viaBase_World(props, compat)eevee/world_materials.py— Eevee 2.93+ (different API pattern, has_set_mapping()helper for cross-version Mapping node)legacy_eevee/world_materials.py— Eevee 2.80–2.92 (same as eevee, copied)legacy_cycles/— uses adapter wrappers to convert world class signatures
Script generation
shared/script_gen.py uses data-driven generation with named PlaneVisibility dataclasses. Each variant's generate_scripts.py is ~5 lines delegating to generate_all_scripts().
Linting
uv run ruff check .
Config in pyproject.toml: 2-space indent, 200-char line length.
Commits and PR titles must follow Conventional Commits — enforced by commitlint on every PR (.github/workflows/commitlint.yml, commitlint.config.cjs). This matters because release-please parses commit types to decide version bumps and CHANGELOG grouping.
Testing
# Non-bpy tests only (fast, no Blender needed)
uv run pytest tests/ -v
# All tests including bpy integration (needs Blender)
uv run pytest -p pytest-blender --blender-executable $BLENDER_EXE tests/ -v
Two test tiers:
- Non-bpy (~137 tests): compat interface, compat runtime (mock-based), scene names, script generation, version, env format. All run in any Python.
- bpy integration (~27 tests): plane materials, build utils, BaseScene. Run inside Blender via pytest-blender.
pyproject.toml disables pytest-blender by default (addopts = "-p no:pytest-blender"). Re-enable with -p pytest-blender when running bpy tests.
bpy tests require pytest installed in Blender's own Python:
./path/to/blender -b --python-expr "import sys; print(sys.executable)" | xargs -I{} {} -m pip install pytest
Key conventions
- 2-space indentation throughout (enforced by ruff).
- Docstrings: keep short docstrings on functions/classes, matching the existing style in
shared/*.py(e.g.build_utils.py). No no-docstring rule. - Scripts inside version folders run in Blender's Python interpreter, not standalone.
bpyis the core API. Do not write Blender add-ons — these are headless automation scripts. - Each game scene (RA2, TS, RW, RA1, RM, D2K) has 3 variants: base, INF (infantry, lower res), FX (effects). The
_FXclasses typically inherit from the base and override a few attributes. shared/node_arrange.pymust be called after creating node trees to auto-layout nodes.scripts/directories andrelease/are gitignored. Never commit generated artifacts.TEMPLATE_VERSIONlives only inshared/constants.py. Never put it in variant folders.- Output files:
{prefix}_{version}_build{number}_{date}.blendinrelease/{variant}/
Git conventions
- Commits: Conventional Commits format —
type(scope): description (#issue). Commit messages are enforced by commitlint (commitlint.config.cjs); valid types arefeat,fix,docs,style,refactor,perf,test,build,ci,chore,revert(nobug— usefixso release-please can version it). Always suffix the GH issue number in parentheses, e.g.fix(compat): routing for 2.79 (#14). Scope is optional but preferred (the file or system affected). - Branch names:
type/issue-number-kebab-description, e.g.feat/14-port-release-please. Always include the issue number after the slash. - GH issue titles: same conventional prefix as commits —
feat: port release-please setup from blender-cnc-toolkit. - PR titles: conventional prefix + issue number in parentheses —
feat: port release-please setup (#14). The branch already has the number, but the PR title must include it too.
GitHub CLI
gh is available for issue and PR workflows. Common commands:
gh issue create— file a new issuegh issue list— list open issuesgh pr view— view a pull request
Issues
Bugs and feature requests are tracked via GitHub Issues. Use the existing labels:
bug— something isn't workingenhancement— new feature or requestdocumentation— docs improvementsgood first issue— good for newcomershelp wanted— extra attention needed
Gotchas
build.shis interactive (usesselectfor variant choice). Cannot be run non-interactively.- Version folder names do not match Blender versions exactly (e.g.
hi_five= Blender 5.1,eevee_next= Blender 4.3). shared/constants.pyhas different filter sizes per render engine (Cycles vs Eevee vs Eevee Next). Check this before changing filter-related values.fake-bpy-moduleinrequirements.txtis for IDE support only, not runtime. The realbpycomes from the Blender installation.- Eevee never had Object Index, Material Index, or Cryptomatte in any version. The ShadowLayer approach (2 view layers) works around this for 2.80–2.92.
CompositorNodeCryptomatteV2was introduced in Blender 3.0. Before that, onlyCompositorNodeCryptomatte(4 inputs) exists.eevee/world_materials.pyhas_set_mapping()helper because Mapping node inputs differ: 1 input (Vector) in 2.80, 3 inputs (Location/Rotation/Scale) in 2.90+.compat_280.pyoverrideshas_cryptomatte() → Falseandhas_shader_to_rgb() → Falsebecause Eevee 2.80–2.92 lacks these features. 2.93 has its owncompat_293.pythat returns True for both.legacy/does not use the shared scene classes at all — it has its ownscene_builder.pywith hardcoded camera/light configs per game. Do not add shared scene logic there.- All shared modules must be Python 3.5 compatible for legacy_cycles (no f-strings, no variable annotations, no dataclass).
test.shclears.pytest_cachebetween Blender version runs — pytest-blender cachesblender_versionand stale cache poisons the next run.
