From 3a4cea18834fd6149a46259be67418feebb87981 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:37:32 -0400 Subject: [PATCH] feat: add high-to-low tangent normal bake skill, snippets, and example Cycles cage-bake is stochastic and CI is CPU-only, so the witness uses tolerance gates and a --flat-source falsifier rather than byte-identity. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- .cursor-plugin/plugin.json | 5 + AGENTS.md | 8 +- CLAUDE.md | 15 +- README.md | 24 +- ROADMAP.md | 5 +- .../assets/bake-normal-high-to-low-hero.webp | Bin 0 -> 39000 bytes .../bake-normal-high-to-low/index.html | 858 ++++++++++++++++++ ...bake-normal-high-to-low-contact-sheet.webp | Bin 0 -> 46014 bytes docs/gallery/index.html | 13 +- examples/bake-normal-high-to-low/README.md | 83 ++ .../bake_normal_high_to_low.py | 529 +++++++++++ examples/bake-normal-high-to-low/preview.webp | Bin 0 -> 21552 bytes examples/gallery.json | 11 + skills/bake-high-to-low/SKILL.md | 142 +++ snippets/bake_normal_high_to_low.py | 31 + snippets/save_baked_image.py | 16 + snippets/setup_bake_target_image.py | 30 + tests/smoke/catalog.json | 3 +- 18 files changed, 1755 insertions(+), 18 deletions(-) create mode 100644 docs/gallery/assets/bake-normal-high-to-low-hero.webp create mode 100644 docs/gallery/bake-normal-high-to-low/index.html create mode 100644 docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp create mode 100644 examples/bake-normal-high-to-low/README.md create mode 100644 examples/bake-normal-high-to-low/bake_normal_high_to_low.py create mode 100644 examples/bake-normal-high-to-low/preview.webp create mode 100644 skills/bake-high-to-low/SKILL.md create mode 100644 snippets/bake_normal_high_to_low.py create mode 100644 snippets/save_baked_image.py create mode 100644 snippets/setup_bake_target_image.py diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index bb9bbf5..973c9f9 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -16,6 +16,7 @@ "skills": [ "skills/addon-scaffolding/SKILL.md", "skills/ai-mesh-cleanup/SKILL.md", + "skills/bake-high-to-low/SKILL.md", "skills/engine-export-presets/SKILL.md", "skills/operators/SKILL.md", "skills/ui-panels/SKILL.md", @@ -44,6 +45,7 @@ "snippets": [ "snippets/action-ensure-channelbag-for-slot.py", "snippets/app-handler-registration.py", + "snippets/bake_normal_high_to_low.py", "snippets/bmesh-load-edit-free.py", "snippets/canonical-object-creation.py", "snippets/canonical-object-deletion.py", @@ -62,7 +64,9 @@ "snippets/pointerproperty-binding.py", "snippets/principled-bsdf-material.py", "snippets/register-classes-factory.py", + "snippets/save_baked_image.py", "snippets/shader-node-group.py", + "snippets/setup_bake_target_image.py", "snippets/temp-override-context.py", "snippets/usd-export-evaluation-mode.py", "snippets/version-branch-skeleton.py" @@ -75,6 +79,7 @@ "examples": [ "examples/armature-bend", "examples/attribute-domain-shear", + "examples/bake-normal-high-to-low", "examples/bmesh-gear", "examples/car-mirror-symmetry", "examples/coincident-vert-weld", diff --git a/AGENTS.md b/AGENTS.md index f767024..dba8cee 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, 3 templates, 24 snippets, and 54 +The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 55 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 @@ -31,11 +31,11 @@ in `docs/VISUAL-STYLE.md`; the canonical run prompt is ``` Blender-Developer-Tools/ - skills//SKILL.md # 15 skill files + skills//SKILL.md # 16 skill files rules/.mdc # 9 rule files templates// # 3 starter templates - snippets/.py # 24 standalone Python snippets - examples// # 54 runnable smoke-gated examples (+ gallery.json) + snippets/.py # 27 standalone Python snippets + examples// # 55 runnable smoke-gated examples (+ gallery.json) examples/gallery_framing.py # shared Layer 1 framing measurement (render path only) scripts/build_gallery.py # generates docs/gallery/ (stdlib only) scripts/site/ # vendored landing-page build (build_site.py + template) diff --git a/CLAUDE.md b/CLAUDE.md index d22ab52..2b077bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,23 +17,24 @@ The **Blender Developer Tools** repository is at **v0.56.0**. It packages skills ## Repository Architecture ``` -skills//SKILL.md - AI workflow definitions, 15 total +skills//SKILL.md - AI workflow definitions, 16 total rules/.mdc - Anti-pattern rules, 9 total templates// - Starter projects, 3 total -snippets/.py - Standalone code patterns, 24 total -examples// - Runnable smoke-gated examples, 54 total (+ gallery.json) +snippets/.py - Standalone code patterns, 27 total +examples// - Runnable smoke-gated examples, 55 total (+ gallery.json) scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only) scripts/site/ - Vendored landing-page build (Jinja2) docs/gallery/ - Committed generated gallery pages + hero renders VERSION - Source of truth for the repo version ``` -## Skills (15) +## Skills (16) | Skill | Purpose | | --- | --- | | addon-scaffolding | Extensions Platform manifest, file layout, register/unregister symmetry | | ai-mesh-cleanup | Ordered cleanup for imported generated meshes: units, transform apply, origin, normals, budget, collider | +| bake-high-to-low | Cycles cage-bake of high-poly detail onto a low-poly target as a tangent-space normal map | | engine-export-presets | Unity Y-up, Godot Z-up, and Unreal centimeter glTF/FBX presets; glTF uses export_yup, FBX uses axis_forward/axis_up | | operators | `bpy.types.Operator` lifecycle, `bl_idname`, redo, defensive context handling | | ui-panels | `bpy.types.Panel` declarative `draw()`, layout primitives, conditional UI | @@ -88,7 +89,7 @@ VERSION - Source of truth for the repo version - Unity / Godot / Unreal glTF export via the engine-export-presets contract - Explicit exit codes matching `headless-batch-script-template` (0, then 2+) -## Snippets (24) +## Snippets (27) Small standalone `.py` files at `snippets/.py`, each 5 to 50 lines. @@ -96,9 +97,9 @@ v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`. -AI asset pipeline track: `decimate_to_budget.py`, `convex_hull_collider.py`, `lod_chain.py` (helper duplicated, not imported), `gltf_draco_export.py`, `export_preset_unity.py`, `export_preset_godot.py`, `export_preset_unreal.py`. +AI asset pipeline track: `decimate_to_budget.py`, `convex_hull_collider.py`, `lod_chain.py` (helper duplicated, not imported), `gltf_draco_export.py`, `export_preset_unity.py`, `export_preset_godot.py`, `export_preset_unreal.py`, `setup_bake_target_image.py`, `bake_normal_high_to_low.py`, `save_baked_image.py`. -## Examples (54) +## Examples (55) Runnable scripts at `examples//`, each asserting a real API contract with deterministic checks (exit non-zero on failure) and optionally rendering a still via diff --git a/README.md b/README.md index fd7c1ec..9619686 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- 15 skills  •  9 rules  •  3 templates  •  24 snippets  •  54 examples + 16 skills  •  9 rules  •  3 templates  •  27 snippets  •  55 examples

@@ -36,7 +36,7 @@ ## Overview -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. +This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, and 55 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. @@ -679,6 +679,22 @@ keeps its closed-form counts), each LOD's evaluated triangle count lands within survives within 1e-3 — with the aggressive-ratio nose-tip collapse documented as the caught failure mode. + + + + +Bake normal high to low: an unlit tangent-space normal map card of a six-lobe riveted hatch beside the collapse-decimated bronze plate wearing that map, dark studio + + + +### [bake-normal-high-to-low](examples/bake-normal-high-to-low/) + +Cycles cage-bakes a ribbed hatch onto a `DECIMATE COLLAPSE` LOD. Asserts the +map is not flat (frac 0.7211, MAD 0.09356 vs `(0.5, 0.5, 1.0)`) while a +flat-source control is (frac 0.0000). `--flat-source` exits 5. Byte-identity +across 4.5 / 5.1 / 5.2 is not the contract — Cycles bake is stochastic. +Neighbor of [`lod-decimate-chain`](examples/lod-decimate-chain/). + @@ -1024,10 +1040,10 @@ the duplicates, then glTF ships 24 tris / 48 positions / 8 unique. ## How content is organized ``` -skills//SKILL.md - 15 skill files, YAML frontmatter, one canonical pattern each +skills//SKILL.md - 16 skill files, YAML frontmatter, one canonical pattern each rules/.mdc - 9 rule files, anti-pattern + correction 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 +snippets/.py - 27 standalone Python snippets, 5 to 50 lines each ``` ## Using rules in Cursor diff --git a/ROADMAP.md b/ROADMAP.md index 10d77f8..903ce9d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -21,6 +21,7 @@ derives the actual version from conventional-commit types. | 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 | 15 | 9 | 3 | 24 | Shipped | +| AI asset pipeline: high-to-low bake | 16 | 9 | 3 | 27 | Shipped | | AI asset pipeline: live-session bridge (spike) | - | - | - | - | Upcoming | | Stable | — | — | — | — | Upcoming | @@ -95,7 +96,8 @@ Audit pass on v0.1.0 content: standards-version markers bumped from `1.9.1` to ` 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. +- **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. +- **High-to-low normal bake.** **Delivered.** Skill `bake-high-to-low`, snippets `setup_bake_target_image.py` / `bake_normal_high_to_low.py` / `save_baked_image.py`, witness `examples/bake-normal-high-to-low/`. Cycles CPU cage bake; statistical gates, not byte-identity. UV transfer and atlas packing remain a later phase. Template integration remains a later phase. - **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/`.** **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. @@ -136,6 +138,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo - ~~Lightmap UV channel witness~~ **SHIPPED** as `examples/lightmap-uv-channel/` — two-channel UV contract on a market cart: per-part `UVMap`/`UVLight` with `active`/`active_render` re-asserted after the ops (edit-mode UV ops clear both flags; `uv_layers.new()` never moves them), UV0 drift 0.0 (clobber trap measured 2.068), every UV1 loop in [0,1], 0 overlapping islands via independent binned strict-SAT (15 hits when falsified), min island distance 0.00401 vs nominal margin 0.002 (`MARGIN_DIV × 0.01` semantics measured live); held `MeshUVLoopLayer.data` after edit-mode UV ops segfaults 4.5.11 (5/5, EXCEPTION_ACCESS_VIOLATION), silent no-op pack without prior unwrap; byte-identical on 4.5.11 and 5.1.2 (3680 islands) - ~~Socket attach-point witness~~ **SHIPPED** as `examples/socket-attach-points/` — named `SKT_` empties as the spawn contract on a survey drone: 7 socket world matrices within 1.788e-07 of the authored transform with orthonormal right-handed bases (Gram 3.576e-07, det err 2.980e-07), socket +Z == the mount pad's Newell normal (1.794e-07) by a quaternion-swing construction independent of the Gram-Schmidt basis, origin on the mount-face centroid (6.687e-08), documented up-axis fallback for the +Z/−Z mounts, module seating offset exactly 0.0 with strictly identity local matrices, rigid invariance under a re-pose (2.384e-07); `transform_apply` on an **Empty** root preserves world matrices (2.235e-07) but clears **7/7** `matrix_parent_inverse` and pushes the root scale down into every child (0.35 for an applied 1.35) — both halves asserted; a child left selected during the apply drifts sockets **2.335 m**; `bm.normal_update()` does not fix inward winding (`recalc_face_normals` does — the lofted fuselage rendered as a flat white panel); no-parent-inverse probe jumps 0.690128 m; byte-identical on 4.5.11 and 5.1.2 - ~~Vertex-colour AO witness~~ **SHIPPED** as `examples/vertex-color-ao/` — baked occlusion in a colour attribute on a stone village well, checked against a closed form rather than a captured value: the cosine-weighted hemisphere integral for an infinitely wide wall of height H at distance d is `AO = 1 − ½(1 − 1/√(1+k²))`, `k = H/d`, matched to **6.760e-04** across k = 60.0…0.60 (gate 2.5e-03, QMC noise ~1/√n at 4096 samples); unoccluded plate bakes to **exactly 1.0**; strictly monotone 0.508301 → 0.928467; asset values in [0,1] with spread 1.0; **`BYTE_COLOR` is sRGB-encoded 8-bit, not linear** (0.735 → 0.7379107, peak round-trip error 3.782e-03, matching an independent encode/quantise/decode model to 3.189e-07) while `FLOAT_COLOR` is exact; both survive depsgraph evaluation at deviation 0.0; `bmesh.ops.bevel` offsets along **cached** face normals and flips outward past 90° of staleness (12 mm below ground on a ring of boxes, fixed by `normal_update()`); point-domain AO needs subdivision or the crevice gradient never reaches the attribute; `color_attributes` enumeration order differs between 4.5.11 and 5.1.2 — look up by name; byte-identical on 4.5.11 and 5.1.2 (6966 verts) +- ~~High-to-low tangent normal bake~~ **SHIPPED** as `examples/bake-normal-high-to-low/` — Cycles CPU `type='NORMAL'` cage bake onto a `DECIMATE COLLAPSE` hatch; statistical gates (detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277); `--flat-source` exits 5; RNA identical on 4.5.11, 5.1.2, 5.2.1; byte-identity is not the contract - UV atlas **utilization** witness — coverage/wasted-texel closed forms for a packed lightmap atlas (the non-overlap, unit-square, margin, and active/active_render contracts shipped in `lightmap-uv-channel`; utilization is the remaining unbuilt slice of the old "UV atlas pack" candidate) - ~~GAMMA_CROSS blend-curve witness~~ **SHIPPED** as `examples/vse-gamma-cross/` — the cross blends in a gamma-0.5 space: `((1-t)·√A + t·√B)²` with `t = (frame − start)/duration`, never 1 inside the effect; mid-cross dips 0.115 below the sRGB lerp from crimson/teal (closed form (0.341, 0.349, 0.463) confirmed per frame); AgX-default sampling poisons the fit (0.146 red-channel error, `view_transform='Standard'` mandatory); deleting a consumed input orphans-and-deletes the effect — follow-up to `vse-cut-list` - Falsy `bpy_prop_collection` trap snippet: an empty collection is falsy, so `editor.strips or editor.sequences` silently falls through to the legacy accessor on an empty timeline — always branch on `hasattr`; likely generalizes across the API (found authoring `vse-cut-list`) diff --git a/docs/gallery/assets/bake-normal-high-to-low-hero.webp b/docs/gallery/assets/bake-normal-high-to-low-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..69e6c21a8461fcc7a5bdd2e560f3415e431268af GIT binary patch literal 39000 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 z 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 z Um%a1=vP2Rip?DUW7Vb=509xLN^Z)<= literal 0 HcmV?d00001 diff --git a/docs/gallery/bake-normal-high-to-low/index.html b/docs/gallery/bake-normal-high-to-low/index.html new file mode 100644 index 0000000..0fab88d --- /dev/null +++ b/docs/gallery/bake-normal-high-to-low/index.html @@ -0,0 +1,858 @@ + + + + + + bake-normal-high-to-low — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + +

+
+

bake-normal-high-to-low

+

A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses Statistical gates not byte-identity: detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277; --flat-source exits 5; type=NORMAL not bake_type; RNA identical on 4.5.11, 5.1.2, 5.2.1
+
+
blender --background --python examples/bake-normal-high-to-low/bake_normal_high_to_low.py --
+ +
+
+

A runnable example that cage-bakes a ribbed bronze hatch plate onto a DECIMATE COLLAPSE LOD and asserts the tangent-space normal map carries measurable surface detail — following bake-high-to-low.

+

What it witnesses: Cycles selected-to-active normal bake is a statistical process, not a byte-identical one. A high-poly source produces a map that deviates from flat tangent (0.5, 0.5, 1.0); the same bake from an undisplaced source does not.

+

Byte-identity across 4.5 / 5.1 / 5.2 is not the contract. Tile order and float accumulation differ even at one CPU sample. The gates are fraction of pixels beyond Euclidean 0.04 from flat, mean absolute deviation, and a monotonic gap versus a flat control. Tolerances sit well inside the measured gap (detail frac 0.7211 vs flat 0.0000) so they are not tuned-until-green.

+
  • Detail bake is not flat. frac >= 0.40 and MAD >= 0.05 (measured 0.7211 / 0.09356 on 4.5.11, 5.1.2, and 5.2.1). Catches an inactive Image Texture node, reversed selection, or EEVEE/GPU mis-setup that writes a blank map. MAD floor is half the measured hatch value, still ~30× a flat bake.
  • Flat control is flat. frac <= 0.05 and MAD <= 0.03 (measured 0.0000 / 0.00277). Catches a noisy or wrongly-typed bake that would also satisfy the detail gates.
  • Monotonic gap. detail_frac - flat_frac >= 0.30. The two maps must separate; a tolerance wide enough to pass both would have no discriminating power.
  • --flat-source is the falsifier. Skips the ribs and still runs the detail gates. Must exit 5. Analogous to --same-axis in export-preset-axis.
+

Neighbor of lod-decimate-chain (the LOD is the cage target; collapse keeps UVs) and image-pixels-testcard (save_render, not Image.save(), if you persist the datablock). UV transfer and atlas packing are out of scope.

+

The still stages the baked map as an unlit card beside the LOD wearing it. If the bake were flat, the card would be uniform (128, 128, 255) periwinkle and the plate would shade like the undisplaced cage.

+

Operator RNA (type='NORMAL', use_selected_to_active, cage_extrusion, cage_object as a string, normal_space='TANGENT', margin_type) is identical on 4.5.11, 5.1.2, and 5.2.1 — no shim.

+

Run

+
# Cheap correctness check (no render) - the CI check:
+blender --background --python bake_normal_high_to_low.py --
+
+# Falsifier: undisplaced high. Must exit non-zero (detail frac gate).
+blender --background --python bake_normal_high_to_low.py -- --flat-source
+
+# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
+blender --background --python bake_normal_high_to_low.py -- --output hatch.png
+blender --background --python bake_normal_high_to_low.py -- --output hatch.png --engine cycles
+

Exit codes

+

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

+

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Missing UV layer on the target | | 4 | Bake did not FINISHED or image has_data is false | | 5 | Detail deviant-pixel fraction below 0.40 (--flat-source lands here) | | 6 | Detail MAD below 0.05 | | 7 | Flat control above 0.05 frac / 0.03 MAD | | 8 | Monotonic gap below 0.30 | | 9 | --output produced no file | | 10 | Gallery framing violation |

+

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output.

+
+
+

Source

+
+ examples/bake-normal-high-to-low/bake_normal_high_to_low.py + View on GitHub → +
+
"""High-to-low tangent normal bake — a runnable example.
+
+Witnesses transferring high-poly surface detail onto a collapse-decimated LOD
+via Cycles cage bake. Pixel buffers are stochastic: this example does **not**
+assert byte-identity across 4.5 / 5.1 / 5.2. The contract is statistical.
+
+1. A bake from a ribbed hatch plate produces a map whose pixels deviate
+   from flat tangent-space ``(0.5, 0.5, 1.0)`` above a stated fraction and MAD.
+2. The same bake from an undisplaced source does not.
+3. ``--flat-source`` skips the ribs and rivets and still runs the *detail* gates, so the
+   assertion fails. That is the falsifier (``--same-axis`` in export-preset-axis).
+
+Operator RNA is ``type='NORMAL'``, not ``bake_type``. Identifiers match on
+4.5.11, 5.1.2, and 5.2.1 — no shim.
+
+By default it runs only the correctness check (no render) — the CI smoke
+check. Pass --output to also render a still:
+
+    blender --background --python bake_normal_high_to_low.py --
+    blender --background --python bake_normal_high_to_low.py -- --flat-source
+    blender --background --python bake_normal_high_to_low.py -- --output p.png
+"""
+import argparse
+import math
+import os
+import sys
+
+import bmesh
+import bpy
+
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
+sys.dont_write_bytecode = True
+import gallery_framing
+
+BAKE_RES = 256
+CAGE_EXTRUSION = 0.20
+MARGIN = 16
+THRESH = 0.04
+DETAIL_FRAC_MIN = 0.40
+DETAIL_MAD_MIN = 0.05
+FLAT_FRAC_MAX = 0.05
+FLAT_MAD_MAX = 0.03
+MONO_FRAC_GAP = 0.30
+GRID_SEGS = 40
+HATCH_SIZE = 1.15
+THICKNESS = 0.12
+RIB_AMP = 0.10
+RIVET_AMP = 0.05
+TARGET_TRIS = 900
+FLAT_RGB = (0.5, 0.5, 1.0)
+
+
+def eevee_engine_id():
+    return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"
+
+
+def fail(msg, code):
+    print(f"ERROR: {msg}", file=sys.stderr)
+    return code
+
+
+def duplicate_object(obj, name):
+    dup = obj.copy()
+    dup.data = obj.data.copy()
+    dup.name = name
+    bpy.context.scene.collection.objects.link(dup)
+    return dup
+
+
+def make_hatch(name):
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(
+            bm, x_segments=GRID_SEGS, y_segments=GRID_SEGS, size=HATCH_SIZE
+        )
+        uv = bm.loops.layers.uv.new("UVMap")
+        span = 2.0 * HATCH_SIZE
+        for face in bm.faces:
+            face.smooth = True
+            for loop in face.loops:
+                loop[uv].uv = (
+                    (loop.vert.co.x + HATCH_SIZE) / span,
+                    (loop.vert.co.y + HATCH_SIZE) / span,
+                )
+        me = bpy.data.meshes.new(name)
+        bm.to_mesh(me)
+    finally:
+        bm.free()
+    obj = bpy.data.objects.new(name, me)
+    bpy.context.scene.collection.objects.link(obj)
+    return obj
+
+
+def displace_hatch(obj):
+    me = obj.data
+    n = len(me.vertices)
+    buf = [0.0] * (n * 3)
+    me.vertices.foreach_get("co", buf)
+    for i in range(n):
+        x, y, z = buf[i * 3], buf[i * 3 + 1], buf[i * 3 + 2]
+        rad = math.sqrt(x * x + y * y)
+        theta = math.atan2(y, x)
+        falloff = max(0.0, 1.0 - rad / HATCH_SIZE)
+        rib = RIB_AMP * math.cos(6.0 * theta) * falloff
+        rivet = 0.0
+        for k in range(8):
+            ang = k * math.pi / 4.0
+            px = 0.70 * HATCH_SIZE * math.cos(ang)
+            py = 0.70 * HATCH_SIZE * math.sin(ang)
+            d2 = (x - px) ** 2 + (y - py) ** 2
+            rivet += RIVET_AMP * math.exp(-d2 / 0.010)
+        rim = 0.025 * math.exp(-((rad - 0.92 * HATCH_SIZE) ** 2) / 0.008)
+        buf[i * 3 + 2] = z + rib + rivet + rim
+    me.vertices.foreach_set("co", buf)
+    me.update()
+
+
+def evaluated_triangle_count(obj):
+    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_apply(obj, target_tris):
+    current = evaluated_triangle_count(obj)
+    if current == 0 or current <= target_tris:
+        return current, current
+    ratio = min(1.0, target_tris / current)
+    mod = obj.modifiers.new("DecimateBudget", "DECIMATE")
+    mod.decimate_type = "COLLAPSE"
+    mod.ratio = ratio
+    bpy.context.view_layer.objects.active = obj
+    obj.select_set(True)
+    with bpy.context.temp_override(
+        object=obj, active_object=obj, selected_objects=[obj]
+    ):
+        bpy.ops.object.modifier_apply(modifier=mod.name)
+    for poly in obj.data.polygons:
+        poly.use_smooth = True
+    obj.data.update()
+    return current, evaluated_triangle_count(obj)
+
+
+def setup_bake_target(obj, name, size=BAKE_RES):
+    if not obj.data.uv_layers:
+        return None, None, None
+    img = bpy.data.images.new(name, size, size, alpha=True, float_buffer=False)
+    img.colorspace_settings.name = "Non-Color"
+    mat = bpy.data.materials.new(name + "Mat")
+    mat.use_nodes = True
+    nodes = mat.node_tree.nodes
+    tex = nodes.new("ShaderNodeTexImage")
+    tex.image = img
+    nodes.active = tex
+    tex.select = True
+    if obj.data.materials:
+        obj.data.materials[0] = mat
+    else:
+        obj.data.materials.append(mat)
+    return img, mat, tex
+
+
+def configure_cycles_cpu():
+    scene = bpy.context.scene
+    scene.render.engine = "CYCLES"
+    scene.cycles.device = "CPU"
+    scene.cycles.samples = 1
+    scene.cycles.use_denoising = False
+
+
+def isolate_select(high, low):
+    for ob in bpy.context.view_layer.objects:
+        ob.select_set(False)
+    high.select_set(True)
+    low.select_set(True)
+    bpy.context.view_layer.objects.active = low
+
+
+def bake_normal(high, low):
+    configure_cycles_cpu()
+    isolate_select(high, low)
+    return bpy.ops.object.bake(
+        type="NORMAL",
+        use_selected_to_active=True,
+        cage_extrusion=CAGE_EXTRUSION,
+        use_cage=False,
+        normal_space="TANGENT",
+        margin=MARGIN,
+        margin_type="ADJACENT_FACES",
+        use_clear=True,
+        target="IMAGE_TEXTURES",
+    )
+
+
+def map_stats(image, thresh=THRESH):
+    width, height = image.size
+    n = width * height
+    buf = [0.0] * (n * 4)
+    image.pixels.foreach_get(buf)
+    deviant = 0
+    mad_acc = 0.0
+    fr, fg, fb = FLAT_RGB
+    for i in range(n):
+        r, g, b = buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2]
+        d = math.sqrt((r - fr) ** 2 + (g - fg) ** 2 + (b - fb) ** 2)
+        mad_acc += d
+        if d > thresh:
+            deviant += 1
+    return deviant / n, mad_acc / n
+
+
+def paint_principled(mat, color, metallic, roughness):
+    bsdf = next(n for n in mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED")
+    bsdf.inputs["Base Color"].default_value = color
+    bsdf.inputs["Metallic"].default_value = metallic
+    bsdf.inputs["Roughness"].default_value = roughness
+    return bsdf
+
+
+def wire_normal_map(mat, tex):
+    nodes = mat.node_tree.nodes
+    links = mat.node_tree.links
+    bsdf = paint_principled(mat, (0.22, 0.13, 0.07, 1.0), 0.58, 0.44)
+    nrm = nodes.new("ShaderNodeNormalMap")
+    nrm.space = "TANGENT"
+    links.new(tex.outputs["Color"], nrm.inputs["Color"])
+    links.new(nrm.outputs["Normal"], bsdf.inputs["Normal"])
+
+
+def new_image(name, size=BAKE_RES):
+    img = bpy.data.images.new(name, size, size, alpha=True, float_buffer=False)
+    img.colorspace_settings.name = "Non-Color"
+    return img
+
+
+def check(flat_source):
+    base = make_hatch("BakeBase")
+    if not base.data.uv_layers:
+        return fail("base mesh has no UV layer", 3), None, None, None, None
+
+    low = duplicate_object(base, "BakeLow")
+    before, after = decimate_apply(low, TARGET_TRIS)
+    print(f"lod_tris before={before} after={after} target={TARGET_TRIS}")
+    if not low.data.uv_layers:
+        return fail("decimated LOD lost its UV layer", 3), None, None, None, None
+
+    high_detail = duplicate_object(base, "BakeHigh")
+    if not flat_source:
+        displace_hatch(high_detail)
+
+    img_detail, mat, tex = setup_bake_target(low, "BakeNrmDetail")
+    if img_detail is None:
+        return fail("low mesh has no UV layer", 3), None, None, None, None
+
+    result = bake_normal(high_detail, low)
+    if result != {"FINISHED"}:
+        return fail(f"detail bake returned {result}", 4), None, None, None, None
+    if not img_detail.has_data:
+        return fail("detail bake image has_data is False", 4), None, None, None, None
+
+    detail_frac, detail_mad = map_stats(img_detail)
+    src_label = "flat-source" if flat_source else "detail"
+    print(
+        f"bake_stats source={src_label} frac={detail_frac:.4f} mad={detail_mad:.5f} "
+        f"thresh={THRESH} flat_rgb={FLAT_RGB}"
+    )
+
+    if detail_frac < DETAIL_FRAC_MIN:
+        return (
+            fail(
+                f"detail frac {detail_frac:.4f} < {DETAIL_FRAC_MIN} "
+                "(map is too close to flat tangent; --flat-source is the "
+                "designed fail for this gate)",
+                5,
+            ),
+            None,
+            None,
+            None,
+            None,
+        )
+    if detail_mad < DETAIL_MAD_MIN:
+        return (
+            fail(
+                f"detail MAD {detail_mad:.5f} < {DETAIL_MAD_MIN}",
+                6,
+            ),
+            None,
+            None,
+            None,
+            None,
+        )
+
+    if flat_source:
+        return 0, high_detail, low, img_detail, mat
+
+    img_flat = new_image("BakeNrmFlat")
+    tex.image = img_flat
+    mat.node_tree.nodes.active = tex
+    result = bake_normal(base, low)
+    if result != {"FINISHED"}:
+        return fail(f"flat bake returned {result}", 4), None, None, None, None
+    if not img_flat.has_data:
+        return fail("flat bake image has_data is False", 4), None, None, None, None
+
+    flat_frac, flat_mad = map_stats(img_flat)
+    print(
+        f"bake_stats source=flat frac={flat_frac:.4f} mad={flat_mad:.5f} "
+        f"thresh={THRESH}"
+    )
+
+    if flat_frac > FLAT_FRAC_MAX or flat_mad > FLAT_MAD_MAX:
+        return (
+            fail(
+                f"flat control not flat frac={flat_frac:.4f} "
+                f"(max {FLAT_FRAC_MAX}) mad={flat_mad:.5f} (max {FLAT_MAD_MAX})",
+                7,
+            ),
+            None,
+            None,
+            None,
+            None,
+        )
+    if detail_frac - flat_frac < MONO_FRAC_GAP:
+        return (
+            fail(
+                f"monotonic gap {detail_frac - flat_frac:.4f} < {MONO_FRAC_GAP} "
+                f"(detail={detail_frac:.4f} flat={flat_frac:.4f})",
+                8,
+            ),
+            None,
+            None,
+            None,
+            None,
+        )
+
+    tex.image = img_detail
+    mat.node_tree.nodes.active = tex
+    return 0, high_detail, low, img_detail, mat
+
+
+def make_map_card(image, name="BakeCard"):
+    me = bpy.data.meshes.new(name)
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1.05)
+        for vert in bm.verts:
+            vert.co.x, vert.co.y, vert.co.z = vert.co.x, 0.0, vert.co.y
+        uv = bm.loops.layers.uv.new("UVMap")
+        for face in bm.faces:
+            for loop in face.loops:
+                loop[uv].uv = (
+                    (loop.vert.co.x + 1.05) / 2.10,
+                    (loop.vert.co.z + 1.05) / 2.10,
+                )
+        bm.to_mesh(me)
+    finally:
+        bm.free()
+    mat = bpy.data.materials.new(name + "Mat")
+    mat.use_nodes = True
+    nodes = mat.node_tree.nodes
+    links = mat.node_tree.links
+    nodes.clear()
+    tex = nodes.new("ShaderNodeTexImage")
+    tex.image = image
+    emit = nodes.new("ShaderNodeEmission")
+    emit.inputs["Strength"].default_value = 1.0
+    out = nodes.new("ShaderNodeOutputMaterial")
+    links.new(tex.outputs["Color"], emit.inputs["Color"])
+    links.new(emit.outputs["Emission"], out.inputs["Surface"])
+    me.materials.append(mat)
+    ob = bpy.data.objects.new(name, me)
+    bpy.context.scene.collection.objects.link(ob)
+    return ob
+
+
+def render_still(low, mat, tex, path, engine):
+    scene = bpy.context.scene
+    for ob in list(scene.objects):
+        if ob.type == "MESH" and ob != low:
+            ob.hide_render = True
+            ob.hide_viewport = True
+
+    wire_normal_map(mat, tex)
+    solid = low.modifiers.new("SolidifyDisplay", "SOLIDIFY")
+    solid.thickness = THICKNESS
+    solid.offset = 1.0
+    low.rotation_euler.x = math.radians(72.0)
+    low.location = (1.20, 0.0, 1.05)
+    card = make_map_card(tex.image)
+    card.location = (-1.50, 0.0, 1.05)
+
+    floor_me = bpy.data.meshes.new("Floor")
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=12.0)
+        bm.to_mesh(floor_me)
+    finally:
+        bm.free()
+    fmat = bpy.data.materials.new("Floor")
+    fmat.use_nodes = True
+    fb = fmat.node_tree.nodes["Principled BSDF"]
+    fb.inputs["Base Color"].default_value = (0.03, 0.032, 0.037, 1.0)
+    fb.inputs["Roughness"].default_value = 0.7
+    floor_me.materials.append(fmat)
+    floor = bpy.data.objects.new("Floor", floor_me)
+    scene.collection.objects.link(floor)
+    wall = bpy.data.objects.new("Wall", floor_me.copy())
+    wall.location = (0.0, 9.0, 0.0)
+    wall.rotation_euler = (math.radians(90), 0.0, 0.0)
+    scene.collection.objects.link(wall)
+
+    world = bpy.data.worlds.new("World")
+    world.use_nodes = True
+    world.node_tree.nodes["Background"].inputs["Color"].default_value = (
+        0.02,
+        0.021,
+        0.025,
+        1.0,
+    )
+    scene.world = world
+
+    def light(name, loc, energy, size, col, rot):
+        ld = bpy.data.lights.new(name, "AREA")
+        ld.energy = energy
+        ld.size = size
+        ld.color = col
+        ob = bpy.data.objects.new(name, ld)
+        ob.location = loc
+        ob.rotation_euler = tuple(math.radians(a) for a in rot)
+        scene.collection.objects.link(ob)
+
+    light("Key", (-4.0, -5.0, 6.0), 600.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -38))
+    light("Fill", (5.0, -4.0, 3.0), 110.0, 9.0, (0.75, 0.85, 1.0), (62, 0, 50))
+    light("Rim", (0.5, 4.5, 5.0), 350.0, 4.0, (0.6, 0.78, 1.0), (-55, 0, 175))
+    light("Wedge", (2.5, 3.5, 4.2), 480.0, 6.0, (1.0, 0.76, 0.5), (-72, 0, 195))
+
+    cam_data = bpy.data.cameras.new("Cam")
+    cam_data.lens = 50.0
+    cam = bpy.data.objects.new("Cam", cam_data)
+    cam.location = (0.0, -8.4, 3.15)
+    scene.collection.objects.link(cam)
+    aim = bpy.data.objects.new("Aim", None)
+    aim.location = (0.0, 0.0, 1.05)
+    scene.collection.objects.link(aim)
+    con = cam.constraints.new("TRACK_TO")
+    con.target = aim
+    con.track_axis = "TRACK_NEGATIVE_Z"
+    con.up_axis = "UP_Y"
+    scene.camera = cam
+
+    scene.render.engine = "CYCLES" if engine == "cycles" else eevee_engine_id()
+    if engine == "cycles":
+        scene.cycles.samples = 32
+        scene.cycles.device = "CPU"
+    else:
+        try:
+            scene.eevee.taa_render_samples = 64
+        except AttributeError:
+            pass
+    scene.render.resolution_x = 1280
+    scene.render.resolution_y = 720
+    scene.render.image_settings.file_format = "PNG"
+    scene.render.filepath = path
+    scene.view_settings.view_transform = "Standard"
+
+    fcode = gallery_framing.check_framing(
+        scene,
+        cam,
+        hero=[card, low],
+        elements=[card, low],
+        stage=[floor, wall],
+    )
+    if fcode:
+        return fcode
+    bpy.ops.render.render(write_still=True)
+    if not (os.path.exists(path) and os.path.getsize(path) > 0):
+        return fail("render produced no file", 9)
+    return 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)",
+    )
+    p.add_argument(
+        "--flat-source",
+        action="store_true",
+        help="bake from undisplaced high; detail gates must fail",
+    )
+    args = p.parse_args(argv)
+
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    code, high, low, img, mat = check(args.flat_source)
+    if code:
+        return code
+
+    if args.output:
+        tex = next(
+            n for n in mat.node_tree.nodes if n.type == "TEX_IMAGE" and n.image == img
+        )
+        rcode = render_still(low, mat, tex, os.path.abspath(args.output), args.engine)
+        if rcode:
+            return rcode
+        print(f"rendered still {args.output}")
+
+    print("bake-normal-high-to-low 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/contact-sheets/bake-normal-high-to-low-contact-sheet.webp b/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp new file mode 100644 index 0000000000000000000000000000000000000000..60419763d60c6153793f1b0648d4d013db80086d GIT binary patch literal 46014 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 Oyt7G=1j}nuIDi0iDhf3K literal 0 HcmV?d00001 diff --git a/docs/gallery/index.html b/docs/gallery/index.html index ee83999..1a846a7 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -272,7 +272,7 @@

Examples Gallery

autocomplete="off" spellcheck="false" aria-label="Search examples" /> - 49 examples + 50 examples
+
+ + bake-normal-high-to-low — A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source + +
+

bake-normal-high-to-low

+

A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source

+

witnesses Statistical gates not byte-identity: detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277; --flat-source exits 5; type=NORMAL not bake_type; RNA identical on 4.5.11, 5.1.2, 5.2.1

+ View example +
+
diff --git a/examples/bake-normal-high-to-low/README.md b/examples/bake-normal-high-to-low/README.md new file mode 100644 index 0000000..981061c --- /dev/null +++ b/examples/bake-normal-high-to-low/README.md @@ -0,0 +1,83 @@ +# Bake Normal High to Low + +A runnable example that cage-bakes a ribbed bronze hatch plate onto a +`DECIMATE COLLAPSE` LOD and asserts the tangent-space normal map carries +measurable surface detail — following +[`bake-high-to-low`](../../skills/bake-high-to-low/SKILL.md). + +**What it witnesses:** Cycles selected-to-active normal bake is a statistical +process, not a byte-identical one. A high-poly source produces a map that +deviates from flat tangent `(0.5, 0.5, 1.0)`; the same bake from an +undisplaced source does not. + +Byte-identity across 4.5 / 5.1 / 5.2 is **not** the contract. Tile order and +float accumulation differ even at one CPU sample. The gates are fraction of +pixels beyond Euclidean `0.04` from flat, mean absolute deviation, and a +monotonic gap versus a flat control. Tolerances sit well inside the measured +gap (detail frac 0.7211 vs flat 0.0000) so they are not +tuned-until-green. + +- **Detail bake is not flat.** `frac >= 0.40` and `MAD >= 0.05` (measured + 0.7211 / 0.09356 on 4.5.11, 5.1.2, and 5.2.1). Catches an inactive Image + Texture node, reversed selection, or EEVEE/GPU mis-setup that writes a + blank map. MAD floor is half the measured hatch value, still ~30× a + flat bake. +- **Flat control is flat.** `frac <= 0.05` and `MAD <= 0.03` (measured + 0.0000 / 0.00277). Catches a noisy or wrongly-typed bake that would also + satisfy the detail gates. +- **Monotonic gap.** `detail_frac - flat_frac >= 0.30`. The two maps must + separate; a tolerance wide enough to pass both would have no discriminating + power. +- **`--flat-source` is the falsifier.** Skips the ribs and still runs the + detail gates. Must exit 5. Analogous to `--same-axis` in + [`export-preset-axis`](../export-preset-axis/). + +Neighbor of [`lod-decimate-chain`](../lod-decimate-chain/) (the LOD is the +cage target; collapse keeps UVs) and [`image-pixels-testcard`](../image-pixels-testcard/) +(`save_render`, not `Image.save()`, if you persist the datablock). UV transfer +and atlas packing are out of scope. + +The still stages the baked map as an unlit card beside the LOD wearing it. +If the bake were flat, the card would be uniform `(128, 128, 255)` periwinkle +and the plate would shade like the undisplaced cage. + +Operator RNA (`type='NORMAL'`, `use_selected_to_active`, `cage_extrusion`, +`cage_object` as a string, `normal_space='TANGENT'`, `margin_type`) is +identical on 4.5.11, 5.1.2, and 5.2.1 — no shim. + +## Run + +```bash +# Cheap correctness check (no render) - the CI check: +blender --background --python bake_normal_high_to_low.py -- + +# Falsifier: undisplaced high. Must exit non-zero (detail frac gate). +blender --background --python bake_normal_high_to_low.py -- --flat-source + +# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): +blender --background --python bake_normal_high_to_low.py -- --output hatch.png +blender --background --python bake_normal_high_to_low.py -- --output hatch.png --engine cycles +``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Missing UV layer on the target | +| 4 | Bake did not `FINISHED` or image `has_data` is false | +| 5 | Detail deviant-pixel fraction below 0.40 (`--flat-source` lands here) | +| 6 | Detail MAD below 0.05 | +| 7 | Flat control above 0.05 frac / 0.03 MAD | +| 8 | Monotonic gap below 0.30 | +| 9 | `--output` produced no file | +| 10 | Gallery framing violation | + +The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS +(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch). +Smoke does not pass `--output`. diff --git a/examples/bake-normal-high-to-low/bake_normal_high_to_low.py b/examples/bake-normal-high-to-low/bake_normal_high_to_low.py new file mode 100644 index 0000000..0959446 --- /dev/null +++ b/examples/bake-normal-high-to-low/bake_normal_high_to_low.py @@ -0,0 +1,529 @@ +"""High-to-low tangent normal bake — a runnable example. + +Witnesses transferring high-poly surface detail onto a collapse-decimated LOD +via Cycles cage bake. Pixel buffers are stochastic: this example does **not** +assert byte-identity across 4.5 / 5.1 / 5.2. The contract is statistical. + +1. A bake from a ribbed hatch plate produces a map whose pixels deviate + from flat tangent-space ``(0.5, 0.5, 1.0)`` above a stated fraction and MAD. +2. The same bake from an undisplaced source does not. +3. ``--flat-source`` skips the ribs and rivets and still runs the *detail* gates, so the + assertion fails. That is the falsifier (``--same-axis`` in export-preset-axis). + +Operator RNA is ``type='NORMAL'``, not ``bake_type``. Identifiers match on +4.5.11, 5.1.2, and 5.2.1 — no shim. + +By default it runs only the correctness check (no render) — the CI smoke +check. Pass --output to also render a still: + + blender --background --python bake_normal_high_to_low.py -- + blender --background --python bake_normal_high_to_low.py -- --flat-source + blender --background --python bake_normal_high_to_low.py -- --output p.png +""" +import argparse +import math +import os +import sys + +import bmesh +import bpy + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)) +sys.dont_write_bytecode = True +import gallery_framing + +BAKE_RES = 256 +CAGE_EXTRUSION = 0.20 +MARGIN = 16 +THRESH = 0.04 +DETAIL_FRAC_MIN = 0.40 +DETAIL_MAD_MIN = 0.05 +FLAT_FRAC_MAX = 0.05 +FLAT_MAD_MAX = 0.03 +MONO_FRAC_GAP = 0.30 +GRID_SEGS = 40 +HATCH_SIZE = 1.15 +THICKNESS = 0.12 +RIB_AMP = 0.10 +RIVET_AMP = 0.05 +TARGET_TRIS = 900 +FLAT_RGB = (0.5, 0.5, 1.0) + + +def eevee_engine_id(): + return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT" + + +def fail(msg, code): + print(f"ERROR: {msg}", file=sys.stderr) + return code + + +def duplicate_object(obj, name): + dup = obj.copy() + dup.data = obj.data.copy() + dup.name = name + bpy.context.scene.collection.objects.link(dup) + return dup + + +def make_hatch(name): + bm = bmesh.new() + try: + bmesh.ops.create_grid( + bm, x_segments=GRID_SEGS, y_segments=GRID_SEGS, size=HATCH_SIZE + ) + uv = bm.loops.layers.uv.new("UVMap") + span = 2.0 * HATCH_SIZE + for face in bm.faces: + face.smooth = True + for loop in face.loops: + loop[uv].uv = ( + (loop.vert.co.x + HATCH_SIZE) / span, + (loop.vert.co.y + HATCH_SIZE) / span, + ) + me = bpy.data.meshes.new(name) + bm.to_mesh(me) + finally: + bm.free() + obj = bpy.data.objects.new(name, me) + bpy.context.scene.collection.objects.link(obj) + return obj + + +def displace_hatch(obj): + me = obj.data + n = len(me.vertices) + buf = [0.0] * (n * 3) + me.vertices.foreach_get("co", buf) + for i in range(n): + x, y, z = buf[i * 3], buf[i * 3 + 1], buf[i * 3 + 2] + rad = math.sqrt(x * x + y * y) + theta = math.atan2(y, x) + falloff = max(0.0, 1.0 - rad / HATCH_SIZE) + rib = RIB_AMP * math.cos(6.0 * theta) * falloff + rivet = 0.0 + for k in range(8): + ang = k * math.pi / 4.0 + px = 0.70 * HATCH_SIZE * math.cos(ang) + py = 0.70 * HATCH_SIZE * math.sin(ang) + d2 = (x - px) ** 2 + (y - py) ** 2 + rivet += RIVET_AMP * math.exp(-d2 / 0.010) + rim = 0.025 * math.exp(-((rad - 0.92 * HATCH_SIZE) ** 2) / 0.008) + buf[i * 3 + 2] = z + rib + rivet + rim + me.vertices.foreach_set("co", buf) + me.update() + + +def evaluated_triangle_count(obj): + 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_apply(obj, target_tris): + current = evaluated_triangle_count(obj) + if current == 0 or current <= target_tris: + return current, current + ratio = min(1.0, target_tris / current) + mod = obj.modifiers.new("DecimateBudget", "DECIMATE") + mod.decimate_type = "COLLAPSE" + mod.ratio = ratio + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + with bpy.context.temp_override( + object=obj, active_object=obj, selected_objects=[obj] + ): + bpy.ops.object.modifier_apply(modifier=mod.name) + for poly in obj.data.polygons: + poly.use_smooth = True + obj.data.update() + return current, evaluated_triangle_count(obj) + + +def setup_bake_target(obj, name, size=BAKE_RES): + if not obj.data.uv_layers: + return None, None, None + img = bpy.data.images.new(name, size, size, alpha=True, float_buffer=False) + img.colorspace_settings.name = "Non-Color" + mat = bpy.data.materials.new(name + "Mat") + mat.use_nodes = True + nodes = mat.node_tree.nodes + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + nodes.active = tex + tex.select = True + if obj.data.materials: + obj.data.materials[0] = mat + else: + obj.data.materials.append(mat) + return img, mat, tex + + +def configure_cycles_cpu(): + scene = bpy.context.scene + scene.render.engine = "CYCLES" + scene.cycles.device = "CPU" + scene.cycles.samples = 1 + scene.cycles.use_denoising = False + + +def isolate_select(high, low): + for ob in bpy.context.view_layer.objects: + ob.select_set(False) + high.select_set(True) + low.select_set(True) + bpy.context.view_layer.objects.active = low + + +def bake_normal(high, low): + configure_cycles_cpu() + isolate_select(high, low) + return bpy.ops.object.bake( + type="NORMAL", + use_selected_to_active=True, + cage_extrusion=CAGE_EXTRUSION, + use_cage=False, + normal_space="TANGENT", + margin=MARGIN, + margin_type="ADJACENT_FACES", + use_clear=True, + target="IMAGE_TEXTURES", + ) + + +def map_stats(image, thresh=THRESH): + width, height = image.size + n = width * height + buf = [0.0] * (n * 4) + image.pixels.foreach_get(buf) + deviant = 0 + mad_acc = 0.0 + fr, fg, fb = FLAT_RGB + for i in range(n): + r, g, b = buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2] + d = math.sqrt((r - fr) ** 2 + (g - fg) ** 2 + (b - fb) ** 2) + mad_acc += d + if d > thresh: + deviant += 1 + return deviant / n, mad_acc / n + + +def paint_principled(mat, color, metallic, roughness): + bsdf = next(n for n in mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED") + bsdf.inputs["Base Color"].default_value = color + bsdf.inputs["Metallic"].default_value = metallic + bsdf.inputs["Roughness"].default_value = roughness + return bsdf + + +def wire_normal_map(mat, tex): + nodes = mat.node_tree.nodes + links = mat.node_tree.links + bsdf = paint_principled(mat, (0.22, 0.13, 0.07, 1.0), 0.58, 0.44) + nrm = nodes.new("ShaderNodeNormalMap") + nrm.space = "TANGENT" + links.new(tex.outputs["Color"], nrm.inputs["Color"]) + links.new(nrm.outputs["Normal"], bsdf.inputs["Normal"]) + + +def new_image(name, size=BAKE_RES): + img = bpy.data.images.new(name, size, size, alpha=True, float_buffer=False) + img.colorspace_settings.name = "Non-Color" + return img + + +def check(flat_source): + base = make_hatch("BakeBase") + if not base.data.uv_layers: + return fail("base mesh has no UV layer", 3), None, None, None, None + + low = duplicate_object(base, "BakeLow") + before, after = decimate_apply(low, TARGET_TRIS) + print(f"lod_tris before={before} after={after} target={TARGET_TRIS}") + if not low.data.uv_layers: + return fail("decimated LOD lost its UV layer", 3), None, None, None, None + + high_detail = duplicate_object(base, "BakeHigh") + if not flat_source: + displace_hatch(high_detail) + + img_detail, mat, tex = setup_bake_target(low, "BakeNrmDetail") + if img_detail is None: + return fail("low mesh has no UV layer", 3), None, None, None, None + + result = bake_normal(high_detail, low) + if result != {"FINISHED"}: + return fail(f"detail bake returned {result}", 4), None, None, None, None + if not img_detail.has_data: + return fail("detail bake image has_data is False", 4), None, None, None, None + + detail_frac, detail_mad = map_stats(img_detail) + src_label = "flat-source" if flat_source else "detail" + print( + f"bake_stats source={src_label} frac={detail_frac:.4f} mad={detail_mad:.5f} " + f"thresh={THRESH} flat_rgb={FLAT_RGB}" + ) + + if detail_frac < DETAIL_FRAC_MIN: + return ( + fail( + f"detail frac {detail_frac:.4f} < {DETAIL_FRAC_MIN} " + "(map is too close to flat tangent; --flat-source is the " + "designed fail for this gate)", + 5, + ), + None, + None, + None, + None, + ) + if detail_mad < DETAIL_MAD_MIN: + return ( + fail( + f"detail MAD {detail_mad:.5f} < {DETAIL_MAD_MIN}", + 6, + ), + None, + None, + None, + None, + ) + + if flat_source: + return 0, high_detail, low, img_detail, mat + + img_flat = new_image("BakeNrmFlat") + tex.image = img_flat + mat.node_tree.nodes.active = tex + result = bake_normal(base, low) + if result != {"FINISHED"}: + return fail(f"flat bake returned {result}", 4), None, None, None, None + if not img_flat.has_data: + return fail("flat bake image has_data is False", 4), None, None, None, None + + flat_frac, flat_mad = map_stats(img_flat) + print( + f"bake_stats source=flat frac={flat_frac:.4f} mad={flat_mad:.5f} " + f"thresh={THRESH}" + ) + + if flat_frac > FLAT_FRAC_MAX or flat_mad > FLAT_MAD_MAX: + return ( + fail( + f"flat control not flat frac={flat_frac:.4f} " + f"(max {FLAT_FRAC_MAX}) mad={flat_mad:.5f} (max {FLAT_MAD_MAX})", + 7, + ), + None, + None, + None, + None, + ) + if detail_frac - flat_frac < MONO_FRAC_GAP: + return ( + fail( + f"monotonic gap {detail_frac - flat_frac:.4f} < {MONO_FRAC_GAP} " + f"(detail={detail_frac:.4f} flat={flat_frac:.4f})", + 8, + ), + None, + None, + None, + None, + ) + + tex.image = img_detail + mat.node_tree.nodes.active = tex + return 0, high_detail, low, img_detail, mat + + +def make_map_card(image, name="BakeCard"): + me = bpy.data.meshes.new(name) + bm = bmesh.new() + try: + bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1.05) + for vert in bm.verts: + vert.co.x, vert.co.y, vert.co.z = vert.co.x, 0.0, vert.co.y + uv = bm.loops.layers.uv.new("UVMap") + for face in bm.faces: + for loop in face.loops: + loop[uv].uv = ( + (loop.vert.co.x + 1.05) / 2.10, + (loop.vert.co.z + 1.05) / 2.10, + ) + bm.to_mesh(me) + finally: + bm.free() + mat = bpy.data.materials.new(name + "Mat") + mat.use_nodes = True + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + tex = nodes.new("ShaderNodeTexImage") + tex.image = image + emit = nodes.new("ShaderNodeEmission") + emit.inputs["Strength"].default_value = 1.0 + out = nodes.new("ShaderNodeOutputMaterial") + links.new(tex.outputs["Color"], emit.inputs["Color"]) + links.new(emit.outputs["Emission"], out.inputs["Surface"]) + me.materials.append(mat) + ob = bpy.data.objects.new(name, me) + bpy.context.scene.collection.objects.link(ob) + return ob + + +def render_still(low, mat, tex, path, engine): + scene = bpy.context.scene + for ob in list(scene.objects): + if ob.type == "MESH" and ob != low: + ob.hide_render = True + ob.hide_viewport = True + + wire_normal_map(mat, tex) + solid = low.modifiers.new("SolidifyDisplay", "SOLIDIFY") + solid.thickness = THICKNESS + solid.offset = 1.0 + low.rotation_euler.x = math.radians(72.0) + low.location = (1.20, 0.0, 1.05) + card = make_map_card(tex.image) + card.location = (-1.50, 0.0, 1.05) + + floor_me = bpy.data.meshes.new("Floor") + bm = bmesh.new() + try: + bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=12.0) + bm.to_mesh(floor_me) + finally: + bm.free() + fmat = bpy.data.materials.new("Floor") + fmat.use_nodes = True + fb = fmat.node_tree.nodes["Principled BSDF"] + fb.inputs["Base Color"].default_value = (0.03, 0.032, 0.037, 1.0) + fb.inputs["Roughness"].default_value = 0.7 + floor_me.materials.append(fmat) + floor = bpy.data.objects.new("Floor", floor_me) + scene.collection.objects.link(floor) + wall = bpy.data.objects.new("Wall", floor_me.copy()) + wall.location = (0.0, 9.0, 0.0) + wall.rotation_euler = (math.radians(90), 0.0, 0.0) + scene.collection.objects.link(wall) + + world = bpy.data.worlds.new("World") + world.use_nodes = True + world.node_tree.nodes["Background"].inputs["Color"].default_value = ( + 0.02, + 0.021, + 0.025, + 1.0, + ) + scene.world = world + + def light(name, loc, energy, size, col, rot): + ld = bpy.data.lights.new(name, "AREA") + ld.energy = energy + ld.size = size + ld.color = col + ob = bpy.data.objects.new(name, ld) + ob.location = loc + ob.rotation_euler = tuple(math.radians(a) for a in rot) + scene.collection.objects.link(ob) + + light("Key", (-4.0, -5.0, 6.0), 600.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -38)) + light("Fill", (5.0, -4.0, 3.0), 110.0, 9.0, (0.75, 0.85, 1.0), (62, 0, 50)) + light("Rim", (0.5, 4.5, 5.0), 350.0, 4.0, (0.6, 0.78, 1.0), (-55, 0, 175)) + light("Wedge", (2.5, 3.5, 4.2), 480.0, 6.0, (1.0, 0.76, 0.5), (-72, 0, 195)) + + cam_data = bpy.data.cameras.new("Cam") + cam_data.lens = 50.0 + cam = bpy.data.objects.new("Cam", cam_data) + cam.location = (0.0, -8.4, 3.15) + scene.collection.objects.link(cam) + aim = bpy.data.objects.new("Aim", None) + aim.location = (0.0, 0.0, 1.05) + scene.collection.objects.link(aim) + con = cam.constraints.new("TRACK_TO") + con.target = aim + con.track_axis = "TRACK_NEGATIVE_Z" + con.up_axis = "UP_Y" + scene.camera = cam + + scene.render.engine = "CYCLES" if engine == "cycles" else eevee_engine_id() + if engine == "cycles": + scene.cycles.samples = 32 + scene.cycles.device = "CPU" + else: + try: + scene.eevee.taa_render_samples = 64 + except AttributeError: + pass + scene.render.resolution_x = 1280 + scene.render.resolution_y = 720 + scene.render.image_settings.file_format = "PNG" + scene.render.filepath = path + scene.view_settings.view_transform = "Standard" + + fcode = gallery_framing.check_framing( + scene, + cam, + hero=[card, low], + elements=[card, low], + stage=[floor, wall], + ) + if fcode: + return fcode + bpy.ops.render.render(write_still=True) + if not (os.path.exists(path) and os.path.getsize(path) > 0): + return fail("render produced no file", 9) + return 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)", + ) + p.add_argument( + "--flat-source", + action="store_true", + help="bake from undisplaced high; detail gates must fail", + ) + args = p.parse_args(argv) + + bpy.ops.wm.read_factory_settings(use_empty=True) + code, high, low, img, mat = check(args.flat_source) + if code: + return code + + if args.output: + tex = next( + n for n in mat.node_tree.nodes if n.type == "TEX_IMAGE" and n.image == img + ) + rcode = render_still(low, mat, tex, os.path.abspath(args.output), args.engine) + if rcode: + return rcode + print(f"rendered still {args.output}") + + print("bake-normal-high-to-low 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) diff --git a/examples/bake-normal-high-to-low/preview.webp b/examples/bake-normal-high-to-low/preview.webp new file mode 100644 index 0000000000000000000000000000000000000000..54c1dc8856f91ce590c3e0c181ba20e8df1ea5b9 GIT binary patch literal 21552 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
    D&MS01VCsts8}+2Q!#mB%mK~4G=kr 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 nufYI-J=egZ-Ca1>N5ut(J&nWb@RMWyd literal 0 HcmV?d00001 diff --git a/examples/gallery.json b/examples/gallery.json index 4481790..d9f9258 100644 --- a/examples/gallery.json +++ b/examples/gallery.json @@ -616,6 +616,17 @@ "tags": [ "geometry-nodes" ] + }, + { + "name": "bake-normal-high-to-low", + "dir": "examples/bake-normal-high-to-low", + "teaches": "A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source", + "witnessesFix": "Statistical gates not byte-identity: detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277; --flat-source exits 5; type=NORMAL not bake_type; RNA identical on 4.5.11, 5.1.2, 5.2.1", + "hero": "docs/gallery/assets/bake-normal-high-to-low-hero.webp", + "preview": "examples/bake-normal-high-to-low/preview.webp", + "tags": [ + "mesh" + ] } ] } diff --git a/skills/bake-high-to-low/SKILL.md b/skills/bake-high-to-low/SKILL.md new file mode 100644 index 0000000..6991b48 --- /dev/null +++ b/skills/bake-high-to-low/SKILL.md @@ -0,0 +1,142 @@ +--- +name: bake-high-to-low +description: Cage-bake high-poly surface detail onto a low-poly target as a tangent-space normal map. Cycles CPU, selected-to-active, active image node, UV layer, save_render. Targets 5.2 LTS with 4.5 LTS fallback. +standards-version: 1.10.0 +--- + +# Bake High to Low + +## Trigger + +Use this skill when the user: + +- Wants a tangent-space normal map from a dense source onto a game-resolution mesh +- Mentions cage bake, `use_selected_to_active`, `cage_extrusion`, or `bpy.ops.object.bake` +- Has an LOD from `DECIMATE COLLAPSE` (or a retopo) and needs the missing surface detail in a map +- Is about to call bake from EEVEE, on GPU in CI, or with `bake_type=` + +This skill is the bake step. It composes `ai-mesh-cleanup` (identity scale, applied transforms, a UV layer that already exists) and the LOD contract in `examples/lod-decimate-chain/` / `snippets/decimate_to_budget.py` (collapse onto a budget; UVs survive well enough to bake). It does not unwrap, transfer UVs, or pack an atlas — those are a later phase. It does not generate meshes. + +## The core misunderstanding + +Baking is not a render. EEVEE has no bake path. The operator writes into the **active Image Texture node** of the **active** object's material, not into a file and not into "the selected image datablock." A missing UV layer, the wrong object active, or a node that is not `nodes.active` finishes without error and leaves a blank or unchanged image. + +The pass type RNA is `type`, not `bake_type`. `bake_type` is not on `bpy.ops.object.bake`. Passing it is a TypeError. + +Cycles bake is stochastic. Pixel buffers are not byte-identical across 4.5 / 5.1 / 5.2, even at one sample on CPU. Assert with tolerances and a flat-source control, not a hash. + +## RNA (verified) + +Live dump of `bpy.ops.object.bake.get_rna_type()` on 4.5.11 LTS, 5.1.2, and 5.2.1 LTS: **22 properties, identical identifiers and enums.** No version shim. The kwargs below are the contract. + +Docs: + +- 5.2: https://docs.blender.org/api/current/bpy.ops.object.html#bpy.ops.object.bake +- 5.1: https://docs.blender.org/api/5.1/bpy.ops.object.html#bpy.ops.object.bake +- 4.5: https://docs.blender.org/api/4.5/bpy.ops.object.html#bpy.ops.object.bake + +`cage_object` is a **string name**, never an Object pointer. + +## The canonical pattern + +Every step is a silent-failure point. Do them in this order. + +### 1. Engine and device + +```python +scene = bpy.context.scene +scene.render.engine = "CYCLES" +scene.cycles.device = "CPU" +scene.cycles.samples = 1 +scene.cycles.use_denoising = False +``` + +Do not rely on the default device. Headless CI has no GPU. `samples = 1` is enough for a statistical check; raise it for production maps, not for smoke. + +### 2. Target: UV layer, image, active Image Texture node + +The low-poly object needs a UV layer (`mesh.uv_layers`). Confirm it; do not unwrap here. + +```python +img = bpy.data.images.new("BakeNrm", 128, 128, alpha=True, float_buffer=False) +img.colorspace_settings.name = "Non-Color" + +mat = bpy.data.materials.new("BakeTarget") +mat.use_nodes = True +nodes = mat.node_tree.nodes +tex = nodes.new("ShaderNodeTexImage") +tex.image = img +nodes.active = tex +tex.select = True + +if obj.data.materials: + obj.data.materials[0] = mat +else: + obj.data.materials.append(mat) +``` + +The Image Texture does **not** need to be linked into Principled for the bake to land. It must be `nodes.active`. After the bake, wire `ShaderNodeNormalMap` for display or export; do not plug Image Texture Color into Principled Normal. + +Snippet: `snippets/setup_bake_target_image.py`. + +### 3. Selection: high selected, low active + +```python +high.select_set(True) +low.select_set(True) +bpy.context.view_layer.objects.active = low +``` + +`use_selected_to_active=True` bakes **selected** sources onto the **active** target. Reverse that and the map is empty or self-bakes the low. + +### 4. Cage normal bake + +```python +result = bpy.ops.object.bake( + type="NORMAL", + use_selected_to_active=True, + cage_extrusion=0.20, + use_cage=False, + normal_space="TANGENT", + margin=4, + margin_type="ADJACENT_FACES", + use_clear=True, + target="IMAGE_TEXTURES", +) +``` + +`cage_extrusion` inflates the active object along its normals so rays hit the high mesh. It must clear the high-frequency amplitude (ribs at 0.10 need extrusion > 0.10; 0.20 is the worked value). If extrusion alone skims past concavities, set `use_cage=True` and `cage_object="CageName"` (the object's `.name`). Do not pass the Object. + +`margin_type` is `ADJACENT_FACES` or `EXTEND`. Prefer `ADJACENT_FACES` at UV seams. + +Snippet: `snippets/bake_normal_high_to_low.py`. + +### 5. Save the datablock + +The bake writes the image **in memory**. `Image.save()` on a `GENERATED` image flips `source` to `'FILE'` and drops the buffer — the trap `examples/image-pixels-testcard/` witnesses. Use `save_render()`: + +```python +img.file_format = "PNG" +img.save_render(filepath) +``` + +Snippet: `snippets/save_baked_image.py`. Folded save is the wrong size: the trap is its own contract. + +## Common mistakes + +| Wrong | Right | +| --- | --- | +| `bake_type='NORMAL'` | `type='NORMAL'` | +| `scene.render.engine = 'BLENDER_EEVEE'` then bake | `CYCLES` only | +| GPU / default device in CI | `scene.cycles.device = 'CPU'` | +| Low selected, high active | High selected, low **active** | +| Image Texture present but not `nodes.active` | Set `nodes.active = tex` | +| No UV layer | Confirm `mesh.uv_layers` before bake | +| `Image.save()` after bake | `Image.save_render(path)` | +| Hash / byte-compare maps across versions | Fraction / MAD vs `(0.5, 0.5, 1.0)` plus a flat-source control | + +## Version notes + +Bake operator RNA is identical on 4.5 LTS, 5.1, and 5.2 LTS. Branch on `bpy.app.version` only for unrelated neighbors (EEVEE engine id for a gallery still, NodesModifier inputs). Do not invent a bake shim. + +Witness: `examples/bake-normal-high-to-low/`. `--flat-source` feeds an undisplaced high into the same detail assertion and must exit non-zero. diff --git a/snippets/bake_normal_high_to_low.py b/snippets/bake_normal_high_to_low.py new file mode 100644 index 0000000..d59dece --- /dev/null +++ b/snippets/bake_normal_high_to_low.py @@ -0,0 +1,31 @@ +# Cage-bake a high-poly source onto a low-poly target as a tangent-space +# normal map. Cycles CPU only. High must be selected; low must be active. +# Operator RNA is `type`, not `bake_type`. cage_object is a string name. +# Identifiers match on 4.5 LTS, 5.1, and 5.2 LTS — no version shim. +# +# Reference: +# https://docs.blender.org/api/current/bpy.ops.object.html#bpy.ops.object.bake + +import bpy + + +def bake_normal_high_to_low(high, low, cage_extrusion=0.20, margin=16): + scene = bpy.context.scene + scene.render.engine = "CYCLES" + scene.cycles.device = "CPU" + scene.cycles.samples = 1 + scene.cycles.use_denoising = False + high.select_set(True) + low.select_set(True) + bpy.context.view_layer.objects.active = low + return bpy.ops.object.bake( + type="NORMAL", + use_selected_to_active=True, + cage_extrusion=cage_extrusion, + use_cage=False, + normal_space="TANGENT", + margin=margin, + margin_type="ADJACENT_FACES", + use_clear=True, + target="IMAGE_TEXTURES", + ) diff --git a/snippets/save_baked_image.py b/snippets/save_baked_image.py new file mode 100644 index 0000000..ee46f51 --- /dev/null +++ b/snippets/save_baked_image.py @@ -0,0 +1,16 @@ +# Write a baked image datablock to disk without dropping the buffer. +# Image.save() on a GENERATED image flips source to FILE and the pixels +# re-source from disk — empty if the file was not the bake. save_render() +# writes the same PNG and leaves source GENERATED. +# +# Reference: +# https://docs.blender.org/api/current/bpy.types.Image.html#bpy.types.Image.save_render +# Witness: examples/image-pixels-testcard/ + +import bpy + + +def save_baked_image(image, filepath, file_format="PNG"): + image.file_format = file_format + image.save_render(filepath) + return filepath diff --git a/snippets/setup_bake_target_image.py b/snippets/setup_bake_target_image.py new file mode 100644 index 0000000..26c011d --- /dev/null +++ b/snippets/setup_bake_target_image.py @@ -0,0 +1,30 @@ +# Target setup for bpy.ops.object.bake: generated image, Non-Color, +# a material whose Image Texture node is nodes.active, UV layer present. +# The texture does not need a link into Principled. If it is not active, +# the bake finishes and writes nowhere. +# +# Reference: +# https://docs.blender.org/api/current/bpy.ops.object.html#bpy.ops.object.bake +# https://docs.blender.org/api/current/bpy.types.Image.html +# https://docs.blender.org/api/current/bpy.types.ShaderNodeTree.html + +import bpy + + +def setup_bake_target_image(obj, name="BakeNrm", size=128): + if not obj.data.uv_layers: + raise ValueError(f"{obj.name} has no UV layer") + img = bpy.data.images.new(name, size, size, alpha=True, float_buffer=False) + img.colorspace_settings.name = "Non-Color" + mat = bpy.data.materials.new(name + "Mat") + mat.use_nodes = True + nodes = mat.node_tree.nodes + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + nodes.active = tex + tex.select = True + if obj.data.materials: + obj.data.materials[0] = mat + else: + obj.data.materials.append(mat) + return img, mat, tex diff --git a/tests/smoke/catalog.json b/tests/smoke/catalog.json index 16054b0..d18d5d9 100644 --- a/tests/smoke/catalog.json +++ b/tests/smoke/catalog.json @@ -68,5 +68,6 @@ }, {"name": "ngon-triangulate", "script": "examples/ngon-triangulate/ngon_triangulate.py"}, {"name": "unapplied-scale-gltf", "script": "examples/unapplied-scale-gltf/unapplied_scale_gltf.py"}, - {"name": "coincident-vert-weld", "script": "examples/coincident-vert-weld/coincident_vert_weld.py"} + {"name": "coincident-vert-weld", "script": "examples/coincident-vert-weld/coincident_vert_weld.py"}, + {"name": "bake-normal-high-to-low", "script": "examples/bake-normal-high-to-low/bake_normal_high_to_low.py"} ]