Docs: art-dev skill - AI-generate->conform-to-Synty recipe (cookbook 14)

Bank the Mark-V lessons: when scripted primitives can't nail an iconic organic shape, generate with
Hyper3D Rodin then CONFORM - watertight clean-decimate (weld+recalc first, gentle collapse, 0 boundary
edges = hole detector), drop the baked photoreal texture for the shared flat palette, flat-shade,
ontology-tie emissives, and don't skin a meter-scale generated mesh onto a cm-scale rig (~100x shrink).
Added the generate path to the Build phase + a keystone clause (strip a kitbash/gen mesh's own texture).
Also fixed a duplicate section number (12 -> 13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 14:28:16 -07:00
parent be55683f11
commit c78768cbdf
2 changed files with 47 additions and 2 deletions
+2 -1
View File
@@ -11,7 +11,7 @@ The spine: **size by complexity → ground → agree the change → setup scene
## Non-negotiables (every track) ## 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`. 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. **A kitbashed or AI-generated mesh arrives with its OWN baked texture — that clash IS the keystone violation; strip it and re-material to the shared palette + flat-shade before it ships** (§14). 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. 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*. 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]]. 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]].
@@ -55,6 +55,7 @@ Establish the LANTERN working scene (snippets → `references/blender-cookbook.m
### Phase 4 — Build ### 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. - **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.
- **AI-generate an iconic shape (Auto, last resort):** when scripted primitives can't nail an *iconic organic hard-surface* form (a Mark-V helmet, an ornate horn) and hand attempts read as golf-ball/egg/blob, **generate the base with Hyper3D Rodin** then **conform it** — it arrives as raw photoreal high-poly meter-scale clay that clashes with the flat low-poly body. The conform pass IS the work: clean-decimate to budget (watertight — weld+recalc FIRST, gentle collapse, 0 boundary edges), **drop the baked texture for the shared palette**, flat-shade, ontology-tie any emissive, and (rigid accessory) don't skin a generated mesh onto a cm-scale rig (~100× shrink → invisible). Full recipe → `references/blender-cookbook.md` §14.
- **Precision / pose / silhouette (Guided) → the nudge-and-bake loop** (next section). - **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. - 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.
@@ -268,7 +268,7 @@ The direction that landed (operator-approved): **the interest lives in the LIGHT
To drop a posed suit/creature into the staging scene as a static mesh (skinned Rukhanka bake is separate `/dots-dev` work): in Blender — **apply the `Armature` modifier** on each skinned mesh (bakes the current pose into geometry), **`visual_transform_apply` + clear constraints** on bone-parented kit (eyes/lamp Child-Of), then export a static glb (meshes only, no armature). ⚠ both `modifier_apply` and `export_scene.gltf` need the **window `temp_override`** after `open_mainfile` (context.active_object). Import via `import_model_file`, `InstantiatePrefab` at the slot — glTFast carries the embedded materials (Synty atlas + emission). To drop a posed suit/creature into the staging scene as a static mesh (skinned Rukhanka bake is separate `/dots-dev` work): in Blender — **apply the `Armature` modifier** on each skinned mesh (bakes the current pose into geometry), **`visual_transform_apply` + clear constraints** on bone-parented kit (eyes/lamp Child-Of), then export a static glb (meshes only, no armature). ⚠ both `modifier_apply` and `export_scene.gltf` need the **window `temp_override`** after `open_mainfile` (context.active_object). Import via `import_model_file`, `InstantiatePrefab` at the slot — glTFast carries the embedded materials (Synty atlas + emission).
``` ```
## 12. Skinned attachment kit → existing Unity rig (proven 07-16, Bathynaut dome/tank/lamp) ## 13. Skinned attachment kit → existing Unity rig (proven 07-16, Bathynaut dome/tank/lamp)
Rigid accessories that must RIDE an already-in-engine Rukhanka rig (helmet, packs, lamps). Full failure-chain + Unity-side detail: gotchas archive 2026-07-16 + [[DR-052_SoD_Facing_Underwater_Feel]]. Rigid accessories that must RIDE an already-in-engine Rukhanka rig (helmet, packs, lamps). Full failure-chain + Unity-side detail: gotchas archive 2026-07-16 + [[DR-052_SoD_Facing_Underwater_Feel]].
@@ -291,3 +291,47 @@ Unity side (`PlayerRigTools.AttachBathynautKit` / `GraftSmr` is the reference im
- **`mesh.RecalculateTangents()` is mandatory** — a tangent-less procedural skinned mesh fails Rukhanka/BRG registration (`BatchMeshID not present`) and the WHOLE rig disappears. - **`mesh.RecalculateTangents()` is mandatory** — a tangent-less procedural skinned mesh fails Rukhanka/BRG registration (`BatchMeshID not present`) and the WHOLE rig disappears.
- Persist rebased meshes as `Rebased_*.asset` (Clear+refill an existing asset = GUID-stable re-runs). - Persist rebased meshes as `Rebased_*.asset` (Clear+refill an existing asset = GUID-stable re-runs).
- Emissive pieces: `ProjectM/EmissiveGloamSkinned` (hand-written HLSL + Rukhanka `ComputeDeformedVertex`; the DOTS-instanced `_DeformedMeshIndex` block must be declared BEFORE the include, and the property must ALSO be in the Properties block for the baker's `HasProperty` validation). BRG-only: invisible in plain classic scenes, correct in the baked ECS world. - Emissive pieces: `ProjectM/EmissiveGloamSkinned` (hand-written HLSL + Rukhanka `ComputeDeformedVertex`; the DOTS-instanced `_DeformedMeshIndex` block must be declared BEFORE the include, and the property must ALSO be in the Properties block for the baker's `HasProperty` validation). BRG-only: invisible in plain classic scenes, correct in the baked ECS world.
## 14. AI-generated hero shape → conform to Synty (proven 07-24, Bathynaut Mark-V helmet)
When scripted `bmesh` primitives can't nail an **iconic organic hard-surface shape** (a classic diving helmet, an ornate boss horn) — repeated hand attempts read as "golf-ball / egg / blocky robot" — **generate the base shape with Hyper3D Rodin** (`generate_hyper3d_model_via_text` → poll → `import_generated_asset`), then **conform it to the LANTERN/Synty style**. Generation is a valid *modeling* path (like a kitbash); the output is raw clay, NOT a finished asset — it arrives photoreal, high-poly, meter-scale, single-textured, and **clashes hard** dropped next to the flat-shaded low-poly body. The conform pass is the real work:
**A. Watertight decimate — clean topology FIRST, then a GENTLE collapse.** A raw generated mesh has duplicate verts + loose geometry; an aggressive collapse (ratio ~0.10) on it **tears holes and shatters facets** (the operator will see it). Order matters:
```python
import bmesh
bm=bmesh.new(); bm.from_mesh(h.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0006) # weld
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
loose=[v for v in bm.verts if not v.link_faces]
if loose: bmesh.ops.delete(bm, geom=loose, context='VERTS')
bm.to_mesh(h.data); bm.free()
d=h.modifiers.new("Dec","DECIMATE"); d.decimate_type='COLLAPSE'; d.ratio=0.26 # ~1k tris for a hero prop
dg=bpy.context.evaluated_depsgraph_get(); h.data=bpy.data.meshes.new_from_object(h.evaluated_get(dg)); h.modifiers.clear()
# HOLE DETECTOR: boundary edges (edges touching <2 faces). MUST be 0 = watertight.
bm=bmesh.new(); bm.from_mesh(h.data); holes=sum(1 for e in bm.edges if len(e.link_faces)<2); bm.free()
```
23k→1.1k with 0 boundary edges reads as chunky Synty facets. If `holes>0`, the collapse was too aggressive for the cleanup — raise the ratio (gentler). Report tris vs budget.
**B. Kill the photoreal texture — flat palette color is the biggest tell.** The single baked albedo/PBR map (painted weathering, smooth metal highlights) is what makes it look "AI dropped on low-poly." **Discard the imported material** and re-paint the mesh with the scene's **OWN shared palette materials** (assign by face region → guaranteed cohesion, literally the same material asset). Set **flat/faceted shading** (`for p in mesh.polygons: p.use_smooth=False`) so each plane reads as a facet. Region-assign by world-space geometry on the *decimated* mesh (fewer faces = cleaner colour blocks):
```python
h.data.materials.clear()
for m in (brass, gunmetal, glass): h.data.materials.append(m) # reuse the suit/scene's flat mats
mw=h.matrix_world; bm=bmesh.new(); bm.from_mesh(h.data); bm.faces.ensure_lookup_table(); bm.verts.ensure_lookup_table()
zs=[(mw@v.co).z for v in bm.verts]; zmin,zmax=min(zs),max(zs); zh=zmax-zmin
port=mathutils.Vector((0,-0.15,1.68)) # feature centre in WORLD space
for f in bm.faces:
c=mw@f.calc_center_median(); n=(mw.to_3x3()@f.normal).normalized(); vz=[(mw@v.co).z for v in f.verts]
s=0 # brass body (dominant)
if min(vz)>zmax-zh*0.085 or max(vz)<zmin+zh*0.11: s=1 # top knob / bottom collar = accent (all-verts test = clean band)
d=(c-port).length
if n.y<-0.25 and d<0.10: s=2 if d<0.072 else 1 # front-facing near feature: inner disc glass, ring bezel
f.material_index=s; f.smooth=False
bm.to_mesh(h.data); bm.free()
```
Tie any emissive to the **ontology** (the helmet porthole = teal glass, faint glow ~0.5 → reads as the same bioluminescent palette as the flora/enemy-eyes, not a competing warm light — the shoulder lamp carries "our light"). Use `min/max(vert-z)` (all verts in/out) for clean bands, not face-centre thresholds (torn edges on a dense mesh).
**C. Scale gotcha — generated meshes are METER-scale; a cm-scale rig shrinks a skinned attachment ~100×.** Rodin exports ~12 m meshes; the Synty suit rig is cm-scale (0.01 armature). Skinning the generated piece to a bone applies the armature's 0.01 → it imports at **~5 mm (invisible)** with a wrong bindpose. For a rigid accessory (helmet, pack) **do NOT skin it** — drop the armature modifier + vgroups, `parent=None`, and set a plain `scale` so it sits at the feature's world size (a 1.7-unit generated mesh × 0.25 = ~0.42 m = head-sized). It rides the bone rigidly in-engine (Unity-side: parent the GO to the `Head` bone, §13 covers the skinned path when you DO want deform).
**D. Judge cohesion in-engine, at game scale, next to the existing Synty assets.** A generated mesh that looks fine solo can still clash beside the low-poly body — the only true test is a Play capture with the character next to its neighbours (the suit, an enemy) at the real game framing (§9). The Blender render confirms the mesh is clean; the *style match* is an in-engine, next-to-siblings call.
Recipe verified on the Mark-V: 4 hand-model attempts failed → Rodin generate → clean-decimate 23k→1.1k (0 holes) → drop the copper photoreal map → flat `M_Diver_Brass`/`HelmetMetal` + a teal `M_Diver_Porthole` + flat-shade → reads as one cohesive Synty diver (operator-approved). GLB 1.7 MB → 736 KB (texture dropped).