diff --git a/.claude/skills/art-dev/SKILL.md b/.claude/skills/art-dev/SKILL.md new file mode 100644 index 000000000..46f2493f9 --- /dev/null +++ b/.claude/skills/art-dev/SKILL.md @@ -0,0 +1,103 @@ +--- +name: art-dev +description: Drives a LANTERN art-asset session in Blender (blendermcp) — create a new asset or modify an existing one, staged so a Blender-BEGINNER operator can jump in. Use when the operator types /art-dev or opens art work ("refine the player helmet by doing X", "make a new enemy", "model/kitbash/retexture Y", "fix the eyes on Z"). Does simple changes fully headless; drops into a hybrid nudge-and-bake feedback loop (operator drives, I bake back) for anything needing precision or aesthetic judgment. +--- + +# /art-dev — LANTERN Art-Asset Session Orchestrator (Blender) + +Drives a **Blender 5.1 (blendermcp)** art session for the LANTERN art track — modeling, kitbashing, retexturing, posing, and staging assets. **The style is binding and lives elsewhere:** [[Art_Direction_Lantern]] (the constitution) + [[Lantern_Art_Pipeline_Recipes]] (the operating manual) are authoritative; `CLAUDE.md` owns repo conventions (asmdefs, the Unity bake rules, memory layers, git). This skill carries the **process** those don't: how to take an art goal → a staged, reproducible Blender result, working *with* a Blender-beginner operator. + +The spine: **size by complexity → ground → agree the change → setup scene → BUILD (do-it-all headless, or the nudge-and-bake loop) → verify by render → export/save → (Unity bake) → doc/memory bookend → offer asset commit.** + +## Non-negotiables (every track) + +1. **The material keystone ★ — no per-asset texturing, ever.** Every asset shares the ONE palette atlas (`Assets/_Project/Art/Palette/PaletteAtlas.png`) + emissive materials; color = *placing* a face's UV onto a colour band, never unwrap/paint. Three shader roles only: Lit-Palette (statics), Emissive-Gloam (glows — hue/flicker = gameplay info), Skinned-Palette (creatures/suits). This is the solo-dev keystone — deviating breaks batching, coherence, and the whole factory. Detail → [[Art_Direction_Lantern]] + `references/style-and-practices.md`. +2. **Poly budgets + the readability law ★.** props ≤ ~800 tris · creatures ≤ ~3k · hero/boss/suit ≤ ~6k. Value-first (every warm/cold read carried by luminance + shape + flicker-cadence, not hue alone); reads at 200 px; silhouette-first. Report the tri count on every asset. +3. **The palette ontology ★.** warm gold/amber = true light (ours) · cold corpse-green/blue = gloam (counterfeit) · lure-red = bait · white-hot = conversion. Steady = true, flicker = false. Pick emissive hues to match what the asset *is*. +4. **Nudge-and-bake for precision ★.** Never script fine placement/pose/aesthetics blind (a script can't see painted/low-poly features + judges via a low-res grab). Stage + hand off to the operator's live GUI, then **bake the result back into a reproducible form** (bone-local offsets, saved transforms). See *The nudge-and-bake loop* below + [[blender-hybrid-nudge-and-bake]]. +5. **Beginner-coach ★.** The operator is a **Blender beginner.** Every handoff spells out exact keys/clicks (select, `G`/`S`/`R`, orbit = MMB-drag, zoom = scroll, save) and the *why* (the best practice). Teach proactively; never assume Blender fluency. Wrong-but-confident Blender advice wastes their time — verify an operator's-eyes step rather than guess. +6. **Reproducible + source-art home.** All work persists as a per-asset master `.blend` in `/ArtSource/Blender/` (OUTSIDE `Assets/` so a live Unity session never auto-imports it), textures **packed** (`File > Pack All`). The script/steps are the source of truth — bake operator nudges back into them. +7. **Verify by render, not blind orbit.** Judge with a high-res EEVEE **render-to-file** (Read the PNG) under the murk + capture grade; check material *values*, not just a dark screenshot. The ~800px viewport grab is a quick check only (and returns BLACK when the window isn't drawing — `area.tag_redraw()` + re-capture, or ask the operator to focus Blender). +8. **Asset commits gated; docs commits standing.** New `.blend`/`.glb`/imported assets: **offer** + propose grouping; commit only on explicit "commit". Docs-only (vault / CLAUDE.md / this skill) auto-commit at session end in a `Docs:` group + auto-push `main`→`gitea` (standing auth 2026-07-07); `origin` only when asked. +9. **Path-portable skill.** No machine-specific absolute paths in this skill/refs. Blender's Python runs outside the harness, so runtime code sets `PROJ = r""` (resolved at runtime) and derives every path from it. + +## When to use / not use + +**Use** for any LANTERN visual asset: model a prop/creature/suit, kitbash off a Synty base (licensed to modify), retexture to the palette, pose, add accents (eyes/lamps/kelp), assemble a style-proof composition, or refine an existing `.blend`. + +**Don't use** for: the **in-engine Unity bake/shader authoring** as the primary job (that's DOTS/editor work — `/dots-dev`; this skill *hands off* to it); pure design/fiction questions (answer directly); non-art tasks. + +## Size by complexity — do this first (NOT by time) + +| Track | When | Path | +|---|---|---| +| **Auto** | deterministic, no feature-alignment or taste call: resize/rescale, recolour to a band, add a bolt/greeble ring, boolean, duplicate, batch-rename, export | I do it **end-to-end** headless → render → leave it staged & editable → report. | +| **Guided** | needs precision (align to a painted/modeled feature), aesthetic judgment (silhouette read, "does this look right"), or pose polish | I do the bulk, then **the nudge-and-bake loop** — stage, operator drives, I bake back, iterate. | +| **New-asset** | a brand-new model/creature/suit from scratch or a Synty kitbash | full spine; the kitbash/model/UV/rig steps are Auto, the placement/pose/silhouette calls are Guided. | + +Push to do as much as possible in **Auto**; hand off only what genuinely needs eyes-on. Unsure which → one `AskUserQuestion` ("shall I just make the change, or stage it for you to place?"). **Always leave the scene editable** (named objects, relevant pieces selected, a good camera) even in Auto — the operator may want to jump in. + +## The spine + +### Phase 0 — Pre-flight +1. **Blender live?** `get_scene_info` (needs `user_prompt`). No response → blendermcp addon not connected (socket 9876) / window hidden — surface, ask the operator to open Blender + check the BlenderMCP panel. **Blender is 5.1 here** (EEVEE Next; some 4.x APIs differ). +2. **Which asset / .blend?** New → a fresh per-asset scene. Existing → `open_mainfile` the master `.blend` from `/ArtSource/Blender/`. Never clobber another asset's scene — one `.blend` per asset. +3. **Intake gate.** Vague goal ("make it cooler") → one `AskUserQuestion` bundling the real forks (what silhouette / which base / warm-or-cold). A concrete goal ("add a porthole grille to the helmet") → skip. + +### Phase 1 — Ground (read-only) +Scan the art canon before touching geometry: [[Art_Direction_Lantern]] + [[Lantern_Art_Pipeline_Recipes]] (budgets, shaders, rig families, ontology), the target's fiction (Grep the vault for the creature/prop name — build *on-fiction*), the palette atlas band layout (`references/style-and-practices.md`), and the machine gotchas ([[blender-mcp-and-unity-mcp-v10]] = FBX-import workaround + ArtSource home; [[blender-hybrid-nudge-and-bake]] = the loop). For an existing asset, `get_scene_info` + `get_object_info` to map what's there. Synthesize a **≤200-word plan** (below). + +### Phase 2 — Agree the change +State a short plan: what changes, which track (Auto/Guided), the target budget/silhouette, warm/cold intent, and — for Guided — exactly **which step you'll hand to the operator** and which you'll do. Present forks with a recommendation. For a substantial *new* asset, this is a light gate; for a tweak, a one-liner. Don't over-ceremony a small change. + +### Phase 3 — Setup scene (standing tooling) +Establish the LANTERN working scene (snippets → `references/blender-cookbook.md`): metric units (1 u = 1 m); the murk world + warm-key/gloam-fill lights + the capture grade; the palette atlas material (`Closest` filter) + emissive materials; a **fixed render camera** + named views for consistent framing; `view_transform='Standard'` for judging hue (AgX desaturates bright emission to white). These are pure wins — set them even in Auto. + +### Phase 4 — Build +- **Model/kitbash (Auto):** primitives via `bmesh.ops`; faceted (flat shading); UVs *placed* on atlas cells (never unwrap); Synty base import via the **5.1 window-override + `ignore_leaf_bones=True, automatic_bone_orientation=False`** workaround; normalize to metres/feet-at-floor. Report tris vs budget. +- **Precision / pose / silhouette (Guided) → the nudge-and-bake loop** (next section). +- Iterate: for Guided, the operator can request modifications ("bigger", "move it", "add another there", "stage the vent for me") — treat each as a mini Auto or a new stage. Keep going until they're happy. + +### Phase 5 — Verify +High-res EEVEE **render-to-file** → Read the PNG (murk + capture grade); judge silhouette + value + ontology at full size AND ~200px. **Render from the GAME CAMERA too, not only a hero 3/4 ★** — this is a top-down ARPG (Pitch 45° / FOV 55°, rig values in `references/blender-cookbook.md` §9), and detail belongs where the camera actually looks: top-facing surfaces (crown, shoulders, back-pack) read; the face/underside barely does. Check **both facings** (front + back-away). Check material *values* if anything looks off (a dark render hides bugs). Report tri count. For skinned meshes, `WorldRenderBounds`/bone reads return the bind AABB not the deformed pose — use the operator's eyes. + +### Phase 6 — Export / save +Save the master `.blend` to `/ArtSource/Blender/_.blend`, **Pack All** first (a Synty `.psd` pack-warn is harmless). Export a `.glb` (statics) / per-action FBX (skinned) per the recipes with correct scale settings — only when an export is actually wanted (concept assets can stay `.blend`-only). **Never write into `Assets/`** while a Unity session may be live. + +### Phase 7 — Unity bake (optional, hand off) +The real A0/game path is the in-engine bake (collider policy, assign the shared material, subscene, Rukhanka for skinned). If the editor is busy/another agent holds it → **stay read-only on Unity**, note the bake as the pending follow-up. Otherwise hand to `/dots-dev` (statics recipe / skinned recipe in [[Lantern_Art_Pipeline_Recipes]]). + +### Phase 8 — Doc + memory bookend +Session log → `Docs/Vault/07_Sessions//_.md`; a new reusable gotcha/technique → update [[blender-mcp-and-unity-mcp-v10]] or [[blender-hybrid-nudge-and-bake]]; a new reusable placement offset / rig fact → bake it into `references/blender-cookbook.md` so the next asset starts correct. CLAUDE.md self-maintenance if a rule changed (40 KB budget — archive, don't delete). + +### Phase 9 — Report + offer commit +Summary; what changed (repo-relative paths); tris vs budget; render verdict; what's staged/editable for the operator; the pending Unity bake. **Commit docs + push gitea** (standing); **offer** asset commits — keep each asset with its `.meta`/sidecar, stage explicit paths, never `git add -A`. + +## The nudge-and-bake loop (the heart of Guided) + +The operator has a **live Blender GUI** — use it for the last 20% (precision + taste) a script can't do. Three steps, iterated: + +1. **I stage (headless):** build/position the pieces at a best guess; **name** them clearly; give them a temp **bright emissive marker** material so they're unmistakable; float them clearly in view (in front of the target, not buried); **select** them (active = the first); set flat `MATERIAL` shading so the *target* feature (painted eyes, panel line) is visible; frame the area; screenshot the start state. Then hand off with **exact beginner steps**. +2. **Operator drives (GUI):** they nudge/scale/rotate onto the target while watching full-framerate. Tell them which axis to ignore when tooling can fix it ("place it in this front view — don't worry about depth, I'll snap it"). They can also request changes ("make it bigger", "add another", "stage the next piece"). Wait for "done". +3. **I bake back (headless):** read the objects' `matrix_world`; snap the hard-to-see axis with a **raycast or shrinkwrap** to the surface; restore the real palette/emissive material; **parent to the driving bone** (`CHILD_OF`, `inverse_matrix=(arm.matrix_world @ pb[bone].matrix).inverted()`) so it rides the rig; and **express the placement as a bone-local offset** (`(arm.matrix_world @ pb[bone].matrix).inverted() @ world_pos`) — the reusable value so the NEXT asset on that rig starts correct with no re-nudge. Symmetrize L/R for the default. Save. Confirm with a render. + +Code for every step → `references/blender-cookbook.md`. Alternatives when nudging is awkward: the **empty-marker handshake** (operator drops empties, I snap geo to them) and **shrinkwrap** (auto-conform to a surface). + +## Beginner-coach rules + +- On any handoff, give a **numbered, literal** step list: how to select (left-click; Shift-click to add), grab (`G`, move mouse, left-click to confirm, `Esc`/right-click to cancel), constrain to an axis (`G` then `X`/`Y`/`Z`), scale (`S`), rotate (`R`), orbit (middle-mouse drag / numpad), zoom (scroll), frame selected (`Numpad .`), and **save** (`Ctrl S`) — only the ones they need for that step. +- Explain the **why / best practice** briefly (flat shading for the low-poly look; UVs onto a band not unwrapped; apply transforms before export; keep under the tri budget; one shared material). +- Prefer letting me do the risky/fiddly Blender ops (mode switches, modifiers, export settings) headless; hand the operator only intuitive direct-manipulation. +- If an operator request would break a non-negotiable (e.g. "just paint a texture on it"), say so and offer the palette-correct way. + +## Operator gates (same as the project standard) +Present forks don't auto-decide · no time/effort estimates · never silently defer/descope · tuning-defaults autonomy (sensible defaults + let them tune) · aggressive-cleanup only on explicit opt-in. + +## Boundaries +**Stop and consult:** intake too thin · goal contradicts the style bible / a locked decision · a silhouette/identity call with no clear best answer · a precision step that needs the operator's eyes · a destructive op (overwrite/delete an existing `.blend` or Synty source) outside the agreed change · Unity mutation while another agent may hold the editor (stay read-only). +**May loop without consulting (≤3):** setup/export/render retries · an axis-probe → pose fix → re-render · a black-viewport `tag_redraw` re-capture. +**Never:** overwrite a Synty pack source (GUID refs) — new files under `ArtSource/`/`Assets/_Project/Art/`; per-asset texturing / unwrapping; write to `Assets/` during a live Unity session; hardcode a machine path into the committed skill. + +## References (load on demand) +- [`references/blender-cookbook.md`](references/blender-cookbook.md) — copy-paste-ready blendermcp code: scene/lights/camera setup, palette + emissive materials, atlas band map + cell-UV, faceted-primitive + UV-placement helper, the 5.1 Synty-FBX import workaround, normalize scale/feet, pose-bone axis-probe + set, the full nudge-and-bake stage + bake-back (raycast depth-snap, Child-Of parent, bone-local offset), shrinkwrap, high-res render-to-file (+ black-viewport fix), export (glb/FBX) + save/pack. +- [`references/style-and-practices.md`](references/style-and-practices.md) — the binding style essentials (palette ontology, readability law, budgets, the 3 shader roles), the palette atlas band layout, the rig families, and the Blender-beginner cheat-sheet + best-practice do/don't. diff --git a/.claude/skills/art-dev/references/blender-cookbook.md b/.claude/skills/art-dev/references/blender-cookbook.md new file mode 100644 index 000000000..9d9fa45e4 --- /dev/null +++ b/.claude/skills/art-dev/references/blender-cookbook.md @@ -0,0 +1,226 @@ +# Blender cookbook (blendermcp, Blender 5.1) — LANTERN art + +Copy-paste-ready snippets, all verified this-machine via `mcp__blender__execute_blender_code`. **Every runtime script sets `PROJ` first** (Blender runs outside the harness — resolve the absolute repo root at runtime; do NOT commit a machine path into this file's *usage*, the `<...>` below is a placeholder): + +```python +PROJ = r"" +ATLAS = PROJ + r"\Assets\_Project\Art\Palette\PaletteAtlas.png" +SRC = PROJ + r"\ArtSource\Blender" # per-asset master .blend + exports live here +``` + +General gotchas: `get_scene_info`/`get_viewport_screenshot` need a `user_prompt` arg. `Date.now`/`random` are fine in Blender Python (unlike Workflow scripts) — seed `random` for reproducibility. Blender 5.1 = EEVEE Next; `action.fcurves` is gone (slotted actions). + +## 1. Scene setup (units + murk world + lights + camera + view transform) + +```python +import bpy +sc = bpy.context.scene +sc.unit_settings.system='METRIC'; sc.unit_settings.scale_length=1.0; sc.unit_settings.length_unit='METERS' +try: sc.render.engine='BLENDER_EEVEE_NEXT' +except Exception: sc.render.engine='BLENDER_EEVEE' +sc.view_settings.view_transform='Standard'; sc.view_settings.look='None' # judge HUE truly; AgX whitens bright emission +# murk world (deep cold blue, not black) +w = sc.world or bpy.data.worlds.new("World"); sc.world=w; w.use_nodes=True +bg=w.node_tree.nodes.get("Background"); bg.inputs[0].default_value=(0.015,0.03,0.05,1.0); bg.inputs[1].default_value=0.3 +# warm KEY (gold = our light) + faint cold GLOAM fill (the deep) +def area_light(name, loc, color, energy, size=1.3): + ld=bpy.data.lights.new(name,'AREA'); ld.size=size; ld.color=color; ld.energy=energy + ob=bpy.data.objects.new(name,ld); ob.location=loc; sc.collection.objects.link(ob); return ob +area_light("KeyWarm",(1.9,-2.2,2.4),(1.0,0.75,0.45),220) +area_light("GloamFill",(-2.0,1.8,1.5),(0.22,0.55,0.8),120) +``` +- A default startup scene has objects `Cube`/`Light`/`Camera`. `bpy.ops.wm.read_homefile()` (NOT `use_empty=True` — an empty scene breaks the FBX importer's context). Delete `Cube` after. +- For a self-emissive subject (wisp/flora), drop key energy (~30–80) + world (~0.25) so the emission reads as the light source. + +## 2. Palette + emissive materials (the ONLY materials — color by UV placement) + +```python +def palette_mat(name="M_Palette_Atlas"): + m=bpy.data.materials.get(name) + if m: return m + m=bpy.data.materials.new(name); m.use_nodes=True; nt=m.node_tree; nt.nodes.clear() + o=nt.nodes.new("ShaderNodeOutputMaterial"); o.location=(400,0) + b=nt.nodes.new("ShaderNodeBsdfPrincipled"); b.location=(120,0); b.inputs["Roughness"].default_value=0.65 + t=nt.nodes.new("ShaderNodeTexImage"); t.location=(-260,0) + t.image=bpy.data.images.load(ATLAS, check_existing=True); t.interpolation='Closest' # crisp flat bands + nt.links.new(t.outputs["Color"],b.inputs["Base Color"]); nt.links.new(b.outputs["BSDF"],o.inputs["Surface"]) + return m +def emissive(name, color, strength): # warm(1.0,0.66,0.28) true | gloam(0.1,0.85,0.34) false | teal(0.12,0.62,0.85) ambient + m=bpy.data.materials.get(name) + if m: return m + m=bpy.data.materials.new(name); m.use_nodes=True; nt=m.node_tree; nt.nodes.clear() + o=nt.nodes.new("ShaderNodeOutputMaterial"); e=nt.nodes.new("ShaderNodeEmission") + e.inputs["Color"].default_value=(*color,1.0); e.inputs["Strength"].default_value=strength + nt.links.new(e.outputs["Emission"],o.inputs["Surface"]); return m +``` +⚠ Emission **strength 4–6 + AgX** clips to white and desaturates. On `Standard`, keep strength ~1.5–3 and use a saturated color (one channel high, others low) so it doesn't clip to white/cyan. URP bloom supplies the glow halo in-engine (Blender preview has none). + +## 3. Palette atlas band map + cell-UV (row 0 = BOTTOM) + +The atlas is an 8×8 grid; each face samples ONE flat cell. Cell centre UV: +```python +def cell(cx, cy): return ((cx+0.5)/8.0, (cy+0.5)/8.0) # cx,cy in 0..7 +``` +| Row (from bottom) | Band | Use | +|---|---|---| +| 6 | pure grey ramp (col0 dark → col7 light) | neutral hard-surface | +| 5 | warm gold/amber → cream | **true light / ours** | +| 4 | dark → bright red | lure-red / bait | +| 3 | dark navy → bright blue | **gloam (cold)** | +| 2 | dark → bright corpse-green | **gloam (cold)** | +| 0–1 | warm brown/rust → amber/brass | warm metal | +Handy picks: grey body `cell(3,6)`, brass trim `cell(5,0)`, steel `cell(2,6)`, dark-gloam accent `cell(1,2)`. + +## 4. Faceted primitive + UV-placement helper + +```python +import bmesh +def new_obj(name, bm, mat, uv, loc=(0,0,0), rot=None): + me=bpy.data.meshes.new(name); ob=bpy.data.objects.new(name,me); bpy.context.collection.objects.link(ob) + L=bm.loops.layers.uv.verify() + for f in bm.faces: + f.smooth=False # FLAT / faceted shading (the style) + for l in f.loops: l[L].uv=uv # every loop -> one cell centre = flat colour + bm.to_mesh(me); bm.free(); me.materials.append(mat); ob.location=loc + if rot: ob.rotation_euler=rot + return ob +# examples +bm=bmesh.new(); bmesh.ops.create_cube(bm,size=1.0) # box +bm=bmesh.new(); bmesh.ops.create_icosphere(bm,subdivisions=1,radius=0.15) # faceted orb (20 faces) +bm=bmesh.new(); bmesh.ops.create_cone(bm,cap_ends=True,segments=8,radius1=0.08,radius2=0.0,depth=0.3) # taper/cylinder +bmesh.ops.bevel(bm,geom=list(bm.edges)+list(bm.verts),offset=0.045,segments=1,affect='EDGES',clamp_overlap=True) +bmesh.ops.inset_individual(bm,faces=[f for f in bm.faces if max(abs(c) for c in f.normal)>0.95],thickness=0.075,depth=-0.028) # recessed panels +``` +Report tris: `me.calc_loop_triangles(); len(me.loop_triangles)`. + +## 5. Synty character FBX import (Blender 5.1 workaround) + +Plain `import_scene.fbx` on a combined Synty character FBX FAILS on 5.1 (`mode_set('EDIT') Context missing active object`, then `pose.bones['Root'] KeyError`). Fix = window-override + these flags; do NOT force `object=`/`active_object=` (fights the importer's armature activation → the 'Root' error): +```python +bpy.ops.wm.read_homefile() # need a valid active object for the importer +fbx = PROJ + r"\Assets\Synty\PolygonSciFiSpace\Models\Characters.fbx" +win=bpy.context.window_manager.windows[0]; scr=win.screen +area=next(a for a in scr.areas if a.type=='VIEW_3D'); region=next(r for r in area.regions if r.type=='WINDOW') +with bpy.context.temp_override(window=win, screen=scr, area=area, region=region): + bpy.ops.import_scene.fbx(filepath=fbx, ignore_leaf_bones=True, automatic_bone_orientation=False) +``` +Bodies live INSIDE the combined FBX (20 chars on one rig): `SM_Chr_SpaceSoldier_Male_01` (armored suit base), `SM_Chr_Crew_Male_01`/`_Junker_Male_01` (lean → drowner/enemy bases), `SM_Chr_..._Armour_01` (plate shell), EVA dome parts are separate FBX. Cull the 19 you don't want; keep the one body + the `Armature`. We are **licensed to modify Synty** — but never overwrite the pack FBX (GUID refs). + +Synty bone names: `Root, Hips, Spine_01/02/03, Neck, Head, Eyes, Clavicle_L/R, Shoulder_L/R (upper arm), Elbow_L/R, Hand_L/R, UpperLeg/LowerLeg/Ankle/Ball _L/R`. + +## 6. Normalize scale + feet-at-floor, apply for export + +```python +from mathutils import Vector +def world_bounds(objs): + mn=Vector((1e9,)*3); mx=Vector((-1e9,)*3) + for ob in objs: + for c in ob.bound_box: + wv=ob.matrix_world@Vector(c); mn=Vector((min(mn[i],wv[i]) for i in range(3))); mx=Vector((max(mx[i],wv[i]) for i in range(3))) + return mn,mx +mn,mx=world_bounds(body_parts); root=arm or body_parts[0] +s=1.8/(mx.z-mn.z); root.scale=[v*s for v in root.scale]; bpy.context.view_layer.update() +mn,mx=world_bounds(body_parts); root.location.z-=mn.z # drop feet to z=0 +# before export of a static: origin to geometry, apply transforms +bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') +bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) +``` + +## 7. Pose bones (rough pose headless; polish is Guided) + +Set `pose.bones[...].rotation_euler` directly in object mode (no mode switch); `view_layer.update()` to evaluate. **Probe an unknown axis** before committing: rotate +1.0 rad on each local axis, read a child bone's world pos. +```python +arm=next(o for o in bpy.data.objects if o.type=='ARMATURE'); pb=arm.pose.bones +for b in pb: b.rotation_mode='XYZ'; b.rotation_euler=(0,0,0) +def child_w(n): bpy.context.view_layer.update(); return arm.matrix_world @ pb[n].head +``` +**Verified Synty-humanoid facts (this rig):** arm-DOWN (T-pose→hang) = **−Z** on `Shoulder_*`; forward-hunch = **−X** on the `Spine_*` chain + `Neck`/`Head`. Z is symmetric L/R; **X and Y MIRROR** — negate them for `*_L` (e.g. `Shoulder_L=(0, +y, -z)`, `Shoulder_R=(0, -y, -z)`). Hide the bone overlay for shots: `arm.hide_set(True)`. + +## 8. The nudge-and-bake loop + +**STAGE (I run):** mark + isolate + float + select + flat-shade + frame. +```python +mk=emissive("M_Marker",(1.0,0.0,0.85),4.0) # unmistakable magenta +head=arm.matrix_world @ pb["Head"].head +for nm,sx in (("SM_X_Eye_L",-0.05),("SM_X_Eye_R",0.05)): + e=bpy.data.objects[nm] + e.location=(head.x+sx, head.y-0.16, head.z+0.03) # float clearly IN FRONT of the face + e.data.materials.clear(); e.data.materials.append(mk) +for a in bpy.context.screen.areas: + if a.type=='VIEW_3D': + sp=a.spaces.active; sp.shading.type='MATERIAL'; sp.shading.use_scene_lights=False; sp.shading.use_scene_world=False + r=next(rg for rg in a.regions if rg.type=='WINDOW') + with bpy.context.temp_override(area=a,region=r): bpy.ops.view3d.view_axis(type='FRONT') + sp.region_3d.view_location=(head.x,head.y,head.z+0.03); sp.region_3d.view_distance=0.75 +for ob in bpy.data.objects: ob.select_set(False) +eL=bpy.data.objects["SM_X_Eye_L"]; eR=bpy.data.objects["SM_X_Eye_R"] +eL.select_set(True); eR.select_set(True); bpy.context.view_layer.objects.active=eL +``` +Then give the operator literal steps (select, `G`, orbit, "don't worry about depth"). Wait for "done". + +**BAKE BACK (I run):** read placement → raycast depth-snap → restore material → parent to bone → reusable offset. +```python +from mathutils import Vector +dg=bpy.context.evaluated_depsgraph_get(); head=arm.matrix_world @ pb["Head"].head +GLO=emissive("M_Emissive_Gloam",(0.15,0.9,0.5),2.5) +hbmat=arm.matrix_world @ pb["Head"].matrix; hinv=hbmat.inverted() +for nm in ("SM_X_Eye_L","SM_X_Eye_R"): + e=bpy.data.objects[nm]; loc=e.matrix_world.translation.copy() + hit,hl,hn,idx,obj,mtx=bpy.context.scene.ray_cast(dg, Vector((loc.x,head.y-0.4,loc.z)), Vector((0,1,0))) + if hit: e.location=(loc.x, hl.y-0.010, loc.z) # snap depth to the face surface, keep operator X/Z + e.data.materials.clear(); e.data.materials.append(GLO) + for c in list(e.constraints): + if c.type=='CHILD_OF': e.constraints.remove(c) + c=e.constraints.new('CHILD_OF'); c.target=arm; c.subtarget="Head"; c.inverse_matrix=hinv # rides the rig + print(nm, "head-local offset:", tuple(round(v,3) for v in (hinv @ e.matrix_world.translation))) # REUSABLE +``` +The printed **head-local offset** is the bake — record it (here + the asset's build step) so the next creature on this rig gets eyes auto-placed. Symmetrize L/R (±avg x, avg y/z) for the default. If a raycast misses (piece outside the mesh silhouette), keep the operator depth. + +Alternatives: **empty-marker handshake** (operator drops empties named `eye.L`/`eye.R`; snap: `e.location = bpy.data.objects['eye.L'].location`). **Shrinkwrap** (`m=e.modifiers.new('sw','SHRINKWRAP'); m.target=body; m.wrap_method='PROJECT'`) to auto-conform to a surface — note it evaluates the target's geometry, verify on a posed/skinned mesh. + +## 9. Verify — high-res render-to-file (beats the flaky viewport grab) + +```python +import tempfile, os +sc=bpy.context.scene; sc.render.resolution_x=1000; sc.render.resolution_y=1000 +for eng in ('BLENDER_EEVEE_NEXT','BLENDER_EEVEE'): # ⚠ on THIS 5.1 build the enum is 'BLENDER_EEVEE' (not _NEXT) + try: sc.render.engine=eng; break + except Exception: continue +sc.render.filepath = os.path.join(tempfile.gettempdir(), "artdev_preview.png") # OS temp — NEVER Library/Temp/Assets or any committed path +# ensure a camera exists + is active; frame it, then: +bpy.ops.render.render(write_still=True) +``` +Then `Read` the PNG. The ~800px `get_viewport_screenshot` is a quick check but returns **BLACK/garbled when the window isn't drawing** — `area.tag_redraw()` + `view3d.view_selected` then re-capture, or ask the operator to focus Blender. (Reason + more: [[mcp-screenshot-and-pose-validation]].) + +### ★ Verify from the GAME CAMERA too, not just a hero 3/4 +This is a **top-down ARPG** — an asset that reads in a hero shot can be invisible/wrong from the gameplay angle. **Always render from the game-camera angle** and judge there. Real rig values (`Assets/_Project/Scripts/Client/Presentation/PrototypeCameraRig.cs`): **Pitch 45° · Yaw 45° (default) · Distance 13 m · TargetHeight 1 m · FOV 55°**. Render at that *angle* but framed tighter (dist ~3–3.5) to see the asset, and check **both facings** — the camera sees the character's front when they move toward it and their **back when they move away** (so back-mounted kit reads too). +```python +import math; from mathutils import Vector +def make_cam(name,fov): + c=bpy.data.cameras.get(name) or bpy.data.cameras.new(name); c.lens_unit='FOV'; c.angle=math.radians(fov) + o=bpy.data.objects.get(name) or bpy.data.objects.new(name,c) + if o.name not in bpy.context.collection.objects: bpy.context.collection.objects.link(o) + o.data=c; return o +def aim(o,frm,tgt): o.location=Vector(frm); o.rotation_euler=(Vector(tgt)-Vector(frm)).normalized().to_track_quat('-Z','Y').to_euler() +def opos(tgt,pitch,yaw,dist): # game-cam geometry: pitch DOWN from horizontal, yaw around Z + t=Vector(tgt); p=math.radians(pitch); y=math.radians(yaw) + return t+(Vector((math.sin(y),-math.cos(y),0))*math.cos(p)+Vector((0,0,1))*math.sin(p))*dist +gf=make_cam("GameFront",55); aim(gf, opos((0,0,1.15),45,15,3.2),(0,0,1.15)) # facing toward +gr=make_cam("GameRear",55); aim(gr, opos((0,0,1.15),45,195,3.2),(0,0,1.15)) # facing away (back-kit) +# sc.camera = gf; render... then sc.camera = gr; render... +``` +**The design consequence (learned on the Bathynaut):** from 45° top-down you see the **dome crown, shoulders, back-pack tops** — the **face/porthole is nearly invisible in-game.** Put detail + emissive accents on **top-facing surfaces** (helmet crown, shoulder lamp, tank tops); treat the face as a hero/close-up-only detail. Budget effort by where the camera actually looks. + +## 10. Export + save + +```python +import os; os.makedirs(SRC, exist_ok=True) +try: bpy.ops.file.pack_all() # self-contained .blend (a Synty .psd pack-warn is harmless) +except Exception as e: print("pack warn:", e) +bpy.ops.wm.save_as_mainfile(filepath=SRC + r"\_.blend") +# static export (glTF; metric scene = correct scale) +bpy.ops.export_scene.gltf(filepath=SRC + r"\SM_.glb", export_format='GLB', + use_selection=True, export_apply=True, export_yup=True) +``` +Skinned/animation export → the per-action FBX recipe in [[blender-mcp-and-unity-mcp-v10]] (`bake_anim_use_all_bones`, `bake_anim_force_startend_keying`, `apply_scale_options='FBX_SCALE_UNITS'`, `add_leaf_bones=False`; Unity import `CreateFromThisModel`). **Never export into `Assets/` while a Unity session may be live** — write to `ArtSource/`, bake in later. +``` diff --git a/.claude/skills/art-dev/references/style-and-practices.md b/.claude/skills/art-dev/references/style-and-practices.md new file mode 100644 index 000000000..754f96b8a --- /dev/null +++ b/.claude/skills/art-dev/references/style-and-practices.md @@ -0,0 +1,57 @@ +# Style essentials + Blender best-practices (LANTERN art-dev) + +The **binding** style lives in [[Art_Direction_Lantern]] (constitution) + [[Lantern_Art_Pipeline_Recipes]] (manual). This file is the *operative subset* to hold in-head during a session, plus the **Blender-beginner** coaching the operator needs. When this and the art docs disagree, the docs win. + +## The five rules that shape every asset + +1. **One material, color by UV placement.** Never texture/unwrap per asset. Model → *place* each face's UV onto a palette-atlas colour band (`Closest`-filtered) → flat colour. Three shader roles only: **Lit-Palette** (statics), **Emissive-Gloam** (glows), **Skinned-Palette** (creatures/suits, deformation-aware). Batching + coherence + zero texture skill = the solo-dev factory. +2. **Faceted low-poly, silhouette-first.** Flat shading (`face.smooth=False`). The read is the *shape* + *light*, not detail — "a readable blob with a great light beats a detailed model nobody can see." +3. **Budgets (hard):** props ≤ ~800 tris · creatures ≤ ~3k · hero/boss/suit-frame ≤ ~6k. Report tris every time; over-budget → decimate or drop parts (e.g. body OR armour, not both). +4. **The palette IS the ontology:** warm gold/amber = **true light (ours)** · cold corpse-green/blue = **gloam (the deep's counterfeit)** · lure-red = **bait** · white-hot = **conversion**. Pick an asset's emissive hue for what it *is* (diver beacon = warm; wisp/gloam-enemy = cold green; ambient flora = cold teal; lure = red). +5. **The readability law:** value-first (carry every warm/cold read by luminance + shape + **flicker-cadence**, not hue alone — steady = true, flicker = false; this is the colorblind answer). Reads at **200 px**. A **capture grade** (lifted/saturated) for shots; the murk grade is darker. + +## Rig families (skinned assets — clips are made per FAMILY, not per creature) + +| Family | Body plan | Members | Base to kitbash | +|---|---|---|---| +| **Drifter** | spine-chain swimmer | Wisp-Choir motes, lure-fish, serpents | procedural blobs + tail beads (no humanoid rig) | +| **Crawler** | legged/scuttling | Grindylow, kappa-kin, Tenders | (quadruped — separate rig) | +| **Strider** | biped-ish | Drowner, Wrecker, **suit-frames** | Synty humanoid (`SM_Chr_*` in the combined FBX) | + +Suit-frames = kitbash on the Synty humanoid (helmet/tank/plate swaps over the proven rig); a custom silhouette sculpt is a later upgrade, not a blocker. + +## Assets built so far (in `/ArtSource/Blender/`, the style-proof set) + +`Prop_CargoCrate` (92 tris, statics/palette proof) · `Prop_GlowFlora` (380, teal ambient) · `Creature_WispMote` (110, gloam-green Drifter) · `Creature_Drowner` (2742, Synty-crew Strider kitbash — gloam skin, hunched, bone-parented glowing eyes) · `Suit_Bathynaut_Kitbash` (8.4k ⚠ over hero budget — body+armour+kit; game-ready pass must pick body-or-armour + decimate). Concepts: suit kit + wisp not rig-bound; drowner posed not animated. Pending: the in-engine Unity bake + the 3 master ShaderGraphs (esp. Emissive-Gloam's flicker param). + +## Coaching a Blender beginner (the operator) + +On **every** handoff, give a short **numbered, literal** step list — only the keys they need for that step — and one line on *why* (the best practice). The vocabulary: + +| Do this | How (tell them exactly) | +|---|---| +| Orbit the view | **middle-mouse drag** (or numpad `4/6/8/2`) | +| Zoom | **scroll** (or `Ctrl`+MMB drag) | +| Pan | **Shift**+MMB drag | +| Frame selected | **`Numpad .`** (or View ▸ Frame Selected) | +| Front / side / top view | **`Numpad 1` / `3` / `7`** | +| Select an object | **left-click** it; **Shift**+click to add to selection | +| Move (grab) | **`G`**, move mouse, **left-click** to confirm, **`Esc`/right-click** to cancel | +| Move on one axis | **`G` then `X`/`Y`/`Z`** (e.g. `G Z` = up/down only) | +| Scale / Rotate | **`S`** / **`R`** (same confirm/cancel; add an axis letter to constrain) | +| Save | **`Ctrl S`** | + +- Hand the operator only **intuitive direct-manipulation** (grab/scale/rotate a selected object onto a target). Keep the fiddly/risky ops (mode switches, modifiers, bmesh, export settings, materials) **headless** on my side. +- When one axis is hard to see in a single view (depth), tell them to ignore it — "place it in this front view, I'll snap the depth after" (raycast/shrinkwrap). +- Explain the *why* briefly so they learn: flat shading = the low-poly look; UVs onto a band (not unwrapped) = the shared-material rule; apply transforms before export = correct scale in Unity; under the tri budget = it stays cheap in the dark. +- If a request would break a rule (e.g. "paint a texture", "make it super smooth/high-poly"), say so plainly and offer the palette-correct / budget-correct way. + +## Best-practice do / don't + +**Do:** faceted flat shading · UVs placed on atlas cells · metric units, 1 u = 1 m · feet at z=0, origin sensible · apply rotation/scale before export · pack textures before saving the `.blend` · one shared material per asset · report tris vs budget · verify with a hi-res render under murk, judging silhouette + value + 200px · **also render from the game camera (top-down ARPG, Pitch 45°/FOV 55°), both facings — put detail + emissive accents on TOP-facing surfaces (crown/shoulders/back-pack), not the face the camera never sees** · keep each asset a separate master `.blend` under `ArtSource/Blender/`. + +**Don't:** per-asset texture/unwrap · smooth-shade the low-poly forms · exceed the tri budget silently (log the upgrade if you ship a placeholder) · overwrite a Synty pack FBX (GUID refs — new files only) · write into `Assets/` while a Unity session may be live · trust a dark screenshot (check material *values*) · trust `WorldRenderBounds`/bone reads for a *skinned/posed* mesh (they return the bind AABB — use the operator's eyes) · leave emission at strength 4–6 on AgX (whites out — Standard + saturated color, ~1.5–3). + +## The pipeline into Unity (hand off to /dots-dev) + +Statics: model → export `.glb` → collider policy (cosmetic = strip colliders/no ghost; blocking = `Environment` collider baked into the gameplay subscene, mesh Read/Write on) → assign the shared `M_Lit_Palette` → bake into subscene → verify baked CollisionWorld + a Play screenshot. Skinned: Humanoid rig → per-action clips (Key All Bones, bake-to-pose) → FBX → Rukhanka ingest (`CreateFromThisModel`) → `Skinned-Palette` → prefab via `EnemyRigTools` (ownerless interpolated ghost; server strips Rukhanka). Full checklists + gotchas → [[Lantern_Art_Pipeline_Recipes]] + `CLAUDE.md` (Animation / Art-import sections). diff --git a/Docs/Vault/07_Sessions/2026/2026-07-14_B_Art_Track_Kickoff_ArtDev_Skill.md b/Docs/Vault/07_Sessions/2026/2026-07-14_B_Art_Track_Kickoff_ArtDev_Skill.md new file mode 100644 index 000000000..a8d229bec --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-07-14_B_Art_Track_Kickoff_ArtDev_Skill.md @@ -0,0 +1,43 @@ +--- +date: 2026-07-14 +type: session +tags: [session, lantern, art, workstream-a, blender, skill, a0, kitbash] +permalink: gamevault/07-sessions/2026/2026-07-14-b-art-track-kickoff-artdev-skill +--- + +# Session 2026-07-14 (B) — Art track kickoff: A0 style-proof assets + the `/art-dev` skill + +> First real Workstream-A sitting. Operator wanted to explore the art side in Blender (parallel agent held Unity → read-only there). Built the A0 style-proof asset set, adopted a hybrid Blender workflow, and productized the whole thing as a reusable skill. Then refined the suit and (next) started the in-engine bake once the editor freed up. + +## Done + +**A0 status grounded:** written constitution ([[Art_Direction_Lantern]]) + recipes ([[Lantern_Art_Pipeline_Recipes]]) + the material foundation (`PaletteAtlas.png` + 3 shared materials) already existed; the **in-engine style-proof scene gate was still open**. Blender was an empty default scene. + +**Built the A0 style-proof asset set** (all in `ArtSource/Blender/`, a NEW repo-level source-art home OUTSIDE `Assets/` so a live Unity session never auto-imports mid-work; textures packed): +- `Prop_CargoCrate` (92 tris) — proves the material keystone: ONE palette-atlas material, colour by *placed* UV (Imphenzia), zero per-asset texturing. Also exported `SM_Prop_CargoCrate.glb`. +- `Prop_GlowFlora` (380) — bioluminescent anemone clump, cold **teal** emissive (ambient gloam clutter). +- `Creature_WispMote` (110) — Drifter will-o'-the-wisp: gloam-green emissive core + dark petal crown + curved shrinking tail trail. +- `Creature_Drowner` (2742) — **Synty-crew Strider kitbash**: gloam-tinted drowned flesh, hunched shamble pose on the rig, cold glowing eyes (operator-placed via nudge-and-bake, bone-parented), kelp strands. +- `Suit_Bathynaut_Kitbash` (~9k ⚠ over the ≤6k hero budget) — Synty SpaceSoldier body+armour + custom brass diving-bell dome, back tank pack, chest beacon. Refined this session (below). + +**Palette ontology demonstrated across the set in 3 light-flavors:** warm gold = true/ours (diver beacon/porthole/shoulder lamp) · cold green = false/gloam (wisp core, drowner eyes) · cold teal = ambient clutter (flora). + +**Bathynaut refinement pass** (via the new skill): porthole rebuilt as ONE dome-*seated* bezel assembly (open bezel → amber glass + grille + rivets visible; dropped the floating brow) → cohesion fixed; tank pack **rounded out** (domed caps, curved yoke, regulator, backplate, warm valve tops); **shoulder dive-lamp** seated on the outer deltoid pauldron (raycast-to-surface) + parented to `Clavicle_L`. + +**Productized as the `/art-dev` skill** (`.claude/skills/art-dev/`): SKILL.md (process spine — size by complexity → ground → agree → setup → build → verify → export → Unity-bake handoff → doc bookend → commit) + `references/blender-cookbook.md` (all battle-tested blendermcp code) + `references/style-and-practices.md` (style essentials + Blender-beginner cheat-sheet). Does simple changes headless (Auto); drops into the hybrid **nudge-and-bake** loop for precision/aesthetics. + +## Decisions / notes + +- **Hybrid nudge-and-bake workflow adopted** (operator's call): headless is great at bulk, bad at precision/taste; the operator (a Blender **beginner**) has a live GUI → I stage+isolate+select, they nudge, I bake back (raycast/shrinkwrap depth-snap → restore material → Child-Of bone-parent → **reusable bone-local offset**). Proven end-to-end fixing the Drowner eyes. Native memory [[blender-hybrid-nudge-and-bake]]. +- **★ Verify from the GAME CAMERA, not just a hero 3/4** (operator's insight, now a skill Phase-5 rule): this is a top-down ARPG (rig = Pitch 45° / Dist 13 / FOV 55, `PrototypeCameraRig.cs`). Finding: from the gameplay angle you see the **dome crown + shoulders + back-pack**; the **face/porthole is near-invisible in-game** → put detail + emissive accents on TOP-facing surfaces; the face is hero/close-up only. Check both facings (front + back-away). +- **Blender 5.1 gotchas** (→ [[blender-mcp-and-unity-mcp-v10]]): combined Synty character FBX import needs a window-override + `ignore_leaf_bones=True, automatic_bone_orientation=False`; the render-engine enum is `BLENDER_EEVEE` (not `_NEXT`); Synty faces have modeled eye-sockets + a shared humanoid rig (`Root/Hips/Spine_*/Neck/Head/Clavicle/Shoulder/Elbow/Hand/...`); pose axes on this rig: arm-down = `−Z` on Shoulder, forward-hunch = `−X` on Spine, X/Y mirror L/R, Z symmetric. +- Suit/creature kit are **silhouette concepts** — not rig-bound for animation yet (Drowner posed, eyes/shoulder-lamp bone-parented as a start); the Bathynaut is over hero-budget (game-ready pass decimates + picks body-or-armour). + +## Open / deferred (surfaced, NOT silent) + +- **In-engine verification STARTED this session** (operator lifted the read-only constraint + granted full editor freedom for the art-verification portion): bake the statics (crate, flora) → shared palette materials → gameplay subscene → judge under murk + URP bloom + the game camera (full / 200px / grayscale) = the real A0 gate. Skinned bake (suit/creatures via Rukhanka + Skinned-Palette) is the harder follow-on. +- **3 master ShaderGraphs** (esp. Emissive-Gloam's flicker-cadence param) still unauthored — needs Unity. +- The Bathynaut wears 3 warm lights (porthole+chest+shoulder) — on-ontology but could dial the chest lamp down. + +## Related +- [[Art_Direction_Lantern]] · [[Lantern_Art_Pipeline_Recipes]] · [[Roadmap_Lantern_Slice]] (Workstream A) · [[blender-hybrid-nudge-and-bake]] · [[blender-mcp-and-unity-mcp-v10]]