Docs: LANTERN art-pipeline recipes (Workstream A0/A1) + currency re-mean spec

- Art pipeline recipes: the material foundation (palette atlas + 3 master shaders
  Lit-Palette/Emissive-Gloam/Skinned-Palette), statics recipe, skinned recipe (proven
  Rukhanka HUMANOID path), the 3 rig families (Drifter/Crawler/Strider), CreatureKit (A2)
  framing. Built on DR-022/023 + the proven Blender recipe; every carry-over gotcha flagged.
- Currency re-mean: Ore/Aether/Biomass/Charge -> Flame/Lumen/Gloam/Loudness/Salvage/Chum/
  Tallow with byte-id assignments; flags the fresh-saves-vs-migrate fork (Charge byte reuse).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 23:49:20 -07:00
parent f53553581b
commit 73afab6ba5
2 changed files with 213 additions and 0 deletions
@@ -0,0 +1,139 @@
---
title: LANTERN — Art Pipeline Recipes (Workstream A0 material + A1 statics/skinned)
date: 2026-07-13
tags: [roadmap, lantern, art, blender, pipeline, workstream-a, recipes]
status: draft-recipe
permalink: gamevault/06-roadmap/lantern-art-pipeline-recipes
---
# LANTERN — Art Pipeline Recipes
> The **operating manual** the [[Art_Direction_Lantern|A0 constitution]] hands off to. Workstream A of
> [[Roadmap_Lantern_Slice]]: the material decision made concrete (A0) + the statics/skinned recipes (A1) + the
> three rig families + the CreatureKit framing (A2). Built on the project's **proven** animation pipeline
> ([[DR-022_Animation_Pipeline_Rukhanka_Synty]] / [[DR-023_Enemy_Animation_MonsterMash]] + the machine-local
> Blender recipe). The product isn't assets — it's a **repeatable factory** where creature N+1 is a sitting.
>
> **Checklist-grade by design** — this project runs on distilled gotchas; the art pipeline gets the same
> treatment. Every ⚠ is a carry-over hazard already paid for in blood.
## 0. The material foundation (A0 keystone — no per-asset texturing, ever)
**One palette atlas + vertex colours under three master shaders. Every asset shares ~3 materials.**
### The palette atlas
- A single small texture (e.g. 256×256) of flat colour **bands** — the whole game's palette. UVs are *placed*
onto a band (island = a quad snapped to a colour cell), **never unwrapped/painted** (Imphenzia method).
- Encode the ontology as bands: warm gold/amber (light/ours), cold corpse-green/blue (gloam), lure-red (bait),
white-hot (conversion), plus neutral greys for hard-surface.
-**Import the atlas LINEAR, not sRGB** — the ground bundle learned this: an sRGB-imported detail/colour map
used in a multiply darkens ~2.3×. Palette atlas = colour source → sRGB is usually right for a base-colour
map, but any map used as a MULTIPLY/detail must be LINEAR. Verify the actual look in Play, not the importer.
- Generatable procedurally (deterministic, no artist eyes needed for v1): a script paints N bands into a PNG.
### The three master shaders (author in ShaderGraph, multi-target `UniversalTarget`)
1. **`Lit-Palette`** — statics. Samples the atlas by UV; vertex-colour tint; flat/near-flat lit. Cheap, batches.
2. **`Emissive-Gloam`** — the palette rule as a shader. Parameterized **hue · flicker-cadence · intensity**
emissive over the atlas base. One shader is the whole "true vs false light" language: steady = true, flicker
= false (the load-bearing readability channel). Every glow in the game is this shader.
3. **`Skinned-Palette`** — deformation-aware (Rukhanka-skinned creatures/suits). This is the role the current
`AnimatedLitShader` fills — a multi-target ShaderGraph with `UniversalTarget` + the deformation nodes so
Entities-Graphics GPU skinning works. ⚠ Stock URP/Lit renders a skinned mesh **unskinned static** + a "does
not support skinning" warning — the deformation-aware graph is mandatory for anything Rukhanka drives.
-**Author all three Hybrid-Per-Instance** if we want per-instance gloam-tint variants (the free variant
axis): ShaderGraph `overrideHLSLDeclaration:true` + `hlslDeclarationOverride:2` on `_BaseColor`. A stock
Unity-Per-Material graph makes an `URPMaterialPropertyBaseColor` override **silently no-op**
([[material-property-body-flash-rukhanka]]) — the DOTS-authored `AnimatedLitShader` honours it; a stock Synty
prop graph does not.
- ⚠ Keep the three deformation/palette graphs in `_Project/Shaders/` (GUID-preserved), not in any imported
sample tree (importing Rukhanka "Animation Samples" drags in 26 subscenes + world-running sample systems +
a TMP conflict — the archive documents the extraction).
## 1. Statics recipe (target: minutes per prop)
1. **Model** in Blender to budget (props ≤ ~800 tris); place UVs onto atlas bands (no unwrap).
2. **Export** FBX (skinned-capable) or glb via glTFast (statics). ⚠ **Apply Scalings = FBX Units Scale**;
realize any geometry-node instances before export (geo-nodes don't survive the exporter).
3. **Collider policy:** cosmetic decor → **strip colliders**, no `GhostAuthoring` (classic-URP colliders are
inert to the DOTS PhysicsWorld anyway). Gameplay-blocking geometry → an `Environment`-layer collider baked
into the gameplay **subscene** (EG renders only baked entities; static collider, no Rigidbody →
deterministic, no replication). ⚠ A `MeshCollider` bakes ONLY if the mesh has Read/Write enabled — else an
`InvalidOperationException` per bake and NO baked shape (the classic view lies; only a baked-`CollisionWorld`
probe catches it). Prefer `ColliderFitTools` (audit/apply) to fit convex hulls to the visual meshes.
4. **Assign `Lit-Palette`** (or `Emissive-Gloam` for glow props); one material, atlas-driven.
5. **Bake** into the subscene; verify in the baked CollisionWorld + a Play screenshot (⚠ a dark render masks
material bugs — check shader property *values*, `GetPropertyType`-guarded).
## 2. Skinned recipe (creatures + suit-frames)
The proven Rukhanka path — **HUMANOID rig pipeline** (muscle clips retarget across rigs; the old "Generic" note
was wrong):
1. **Rig** in Blender on a shared family armature (see §3). The rig is **Humanoid**.
2. **Per-action clips** — one FBX per action. ⚠ **Key All Bones + Force Start/End keys + FBX Units Scale**;
**root motion baked into the pose** (CC owns the transform → root motion OFF on import). ⚠ Base any overlay/
partial anim on a **full-body pose** — a clip keying only a bone-subset collapses the un-keyed bones (mesh
sinks) ([[rukhanka-partial-clip-collapse]]).
3. **Import** `CreateFromThisModel` — ⚠ **`CopyFromOther` FAILS** on Blender's extra `Armature` node.
Optimize Game Objects **OFF**.
4. **Muscle retarget** the clips onto the player + creature rigs (humanoid muscle space).
5. **Rig bakes on the SAME entity holding the gameplay components** the drive job reads — `Animator` +
`RigDefinitionAuthoring` on the **root**, SMRs flattened under it. ⚠ **Skeleton-root = walk up from a bone
to the model's direct child**, NOT `SkinnedMeshRenderer.rootBone` (the bounds root — e.g. a head SMR's is
`Spine_03`, which destroys the lower skeleton).
6. **Material = `Skinned-Palette`** (deformation-aware, atlas-driven). ⚠ First Rukhanka bake is **~60 s, main-
thread-synchronous** (editor freezes — not a hang; blob caches after).
7. **Prefab** via `EnemyRigTools` (GUID-preserving `CopyAsset` + marker authoring — ⚠ `DeleteAsset+CopyAsset`
orphans subscene refs). Enemies = ownerless interpolated ghosts driven client-side by `EnemyAnimationDriveSystem`
(remote-path velocity from `LocalTransform` delta). ⚠ The **server runs Rukhanka unless stripped**
`ServerStripAnimationSystem` disables every `Rukhanka.Runtime` system server-side (matched by assembly name).
## 3. The three rig families (the amortization decision — clips per FAMILY, not per creature)
Every slice creature maps onto one shared armature so **clips are made once per family**:
| Family | Body plan | Slice members | Clip budget (45) |
|---|---|---|---|
| **Drifter** | spine-chain swimmer | Wisp-Choir motes, lure-fish (later: serpents/rays) | swim-idle · dart · attack · death-sink |
| **Crawler** | legged/scuttling floor unit | Grindylow (later: kappa-kin, Tenders) | skitter · leap · attack · death |
| **Strider** | biped-ish | Drowner, Wrecker, **and the suit-frames** | walk · windup · attack · death |
Per-creature **attack accents only** where identity demands (a Wrecker's cast, a boss phase). Suit-frames: A1
pass = **kitbash on the existing humanoid (Strider) rig** (helmet/tank/plate swaps over the proven player rig);
the custom Wick silhouette sculpt is an A3 upgrade, not a Phase-1 blocker.
## 4. Procedural assists (geometry nodes + blendermcp scripting)
- **Geometry nodes** for glow-gardens / coral / tube-worm / wreck families — parametric families, not hand-
modeled instances. ⚠ Realize instances before export.
- **blendermcp scripts** for the repetitive steps (scripted posing → per-action FBX export; batch import/
material-assign). The Blender MCP is live this machine; the proven combat-clip recipe is scripted end-to-end.
## 5. CreatureKit (A2 — the factory tool; spec, built in Phase 2)
**One editor tool or hard checklist that takes a Blender export → a play-ready enemy.** The measure: adding
enemy **N+1 touches zero undocumented steps.** It must yield, from one export:
- the prefab (rig + SMRs on root, `Skinned-Palette` material),
- ghost/network authoring (ownerless interpolated `GhostAuthoringComponent` + `LinkedEntityGroup`),
- `Environment`/hit colliders,
- a tuning-table row (Health/speed/windup/attack-range — bytes, not enums, for Burst),
- spawn-table + bestiary hooks (the kind-indexed director roster — ⚠ directors index the prefab buffer by
**Kind**, not round-robin; a stale round-robin pool silently mis-maps — [[enemy-director-kind-indexed-roster]]).
- **Variant policy (cheapest content first):** palette-shift / gloam-tint / scale / part-swap of an existing
mesh BEFORE any new mesh. One Drowner mesh → 3 visual tiers via the `Emissive-Gloam` params (which are *also*
gameplay information). **VFX-first identity:** emissive + a dynamic light + silhouette + animation carry a
creature in the dark; mesh detail is the last 20%.
## Throughput rules (solo sanity)
Timebox per class — a prop is a morning, a creature is days-not-weeks, the boss is the exception. The done-bar
is *"reads at gameplay zoom, under murk, in motion"* — never render-portfolio quality. When stuck, ship the
placeholder and log the upgrade. ⚠ For skinned-mesh visual bugs, **use the operator's eyes**`WorldRenderBounds`/
bone reads return the bind AABB, not the deformed pose ([[rukhanka-partial-clip-collapse]]).
## Related
- [[Art_Direction_Lantern]] — the A0 constitution (this is its operating manual)
- [[Roadmap_Lantern_Slice]] — Workstream A gates (A0→A1→A2→A3)
- [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_Animation_MonsterMash]] — the proven pipeline
- [[Synty_Asset_Inventory]] — placeholder quarry during the transition
@@ -0,0 +1,74 @@
---
title: LANTERN — Currency Re-Mean (Ore/Aether/Biomass/Charge → the light economy)
date: 2026-07-13
tags: [roadmap, lantern, economy, currency, save-schema]
status: draft-spec
permalink: gamevault/06-roadmap/lantern-currency-remean
---
# LANTERN — Currency Re-Mean
> Re-means the current economy to the LANTERN currency set ([[Identity_Lantern]] currency table). Feeds the
> economy rework + the save schema. **Most of this is a value-relabel** — the ledger (`StorageEntry`, ItemId-
> keyed) + personal bag (`InventorySlot`) + swept harvest plumbing stay; the **byte ids re-mean and the meters
> change**. Grounded in the resource ground-truth: 4 `ResourceId` bytes today (Ore/Aether/Biomass/Charge),
> `StorageEntry{ushort ItemId; int Count}` on the global untagged ledger, personal `InventorySlot`, unified
> swept `ResourceHarvestSystem`.
## The seven LANTERN currencies — and what each maps to
| LANTERN | What it is | Current analog | Representation |
|---|---|---|---|
| **Flame** | your life — the loaned fire in your chest | `Health` + respawn | **NOT a ledger resource** — it's `Health`/`RespawnState`. The death "split" (Phase 5) rides this. |
| **Lumen** | spendable light — abilities, fuel, the banked economy | **Aether** (universal charge) + the win-meter feed | Ledger byte; also **banked** at lanterns → the **Lamp meter** (the `GoalProgress`-equivalent campaign readout). The primary economy. |
| **Gloam** | the wild radiance grafts drink — the **mutation feed** | (new; kill/vent/bloom-derived) | A run/descent-scoped meter that fills toward the **paced mutation thresholds** (Phase 4). Server-derived from kills/vents; likely NOT a stored ledger row (a per-descent counter). |
| **Loudness** | how much attention your light draws | the threat/aggro pressure (`WaveSystem` escalation) | **Derived meter, not stored** — party lamp-output sum → spawn/aggro pressure; per-Wick pip on the HUD. Rides the global untagged ghost ([GhostField]). |
| **Salvage** | build materials | **Ore** | Ledger byte. Feeds building v1 (lanterns, small buildables). |
| **Chum** | garden feedstock (automation input) | **Biomass** (reserved automation input) | Ledger byte. **Slice: likely dormant** (automation is assignment-based, v1.0 — [[Lantern_Strip_Mothball_Inventory]]). Keep the byte reserved. |
| **Tallow** | rendered fixative — **fixes a mutation** | (new; the scarce fix material) | The scarcity gate of the mutation economy (1 fix slot/Spark). A ledger byte OR a slot count. Phase 4. |
## Proposed `ResourceId` byte assignment
Bytes, never enums (Burst rule). Re-mean the existing values where the meaning survives; free the mothballed
one; append the new ones:
| Byte | Was | Becomes | Notes |
|---|---|---|---|
| 0 | Ore | **Salvage** | build material (value survives, relabel) |
| 1 | Aether | **Lumen** | universal spend + banked light (value survives, relabel) |
| 2 | Biomass | **Chum** | garden feedstock, slice-dormant (relabel, reserved) |
| 3 | Charge | *(freed)***Tallow** | turret-ammo Charge is mothballed (no turrets); reuse the byte for Tallow, or leave 3 dead and add Tallow=4 |
| 4 | — | **(Gloam)** | if stored; more likely a per-descent server counter, not a ledger row |
- **Flame** and **Loudness** are **not** ledger bytes (Health; a derived meter). Do not give them `ResourceId`s.
- ⚠ **Re-meaning byte 3 (Charge→Tallow) changes a persisted VALUE's meaning.** The save persists `Ledger[]` as
`{ItemId, Count}` rows — an old save's `ItemId=3` (Charge) would load as Tallow. **Recommendation:** since
the pivot is a hard direction change and old co-op-Hades saves are not carried into LANTERN, **start LANTERN
saves fresh** (a new save version epoch) rather than migrate — cleaner than a value-remap migration. Confirm
with the operator (parked). If saves must migrate, add a `SaveData.Version`-gated remap in `SaveService.Load`.
## What changes vs. stays
- **STAYS:** `StorageEntry` global ledger (ItemId-keyed), `InventorySlot` personal bag (`[GhostField]`
`OwnerSendType.All`), the unified swept `ResourceHarvestSystem` (nodes + clutter, immortal-sink guard,
`math.max(1,(int)yield)`), the `G`-deposit RPC, cargo-into-bag routing.
- **CHANGES:** harvest sources re-theme (Ore/Aether/Biomass nodes → **Lumen nodes + organ drops**); the
**carried-vs-banked cargo** distinction is new (unbanked Lumen/organs strand with the suit on death — the
Phase-5 split); the win-meter (`GoalProgress`) becomes the **Lamp meter** fed by banked Lumen; **Loudness**
replaces the incursion/attention timer as the spawn-pressure driver.
- **NEW meters on the global untagged ghost:** Lamp (campaign), party Loudness sum. **New per-descent server
counters:** Gloam (mutation feed), Tallow slots.
## Sequencing
This is **not** a standalone slice — it lands *inside* the phases that need each currency: Lumen + Salvage +
harvest re-theme with Phase 2 (harvest) / Phase 3 (banked light, Lamp meter); Gloam + Tallow with Phase 4
(mutations); Loudness with Phase 2 (brighter-is-louder). This doc is the **single source of the mapping** so
those phases don't each re-decide the byte ids.
## Related
- [[Identity_Lantern]] — the currency table + the death split + brighter-is-louder
- [[Lantern_Strip_Mothball_Inventory]] — ledger/harvest/automation salvage verdicts
- [[Lantern_Operator_Questions]] — the fresh-saves-vs-migrate fork (item 6)
- [[Roadmap_Lantern_Slice]] — the phases each currency lands in