diff --git a/docs/design/character-visuals-spec.md b/docs/design/character-visuals-spec.md new file mode 100644 index 000000000..9f3190bcb --- /dev/null +++ b/docs/design/character-visuals-spec.md @@ -0,0 +1,292 @@ +--- +title: "Character Visuals Specification" +description: "Complete spec for character rendering: layer stack, color regions, body/face/hair types, direction system, outline, LOD" +type: spec +status: active +sprint: 28 +ticket: 684 +created: 2026-03-17 +--- + +# Character Visuals Specification + +**Produced by:** Sprint 28 Character Visuals Workshop (ticket #684) +**Date:** 2026-03-17 +**Art direction reference:** Invisible Inc — clean, adult, high contrast. Not a candy store. +**Downstream:** `docs/design/compositor-api-spec.md`, tickets #686–#692 (visual team), #693 (compositor) + +--- + +## 1. Camera and Display Context + +All art must be authored and tested at the confirmed gameplay camera angle: + +- **Camera tilt:** 30° from vertical (60° from horizontal) — orthographic +- **Map rotation:** 45° (diamond grid) +- **Projection:** Orthographic — no perspective foreshortening +- **Reference games:** Hades, Divinity: Original Sin (not Diablo — 45° is NOT the default) + +Characters are rendered as live 3D models in the Godot scene (D-149). The camera is a real `Camera3D` — perspective is not faked in art. Artwork is authored as 3D assets, not pre-rendered sprites. + +**Art direction notes at this angle:** +- Front faces are significantly visible at 30° — face and clothing detail matters more than at 45° +- The top of characters' heads is less prominent — hair silhouette reads from the side, not top-down +- Wall depth and character shadow are both visible — spatial grounding is strong +- Characters must read clearly at tile scale (~32–64px display height) + +--- + +## 2. Rendering Architecture Overview + +Characters use a `CharacterCompositor` (Node3D subtree). See `docs/design/compositor-api-spec.md` for the full API. + +- **Not sprites:** No pre-rendered sprite sheets. All composition is runtime in Godot. +- **Outline:** Inverted hull method — uniform dark gray (#1a1a1a) for all characters. Not a relationship indicator (D-154). See §10. +- **Relationship data:** Lives on minimap, name bubbles, and insert/perception mode overlay (D-033 amended). Not on character body in normal gameplay. +- **Player character:** Visually identical to NPCs — same models, same variety (D-153). + +--- + +## 3. Direction System + +**Server (authoritative):** 8 facing directions — N, NE, E, SE, S, SW, W, NW. + +**Client Sprint 28:** 4 visual groups. The 3D model rotates to the true 8-direction angle; only the visual mesh group snaps to 4: + +| Visual Group | Covers Server Facings | Mirror | +|---|---|---| +| North | N, NW | — | +| East | NE, E | — (source mesh) | +| South | SE, S | — | +| West | SW, W | Mirror of East | + +The exact mapping of server facings to visual groups depends on camera orientation relative to the diamond grid — Tyre to confirm in compositor implementation. West is a horizontal mirror of East; no additional art required. + +**Character editor:** Four cardinal direction buttons. Button order: **S → E → N → W**. Default on open: **South** (face-forward at 30° camera — maximum cosmetic utility, highest clothing/face visibility). No free-spin (D-155). The four buttons correspond directly to the four visual groups. + +**Critical distinction — 8 facings vs 4 visual groups:** +- **8 server-side facings** = perception system input. Used by fog-of-war, vision cone direction, and all simulation logic. The server always tracks the true 8-direction facing. +- **4 visual groups** = character rendering output. What the player actually sees. Characters snap to the nearest cardinal visual group. Diagonal facings do not exist as visible character states — a character facing NE renders as the East visual group. +- Smooth rotation interpolation between directions is applied at the model root level (ModelRoot rotation), so the body physically points the correct way even while the visual mesh group is a snap. + +**Post-Sprint 28:** Diagonal mesh variants (NE, NW, SE, SW as separate visual groups) can be added without protocol changes — the 8 server facings are already tracked. + +--- + +## 4. Layer Stack + +Layers are rendered bottom-to-top. Each layer is a separate mesh in the `CharacterCompositor` subtree. + +| # | Layer | Notes | +|---|---|---| +| 1 | Body base | Body type variant (slim / average / stocky). Skin tone mesh regions. | +| 2 | Leg clothing | Trousers, shorts, skirt. Occludes body base from waist down. | +| 3 | Footwear | Shoes, boots. Occludes leg clothing at ankle/foot. | +| 4 | Torso clothing (back) | Rear of jacket/shirt. Behind arms. | +| 5 | Torso clothing (front) | Front of jacket/shirt. Over arms. | +| 6 | Accessories | Belt, bag, holster, jewelry. Multiple items possible. | +| 7 | Head / face | Face mesh with skin regions. Neck connects to body base. | +| 8 | Hair (back) | Hair behind the head/shoulders. | +| 9 | Hair (front) | Hair in front of face / over forehead. | +| — | Outline | Inverted hull pass (not a layer — shader effect on all meshes). | +| 10 | Scar overlays | Placed on exposed skin regions. Additive blend on skin. | +| 11 | Tattoo overlays | Placed on exposed skin regions. Multiply blend on skin. | +| 12 | Injury overlays | Applied over clothing (tears, stains) and face (bruises, cuts). | +| 13 | Expression overlay | Replaces/modifies face mesh for expression state. | + +**Occlusion rule:** Each layer should include an occlusion mesh (invisible, write-only depth) matching the body shape, so underlying layers do not bleed through clothing at edges. + +--- + +## 5. Color Mesh Regions + +Each layer exposes named UV-mapped color regions that can be overridden by the compositor. Regions that are not overridden use the layer's authored default color. + +### Body base +| Region | Description | +|---|---| +| `skin_primary` | Face, hands, neck, visible forearms | +| `skin_secondary` | Inner arm, sole of hand — subtle variation (may auto-derive from `skin_primary`) | + +### Hair +| Region | Description | +|---|---| +| `hair_primary` | Main hair color | +| `hair_highlight` | Secondary sheen / highlight (may auto-derive from `hair_primary` as lighter tint) | + +### Clothing (all clothing items) +| Region | Description | +|---|---| +| `cloth_primary` | Dominant fabric — the main color the player picks | +| `cloth_secondary` | Trim, seams, buttons, collar, cuffs | +| `cloth_accent` | Optional third zone — logo patch, lining, contrast panel. May be absent on simpler items. | + +### Accessories +| Region | Description | +|---|---| +| `accessory_primary` | Main material color | +| `accessory_secondary` | Optional secondary (e.g. stitching on a bag, gem on jewelry) | + +**Design note:** Not all clothing recolors uniformly. A jacket's body is `cloth_primary`; its piping is `cloth_secondary`. Per-item region definition is the artist's responsibility. Invisible Inc reference: colored bodysuits with trim lines — crisp and readable at small scale. + +--- + +## 6. Body Types (Sprint 28: 2–3) + +Sprint 28 ships with 2–3 body type variants. Minimum viable: slim + average. Stocky as stretch if time allows. + +| Type | Visual description | Notes | +|---|---|---| +| Slim | Narrow shoulders, slight frame, elongated limbs | | +| Average | Medium proportions, balanced silhouette | | +| Stocky | Broad shoulders, thicker torso and limbs | Stretch goal, Sprint 28 | + +**Constraints:** +- All body types share the same layer interface (`skin_primary`, `skin_secondary` region names) +- Clothing mesh layers must fit all body types without visible clipping or floating +- Clothing artists author per–body-type mesh variants OR the compositor scales the clothing mesh to the body type (implementation TBD — Tyre to specify in compositor-api-spec) +- Silhouette must differ meaningfully at 32px display height — not just subtle (otherwise pointless at tile scale) + +**Combination validation:** Sprint 28 goal is to confirm that switching body types with any clothing combination produces no visual artifacts. This is the primary QA target. + +--- + +## 7. Face Types + +Face is a compositable region on the `Head/Face` layer. + +**Range:** Broad variety. Asymmetric features valued — these are adults, not game-industry default faces. Invisible Inc reference for readability at small scale. + +**Mesh regions on face:** +- `skin_primary` — face, forehead, cheeks, chin +- `skin_lips` — lips (may auto-derive from `skin_primary`) + +**Compositable elements within the face region (each a separate overlay or mesh swap):** +- Eye shape (mesh swap — left/right eye as separate swappable meshes or UV regions) +- Nose shape (mesh swap) +- Mouth/lip shape (mesh swap) +- Eyebrow shape (mesh swap or UV overlay) + +**Expression states (minimum Sprint 28):** + +| State | Description | Trigger | +|---|---|---| +| `neutral` | Default resting face | No active emotional state | +| `alert` | Eyes slightly widened, posture tense | Danger or person-of-interest nearby | +| `stressed` | Furrowed brow, tight mouth | High stress value from simulation | +| `distressed` | Open distress, visible tension | Extreme stress or injury | + +Expression overlay replaces or modifies the base face mesh. The expression does not affect the body posture in Sprint 28. + +--- + +## 8. Hair Style Spec + +Hair silhouette must read legibly at tile scale. Overly detailed styles will read as noise at small size — err on the side of clean, strong silhouettes. + +**Minimum Sprint 28 styles:** +- Short A (close-cropped) +- Short B (textured/spiky) +- Medium A (smooth, parted) +- Medium B (wavy/voluminous) +- Long A (flowing, shoulder-length or longer) +- Bald / shaved + +**Color regions:** `hair_primary`, `hair_highlight` (see §5). + +**Layer split:** Every style must have a `Hair (back)` and `Hair (front)` mesh — these straddle the head/face layer so face is visible between them at certain angles. + +--- + +## 9. Scar and Tattoo Overlays + +Overlays are placed on **exposed skin regions only** (face, hands, forearms — not on covered areas). + +**Placement zones:** +- Face zone +- Left arm / hand zone +- Right arm / hand zone +- Torso zone (only visible when wearing open/sleeveless clothing) + +**Blending:** +- **Tattoos:** Multiply blend on `skin_primary` region — color mixes with skin tone +- **Scars:** Additive blend on `skin_primary` — lighter, raised-texture appearance + +**Optional:** Not all characters have scars or tattoos. Zero overlays is valid and common. + +--- + +## 10. Injury Overlays + +Injury state represents physical damage visible on the character's appearance. + +**States (minimum Sprint 28):** + +| State | Clothing effect | Skin effect | +|---|---|---| +| `undamaged` | None | None | +| `light_injury` | Minor tear/dirt on torso clothing | Minor bruising on face/exposed skin | +| `heavy_injury` | Visible tears, blood staining | Heavy bruising, cuts visible | + +Injury overlays are applied on top of clothing (not inside it) and on top of exposed skin. Injury tint auto-derives from the layer's base color (darker, desaturated). + +--- + +## 11. Outline Specification + +- **Method:** Inverted hull — GPU vertex extrusion, back-face-only render pass (D-150) +- **Color:** `#1e1e24` (very dark blue-grey) for all characters, always — not pure black +- **Width:** 1px at gameplay zoom (consistent regardless of zoom level — use shader-based fixed screen-space width) +- **Semantic content:** None — the outline does not encode relationship, status, or any game state +- **LOD behavior:** At Tier 2 (billboard impostor), the outline is baked into the impostor sprite. The inverted hull is disabled at Tier 2 — no separate draw call. + +--- + +## 12. LOD Strategy + +Three tiers, degraded based on GPU frame budget (not distance, not fixed count) (D-152): + +| Tier | Label | Description | +|---|---|---| +| 0 | Full | All compositor layers active. Full 3D model. Inverted hull outline. | +| 1 | Simplified mesh | Reduced-poly model. Clothing layers merged into combined mesh. Outline active. | +| 2 | Billboard impostor | Flat sprite impostor with outline baked in. One draw call. | + +**Degradation order:** Characters furthest from the player's character degrade first. Nearest characters stay at Tier 0 longest. Threshold is a frame budget check — when the GPU is under pressure, demote the outermost tier-eligible characters. + +**LOD trigger:** Proactive on **projected character count**, not reactive on frame drop. The LOD manager anticipates load based on how many characters will be in the scene this frame and pre-demotes before GPU pressure occurs. Reactive triggering (demoting after a frame drops) produces visible hitches. Proactive triggering is invisible. + +**Billboard LOD requirement (Tier 2 preservation):** At billboard tier, each impostor sprite **must visually encode two signals per character:** +1. **Body size tier** (slim / average / stocky) — silhouette must differ meaningfully at billboard scale +2. **Dominant clothing color** — the primary `cloth_primary` of the most visible clothing item must be readable + +These signals must not be optimized away during impostor baking. The impostor is not a generic character silhouette — it is a per-character snapshot preserving identity. + +**Pause behavior:** When the game is paused, the render budget is fully freed. All characters restore to Tier 0. The pause button is a deliberate affordance — player can inspect any character at full detail. + +**Chaos feel:** In a very large crowd (400+ characters), peripheral degradation is intentional and cognitively appropriate — real crowds have visual noise at the edges. The player naturally focuses center-screen where Tier 0 characters are. + +--- + +## 13. Art Direction Reference Summary + +| Principle | Notes | +|---|---| +| Reference: Invisible Inc | Clean, sleek, adult. High contrast, strong silhouettes. Not Rimworld-chunky, not anime. | +| Scale readability | Every design decision must be validated at 32–64px. Detail that doesn't read at tile scale should be removed. | +| Uniform outlines | Dark gray for everyone. No auras, no glows, no player distinction. | +| Per-item color variety | Clothing colors are per-item, not per-faction, not per-team. Life-sim, not RTS. | +| Adult faces | Asymmetric features, variety across ages and types. Not default RPG faces. | + +--- + +## 14. Open Questions / Flags + +| Item | Status | Notes | +|---|---|---| +| Exact N/E/S/W → server facing mapping | Open | Tyre to confirm in compositor-api-spec based on camera orientation | +| Stocky body type (Sprint 28) | Conditional | Stretch goal — drop if time pressure | +| Clothing mesh per body type vs. scale | Open | Tyre to specify — separate mesh variants or compositor-level scaling | +| Torso-zone tattoo visibility | Open | Depends on clothing occlusion rules — artist + Tyre to confirm | +| Expression overlay = mesh swap or morph target | Open | Tyre to decide in compositor-api-spec | +| D-146 interaction with 3D rendering | Flag | D-146 says "tile-scale sprite with heavy zoom" for editor preview; D-149 says live 3D rendering. The editor preview may show the live 3D model at zoom scale — compatible if interpreted as "tile-scale representation" not "2D sprite". Do not reopen D-146 without explicit direction from Jeroen. | diff --git a/docs/design/compositor-api-spec.md b/docs/design/compositor-api-spec.md new file mode 100644 index 000000000..7e35ead44 --- /dev/null +++ b/docs/design/compositor-api-spec.md @@ -0,0 +1,298 @@ +--- +title: "Character Compositor API Specification" +description: "Godot-side compositor data model and node architecture for runtime character compositing" +type: spec +status: active +sprint: 28 +ticket: 684 +created: 2026-03-17 +--- + +# Character Compositor API Specification + +**Produced by:** Sprint 28 Character Visuals Workshop (ticket #684) +**Date:** 2026-03-17 +**Implements:** D-149 (3D live rendering), D-150 (inverted hull outline), D-151 (direction count), D-152 (LOD strategy) +**Upstream:** `docs/design/character-visuals-spec.md` +**Downstream:** Ticket #693 (compositor implementation, client team) + +--- + +## 1. Overview + +The `CharacterCompositor` is a `Node3D` subtree that replaces the current single `Sprite2D` in `EntityRenderer`. It manages all visual layers for a character: body, clothing, hair, face, accessories, and overlays. It exposes a clean API that `EntityRenderer` calls when appearance or state changes. + +The compositor is **purely presentational** — it never queries the server or simulation. All data flows from `EntityRenderer` downward. + +--- + +## 2. Direction Enum + +```gdscript +enum CharacterFacing { + NORTH, # Server facings: N, NW + EAST, # Server facings: NE, E (source mesh — not mirrored) + SOUTH, # Server facings: SE, S + WEST, # Server facings: SW, W (mirror of EAST) +} +``` + +**Perception vs. rendering — critical distinction:** +- `CharacterFacing` (4 values) is the **rendering output** — what the compositor acts on. +- The server tracks 8 facing directions for the **perception system** (fog-of-war, vision cone). The server's 8-direction value is never passed directly to the compositor. +- `EntityRenderer` is responsible for mapping the server's 8-direction value to one of the 4 `CharacterFacing` values and passing that to `compositor.set_facing()`. +- Diagonal facings (NE, NW, SE, SW) do not exist as compositor states. They are perception inputs only. +- `ModelRoot` rotation uses the true 8-direction angle for body lean; `CharacterFacing` controls mesh/material group selection. + +**Server-to-client mapping** (exact snap logic — Tyre to confirm based on camera/grid orientation): + +| Server Facing | Client Visual Group | Notes | +|---|---|---| +| N | NORTH | | +| NW | NORTH | | +| NE | EAST | | +| E | EAST | | +| SE | SOUTH | | +| S | SOUTH | | +| SW | WEST | Mirror of EAST | +| W | WEST | Mirror of EAST | + +The 3D model root rotates to the true server-facing angle (all 8). The visual group snap controls which mesh variant is loaded, but the root rotation gives subtle body lean within a group. + +--- + +## 3. Color Override Data + +```gdscript +class_name CharacterColors +extends Resource + +## Skin +@export var skin_primary: Color = Color("#c8a882") +@export var skin_secondary: Color = Color.TRANSPARENT # TRANSPARENT = auto-derive from skin_primary + +## Hair +@export var hair_primary: Color = Color("#3a2a1a") +@export var hair_highlight: Color = Color.TRANSPARENT # TRANSPARENT = auto-derive from hair_primary + +## Clothing slot overrides — keyed by item_id +## Each item carries its own cloth_primary/secondary/accent +## This dict maps item_id -> ClothingColors +@export var clothing: Dictionary = {} # item_id (int) -> ClothingColors +``` + +```gdscript +class_name ClothingColors +extends Resource + +@export var cloth_primary: Color = Color.WHITE +@export var cloth_secondary: Color = Color.TRANSPARENT # TRANSPARENT = use authored default +@export var cloth_accent: Color = Color.TRANSPARENT # TRANSPARENT = use authored default +``` + +--- + +## 4. Appearance Data + +```gdscript +class_name CharacterAppearance +extends Resource + +## Body +@export var body_type: int = 1 # 0=slim, 1=average, 2=stocky +@export var face_id: int = 0 # index into face mesh library +@export var hair_id: int = 0 # index into hair mesh library + +## Clothing — item IDs (0 = no item for that slot) +@export var clothing_torso_id: int = 0 +@export var clothing_legs_id: int = 0 +@export var footwear_id: int = 0 +@export var accessory_ids: Array[int] = [] # multiple accessories supported + +## Overlays — IDs (empty = none) +@export var scar_ids: Array[int] = [] +@export var tattoo_ids: Array[int] = [] + +## State +@export var injury_state: int = 0 # 0=undamaged, 1=light, 2=heavy +@export var expression_state: int = 0 # 0=neutral, 1=alert, 2=stressed, 3=distressed + +## Colors +@export var colors: CharacterColors = CharacterColors.new() +``` + +--- + +## 5. Node Architecture + +``` +CharacterCompositor (Node3D) — root; receives API calls +├── ModelRoot (Node3D) — rotated to true server-facing angle +│ ├── BodyMesh (MeshInstance3D) — body_type variant; skin regions +│ ├── LegClothing (MeshInstance3D) — clothing_legs_id mesh; cloth regions +│ ├── Footwear (MeshInstance3D) — footwear_id mesh; cloth regions +│ ├── TorsoClothingBack (MeshInstance3D) — back half of torso clothing +│ ├── TorsoClothingFront (MeshInstance3D) — front half of torso clothing +│ ├── Accessories (Node3D) — child MeshInstance3D per accessory_id +│ ├── HeadFace (MeshInstance3D) — face_id variant; skin + expression regions +│ ├── HairBack (MeshInstance3D) — hair_id variant (back mesh) +│ ├── HairFront (MeshInstance3D) — hair_id variant (front mesh) +│ └── Overlays (Node3D) +│ ├── ScarOverlay (MeshInstance3D) — scar_ids; additive blend +│ ├── TattooOverlay (MeshInstance3D) — tattoo_ids; multiply blend +│ ├── InjuryOverlay (MeshInstance3D) — injury_state; over clothing + skin +│ └── ExpressionOverlay (MeshInstance3D) — expression_state; over face +└── LOD (Node3D) — manages tier transitions + └── BillboardSprite (Sprite3D) — tier 2 impostor (baked outline included) +``` + +**Outline:** Inverted hull is a material property on `ModelRoot` and all child meshes — not a separate node. Disabled at LOD Tier 2 (billboard replaces the entire ModelRoot). + +--- + +## 6. Public API + +```gdscript +class_name CharacterCompositor +extends Node3D + +## Apply a full appearance update. +## Called on: character creation, equip/unequip, editor preview. +func apply_appearance(appearance: CharacterAppearance) -> void: + pass + +## Update facing direction. Called every time server reports a facing change. +func set_facing(facing: CharacterFacing) -> void: + pass + +## Set LOD tier. Called by the global LOD manager based on frame budget. +## 0 = full, 1 = simplified mesh, 2 = billboard impostor +func set_lod_tier(tier: int) -> void: + pass + +## Return a snapshot of the current appearance (used by character editor). +func get_appearance() -> CharacterAppearance: + return CharacterAppearance.new() + +## Update a single color region without a full appearance rebuild. +## More efficient than apply_appearance() for color picker preview. +func set_color(region: StringName, color: Color) -> void: + pass +``` + +--- + +## 7. EntityRenderer Integration + +Current state: `EntityRenderer` (`client/scripts/rendering/entity_renderer.gd`) uses a single `Sprite2D` per entity. + +**Migration plan:** +1. Add `CharacterCompositor` as a packed scene resource +2. In `EntityRenderer._ready()`: if entity is a character type, instantiate `CharacterCompositor` and add as child; hide `Sprite2D` +3. Wire `EntityRenderer`'s existing facing-update path to call `compositor.set_facing()` +4. Wire `EntityRenderer`'s appearance-update path to call `compositor.apply_appearance()` +5. Register `EntityRenderer` with the global LOD manager to receive `set_lod_tier()` calls + +Non-character entities (items, furniture, tiles) continue to use `Sprite2D` and are unaffected. + +--- + +## 8. LOD Manager + +A singleton (`CharacterLODManager`) monitors the GPU frame budget and calls `set_lod_tier()` on registered `CharacterCompositor` instances. + +**Interface:** +```gdscript +class_name CharacterLODManager +extends Node + +## Register a compositor for LOD management. +func register(compositor: CharacterCompositor, entity_id: int) -> void: + pass + +## Unregister when entity leaves scene. +func unregister(entity_id: int) -> void: + pass + +## Called by compositor to report its distance from player (updated each frame by EntityRenderer). +func update_distance(entity_id: int, distance: float) -> void: + pass +``` + +**LOD algorithm — proactive, not reactive:** +The LOD manager operates on **projected character count**, not frame-drop detection. Triggering on frame drop produces visible hitches. Proactive demotion is invisible to the player. + +``` +Each frame: + projected_full_count = count of registered compositors within full-detail radius + if projected_full_count > TIER_0_BUDGET: + demote furthest tier-0 compositors until within budget + if still over TIER_1_BUDGET: + demote furthest tier-1 compositors to tier-2 until within budget + When count drops below budget * HYSTERESIS_FACTOR: + promote nearest tier-2 → tier-1, tier-1 → tier-0 + When paused (get_tree().paused == true): + promote all compositors to tier 0 +``` + +`TIER_0_BUDGET` and `TIER_1_BUDGET` are tunable constants. Initial values TBD by profiling. + +**Billboard impostor requirements (Tier 2):** +The impostor sprite is not a generic silhouette — it is a per-character snapshot. The bake process **must** preserve: +1. **Body size tier** — slim / average / stocky silhouette must be distinguishable at billboard scale +2. **Dominant clothing color** — `cloth_primary` of the most visible clothing item must read clearly + +These are non-negotiable fidelity requirements. Implementation must not optimize them away. + +Implementation detail: use an indexed priority queue keyed by distance. Full implementation is Tyre's responsibility in ticket #693. + +--- + +## 9. Character Editor Integration + +The character creation/editor screen (`character_select.gd` and related) can use `CharacterCompositor` directly for the preview pane. + +**Preview pane setup:** +- Instantiate `CharacterCompositor` in a `SubViewport` +- Apply `CharacterAppearance` from the current editor state on every change +- Default facing on open: **`CharacterFacing.SOUTH`** — face-forward at 30° camera, maximum cosmetic utility +- Button order: S → E → N → W +- No LOD management in the editor — always tier 0 + +```gdscript +# In editor preview pane script: +func _on_direction_button_pressed(facing: CharacterFacing) -> void: + preview_compositor.set_facing(facing) + +func _on_appearance_changed() -> void: + preview_compositor.apply_appearance(build_appearance_from_editor_state()) +``` + +**D-146 compatibility note:** D-146 specifies "tile-scale sprite with heavy zoom." Under D-149 (3D live rendering), the editor preview shows the live 3D compositor rendered in a SubViewport at tile scale, then displayed zoomed. This is compatible with D-146's intent (showing the actual in-game appearance, not a separate portrait render). D-146 is not superseded. + +--- + +## 10. Asset Pipeline Notes + +**For Araminta (visual team):** +- Each body type variant is a separate `.blend`/`.glb` export for `BodyMesh` +- Clothing items export as separate `.glb` per body type (e.g. `jacket_01_slim.glb`, `jacket_01_average.glb`) +- Hair styles export as two meshes per style: `hair_short_a_back.glb`, `hair_short_a_front.glb` +- Color regions are defined via UV2 channel: compositor reads UV2 to apply `ShaderMaterial` color overrides per region +- Naming convention for regions: UV2 islands correspond to named shader uniforms (`uniform sampler2D skin_primary_mask`, etc.) + +**For tickets #686–#692 (visual team):** +- Reference this spec and the `CharacterAppearance` data class for mesh naming and region conventions +- All meshes must validate at the 30° tilt, 45° map rotation camera (D-148) — no art review at top-down angle + +--- + +## 11. Open Questions + +| Item | Status | Owner | +|---|---|---| +| Exact server-facing → visual group mapping | Open | Tyre — confirm against camera/grid orientation in compositor implementation | +| Expression: mesh swap vs. morph target | Open | Tyre — cost/quality tradeoff | +| Clothing mesh per body type vs. compositor-level scale | Open | Tyre — separate `.glb` per body type is simpler; scaling risks clipping | +| UV2 region masking vs. vertex color for color regions | Open | Tyre — UV2 is cleaner; vertex color is cheaper; both work | +| Impostor bake process for LOD Tier 2 | Open | Tyre — static pre-bake vs. runtime bake | diff --git a/docs/discussions/README.md b/docs/discussions/README.md index 7c477e4f9..043215623 100644 --- a/docs/discussions/README.md +++ b/docs/discussions/README.md @@ -33,3 +33,4 @@ Historical discussion rounds from the Settled Reach game design process. | 18 | v0.1 Gap Analysis Workshop | D-030, D-031, D-032, D-033, D-034, D-035, D-036, D-037, D-038, D-039, D-040 | [round-18](round-18-v01-gap-analysis-workshop.md) | | 19 | Knowledge Graph & Information Boundaries Workshop | D-041, Q-016 resolved, Q-019 partially resolved, Q-024, Q-025, Q-026 | [workshop brief](../workshops/knowledge-graph-information-boundaries/workshop-brief.md), [synthesis](../workshops/knowledge-graph-information-boundaries/round2-synthesis.md) | | 20 | Station District Layout Design (Workshop #153) | D-093, D-094, D-095; Q-040–Q-044 resolved/partially resolved | [round-20](round-20-station-district-layout.md) | +| 22 | Sprint 28 Character Visuals Workshop | D-148–D-157; D-019 superseded; D-033 amended | [round-22](round-22-sprint28-character-visuals.md) | diff --git a/docs/discussions/round-22-sprint28-character-visuals.md b/docs/discussions/round-22-sprint28-character-visuals.md new file mode 100644 index 000000000..fce289d23 --- /dev/null +++ b/docs/discussions/round-22-sprint28-character-visuals.md @@ -0,0 +1,168 @@ +--- +title: "Round 22: Sprint 28 Character Visuals Workshop" +description: "Three-round structured workshop producing character visual architecture specs; D-148–D-157; supersedes D-019" +type: discussion +status: complete +round: 22 +created: 2026-03-17 +--- + +# ROUND 22: Sprint 28 Character Visuals Workshop + +**Date:** 2026-03-17 +**Type:** Two active rounds + spec writing +**Participants:** Araminta (art direction), Gestalt (systems), Tyre (technical), Ozzie (player experience), Qatux (documenter), SI (project manager) +**Facilitator:** Team Leader (Jeroen) +**Documenter:** Qatux + +--- + +## 1. Workshop Purpose + +Move beyond placeholder art to a final-candidate character visual architecture. Starting point: a live Godot 4 spike prototype with 20 CSG placeholder characters on a 14×14 checkerboard grid in diamond orientation. + +Spike configuration at workshop start: +- 20 CSG characters (cylinder body/legs, sphere head/hair) +- 14×14 flat quad grid, rotated 45° (diamond layout) +- Four wall boxes along north edge +- Orthographic camera with T-key cycle: top-down → 45° → 30° +- Characters visible as standing figures at isometric scale + +Workshop deliverables per briefing: +1. `docs/design/character-visuals-spec.md` +2. `docs/design/compositor-api-spec.md` +3. `docs/workshops/character-visuals/workshop-outcomes.md` +4. D-records to `decisions/` for all architectural decisions + +--- + +## 2. Round 1 — Agent Proposals (Inventory and Constraints) + +Round 1 agents assessed their domain areas independently. Full proposals were collected, then reviewed by Jeroen. + +### JEROEN'S ROUND 1 CORRECTIONS + +**Overruled proposals:** +1. **Relationship-colored outlines.** Uniform dark gray/black for all characters, always. Relationship info → minimap, name bubbles, and insert/perception mode. Outline is not a semantic channel. +2. **Player character visually distinct from NPCs.** No protagonist glow, no saturation boost, no special outline. Same models, same variety. Distinction comes from gear only. +3. **Free-spin rotation in character editor.** Cardinal direction buttons only (N/E/S/W). Free-spin exposes angles that don't exist in gameplay. +4. **Game start as camera pull-back from editor.** Fade from black + alarm clock + waking in bed. Editor is its own separate context. +5. **Faction colors as visual system.** Faction affiliation is not in the compositor. Clothing color choices emerge from character narrative, not faction assignment. + +**Confirmed from Round 1:** +- 2–3 body types for Sprint 28 (combination-switching validation as primary QA goal) +- Mugshot navigator = character editor (no knowledge-tier complexity) +- Wear level as continuous shader parameter — liked for flavor, not primary Sprint 28 deliverable +- Art direction: **Invisible Inc** as primary reference — clean, sleek, adult, uniform dark outlines +- Per-item clothing color variety — not uniform recolor across an item + +**New major decision introduced at end of Round 1:** +The spike prototype showed camera tilt options. The 45° isometric angle (Diablo-style) was initially discussed, then Jeroen announced the 30° low-angle (Hades-style) may be better. This was locked at start of Round 2. + +--- + +## 3. Round 2 — Specification Proposals + +### JEROEN — Round 2 Opening + +Camera decision locked: **30° tilt with 45° map rotation** is the confirmed default gameplay camera. Not 45° isometric. More character front visibility. More wall depth. Closer to Hades than Diablo. This supersedes D-019 ("Top-down confirmed as primary camera") and its 2026-02-12 amendment. + +Two open questions posed to the team: + +**Q1: 4 or 8 directions?** +With 45° map rotation, characters face along diamond axes. What are the trade-offs? + +**Q2: Large crowd performance.** +What happens at 400+ characters simultaneously? Current budget assumes 80 NPCs. + +--- + +### QATUX (Documenter) + +Flagged pending D-records from Round 1 overrules: 5 confirmed decisions requiring formal D-record IDs before Round 3. + +Flagged inconsistency: the Sprint 28 briefing specified "8-direction rotation" throughout. Q1 was still open. Nothing in the spec structure should be written until Q1 closed. + +Q1 framing: At 30° tilt + 45° map rotation, the four diagonal movements (NE/NW/SE/SW) are the most common player-facing directions. 4-direction mirroring at those angles risks asymmetry in the most common case — this is a significant risk factor for 4-direction-only approaches. + +Q2 framing: 80-NPC budget stated in sprint context; 400+ as named scenario. No existing open question on crowd rendering at scale — flagged for Q-NNN registration if needed. + +--- + +### TYRE — Q1 Resolution Proposal + +Hybrid approach: **8 server-side facings + 4 client visual groups**. +- Server tracks 8 directions — future-proof, no protocol migration needed +- Client renders 4 visual groups for Sprint 28 — keeps asset authoring cost manageable +- 3D model rotates to true 8-direction angle (subtle lean within a group) +- East and West share mirrored meshes — net unique mesh count per clothing item: 2 (N + E) + +### JEROEN — Q2 Resolution + +Performance-driven LOD (not distance threshold, not fixed count): +- Normal gameplay: full detail, all characters +- Under GPU pressure: progressive degradation from player outward (Tier 0 → 1 → 2) +- Tier 2 = billboard impostor with baked-in outline +- Paused: render budget freed, all characters restore to full detail +- Chaos effect: peripheral degradation is cognitively appropriate — matches real crowd experience + +Outline method confirmed as part of Q2 resolution: **inverted hull** for normal rendering. LOD handles outline naturally (baked into impostor at Tier 2). + +--- + +### JEROEN — Round 2 Final Lock + +**Q1: 8 server / 4 client visual groups (Sprint 28). Tyre's hybrid. Closed.** +**Q2: Performance-driven LOD. Three tiers. Pause = full detail. Closed.** +**All open questions closed. Moving to spec writing.** + +--- + +## 4. Round 3 — Spec Writing (Qatux) + +D-record IDs claimed before writing: +- D-148: architecture — 30° camera with 45° map rotation (supersedes D-019) +- D-149: architecture — 3D characters rendered live +- D-150: architecture — inverted hull outline +- D-151: architecture — 8 server / 4 client direction count +- D-152: architecture — performance-driven LOD +- D-153: scope — player not visually distinct from NPCs +- D-154: scope — outline not a relationship indicator +- D-155: scope — cardinal rotation only in editor +- D-156: scope — faction colors are narrative +- D-157: scope — game start = alarm clock fade from black + +Specs written: `character-visuals-spec.md`, `compositor-api-spec.md`, `workshop-outcomes.md`. +Decisions filed: D-148–D-152 to `decisions/architecture.md`, D-153–D-157 to `decisions/scope.md`. +D-019 marked `[SUPERSEDED by D-148]`. D-033 amended with insert/perception-mode clarification. + +--- + +## 5. Key Decisions Produced + +| D-ID | Domain | Decision | +|---|---|---| +| D-148 | architecture | 30° low-angle camera + 45° map rotation (supersedes D-019) | +| D-149 | architecture | 3D characters rendered live (replaces sprite approach) | +| D-150 | architecture | Inverted hull outline, uniform dark, baked into billboard LOD | +| D-151 | architecture | 8 server facings, 4 visual groups client Sprint 28 | +| D-152 | architecture | Performance-driven LOD (3 tiers, pause = full detail) | +| D-153 | scope | Player character not visually distinct from NPCs | +| D-154 | scope | Outline not a relationship indicator (amends D-033) | +| D-155 | scope | Character editor cardinal rotation only | +| D-156 | scope | Faction colors = narrative emergence, not visual compositor | +| D-157 | scope | Game start = alarm clock fade from black, not pull-back | + +--- + +## 6. Downstream Impact Summary + +- **D-019 superseded** — all references to "top-down camera" in briefings/specs should be updated +- **D-033 amended** — entity relationship color is insert/perception-mode only, not on normal outlines +- **Old placeholder-art-spec** (`docs/design/placeholder-art-spec-v01.md`) is superseded for character rendering +- **Briefings to update:** `docs/briefings/araminta.md`, `docs/briefings/tyre.md` (camera angle, rendering approach) +- **Tickets unblocked:** #685 (wireframe), #698 (3D character editor), #699 (body type meshes), #700 (compositor). Tickets #686–#692 (sprite-based) cancelled. + +--- + +*Archived by Qatux — 2026-03-17* diff --git a/docs/workshops/character-visuals/workshop-outcomes.md b/docs/workshops/character-visuals/workshop-outcomes.md new file mode 100644 index 000000000..eec424880 --- /dev/null +++ b/docs/workshops/character-visuals/workshop-outcomes.md @@ -0,0 +1,130 @@ +--- +title: "Sprint 28 Character Visuals Workshop — Outcomes" +description: "Decisions, resolved questions, and deliverables from the Sprint 28 character visuals planning workshop (ticket #684)" +type: workshop-outcomes +status: complete +sprint: 28 +ticket: 684 +created: 2026-03-17 +--- + +# Sprint 28 Character Visuals Workshop — Outcomes + +**Ticket:** #684 +**Date:** 2026-03-17 +**Participants:** Araminta (art direction), Gestalt (systems), Tyre (technical), Ozzie (player experience), Nigel (replayability), Qatux (documenter), SI (project manager) +**Facilitator:** Jeroen (project lead) +**Rounds:** 2 (Round 3 was spec sign-off; specs written by Qatux) + +--- + +## Summary + +This workshop resolved the foundational visual architecture for The Settled Reach characters. The major outcome is a shift from the previous approach (2D pre-rendered sprites at a shallow art-convention tilt) to **live 3D compositing** at a **30° real camera tilt** with **45° diamond-grid map rotation**. This change was driven by a functioning prototype spike that demonstrated 3D placeholder characters (CSG shapes) reading legibly as people at isometric scale. + +The workshop produced 10 new D-records (D-148–D-157), two spec documents, and this outcomes file. + +--- + +## Decisions Made + +### Architecture decisions (`decisions/architecture.md`) + +| ID | Title | Summary | +|---|---|---| +| D-148 | 30° low-angle camera with 45° map rotation | Supersedes D-019. Default gameplay camera. Real Camera3D, not faked in art. | +| D-149 | 3D characters rendered live in scene | Not pre-rendered sprites. CharacterCompositor replaces Sprite2D in EntityRenderer. | +| D-150 | Character outline — inverted hull method | Uniform dark gray (#1a1a1a). GPU vertex extrusion. Baked into billboard at LOD Tier 2. | +| D-151 | Direction count — 8 server / 4 client visual groups | Server tracks 8 facings. Client renders 4 visual groups (Sprint 28). West mirrors East. | +| D-152 | Character LOD — performance-driven budget | Frame budget threshold, not distance or count. Three tiers. Pause = full detail everywhere. | + +### Scope decisions (`decisions/scope.md`) + +| ID | Title | Summary | +|---|---|---| +| D-153 | Player not visually distinct from NPCs | Same models, same outlines. Distinction comes from gear only. | +| D-154 | Outline not a relationship indicator | Uniform dark always. D-033 color palette valid in insert/perception mode only. | +| D-155 | Character editor: cardinal rotation only | N/E/S/W buttons. No free-spin. Matches the 4 visual groups. | +| D-156 | Faction colors = narrative, not visual | No faction color slot in compositor. Clothing choices emerge from character, not faction. | +| D-157 | Game start = alarm clock / fade from black | Not a camera pull-back from editor. Editor is a separate context. | + +### Amended decisions + +| ID | Change | +|---|---| +| D-019 | Marked [SUPERSEDED by D-148] | +| D-033 | Amended: entity relationship color valid in insert/perception mode only; not on character outline in normal gameplay | + +--- + +## Round 1 Proposals — Overruled + +The following Round 1 proposals were overruled by Jeroen and are recorded here for the archive. They should not be revisited without explicit project lead direction: + +| Proposal | Overruled by | +|---|---| +| Relationship color on outlines | D-154 — outlines are uniform dark always | +| Player character visually distinct (protagonist glow/tint) | D-153 — player = NPC visually | +| Free-spin in character editor | D-155 — cardinal buttons only | +| Camera pull-back from editor into game world | D-157 — alarm clock / fade from black | +| Faction colors as visual system | D-156 — narrative emergence only | + +--- + +## Open Questions Resolved + +| Q | Question | Resolution | +|---|---|---| +| Q1 | 4 or 8 directions? | D-151: 8 server, 4 visual groups (Sprint 28). Hybrid approach. | +| Q2 | Large crowd performance? | D-152: Performance-driven LOD. Progressive degradation outward from player. Pause = full detail. | + +--- + +## Remaining Open Questions / Flags + +These were identified during the workshop but not resolved. They are implementation concerns for the client and visual teams: + +| Item | Owner | Notes | +|---|---|---| +| Exact server-facing → visual group snap mapping | Tyre | Depends on camera/grid orientation. Confirm in compositor implementation. | +| Expression overlay: mesh swap vs. morph target | Tyre | Cost/quality tradeoff. Decide in #693. | +| Clothing mesh per body type vs. compositor scale | Tyre | Separate `.glb` per body type is simpler; scaling risks clipping. Decide in #693. | +| Stocky body type in Sprint 28 | Araminta | Stretch goal — drop if time pressure. | +| D-146 + D-149 interaction | Qatux flag | D-146 says "tile-scale sprite" for editor preview; D-149 says live 3D rendering. Compatible if editor uses SubViewport with the live compositor. Do not reopen D-146. | + +--- + +## Deliverables Produced + +| File | Description | +|---|---| +| `docs/design/character-visuals-spec.md` | Complete spec: layer stack, color regions, body/face/hair types, direction system, outline, LOD | +| `docs/design/compositor-api-spec.md` | Godot compositor data model, node architecture, public API, EntityRenderer integration | +| `docs/workshops/character-visuals/workshop-outcomes.md` | This file | +| `docs/discussions/round-22-sprint28-character-visuals.md` | Workshop discussion archive | +| `decisions/architecture.md` | D-148–D-152 added | +| `decisions/scope.md` | D-153–D-157 added | +| `decisions/perception.md` | D-019 superseded, D-033 amended | + +--- + +## Downstream Impact + +### Tickets unblocked by this workshop +- #699 (visual team): 3D body type meshes — now have full spec +- #698 (client team): 3D character editor — now has confirmed editor spec +- #700 (client team): CharacterCompositor implementation — now has full API spec +- #685 (wireframe): character creation screen — can now proceed with confirmed direction system and editor spec + +Note: tickets #686–#692 (sprite-based character assets) were cancelled by SI following the shift to live 3D rendering (D-149). + +### Tickets that may need re-scoping (flag for SI) +- Any tickets referencing "sprite" for character rendering should be reviewed against D-149 +- The old `placeholder-art-spec-v01.md` is superseded by `character-visuals-spec.md` for character rendering decisions +- D-019 was referenced in several briefings — Qatux to update affected briefings (Araminta, Tyre) + +### Camera angle change (D-019 → D-148) impact +- All in-progress art assets should be validated at 30° tilt (not top-down, not 45°) +- Vision cone and LOS math: unaffected (server-side, 2D, unrotated space) +- Tile grid and world coordinates: unaffected (45° rotation is visual only) +- Briefing files: `docs/briefings/araminta.md`, `docs/briefings/tyre.md` need camera angle update